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();
}