Skip to content

Commit

Permalink
separate some symbol properties into separate ones for text and icons
Browse files Browse the repository at this point in the history
-allow-overlap
-ignore-placement
-rotation-alignment

Also adds logic for requiring both text and icon, or just icon, or just
text.
  • Loading branch information
ansis committed Jul 2, 2014
1 parent 549eb1c commit b5d1a6e
Showing 4 changed files with 135 additions and 101 deletions.
101 changes: 82 additions & 19 deletions js/geometry/symbolbucket.js
Original file line number Diff line number Diff line change
@@ -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,22 +81,24 @@ SymbolBucket.prototype.addFeatures = function() {
}

if (!shaping && !image) continue;
this.addFeature(lines, this.stacks, shaping, image);
this.addFeature(lines, this.stacks, shaping, image, text);
}
};

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';

This comment has been minimized.

Copy link
@mourner

mourner Jul 2, 2014

Member

So it's always "both"? Missing a "TODO" then :)

This comment has been minimized.

Copy link
@ansis

ansis Jul 2, 2014

Author Contributor

yep, exposed as 'symbol-required' in next commit. The name could probably better

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++) {

2 changes: 1 addition & 1 deletion js/render/drawsymbol.js
Original file line number Diff line number Diff line change
@@ -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);
}

78 changes: 0 additions & 78 deletions js/text/collision.js
Original file line number Diff line number Diff line change
@@ -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;
}
55 changes: 52 additions & 3 deletions js/text/placement.js
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit b5d1a6e

Please sign in to comment.