feat(map): reverse geocode location + recenter button on dashboard and history

- Dashboard: recenter button in GPS card header (hidden until first fix)
- Dashboard: estimated location name under lat/lon via Nominatim reverse geocode (throttled 30s)
- History: recenter button fits map back to trip bounds
- Both buttons use crosshairs icon, hidden until data is available

Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Mathieu Moran
2026-08-10 02:43:23 -04:00
co-authored by Copilot
parent 08a3bef558
commit f1145a4dbb
4 changed files with 63 additions and 10 deletions
+9
View File
@@ -21,6 +21,7 @@
let replayPoints = [];
let replayTimer = null;
let replayController = null;
const recenterBtn = document.getElementById("btn-recenter-history");
function formatDuration(seconds) {
const hours = Math.floor(seconds / 3600);
@@ -132,6 +133,14 @@
} 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)} · `
+33
View File
@@ -185,6 +185,31 @@ socket.on('obd_error', function (data) {
});
let mapCentered = false;
let _lastLat = null, _lastLon = null;
let _geocodeTimer = null;
const _recenterBtn = document.getElementById('btn-recenter');
_recenterBtn.addEventListener('click', function () {
if (_lastLat != null && _lastLon != null) {
map.setView([_lastLat, _lastLon], 15);
}
});
function reverseGeocode(lat, lon) {
fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lon}`, {
headers: { 'Accept-Language': 'en' }
})
.then(r => r.ok ? r.json() : null)
.then(data => {
if (!data) return;
const a = data.address || {};
const name = a.road || a.hamlet || a.village || a.suburb || '';
const city = a.city || a.town || a.village || a.municipality || '';
const label = [name, city].filter(Boolean).join(', ');
document.getElementById('gps-location-name').textContent = label || data.display_name?.split(',').slice(0,2).join(',') || '';
})
.catch(() => {});
}
socket.on('gps_location', function(data) {
console.log("location received")
@@ -204,6 +229,14 @@ socket.on('gps_location', function(data) {
map.setView([lat, lon], 15);
mapCentered = true;
}
_lastLat = lat;
_lastLon = lon;
_recenterBtn.hidden = false;
// Throttle geocode to at most once every 30s
if (!_geocodeTimer) {
reverseGeocode(lat, lon);
_geocodeTimer = setTimeout(() => { _geocodeTimer = null; }, 30000);
}
}
});
+5
View File
@@ -46,8 +46,13 @@
<div class="card shadow-sm">
<div class="card-header py-2 d-flex justify-content-between align-items-center">
<h6 class="text-primary fw-bold m-0">Route replay</h6>
<div class="d-flex align-items-center gap-2">
<button id="btn-recenter-history" class="btn btn-outline-secondary btn-sm" type="button" title="Recenter map" hidden>
<i class="fas fa-crosshairs" aria-hidden="true"></i>
</button>
<span id="trip-summary" class="small text-muted">Select a trip</span>
</div>
</div>
<div class="card-body p-0">
<div id="history-map" aria-label="Selected trip route map"></div>
<div id="trip-timeline" class="border-top p-3" hidden>
+8 -2
View File
@@ -95,13 +95,17 @@
<div class="row g-3 mb-3">
<div class="col-lg-4 col-xl-3">
<div class="card shadow-sm h-100">
<div class="card-header py-2">
<div class="card-header py-2 d-flex justify-content-between align-items-center">
<h6 class="text-primary fw-bold m-0">Live GPS</h6>
<button id="btn-recenter" class="btn btn-outline-secondary btn-sm" type="button" title="Recenter map" hidden>
<i class="fas fa-crosshairs" aria-hidden="true"></i>
</button>
</div>
<div class="card-body p-0">
<div id="map"></div>
</div>
<div class="card-footer py-2 d-flex gap-4 bg-white border-top">
<div class="card-footer py-2 bg-white border-top">
<div class="d-flex gap-4 mb-1">
<div>
<div class="text-xs text-muted fw-bold text-uppercase">Lat</div>
<div id="gpsLocationX" class="fw-semibold small">—</div>
@@ -111,6 +115,8 @@
<div id="gpsLocationY" class="fw-semibold small">—</div>
</div>
</div>
<div id="gps-location-name" class="text-muted small" style="font-size:0.75rem;min-height:1rem"></div>
</div>
</div>
</div>
<div class="col-lg-3 col-xl-2">