Skip to content

Commit

Permalink
Replace per-tile symbol querying with global queries.
Browse files Browse the repository at this point in the history
This fixes issue #5475/#6298, so that symbols that bleed over tile boundaries don't get missed.
Under the hood, there are (I hope) some good simplifications:
 - No round-tripping of viewport query coordinates through tile space
 - No more need to merge duplicate results from the same symbol showing up in multiple tiles
 - All querying-related data can now be indexed with a bucket instance id and a feature index.
 - CrossTileSymbolIndex manages lifetime of data needed for querying symbols, Tile is now only responsible for "latest" data
 - CollisionBoxArray no longer involved in querying at all
  • Loading branch information
ChrisLoer committed Apr 11, 2018
1 parent d96101d commit 917ac53
Show file tree
Hide file tree
Showing 19 changed files with 335 additions and 325 deletions.
3 changes: 2 additions & 1 deletion src/data/bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export type BucketParameters<Layer: TypedStyleLayer> = {
zoom: number,
pixelRatio: number,
overscaling: number,
collisionBoxArray: CollisionBoxArray
collisionBoxArray: CollisionBoxArray,
sourceLayerIndex: number
}

export type PopulateParameters = {
Expand Down
10 changes: 8 additions & 2 deletions src/data/bucket/symbol_bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export type CollisionArrays = {
textBox?: SingleCollisionBox;
iconBox?: SingleCollisionBox;
textCircles?: Array<number>;
textFeatureIndex?: number;
iconFeatureIndex?: number;
};

export type SymbolFeature = {|
Expand Down Expand Up @@ -283,6 +285,7 @@ class SymbolBucket implements Bucket {
collisionBox: CollisionBuffers;
collisionCircle: CollisionBuffers;
uploaded: boolean;
sourceLayerIndex: number;

constructor(options: BucketParameters<SymbolStyleLayer>) {
this.collisionBoxArray = options.collisionBoxArray;
Expand All @@ -292,6 +295,7 @@ class SymbolBucket implements Bucket {
this.layerIds = this.layers.map(layer => layer.id);
this.index = options.index;
this.pixelRatio = options.pixelRatio;
this.sourceLayerIndex = options.sourceLayerIndex;

const layer = this.layers[0];
const unevaluatedLayoutValues = layer._unevaluatedLayout._values;
Expand Down Expand Up @@ -579,11 +583,12 @@ class SymbolBucket implements Bucket {
const box: CollisionBox = (collisionBoxArray.get(k): any);
if (box.radius === 0) {
collisionArrays.textBox = { x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY };

collisionArrays.textFeatureIndex = box.featureIndex;
break; // Only one box allowed per instance
} else {
if (!collisionArrays.textCircles) {
collisionArrays.textCircles = [];
collisionArrays.textFeatureIndex = box.featureIndex;
}
const used = 1; // May be updated at collision detection time
collisionArrays.textCircles.push(box.anchorPointX, box.anchorPointY, box.radius, box.signedDistanceFromAnchor, used);
Expand All @@ -593,7 +598,8 @@ class SymbolBucket implements Bucket {
// An icon can only have one box now, so this indexing is a bit vestigial...
const box: CollisionBox = (collisionBoxArray.get(k): any);
if (box.radius === 0) {
collisionArrays.iconBox = { x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY };
collisionArrays.iconBox = { x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY, featureIndex: box.featureIndex };
collisionArrays.iconFeatureIndex = box.featureIndex;
break; // Only one box allowed per instance
}
}
Expand Down
185 changes: 106 additions & 79 deletions src/data/feature_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { arraysIntersect } from '../util/util';
import { OverscaledTileID } from '../source/tile_id';
import { register } from '../util/web_worker_transfer';

import type CollisionIndex from '../symbol/collision_index';
import type StyleLayer from '../style/style_layer';
import type {FeatureFilter} from '../style-spec/feature_filter';
import type Transform from '../geo/transform';
Expand All @@ -31,10 +30,7 @@ type QueryParameters = {
params: {
filter: FilterSpecification,
layers: Array<string>,
},
sourceID: string,
bucketInstanceIds: { [number]: boolean },
collisionIndex: ?CollisionIndex
}
}

class FeatureIndex {
Expand Down Expand Up @@ -90,8 +86,8 @@ class FeatureIndex {
}
}

// Finds features in this tile at a particular position.
query(result: {[string]: Array<{ featureIndex: number, feature: GeoJSONFeature }>}, args: QueryParameters, styleLayers: {[string]: StyleLayer}) {
// Finds non-symbol features in this tile at a particular position.
query(args: QueryParameters, styleLayers: {[string]: StyleLayer}): {[string]: Array<{ featureIndex: number, feature: GeoJSONFeature }>} {
if (!this.vtLayers) {
this.vtLayers = new vt.VectorTile(new Protobuf(this.rawTileData)).layers;
this.sourceLayerCoder = new DictionaryCoder(this.vtLayers ? Object.keys(this.vtLayers).sort() : ['_geojsonTileLayer']);
Expand All @@ -102,50 +98,26 @@ class FeatureIndex {
filter = featureFilter(params.filter);

const queryGeometry = args.queryGeometry;

if (args.collisionIndex) {
// Querying symbol features
const matchingSymbols =
args.collisionIndex.queryRenderedSymbols(queryGeometry, this.tileID, args.tileSize / EXTENT, this.collisionBoxArray, args.sourceID, args.bucketInstanceIds);
matchingSymbols.sort();
this.filterMatching(result, matchingSymbols, this.collisionBoxArray, queryGeometry, filter, params.layers, styleLayers, pixelsToTileUnits, args.posMatrix, args.transform);
} else {
// Querying non-symbol features
const queryPadding = args.queryPadding * pixelsToTileUnits;

let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (let i = 0; i < queryGeometry.length; i++) {
const ring = queryGeometry[i];
for (let k = 0; k < ring.length; k++) {
const p = ring[k];
minX = Math.min(minX, p.x);
minY = Math.min(minY, p.y);
maxX = Math.max(maxX, p.x);
maxY = Math.max(maxY, p.y);
}
const queryPadding = args.queryPadding * pixelsToTileUnits;

let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (let i = 0; i < queryGeometry.length; i++) {
const ring = queryGeometry[i];
for (let k = 0; k < ring.length; k++) {
const p = ring[k];
minX = Math.min(minX, p.x);
minY = Math.min(minY, p.y);
maxX = Math.max(maxX, p.x);
maxY = Math.max(maxY, p.y);
}

const matching = this.grid.query(minX - queryPadding, minY - queryPadding, maxX + queryPadding, maxY + queryPadding);
matching.sort(topDownFeatureComparator);
this.filterMatching(result, matching, this.featureIndexArray, queryGeometry, filter, params.layers, styleLayers, pixelsToTileUnits, args.posMatrix, args.transform);
}
}

filterMatching(
result: {[string]: Array<{ featureIndex: number, feature: GeoJSONFeature }>},
matching: Array<any>,
array: FeatureIndexArray | CollisionBoxArray,
queryGeometry: Array<Array<Point>>,
filter: FeatureFilter,
filterLayerIDs: Array<string>,
styleLayers: {[string]: StyleLayer},
pixelsToTileUnits: number,
posMatrix: Float32Array,
transform: Transform
) {
const matching = this.grid.query(minX - queryPadding, minY - queryPadding, maxX + queryPadding, maxY + queryPadding);
matching.sort(topDownFeatureComparator);
const result = {};
let previousIndex;
for (let k = 0; k < matching.length; k++) {
const index = matching[k];
Expand All @@ -154,48 +126,103 @@ class FeatureIndex {
if (index === previousIndex) continue;
previousIndex = index;

const match = array.get(index);
const match = this.featureIndexArray.get(index);
let geometry = null;
this.generateGeoJSONFeature(
result,
match.bucketIndex,
match.sourceLayerIndex,
match.featureIndex,
filter,
params.layers,
styleLayers,
(feature: VectorTileFeature, styleLayer: StyleLayer) => {
if (!geometry) {
geometry = loadGeometry(feature);
}
return styleLayer.queryIntersectsFeature(queryGeometry, feature, geometry, this.z, args.transform, pixelsToTileUnits, args.posMatrix);
}
);
}

const layerIDs = this.bucketLayerIDs[match.bucketIndex];
if (filterLayerIDs && !arraysIntersect(filterLayerIDs, layerIDs)) continue;
return result;
}

const sourceLayerName = this.sourceLayerCoder.decode(match.sourceLayerIndex);
const sourceLayer = this.vtLayers[sourceLayerName];
const feature = sourceLayer.feature(match.featureIndex);
generateGeoJSONFeature(
result: {[string]: Array<{ featureIndex: number, feature: GeoJSONFeature }>},
bucketIndex: number,
sourceLayerIndex: number,
featureIndex: number,
filter: FeatureFilter,
filterLayerIDs: Array<string>,
styleLayers: {[string]: StyleLayer},
intersectionTest?: (feature: VectorTileFeature, styleLayer: StyleLayer) => boolean) {

if (!filter({zoom: this.tileID.overscaledZ}, feature)) continue;
const layerIDs = this.bucketLayerIDs[bucketIndex];
if (filterLayerIDs && !arraysIntersect(filterLayerIDs, layerIDs))
return;

let geometry = null;
const sourceLayerName = this.sourceLayerCoder.decode(sourceLayerIndex);
const sourceLayer = this.vtLayers[sourceLayerName];
const feature = sourceLayer.feature(featureIndex);

for (let l = 0; l < layerIDs.length; l++) {
const layerID = layerIDs[l];
if (!filter({zoom: this.tileID.overscaledZ}, feature))
return;

if (filterLayerIDs && filterLayerIDs.indexOf(layerID) < 0) {
continue;
}
for (let l = 0; l < layerIDs.length; l++) {
const layerID = layerIDs[l];

const styleLayer = styleLayers[layerID];
if (!styleLayer) continue;
if (filterLayerIDs && filterLayerIDs.indexOf(layerID) < 0) {
continue;
}

if (styleLayer.type !== 'symbol') {
// all symbols already match the style
if (!geometry) {
geometry = loadGeometry(feature);
}
if (!styleLayer.queryIntersectsFeature(queryGeometry, feature, geometry, this.z, transform, pixelsToTileUnits, posMatrix)) {
continue;
}
}
const styleLayer = styleLayers[layerID];
if (!styleLayer) continue;

const geojsonFeature = new GeoJSONFeature(feature, this.z, this.x, this.y);
(geojsonFeature: any).layer = styleLayer.serialize();
let layerResult = result[layerID];
if (layerResult === undefined) {
layerResult = result[layerID] = [];
}
layerResult.push({ featureIndex: index, feature: geojsonFeature });
if (intersectionTest && !intersectionTest(feature, styleLayer)) {
// Only applied for non-symbol features
continue;
}

const geojsonFeature = new GeoJSONFeature(feature, this.z, this.x, this.y);
(geojsonFeature: any).layer = styleLayer.serialize();
let layerResult = result[layerID];
if (layerResult === undefined) {
layerResult = result[layerID] = [];
}
layerResult.push({ featureIndex: featureIndex, feature: geojsonFeature });
}
}

// Given a set of symbol indexes that have already been looked up,
// return a matching set of GeoJSONFeatures
lookupSymbolFeatures(symbolFeatureIndexes: Array<number>,
bucketIndex: number,
sourceLayerIndex: number,
filterSpec: FilterSpecification,
filterLayerIDs: Array<string>,
styleLayers: {[string]: StyleLayer}) {
const result = {};
if (!this.vtLayers) {
this.vtLayers = new vt.VectorTile(new Protobuf(this.rawTileData)).layers;
this.sourceLayerCoder = new DictionaryCoder(this.vtLayers ? Object.keys(this.vtLayers).sort() : ['_geojsonTileLayer']);
}

const filter = featureFilter(filterSpec);

for (const symbolFeatureIndex of symbolFeatureIndexes) {
this.generateGeoJSONFeature(
result,
bucketIndex,
sourceLayerIndex,
symbolFeatureIndex,
filter,
filterLayerIDs,
styleLayers
);

}
return result;
}

hasLayer(id: string) {
Expand Down
34 changes: 29 additions & 5 deletions src/source/query_features.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import type SourceCache from './source_cache';
import type StyleLayer from '../style/style_layer';
import type Coordinate from '../geo/coordinate';
import type CollisionIndex from '../symbol/collision_index';
import type CrossTileSymbolIndex from '../symbol/cross_tile_symbol_index';
import type Transform from '../geo/transform';

export function queryRenderedFeatures(sourceCache: SourceCache,
styleLayers: {[string]: StyleLayer},
queryGeometry: Array<Coordinate>,
params: { filter: FilterSpecification, layers: Array<string> },
transform: Transform,
collisionIndex: ?CollisionIndex) {
transform: Transform) {
const maxPitchScaleFactor = transform.maxPitchScaleFactor();
const tilesIn = sourceCache.tilesIn(queryGeometry, maxPitchScaleFactor);

Expand All @@ -28,15 +28,39 @@ export function queryRenderedFeatures(sourceCache: SourceCache,
params,
transform,
maxPitchScaleFactor,
sourceCache.transform.calculatePosMatrix(tileIn.tileID.toUnwrapped()),
sourceCache.id,
collisionIndex)
sourceCache.transform.calculatePosMatrix(tileIn.tileID.toUnwrapped()))
});
}

return mergeRenderedFeatureLayers(renderedFeatureLayers);
}

export function queryRenderedSymbols(styleLayers: {[string]: StyleLayer},
queryGeometry: Array<Point>,
params: { filter: FilterSpecification, layers: Array<string> },
collisionIndex: CollisionIndex,
crossTileSymbolIndex: CrossTileSymbolIndex) {
const result = {};
const renderedSymbols = collisionIndex.queryRenderedSymbols(queryGeometry);
for (const bucketInstanceId of Object.keys(renderedSymbols).map(Number)) {
const queryData = crossTileSymbolIndex.retainedBuckets[bucketInstanceId];
const bucketSymbols = queryData.featureIndex.lookupSymbolFeatures(
renderedSymbols[bucketInstanceId],
queryData.bucketIndex,
queryData.sourceLayerIndex,
params.filter,
params.layers,
styleLayers);
for (const layerID in bucketSymbols) {
const resultFeatures = result[layerID] = result[layerID] || [];
for (const symbolFeature of bucketSymbols[layerID]) {
resultFeatures.push(symbolFeature.feature);
}
}
}
return result;
}

export function querySourceFeatures(sourceCache: SourceCache, params: any) {
const tiles = sourceCache.getRenderableIds().map((id) => {
return sourceCache.getTileByID(id);
Expand Down
8 changes: 0 additions & 8 deletions src/source/source_cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -757,14 +757,6 @@ class SourceCache extends Evented {

return false;
}

commitPlacement(usedBucketInstanceIds: {[number]: boolean}) {
const ids = this.getIds();
for (let i = 0; i < ids.length; i++) {
const tile = this.getTileByID(ids[i]);
tile.pruneFeatureIndexes(usedBucketInstanceIds);
}
}
}

SourceCache.maxOverzooming = 10;
Expand Down
Loading

0 comments on commit 917ac53

Please sign in to comment.