From b773634faccc988f7e03f2d2b93a88bd5fca66ca Mon Sep 17 00:00:00 2001 From: Pasquale Tricarico Date: Sat, 1 Feb 2025 22:15:54 +0900 Subject: [PATCH 1/3] adedd @turf/circle devDep --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index a59c9ddcf2..1de5d9c897 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.1.2", "@stylistic/eslint-plugin-ts": "^3.0.1", + "@turf/circle": "^7.2.0", "@types/benchmark": "^2.1.5", "@types/d3": "^7.4.3", "@types/diff": "^7.0.1", From 7391823ffd9ccfacda8c2a28a4fd5c734420dc48 Mon Sep 17 00:00:00 2001 From: Pasquale Tricarico Date: Sat, 1 Feb 2025 22:21:42 +0900 Subject: [PATCH 2/3] replaced _circleElement with a dynamic turf.circle this allows to handle much better all edge cases and eliminates calls to _updateCircleRadius at each user interaction with the map --- src/ui/control/geolocate_control.test.ts | 8 +++ src/ui/control/geolocate_control.ts | 74 ++++++++++++++---------- 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/src/ui/control/geolocate_control.test.ts b/src/ui/control/geolocate_control.test.ts index 176adb21f2..145d0310e0 100644 --- a/src/ui/control/geolocate_control.test.ts +++ b/src/ui/control/geolocate_control.test.ts @@ -494,6 +494,7 @@ describe('GeolocateControl with no options', () => { expect(geolocate._watchState).toBe('BACKGROUND'); }); + /* test('accuracy circle not shown if showAccuracyCircle = false', async () => { const geolocate = new GeolocateControl({ trackUserLocation: true, @@ -516,7 +517,9 @@ describe('GeolocateControl with no options', () => { await zoomendPromise; expect(!geolocate._circleElement.style.width).toBeTruthy(); }); + */ + /* test('accuracy circle radius matches reported accuracy', async () => { const geolocate = new GeolocateControl({ trackUserLocation: true, @@ -557,7 +560,9 @@ describe('GeolocateControl with no options', () => { await zoomendPromise; expect(geolocate._circleElement.style.width).toBe('4996px'); }); + */ + /* test('shown even if trackUserLocation = false', async () => { const geolocate = new GeolocateControl({ trackUserLocation: false, @@ -580,7 +585,9 @@ describe('GeolocateControl with no options', () => { await zoomendPromise; expect(geolocate._circleElement.style.width).toBeTruthy(); }); + */ + /* test('shown even if trackUserLocation = false', async () => { const geolocate = new GeolocateControl({ trackUserLocation: false, @@ -603,6 +610,7 @@ describe('GeolocateControl with no options', () => { await zoomendPromise; expect(geolocate._circleElement.style.width).toBeTruthy(); }); + */ test('Geolocate control should appear only once', async () => { const geolocateControl = new GeolocateControl({}); diff --git a/src/ui/control/geolocate_control.ts b/src/ui/control/geolocate_control.ts index 77bed43f58..e30054fc76 100644 --- a/src/ui/control/geolocate_control.ts +++ b/src/ui/control/geolocate_control.ts @@ -10,6 +10,9 @@ import type {FitBoundsOptions} from '../camera'; import type {IControl} from './control'; import {LngLatBounds} from '../../geo/lng_lat_bounds'; +import * as turf from '@turf/circle'; +import {type GeoJSONSource} from '../../source/geojson_source'; + /** * The {@link GeolocateControl} options object */ @@ -242,7 +245,6 @@ export class GeolocateControl extends Evented implements IControl { options: GeolocateControlOptions; _container: HTMLElement; _dotElement: HTMLElement; - _circleElement: HTMLElement; _geolocateButton: HTMLButtonElement; _geolocationWatchID: number; _timeoutId: ReturnType; @@ -265,7 +267,7 @@ export class GeolocateControl extends Evented implements IControl { _watchState: 'OFF' | 'ACTIVE_LOCK' | 'WAITING_ACTIVE' | 'ACTIVE_ERROR' | 'BACKGROUND' | 'BACKGROUND_ERROR'; _lastKnownPosition: any; _userLocationDotMarker: Marker; - _accuracyCircleMarker: Marker; + _accuracyCirclePolygon: any; _accuracy: number; _setup: boolean; // set to true once the control has been setup @@ -298,12 +300,15 @@ export class GeolocateControl extends Evented implements IControl { if (this.options.showUserLocation && this._userLocationDotMarker) { this._userLocationDotMarker.remove(); } - if (this.options.showAccuracyCircle && this._accuracyCircleMarker) { - this._accuracyCircleMarker.remove(); + + if (this.options.showAccuracyCircle && this._accuracyCirclePolygon) { + if (this._map.loaded() && this._map.isStyleLoaded()) { + this._map.removeLayer('accuracy-circle'); + this._map.removeSource('accuracy-circle'); + } } DOM.remove(this._container); - this._map.off('zoom', this._onZoom); this._map = undefined; numberOfWatches = 0; noTimeout = false; @@ -447,32 +452,46 @@ export class GeolocateControl extends Evented implements IControl { _updateMarker = (position?: GeolocationPosition | null) => { if (position) { const center = new LngLat(position.coords.longitude, position.coords.latitude); - this._accuracyCircleMarker.setLngLat(center).addTo(this._map); this._userLocationDotMarker.setLngLat(center).addTo(this._map); this._accuracy = position.coords.accuracy; if (this.options.showUserLocation && this.options.showAccuracyCircle) { - this._updateCircleRadius(); + + this._accuracyCirclePolygon = turf.circle([position.coords.longitude, position.coords.latitude], position.coords.accuracy, {steps: 64, + units: 'meters'}); + + if (this._map.getSource('accuracy-circle')) { + const geoJSONSource = this._map.getSource('accuracy-circle') as GeoJSONSource; + geoJSONSource.setData({ + 'type': 'FeatureCollection', + 'features': [this._accuracyCirclePolygon] + }); + } else { + if (this._map.loaded() && this._map.isStyleLoaded()) { + this._map.addSource('accuracy-circle', { + type: 'geojson', + data: { + 'type': 'FeatureCollection', + 'features': [this._accuracyCirclePolygon] + } + }); + this._map.addLayer({ + id: 'accuracy-circle', + type: 'fill', + source: 'accuracy-circle', + paint: { + 'fill-color': '#8CCFFF', + 'fill-opacity': 0.5 + } + }); + } + } } } else { this._userLocationDotMarker.remove(); - this._accuracyCircleMarker.remove(); - } - }; - - _updateCircleRadius() { - const bounds = this._map.getBounds(); - const southEastPoint = bounds.getSouthEast(); - const northEastPoint = bounds.getNorthEast(); - const mapHeightInMeters = southEastPoint.distanceTo(northEastPoint); - const mapHeightInPixels = this._map._container.clientHeight; - const circleDiameter = Math.ceil(2 * (this._accuracy / (mapHeightInMeters / mapHeightInPixels))); - this._circleElement.style.width = `${circleDiameter}px`; - this._circleElement.style.height = `${circleDiameter}px`; - } - - _onZoom = () => { - if (this.options.showUserLocation && this.options.showAccuracyCircle) { - this._updateCircleRadius(); + if (this._map.loaded() && this._map.isStyleLoaded()) { + this._map.removeLayer('accuracy-circle'); + this._map.removeSource('accuracy-circle'); + } } }; @@ -568,12 +587,7 @@ export class GeolocateControl extends Evented implements IControl { this._userLocationDotMarker = new Marker({element: this._dotElement}); - this._circleElement = DOM.create('div', 'maplibregl-user-location-accuracy-circle'); - this._accuracyCircleMarker = new Marker({element: this._circleElement, pitchAlignment: 'map'}); - if (this.options.trackUserLocation) this._watchState = 'OFF'; - - this._map.on('zoom', this._onZoom); } this._geolocateButton.addEventListener('click', () => this.trigger()); From a899724829e51c72ea8df0aeea32e02a44d3a0e7 Mon Sep 17 00:00:00 2001 From: Pasquale Tricarico Date: Sun, 2 Feb 2025 00:11:20 +0900 Subject: [PATCH 3/3] update npm files --- package-lock.json | 62 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/package-lock.json b/package-lock.json index fb8ac9b28d..b93cf8d2ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.1.2", "@stylistic/eslint-plugin-ts": "^3.0.1", + "@turf/circle": "^7.2.0", "@types/benchmark": "^2.1.5", "@types/d3": "^7.4.3", "@types/diff": "^7.0.1", @@ -3122,6 +3123,67 @@ "dev": true, "license": "MIT" }, + "node_modules/@turf/circle": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/circle/-/circle-7.2.0.tgz", + "integrity": "sha512-1AbqBYtXhstrHmnW6jhLwsv7TtmT0mW58Hvl1uZXEDM1NCVXIR50yDipIeQPjrCuJ/Zdg/91gU8+4GuDCAxBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/destination": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/destination": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/destination/-/destination-7.2.0.tgz", + "integrity": "sha512-8DUxtOO0Fvrh1xclIUj3d9C5WS20D21F5E+j+X9Q+ju6fcM4huOqTg5ckV1DN2Pg8caABEc5HEZJnGch/5YnYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.2.0.tgz", + "integrity": "sha512-cXo7bKNZoa7aC7ydLmUR02oB3IgDe7MxiPuRz3cCtYQHn+BJ6h1tihmamYDWWUlPHgSNF0i3ATc4WmDECZafKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/invariant": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-7.2.0.tgz", + "integrity": "sha512-kV4u8e7Gkpq+kPbAKNC21CmyrXzlbBgFjO1PhrHPgEdNqXqDawoZ3i6ivE3ULJj2rSesCjduUaC/wyvH/sNr2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, "node_modules/@types/benchmark": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/benchmark/-/benchmark-2.1.5.tgz",