Skip to content

Commit

Permalink
Replace per-tile symbol querying with global queries.
Browse files Browse the repository at this point in the history
Addresses hover flicker from issues #5887 and #5506.
Also fixes issue #5475/#6298, so that symbols that bleed over tile boundaries don't get missed. Under the hood, there are 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
 - `Placement` now manages lifetime of any data needed to query against its CollisionIndex
 - CollisionBoxArray no longer involved in querying at all
  • Loading branch information
ChrisLoer committed May 18, 2018
1 parent cc3bb6a commit d243cf9
Show file tree
Hide file tree
Showing 23 changed files with 349 additions and 238 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
8 changes: 7 additions & 1 deletion 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 @@ -594,6 +599,7 @@ class SymbolBucket implements Bucket {
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.iconFeatureIndex = box.featureIndex;
break; // Only one box allowed per instance
}
}
Expand Down
154 changes: 88 additions & 66 deletions src/data/feature_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ 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 {CollisionBoxArray} from './array_types';
import type Transform from '../geo/transform';

import { FeatureIndexArray } from './array_types';
Expand All @@ -32,16 +30,11 @@ type QueryParameters = {
params: {
filter: FilterSpecification,
layers: Array<string>,
},
collisionBoxArray: CollisionBoxArray,
sourceID: string,
bucketInstanceIds: { [number]: boolean },
collisionIndex: ?CollisionIndex
}
}

class FeatureIndex {
tileID: OverscaledTileID;
overscaling: number;
x: number;
y: number;
z: number;
Expand All @@ -55,11 +48,9 @@ class FeatureIndex {
sourceLayerCoder: DictionaryCoder;

constructor(tileID: OverscaledTileID,
overscaling: number,
grid?: Grid,
featureIndexArray?: FeatureIndexArray) {
this.tileID = tileID;
this.overscaling = overscaling;
this.x = tileID.canonical.x;
this.y = tileID.canonical.y;
this.z = tileID.canonical.z;
Expand Down Expand Up @@ -92,15 +83,13 @@ class FeatureIndex {
}
}

// Finds features in this tile at a particular position.
query(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']);
}

const result = {};

const params = args.params || {},
pixelsToTileUnits = EXTENT / args.tileSize / args.scale,
filter = featureFilter(params.filter);
Expand All @@ -125,79 +114,112 @@ class FeatureIndex {

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);
const result = {};
let previousIndex;
for (let k = 0; k < matching.length; k++) {
const index = matching[k];

const matchingSymbols = args.collisionIndex ?
args.collisionIndex.queryRenderedSymbols(queryGeometry, this.tileID, args.tileSize / EXTENT, args.collisionBoxArray, args.sourceID, args.bucketInstanceIds) :
[];
matchingSymbols.sort();
this.filterMatching(result, matchingSymbols, args.collisionBoxArray, queryGeometry, filter, params.layers, styleLayers, pixelsToTileUnits, args.posMatrix, args.transform);
// don't check the same feature more than once
if (index === previousIndex) continue;
previousIndex = index;

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

return result;
}

filterMatching(
loadMatchingFeature(
result: {[string]: Array<{ featureIndex: number, feature: GeoJSONFeature }>},
matching: Array<any>,
array: FeatureIndexArray | CollisionBoxArray,
queryGeometry: Array<Array<Point>>,
bucketIndex: number,
sourceLayerIndex: number,
featureIndex: number,
filter: FeatureFilter,
filterLayerIDs: Array<string>,
styleLayers: {[string]: StyleLayer},
pixelsToTileUnits: number,
posMatrix: Float32Array,
transform: Transform
) {
let previousIndex;
for (let k = 0; k < matching.length; k++) {
const index = matching[k];
intersectionTest?: (feature: VectorTileFeature, styleLayer: StyleLayer) => boolean) {

// don't check the same feature more than once
if (index === previousIndex) continue;
previousIndex = index;
const layerIDs = this.bucketLayerIDs[bucketIndex];
if (filterLayerIDs && !arraysIntersect(filterLayerIDs, layerIDs))
return;

const match = array.get(index);
const sourceLayerName = this.sourceLayerCoder.decode(sourceLayerIndex);
const sourceLayer = this.vtLayers[sourceLayerName];
const feature = sourceLayer.feature(featureIndex);

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

const sourceLayerName = this.sourceLayerCoder.decode(match.sourceLayerIndex);
const sourceLayer = this.vtLayers[sourceLayerName];
const feature = sourceLayer.feature(match.featureIndex);
for (let l = 0; l < layerIDs.length; l++) {
const layerID = layerIDs[l];

if (!filter({zoom: this.tileID.overscaledZ}, feature)) continue;
if (filterLayerIDs && filterLayerIDs.indexOf(layerID) < 0) {
continue;
}

let geometry = null;
const styleLayer = styleLayers[layerID];
if (!styleLayer) continue;

for (let l = 0; l < layerIDs.length; l++) {
const layerID = layerIDs[l];
if (intersectionTest && !intersectionTest(feature, styleLayer)) {
// Only applied for non-symbol features
continue;
}

if (filterLayerIDs && filterLayerIDs.indexOf(layerID) < 0) {
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 });
}
}

const styleLayer = styleLayers[layerID];
if (!styleLayer) continue;
// 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']);
}

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 filter = featureFilter(filterSpec);

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

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 });
}
}
return result;
}

hasLayer(id: string) {
Expand Down
1 change: 0 additions & 1 deletion src/source/geojson_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,6 @@ class GeoJSONSource extends Evented implements Source {
tileSize: this.tileSize,
source: this.id,
pixelRatio: browser.devicePixelRatio,
overscaling: tile.tileID.overscaleFactor(),
showCollisionBoxes: this.map.showCollisionBoxes
};

Expand Down
39 changes: 34 additions & 5 deletions src/source/query_features.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import type StyleLayer from '../style/style_layer';
import type Coordinate from '../geo/coordinate';
import type CollisionIndex from '../symbol/collision_index';
import type Transform from '../geo/transform';
import type { RetainedQueryData } from '../symbol/placement';

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,44 @@ 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,
retainedQueryData: {[number]: RetainedQueryData}) {
const result = {};
const renderedSymbols = collisionIndex.queryRenderedSymbols(queryGeometry);
const bucketQueryData = [];
for (const bucketInstanceId of Object.keys(renderedSymbols).map(Number)) {
bucketQueryData.push(retainedQueryData[bucketInstanceId]);
}
bucketQueryData.sort(sortTilesIn);

for (const queryData of bucketQueryData) {
const bucketSymbols = queryData.featureIndex.lookupSymbolFeatures(
renderedSymbols[queryData.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
Loading

0 comments on commit d243cf9

Please sign in to comment.