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 replayPoints = [];
|
||||||
let replayTimer = null;
|
let replayTimer = null;
|
||||||
let replayController = null;
|
let replayController = null;
|
||||||
|
const recenterBtn = document.getElementById("btn-recenter-history");
|
||||||
|
|
||||||
function formatDuration(seconds) {
|
function formatDuration(seconds) {
|
||||||
const hours = Math.floor(seconds / 3600);
|
const hours = Math.floor(seconds / 3600);
|
||||||
@@ -132,6 +133,14 @@
|
|||||||
} else if (coordinates.length === 1) {
|
} else if (coordinates.length === 1) {
|
||||||
map.setView(coordinates[0], 15);
|
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)} · `
|
summary.textContent = `${formatDistance(result.trip.distance_km)} · `
|
||||||
+ `${formatDuration(result.trip.duration_seconds)} · `
|
+ `${formatDuration(result.trip.duration_seconds)} · `
|
||||||
+ `avg ${formatSpeed(result.trip.average_speed_kmh)} · `
|
+ `avg ${formatSpeed(result.trip.average_speed_kmh)} · `
|
||||||
|
|||||||
@@ -185,6 +185,31 @@ socket.on('obd_error', function (data) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let mapCentered = false;
|
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) {
|
socket.on('gps_location', function(data) {
|
||||||
console.log("location received")
|
console.log("location received")
|
||||||
@@ -204,6 +229,14 @@ socket.on('gps_location', function(data) {
|
|||||||
map.setView([lat, lon], 15);
|
map.setView([lat, lon], 15);
|
||||||
mapCentered = true;
|
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 shadow-sm">
|
||||||
<div class="card-header py-2 d-flex justify-content-between align-items-center">
|
<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>
|
<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>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div id="history-map" aria-label="Selected trip route map"></div>
|
<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="row g-3 mb-3">
|
||||||
<div class="col-lg-4 col-xl-3">
|
<div class="col-lg-4 col-xl-3">
|
||||||
<div class="card shadow-sm h-100">
|
<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>
|
<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>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
</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>
|
<div class="d-flex gap-4 mb-1">
|
||||||
<div class="text-xs text-muted fw-bold text-uppercase">Lat</div>
|
<div>
|
||||||
<div id="gpsLocationX" class="fw-semibold small">—</div>
|
<div class="text-xs text-muted fw-bold text-uppercase">Lat</div>
|
||||||
</div>
|
<div id="gpsLocationX" class="fw-semibold small">—</div>
|
||||||
<div>
|
</div>
|
||||||
<div class="text-xs text-muted fw-bold text-uppercase">Lon</div>
|
<div>
|
||||||
<div id="gpsLocationY" class="fw-semibold small">—</div>
|
<div class="text-xs text-muted fw-bold text-uppercase">Lon</div>
|
||||||
|
<div id="gpsLocationY" class="fw-semibold small">—</div>
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user