diff --git a/src/geo/transform.js b/src/geo/transform.js index 997e3f00386..3e964da0cf4 100644 --- a/src/geo/transform.js +++ b/src/geo/transform.js @@ -4,7 +4,7 @@ import LngLat from './lng_lat'; import LngLatBounds from './lng_lat_bounds'; import MercatorCoordinate, {mercatorXfromLng, mercatorYfromLat, mercatorZfromAltitude} from './mercator_coordinate'; import Point from '@mapbox/point-geometry'; -import {wrap, clamp} from '../util/util'; +import {deepEqual, wrap, clamp} from '../util/util'; import {number as interpolate} from '../style-spec/util/interpolate'; import EXTENT from '../data/extent'; import {vec4, mat4, mat2, vec2} from 'gl-matrix'; @@ -565,16 +565,24 @@ class Transform { /** * Sets or clears the map's geographical constraints. * @param {LngLatBounds} bounds A {@link LngLatBounds} object describing the new geographic boundaries of the map. + * @returns {boolean} true if any changes were made; false otherwise */ - setMaxBounds(bounds?: LngLatBounds) { + setMaxBounds(bounds?: LngLatBounds): boolean { + const lngRange = bounds ? [bounds.getWest(), bounds.getEast()] : null; + const latRange = bounds ? [bounds.getSouth(), bounds.getNorth()] : [-this.maxValidLatitude, this.maxValidLatitude]; + + if (deepEqual(this.lngRange, lngRange) && deepEqual(this.latRange, latRange)) { + return false; + } + + this.lngRange = lngRange; + this.latRange = latRange; + if (bounds) { - this.lngRange = [bounds.getWest(), bounds.getEast()]; - this.latRange = [bounds.getSouth(), bounds.getNorth()]; this._constrain(); - } else { - this.lngRange = null; - this.latRange = [-this.maxValidLatitude, this.maxValidLatitude]; } + + return true; } /** diff --git a/src/source/source_cache.js b/src/source/source_cache.js index 525df8ad006..73bd875d24f 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -891,11 +891,12 @@ class SourceCache extends Evented { /** * Resets the value of a particular state key for a feature + * @returns {boolean} true if any changes were made; false otherwise * @private */ - removeFeatureState(sourceLayer?: string, featureId?: number | string, key?: string) { + removeFeatureState(sourceLayer?: string, featureId?: number | string, key?: string): boolean { sourceLayer = sourceLayer || '_geojsonTileLayer'; - this._state.removeFeatureState(sourceLayer, featureId, key); + return this._state.removeFeatureState(sourceLayer, featureId, key); } /** diff --git a/src/source/source_state.js b/src/source/source_state.js index 1cc4d03d99f..c87e7d3f436 100644 --- a/src/source/source_state.js +++ b/src/source/source_state.js @@ -52,11 +52,15 @@ class SourceFeatureState { } } } + } - removeFeatureState(sourceLayer: string, featureId?: number | string, key?: string) { + /** + * @returns {boolean} true if any changes were made; false otherwise + */ + removeFeatureState(sourceLayer: string, featureId?: number | string, key?: string): boolean { const sourceLayerDeleted = this.deletedStates[sourceLayer] === null; - if (sourceLayerDeleted) return; + if (sourceLayerDeleted) return false; const feature = String(featureId); @@ -80,6 +84,7 @@ class SourceFeatureState { this.deletedStates[sourceLayer] = null; } + return true; } getState(sourceLayer: string, featureId: number | string) { diff --git a/src/style/style.js b/src/style/style.js index 70523941871..f492559515f 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -487,7 +487,7 @@ class Style extends Evented { * @returns {boolean} true if any changes were made; false otherwise * @private */ - setState(nextState: StyleSpecification) { + setState(nextState: StyleSpecification): boolean { this._checkLoaded(); if (emitValidationErrors(this, validateStyle(nextState))) return false; @@ -559,7 +559,10 @@ class Style extends Evented { return this.imageManager.listImages(); } - addSource(id: string, source: SourceSpecification, options: StyleSetterOptions = {}) { + /** + * @returns {boolean} true if any changes were made; false otherwise + */ + addSource(id: string, source: SourceSpecification, options: StyleSetterOptions = {}): boolean { this._checkLoaded(); if (this.sourceCaches[id] !== undefined) { @@ -572,7 +575,7 @@ class Style extends Evented { const builtIns = ['vector', 'raster', 'geojson', 'video', 'image']; const shouldValidate = builtIns.indexOf(source.type) >= 0; - if (shouldValidate && this._validate(validateStyle.source, `sources.${id}`, source, null, options)) return; + if (shouldValidate && this._validate(validateStyle.source, `sources.${id}`, source, null, options)) return false; if (this.map && this.map._collectResourceTiming) (source: any).collectResourceTiming = true; const sourceCache = this.sourceCaches[id] = new SourceCache(id, source, this.dispatcher); @@ -585,15 +588,16 @@ class Style extends Evented { sourceCache.onAdd(this.map); this._changed = true; + return true; } /** * Remove a source from this stylesheet, given its id. * @param {string} id id of the source to remove * @throws {Error} if no source is found with the given ID - * @returns {Map} The {@link Map} object. + * @returns {boolean} true if any changes were made; false otherwise */ - removeSource(id: string) { + removeSource(id: string): boolean { this._checkLoaded(); if (this.sourceCaches[id] === undefined) { @@ -601,7 +605,8 @@ class Style extends Evented { } for (const layerId in this._layers) { if (this._layers[layerId].source === id) { - return this.fire(new ErrorEvent(new Error(`Source "${id}" cannot be removed while layer "${layerId}" is using it.`))); + this.fire(new ErrorEvent(new Error(`Source "${id}" cannot be removed while layer "${layerId}" is using it.`))); + return false; } } @@ -614,6 +619,7 @@ class Style extends Evented { if (sourceCache.onRemove) sourceCache.onRemove(this.map); this._changed = true; + return true; } /** @@ -647,22 +653,22 @@ class Style extends Evented { * @param {Object | CustomLayerInterface} layerObject The style layer to add. * @param {string} [before] ID of an existing layer to insert before * @param {Object} options Style setter options. - * @returns {Map} The {@link Map} object. + * @returns {boolean} true if any changes were made; false otherwise */ - addLayer(layerObject: LayerSpecification | CustomLayerInterface, before?: string, options: StyleSetterOptions = {}) { + addLayer(layerObject: LayerSpecification | CustomLayerInterface, before?: string, options: StyleSetterOptions = {}): boolean { this._checkLoaded(); const id = layerObject.id; if (this.getLayer(id)) { this.fire(new ErrorEvent(new Error(`Layer with id "${id}" already exists on this map`))); - return; + return false; } let layer; if (layerObject.type === 'custom') { - if (emitValidationErrors(this, validateCustomStyleLayer(layerObject))) return; + if (emitValidationErrors(this, validateCustomStyleLayer(layerObject))) return false; layer = createStyleLayer(layerObject); @@ -675,7 +681,7 @@ class Style extends Evented { // this layer is not in the style.layers array, so we pass an impossible array index if (this._validate(validateStyle.layer, - `layers.${id}`, layerObject, {arrayIndex: -1}, options)) return; + `layers.${id}`, layerObject, {arrayIndex: -1}, options)) return false; layer = createStyleLayer(layerObject); this._validateLayer(layer); @@ -687,7 +693,7 @@ class Style extends Evented { const index = before ? this._order.indexOf(before) : this._order.length; if (before && index === -1) { this.fire(new ErrorEvent(new Error(`Layer with id "${before}" does not exist on this map.`))); - return; + return false; } this._order.splice(index, 0, id); @@ -717,6 +723,7 @@ class Style extends Evented { if (layer.onAdd) { layer.onAdd(this.map); } + return true; } /** @@ -724,19 +731,20 @@ class Style extends Evented { * ID `before`, or appended if `before` is omitted. * @param {string} id ID of the layer to move * @param {string} [before] ID of an existing layer to insert before + * @returns {boolean} true if any changes were made; false otherwise */ - moveLayer(id: string, before?: string) { + moveLayer(id: string, before?: string): boolean { this._checkLoaded(); this._changed = true; const layer = this._layers[id]; if (!layer) { this.fire(new ErrorEvent(new Error(`The layer '${id}' does not exist in the map's style and cannot be moved.`))); - return; + return false; } if (id === before) { - return; + return false; } const index = this._order.indexOf(id); @@ -745,11 +753,12 @@ class Style extends Evented { const newIndex = before ? this._order.indexOf(before) : this._order.length; if (before && newIndex === -1) { this.fire(new ErrorEvent(new Error(`Layer with id "${before}" does not exist on this map.`))); - return; + return false; } this._order.splice(newIndex, 0, id); this._layerOrderChanged = true; + return true; } /** @@ -758,15 +767,16 @@ class Style extends Evented { * If no such layer exists, an `error` event is fired. * * @param {string} id id of the layer to remove + * @returns {boolean} true if any changes were made; false otherwise * @fires error */ - removeLayer(id: string) { + removeLayer(id: string): boolean { this._checkLoaded(); const layer = this._layers[id]; if (!layer) { this.fire(new ErrorEvent(new Error(`The layer '${id}' does not exist in the map's style and cannot be removed.`))); - return; + return false; } layer.setEventedParent(null); @@ -785,6 +795,7 @@ class Style extends Evented { if (layer.onRemove) { layer.onRemove(this.map); } + return true; } /** @@ -807,16 +818,19 @@ class Style extends Evented { return id in this._layers; } - setLayerZoomRange(layerId: string, minzoom: ?number, maxzoom: ?number) { + /** + * @returns {boolean} true if any changes were made; false otherwise + */ + setLayerZoomRange(layerId: string, minzoom: ?number, maxzoom: ?number): boolean { this._checkLoaded(); const layer = this.getLayer(layerId); if (!layer) { this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style and cannot have zoom extent.`))); - return; + return false; } - if (layer.minzoom === minzoom && layer.maxzoom === maxzoom) return; + if (layer.minzoom === minzoom && layer.maxzoom === maxzoom) return false; if (minzoom != null) { layer.minzoom = minzoom; @@ -825,33 +839,38 @@ class Style extends Evented { layer.maxzoom = maxzoom; } this._updateLayer(layer); + return true; } - setFilter(layerId: string, filter: ?FilterSpecification, options: StyleSetterOptions = {}) { + /** + * @returns {boolean} true if any changes were made; false otherwise + */ + setFilter(layerId: string, filter: ?FilterSpecification, options: StyleSetterOptions = {}): boolean { this._checkLoaded(); const layer = this.getLayer(layerId); if (!layer) { this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style and cannot be filtered.`))); - return; + return false; } if (deepEqual(layer.filter, filter)) { - return; + return false; } if (filter === null || filter === undefined) { layer.filter = undefined; this._updateLayer(layer); - return; + return true; } if (this._validate(validateStyle.filter, `layers.${layer.id}.filter`, filter, null, options)) { - return; + return false; } layer.filter = clone(filter); this._updateLayer(layer); + return true; } /** @@ -863,19 +882,23 @@ class Style extends Evented { return clone(this.getLayer(layer).filter); } - setLayoutProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}) { + /** + * @returns {boolean} true if any changes were made; false otherwise + */ + setLayoutProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}): boolean { this._checkLoaded(); const layer = this.getLayer(layerId); if (!layer) { this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style and cannot be styled.`))); - return; + return false; } - if (deepEqual(layer.getLayoutProperty(name), value)) return; + if (deepEqual(layer.getLayoutProperty(name), value)) return false; layer.setLayoutProperty(name, value, options); this._updateLayer(layer); + return true; } /** @@ -894,16 +917,19 @@ class Style extends Evented { return layer.getLayoutProperty(name); } - setPaintProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}) { + /** + * @returns {boolean} true if any changes were made; false otherwise + */ + setPaintProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}): boolean { this._checkLoaded(); const layer = this.getLayer(layerId); if (!layer) { this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style and cannot be styled.`))); - return; + return false; } - if (deepEqual(layer.getPaintProperty(name), value)) return; + if (deepEqual(layer.getPaintProperty(name), value)) return false; const requiresRelayout = layer.setPaintProperty(name, value, options); if (requiresRelayout) { @@ -912,13 +938,17 @@ class Style extends Evented { this._changed = true; this._updatedPaintProps[layerId] = true; + return true; } getPaintProperty(layer: string, name: string) { return this.getLayer(layer).getPaintProperty(name); } - setFeatureState(target: { source: string; sourceLayer?: string; id: string | number; }, state: Object) { + /** + * @returns {boolean} true if any changes were made; false otherwise + */ + setFeatureState(target: { source: string; sourceLayer?: string; id: string | number; }, state: Object): boolean { this._checkLoaded(); const sourceId = target.source; const sourceLayer = target.sourceLayer; @@ -926,32 +956,39 @@ class Style extends Evented { if (sourceCache === undefined) { this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`))); - return; + return false; } const sourceType = sourceCache.getSource().type; if (sourceType === 'geojson' && sourceLayer) { this.fire(new ErrorEvent(new Error(`GeoJSON sources cannot have a sourceLayer parameter.`))); - return; + return false; } if (sourceType === 'vector' && !sourceLayer) { this.fire(new ErrorEvent(new Error(`The sourceLayer parameter must be provided for vector source types.`))); - return; + return false; } if (target.id === undefined) { this.fire(new ErrorEvent(new Error(`The feature id parameter must be provided.`))); + return false; } + if (deepEqual(sourceCache.getFeatureState(sourceLayer, target.id), state)) return false; + sourceCache.setFeatureState(sourceLayer, target.id, state); + return true; } - removeFeatureState(target: { source: string; sourceLayer?: string; id?: string | number; }, key?: string) { + /** + * @returns {boolean} true if any changes were made; false otherwise + */ + removeFeatureState(target: { source: string; sourceLayer?: string; id?: string | number; }, key?: string): boolean { this._checkLoaded(); const sourceId = target.source; const sourceCache = this.sourceCaches[sourceId]; if (sourceCache === undefined) { this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`))); - return; + return false; } const sourceType = sourceCache.getSource().type; @@ -959,15 +996,15 @@ class Style extends Evented { if (sourceType === 'vector' && !sourceLayer) { this.fire(new ErrorEvent(new Error(`The sourceLayer parameter must be provided for vector source types.`))); - return; + return false; } if (key && (typeof target.id !== 'string' && typeof target.id !== 'number')) { this.fire(new ErrorEvent(new Error(`A feature id is required to remove its specific state property.`))); - return; + return false; } - sourceCache.removeFeatureState(sourceLayer, target.id, key); + return sourceCache.removeFeatureState(sourceLayer, target.id, key); } getFeatureState(target: { source: string; sourceLayer?: string; id: string | number; }) { @@ -1179,7 +1216,10 @@ class Style extends Evented { return this.light.getLight(); } - setLight(lightOptions: LightSpecification, options: StyleSetterOptions = {}) { + /** + * @returns {boolean} true if any changes were made; false otherwise + */ + setLight(lightOptions: LightSpecification, options: StyleSetterOptions = {}): boolean { this._checkLoaded(); const light = this.light.getLight(); @@ -1190,7 +1230,7 @@ class Style extends Evented { break; } } - if (!_update) return; + if (!_update) return false; const parameters = { now: browser.now(), @@ -1202,6 +1242,7 @@ class Style extends Evented { this.light.setLight(lightOptions, options); this.light.updateTransitions(parameters); + return true; } _validate(validate: Validator, key: string, value: any, props: any, options: { validate?: boolean } = {}) { diff --git a/src/ui/camera.js b/src/ui/camera.js index f0a922acb8a..a906470fd4e 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -710,9 +710,11 @@ class Camera extends Evented { this.stop(); const tr = this.transform; - let zoomChanged = false, + let centerChanged = false, + zoomChanged = false, bearingChanged = false, - pitchChanged = false; + pitchChanged = false, + paddingChanged = false; if ('zoom' in options && tr.zoom !== +options.zoom) { zoomChanged = true; @@ -720,7 +722,11 @@ class Camera extends Evented { } if (options.center !== undefined) { - tr.center = LngLat.convert(options.center); + const lngLatCenter = LngLat.convert(options.center); + if (tr.center.lng !== lngLatCenter.lng || tr.center.lat !== lngLatCenter.lat) { + centerChanged = true; + tr.center = lngLatCenter; + } } if ('bearing' in options && tr.bearing !== +options.bearing) { @@ -734,9 +740,15 @@ class Camera extends Evented { } if (options.padding != null && !tr.isPaddingEqual(options.padding)) { + paddingChanged = true; tr.padding = options.padding; } + const changed = centerChanged || zoomChanged || bearingChanged || pitchChanged || paddingChanged; + if (!changed) { + return this; + } + this.fire(new Event('movestart', eventData)) .fire(new Event('move', eventData)); diff --git a/src/ui/map.js b/src/ui/map.js index 851236d15bb..dc870a937e1 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -656,7 +656,9 @@ class Map extends Camera { * map.setMaxBounds(bounds); */ setMaxBounds(bounds: LngLatBoundsLike) { - this.transform.setMaxBounds(LngLatBounds.convert(bounds)); + if (!this.transform.setMaxBounds(LngLatBounds.convert(bounds))) { + return this; + } return this._update(); } @@ -681,6 +683,10 @@ class Map extends Camera { minZoom = minZoom === null || minZoom === undefined ? defaultMinZoom : minZoom; if (minZoom >= defaultMinZoom && minZoom <= this.transform.maxZoom) { + if (minZoom === this.transform.minZoom) { + return this; + } + this.transform.minZoom = minZoom; this._update(); @@ -716,6 +722,10 @@ class Map extends Camera { maxZoom = maxZoom === null || maxZoom === undefined ? defaultMaxZoom : maxZoom; if (maxZoom >= this.transform.minZoom) { + if (maxZoom === this.transform.maxZoom) { + return this; + } + this.transform.maxZoom = maxZoom; this._update(); @@ -753,6 +763,10 @@ class Map extends Camera { } if (minPitch >= defaultMinPitch && minPitch <= this.transform.maxPitch) { + if (minPitch === this.transform.minPitch) { + return this; + } + this.transform.minPitch = minPitch; this._update(); @@ -788,6 +802,10 @@ class Map extends Camera { } if (maxPitch >= this.transform.minPitch) { + if (maxPitch === this.transform.maxPitch) { + return this; + } + this.transform.maxPitch = maxPitch; this._update(); @@ -834,6 +852,9 @@ class Map extends Camera { * @see [Render world copies](https://docs.mapbox.com/mapbox-gl-js/example/render-world-copies/) */ setRenderWorldCopies(renderWorldCopies?: ?boolean) { + if (renderWorldCopies === this.transform.renderWorldCopies) { + return this; + } this.transform.renderWorldCopies = renderWorldCopies; return this._update(); } @@ -1480,7 +1501,9 @@ class Map extends Camera { */ addSource(id: string, source: SourceSpecification) { this._lazyInitEmptyStyle(); - this.style.addSource(id, source); + if (!this.style.addSource(id, source)) { + return this; + } return this._update(true); } @@ -1546,7 +1569,9 @@ class Map extends Camera { * map.removeSource('bathymetry-data'); */ removeSource(id: string) { - this.style.removeSource(id); + if (!this.style.removeSource(id)) { + return this; + } return this._update(true); } @@ -1885,7 +1910,9 @@ class Map extends Camera { */ addLayer(layer: LayerSpecification | CustomLayerInterface, beforeId?: string) { this._lazyInitEmptyStyle(); - this.style.addLayer(layer, beforeId); + if (!this.style.addLayer(layer, beforeId)) { + return this; + } return this._update(true); } @@ -1901,7 +1928,9 @@ class Map extends Camera { * map.moveLayer('polygon', 'country-label'); */ moveLayer(id: string, beforeId?: string) { - this.style.moveLayer(id, beforeId); + if (!this.style.moveLayer(id, beforeId)) { + return this; + } return this._update(true); } @@ -1919,7 +1948,9 @@ class Map extends Camera { * if (map.getLayer('state-data')) map.removeLayer('state-data'); */ removeLayer(id: string) { - this.style.removeLayer(id); + if (!this.style.removeLayer(id)) { + return this; + } return this._update(true); } @@ -1961,7 +1992,9 @@ class Map extends Camera { * */ setLayerZoomRange(layerId: string, minzoom: number, maxzoom: number) { - this.style.setLayerZoomRange(layerId, minzoom, maxzoom); + if (!this.style.setLayerZoomRange(layerId, minzoom, maxzoom)) { + return this; + } return this._update(true); } @@ -1999,7 +2032,9 @@ class Map extends Camera { * @see Tutorial: [Show changes over time](https://docs.mapbox.com/help/tutorials/show-changes-over-time/) */ setFilter(layerId: string, filter: ?FilterSpecification, options: StyleSetterOptions = {}) { - this.style.setFilter(layerId, filter, options); + if (!this.style.setFilter(layerId, filter, options)) { + return this; + } return this._update(true); } @@ -2030,7 +2065,9 @@ class Map extends Camera { * @see [Create a draggable point](https://www.mapbox.com/mapbox-gl-js/example/drag-a-point/) */ setPaintProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}) { - this.style.setPaintProperty(layerId, name, value, options); + if (!this.style.setPaintProperty(layerId, name, value, options)) { + return this; + } return this._update(true); } @@ -2059,7 +2096,9 @@ class Map extends Camera { * @see [Show and hide layers](https://docs.mapbox.com/mapbox-gl-js/example/toggle-layers/) */ setLayoutProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}) { - this.style.setLayoutProperty(layerId, name, value, options); + if (!this.style.setLayoutProperty(layerId, name, value, options)) { + return this; + } return this._update(true); } @@ -2087,7 +2126,9 @@ class Map extends Camera { */ setLight(light: LightSpecification, options: StyleSetterOptions = {}) { this._lazyInitEmptyStyle(); - this.style.setLight(light, options); + if (!this.style.setLight(light, options)) { + return this; + } return this._update(true); } @@ -2140,7 +2181,9 @@ class Map extends Camera { * @see Tutorial: [Create interactive hover effects with Mapbox GL JS](https://docs.mapbox.com/help/tutorials/create-interactive-hover-effects-with-mapbox-gl-js/) */ setFeatureState(feature: { source: string; sourceLayer?: string; id: string | number; }, state: Object) { - this.style.setFeatureState(feature, state); + if (!this.style.setFeatureState(feature, state)) { + return this; + } return this._update(); } @@ -2192,7 +2235,9 @@ class Map extends Camera { * */ removeFeatureState(target: { source: string; sourceLayer?: string; id?: string | number; }, key?: string) { - this.style.removeFeatureState(target, key); + if (!this.style.removeFeatureState(target, key)) { + return this; + } return this._update(); } diff --git a/test/unit/ui/control/geolocate.test.js b/test/unit/ui/control/geolocate.test.js index 6888edd87c8..92bc68e3d49 100644 --- a/test/unit/ui/control/geolocate.test.js +++ b/test/unit/ui/control/geolocate.test.js @@ -424,7 +424,7 @@ test('GeolocateControl switches to BACKGROUND state on map manipulation', (t) => geolocate.once('geolocate', () => { t.equal(geolocate._watchState, 'ACTIVE_LOCK'); map.jumpTo({ - center: [0, 0] + center: [10, 5] }); t.equal(geolocate._watchState, 'BACKGROUND'); t.end();