feat: add user geolocation marker and auto-zoom to map based on search radius
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

This commit is contained in:
Ovidiu U
2026-04-07 20:21:31 +01:00
parent 0b26c4c257
commit 4e9b809a10
7 changed files with 883 additions and 36 deletions

View File

@@ -26,15 +26,38 @@ const CLASSIFICATION_COLOURS = {
};
const UK_CENTRE = [54.0, -2.0];
const UK_ZOOM = 6;
const UK_ZOOM = 7;
export function stationMap(results) {
const USER_MARKER_CSS = `
@keyframes fuelalert-pulse {
0% { transform: scale(1); opacity: 0.6; }
70% { transform: scale(2.8); opacity: 0; }
100% { transform: scale(1); opacity: 0; }
}
.fuelalert-user-marker { position: relative; width: 16px; height: 16px; }
.fuelalert-user-dot { position: absolute; inset: 0; border-radius: 50%; background: #3b82f6; border: 2px solid #fff; box-shadow: 0 0 0 2px #3b82f6; }
.fuelalert-user-ring { position: absolute; inset: 0; border-radius: 50%; background: #3b82f6; animation: fuelalert-pulse 2s ease-out infinite; }
`;
function injectUserMarkerStyles() {
if (document.getElementById('fuelalert-user-marker-styles')) return;
const style = document.createElement('style');
style.id = 'fuelalert-user-marker-styles';
style.textContent = USER_MARKER_CSS;
document.head.appendChild(style);
}
export function stationMap(results, meta, radius) {
return {
results,
meta,
radius,
_map: null,
_markers: [],
_userMarker: null,
init() {
injectUserMarkerStyles();
this._map = L.map(this.$el, { zoomControl: true }).setView(UK_CENTRE, UK_ZOOM);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
@@ -47,6 +70,18 @@ export function stationMap(results) {
}
this.$watch('results', () => this._plotMarkers());
this.locateUser();
},
getZoomForRadius(radiusMiles) {
if (radiusMiles <= 1) return 15;
if (radiusMiles <= 2) return 14;
if (radiusMiles <= 5) return 12;
if (radiusMiles <= 10) return 11;
if (radiusMiles <= 15) return 10;
if (radiusMiles <= 25) return 9;
if (radiusMiles <= 50) return 8;
return 7;
},
_clearMarkers() {
@@ -54,11 +89,61 @@ export function stationMap(results) {
this._markers = [];
},
addUserMarker(lat, lng) {
if (this._userMarker) {
this._userMarker.remove();
}
const icon = L.divIcon({
className: '',
html: '<div class="fuelalert-user-marker"><div class="fuelalert-user-ring"></div><div class="fuelalert-user-dot"></div></div>',
iconSize: [16, 16],
iconAnchor: [8, 8],
});
this._userMarker = L.marker([lat, lng], { icon, zIndexOffset: 1000 })
.bindPopup('Your location')
.addTo(this._map);
console.log(`[stationMap] user marker lat=${lat} lng=${lng}`);
},
locateUser() {
if (!navigator.geolocation) {
console.warn('[stationMap] Geolocation not supported');
return;
}
const ipFallback = () => {
fetch('https://ipapi.co/json/')
.then((r) => r.json())
.then((d) => d.latitude && d.longitude && this.addUserMarker(d.latitude, d.longitude))
.catch(() => {});
};
// Quick low-accuracy fix first — places the marker immediately.
navigator.geolocation.getCurrentPosition(
(pos) => {
this.addUserMarker(pos.coords.latitude, pos.coords.longitude);
// Then refine with high accuracy if GPS is available.
navigator.geolocation.getCurrentPosition(
(precise) => this.addUserMarker(precise.coords.latitude, precise.coords.longitude),
() => {},
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 0 },
);
},
() => ipFallback(),
{ enableHighAccuracy: false, timeout: 5000, maximumAge: 60000 },
);
},
destroy() {
if (this._map) {
this._map.remove();
this._map = null;
this._markers = [];
this._userMarker = null;
}
},
@@ -72,8 +157,6 @@ export function stationMap(results) {
return;
}
const bounds = [];
this.results.forEach((station) => {
const colour = escHtml(CLASSIFICATION_COLOURS[station.price_classification] ?? '#64748b');
const miles = (station.distance_km * 0.621371).toFixed(1);
@@ -101,14 +184,18 @@ export function stationMap(results) {
marker.addTo(this._map);
this._markers.push(marker);
bounds.push([station.lat, station.lng]);
});
if (bounds.length === 1) {
this._map.setView(bounds[0], 14);
} else {
this._map.fitBounds(bounds, { padding: [40, 40], maxZoom: 14 });
}
const map = this._map;
const lat = this.meta?.lat;
const lng = this.meta?.lng;
const zoom = this.getZoomForRadius(this.radius);
setTimeout(() => {
map.invalidateSize();
map.setView([lat, lng], zoom, { animate: true, duration: 0.5 });
console.log(`[stationMap] setView lat=${lat} lng=${lng} zoom=${zoom} (radius=${this.radius}mi)`);
}, 50);
},
};
}