diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index b333170761f..0511c2f5b01 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -292,6 +292,7 @@ class SymbolBucket implements Bucket { uploaded: boolean; sourceLayerIndex: number; sourceID: string; + symbolInstanceIndexes: Array; constructor(options: BucketParameters) { this.collisionBoxArray = options.collisionBoxArray; @@ -704,6 +705,31 @@ 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; @@ -719,33 +745,14 @@ 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.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); diff --git a/src/symbol/placement.js b/src/symbol/placement.js index 482f25ca1d0..cccb5f1eccf 100644 --- a/src/symbol/placement.js +++ b/src/symbol/placement.js @@ -300,149 +300,157 @@ 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 (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]); } } diff --git a/test/integration/render-tests/symbol-z-order/viewport-y/expected.png b/test/integration/render-tests/symbol-z-order/viewport-y/expected.png new file mode 100644 index 00000000000..3a5bf1ea918 Binary files /dev/null and b/test/integration/render-tests/symbol-z-order/viewport-y/expected.png differ diff --git a/test/integration/render-tests/symbol-z-order/viewport-y/style.json b/test/integration/render-tests/symbol-z-order/viewport-y/style.json new file mode 100644 index 00000000000..314838987c5 --- /dev/null +++ b/test/integration/render-tests/symbol-z-order/viewport-y/style.json @@ -0,0 +1,90 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 128, + "width": 128 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 0, + "pitch": 0, + "sources": { + "icon-source": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 7, + 7 + ] + }, + "properties": { + "icon": "building", + "title": "building" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 14, + 14 + ] + }, + "properties": { + "icon": "restaurant", + "title": "restaurant" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 0 + ] + }, + "properties": { + "icon": "school", + "title": "school" + } + } + ] + } + } + }, + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "icons", + "type": "symbol", + "source": "icon-source", + "layout": { + "symbol-z-order": "viewport-y", + "icon-image": "{icon}-12", + "icon-allow-overlap": true, + "icon-ignore-placement": true, + "text-optional": true, + "text-field": "{title}", + "text-font": ["Open Sans Semibold", "Arial Unicode MS Bold"], + "text-offset": [0, 0.6], + "text-anchor": "top" + }, + "paint": { + "text-color": "black" + } + } + ] +}