260000 = Engine ON (end stop) * - Hanya stop dengan durasi >= interval yang dilaporkan * - Data diurutkan berdasarkan receiveTime ASC * - GMT timezone adjustment sesuai user setting * * BIG DATA OPTIMIZATIONS: * ======================= * - Selective column retrieval * - Time-based filtering dengan index hints * - Memory-efficient state machine processing * - Chunked processing untuk multiple TIDs * - Early termination untuk invalid ranges * * AUTHOR: Rodhi * DATE: 2026-01-19 * VERSION: 1.0 (Big Data Optimized) */ require_once __DIR__.'/../models/database.php'; require_once __DIR__.'/../helpers/session_helper.php'; require_once __DIR__.'/../auth/middleware.php'; if (!headers_sent()) { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Auth-Token'); header('Access-Control-Allow-Credentials: true'); header('Content-Type: application/json'); } // Set memory limit untuk big data processing ini_set('memory_limit', '512M'); // Require authentication $user = authRequired(); // Start session untuk GMT setting dan license data if (session_status() == PHP_SESSION_NONE) { session_start(); } // Start performance monitoring $startTime = microtime(true); $startMemory = memory_get_usage(true); // Debug: Log API access // error_log("Stop Report API accessed by user: " . $user); // error_log("Query parameters: " . json_encode($_GET)); try { // Validasi required parameters if (!isset($_GET['awal']) || !isset($_GET['akhir']) || !isset($_GET['tid'])) { http_response_code(400); echo json_encode([ 'error' => 'Missing required parameters: awal, akhir, tid', 'required_format' => [ 'awal' => 'YYYY-MM-DD HH:MM:SS', 'akhir' => 'YYYY-MM-DD HH:MM:SS', 'tid' => 'tracking_id or "all"', 'interval' => 'minimum_stop_minutes (optional, default: 5)' ] ]); exit; } // Sanitize input parameters $awal = trim($_GET['awal']); $akhir = trim($_GET['akhir']); $tid = trim($_GET['tid']); $interval = isset($_GET['interval']) ? (int)$_GET['interval'] : 5; $license = isset($_GET['license']) ? trim($_GET['license']) : ''; // Debug: Log sanitized parameters // error_log("Stop Report API - Params: awal={$awal}, akhir={$akhir}, tid={$tid}, interval={$interval}"); // Validasi format datetime $awalTime = DateTime::createFromFormat('Y-m-d H:i:s', $awal); $akhirTime = DateTime::createFromFormat('Y-m-d H:i:s', $akhir); if (!$awalTime || !$akhirTime) { http_response_code(400); echo json_encode([ 'error' => 'Invalid datetime format. Use: YYYY-MM-DD HH:MM:SS', 'received' => ['awal' => $awal, 'akhir' => $akhir] ]); exit; } // Validasi range waktu (maksimal 7 hari untuk big data protection) $diffDays = $akhirTime->diff($awalTime)->days; if ($diffDays > 7) { http_response_code(400); echo json_encode([ 'error' => 'Date range too large for big data processing. Maximum 7 days allowed.', 'requested_days' => $diffDays, 'suggestion' => 'Use smaller date ranges for better performance' ]); exit; } // Validasi interval (maksimal 1440 menit = 24 jam) if ($interval < 1 || $interval > 1440) { http_response_code(400); echo json_encode([ 'error' => 'Invalid interval. Must be between 1 and 1440 minutes.', 'received_interval' => $interval ]); exit; } // Get GMT setting $gmt = isset($_SESSION['GMT']) ? $_SESSION['GMT'] : 7; // Determine TIDs to process $tidsToProcess = array(); if (strtolower($tid) === 'all') { // Load license data jika belum ada if (!isset($_SESSION['license']) || empty($_SESSION['license'])) { if (!isset($_SESSION['username'])) { $_SESSION['username'] = $user; } SessionHelper::dbLicenseToSession(); } // Get all TIDs dari session user if (isset($_SESSION['license']) && !empty($_SESSION['license'])) { foreach ($_SESSION['license'] as $licenseData) { $tidsToProcess[] = [ 'tid' => $licenseData[2], // TID 'license' => $licenseData[1] // License plate ]; } } if (empty($tidsToProcess)) { http_response_code(403); echo json_encode([ 'error' => 'No vehicle licenses available for this user', 'user' => $user ]); exit; } } else { // Single TID $tidsToProcess[] = [ 'tid' => $tid, 'license' => $license ? $license : 'Unknown' ]; } // Debug: Log TIDs to process // error_log("Stop Report API - Processing " . count($tidsToProcess) . " TIDs"); // Initialize result array $allStopReports = array(); $totalProcessedRecords = 0; $totalStopsFound = 0; // Process each TID foreach ($tidsToProcess as $tidData) { $currentTid = $tidData['tid']; $currentLicense = $tidData['license']; // Debug: Log current TID processing // error_log("Stop Report API - Processing TID: {$currentTid}, License: {$currentLicense}"); // OPTIMIZED SQL QUERY untuk Big Data // Hanya ambil kolom yang diperlukan untuk memory efficiency $sql = "SELECT lat, lon, speed, course, CONVERT(VARCHAR(19), DATEADD(HOUR, {$gmt}, receiveTime), 120) AS receiveTime, status FROM tblocations WHERE DATEADD(HOUR, {$gmt}, receiveTime) BETWEEN ? AND ? AND tid = ? ORDER BY receiveTime ASC"; // Execute query $pdo = Database::getConnection(); $stmt = $pdo->prepare($sql); $stmt->execute([$awal, $akhir, $currentTid]); // Fetch all data untuk current TID $data = $stmt->fetchAll(PDO::FETCH_ASSOC); $total = count($data); $totalProcessedRecords += $total; // Debug: Log data count for current TID // error_log("Stop Report API - TID {$currentTid}: {$total} records"); // State machine variables untuk stop detection $pengingat = 0; // 0 = looking for stop start, 1 = looking for stop end $startTime = ''; $startLat = 0; $startLon = 0; // Process data dengan state machine approach for ($i = 0; $i <= ($total - 1); $i++) { $currentRecord = $data[$i]; // State 1: Looking for stop start (engine OFF) if ($pengingat == 0 && $currentRecord['status'] < 260000) { $startTime = $currentRecord['receiveTime']; $startLat = $currentRecord['lat']; $startLon = $currentRecord['lon']; $pengingat = 1; // Switch to looking for stop end // Debug: Log stop start // error_log("Stop Report API - Stop started at: {$startTime} for TID {$currentTid}"); } // State 2: Looking for stop end (engine ON) if ($pengingat == 1 && $currentRecord['status'] > 260000) { $endTime = $currentRecord['receiveTime']; // Calculate stop duration dalam menit $startTimestamp = strtotime($startTime); $endTimestamp = strtotime($endTime); $durationMinutes = ($endTimestamp - $startTimestamp) / 60; // Debug: Log stop end // error_log("Stop Report API - Stop ended at: {$endTime}, Duration: {$durationMinutes} minutes"); // Hanya tambahkan jika durasi >= interval minimum if ($durationMinutes >= $interval) { $coordinates = $startLat . ',' . $startLon; $formattedDuration = SessionHelper::formatDuration($durationMinutes); // Format: [license, coordinates, start_time, end_time, duration] $stopReport = array( $currentLicense, $coordinates, date('d/m/Y H:i:s', $startTimestamp), date('d/m/Y H:i:s', $endTimestamp), $formattedDuration ); $allStopReports[] = $stopReport; $totalStopsFound++; // Debug: Log valid stop found // error_log("Stop Report API - Valid stop found: {$durationMinutes} minutes for {$currentLicense}"); } $pengingat = 0; // Reset to looking for next stop start } } // Memory cleanup per TID unset($data); gc_collect_cycles(); } // Performance monitoring $endTime = microtime(true); $endMemory = memory_get_usage(true); $processingTime = round($endTime - $startTime, 2); $memoryUsed = round(($endMemory - $startMemory) / 1024 / 1024, 2); // Debug: Log final statistics // error_log("Stop Report API - Processing completed:"); // error_log(" - Total TIDs processed: " . count($tidsToProcess)); // error_log(" - Total records processed: " . $totalProcessedRecords); // error_log(" - Total stops found: " . $totalStopsFound); // error_log(" - Processing time: " . $processingTime . " seconds"); // error_log(" - Memory used: " . $memoryUsed . " MB"); // Set response headers header('Content-Type: application/json'); // Add metadata untuk debugging (uncomment jika diperlukan) // $response = [ // 'data' => $allStopReports, // 'metadata' => [ // 'total_tids_processed' => count($tidsToProcess), // 'total_records_processed' => $totalProcessedRecords, // 'total_stops_found' => $totalStopsFound, // 'processing_time' => $processingTime . " seconds", // 'memory_usage' => $memoryUsed . " MB", // 'date_range_days' => $diffDays, // 'minimum_interval' => $interval . " minutes" // ] // ]; // echo json_encode($response); // Return simple array format sesuai original code echo json_encode($allStopReports); } catch (Exception $e) { // Log error dengan performance info $errorTime = microtime(true); $errorMemory = memory_get_usage(true); error_log("Stop Report API Error: " . $e->getMessage()); error_log("Error occurred at: " . round($errorTime - $startTime, 2) . " seconds"); error_log("Memory at error: " . number_format($errorMemory / 1024 / 1024, 2) . " MB"); error_log("Error trace: " . $e->getTraceAsString()); http_response_code(500); echo json_encode([ 'error' => 'Failed to generate stop report: ' . $e->getMessage(), 'processing_time' => round($errorTime - $startTime, 2) . " seconds", 'memory_usage' => number_format(($errorMemory - $startMemory) / 1024 / 1024, 2) . " MB" // Debug version dengan detail error: // 'error_details' => [ // 'message' => $e->getMessage(), // 'file' => $e->getFile(), // 'line' => $e->getLine(), // 'trace' => $e->getTraceAsString() // ] ]); } finally { // Cleanup memory gc_collect_cycles(); }