Skip to content

Commit

Permalink
add symbol-sort-key style property (#7678)
Browse files Browse the repository at this point in the history
This property allows users to specify a sorting order for symbols.
Symbols are sorted in ascending order based on the key. Symbols
with lower keys will appear below symbols with higher keys when
they are rendered with overlap. Symbols with lower keys will be placed
before symbols with higher keys.

This also fixes #7111 by defining a sort order across tile boundaries.
  • Loading branch information
ansis authored Dec 12, 2018
1 parent 4d8c433 commit 64ebdda
Show file tree
Hide file tree
Showing 12 changed files with 369 additions and 26 deletions.
26 changes: 22 additions & 4 deletions src/data/bucket/symbol_bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export type CollisionArrays = {
};

export type SymbolFeature = {|
sortKey: number | void,
text: Formatted | void,
icon: string | void,
index: number,
Expand Down Expand Up @@ -101,7 +102,7 @@ function addDynamicAttributes(dynamicLayoutVertexArray: StructArray, p: Point, a
dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angle);
}

class SymbolBuffers {
export class SymbolBuffers {
layoutVertexArray: SymbolLayoutArray;
layoutVertexBuffer: VertexBuffer;

Expand Down Expand Up @@ -261,6 +262,7 @@ class SymbolBucket implements Bucket {
tilePixelRatio: number;
compareText: {[string]: Array<Point>};
fadeStartTime: number;
sortFeaturesByKey: boolean;
sortFeaturesByY: boolean;
sortedAngle: number;
featureSortOrder: Array<number>;
Expand Down Expand Up @@ -291,7 +293,10 @@ class SymbolBucket implements Bucket {
this.iconSizeData = getSizeData(this.zoom, unevaluatedLayoutValues['icon-size']);

const layout = this.layers[0].layout;
const zOrderByViewportY = layout.get('symbol-z-order') === 'viewport-y';
const sortKey = layout.get('symbol-sort-key');
const zOrder = layout.get('symbol-z-order');
this.sortFeaturesByKey = zOrder !== 'viewport-y' && sortKey.constantOr(1) !== undefined;
const zOrderByViewportY = zOrder === 'viewport-y' || (zOrder === 'auto' && !this.sortFeaturesByKey);
this.sortFeaturesByY = zOrderByViewportY && (layout.get('text-allow-overlap') || layout.get('icon-allow-overlap') ||
layout.get('text-ignore-placement') || layout.get('icon-ignore-placement'));

Expand Down Expand Up @@ -333,6 +338,7 @@ class SymbolBucket implements Bucket {
(textField.value.kind !== 'constant' || textField.value.value.toString().length > 0) &&
(textFont.value.kind !== 'constant' || textFont.value.value.length > 0);
const hasIcon = iconImage.value.kind !== 'constant' || iconImage.value.value && iconImage.value.value.length > 0;
const symbolSortKey = layout.get('symbol-sort-key');

this.features = [];

Expand Down Expand Up @@ -370,14 +376,19 @@ class SymbolBucket implements Bucket {
continue;
}

const sortKey = this.sortFeaturesByKey ?
symbolSortKey.evaluate(feature, {}) :
undefined;

const symbolFeature: SymbolFeature = {
text,
icon,
index,
sourceLayerIndex,
geometry: loadGeometry(feature),
properties: feature.properties,
type: vectorTileFeatureTypes[feature.type]
type: vectorTileFeatureTypes[feature.type],
sortKey
};
if (typeof feature.id !== 'undefined') {
symbolFeature.id = feature.id;
Expand Down Expand Up @@ -405,6 +416,13 @@ class SymbolBucket implements Bucket {
// It's better to place labels on one long line than on many short segments.
this.features = mergeLines(this.features);
}

if (this.sortFeaturesByKey) {
this.features.sort((a, b) => {
// a.sortKey is always a number when sortFeaturesByKey is true
return ((a.sortKey: any): number) - ((b.sortKey: any): number);
});
}
}

update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[string]: ImagePosition}) {
Expand Down Expand Up @@ -481,7 +499,7 @@ class SymbolBucket implements Bucket {
const layoutVertexArray = arrays.layoutVertexArray;
const dynamicLayoutVertexArray = arrays.dynamicLayoutVertexArray;

const segment = arrays.segments.prepareSegment(4 * quads.length, arrays.layoutVertexArray, arrays.indexArray);
const segment = arrays.segments.prepareSegment(4 * quads.length, arrays.layoutVertexArray, arrays.indexArray, feature.sortKey);
const glyphOffsetArrayStart = this.glyphOffsetArray.length;
const vertexStartIndex = segment.vertexLength;

Expand Down
9 changes: 6 additions & 3 deletions src/data/segment.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type VertexArrayObject from '../render/vertex_array_object';
import type {StructArray} from '../util/struct_array';

export type Segment = {
sortKey: number | void,
vertexOffset: number,
primitiveOffset: number,
vertexLength: number,
Expand All @@ -23,16 +24,17 @@ class SegmentVector {
this.segments = segments;
}

prepareSegment(numVertices: number, layoutVertexArray: StructArray, indexArray: StructArray): Segment {
prepareSegment(numVertices: number, layoutVertexArray: StructArray, indexArray: StructArray, sortKey?: number): Segment {
let segment: Segment = this.segments[this.segments.length - 1];
if (numVertices > SegmentVector.MAX_VERTEX_ARRAY_LENGTH) warnOnce(`Max vertices per segment is ${SegmentVector.MAX_VERTEX_ARRAY_LENGTH}: bucket requested ${numVertices}`);
if (!segment || segment.vertexLength + numVertices > SegmentVector.MAX_VERTEX_ARRAY_LENGTH) {
if (!segment || segment.vertexLength + numVertices > SegmentVector.MAX_VERTEX_ARRAY_LENGTH || segment.sortKey !== sortKey) {
segment = ({
vertexOffset: layoutVertexArray.length,
primitiveOffset: indexArray.length,
vertexLength: 0,
primitiveLength: 0
}: any);
if (sortKey !== undefined) segment.sortKey = sortKey;
this.segments.push(segment);
}
return segment;
Expand All @@ -56,7 +58,8 @@ class SegmentVector {
primitiveOffset,
vertexLength,
primitiveLength,
vaos: {}
vaos: {},
sortKey: 0
}]);
}
}
Expand Down
99 changes: 83 additions & 16 deletions src/render/draw_symbol.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import drawCollisionDebug from './draw_collision_debug';

import SegmentVector from '../data/segment';
import pixelsToTileUnits from '../source/pixels_to_tile_units';
import * as symbolProjection from '../symbol/projection';
import * as symbolSize from '../symbol/symbol_size';
Expand All @@ -20,11 +21,28 @@ import {
import type Painter from './painter';
import type SourceCache from '../source/source_cache';
import type SymbolStyleLayer from '../style/style_layer/symbol_style_layer';
import type SymbolBucket from '../data/bucket/symbol_bucket';
import type SymbolBucket, {SymbolBuffers} from '../data/bucket/symbol_bucket';
import type Texture from '../render/texture';
import type {OverscaledTileID} from '../source/tile_id';
import type {UniformValues} from './uniform_binding';
import type {SymbolSDFUniformsType} from '../render/program/symbol_program';

export default drawSymbols;

type SymbolTileRenderState = {
segments: SegmentVector,
sortKey: number,
state: {
program: any,
buffers: SymbolBuffers,
uniformValues: any,
atlasTexture: Texture,
atlasInterpolation: any,
isSDF: boolean,
hasHalo: boolean
}
};

function drawSymbols(painter: Painter, sourceCache: SourceCache, layer: SymbolStyleLayer, coords: Array<OverscaledTileID>) {
if (painter.renderPass !== 'translucent') return;

Expand Down Expand Up @@ -74,11 +92,15 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate
// Unpitched point labels need to have their rotation applied after projection
const rotateInShader = rotateWithMap && !pitchWithMap && !alongLine;

const sortFeaturesByKey = layer.layout.get('symbol-sort-key').constantOr(1) !== undefined;

const depthMode = painter.depthModeForSublayer(0, DepthMode.ReadOnly);

let program;
let size;

const tileRenderState: Array<SymbolTileRenderState> = [];

for (const coord of coords) {
const tile = sourceCache.getTile(coord);
const bucket: SymbolBucket = (tile.getBucket(layer): any);
Expand All @@ -99,16 +121,21 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate
context.activeTexture.set(gl.TEXTURE0);

let texSize: [number, number];
let atlasTexture;
let atlasInterpolation;
if (isText) {
tile.glyphAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
atlasTexture = tile.glyphAtlasTexture;
atlasInterpolation = gl.LINEAR;
texSize = tile.glyphAtlasTexture.size;

} else {
const iconScaled = layer.layout.get('icon-size').constantOr(0) !== 1 || bucket.iconsNeedLinear;
const iconTransformed = pitchWithMap || tr.pitch !== 0;

tile.imageAtlasTexture.bind(isSDF || painter.options.rotating || painter.options.zooming || iconScaled || iconTransformed ?
gl.LINEAR : gl.NEAREST, gl.CLAMP_TO_EDGE);

atlasTexture = tile.imageAtlasTexture;
atlasInterpolation = isSDF || painter.options.rotating || painter.options.zooming || iconScaled || iconTransformed ?
gl.LINEAR :
gl.NEAREST;
texSize = tile.imageAtlasTexture.size;
}

Expand All @@ -124,36 +151,76 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate
uLabelPlaneMatrix = alongLine ? identityMat4 : labelPlaneMatrix,
uglCoordMatrix = painter.translatePosMatrix(glCoordMatrix, tile, translate, translateAnchor, true);

const hasHalo = isSDF && layer.paint.get(isText ? 'text-halo-width' : 'icon-halo-width').constantOr(1) !== 0;

let uniformValues;
if (isSDF) {
const hasHalo = layer.paint.get(isText ? 'text-halo-width' : 'icon-halo-width').constantOr(1) !== 0;

uniformValues = symbolSDFUniformValues(sizeData.functionType,
size, rotateInShader, pitchWithMap, painter, matrix,
uLabelPlaneMatrix, uglCoordMatrix, isText, texSize, true);

if (hasHalo) {
drawSymbolElements(buffers, layer, painter, program, depthMode, stencilMode, colorMode, uniformValues);
}

uniformValues['u_is_halo'] = 0;

} else {
uniformValues = symbolIconUniformValues(sizeData.functionType,
size, rotateInShader, pitchWithMap, painter, matrix,
uLabelPlaneMatrix, uglCoordMatrix, isText, texSize);
}

drawSymbolElements(buffers, layer, painter, program, depthMode, stencilMode, colorMode, uniformValues);
const state = {
program,
buffers,
uniformValues,
atlasTexture,
atlasInterpolation,
isSDF,
hasHalo
};

if (sortFeaturesByKey) {
const oldSegments = buffers.segments.get();
for (const segment of oldSegments) {
tileRenderState.push({
segments: new SegmentVector([segment]),
sortKey: ((segment.sortKey: any): number),
state
});
}
} else {
tileRenderState.push({
segments: buffers.segments,
sortKey: 0,
state
});
}
}

if (sortFeaturesByKey) {
tileRenderState.sort((a, b) => a.sortKey - b.sortKey);
}

for (const segmentState of tileRenderState) {
const state = segmentState.state;

state.atlasTexture.bind(state.atlasInterpolation, gl.CLAMP_TO_EDGE);

if (state.isSDF) {
const uniformValues = ((state.uniformValues: any): UniformValues<SymbolSDFUniformsType>);
if (state.hasHalo) {
uniformValues['u_is_halo'] = 1;
drawSymbolElements(state.buffers, segmentState.segments, layer, painter, state.program, depthMode, stencilMode, colorMode, uniformValues);
}
uniformValues['u_is_halo'] = 0;
}
drawSymbolElements(state.buffers, segmentState.segments, layer, painter, state.program, depthMode, stencilMode, colorMode, state.uniformValues);
}
}

function drawSymbolElements(buffers, layer, painter, program, depthMode, stencilMode, colorMode, uniformValues) {
function drawSymbolElements(buffers, segments, layer, painter, program, depthMode, stencilMode, colorMode, uniformValues) {
const context = painter.context;
const gl = context.gl;
program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled,
uniformValues, layer.id, buffers.layoutVertexBuffer,
buffers.indexBuffer, buffers.segments, layer.paint,
buffers.indexBuffer, segments, layer.paint,
painter.transform.zoom, buffers.programConfigurations.get(layer.id),
buffers.dynamicLayoutVertexBuffer, buffers.opacityVertexBuffer);
}

20 changes: 19 additions & 1 deletion src/style-spec/reference/v8.json
Original file line number Diff line number Diff line change
Expand Up @@ -958,17 +958,35 @@
},
"property-type": "data-constant"
},
"symbol-sort-key": {
"type": "number",
"doc": "Sorts features in ascending order based on this value. Features with a higher sort key will appear above features with a lower sort key wehn they overlap. Features with a lower sort key will have priority over other features when doing placement.",
"sdk-support": {
"js": "0.53.0"
},
"expression": {
"interpolated": false,
"parameters": [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"symbol-z-order": {
"type": "enum",
"values": {
"auto": {
"doc": "If `symbol-sort-key` is set, sort based on that. Otherwise sort symbols by their position relative to the viewport."
},
"viewport-y": {
"doc": "Symbols will be sorted by their y-position relative to the viewport."
},
"source": {
"doc": "Symbols will be rendered in the same order as the source data with no sorting applied."
}
},
"default": "viewport-y",
"default": "auto",
"doc": "Controls the order in which overlapping symbols in the same layer are rendered",
"sdk-support": {
"basic functionality": {
Expand Down
3 changes: 2 additions & 1 deletion src/style-spec/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,8 @@ export type SymbolLayerSpecification = {|
"symbol-placement"?: PropertyValueSpecification<"point" | "line" | "line-center">,
"symbol-spacing"?: PropertyValueSpecification<number>,
"symbol-avoid-edges"?: PropertyValueSpecification<boolean>,
"symbol-z-order"?: PropertyValueSpecification<"viewport-y" | "source">,
"symbol-sort-key"?: DataDrivenPropertyValueSpecification<number>,
"symbol-z-order"?: PropertyValueSpecification<"auto" | "viewport-y" | "source">,
"icon-allow-overlap"?: PropertyValueSpecification<boolean>,
"icon-ignore-placement"?: PropertyValueSpecification<boolean>,
"icon-optional"?: PropertyValueSpecification<boolean>,
Expand Down
4 changes: 3 additions & 1 deletion src/style/style_layer/symbol_style_layer_properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export type LayoutProps = {|
"symbol-placement": DataConstantProperty<"point" | "line" | "line-center">,
"symbol-spacing": DataConstantProperty<number>,
"symbol-avoid-edges": DataConstantProperty<boolean>,
"symbol-z-order": DataConstantProperty<"viewport-y" | "source">,
"symbol-sort-key": DataDrivenProperty<number>,
"symbol-z-order": DataConstantProperty<"auto" | "viewport-y" | "source">,
"icon-allow-overlap": DataConstantProperty<boolean>,
"icon-ignore-placement": DataConstantProperty<boolean>,
"icon-optional": DataConstantProperty<boolean>,
Expand Down Expand Up @@ -61,6 +62,7 @@ const layout: Properties<LayoutProps> = new Properties({
"symbol-placement": new DataConstantProperty(styleSpec["layout_symbol"]["symbol-placement"]),
"symbol-spacing": new DataConstantProperty(styleSpec["layout_symbol"]["symbol-spacing"]),
"symbol-avoid-edges": new DataConstantProperty(styleSpec["layout_symbol"]["symbol-avoid-edges"]),
"symbol-sort-key": new DataDrivenProperty(styleSpec["layout_symbol"]["symbol-sort-key"]),
"symbol-z-order": new DataConstantProperty(styleSpec["layout_symbol"]["symbol-z-order"]),
"icon-allow-overlap": new DataConstantProperty(styleSpec["layout_symbol"]["icon-allow-overlap"]),
"icon-ignore-placement": new DataConstantProperty(styleSpec["layout_symbol"]["icon-ignore-placement"]),
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 64ebdda

Please sign in to comment.