Skip to content

Commit

Permalink
add circle to GeolocateControl showing accuracy of position (mapbox#9253
Browse files Browse the repository at this point in the history
)

* add circle to GeolocateControl showing accuracy of position

* don't call onZoom when not zooming

* show accuracyCircle even if tracking is off

* docs
  • Loading branch information
Meekohi authored and mike-unearth committed Mar 18, 2020
1 parent 0a690bd commit f4c9e47
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 2 deletions.
7 changes: 7 additions & 0 deletions src/css/mapbox-gl.css
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,13 @@ a.mapboxgl-ctrl-logo.mapboxgl-compact {
display: none;
}

.mapboxgl-user-location-accuracy-circle {
background-color: #1da1f233;
width: 1px;
height: 1px;
border-radius: 100%;
}

.mapboxgl-crosshair,
.mapboxgl-crosshair .mapboxgl-interactive,
.mapboxgl-crosshair .mapboxgl-interactive:active {
Expand Down
43 changes: 41 additions & 2 deletions src/ui/control/geolocate_control.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Options = {
positionOptions?: PositionOptions,
fitBoundsOptions?: AnimationOptions & CameraOptions,
trackUserLocation?: boolean,
showAccuracyCircle?: boolean,
showUserLocation?: boolean
};

Expand All @@ -28,6 +29,7 @@ const defaultOptions: Options = {
maxZoom: 15
},
trackUserLocation: false,
showAccuracyCircle: true,
showUserLocation: true
};

