diff --git a/src/data/feature_index.js b/src/data/feature_index.js index 85bc7782385..2937d314d3c 100644 --- a/src/data/feature_index.js +++ b/src/data/feature_index.js @@ -10,12 +10,13 @@ import DictionaryCoder from '../util/dictionary_coder'; import vt from '@mapbox/vector-tile'; import Protobuf from 'pbf'; import GeoJSONFeature from '../util/vectortile_to_geojson'; -import {arraysIntersect} from '../util/util'; +import {arraysIntersect, mapObject} from '../util/util'; import {OverscaledTileID} from '../source/tile_id'; import {register} from '../util/web_worker_transfer'; import EvaluationParameters from '../style/evaluation_parameters'; import SourceFeatureState from '../source/source_state'; import {polygonIntersectsBox} from '../util/intersection_tests'; +import {PossiblyEvaluated} from '../style/properties'; import type StyleLayer from '../style/style_layer'; import type {FeatureFilter} from '../style-spec/feature_filter'; @@ -35,6 +36,7 @@ type QueryParameters = { params: { filter: FilterSpecification, layers: Array, + availableImages: Array } } @@ -101,7 +103,7 @@ class FeatureIndex { } // Finds non-symbol features in this tile at a particular position. - query(args: QueryParameters, styleLayers: {[_: string]: StyleLayer}, sourceFeatureState: SourceFeatureState): {[_: string]: Array<{ featureIndex: number, feature: GeoJSONFeature }>} { + query(args: QueryParameters, styleLayers: {[_: string]: StyleLayer}, serializedLayers: {[_: string]: Object}, sourceFeatureState: SourceFeatureState): {[_: string]: Array<{ featureIndex: number, feature: GeoJSONFeature }>} { this.loadVTLayers(); const params = args.params || {}, @@ -145,16 +147,15 @@ class FeatureIndex { match.featureIndex, filter, params.layers, + params.availableImages, styleLayers, - (feature: VectorTileFeature, styleLayer: StyleLayer, id: string | number | void) => { + serializedLayers, + sourceFeatureState, + (feature: VectorTileFeature, styleLayer: StyleLayer, featureState: Object) => { if (!featureGeometry) { featureGeometry = loadGeometry(feature); } - let featureState = {}; - if (id !== undefined) { - // `feature-state` expression evaluation requires feature state to be available - featureState = sourceFeatureState.getState(styleLayer.sourceLayer || '_geojsonTileLayer', id); - } + return styleLayer.queryIntersectsFeature(queryGeometry, feature, featureState, featureGeometry, this.z, args.transform, pixelsToTileUnits, args.pixelPosMatrix); } ); @@ -170,8 +171,11 @@ class FeatureIndex { featureIndex: number, filter: FeatureFilter, filterLayerIDs: Array, + availableImages: Array, styleLayers: {[_: string]: StyleLayer}, - intersectionTest?: (feature: VectorTileFeature, styleLayer: StyleLayer, id: string | number | void) => boolean | number) { + serializedLayers: {[_: string]: Object}, + sourceFeatureState?: SourceFeatureState, + intersectionTest?: (feature: VectorTileFeature, styleLayer: StyleLayer, featureState: Object, id: string | number | void) => boolean | number) { const layerIDs = this.bucketLayerIDs[bucketIndex]; if (filterLayerIDs && !arraysIntersect(filterLayerIDs, layerIDs)) @@ -194,16 +198,28 @@ class FeatureIndex { } const styleLayer = styleLayers[layerID]; + if (!styleLayer) continue; - const intersectionZ = !intersectionTest || intersectionTest(feature, styleLayer, id); + let featureState = {}; + if (id !== undefined && sourceFeatureState) { + // `feature-state` expression evaluation requires feature state to be available + featureState = sourceFeatureState.getState(styleLayer.sourceLayer || '_geojsonTileLayer', id); + } + + const serializedLayer = serializedLayers[layerID]; + + serializedLayer.paint = evaluateProperties(serializedLayer.paint, styleLayer.paint, feature, featureState, availableImages); + serializedLayer.layout = evaluateProperties(serializedLayer.layout, styleLayer.layout, feature, featureState, availableImages); + + const intersectionZ = !intersectionTest || intersectionTest(feature, styleLayer, featureState); if (!intersectionZ) { // Only applied for non-symbol features continue; } const geojsonFeature = new GeoJSONFeature(feature, this.z, this.x, this.y, id); - (geojsonFeature: any).layer = styleLayer.serialize(); + (geojsonFeature: any).layer = serializedLayer; let layerResult = result[layerID]; if (layerResult === undefined) { layerResult = result[layerID] = []; @@ -215,10 +231,12 @@ class FeatureIndex { // Given a set of symbol indexes that have already been looked up, // return a matching set of GeoJSONFeatures lookupSymbolFeatures(symbolFeatureIndexes: Array, + serializedLayers: {[string]: StyleLayer}, bucketIndex: number, sourceLayerIndex: number, filterSpec: FilterSpecification, filterLayerIDs: Array, + availableImages: Array, styleLayers: {[_: string]: StyleLayer}) { const result = {}; this.loadVTLayers(); @@ -233,7 +251,9 @@ class FeatureIndex { symbolFeatureIndex, filter, filterLayerIDs, - styleLayers + availableImages, + styleLayers, + serializedLayers ); } @@ -269,6 +289,13 @@ register( export default FeatureIndex; +function evaluateProperties(serializedProperties, styleLayerProperties, feature, featureState, availableImages) { + return mapObject(serializedProperties, (property, key) => { + const prop = styleLayerProperties instanceof PossiblyEvaluated ? styleLayerProperties.get(key) : null; + return prop && prop.evaluate ? prop.evaluate(feature, featureState, availableImages) : prop; + }); +} + function getBounds(geometry: Array) { let minX = Infinity; let minY = Infinity; diff --git a/src/source/query_features.js b/src/source/query_features.js index c37cef012ed..c7017b577be 100644 --- a/src/source/query_features.js +++ b/src/source/query_features.js @@ -40,23 +40,23 @@ function queryIncludes3DLayer(layers?: Array, styleLayers: {[_: string]: export function queryRenderedFeatures(sourceCache: SourceCache, styleLayers: {[_: string]: StyleLayer}, + serializedLayers: {[_: string]: Object}, queryGeometry: Array, - params: { filter: FilterSpecification, layers: Array }, + params: { filter: FilterSpecification, layers: Array, availableImages: Array }, transform: Transform) { const has3DLayer = queryIncludes3DLayer(params && params.layers, styleLayers, sourceCache.id); - const maxPitchScaleFactor = transform.maxPitchScaleFactor(); const tilesIn = sourceCache.tilesIn(queryGeometry, maxPitchScaleFactor, has3DLayer); tilesIn.sort(sortTilesIn); - const renderedFeatureLayers = []; for (const tileIn of tilesIn) { renderedFeatureLayers.push({ wrappedTileID: tileIn.tileID.wrapped().key, queryResults: tileIn.tile.queryRenderedFeatures( styleLayers, + serializedLayers, sourceCache._state, tileIn.queryGeometry, tileIn.cameraQueryGeometry, @@ -86,9 +86,10 @@ export function queryRenderedFeatures(sourceCache: SourceCache, } export function queryRenderedSymbols(styleLayers: {[_: string]: StyleLayer}, + serializedLayers: {[_: string]: StyleLayer}, sourceCaches: {[_: string]: SourceCache}, queryGeometry: Array, - params: { filter: FilterSpecification, layers: Array }, + params: { filter: FilterSpecification, layers: Array, availableImages: Array }, collisionIndex: CollisionIndex, retainedQueryData: {[_: number]: RetainedQueryData}) { const result = {}; @@ -102,10 +103,12 @@ export function queryRenderedSymbols(styleLayers: {[_: string]: StyleLayer}, for (const queryData of bucketQueryData) { const bucketSymbols = queryData.featureIndex.lookupSymbolFeatures( renderedSymbols[queryData.bucketInstanceId], + serializedLayers, queryData.bucketIndex, queryData.sourceLayerIndex, params.filter, params.layers, + params.availableImages, styleLayers); for (const layerID in bucketSymbols) { diff --git a/src/source/tile.js b/src/source/tile.js index 4b4f5866cc6..c0e255a6c3b 100644 --- a/src/source/tile.js +++ b/src/source/tile.js @@ -265,11 +265,12 @@ class Tile { // Queries non-symbol features rendered for this tile. // Symbol features are queried globally queryRenderedFeatures(layers: {[_: string]: StyleLayer}, + serializedLayers: {[string]: Object}, sourceFeatureState: SourceFeatureState, queryGeometry: Array, cameraQueryGeometry: Array, scale: number, - params: { filter: FilterSpecification, layers: Array }, + params: { filter: FilterSpecification, layers: Array, availableImages: Array }, transform: Transform, maxPitchScaleFactor: number, pixelPosMatrix: Float32Array): {[_: string]: Array<{ featureIndex: number, feature: GeoJSONFeature }>} { @@ -285,7 +286,7 @@ class Tile { transform, params, queryPadding: this.queryPadding * maxPitchScaleFactor - }, layers, sourceFeatureState); + }, layers, serializedLayers, sourceFeatureState); } querySourceFeatures(result: Array, params: any) { diff --git a/src/style/style.js b/src/style/style.js index fdf3274d95c..eb8137d2a0b 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -114,6 +114,7 @@ class Style extends Evented { _request: ?Cancelable; _spriteRequest: ?Cancelable; _layers: {[_: string]: StyleLayer}; + _serializedLayers: {[_: string]: Object}; _order: Array; sourceCaches: {[_: string]: SourceCache}; zoomHistory: ZoomHistory; @@ -126,6 +127,7 @@ class Style extends Evented { _changedImages: {[_: string]: true}; _updatedPaintProps: {[layer: string]: true}; _layerOrderChanged: boolean; + _availableImages: Array; crossTileSymbolIndex: CrossTileSymbolIndex; pauseablePlacement: PauseablePlacement; @@ -149,10 +151,12 @@ class Style extends Evented { this.crossTileSymbolIndex = new CrossTileSymbolIndex(); this._layers = {}; + this._serializedLayers = {}; this._order = []; this.sourceCaches = {}; this.zoomHistory = new ZoomHistory(); this._loaded = false; + this._availableImages = []; this._resetUpdates(); @@ -262,10 +266,12 @@ class Style extends Evented { this._order = layers.map((layer) => layer.id); this._layers = {}; + this._serializedLayers = {}; for (let layer of layers) { layer = createStyleLayer(layer); layer.setEventedParent(this, {layer: {id: layer.id}}); this._layers[layer.id] = layer; + this._serializedLayers[layer.id] = layer.serialize(); } this.dispatcher.broadcast('setLayers', this._serializeLayers(this._order)); @@ -287,7 +293,8 @@ class Style extends Evented { } this.imageManager.setLoaded(true); - this.dispatcher.broadcast('setImages', this.imageManager.listImages()); + this._availableImages = this.imageManager.listImages(); + this.dispatcher.broadcast('setImages', this._availableImages); this.fire(new Event('data', {dataType: 'style'})); }); } @@ -408,11 +415,10 @@ class Style extends Evented { this.sourceCaches[sourceId].used = false; } - const availableImages = this.imageManager.listImages(); for (const layerId of this._order) { const layer = this._layers[layerId]; - layer.recalculate(parameters, availableImages); + layer.recalculate(parameters, this._availableImages); if (!layer.isHidden(parameters.zoom) && layer.source) { this.sourceCaches[layer.source].used = true; } @@ -508,6 +514,7 @@ class Style extends Evented { return this.fire(new ErrorEvent(new Error('An image with this name already exists.'))); } this.imageManager.addImage(id, image); + this._availableImages = this.imageManager.listImages(); this._changedImages[id] = true; this._changed = true; this.fire(new Event('data', {dataType: 'style'})); @@ -526,6 +533,7 @@ class Style extends Evented { return this.fire(new ErrorEvent(new Error('No image with this name exists.'))); } this.imageManager.removeImage(id); + this._availableImages = this.imageManager.listImages(); this._changedImages[id] = true; this._changed = true; this.fire(new Event('data', {dataType: 'style'})); @@ -655,6 +663,7 @@ class Style extends Evented { this._validateLayer(layer); layer.setEventedParent(this, {layer: {id}}); + this._serializedLayers[layer.id] = layer.serialize(); } const index = before ? this._order.indexOf(before) : this._order.length; @@ -751,6 +760,7 @@ class Style extends Evented { this._changed = true; this._removedLayers[id] = layer; delete this._layers[id]; + delete this._serializedLayers[id]; delete this._updatedLayers[id]; delete this._updatedPaintProps[id]; @@ -1089,12 +1099,15 @@ class Style extends Evented { const sourceResults = []; + params.availableImages = this._availableImages; + for (const id in this.sourceCaches) { if (params.layers && !includedSources[id]) continue; sourceResults.push( queryRenderedFeatures( this.sourceCaches[id], this._layers, + this._serializedLayers, queryGeometry, params, transform) @@ -1107,6 +1120,7 @@ class Style extends Evented { sourceResults.push( queryRenderedSymbols( this._layers, + this._serializedLayers, this.sourceCaches, queryGeometry, params, diff --git a/test/integration/lib/query-browser.js b/test/integration/lib/query-browser.js index 8b5fe92a2d2..4eb275a70af 100644 --- a/test/integration/lib/query-browser.js +++ b/test/integration/lib/query-browser.js @@ -22,6 +22,7 @@ function testFunc(t) { const style = fixtures[currentTestName].style; const expected = fixtures[currentTestName].expected; const options = style.metadata.test; + const skipLayerDelete = style.metadata.skipLayerDelete; window.devicePixelRatio = options.pixelRatio; @@ -60,7 +61,7 @@ function testFunc(t) { const actual = results.map((feature) => { const featureJson = JSON.parse(JSON.stringify(feature.toJSON())); - delete featureJson.layer; + if (!skipLayerDelete) delete featureJson.layer; return featureJson; }); diff --git a/test/integration/query-tests/evaluated/line-width/expected.json b/test/integration/query-tests/evaluated/line-width/expected.json new file mode 100644 index 00000000000..e53c8e492ae --- /dev/null +++ b/test/integration/query-tests/evaluated/line-width/expected.json @@ -0,0 +1,33 @@ +[ + { + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 0, + 0 + ], + [ + 5.009765625, + 14.987239525774243 + ] + ] + }, + "type": "Feature", + "properties": {}, + "id": 1, + "layer": { + "id": "line", + "type": "line", + "source": "mapbox", + "paint": { + "line-width": 20 + }, + "layout": {} + }, + "source": "mapbox", + "state": { + "big": true + } + } +] diff --git a/test/integration/query-tests/evaluated/line-width/style.json b/test/integration/query-tests/evaluated/line-width/style.json new file mode 100644 index 00000000000..2e30e2dee4a --- /dev/null +++ b/test/integration/query-tests/evaluated/line-width/style.json @@ -0,0 +1,62 @@ +{ + "version": 8, + "metadata": { + "skipLayerDelete": true, + "test": { + "width": 64, + "height": 64, + "operations": [ + [ + "wait" + ], + [ + "setFeatureState", + { "source": "mapbox", "id": 1}, + { "big": true } + ], + [ + "wait" + ] + ], + "queryGeometry": [ + 32, + 16 + ] + } + }, + "sources": { + "mapbox": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "id": 1, + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ 0, 0], + [ 5, 15] + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "line", + "type": "line", + "source": "mapbox", + "paint": { + "line-width": ["case", + ["boolean", ["feature-state", "big"], false], + ["number", 20], + ["number", 1] + ] + } + } + ] +} diff --git a/test/unit/source/query_features.test.js b/test/unit/source/query_features.test.js index 94c77ee03ed..a2cf20d60c4 100644 --- a/test/unit/source/query_features.test.js +++ b/test/unit/source/query_features.test.js @@ -10,7 +10,7 @@ test('QueryFeatures#rendered', (t) => { t.test('returns empty object if source returns no tiles', (t) => { const mockSourceCache = {tilesIn () { return []; }}; const transform = new Transform(); - const result = queryRenderedFeatures(mockSourceCache, undefined, {}, undefined, transform); + const result = queryRenderedFeatures(mockSourceCache, {}, undefined, {}, undefined, transform); t.deepEqual(result, []); t.end(); }); diff --git a/test/unit/style/style.test.js b/test/unit/style/style.test.js index 1dc032104b6..ffe0bc968a6 100644 --- a/test/unit/style/style.test.js +++ b/test/unit/style/style.test.js @@ -1811,7 +1811,7 @@ test('Style#queryRenderedFeatures', (t) => { const transform = new Transform(); transform.resize(512, 512); - function queryMapboxFeatures(layers, getFeatureState, queryGeom, cameraQueryGeom, scale, params) { + function queryMapboxFeatures(layers, serializedLayers, getFeatureState, queryGeom, cameraQueryGeom, scale, params) { const features = { 'land': [{ type: 'Feature', diff --git a/test/unit/ui/map.test.js b/test/unit/ui/map.test.js index c110e41d100..ee19beb7b30 100755 --- a/test/unit/ui/map.test.js +++ b/test/unit/ui/map.test.js @@ -1065,7 +1065,7 @@ test('Map', (t) => { const args = map.style.queryRenderedFeatures.getCall(0).args; t.ok(args[0]); - t.deepEqual(args[1], {}); + t.deepEqual(args[1], {availableImages: []}); t.deepEqual(output, []); t.end(); @@ -1081,7 +1081,7 @@ test('Map', (t) => { const args = map.style.queryRenderedFeatures.getCall(0).args; t.deepEqual(args[0], [{x: 100, y: 100}]); // query geometry - t.deepEqual(args[1], {}); // params + t.deepEqual(args[1], {availableImages: []}); // params t.deepEqual(args[2], map.transform); // transform t.deepEqual(output, []); @@ -1098,7 +1098,7 @@ test('Map', (t) => { const args = map.style.queryRenderedFeatures.getCall(0).args; t.ok(args[0]); - t.deepEqual(args[1], {filter: ['all']}); + t.deepEqual(args[1], {availableImages: [], filter: ['all']}); t.deepEqual(output, []); t.end(); @@ -1114,7 +1114,7 @@ test('Map', (t) => { const args = map.style.queryRenderedFeatures.getCall(0).args; t.ok(args[0]); - t.deepEqual(args[1], {filter: ['all']}); + t.deepEqual(args[1], {availableImages: [], filter: ['all']}); t.deepEqual(output, []); t.end();