Skip to content

Commit

Permalink
Add 'symbol-placement: line-center'.
Browse files Browse the repository at this point in the history
With this placement option, we will attempt to place a single label in the center of each "line" geometry of a feature. If the label doesn't fit or the 'text-max-angle' check fails, we don't place anything.
Labels using 'line-center' are allowed to extend past the edge of the tile they're centered in, following line geometry included in the tile's buffers.
  • Loading branch information
ChrisLoer committed Jun 26, 2018
1 parent a3aaa76 commit e33d2fa
Show file tree
Hide file tree
Showing 16 changed files with 293 additions and 30 deletions.
2 changes: 1 addition & 1 deletion flow-typed/style-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ declare type SymbolLayerSpecification = {|
"maxzoom"?: number,
"filter"?: FilterSpecification,
"layout"?: {|
"symbol-placement"?: PropertyValueSpecification<"point" | "line">,
"symbol-placement"?: PropertyValueSpecification<"point" | "line" | "line-center">,
"symbol-spacing"?: PropertyValueSpecification<number>,
"symbol-avoid-edges"?: PropertyValueSpecification<boolean>,
"icon-allow-overlap"?: PropertyValueSpecification<boolean>,
Expand Down
2 changes: 1 addition & 1 deletion src/data/bucket/symbol_bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ class SymbolBucket implements Bucket {
if (text) {
const fontStack = textFont.evaluate(feature, {}).join(',');
const stack = stacks[fontStack] = stacks[fontStack] || {};
const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') === 'line';
const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point';
const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text);
for (let i = 0; i < text.length; i++) {
stack[text.charCodeAt(i)] = true;
Expand Down
2 changes: 1 addition & 1 deletion src/render/draw_symbol.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate

const rotateWithMap = rotationAlignment === 'map';
const pitchWithMap = pitchAlignment === 'map';
const alongLine = rotateWithMap && layer.layout.get('symbol-placement') === 'line';
const alongLine = rotateWithMap && layer.layout.get('symbol-placement') !== 'point';
// Line label rotation happens in `updateLineLabels`
// Pitched point labels are automatically rotated by the labelPlaneMatrix projection
// Unpitched point labels need to have their rotation applied after projection
Expand Down
26 changes: 19 additions & 7 deletions src/style-spec/reference/v8.json
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,9 @@
},
"line": {
"doc": "The label is placed along the line of the geometry. Can only be used on `LineString` and `Polygon` geometries."
},
"line-center": {
"doc": "The label is placed at the center of the line of the geometry. Can only be used on `LineString` and `Polygon` geometries. Note that a single feature in a vector tile may contain multiple line geometries. The center is calculated based on the entirety of the geometry included in the tile buffers, and the label may extend past the edge of the tile."
}
},
"default": "point",
Expand Down Expand Up @@ -1008,13 +1011,13 @@
"type": "enum",
"values": {
"map": {
"doc": "When `symbol-placement` is set to `point`, aligns icons east-west. When `symbol-placement` is set to `line`, aligns icon x-axes with the line."
"doc": "When `symbol-placement` is set to `point`, aligns icons east-west. When `symbol-placement` is set to `line` or `line-center`, aligns icon x-axes with the line."
},
"viewport": {
"doc": "Produces icons whose x-axes are aligned with the x-axis of the viewport, regardless of the value of `symbol-placement`."
},
"auto": {
"doc": "When `symbol-placement` is set to `point`, this is equivalent to `viewport`. When `symbol-placement` is set to `line`, this is equivalent to `map`."
"doc": "When `symbol-placement` is set to `point`, this is equivalent to `viewport`. When `symbol-placement` is set to `line` or `line-center`, this is equivalent to `map`."
}
},
"default": "auto",
Expand Down Expand Up @@ -1251,7 +1254,10 @@
"icon-rotation-alignment": "map"
},
{
"symbol-placement": "line"
"symbol-placement": [
"line",
"line-center"
]
}
],
"sdk-support": {
Expand Down Expand Up @@ -1445,13 +1451,13 @@
"type": "enum",
"values": {
"map": {
"doc": "When `symbol-placement` is set to `point`, aligns text east-west. When `symbol-placement` is set to `line`, aligns text x-axes with the line."
"doc": "When `symbol-placement` is set to `point`, aligns text east-west. When `symbol-placement` is set to `line` or `line-center`, aligns text x-axes with the line."
},
"viewport": {
"doc": "Produces glyphs whose x-axes are aligned with the x-axis of the viewport, regardless of the value of `symbol-placement`."
},
"auto": {
"doc": "When `symbol-placement` is set to `point`, this is equivalent to `viewport`. When `symbol-placement` is set to `line`, this is equivalent to `map`."
"doc": "When `symbol-placement` is set to `point`, this is equivalent to `viewport`. When `symbol-placement` is set to `line` or `line-center`, this is equivalent to `map`."
}
},
"default": "auto",
Expand Down Expand Up @@ -1772,7 +1778,10 @@
"requires": [
"text-field",
{
"symbol-placement": "line"
"symbol-placement": [
"line",
"line-center"
]
}
],
"sdk-support": {
Expand Down Expand Up @@ -1860,7 +1869,10 @@
"text-rotation-alignment": "map"
},
{
"symbol-placement": "line"
"symbol-placement": [
"line",
"line-center"
]
}
],
"sdk-support": {
Expand Down
4 changes: 2 additions & 2 deletions src/style/style_layer/symbol_style_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ class SymbolStyleLayer extends StyleLayer {
super.recalculate(parameters);

if (this.layout.get('icon-rotation-alignment') === 'auto') {
if (this.layout.get('symbol-placement') === 'line') {
if (this.layout.get('symbol-placement') !== 'point') {
this.layout._values['icon-rotation-alignment'] = 'map';
} else {
this.layout._values['icon-rotation-alignment'] = 'viewport';
}
}

if (this.layout.get('text-rotation-alignment') === 'auto') {
if (this.layout.get('symbol-placement') === 'line') {
if (this.layout.get('symbol-placement') !== 'point') {
this.layout._values['text-rotation-alignment'] = 'map';
} else {
this.layout._values['text-rotation-alignment'] = 'viewport';
Expand Down
77 changes: 65 additions & 12 deletions src/symbol/get_anchors.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,68 @@ import checkMaxAngle from './check_max_angle';
import type Point from '@mapbox/point-geometry';
import type {Shaping, PositionedIcon} from './shaping';

export default getAnchors;
export { getAnchors, getCenterAnchor };

function getLineLength(line: Array<Point>): number {
let lineLength = 0;
for (let k = 0; k < line.length - 1; k++) {
lineLength += line[k].dist(line[k + 1]);
}
return lineLength;
}

function getAngleWindowSize(shapedText: ?Shaping,
glyphSize: number,
boxScale: number): number {
return shapedText ?
3 / 5 * glyphSize * boxScale :
0;
}

function getLabelLength(shapedText: ?Shaping, shapedIcon: ?PositionedIcon): number {
return Math.max(
shapedText ? shapedText.right - shapedText.left : 0,
shapedIcon ? shapedIcon.right - shapedIcon.left : 0);
}

function getCenterAnchor(line: Array<Point>,
maxAngle: number,
shapedText: ?Shaping,
shapedIcon: ?PositionedIcon,
glyphSize: number,
boxScale: number) {
const angleWindowSize = getAngleWindowSize(shapedText, glyphSize, boxScale);
const labelLength = getLabelLength(shapedText, shapedIcon);

let prevDistance = 0;
const centerDistance = getLineLength(line) / 2;

for (let i = 0; i < line.length - 1; i++) {

const a = line[i],
b = line[i + 1];

const segmentDistance = a.dist(b);

if (prevDistance + segmentDistance > centerDistance) {
// The center is on this segment
const t = (centerDistance - prevDistance) / segmentDistance,
x = interpolate(a.x, b.x, t),
y = interpolate(a.y, b.y, t);

const anchor = new Anchor(x, y, b.angleTo(a), i);
anchor._round();
if (angleWindowSize &&
!checkMaxAngle(line, anchor, labelLength, angleWindowSize, maxAngle)) {
return;
}

return anchor;
}

prevDistance += segmentDistance;
}
}

function getAnchors(line: Array<Point>,
spacing: number,
Expand All @@ -24,13 +85,8 @@ function getAnchors(line: Array<Point>,
// potential label passes text-max-angle check and has enough froom to fit
// on the line.

const angleWindowSize = shapedText ?
3 / 5 * glyphSize * boxScale :
0;

const labelLength = Math.max(
shapedText ? shapedText.right - shapedText.left : 0,
shapedIcon ? shapedIcon.right - shapedIcon.left : 0);
const angleWindowSize = getAngleWindowSize(shapedText, glyphSize, boxScale);
const labelLength = getLabelLength(shapedText, shapedIcon);

// Is the line continued from outside the tile boundary?
const isLineContinued = line[0].x === 0 || line[0].x === tileExtent || line[0].y === 0 || line[0].y === tileExtent;
Expand Down Expand Up @@ -59,10 +115,7 @@ function getAnchors(line: Array<Point>,
function resample(line, offset, spacing, angleWindowSize, maxAngle, labelLength, isLineContinued, placeAtMiddle, tileExtent) {

const halfLabelLength = labelLength / 2;
let lineLength = 0;
for (let k = 0; k < line.length - 1; k++) {
lineLength += line[k].dist(line[k + 1]);
}
const lineLength = getLineLength(line);

let distance = 0,
markedDistance = offset - spacing;
Expand Down
27 changes: 22 additions & 5 deletions src/symbol/symbol_layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import Anchor from './anchor';

import getAnchors from './get_anchors';
import { getAnchors, getCenterAnchor } from './get_anchors';
import clipLine from './clip_line';
import OpacityState from './opacity_state';
import { shapeText, shapeIcon, WritingMode } from './shaping';
Expand Down Expand Up @@ -94,7 +94,7 @@ export function performSymbolLayout(bucket: SymbolBucket,

const oneEm = 24;
const lineHeight = layout.get('text-line-height') * oneEm;
const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') === 'line';
const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point';
const keepUpright = layout.get('text-keep-upright');


Expand All @@ -111,7 +111,7 @@ export function performSymbolLayout(bucket: SymbolBucket,
const spacingIfAllowed = allowsLetterSpacing(text) ? spacing : 0;
const textAnchor = layout.get('text-anchor').evaluate(feature, {});
const textJustify = layout.get('text-justify').evaluate(feature, {});
const maxWidth = layout.get('symbol-placement') !== 'line' ?
const maxWidth = layout.get('symbol-placement') === 'point' ?
layout.get('text-max-width').evaluate(feature, {}) * oneEm :
0;

Expand Down Expand Up @@ -191,8 +191,8 @@ function addFeature(bucket: SymbolBucket,
textPadding = layout.get('text-padding') * bucket.tilePixelRatio,
iconPadding = layout.get('icon-padding') * bucket.tilePixelRatio,
textMaxAngle = layout.get('text-max-angle') / 180 * Math.PI,
textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') === 'line',
iconAlongLine = layout.get('icon-rotation-alignment') === 'map' && layout.get('symbol-placement') === 'line',
textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point',
iconAlongLine = layout.get('icon-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point',
symbolPlacement = layout.get('symbol-placement'),
textRepeatDistance = symbolMinDistance / 2;

Expand Down Expand Up @@ -231,6 +231,23 @@ function addFeature(bucket: SymbolBucket,
}
}
}
} else if (symbolPlacement === 'line-center') {
// No clipping, multiple lines per feature are allowed
// "lines" with only one point are ignored as in clipLines
for (const line of feature.geometry) {
if (line.length > 1) {
const anchor = getCenterAnchor(
line,
textMaxAngle,
shapedTextOrientations.vertical || shapedTextOrientations.horizontal,
shapedIcon,
glyphSize,
textMaxBoxScale);
if (anchor) {
addSymbolAtAnchor(line, anchor);
}
}
}
} else if (feature.type === 'Polygon') {
for (const polygon of classifyRings(feature.geometry, 0)) {
// 16 here represents 2 pixels
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"version": 8,
"metadata": {
"test": {
"description": "'Carribean Sea' crosses a tile boundary, but we don't draw the tile boundary in the test because JS and Native render tile boundaries differently.",
"height": 256,
"width": 1024
}
},
"center": [
-73,
15
],
"zoom": 4.5,
"sources": {
"mapbox": {
"type": "vector",
"maxzoom": 14,
"tiles": [
"local://tiles/mapbox.mapbox-streets-v7/{z}-{x}-{y}.mvt"
]
}
},
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "white"
}
},
{
"id": "line-center",
"type": "symbol",
"source": "mapbox",
"source-layer": "marine_label",
"layout": {
"text-field": "{name_en}",
"symbol-placement": "line-center",
"text-allow-overlap": true,
"text-size": 50,
"text-letter-spacing": 0.5,
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
]
}
},
{
"id": "line",
"type": "line",
"source": "mapbox",
"source-layer": "marine_label",
"paint": {
"line-width": 1
}
}
]
}
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 e33d2fa

Please sign in to comment.