Expand Down Expand Up @@ -78,6 +80,7 @@ let noTimeout = false;
* @param {Object} [options.positionOptions={enableHighAccuracy: false, timeout: 6000}] A Geolocation API [PositionOptions](https://developer.mozilla.org/en-US/docs/Web/API/PositionOptions) object.
* @param {Object} [options.fitBoundsOptions={maxZoom: 15}] A [`fitBounds`](#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.
* @param {Object} [options.trackUserLocation=false] If `true` the Geolocate Control becomes a toggle button and when active the map will receive updates to the user's location as it changes.
* @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`.
* @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.
*
* @example
Expand All @@ -94,12 +97,15 @@ class GeolocateControl extends Evented {
options: Options;
_container: HTMLElement;
_dotElement: HTMLElement;
_circleElement: HTMLElement;
_geolocateButton: HTMLButtonElement;
_geolocationWatchID: number;
_timeoutId: ?TimeoutID;
_watchState: 'OFF' | 'ACTIVE_LOCK' | 'WAITING_ACTIVE' | 'ACTIVE_ERROR' | 'BACKGROUND' | 'BACKGROUND_ERROR';
_lastKnownPosition: any;
_userLocationDotMarker: Marker;
_accuracyCircleMarker: Marker;
_accuracy: number;
_setup: boolean; // set to true once the control has been setup

constructor(options: Options) {
Expand All @@ -109,6 +115,7 @@ class GeolocateControl extends Evented {
bindAll([
'_onSuccess',
'_onError',
'_onZoom',
'_finish',
'_setupUI',
'_updateCamera',
Expand All @@ -120,6 +127,7 @@ class GeolocateControl extends Evented {
this._map = map;
this._container = DOM.create('div', `mapboxgl-ctrl mapboxgl-ctrl-group`);
checkGeolocationSupport(this._setupUI);
this._map.on('zoom', this._onZoom);
return this._container;
}

Expand All @@ -130,12 +138,16 @@ class GeolocateControl extends Evented {
this._geolocationWatchID = (undefined: any);
}

// clear the marker from the map
// clear the markers from the map
if (this.options.showUserLocation && this._userLocationDotMarker) {
this._userLocationDotMarker.remove();
}
if (this.options.showAccuracyCircle && this._accuracyCircleMarker) {
this._accuracyCircleMarker.remove();
}

DOM.remove(this._container);
this._map.off('zoom', this._onZoom);
this._map = (undefined: any);
numberOfWatches = 0;
noTimeout = false;
Expand Down Expand Up @@ -251,9 +263,33 @@ class GeolocateControl extends Evented {

_updateMarker(position: ?Position) {
if (position) {
this._userLocationDotMarker.setLngLat([position.coords.longitude, position.coords.latitude]).addTo(this._map);
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();
}
} else {
this._userLocationDotMarker.remove();
this._accuracyCircleMarker.remove();
}
}

_updateCircleRadius() {
assert(this._circleElement);
const y = this._map._container.clientHeight / 2;
const a = this._map.unproject([0, y]);
const b = this._map.unproject([1, y]);
const metersPerPixel = a.distanceTo(b);
const circleDiameter = Math.ceil(2.0 * this._accuracy / metersPerPixel);
this._circleElement.style.width = `${circleDiameter}px`;
this._circleElement.style.height = `${circleDiameter}px`;
}

_onZoom() {
if (this.options.showUserLocation && this.options.showAccuracyCircle) {
this._updateCircleRadius();
}
}

Expand Down Expand Up @@ -329,6 +365,9 @@ class GeolocateControl extends Evented {

this._userLocationDotMarker = new Marker(this._dotElement);

this._circleElement = DOM.create('div', 'mapboxgl-user-location-accuracy-circle');
this._accuracyCircleMarker = new Marker({element: this._circleElement, pitchAlignment: 'map'});

if (this.options.trackUserLocation) this._watchState = 'OFF';
}

Expand Down
83 changes: 83 additions & 0 deletions test/unit/ui/control/geolocate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,3 +422,86 @@ test('GeolocateControl switches to BACKGROUND state on map manipulation', (t) =>
geolocate._geolocateButton.dispatchEvent(click);
geolocation.send({latitude: 10, longitude: 20, accuracy: 30, timestamp: 40});
});

test('GeolocateControl accuracy circle not shown if showAccuracyCircle = false', (t) => {
const map = createMap(t);
const geolocate = new GeolocateControl({
trackUserLocation: true,
showUserLocation: true,
showAccuracyCircle: false,
});
map.addControl(geolocate);

const click = new window.Event('click');

geolocate.once('geolocate', () => {
map.jumpTo({
center: [10, 20]
});
map.once('zoomend', () => {
t.ok(!geolocate._circleElement.style.width);
t.end();
});
map.zoomTo(10, {duration: 0});
});

geolocate._geolocateButton.dispatchEvent(click);
geolocation.send({latitude: 10, longitude: 20, accuracy: 700});
});

test('GeolocateControl accuracy circle radius matches reported accuracy', (t) => {
const map = createMap(t);
const geolocate = new GeolocateControl({
trackUserLocation: true,
showUserLocation: true,
});
map.addControl(geolocate);

const click = new window.Event('click');

geolocate.once('geolocate', () => {
t.ok(geolocate._accuracyCircleMarker._map, 'userLocation accuracy circle marker on map');
t.equal(geolocate._accuracy, 700);
map.jumpTo({
center: [10, 20]
});
map.once('zoomend', () => {
t.equal(geolocate._circleElement.style.width, '20px'); // 700m = 20px at zoom 10
map.once('zoomend', () => {
t.equal(geolocate._circleElement.style.width, '79px'); // 700m = 79px at zoom 12
t.end();
});
map.zoomTo(12, {duration: 0});
});
map.zoomTo(10, {duration: 0});
});

geolocate._geolocateButton.dispatchEvent(click);
geolocation.send({latitude: 10, longitude: 20, accuracy: 700});
});

test('GeolocateControl shown even if trackUserLocation = false', (t) => {
const map = createMap(t);
const geolocate = new GeolocateControl({
trackUserLocation: false,
showUserLocation: true,
showAccuracyCircle: true,
});
map.addControl(geolocate);

const click = new window.Event('click');

geolocate.once('geolocate', () => {
map.jumpTo({
center: [10, 20]
});
map.once('zoomend', () => {
t.ok(geolocate._circleElement.style.width);
t.end();
});
map.zoomTo(10, {duration: 0});
});

geolocate._geolocateButton.dispatchEvent(click);
geolocation.send({latitude: 10, longitude: 20, accuracy: 700});
});

0 comments on commit f4c9e47

Please sign in to comment.