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

Add "crossSourceCollisions" option to restore per-source collision detection #6566

Merged
merged 5 commits into from
Jun 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/data/bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export type BucketParameters<Layer: TypedStyleLayer> = {
pixelRatio: number,
overscaling: number,
collisionBoxArray: CollisionBoxArray,
sourceLayerIndex: number
sourceLayerIndex: number,
sourceID: string
}

export type PopulateParameters = {
Expand Down
3 changes: 3 additions & 0 deletions src/data/bucket/symbol_bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ class SymbolBucket implements Bucket {
collisionCircle: CollisionBuffers;
uploaded: boolean;
sourceLayerIndex: number;
sourceID: string;

constructor(options: BucketParameters<SymbolStyleLayer>) {
this.collisionBoxArray = options.collisionBoxArray;
Expand All @@ -313,6 +314,8 @@ class SymbolBucket implements Bucket {
const layout = this.layers[0].layout;
this.sortFeaturesByY = layout.get('text-allow-overlap') || layout.get('icon-allow-overlap') ||
layout.get('text-ignore-placement') || layout.get('icon-ignore-placement');

this.sourceID = options.sourceID;
}

createArrays() {
Expand Down
3 changes: 2 additions & 1 deletion src/source/worker_tile.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ class WorkerTile {
pixelRatio: this.pixelRatio,
overscaling: this.overscaling,
collisionBoxArray: this.collisionBoxArray,
sourceLayerIndex: sourceLayerIndex
sourceLayerIndex: sourceLayerIndex,
sourceID: this.source
});

bucket.populate(features, options);
Expand Down
7 changes: 5 additions & 2 deletions src/style/pauseable_placement.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@ class PauseablePlacement {
_inProgressLayer: ?LayerPlacement;

constructor(transform: Transform, order: Array<string>,
forceFullPlacement: boolean, showCollisionBoxes: boolean, fadeDuration: number) {
forceFullPlacement: boolean,
showCollisionBoxes: boolean,
fadeDuration: number,
crossSourceCollisions: boolean) {

this.placement = new Placement(transform, fadeDuration);
this.placement = new Placement(transform, fadeDuration, crossSourceCollisions);
this._currentPlacementIndex = order.length - 1;
this._forceFullPlacement = forceFullPlacement;
this._showCollisionBoxes = showCollisionBoxes;
Expand Down
4 changes: 2 additions & 2 deletions src/style/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -1004,7 +1004,7 @@ class Style extends Evented {
}
}

_updatePlacement(transform: Transform, showCollisionBoxes: boolean, fadeDuration: number) {
_updatePlacement(transform: Transform, showCollisionBoxes: boolean, fadeDuration: number, crossSourceCollisions: boolean) {
let symbolBucketsChanged = false;
let placementCommitted = false;

Expand Down Expand Up @@ -1033,7 +1033,7 @@ class Style extends Evented {
const forceFullPlacement = this._layerOrderChanged;

if (forceFullPlacement || !this.pauseablePlacement || (this.pauseablePlacement.isDone() && !this.placement.stillRecent(browser.now()))) {
this.pauseablePlacement = new PauseablePlacement(transform, this._order, forceFullPlacement, showCollisionBoxes, fadeDuration);
this.pauseablePlacement = new PauseablePlacement(transform, this._order, forceFullPlacement, showCollisionBoxes, fadeDuration, crossSourceCollisions);
this._layerOrderChanged = false;
}

Expand Down
17 changes: 9 additions & 8 deletions src/symbol/collision_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class CollisionIndex {
this.screenBottomBoundary = transform.height + viewportPadding;
}

placeCollisionBox(collisionBox: SingleCollisionBox, allowOverlap: boolean, textPixelRatio: number, posMatrix: mat4): { box: Array<number>, offscreen: boolean } {
placeCollisionBox(collisionBox: SingleCollisionBox, allowOverlap: boolean, textPixelRatio: number, posMatrix: mat4, collisionGroupPredicate?: any): { box: Array<number>, offscreen: boolean } {
const projectedPoint = this.projectAndGetPerspectiveRatio(posMatrix, collisionBox.anchorPointX, collisionBox.anchorPointY);
const tileToViewport = textPixelRatio * projectedPoint.perspectiveRatio;
const tlX = collisionBox.x1 * tileToViewport + projectedPoint.point.x;
Expand All @@ -67,7 +67,7 @@ class CollisionIndex {
const brY = collisionBox.y2 * tileToViewport + projectedPoint.point.y;

if (!allowOverlap) {
if (this.grid.hitTest(tlX, tlY, brX, brY)) {
if (this.grid.hitTest(tlX, tlY, brX, brY, collisionGroupPredicate)) {
return {
box: [],
offscreen: false
Expand Down Expand Up @@ -113,7 +113,8 @@ class CollisionIndex {
posMatrix: mat4,
labelPlaneMatrix: mat4,
showCollisionCircles: boolean,
pitchWithMap: boolean): { circles: Array<number>, offscreen: boolean } {
pitchWithMap: boolean,
collisionGroupPredicate?: any): { circles: Array<number>, offscreen: boolean } {
const placedCollisionCircles = [];

const projectedAnchor = this.projectAnchor(posMatrix, symbol.anchorX, symbol.anchorY);
Expand Down Expand Up @@ -208,7 +209,7 @@ class CollisionIndex {
entirelyOffscreen = entirelyOffscreen && this.isOffscreen(projectedPoint.x - radius, projectedPoint.y - radius, projectedPoint.x + radius, projectedPoint.y + radius);

if (!allowOverlap) {
if (this.grid.hitTestCircle(projectedPoint.x, projectedPoint.y, radius)) {
if (this.grid.hitTestCircle(projectedPoint.x, projectedPoint.y, radius, collisionGroupPredicate)) {
if (!showCollisionCircles) {
return {
circles: [],
Expand Down Expand Up @@ -296,17 +297,17 @@ class CollisionIndex {
return result;
}

insertCollisionBox(collisionBox: Array<number>, ignorePlacement: boolean, bucketInstanceId: number, featureIndex: number) {
insertCollisionBox(collisionBox: Array<number>, ignorePlacement: boolean, bucketInstanceId: number, featureIndex: number, collisionGroupID: number) {
const grid = ignorePlacement ? this.ignoredGrid : this.grid;

const key = { bucketInstanceId: bucketInstanceId, featureIndex: featureIndex };
const key = { bucketInstanceId: bucketInstanceId, featureIndex: featureIndex, collisionGroupID: collisionGroupID };
grid.insert(key, collisionBox[0], collisionBox[1], collisionBox[2], collisionBox[3]);
}

insertCollisionCircles(collisionCircles: Array<number>, ignorePlacement: boolean, bucketInstanceId: number, featureIndex: number) {
insertCollisionCircles(collisionCircles: Array<number>, ignorePlacement: boolean, bucketInstanceId: number, featureIndex: number, collisionGroupID: number) {
const grid = ignorePlacement ? this.ignoredGrid : this.grid;

const key = { bucketInstanceId: bucketInstanceId, featureIndex: featureIndex };
const key = { bucketInstanceId: bucketInstanceId, featureIndex: featureIndex, collisionGroupID: collisionGroupID };
for (let k = 0; k < collisionCircles.length; k += 4) {
grid.insertCircle(key, collisionCircles[k], collisionCircles[k + 1], collisionCircles[k + 2]);
}
Expand Down
43 changes: 24 additions & 19 deletions src/symbol/grid_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class GridIndex {
this.circleCells[cellIndex].push(uid);
}

_query(x1: number, y1: number, x2: number, y2: number, hitTest: boolean) {
_query(x1: number, y1: number, x2: number, y2: number, hitTest: boolean, predicate?: any) {
if (x2 < 0 || x1 > this.width || y2 < 0 || y1 > this.height) {
return hitTest ? false : [];
}
Expand Down Expand Up @@ -117,17 +117,18 @@ class GridIndex {
y2: y + radius
});
}
return predicate ? result.filter(predicate) : result;
} else {
const queryArgs = {
hitTest,
seenUids: { box: {}, circle: {} }
};
this._forEachCell(x1, y1, x2, y2, this._queryCell, result, queryArgs);
this._forEachCell(x1, y1, x2, y2, this._queryCell, result, queryArgs, predicate);
return hitTest ? result.length > 0 : result;
}
return hitTest ? result.length > 0 : result;
}

_queryCircle(x: number, y: number, radius: number, hitTest: boolean) {
_queryCircle(x: number, y: number, radius: number, hitTest: boolean, predicate?: any) {
// Insert circle into grid for all cells in the circumscribing square
// It's more than necessary (by a factor of 4/PI), but fast to insert
const x1 = x - radius;
Expand All @@ -147,23 +148,23 @@ class GridIndex {
circle: { x: x, y: y, radius: radius },
seenUids: { box: {}, circle: {} }
};
this._forEachCell(x1, y1, x2, y2, this._queryCellCircle, result, queryArgs);
this._forEachCell(x1, y1, x2, y2, this._queryCellCircle, result, queryArgs, predicate);
return hitTest ? result.length > 0 : result;
}

query(x1: number, y1: number, x2: number, y2: number): Array<any> {
return (this._query(x1, y1, x2, y2, false): any);
query(x1: number, y1: number, x2: number, y2: number, predicate?: any): Array<any> {
return (this._query(x1, y1, x2, y2, false, predicate): any);
}

hitTest(x1: number, y1: number, x2: number, y2: number): boolean {
return (this._query(x1, y1, x2, y2, true): any);
hitTest(x1: number, y1: number, x2: number, y2: number, predicate?: any): boolean {
return (this._query(x1, y1, x2, y2, true, predicate): any);
}

hitTestCircle(x: number, y: number, radius: number): boolean {
return (this._queryCircle(x, y, radius, true): any);
hitTestCircle(x: number, y: number, radius: number, predicate?: any): boolean {
return (this._queryCircle(x, y, radius, true, predicate): any);
}

_queryCell(x1: number, y1: number, x2: number, y2: number, cellIndex: number, result: any, queryArgs: any) {
_queryCell(x1: number, y1: number, x2: number, y2: number, cellIndex: number, result: any, queryArgs: any, predicate?: any) {
const seenUids = queryArgs.seenUids;
const boxCell = this.boxCells[cellIndex];
if (boxCell !== null) {
Expand All @@ -175,7 +176,8 @@ class GridIndex {
if ((x1 <= bboxes[offset + 2]) &&
(y1 <= bboxes[offset + 3]) &&
(x2 >= bboxes[offset + 0]) &&
(y2 >= bboxes[offset + 1])) {
(y2 >= bboxes[offset + 1]) &&
(!predicate || predicate(this.boxKeys[boxUid]))) {
if (queryArgs.hitTest) {
result.push(true);
return true;
Expand Down Expand Up @@ -206,7 +208,8 @@ class GridIndex {
x1,
y1,
x2,
y2)) {
y2) &&
(!predicate || predicate(this.circleKeys[circleUid]))) {
if (queryArgs.hitTest) {
result.push(true);
return true;
Expand All @@ -228,7 +231,7 @@ class GridIndex {
}
}

_queryCellCircle(x1: number, y1: number, x2: number, y2: number, cellIndex: number, result: any, queryArgs: any) {
_queryCellCircle(x1: number, y1: number, x2: number, y2: number, cellIndex: number, result: any, queryArgs: any, predicate?: any) {
const circle = queryArgs.circle;
const seenUids = queryArgs.seenUids;
const boxCell = this.boxCells[cellIndex];
Expand All @@ -245,7 +248,8 @@ class GridIndex {
bboxes[offset + 0],
bboxes[offset + 1],
bboxes[offset + 2],
bboxes[offset + 3])) {
bboxes[offset + 3]) &&
(!predicate || predicate(this.boxKeys[boxUid]))) {
result.push(true);
return true;
}
Expand All @@ -266,7 +270,8 @@ class GridIndex {
circles[offset + 2],
circle.x,
circle.y,
circle.radius)) {
circle.radius) &&
(!predicate || predicate(this.circleKeys[circleUid]))) {
result.push(true);
return true;
}
Expand All @@ -275,7 +280,7 @@ class GridIndex {
}
}

_forEachCell(x1: number, y1: number, x2: number, y2: number, fn: any, arg1: any, arg2?: any) {
_forEachCell(x1: number, y1: number, x2: number, y2: number, fn: any, arg1: any, arg2?: any, predicate?: any) {
const cx1 = this._convertToXCellCoord(x1);
const cy1 = this._convertToYCellCoord(y1);
const cx2 = this._convertToXCellCoord(x2);
Expand All @@ -284,7 +289,7 @@ class GridIndex {
for (let x = cx1; x <= cx2; x++) {
for (let y = cy1; y <= cy2; y++) {
const cellIndex = this.xCellCount * y + x;
if (fn.call(this, x1, y1, x2, y2, cellIndex, arg1, arg2)) return;
if (fn.call(this, x1, y1, x2, y2, cellIndex, arg1, arg2, predicate)) return;
}
}
}
Expand Down
53 changes: 45 additions & 8 deletions src/symbol/placement.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ export class RetainedQueryData {
bucketIndex: number;
tileID: OverscaledTileID;
featureSortOrder: ?Array<number>

constructor(bucketInstanceId: number,
featureIndex: FeatureIndex,
sourceLayerIndex: number,
Expand All @@ -83,6 +82,39 @@ export class RetainedQueryData {
}
}

class CollisionGroups {
collisionGroups: { [groupName: string]: { ID: number, predicate?: any }};
maxGroupID: number;
crossSourceCollisions: boolean;

constructor(crossSourceCollisions: boolean) {
this.crossSourceCollisions = crossSourceCollisions;
this.maxGroupID = 0;
this.collisionGroups = {};
}

get(sourceID: string) {
// The predicate/groupID mechanism allows for arbitrary grouping,
// but the current interface defines one source == one group when
// crossSourceCollisions == true.
if (!this.crossSourceCollisions) {
if (!this.collisionGroups[sourceID]) {
const nextGroupID = ++this.maxGroupID;
this.collisionGroups[sourceID] = {
ID: nextGroupID,
predicate: (key) => {
return key.collisionGroupID === nextGroupID;
}
};
}
return this.collisionGroups[sourceID];
} else {
return { ID: 0, predicate: null };
}
}
}


export class Placement {
transform: Transform;
collisionIndex: CollisionIndex;
Expand All @@ -93,15 +125,17 @@ export class Placement {
stale: boolean;
fadeDuration: number;
retainedQueryData: {[number]: RetainedQueryData};
collisionGroups: CollisionGroups;

constructor(transform: Transform, fadeDuration: number) {
constructor(transform: Transform, fadeDuration: number, crossSourceCollisions: boolean) {
this.transform = transform.clone();
this.collisionIndex = new CollisionIndex(this.transform);
this.placements = {};
this.opacities = {};
this.stale = false;
this.fadeDuration = fadeDuration;
this.retainedQueryData = {};
this.collisionGroups = new CollisionGroups(crossSourceCollisions);
}

placeLayerTile(styleLayer: StyleLayer, tile: Tile, showCollisionBoxes: boolean, seenCrossTileIDs: { [string | number]: boolean }) {
Expand Down Expand Up @@ -155,6 +189,8 @@ export class Placement {
const iconWithoutText = !bucket.hasTextData() || layout.get('text-optional');
const textWithoutIcon = !bucket.hasIconData() || layout.get('icon-optional');

const collisionGroup = this.collisionGroups.get(bucket.sourceID);

for (const symbolInstance of bucket.symbolInstances) {
if (!seenCrossTileIDs[symbolInstance.crossTileID]) {

Expand All @@ -180,7 +216,7 @@ export class Placement {
}
if (symbolInstance.collisionArrays.textBox) {
placedGlyphBoxes = this.collisionIndex.placeCollisionBox(symbolInstance.collisionArrays.textBox,
layout.get('text-allow-overlap'), textPixelRatio, posMatrix);
layout.get('text-allow-overlap'), textPixelRatio, posMatrix, collisionGroup.predicate);
placeText = placedGlyphBoxes.box.length > 0;
offscreen = offscreen && placedGlyphBoxes.offscreen;
}
Expand All @@ -200,7 +236,8 @@ export class Placement {
posMatrix,
textLabelPlaneMatrix,
showCollisionBoxes,
layout.get('text-pitch-alignment') === 'map');
layout.get('text-pitch-alignment') === 'map',
collisionGroup.predicate);
// If text-allow-overlap is set, force "placedCircles" to true
// In theory there should always be at least one circle placed
// in this case, but for now quirks in text-anchor
Expand All @@ -214,7 +251,7 @@ export class Placement {
}
if (symbolInstance.collisionArrays.iconBox) {
placedIconBoxes = this.collisionIndex.placeCollisionBox(symbolInstance.collisionArrays.iconBox,
layout.get('icon-allow-overlap'), textPixelRatio, posMatrix);
layout.get('icon-allow-overlap'), textPixelRatio, posMatrix, collisionGroup.predicate);
placeIcon = placedIconBoxes.box.length > 0;
offscreen = offscreen && placedIconBoxes.offscreen;
}
Expand All @@ -230,15 +267,15 @@ export class Placement {

if (placeText && placedGlyphBoxes) {
this.collisionIndex.insertCollisionBox(placedGlyphBoxes.box, layout.get('text-ignore-placement'),
bucket.bucketInstanceId, textFeatureIndex);
bucket.bucketInstanceId, textFeatureIndex, collisionGroup.ID);
}
if (placeIcon && placedIconBoxes) {
this.collisionIndex.insertCollisionBox(placedIconBoxes.box, layout.get('icon-ignore-placement'),
bucket.bucketInstanceId, iconFeatureIndex);
bucket.bucketInstanceId, iconFeatureIndex, collisionGroup.ID);
}
if (placeText && placedGlyphCircles) {
this.collisionIndex.insertCollisionCircles(placedGlyphCircles.circles, layout.get('text-ignore-placement'),
bucket.bucketInstanceId, textFeatureIndex);
bucket.bucketInstanceId, textFeatureIndex, collisionGroup.ID);
}

assert(symbolInstance.crossTileID !== 0);
Expand Down
Loading