50 km/jam
Jarak:0.15km
Mesin: Hidup
Total:15.25 km", // info popup * 0.15, // distance from previous point * "Hidup" // engine status * ] * ], * "total_distance": "155.69 km", * "total_points": 1180, * "start_coordinates": [-6.2088, 106.8456], * "processing_time": "0.25 seconds", * "date_range": { * "start": "2026-01-19 00:00:00", * "end": "2026-01-19 23:59:59" * } * } * * RESPONSE ERROR (400/401/500): * ============================= * { * "error": "string" * } * * BUSINESS LOGIC: * =============== * - Hanya ambil data dengan lat != 0 (valid GPS coordinates) * - Filter berdasarkan oilLevel = 0 untuk data yang valid * - Status < 260000 = Engine OFF (Mati) * - Status > 260000 = Engine ON (Hidup) * - Jarak dihitung menggunakan formula Haversine * - Data diurutkan berdasarkan time ASC untuk playback sequence * - GMT timezone adjustment sesuai user setting * * BIG DATA OPTIMIZATIONS: * ======================= * - Selective column retrieval * - Time-based filtering dengan index hints * - Memory-efficient processing * - GROUP BY untuk mengurangi duplicate coordinates * - 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 precision untuk floating point calculations ini_set('precision', 14); // 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("Playback API accessed by user: " . $user); // error_log("Query parameters: " . json_encode($_GET)); try { // Validasi required parameter TID if (!isset($_GET['tid'])) { http_response_code(400); echo json_encode([ 'error' => 'Missing required parameter: tid', 'required_format' => [ 'tid' => 'tracking_id (required)', 'awal' => 'YYYY-MM-DD HH:MM:SS (optional, default: today 00:00:00)', 'akhir' => 'YYYY-MM-DD HH:MM:SS (optional, default: now)', 'license' => 'license_plate (optional)' ] ]); exit; } // Get parameters dengan default values $tid = trim($_GET['tid']); $license = isset($_GET['license']) ? trim($_GET['license']) : ''; // Set default date range jika tidak ada parameter if (!isset($_GET['awal']) && !isset($_GET['akhir'])) { $start = date('Y-m-d') . " 00:00:00"; $end = date('Y-m-d H:i:s'); // Current time } else { $start = isset($_GET['awal']) ? trim($_GET['awal']) : date('Y-m-d') . " 00:00:00"; $end = isset($_GET['akhir']) ? trim($_GET['akhir']) : date('Y-m-d H:i:s'); } // Debug: Log parameters // error_log("Playback API - Params: tid={$tid}, start={$start}, end={$end}, license={$license}"); // Validasi format datetime jika ada parameter if (isset($_GET['awal'])) { $startTime_check = DateTime::createFromFormat('Y-m-d H:i:s', $start); if (!$startTime_check) { http_response_code(400); echo json_encode([ 'error' => 'Invalid start datetime format. Use: YYYY-MM-DD HH:MM:SS', 'received' => $start ]); exit; } } if (isset($_GET['akhir'])) { $endTime_check = DateTime::createFromFormat('Y-m-d H:i:s', $end); if (!$endTime_check) { http_response_code(400); echo json_encode([ 'error' => 'Invalid end datetime format. Use: YYYY-MM-DD HH:MM:SS', 'received' => $end ]); exit; } } // Validasi range waktu (maksimal 7 hari untuk big data protection) $startDateTime = new DateTime($start); $endDateTime = new DateTime($end); $diffDays = $endDateTime->diff($startDateTime)->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; } // Get GMT setting $gmt = isset($_SESSION['GMT']) ? $_SESSION['GMT'] : 7; // Load license data jika belum ada dan license tidak di-provide if (!$license && (!isset($_SESSION['license']) || empty($_SESSION['license']))) { if (!isset($_SESSION['username'])) { $_SESSION['username'] = $user; } SessionHelper::dbLicenseToSession(); } // Get license dari session jika tidak di-provide if (!$license && isset($_SESSION['license'])) { foreach ($_SESSION['license'] as $lic) { if ($lic[2] == $tid) { $license = $lic[1]; break; } } } // Set default license jika masih kosong if (!$license) { $license = 'Unknown Vehicle'; } // Debug: Log license resolution // error_log("Playback API - Resolved license: {$license} for TID: {$tid}"); // OPTIMIZED SQL QUERY untuk Big Data // Berdasarkan original code dengan optimizations $sql = "SELECT lat, lon, CONVERT(VARCHAR(19), DATEADD(HOUR, {$gmt}, [time]), 120) AS time, speed, status, oilLevel FROM tblocations WHERE tid = ? AND lat != 0 AND DATEADD(HOUR, {$gmt}, [time]) >= ? AND DATEADD(HOUR, {$gmt}, [time]) <= ? AND oilLevel = 0 GROUP BY lat, lon, [time], speed, status, oilLevel ORDER BY [time] ASC"; // Execute query $pdo = Database::getConnection(); $stmt = $pdo->prepare($sql); $stmt->execute([$tid, $start, $end]); // Fetch all data $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $totalRecords = count($rows); // Debug: Log data count // error_log("Playback API - Retrieved {$totalRecords} records for TID {$tid}"); // Initialize variables untuk processing $routeData = array(); $totalDistance = 0; $pointIndex = 0; // Process each record foreach ($rows as $record) { $pointIndex++; // Calculate distance dari titik sebelumnya if ($pointIndex == 1) { $distance = 0; // First point } else { $prevRecord = $rows[$pointIndex - 2]; // Previous record $distance = SessionHelper::calculateDistance( $record['lat'], $record['lon'], $prevRecord['lat'], $prevRecord['lon'] ); } // Determine engine status if ($record['status'] < 260000) { $engineStatus = 'Mati'; } else { $engineStatus = 'Hidup'; } // Format speed $speed = ($record['speed'] == 0) ? '0 km/Jam' : round($record['speed'], 0) . ' km/jam'; // Add to total distance $totalDistance += $distance; // Create info popup (sesuai format original) $formattedTime = date('d/m/y H:i:s', strtotime($record['time'])); $formattedDistance = number_format($distance, 2, ",", "."); $formattedTotalDistance = number_format($totalDistance, 2, ",", "."); $info = $formattedTime . "
" . $speed . "
" . "Jarak:" . $formattedDistance . "km
" . "Mesin: " . $engineStatus . "
" . "Total:" . $formattedTotalDistance . " km"; // Create route point (sesuai format original) // Format: [lat, lon, info, distance, status] $routePoint = array( (float)$record['lat'], (float)$record['lon'], $info, (float)round($distance, 2), $engineStatus ); $routeData[] = $routePoint; } // Get start coordinates untuk map initialization $startCoordinates = array(); if (!empty($routeData)) { $startCoordinates = array($routeData[0][0], $routeData[0][1]); } else { // Default coordinates (Indonesia center) $startCoordinates = array(-7, 110); } // Performance monitoring $endTime = microtime(true); $endMemory = memory_get_usage(true); $processingTime = round($endTime - $startTime, 2); $memoryUsed = round(($endMemory - $startMemory) / 1024 / 1024, 2); // Debug: Log processing statistics // error_log("Playback API - Processing completed:"); // error_log(" - Total records processed: " . $totalRecords); // error_log(" - Total route points: " . count($routeData)); // error_log(" - Total distance: " . number_format($totalDistance, 2) . " km"); // error_log(" - Processing time: " . $processingTime . " seconds"); // error_log(" - Memory used: " . $memoryUsed . " MB"); // Set response headers header('Content-Type: application/json'); // Prepare response $response = array( 'route_data' => $routeData, 'total_distance' => number_format($totalDistance, 2, ".", "") . " km", 'total_distance_numeric' => round($totalDistance, 2), 'total_points' => count($routeData), 'start_coordinates' => $startCoordinates, 'vehicle_info' => array( 'tid' => $tid, 'license' => $license ), 'date_range' => array( 'start' => $start, 'end' => $end, 'days' => $diffDays ), 'processing_info' => array( 'processing_time' => $processingTime . " seconds", 'memory_usage' => $memoryUsed . " MB", 'records_processed' => $totalRecords, 'gmt_offset' => $gmt ) ); // Return response echo json_encode($response); } catch (Exception $e) { // Log error dengan performance info $errorTime = microtime(true); $errorMemory = memory_get_usage(true); error_log("Playback 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 playback data: ' . $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(); }