diff --git a/src/ui/control/geolocate_control.js b/src/ui/control/geolocate_control.js index e6df08589c2..eca7accc8df 100644 --- a/src/ui/control/geolocate_control.js +++ b/src/ui/control/geolocate_control.js @@ -137,7 +137,57 @@ class GeolocateControl extends Evented { this._map = (undefined: any); } + _isOutOfMapMaxBounds(position: Position) { + const bounds = this._map.getMaxBounds(); + const coordinates = position.coords; + + return bounds && ( + coordinates.longitude < bounds.getWest() || + coordinates.longitude > bounds.getEast() || + coordinates.latitude < bounds.getSouth() || + coordinates.latitude > bounds.getNorth() + ); + } + + _setErrorState() { + switch (this._watchState) { + case 'WAITING_ACTIVE': + this._watchState = 'ACTIVE_ERROR'; + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active-error'); + break; + case 'ACTIVE_LOCK': + this._watchState = 'ACTIVE_ERROR'; + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active-error'); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); + // turn marker grey + break; + case 'BACKGROUND': + this._watchState = 'BACKGROUND_ERROR'; + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background'); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-background-error'); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); + // turn marker grey + break; + case 'ACTIVE_ERROR': + break; + default: + assert(false, `Unexpected watchState ${this._watchState}`); + } + } + _onSuccess(position: Position) { + if (this._isOutOfMapMaxBounds(position)) { + this._setErrorState(); + + this.fire(new Event('outofmaxbounds', position)); + this._updateMarker(); + this._finish(); + + return; + } + if (this.options.trackUserLocation) { // keep a record of the position so that if the state is BACKGROUND and the user // clicks the button, we can move to ACTIVE_LOCK immediately without waiting for @@ -218,31 +268,7 @@ class GeolocateControl extends Evented { this._clearWatch(); } } else { - switch (this._watchState) { - case 'WAITING_ACTIVE': - this._watchState = 'ACTIVE_ERROR'; - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active-error'); - break; - case 'ACTIVE_LOCK': - this._watchState = 'ACTIVE_ERROR'; - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active-error'); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); - // turn marker grey - break; - case 'BACKGROUND': - this._watchState = 'BACKGROUND_ERROR'; - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background'); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-background-error'); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); - // turn marker grey - break; - case 'ACTIVE_ERROR': - break; - default: - assert(false, `Unexpected watchState ${this._watchState}`); - } + this._setErrorState(); } } @@ -456,6 +482,16 @@ export default GeolocateControl; * */ +/** + * Fired on each Geolocation API position update which returned as success but user position is out of map maxBounds. + * + * @event outofmaxbounds + * @memberof GeolocateControl + * @instance + * @property {Position} data The returned [Position](https://developer.mozilla.org/en-US/docs/Web/API/Position) object from the callback in [Geolocation.getCurrentPosition()](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition) or [Geolocation.watchPosition()](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/watchPosition). + * + */ + /** * Fired when the Geolocate Control changes to the active lock state, which happens either upon first obtaining a successful Geolocation API position for the user (a geolocate event will follow), or the user clicks the geolocate button when in the background state which uses the last known position to recenter the map and enter active lock state (no geolocate event will follow unless the users's location changes). * diff --git a/test/unit/ui/control/geolocate.test.js b/test/unit/ui/control/geolocate.test.js index beda845f55e..523e96076e7 100644 --- a/test/unit/ui/control/geolocate.test.js +++ b/test/unit/ui/control/geolocate.test.js @@ -48,6 +48,52 @@ test('GeolocateControl error event', (t) => { geolocation.sendError({code: 2, message: 'error message'}); }); +test('GeolocateControl outofmaxbounds event in active lock state', (t) => { + t.plan(5); + + const map = createMap(t); + const geolocate = new GeolocateControl(); + map.addControl(geolocate); + map.setMaxBounds([[0, 0], [10, 10]]); + geolocate._watchState = 'ACTIVE_LOCK'; + + const click = new window.Event('click'); + + geolocate.on('outofmaxbounds', (position) => { + t.equal(geolocate._watchState, 'ACTIVE_ERROR', 'geolocate state'); + t.equal(position.coords.latitude, 10, 'geolocate position latitude'); + t.equal(position.coords.longitude, 20, 'geolocate position longitude'); + t.equal(position.coords.accuracy, 3, 'geolocate position accuracy'); + t.equal(position.timestamp, 4, 'geolocate timestamp'); + t.end(); + }); + geolocate._geolocateButton.dispatchEvent(click); + geolocation.send({latitude: 10, longitude: 20, accuracy: 3, timestamp: 4}); +}); + +test('GeolocateControl outofmaxbounds event in background state', (t) => { + t.plan(5); + + const map = createMap(t); + const geolocate = new GeolocateControl(); + map.addControl(geolocate); + map.setMaxBounds([[0, 0], [10, 10]]); + geolocate._watchState = 'BACKGROUND'; + + const click = new window.Event('click'); + + geolocate.on('outofmaxbounds', (position) => { + t.equal(geolocate._watchState, 'BACKGROUND_ERROR', 'geolocate state'); + t.equal(position.coords.latitude, 10, 'geolocate position latitude'); + t.equal(position.coords.longitude, 20, 'geolocate position longitude'); + t.equal(position.coords.accuracy, 3, 'geolocate position accuracy'); + t.equal(position.timestamp, 4, 'geolocate timestamp'); + t.end(); + }); + geolocate._geolocateButton.dispatchEvent(click); + geolocation.send({latitude: 10, longitude: 20, accuracy: 3, timestamp: 4}); +}); + test('GeolocateControl geolocate event', (t) => { t.plan(4);