diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index d03e8302fb2..420703acf9c 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -56,6 +56,7 @@ export type CollisionArrays = { }; export type SymbolFeature = {| + sortKey: number | void, text: Formatted | void, icon: string | void, index: number, @@ -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; @@ -261,6 +262,7 @@ class SymbolBucket implements Bucket { tilePixelRatio: number; compareText: {[string]: Array}; fadeStartTime: number; + sortFeaturesByKey: boolean; sortFeaturesByY: boolean; sortedAngle: number; featureSortOrder: Array; @@ -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')); @@ -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 = []; @@ -370,6 +376,10 @@ class SymbolBucket implements Bucket { continue; } + const sortKey = this.sortFeaturesByKey ? + symbolSortKey.evaluate(feature, {}) : + undefined; + const symbolFeature: SymbolFeature = { text, icon, @@ -377,7 +387,8 @@ class SymbolBucket implements Bucket { 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; @@ -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}) { @@ -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; diff --git a/src/data/segment.js b/src/data/segment.js index ff20b529ef3..9d3335923a7 100644 --- a/src/data/segment.js +++ b/src/data/segment.js @@ -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, @@ -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; @@ -56,7 +58,8 @@ class SegmentVector { primitiveOffset, vertexLength, primitiveLength, - vaos: {} + vaos: {}, + sortKey: 0 }]); } } diff --git a/src/render/draw_symbol.js b/src/render/draw_symbol.js index 2d96707db69..26b9fca165b 100644 --- a/src/render/draw_symbol.js +++ b/src/render/draw_symbol.js @@ -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'; @@ -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) { if (painter.renderPass !== 'translucent') return; @@ -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 = []; + for (const coord of coords) { const tile = sourceCache.getTile(coord); const bucket: SymbolBucket = (tile.getBucket(layer): any); @@ -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; } @@ -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); + 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); } + diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index cc82a18e8b3..0eadbdfb20a 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -958,9 +958,27 @@ }, "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." }, @@ -968,7 +986,7 @@ "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": { diff --git a/src/style-spec/types.js b/src/style-spec/types.js index 5690824235e..100fddeae67 100644 --- a/src/style-spec/types.js +++ b/src/style-spec/types.js @@ -212,7 +212,8 @@ export type SymbolLayerSpecification = {| "symbol-placement"?: PropertyValueSpecification<"point" | "line" | "line-center">, "symbol-spacing"?: PropertyValueSpecification, "symbol-avoid-edges"?: PropertyValueSpecification, - "symbol-z-order"?: PropertyValueSpecification<"viewport-y" | "source">, + "symbol-sort-key"?: DataDrivenPropertyValueSpecification, + "symbol-z-order"?: PropertyValueSpecification<"auto" | "viewport-y" | "source">, "icon-allow-overlap"?: PropertyValueSpecification, "icon-ignore-placement"?: PropertyValueSpecification, "icon-optional"?: PropertyValueSpecification, diff --git a/src/style/style_layer/symbol_style_layer_properties.js b/src/style/style_layer/symbol_style_layer_properties.js index 7c77ff40fe7..90ced3407a3 100644 --- a/src/style/style_layer/symbol_style_layer_properties.js +++ b/src/style/style_layer/symbol_style_layer_properties.js @@ -21,7 +21,8 @@ export type LayoutProps = {| "symbol-placement": DataConstantProperty<"point" | "line" | "line-center">, "symbol-spacing": DataConstantProperty, "symbol-avoid-edges": DataConstantProperty, - "symbol-z-order": DataConstantProperty<"viewport-y" | "source">, + "symbol-sort-key": DataDrivenProperty, + "symbol-z-order": DataConstantProperty<"auto" | "viewport-y" | "source">, "icon-allow-overlap": DataConstantProperty, "icon-ignore-placement": DataConstantProperty, "icon-optional": DataConstantProperty, @@ -61,6 +62,7 @@ const layout: Properties = 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"]), diff --git a/test/integration/render-tests/symbol-sort-key/icon-expression/expected.png b/test/integration/render-tests/symbol-sort-key/icon-expression/expected.png new file mode 100644 index 00000000000..6b17890462d Binary files /dev/null and b/test/integration/render-tests/symbol-sort-key/icon-expression/expected.png differ diff --git a/test/integration/render-tests/symbol-sort-key/icon-expression/style.json b/test/integration/render-tests/symbol-sort-key/icon-expression/style.json new file mode 100644 index 00000000000..90eb8aa20c1 --- /dev/null +++ b/test/integration/render-tests/symbol-sort-key/icon-expression/style.json @@ -0,0 +1,76 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 64, + "width": 64 + } + }, + "center": [0, 30], + "zoom": 1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "sort-key": 0, + "image": "bank-12" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 1, + 34 + ] + } + }, + { + "type": "Feature", + "properties": { + "sort-key": 2, + "image": "bank-12" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 1, + 30 + ] + } + }, + { + "type": "Feature", + "properties": { + "sort-key": 1, + "image": "fav-campsite-18" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -1, + 32 + ] + } + } + ] + } + } + }, + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "icon", + "type": "symbol", + "source": "geojson", + "layout": { + "symbol-sort-key": ["get", "sort-key"], + "icon-allow-overlap": true, + "icon-image": ["get", "image"] + } + } + ] +} diff --git a/test/integration/render-tests/symbol-sort-key/text-expression/expected.png b/test/integration/render-tests/symbol-sort-key/text-expression/expected.png new file mode 100644 index 00000000000..af38ed8c665 Binary files /dev/null and b/test/integration/render-tests/symbol-sort-key/text-expression/expected.png differ diff --git a/test/integration/render-tests/symbol-sort-key/text-expression/style.json b/test/integration/render-tests/symbol-sort-key/text-expression/style.json new file mode 100644 index 00000000000..773f0912acc --- /dev/null +++ b/test/integration/render-tests/symbol-sort-key/text-expression/style.json @@ -0,0 +1,87 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 64, + "width": 64 + } + }, + "center": [0, 30], + "zoom": 1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "color": "red", + "sort-key": 0, + "image": "bank-12" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 1, + 34 + ] + } + }, + { + "type": "Feature", + "properties": { + "color": "red", + "sort-key": 2, + "image": "bank-12" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 1, + 30 + ] + } + }, + { + "type": "Feature", + "properties": { + "color": "blue", + "sort-key": 1, + "image": "fav-campsite-18" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -1, + 32 + ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "icon", + "type": "symbol", + "source": "geojson", + "layout": { + "text-allow-overlap": true, + "symbol-sort-key": ["get", "sort-key"], + "text-field": ["get", "sort-key"], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-halo-width": 5, + "text-halo-color": ["get", "color"] + } + } + ] +} diff --git a/test/integration/render-tests/symbol-sort-key/text-placement/expected.png b/test/integration/render-tests/symbol-sort-key/text-placement/expected.png new file mode 100644 index 00000000000..7ce1c08d003 Binary files /dev/null and b/test/integration/render-tests/symbol-sort-key/text-placement/expected.png differ diff --git a/test/integration/render-tests/symbol-sort-key/text-placement/style.json b/test/integration/render-tests/symbol-sort-key/text-placement/style.json new file mode 100644 index 00000000000..60a2136dfe1 --- /dev/null +++ b/test/integration/render-tests/symbol-sort-key/text-placement/style.json @@ -0,0 +1,71 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 64, + "width": 64 + } + }, + "center": [0, 30], + "zoom": 1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "color": "red", + "sort-key": 1, + "image": "bank-12" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 1, + 34 + ] + } + }, + { + "type": "Feature", + "properties": { + "color": "green", + "sort-key": 0, + "image": "bank-12" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 1, + 30 + ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "icon", + "type": "symbol", + "source": "geojson", + "layout": { + "symbol-sort-key": ["get", "sort-key"], + "text-field": ["get", "sort-key"], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-halo-width": 5, + "text-halo-color": ["get", "color"] + } + } + ] +}