Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add circle to GeolocateControl showing accuracy of position #9181

Closed
wants to merge 46 commits into from
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
8d05a1d
add circle to GeolocateControl showing accuracy of position
Meekohi Jan 14, 2020
d93e091
number not Number for typechecks
Meekohi Jan 14, 2020
d694f36
looks slightly better with the circle underneath
Meekohi Jan 14, 2020
b290bce
clarify div id
Meekohi Jan 15, 2020
d13e371
clarify div id
Meekohi Jan 15, 2020
036c2e0
onZoom not onMove
Meekohi Jan 15, 2020
f77d16d
docs
Meekohi Jan 15, 2020
e1980e3
Update package version to 1.8.0-dev (#9190)
ahk Jan 15, 2020
b2c4b04
Prefer line-pattern over line-dasharray in accordance with documentat…
kkaefer Jan 15, 2020
a5ab875
rename, extra safety checks
Meekohi Jan 15, 2020
85a11c8
a test
Meekohi Jan 15, 2020
d915500
test for showAccuracy toggle
Meekohi Jan 15, 2020
bbcfbe5
typo
Meekohi Jan 15, 2020
954bb99
pitch with map
Meekohi Jan 15, 2020
ff03457
Remove Object.values usage that was causing IE11 to fail (#9193)
Jan 16, 2020
51d75fc
LOD support for tile coverage (#8975)
mpulkki-mapbox Jan 17, 2020
2af4c8b
pull updateCircle back into GeolocateControl
Meekohi Jan 17, 2020
87f3393
fix promoteId spec definitions (#9212)
mourner Jan 21, 2020
a13efc0
upgrade earcut to v2.2.2 (#9214)
mourner Jan 21, 2020
28603d1
Fix promoteId for line layers (#9210)
mourner Jan 22, 2020
980d1fb
Fix line distances breaking gradient across tile boundaries (#9220)
karimnaaji Jan 23, 2020
b7e8fb3
Update image expression SDK support table (#9228)
Jan 24, 2020
8cd474e
Refactor style._load function, move sprite loading to a private metho…
webdeb Jan 24, 2020
8c9ace1
[tests][tile mode] Add left-top-right-buttom-offset-tile-map-mode test
pozdnyakov Jan 27, 2020
be4f189
Reduce size of line atlas by removing unused channels (#9232)
karimnaaji Jan 27, 2020
99bfc7f
Fix a bug where lines with duplicate endpoint disappear on z18+ (#9218)
mourner Jan 27, 2020
77cfc5c
Canonicalize Mapbox tile URLs from inline TileJSON as well (#9217)
kkaefer Jan 28, 2020
1eed2ae
Hide glyphs behind the camera (#9229)
mpulkki-mapbox Jan 29, 2020
fa59cf3
Prevent empty buffers from being created for debug data (#9237)
karimnaaji Jan 30, 2020
5d8ef1a
refactor LngLat distance calculations (#9202)
Meekohi Jan 31, 2020
03c4339
add circle to GeolocateControl showing accuracy of position
Meekohi Jan 14, 2020
3574318
number not Number for typechecks
Meekohi Jan 14, 2020
1f76684
looks slightly better with the circle underneath
Meekohi Jan 14, 2020
b3ed503
clarify div id
Meekohi Jan 15, 2020
ae8bd00
clarify div id
Meekohi Jan 15, 2020
d8a5e0d
onZoom not onMove
Meekohi Jan 15, 2020
e2da6ab
docs
Meekohi Jan 15, 2020
0c45a27
rename, extra safety checks
Meekohi Jan 15, 2020
5fd231f
a test
Meekohi Jan 15, 2020
412d257
test for showAccuracy toggle
Meekohi Jan 15, 2020
790732a
typo
Meekohi Jan 15, 2020
64e8312
pitch with map
Meekohi Jan 15, 2020
125c623
pull updateCircle back into GeolocateControl
Meekohi Jan 17, 2020
b1a67c6
Merge branch 'master' of github.com:Meekohi/mapbox-gl-js
Meekohi Jan 31, 2020
6f7f2ef
refactor distance calculations
Meekohi Jan 31, 2020
136a291
rename param and better docs
Meekohi Jan 31, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
54 changes: 51 additions & 3 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,
showAccuracy?: boolean,
showUserLocation?: boolean
};

Expand All @@ -28,6 +29,7 @@ const defaultOptions: Options = {
maxZoom: 15
},
trackUserLocation: false,
showAccuracy: 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.showAccuracy=true] If trackUserLocation is true, by default 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.
Meekohi marked this conversation as resolved.
Show resolved Hide resolved
* @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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given both markers will always have the same location, do you think it would be better to combine this with the _userLocationDotMarker to have a single Marker? Still having it optional and still using two separate classes for styling.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I think the code will be more complicated trying to force both of these objects to use one Marker: that would introduce some container element that will need to be styled carefully to avoid breaking anything, some additional if/else around the flag, and I'd be worried that might introduce some other breaking change related to code I'm not familiar with, whereas I'm pretty confident the current design should be independent. (Obvious caveat I've only been looking at this codebase for 2 days so... let me know ;D)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably prefer a single Marker since their location is always going to be the same, but I can also see how two Markers is easier to implement and in some ways makes the code easier to manage (though single Marker also makes the Marker update code cleaner).

That said, I'm happy with either option.

_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.showAccuracy && 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,21 @@ 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;
this._onZoom();
} else {
this._userLocationDotMarker.remove();
this._accuracyCircleMarker.remove();
}
}

_onZoom() {
if (this.options.trackUserLocation && this.options.showAccuracy) {
assert(this._circleElement);
updateCircleRadius(this._map, this._circleElement, this._accuracy);
}
}

Expand Down Expand Up @@ -326,9 +350,11 @@ class GeolocateControl extends Evented {
// when showUserLocation is enabled, keep the Geolocate button disabled until the device location marker is setup on the map
if (this.options.showUserLocation) {
this._dotElement = DOM.create('div', 'mapboxgl-user-location-dot');

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

this._circleElement = DOM.create('div', 'mapboxgl-user-location-accuracy-circle');
this._accuracyCircleMarker = new Marker(this._circleElement);
Meekohi marked this conversation as resolved.
Show resolved Hide resolved

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

Expand Down Expand Up @@ -476,6 +502,28 @@ class GeolocateControl extends Evented {

export default GeolocateControl;

function getDistance(latlng1, latlng2) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this needs to be addressed within this PR, but eventually we should eliminate the duplication between this and

function getDistance(latlng1, latlng2) {
// Uses spherical law of cosines approximation.
const R = 6371000;
const rad = Math.PI / 180,
lat1 = latlng1.lat * rad,
lat2 = latlng2.lat * rad,
a = Math.sin(lat1) * Math.sin(lat2) +
Math.cos(lat1) * Math.cos(lat2) * Math.cos((latlng2.lng - latlng1.lng) * rad);
const maxMeters = R * Math.acos(Math.min(a, 1));
return maxMeters;
}
and in turn hopefully address #8777.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah agreed it's a copy/paste job -- good spot to refactor these into some helpers in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll likely forget about it later so let's eliminate duplication now — I think the best place to put it is LngLat distanceTo(lngLat) method. This would follow the same convention as Leaflet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened a separate PR for this: #9202

// Uses spherical law of cosines approximation.
const R = 6371000;

const rad = Math.PI / 180,
lat1 = latlng1.lat * rad,
lat2 = latlng2.lat * rad,
a = Math.sin(lat1) * Math.sin(lat2) +
Math.cos(lat1) * Math.cos(lat2) * Math.cos((latlng2.lng - latlng1.lng) * rad);

const maxMeters = R * Math.acos(Math.min(a, 1));
return maxMeters;
}

function updateCircleRadius(map, circle, accuracy) {
const y = map._container.clientHeight / 2;
const metersPerPixel = getDistance(map.unproject([0, y]), map.unproject([1, y]));
const circleDiameter = Math.ceil(2.0 * accuracy / metersPerPixel);
circle.style.width = `${circleDiameter}px`;
circle.style.height = `${circleDiameter}px`;
}

Meekohi marked this conversation as resolved.
Show resolved Hide resolved
/* Geolocate Control Watch States
* This is the private state of the control.
*
Expand Down
57 changes: 57 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,60 @@ 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 showAccuracy = false', (t) => {
const map = createMap(t);
const geolocate = new GeolocateControl({
trackUserLocation: true,
showUserLocation: true,
showAccuracy: 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});
});