From b5d1a6eb08ac93a9ba319d264f14782a5b0e1717 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 2 Jul 2014 13:29:00 -0700 Subject: [PATCH] separate some symbol properties into separate ones for text and icons -allow-overlap -ignore-placement -rotation-alignment Also adds logic for requiring both text and icon, or just icon, or just text. --- js/geometry/symbolbucket.js | 101 +++++++++++++++++++++++++++++------- js/render/drawsymbol.js | 2 +- js/text/collision.js | 78 ---------------------------- js/text/placement.js | 55 ++++++++++++++++++-- 4 files changed, 135 insertions(+), 101 deletions(-) diff --git a/js/geometry/symbolbucket.js b/js/geometry/symbolbucket.js index eed9e992ae1..7df77e70e6c 100644 --- a/js/geometry/symbolbucket.js +++ b/js/geometry/symbolbucket.js @@ -16,13 +16,20 @@ if (typeof self !== 'undefined') { module.exports = SymbolBucket; +var fullRange = [2 * Math.PI , 0]; + function SymbolBucket(info, buffers, collision, elementGroups) { this.info = info; this.buffers = buffers; this.collision = collision; - if (info['symbol-placement'] === 'line' && !info.hasOwnProperty('symbol-rotation-alignment')) { - info['symbol-rotation-alignment'] = 'map'; + if (info['symbol-placement'] === 'line') { + if (!info.hasOwnProperty('text-rotation-alignment')) { + info['text-rotation-alignment'] = 'map'; + } + if (!info.hasOwnProperty('icon-rotation-alignment')) { + info['icon-rotation-alignment'] = 'map'; + } } if (elementGroups) { @@ -74,7 +81,7 @@ SymbolBucket.prototype.addFeatures = function() { } if (!shaping && !image) continue; - this.addFeature(lines, this.stacks, shaping, image); + this.addFeature(lines, this.stacks, shaping, image, text); } }; @@ -82,14 +89,16 @@ function byScale(a, b) { return a.scale - b.scale; } -SymbolBucket.prototype.addFeature = function(lines, faces, shaping, image) { +SymbolBucket.prototype.addFeature = function(lines, faces, shaping, image, text) { + if (!text) return; var info = this.info; var collision = this.collision; var minScale = 0.5; var glyphSize = 24; - var horizontal = info['symbol-rotation-alignment'] === 'viewport', + var horizontalText = info['text-rotation-alignment'] === 'viewport', + horizontalIcon = info['icon-rotation-alignment'] === 'viewport', fontScale = info['text-max-size'] / glyphSize, textBoxScale = collision.tilePixelRatio * fontScale, iconBoxScale = collision.tilePixelRatio * info['icon-max-size']; @@ -119,34 +128,88 @@ SymbolBucket.prototype.addFeature = function(lines, faces, shaping, image) { for (var j = 0, len = anchors.length; j < len; j++) { var anchor = anchors[j]; - var symbols = { - glyphs: [], - icons: [], - boxes: [] - }; + var symbols = {}; + + // Calculate the scales at which the text and icons can be first shown without overlap + var glyphScale = null; + var iconScale = null; - if (shaping) Placement.getGlyphs(symbols, anchor, origin, shaping, faces, textBoxScale, horizontal, line, info); - if (image) Placement.getIcon(symbols, anchor, image, iconBoxScale, line, this.spritePixelRatio, info); + if (shaping) { + symbols.glyphs = []; + symbols.glyphBoxes = []; + Placement.getGlyphs(symbols, anchor, origin, shaping, faces, textBoxScale, horizontalText, line, info); + if (horizontalText) { + // TODO merge this into getGlyphs to avoid creating the boxes in the first place + symbols.glyphBoxes = [Placement.getMergedGlyphs(symbols.glyphBoxes, anchor)]; + } + glyphScale = info['text-allow-overlap'] ? symbols.minGlyphScale + : collision.getPlacementScale(symbols.glyphBoxes, symbols.minGlyphScale); - var place = collision.place(symbols.boxes, anchor, horizontal, info); + } + + if (image) { + symbols.icons = []; + symbols.iconBoxes = symbols.glyphBoxes || []; + Placement.getIcon(symbols, anchor, image, iconBoxScale, line, this.spritePixelRatio, info); + iconScale = info['icon-allow-overlap'] ? symbols.minIconScale + : collision.getPlacementScale(symbols.iconBoxes, symbols.minIconScale); + } - if (place) { - this.addSymbols(this.buffers.glyphVertex, this.elementGroups.text, symbols.glyphs, place); - this.addSymbols(this.buffers.iconVertex, this.elementGroups.icon, symbols.icons, place); + var required = 'both'; + if (required === 'both' && shaping && image) { + if (!iconScale || !glyphScale) continue; + iconScale = glyphScale = Math.max(iconScale, glyphScale); + } else if (required === 'icon') { + if (!iconScale) continue; + glyphScale = Math.max(iconScale, glyphScale); + } else if (required === 'text') { + if (!glyphScale) continue; + iconScale = Math.max(iconScale, glyphScale); } + + // Get the rotation ranges it is safe to show the glyphs + var glyphRange = !glyphScale || info['text-allow-overlap'] ? fullRange + : collision.getPlacementRange(symbols.glyphBoxes, glyphScale, horizontalText); + var iconRange = !iconScale || info['icon-allow-overlap'] ? fullRange + : collision.getPlacementRange(symbols.iconBoxes, iconScale, horizontalIcon); + + var maxRange = [ + Math.min(iconRange[0], glyphRange[0]), + Math.max(iconRange[1], glyphRange[1])]; + if (required === 'both' && shaping && image) { + iconRange = glyphRange = maxRange; + } else if (required === 'icon') { + glyphRange = maxRange; + } else if (required === 'text') { + iconRange = maxRange; + } + + if (glyphScale) { + if (!info['text-ignore-placement']) { + collision.insert(symbols.glyphBoxes, anchor, glyphScale, glyphRange, horizontalText); + } + this.addSymbols(this.buffers.glyphVertex, this.elementGroups.text, symbols.glyphs, glyphScale, glyphRange); + } + + if (iconScale) { + if (!info['icon-ignore-placement']) { + collision.insert(symbols.iconBoxes, anchor, iconScale, iconRange, horizontalIcon); + } + this.addSymbols(this.buffers.iconVertex, this.elementGroups.icon, symbols.icons, glyphScale, glyphRange); + } + } } }; -SymbolBucket.prototype.addSymbols = function(buffer, elementGroups, symbols, place) { +SymbolBucket.prototype.addSymbols = function(buffer, elementGroups, symbols, scale, placementRange) { var zoom = this.collision.zoom; elementGroups.makeRoomFor(0); var elementGroup = elementGroups.current; - var placementZoom = place.zoom + zoom; - var placementRange = place.rotationRange; + var placementZoom = Math.log(scale) / Math.LN2 + zoom; for (var k = 0; k < symbols.length; k++) { diff --git a/js/render/drawsymbol.js b/js/render/drawsymbol.js index 117efac963d..d520a778da7 100644 --- a/js/render/drawsymbol.js +++ b/js/render/drawsymbol.js @@ -23,7 +23,7 @@ function drawSymbol(gl, painter, bucket, layerStyle, posMatrix, params, imageSpr var info = bucket.info; var exMatrix = mat4.clone(painter.projectionMatrix); - if (info['symbol-rotation-alignment'] === 'map') { + if (info[prefix + '-rotation-alignment'] === 'map') { mat4.rotateZ(exMatrix, exMatrix, painter.transform.angle); } diff --git a/js/text/collision.js b/js/text/collision.js index 6374d0284db..efa1660312a 100644 --- a/js/text/collision.js +++ b/js/text/collision.js @@ -48,60 +48,6 @@ function Collision(zoom, tileExtent, tileSize) { }], new Point(m, m), 1, [Math.PI * 2, 0], false, 2); } -Collision.prototype.place = function(boxes, anchor, horizontal, props) { - - var allowOverlap = props['symbol-allow-overlap']; - var ignorePlacement = props['symbol-ignore-placement']; - var minPlacementScale = anchor.scale; - - var minScale = Infinity; - for (var m = 0; m < boxes.length; m++) { - minScale = Math.min(minScale, boxes[m].minScale); - } - minPlacementScale = Math.max(minPlacementScale, minScale); - - if (horizontal) { - // Collision checks between rotating and fixed labels are relatively expensive, - // so we use one box per label, not per glyph for horizontal labels. - boxes = [getMergedGlyphs(boxes, anchor)]; - - // for all horizontal labels, calculate bbox covering all rotated positions - var box = boxes[0].box, - x12 = box.x1 * box.x1, - y12 = box.y1 * box.y1, - x22 = box.x2 * box.x2, - y22 = box.y2 * box.y2, - diag = Math.sqrt(Math.max(x12 + y12, x12 + y22, x22 + y12, x22 + y22)); - - boxes[0].hBox = { - x1: -diag, - y1: -diag, - x2: diag, - y2: diag - }; - } - - // Calculate the minimum scale the entire label can be shown without collisions - var scale = allowOverlap ? minPlacementScale : - this.getPlacementScale(boxes, minPlacementScale); - - // Return if the label can never be placed without collision - if (scale === null) return null; - - // Calculate the range it is safe to rotate all glyphs - var rotationRange = allowOverlap ? [2 * Math.PI, 0] : this.getPlacementRange(boxes, scale, horizontal); - - if (!ignorePlacement) this.insert(boxes, anchor, scale, rotationRange, horizontal); - - var zoom = Math.log(scale) / Math.LN2; - - return { - zoom: zoom, - rotationRange: rotationRange - }; -}; - - Collision.prototype.getPlacementScale = function(glyphs, minPlacementScale) { for (var k = 0; k < glyphs.length; k++) { @@ -271,27 +217,3 @@ Collision.prototype.insert = function(glyphs, anchor, placementScale, placementR (horizontal ? this.hTree : this.cTree).load(allBounds); }; - -function getMergedGlyphs(glyphs, anchor) { - - var mergedglyphs = { - box: { x1: Infinity, y1: Infinity, x2: -Infinity, y2: -Infinity }, - anchor: anchor, - minScale: 0, - padding: -Infinity - }; - - var box = mergedglyphs.box; - - for (var m = 0; m < glyphs.length; m++) { - var gbox = glyphs[m].box; - box.x1 = Math.min(box.x1, gbox.x1); - box.y1 = Math.min(box.y1, gbox.y1); - box.x2 = Math.max(box.x2, gbox.x2); - box.y2 = Math.max(box.y2, gbox.y2); - mergedglyphs.minScale = Math.max(mergedglyphs.minScale, glyphs[m].minScale); - mergedglyphs.padding = Math.max(mergedglyphs.padding, glyphs[m].padding); - } - - return mergedglyphs; -} diff --git a/js/text/placement.js b/js/text/placement.js index b2c27f8861f..10cd519ae7f 100644 --- a/js/text/placement.js +++ b/js/text/placement.js @@ -4,7 +4,8 @@ var Point = require('point-geometry'); module.exports = { getIcon: getIcon, - getGlyphs: getGlyphs + getGlyphs: getGlyphs, + getMergedGlyphs: getMergedGlyphs }; var minScale = 0.5; // underscale by 1 zoom level @@ -28,7 +29,7 @@ function getIcon(result, anchor, image, boxScale, line, spritePixelRatio, props) y2: y2 * boxScale }; - result.boxes.push({ + result.iconBoxes.push({ box: box, anchor: anchor, minScale: minScale, @@ -69,6 +70,8 @@ function getIcon(result, anchor, image, boxScale, line, spritePixelRatio, props) minScale: minScale, maxScale: Infinity }); + + result.minIconScale = anchor.scale; } function getGlyphs(result, anchor, origin, shaping, faces, boxScale, horizontal, line, props) { @@ -78,7 +81,7 @@ function getGlyphs(result, anchor, origin, shaping, faces, boxScale, horizontal, var padding = props['text-padding']; var glyphs = result.glyphs, - boxes = result.boxes; + boxes = result.glyphBoxes; var buffer = 3; @@ -188,6 +191,13 @@ function getGlyphs(result, anchor, origin, shaping, faces, boxScale, horizontal, } } } + + var minPlacementScale = anchor.scale; + var minIconScale = Infinity; + for (var m = 0; m < boxes.length; m++) { + minIconScale = Math.min(minIconScale, boxes[m].minScale); + } + result.minGlyphScale = Math.max(minPlacementScale, minScale); } function getSegmentGlyphs(glyphs, anchor, offset, line, segment, direction, maxAngleDelta) { @@ -250,3 +260,42 @@ function getSegmentGlyphs(glyphs, anchor, offset, line, segment, direction, maxA prevAngle = angle; } } + +function getMergedGlyphs(glyphs, anchor) { + // Collision checks between rotating and fixed labels are relatively expensive, + // so we use one box per label, not per glyph for horizontal labels. + + var mergedglyphs = { + box: { x1: Infinity, y1: Infinity, x2: -Infinity, y2: -Infinity }, + anchor: anchor, + minScale: 0, + padding: -Infinity + }; + + var box = mergedglyphs.box; + + for (var m = 0; m < glyphs.length; m++) { + var gbox = glyphs[m].box; + box.x1 = Math.min(box.x1, gbox.x1); + box.y1 = Math.min(box.y1, gbox.y1); + box.x2 = Math.max(box.x2, gbox.x2); + box.y2 = Math.max(box.y2, gbox.y2); + mergedglyphs.minScale = Math.max(mergedglyphs.minScale, glyphs[m].minScale); + mergedglyphs.padding = Math.max(mergedglyphs.padding, glyphs[m].padding); + } + // for all horizontal labels, calculate bbox covering all rotated positions + var x12 = box.x1 * box.x1, + y12 = box.y1 * box.y1, + x22 = box.x2 * box.x2, + y22 = box.y2 * box.y2, + diag = Math.sqrt(Math.max(x12 + y12, x12 + y22, x22 + y12, x22 + y22)); + + mergedglyphs.hBox = { + x1: -diag, + y1: -diag, + x2: diag, + y2: diag + }; + + return mergedglyphs; +}