Skip to content

Commit

Permalink
Return evaluated properties in query results (mapbox#9198)
Browse files Browse the repository at this point in the history
* Cache serialized layer rather than serializing on every feature

* Fix some lint & flow errors

* Evaluate properties

* Fix unit test and Flow

* Revert comment changes

* Revert comment changes

* Cache serialized layers

* Delete serializedLayers on removeLayer

* Cache available images

* Update availableImages

* Remove comment

* Simplify evaluation code

* Remove unused variable

* Clean up change to resolved images

* Update serializedLayers when adding/removing layers

* Only serialized non-custom layers

* Simplify code

* Add query test
  • Loading branch information
Ryan Hamley authored and mike-unearth committed Mar 18, 2020
1 parent 0ae6804 commit a77a79d
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 28 deletions.
51 changes: 39 additions & 12 deletions src/data/feature_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -35,6 +36,7 @@ type QueryParameters = {
params: {
filter: FilterSpecification,
layers: Array<string>,
availableImages: Array<string>
}
}

Expand Down Expand Up @@ -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 || {},
Expand Down Expand Up @@ -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);
}
);
Expand All @@ -170,8 +171,11 @@ class FeatureIndex {
featureIndex: number,
filter: FeatureFilter,
filterLayerIDs: Array<string>,
availableImages: Array<string>,
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))
Expand All @@ -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] = [];
Expand All @@ -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<number>,
serializedLayers: {[string]: StyleLayer},
bucketIndex: number,
sourceLayerIndex: number,
filterSpec: FilterSpecification,
filterLayerIDs: Array<string>,
availableImages: Array<string>,
styleLayers: {[_: string]: StyleLayer}) {
const result = {};
this.loadVTLayers();
Expand All @@ -233,7 +251,9 @@ class FeatureIndex {
symbolFeatureIndex,
filter,
filterLayerIDs,
styleLayers
availableImages,
styleLayers,
serializedLayers
);

}
Expand Down Expand Up @@ -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<Point>) {
let minX = Infinity;
let minY = Infinity;
Expand Down
11 changes: 7 additions & 4 deletions src/source/query_features.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,23 @@ function queryIncludes3DLayer(layers?: Array<string>, styleLayers: {[_: string]:

export function queryRenderedFeatures(sourceCache: SourceCache,
styleLayers: {[_: string]: StyleLayer},
serializedLayers: {[_: string]: Object},
queryGeometry: Array<Point>,
params: { filter: FilterSpecification, layers: Array<string> },
params: { filter: FilterSpecification, layers: Array<string>, availableImages: Array<string> },
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,
Expand Down Expand Up @@ -86,9 +86,10 @@ export function queryRenderedFeatures(sourceCache: SourceCache,
}

export function queryRenderedSymbols(styleLayers: {[_: string]: StyleLayer},
serializedLayers: {[_: string]: StyleLayer},
sourceCaches: {[_: string]: SourceCache},
queryGeometry: Array<Point>,
params: { filter: FilterSpecification, layers: Array<string> },
params: { filter: FilterSpecification, layers: Array<string>, availableImages: Array<string> },
collisionIndex: CollisionIndex,
retainedQueryData: {[_: number]: RetainedQueryData}) {
const result = {};
Expand All @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions src/source/tile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Point>,
cameraQueryGeometry: Array<Point>,
scale: number,
params: { filter: FilterSpecification, layers: Array<string> },
params: { filter: FilterSpecification, layers: Array<string>, availableImages: Array<string> },
transform: Transform,
maxPitchScaleFactor: number,
pixelPosMatrix: Float32Array): {[_: string]: Array<{ featureIndex: number, feature: GeoJSONFeature }>} {
Expand All @@ -285,7 +286,7 @@ class Tile {
transform,
params,
queryPadding: this.queryPadding * maxPitchScaleFactor
}, layers, sourceFeatureState);
}, layers, serializedLayers, sourceFeatureState);
}

querySourceFeatures(result: Array<GeoJSONFeature>, params: any) {
Expand Down
20 changes: 17 additions & 3 deletions src/style/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class Style extends Evented {
_request: ?Cancelable;
_spriteRequest: ?Cancelable;
_layers: {[_: string]: StyleLayer};
_serializedLayers: {[_: string]: Object};
_order: Array<string>;
sourceCaches: {[_: string]: SourceCache};
zoomHistory: ZoomHistory;
Expand All @@ -126,6 +127,7 @@ class Style extends Evented {
_changedImages: {[_: string]: true};
_updatedPaintProps: {[layer: string]: true};
_layerOrderChanged: boolean;
_availableImages: Array<string>;

crossTileSymbolIndex: CrossTileSymbolIndex;
pauseablePlacement: PauseablePlacement;
Expand All @@ -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();

Expand Down Expand Up @@ -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));

Expand All @@ -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'}));
});
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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'}));
Expand All @@ -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'}));
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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];

Expand Down Expand Up @@ -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)
Expand All @@ -1107,6 +1120,7 @@ class Style extends Evented {
sourceResults.push(
queryRenderedSymbols(
this._layers,
this._serializedLayers,
this.sourceCaches,
queryGeometry,
params,
Expand Down
3 changes: 2 additions & 1 deletion test/integration/lib/query-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
});

Expand Down
33 changes: 33 additions & 0 deletions test/integration/query-tests/evaluated/line-width/expected.json
Original file line number Diff line number Diff line change
@@ -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
}
}
]
Loading

0 comments on commit a77a79d

Please sign in to comment.