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

Return evaluated properties in query results #9198

Merged
merged 18 commits into from
Mar 2, 2020
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;
});
}
Copy link
Member

Choose a reason for hiding this comment

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

Nit: we have a mapObject function in util, so you could simplify this code to just the callback function for mapping.


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