Skip to content

Commit 098147e

Browse files
stepankuzmingithub-actions[bot]
authored andcommitted
Add followUserLocation option to GeolocateControl
GitOrigin-RevId: 5c9d0b1dad7339464f0cc3b9613336f711512a2f
1 parent 3470a93 commit 098147e

File tree

2 files changed

+353
-17
lines changed

2 files changed

+353
-17
lines changed

src/ui/control/geolocate_control.ts

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,18 @@ import {mercatorZfromAltitude} from '../../geo/mercator_coordinate';
99

1010
import type {Map, IControl} from '../map';
1111
import type {MapEventOf} from '../events';
12-
import type {AnimationOptions, CameraOptions} from '../camera';
12+
import type {EasingOptions} from '../camera';
1313

1414
export type GeolocateControlOptions = {
1515
positionOptions?: PositionOptions;
16-
fitBoundsOptions?: AnimationOptions & CameraOptions;
16+
fitBoundsOptions?: EasingOptions;
1717
trackUserLocation?: boolean;
1818
showAccuracyCircle?: boolean;
1919
showUserLocation?: boolean;
2020
showUserHeading?: boolean;
2121
geolocation?: Geolocation;
2222
showButton?: boolean;
23+
followUserLocation?: boolean;
2324
};
2425

