Files
yaris-obd/static/js/history.js
T
Mathieu MoranandCopilot afce229310 feat: odometer in trips/overview, trips upload toggle, fix video count
- OBD odometer now logged to tracking rows on every GPS snapshot
- trips table: START_ODOMETER/END_ODOMETER columns (migration)
- build_trip_summaries: picks odometer from first/last tracking row
- list_trips: uses real OBD odo, falls back to cumulative distance
- overview stats: odometer_km (max seen from OBD), video_count fixed
- history page: Odometer column shows start→end km per trip
- app_upload: TRIPS_UPLOAD_SETTING_KEY, is_trips_upload_enabled()
- _upload_tracking_batch: skips when trips upload is off
- settings: separate 'Upload trips & GPS' toggle from dashcam toggle

Co-authored-by: Copilot <[email protected]>
2026-08-10 02:07:31 -04:00

223 lines
7.8 KiB
JavaScript

(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: "&copy; MapTiler &copy; OpenStreetMap contributors",
});
let routeLayer = null;
let vehicleMarker = null;
let replayPoints = [];
let replayTimer = null;
let replayController = null;
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) {
const miles = kilometers * 0.621371;
return `${kilometers.toFixed(kilometers < 1 ? 2 : 1)} km / `
+ `${miles.toFixed(miles < 1 ? 2 : 1)} mi`;
}
function formatSpeed(kilometersPerHour) {
return `${kilometersPerHour.toFixed(1)} km/h / `
+ `${(kilometersPerHour * 0.621371).toFixed(1)} mph`;
}
function showError(message) {
error.textContent = message;
error.hidden = false;
}
function stopReplay() {
if (replayTimer) {
window.clearInterval(replayTimer);
replayTimer = null;
}
playButton.innerHTML = '<i class="fas fa-play" aria-hidden="true"></i>';
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);
}
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.innerHTML = `
<td>${formatDate(trip.start_datetime)}</td>
<td>${formatDistance(trip.distance_km)}</td>
<td>${formatDuration(trip.duration_seconds)}</td>
<td>${formatSpeed(trip.max_speed_kmh)}</td>
<td class="text-muted small">${trip.odo_start_km.toFixed(0)} → ${trip.odo_end_km.toFixed(0)} km</td>
<td class="text-end">
<button class="btn btn-sm btn-primary" type="button" data-trip-id="${trip.id}">
Replay
</button>
</td>`;
body.appendChild(row);
});
}
body.addEventListener("click", function (event) {
const button = event.target.closest("[data-trip-id]");
if (!button) {
return;
}
replayTrip(button.dataset.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 = '<i class="fas fa-pause" aria-hidden="true"></i>';
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;
});
}());