Skip to content

Commit

Permalink
Placement order matches viewport-y sort
Browse files Browse the repository at this point in the history
Symbols are placed accordingly to their viewport Y order,
if the style `symbol-z-order` is explicitly set to `viewport-y`.

This improves rendering of symbol layers, where icons are allowed
to overlap but not text.
  • Loading branch information
pozdnyakov committed Apr 24, 2019
1 parent 4cce1a9 commit d8bc48a
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 145 deletions.
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]);
});

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

0 comments on commit d8bc48a

Please sign in to comment.