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

Placement order matches viewport-y sort #8180

Merged
merged 1 commit into from
May 8, 2019
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
52 changes: 29 additions & 23 deletions src/data/bucket/symbol_bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ class SymbolBucket implements Bucket {
uploaded: boolean;
sourceLayerIndex: number;
sourceID: string;
symbolInstanceIndexes: Array<number>;

constructor(options: BucketParameters<SymbolStyleLayer>) {
this.collisionBoxArray = options.collisionBoxArray;
Expand Down Expand Up @@ -704,11 +705,34 @@ class SymbolBucket implements Bucket {
}
}

getSortedSymbolIndexes(angle: number) {
if (this.sortedAngle === angle && this.symbolInstanceIndexes !== undefined) {
return this.symbolInstanceIndexes;
}
const sin = Math.sin(angle);
const cos = Math.cos(angle);
const rotatedYs = [];
const featureIndexes = [];
const result = [];

for (let i = 0; i < this.symbolInstances.length; ++i) {
result.push(i);
const symbolInstance = this.symbolInstances.get(i);
rotatedYs.push(Math.round(sin * symbolInstance.anchorX + cos * symbolInstance.anchorY) | 0);
featureIndexes.push(symbolInstance.featureIndex);
}

result.sort((aIndex, bIndex) => {
return (rotatedYs[aIndex] - rotatedYs[bIndex]) ||
(featureIndexes[bIndex] - featureIndexes[aIndex]);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Docs should be updated to include fallback to source order for symbols with same Y. Especially useful is symbol-sort-key is also provided, and the expectation would be to fallback to the sort key.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will make a separate PR for that, thanks!

});

return result;
}

sortFeatures(angle: number) {
if (!this.sortFeaturesByY) return;

if (this.sortedAngle === angle) return;
this.sortedAngle = angle;

// The current approach to sorting doesn't sort across segments so don't try.
// Sorting within segments separately seemed not to be worth the complexity.
Expand All @@ -719,33 +743,15 @@ class SymbolBucket implements Bucket {
// sorted order.

// To avoid sorting the actual symbolInstance array we sort an array of indexes.
const symbolInstanceIndexes = [];
for (let i = 0; i < this.symbolInstances.length; i++) {
symbolInstanceIndexes.push(i);
}

const sin = Math.sin(angle),
cos = Math.cos(angle);

const rotatedYs = [];
const featureIndexes = [];
for (let i = 0; i < this.symbolInstances.length; i++) {
const symbolInstance = this.symbolInstances.get(i);
rotatedYs.push(Math.round(sin * symbolInstance.anchorX + cos * symbolInstance.anchorY) | 0);
featureIndexes.push(symbolInstance.featureIndex);
}

symbolInstanceIndexes.sort((aIndex, bIndex) => {
return (rotatedYs[aIndex] - rotatedYs[bIndex]) ||
(featureIndexes[bIndex] - featureIndexes[aIndex]);
});
this.symbolInstanceIndexes = this.getSortedSymbolIndexes(angle);
this.sortedAngle = angle;

this.text.indexArray.clear();
this.icon.indexArray.clear();

this.featureSortOrder = [];

for (const i of symbolInstanceIndexes) {
for (const i of this.symbolInstanceIndexes) {
const symbolInstance = this.symbolInstances.get(i);
this.featureSortOrder.push(symbolInstance.featureIndex);

Expand Down
253 changes: 131 additions & 122 deletions src/symbol/placement.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type Transform from '../geo/transform';
import type StyleLayer from '../style/style_layer';

import type Tile from '../source/tile';
import type SymbolBucket, { SingleCollisionBox } from '../data/bucket/symbol_bucket';
import type SymbolBucket, { CollisionArrays, SingleCollisionBox } from '../data/bucket/symbol_bucket';
import type {mat4} from 'gl-matrix';
import type {CollisionBoxArray, CollisionVertexArray, SymbolInstance} from '../data/array_types';
import type FeatureIndex from '../data/feature_index';
Expand Down Expand Up @@ -300,149 +300,158 @@ export class Placement {

const rotateWithMap = layout.get('text-rotation-alignment') === 'map';
const pitchWithMap = layout.get('text-pitch-alignment') === 'map';
const zOrderByViewportY = layout.get('symbol-z-order') === 'viewport-y';

if (!bucket.collisionArrays && collisionBoxArray) {
bucket.deserializeCollisionBoxes(collisionBoxArray);
}

for (let i = 0; i < bucket.symbolInstances.length; i++) {
const symbolInstance = bucket.symbolInstances.get(i);
if (!seenCrossTileIDs[symbolInstance.crossTileID]) {
if (holdingForFade) {
// Mark all symbols from this tile as "not placed", but don't add to seenCrossTileIDs, because we don't
// know yet if we have a duplicate in a parent tile that _should_ be placed.
this.placements[symbolInstance.crossTileID] = new JointPlacement(false, false, false);
continue;
}

let placeText = false;
let placeIcon = false;
let offscreen = true;
const placeSymbol = (symbolInstance: SymbolInstance, collisionArrays: CollisionArrays) => {
if (seenCrossTileIDs[symbolInstance.crossTileID]) return;
if (holdingForFade) {
// Mark all symbols from this tile as "not placed", but don't add to seenCrossTileIDs, because we don't
// know yet if we have a duplicate in a parent tile that _should_ be placed.
this.placements[symbolInstance.crossTileID] = new JointPlacement(false, false, false);
return;
}

let placedGlyphBoxes = null;
let placedGlyphCircles = null;
let placedIconBoxes = null;
let textFeatureIndex = 0;
let iconFeatureIndex = 0;
let placeText = false;
let placeIcon = false;
let offscreen = true;

const collisionArrays = bucket.collisionArrays[i];
let placedGlyphBoxes = null;
let placedGlyphCircles = null;
let placedIconBoxes = null;
let textFeatureIndex = 0;
let iconFeatureIndex = 0;

if (collisionArrays.textFeatureIndex) {
textFeatureIndex = collisionArrays.textFeatureIndex;
}
if (collisionArrays.textFeatureIndex) {
textFeatureIndex = collisionArrays.textFeatureIndex;
}

const textBox = collisionArrays.textBox;
if (textBox) {
if (!layout.get('text-variable-anchor')) {
placedGlyphBoxes = this.collisionIndex.placeCollisionBox(textBox,
layout.get('text-allow-overlap'), textPixelRatio, posMatrix, collisionGroup.predicate);
placeText = placedGlyphBoxes.box.length > 0;
} else {
const width = textBox.x2 - textBox.x1;
const height = textBox.y2 - textBox.y1;
const textBoxScale = symbolInstance.textBoxScale;
let anchors = layout.get('text-variable-anchor');

// If we this symbol was in the last placement, shift the previously used
// anchor to the front of the anchor list.
if (this.prevPlacement && this.prevPlacement.variableOffsets[symbolInstance.crossTileID]) {
const prevOffsets = this.prevPlacement.variableOffsets[symbolInstance.crossTileID];
if (anchors[0] !== prevOffsets.anchor) {
anchors = anchors.filter(anchor => anchor !== prevOffsets.anchor);
anchors.unshift(prevOffsets.anchor);
}
const textBox = collisionArrays.textBox;
if (textBox) {
if (!layout.get('text-variable-anchor')) {
placedGlyphBoxes = this.collisionIndex.placeCollisionBox(textBox,
layout.get('text-allow-overlap'), textPixelRatio, posMatrix, collisionGroup.predicate);
placeText = placedGlyphBoxes.box.length > 0;
} else {
const width = textBox.x2 - textBox.x1;
const height = textBox.y2 - textBox.y1;
const textBoxScale = symbolInstance.textBoxScale;
let anchors = layout.get('text-variable-anchor');

// If we this symbol was in the last placement, shift the previously used
// anchor to the front of the anchor list.
if (this.prevPlacement && this.prevPlacement.variableOffsets[symbolInstance.crossTileID]) {
const prevOffsets = this.prevPlacement.variableOffsets[symbolInstance.crossTileID];
if (anchors[0] !== prevOffsets.anchor) {
anchors = anchors.filter(anchor => anchor !== prevOffsets.anchor);
anchors.unshift(prevOffsets.anchor);
}
}

for (const anchor of anchors) {
placedGlyphBoxes = this.attemptAnchorPlacement(
anchor, textBox, width, height, symbolInstance.radialTextOffset,
textBoxScale, rotateWithMap, pitchWithMap, textPixelRatio, posMatrix,
collisionGroup, textAllowOverlap, symbolInstance, bucket);
if (placedGlyphBoxes) {
placeText = true;
break;
}
for (const anchor of anchors) {
placedGlyphBoxes = this.attemptAnchorPlacement(
anchor, textBox, width, height, symbolInstance.radialTextOffset,
textBoxScale, rotateWithMap, pitchWithMap, textPixelRatio, posMatrix,
collisionGroup, textAllowOverlap, symbolInstance, bucket);
if (placedGlyphBoxes) {
placeText = true;
break;
}
}

// If we didn't get placed, we still need to copy our position from the last placement for
// fade animations
if (!this.variableOffsets[symbolInstance.crossTileID] && this.prevPlacement) {
const prevOffset = this.prevPlacement.variableOffsets[symbolInstance.crossTileID];
if (prevOffset) {
this.variableOffsets[symbolInstance.crossTileID] = prevOffset;
this.markUsedJustification(bucket, prevOffset.anchor, symbolInstance);
}
// If we didn't get placed, we still need to copy our position from the last placement for
// fade animations
if (!this.variableOffsets[symbolInstance.crossTileID] && this.prevPlacement) {
const prevOffset = this.prevPlacement.variableOffsets[symbolInstance.crossTileID];
if (prevOffset) {
this.variableOffsets[symbolInstance.crossTileID] = prevOffset;
this.markUsedJustification(bucket, prevOffset.anchor, symbolInstance);
}
}
}
}

offscreen = placedGlyphBoxes && placedGlyphBoxes.offscreen;
const textCircles = collisionArrays.textCircles;
if (textCircles) {
const placedSymbol = bucket.text.placedSymbolArray.get(symbolInstance.centerJustifiedTextSymbolIndex);
const fontSize = symbolSize.evaluateSizeForFeature(bucket.textSizeData, partiallyEvaluatedTextSize, placedSymbol);
placedGlyphCircles = this.collisionIndex.placeCollisionCircles(textCircles,
layout.get('text-allow-overlap'),
scale,
textPixelRatio,
placedSymbol,
bucket.lineVertexArray,
bucket.glyphOffsetArray,
fontSize,
posMatrix,
textLabelPlaneMatrix,
showCollisionBoxes,
pitchWithMap,
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
// and text-offset may prevent that from being true.
placeText = layout.get('text-allow-overlap') || placedGlyphCircles.circles.length > 0;
offscreen = offscreen && placedGlyphCircles.offscreen;
}
offscreen = placedGlyphBoxes && placedGlyphBoxes.offscreen;
const textCircles = collisionArrays.textCircles;
if (textCircles) {
const placedSymbol = bucket.text.placedSymbolArray.get(symbolInstance.centerJustifiedTextSymbolIndex);
const fontSize = symbolSize.evaluateSizeForFeature(bucket.textSizeData, partiallyEvaluatedTextSize, placedSymbol);
placedGlyphCircles = this.collisionIndex.placeCollisionCircles(textCircles,
layout.get('text-allow-overlap'),
scale,
textPixelRatio,
placedSymbol,
bucket.lineVertexArray,
bucket.glyphOffsetArray,
fontSize,
posMatrix,
textLabelPlaneMatrix,
showCollisionBoxes,
pitchWithMap,
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
// and text-offset may prevent that from being true.
placeText = layout.get('text-allow-overlap') || placedGlyphCircles.circles.length > 0;
offscreen = offscreen && placedGlyphCircles.offscreen;
}

if (collisionArrays.iconFeatureIndex) {
iconFeatureIndex = collisionArrays.iconFeatureIndex;
}
if (collisionArrays.iconBox) {
placedIconBoxes = this.collisionIndex.placeCollisionBox(collisionArrays.iconBox,
layout.get('icon-allow-overlap'), textPixelRatio, posMatrix, collisionGroup.predicate);
placeIcon = placedIconBoxes.box.length > 0;
offscreen = offscreen && placedIconBoxes.offscreen;
}
if (collisionArrays.iconFeatureIndex) {
iconFeatureIndex = collisionArrays.iconFeatureIndex;
}
if (collisionArrays.iconBox) {
placedIconBoxes = this.collisionIndex.placeCollisionBox(collisionArrays.iconBox,
layout.get('icon-allow-overlap'), textPixelRatio, posMatrix, collisionGroup.predicate);
placeIcon = placedIconBoxes.box.length > 0;
offscreen = offscreen && placedIconBoxes.offscreen;
}

const iconWithoutText = textOptional ||
(symbolInstance.numHorizontalGlyphVertices === 0 && symbolInstance.numVerticalGlyphVertices === 0);
const textWithoutIcon = iconOptional || symbolInstance.numIconVertices === 0;

// Combine the scales for icons and text.
if (!iconWithoutText && !textWithoutIcon) {
placeIcon = placeText = placeIcon && placeText;
} else if (!textWithoutIcon) {
placeText = placeIcon && placeText;
} else if (!iconWithoutText) {
placeIcon = placeIcon && placeText;
}
const iconWithoutText = textOptional ||
(symbolInstance.numHorizontalGlyphVertices === 0 && symbolInstance.numVerticalGlyphVertices === 0);
const textWithoutIcon = iconOptional || symbolInstance.numIconVertices === 0;

// Combine the scales for icons and text.
if (!iconWithoutText && !textWithoutIcon) {
placeIcon = placeText = placeIcon && placeText;
} else if (!textWithoutIcon) {
placeText = placeIcon && placeText;
} else if (!iconWithoutText) {
placeIcon = placeIcon && placeText;
}

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

assert(symbolInstance.crossTileID !== 0);
assert(bucket.bucketInstanceId !== 0);
assert(symbolInstance.crossTileID !== 0);
assert(bucket.bucketInstanceId !== 0);

this.placements[symbolInstance.crossTileID] = new JointPlacement(placeText || alwaysShowText, placeIcon || alwaysShowIcon, offscreen || bucket.justReloaded);
seenCrossTileIDs[symbolInstance.crossTileID] = true;
this.placements[symbolInstance.crossTileID] = new JointPlacement(placeText || alwaysShowText, placeIcon || alwaysShowIcon, offscreen || bucket.justReloaded);
seenCrossTileIDs[symbolInstance.crossTileID] = true;
};

if (zOrderByViewportY) {
const symbolIndexes = bucket.getSortedSymbolIndexes(this.transform.angle);
for (let i = symbolIndexes.length - 1; i >= 0; --i) {
const symbolIndex = symbolIndexes[i];
placeSymbol(bucket.symbolInstances.get(symbolIndex), bucket.collisionArrays[symbolIndex]);
}
} else {
for (let i = 0; i < bucket.symbolInstances.length; ++i) {
placeSymbol(bucket.symbolInstances.get(i), bucket.collisionArrays[i]);
}
}

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading