(function () { "use strict"; const body = document.querySelector("#trip-list [data-data-list-body]"); const loading = document.getElementById("trip-loading"); const error = document.getElementById("trip-error"); const summary = document.getElementById("trip-summary"); const timeline = document.getElementById("trip-timeline"); const scrubber = document.getElementById("trip-scrubber"); const playButton = document.getElementById("trip-play"); const replayTime = document.getElementById("trip-replay-time"); const replaySpeed = document.getElementById("trip-replay-speed"); const map = L.map("history-map", { minZoom: 8, maxZoom: 15 }).setView([48.505, -0.09], 12); const tiles = L.tileLayer("/static/vendor/tiles/{z}/{x}/{y}.png", { minZoom: 8, maxZoom: 15, attribution: "© MapTiler © OpenStreetMap contributors", }); let routeLayer = null; let vehicleMarker = null; let replayPoints = []; let replayTimer = null; let replayController = null; const recenterBtn = document.getElementById("btn-recenter-history"); function formatDuration(seconds) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); if (hours) { return `${hours}h ${minutes}m`; } return minutes ? `${minutes}m` : `${seconds}s`; } function formatDate(value) { return new Date(value.replace(" ", "T")).toLocaleString(); } function formatDistance(kilometers) { return `${kilometers.toFixed(kilometers < 1 ? 2 : 1)} km`; } function formatSpeed(kilometersPerHour) { return `${kilometersPerHour.toFixed(1)} km/h`; } function showError(message) { error.textContent = message; error.hidden = false; } function stopReplay() { if (replayTimer) { window.clearInterval(replayTimer); replayTimer = null; } playButton.innerHTML = ''; playButton.setAttribute("aria-label", "Play trip replay"); } function showReplayPoint(index) { const point = replayPoints[index]; if (!point) { return; } const coordinate = [point.latitude, point.longitude]; vehicleMarker.setLatLng(coordinate); scrubber.value = index; replayTime.textContent = formatDate(point.datetime); const source = point.speed_source === "obd" ? "OBD wheel" : "GPS"; replaySpeed.textContent = `${source} speed: ${formatSpeed(point.speed)}`; } function initializeTimeline(points) { stopReplay(); replayPoints = points; scrubber.max = Math.max(0, points.length - 1); scrubber.value = 0; timeline.hidden = points.length === 0; if (vehicleMarker) { vehicleMarker.remove(); } if (!points.length) { vehicleMarker = null; return; } vehicleMarker = L.circleMarker( [points[0].latitude, points[0].longitude], { radius: 8, color: "#fff", weight: 3, fillColor: "#e74a3b", fillOpacity: 1, } ).bindTooltip("Vehicle position").addTo(map); showReplayPoint(0); } async function replayTrip(tripId) { error.hidden = true; if (replayController) { replayController.abort(); } replayController = new AbortController(); const response = await fetch( `/api/trip/${tripId}`, { signal: replayController.signal } ); const result = await response.json(); if (!response.ok) { throw new Error(result.error || "Could not load trip route."); } if (routeLayer) { routeLayer.remove(); } if (!map.hasLayer(tiles)) { tiles.addTo(map); } const coordinates = result.points.map(function (point) { return [point.latitude, point.longitude]; }); routeLayer = L.featureGroup(); L.polyline(coordinates, { color: "#4e73df", weight: 5 }).addTo(routeLayer); if (coordinates.length) { L.marker(coordinates[0]).bindPopup("Trip start").addTo(routeLayer); L.marker(coordinates[coordinates.length - 1]).bindPopup("Trip end").addTo(routeLayer); } routeLayer.addTo(map); if (coordinates.length > 1) { map.fitBounds(routeLayer.getBounds(), { padding: [24, 24] }); } else if (coordinates.length === 1) { map.setView(coordinates[0], 15); } recenterBtn.hidden = false; recenterBtn.onclick = function () { if (coordinates.length > 1) { map.fitBounds(routeLayer.getBounds(), { padding: [24, 24] }); } else if (coordinates.length === 1) { map.setView(coordinates[0], 15); } }; summary.textContent = `${formatDistance(result.trip.distance_km)} · ` + `${formatDuration(result.trip.duration_seconds)} · ` + `avg ${formatSpeed(result.trip.average_speed_kmh)} · ` + `max ${formatSpeed(result.trip.max_speed_kmh)} · ` + `${result.points.length} route points`; initializeTimeline(result.points); } function renderTrips(trips) { body.replaceChildren(); trips.forEach(function (trip) { const row = document.createElement("tr"); row.dataset.started = trip.start_datetime; row.dataset.distance = trip.distance_km; row.dataset.duration = trip.duration_seconds; row.dataset.maxSpeed = trip.max_speed_kmh; row.dataset.tripId = trip.id; row.style.cursor = "pointer"; row.innerHTML = ` ${formatDate(trip.start_datetime)} ${formatDistance(trip.distance_km)} ${formatDuration(trip.duration_seconds)} ${formatSpeed(trip.max_speed_kmh)} ${trip.odo_start_km.toFixed(0)} → ${trip.odo_end_km.toFixed(0)} km `; body.appendChild(row); }); } body.addEventListener("click", function (event) { // Button click (desktop) or row click (mobile) const button = event.target.closest("[data-trip-id]"); const row = event.target.closest("tr[data-trip-id]"); const tripId = button ? button.dataset.tripId : (row ? row.dataset.tripId : null); if (!tripId) { return; } replayTrip(tripId).catch(function (replayError) { if (replayError.name !== "AbortError") { showError(replayError.message); } }); }); scrubber.addEventListener("input", function () { stopReplay(); showReplayPoint(Number(scrubber.value)); }); playButton.addEventListener("click", function () { if (replayTimer) { stopReplay(); return; } if (Number(scrubber.value) >= replayPoints.length - 1) { showReplayPoint(0); } playButton.innerHTML = ''; playButton.setAttribute("aria-label", "Pause trip replay"); replayTimer = window.setInterval(function () { const nextIndex = Number(scrubber.value) + 1; if (nextIndex >= replayPoints.length) { stopReplay(); return; } showReplayPoint(nextIndex); }, 500); }); fetch("/api/trips") .then(function (response) { if (!response.ok) { throw new Error("Could not load trip history."); } return response.json(); }) .then(function (result) { renderTrips(result.trips); }) .catch(function (loadError) { showError(loadError.message); }) .finally(function () { loading.hidden = true; }); }());