diff --git a/public/js/locate.js b/public/js/locate.js index 5927746..06107ac 100644 --- a/public/js/locate.js +++ b/public/js/locate.js @@ -18,7 +18,10 @@ export class LocateControl { this._gnssControl = gnssControl; this._active = false; this._marker = null; + this._circleMarker = null; + this._accuracy = null; this._watchId = null; + this._onMapChange = () => this._updateCircleRadius(); } onAdd(map) { @@ -102,13 +105,15 @@ export class LocateControl { this._active = true; this._button.classList.add("maplibregl-ctrl-geolocate-active"); this._watchId = navigator.geolocation.watchPosition( - (position) => this._setPosition(position.coords.longitude, position.coords.latitude), + (position) => this._setPosition(position.coords.longitude, position.coords.latitude, position.coords.accuracy), () => { window.alert("Could not determine your location."); this._stop(); }, { enableHighAccuracy: true }, ); + this._map.on("zoom", this._onMapChange); + this._map.on("move", this._onMapChange); } // Recenters on every fix, following the position the way GeolocateControl @@ -123,16 +128,40 @@ export class LocateControl { // .gnss-marker — reusing that green for a browser-estimated position too // looked like this control was somehow still using GNSS data even when // it very much wasn't; distinct colors for distinct sources. - _setPosition(lon, lat) { + _setPosition(lon, lat, accuracy) { + this._accuracy = accuracy; if (!this._marker) { const el = document.createElement("div"); el.className = "maplibregl-user-location-dot"; this._marker = new Marker({ element: el }).setLngLat([lon, lat]).addTo(this._map); + const circleEl = document.createElement("div"); + circleEl.className = "maplibregl-user-location-accuracy-circle"; + this._circleMarker = new Marker({ element: circleEl, pitchAlignment: "map" }).setLngLat([lon, lat]).addTo(this._map); this._map.flyTo({ center: [lon, lat], zoom: Math.max(this._map.getZoom(), 15) }); } else { this._marker.setLngLat([lon, lat]); + this._circleMarker.setLngLat([lon, lat]); this._map.easeTo({ center: [lon, lat] }); } + this._updateCircleRadius(); + } + + // Mirrors MapLibre's own GeolocateControl._updateCircleRadiusIfNeeded: + // measures real-world meters per pixel at the marker's position (rather + // than assuming a flat conversion, which breaks away from the equator) + // by projecting a 100px offset back to lng/lat and measuring that + // distance, then sizes the circle element to the accuracy radius in + // that many pixels. + _updateCircleRadius() { + if (!this._circleMarker || !this._accuracy) return; + + const lngLat = this._marker.getLngLat(); + const point = this._map.project(lngLat); + const offsetLngLat = this._map.unproject([point.x + 100, point.y]); + const metersPerPixel = lngLat.distanceTo(offsetLngLat) / 100; + const diameter = (2 * this._accuracy) / metersPerPixel; + this._circleMarker.getElement().style.width = `${diameter.toFixed(2)}px`; + this._circleMarker.getElement().style.height = `${diameter.toFixed(2)}px`; } _stop() { @@ -142,9 +171,16 @@ export class LocateControl { navigator.geolocation.clearWatch(this._watchId); this._watchId = null; } + this._map.off("zoom", this._onMapChange); + this._map.off("move", this._onMapChange); if (this._marker) { this._marker.remove(); this._marker = null; } + if (this._circleMarker) { + this._circleMarker.remove(); + this._circleMarker = null; + } + this._accuracy = null; } }