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:
@@ -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)} · `
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -46,7 +46,12 @@
|
||||
<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>
|
||||
<span id="trip-summary" class="small text-muted">Select a trip</span>
|
||||
<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>
|
||||
|
||||
+15
-9
@@ -95,21 +95,27 @@
|
||||
<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>
|
||||
<div class="text-xs text-muted fw-bold text-uppercase">Lat</div>
|
||||
<div id="gpsLocationX" class="fw-semibold small">—</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-muted fw-bold text-uppercase">Lon</div>
|
||||
<div id="gpsLocationY" class="fw-semibold small">—</div>
|
||||
<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>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-muted fw-bold text-uppercase">Lon</div>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user