2526
declare global {
@@ -47,7 +48,8 @@ const defaultOptions = {
4748
showAccuracyCircle: true,
4849
showUserLocation: true,
4950
showUserHeading: false,
50-
showButton: true
51+
showButton: true,
52+
followUserLocation: true
5153
};
5254

5355
type GeolocateControlEvents = {
@@ -80,14 +82,15 @@ type GeolocateControlEvents = {
8082
*
8183
* @implements {IControl}
8284
* @param {Object} [options]
83-
* @param {Object} [options.positionOptions={enableHighAccuracy: false, timeout: 6000}] A Geolocation API [PositionOptions](https://developer.mozilla.org/en-US/docs/Web/API/PositionOptions) object.
84-
* @param {Object} [options.fitBoundsOptions={maxZoom: 15}] A {@link Map#fitBounds} options object to use when the map is panned and zoomed to the user's location. The default is to use a `maxZoom` of 15 to limit how far the map will zoom in for very accurate locations.
85-
* @param {Object} [options.trackUserLocation=false] If `true` the `GeolocateControl` becomes a toggle button and when active the map will receive updates to the user's location as it changes.
86-
* @param {Object} [options.showAccuracyCircle=true] By default, if `showUserLocation` is `true`, a transparent circle will be drawn around the user location indicating the accuracy (95% confidence level) of the user's location. Set to `false` to disable. Always disabled when `showUserLocation` is `false`.
87-
* @param {Object} [options.showUserLocation=true] By default a dot will be shown on the map at the user's location. Set to `false` to disable.
88-
* @param {Object} [options.showUserHeading=false] If `true` an arrow will be drawn next to the user location dot indicating the device's heading. This only has affect when `trackUserLocation` is `true`.
89-
* @param {Object} [options.geolocation=window.navigator.geolocation] `window.navigator.geolocation` by default; you can provide an object with the same shape to customize geolocation handling.
85+
* @param {PositionOptions} [options.positionOptions={enableHighAccuracy: false, timeout: 6000}] A Geolocation API [PositionOptions](https://developer.mozilla.org/en-US/docs/Web/API/PositionOptions) object.
86+
* @param {EasingOptions} [options.fitBoundsOptions={maxZoom: 15}] A {@link Map#fitBounds} options object to use when the map is panned and zoomed to the user's location. The default is to use a `maxZoom` of 15 to limit how far the map will zoom in for very accurate locations.
87+
* @param {boolean} [options.trackUserLocation=false] If `true` the `GeolocateControl` becomes a toggle button and when active the map will receive updates to the user's location as it changes.
88+
* @param {boolean} [options.showAccuracyCircle=true] By default, if `showUserLocation` is `true`, a transparent circle will be drawn around the user location indicating the accuracy (95% confidence level) of the user's location. Set to `false` to disable. Always disabled when `showUserLocation` is `false`.
89+
* @param {boolean} [options.showUserLocation=true] By default a dot will be shown on the map at the user's location. Set to `false` to disable.
90+
* @param {boolean} [options.showUserHeading=false] If `true` an arrow will be drawn next to the user location dot indicating the device's heading. This only has affect when `trackUserLocation` is `true`.
91+
* @param {Geolocation} [options.geolocation=window.navigator.geolocation] `window.navigator.geolocation` by default; you can provide an object with the same shape to customize geolocation handling.
9092
* @param {boolean} [options.showButton=true] If `false`, the control button will be hidden. The user location dot can still be shown by setting `showUserLocation` to `true` and calling {@link GeolocateControl#trigger} programmatically.
93+
* @param {boolean} [options.followUserLocation=true] If `true`, the camera centers on the user's location. If `false`, the location dot will be shown without moving the camera. Clicking the control still centers on the user's location.
9194
*
9295
* @example
9396
* map.addControl(new mapboxgl.GeolocateControl({
@@ -107,6 +110,16 @@ type GeolocateControlEvents = {
107110
* });
108111
* map.addControl(geolocate);
109112
* geolocate.trigger();
113+
*
114+
* @example
115+
* // Show user location without moving the camera
116+
* const geolocate = new mapboxgl.GeolocateControl({
117+
* trackUserLocation: true,
118+
* showUserLocation: true,
119+
* followUserLocation: false
120+
* });
121+
* map.addControl(geolocate);
122+
* geolocate.trigger();
110123
* @see [Example: Locate the user](https://www.mapbox.com/mapbox-gl-js/example/locate-user/)
111124
*/
112125
class GeolocateControl extends Evented<GeolocateControlEvents> implements IControl {
@@ -282,10 +295,18 @@ class GeolocateControl extends Evented<GeolocateControlEvents> implements IContr
282295
case 'WAITING_ACTIVE':
283296
case 'ACTIVE_LOCK':
284297
case 'ACTIVE_ERROR':
285-
this._watchState = 'ACTIVE_LOCK';
298+
// Remove all possible "from" classes first
286299
this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-waiting');
287300
this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active-error');
288-
this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active');
301+
302+
// If followUserLocation is false, go directly to BACKGROUND since camera won't follow
303+
if (!this.options.followUserLocation) {
304+
this._watchState = 'BACKGROUND';
305+
this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-background');
306+
} else {
307+
this._watchState = 'ACTIVE_LOCK';
308+
this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active');
309+
}
289310
break;
290311
case 'BACKGROUND':
291312
case 'BACKGROUND_ERROR':
@@ -304,9 +325,12 @@ class GeolocateControl extends Evented<GeolocateControlEvents> implements IContr
304325
this._updateMarker(position);
305326
}
306327

307-
// if in normal mode (not watch mode), or if in watch mode and the state is active watch
308-
// then update the camera
309-
if (!this.options.trackUserLocation || this._watchState === 'ACTIVE_LOCK') {
328+
// Update camera in one-time mode, or when actively locked in watch mode
329+
const isOneTimeMode = !this.options.trackUserLocation;
330+
const isActivelyTracking = this._watchState === 'ACTIVE_LOCK';
331+
const shouldFollowUser = this.options.followUserLocation;
332+
333+
if (shouldFollowUser && (isOneTimeMode || isActivelyTracking)) {
310334
this._updateCamera(position);
311335
}
312336

@@ -598,8 +622,10 @@ class GeolocateControl extends Evented<GeolocateControlEvents> implements IContr
598622
case 'BACKGROUND':
599623
this._watchState = 'ACTIVE_LOCK';
600624
this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background');
601-
// set camera to last known location
602-
if (this._lastKnownPosition) this._updateCamera(this._lastKnownPosition);
625+
// Button click is explicit user action - always center on their location
626+
if (this._lastKnownPosition) {
627+
this._updateCamera(this._lastKnownPosition);
628+
}
603629

604630
this.fire(new Event('trackuserlocationstart'));
605631
break;
@@ -672,6 +698,52 @@ class GeolocateControl extends Evented<GeolocateControlEvents> implements IContr
672698
return true;
673699
}
674700

701+
/**
702+
* Sets whether the camera follows the user's location.
703+
*
704+
* Fires {@link GeolocateControl#trackuserlocationstart} when enabling,
705+
* {@link GeolocateControl#trackuserlocationend} when disabling.
706+
*
707+
* @param {boolean} [follow] Whether to follow the user's location.
708+
* @returns {GeolocateControl} `this`.
709+
* @example
710+
* geolocate.setFollowUserLocation(false); // stop following
711+
* geolocate.setFollowUserLocation(true); // resume and center
712+
*/
713+
setFollowUserLocation(follow?: boolean): this {
714+
this.options.followUserLocation = follow != null ? follow : defaultOptions.followUserLocation;
715+
716+
if (this.options.trackUserLocation && this._watchState !== 'OFF') {
717+
if (this.options.followUserLocation) {
718+
// Enabling: center camera and transition to ACTIVE_LOCK if in BACKGROUND
719+
if (this._watchState === 'BACKGROUND' || this._watchState === 'BACKGROUND_ERROR') {
720+
this._watchState = 'ACTIVE_LOCK';
721+
this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background');
722+
this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background-error');
723+
this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active');
724+
725+
if (this._lastKnownPosition) {
726+
this._updateCamera(this._lastKnownPosition);
727+
}
728+
729+
this.fire(new Event('trackuserlocationstart'));
730+
}
731+
} else {
732+
// Disabling: transition to BACKGROUND if in ACTIVE_LOCK
733+
if (this._watchState === 'ACTIVE_LOCK' || this._watchState === 'ACTIVE_ERROR') {
734+
this._watchState = 'BACKGROUND';
735+
this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active');
736+
this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active-error');
737+
this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-background');
738+
739+
this.fire(new Event('trackuserlocationend'));
740+
}
741+
}
742+
}
743+
744+
return this;
745+
}
746+
675747
_addDeviceOrientationListener() {
676748
const addListener = () => {
677749
const eventName = 'ondeviceorientationabsolute' in window ?

0 commit comments

Comments
 (0)