maplibre / maplibre-gl-js

MapLibre GL JS - Interactive vector tile maps in the browser
https://maplibre.org/maplibre-gl-js/docs/
Other
6.44k stars 691 forks source link

Alternate anchor placement for line labels #1127

Open drwestco opened 2 years ago

drwestco commented 2 years ago

Motivation

The set of anchors, or possible places a symbol may appear along a line, is generated by the worker, and is done independently per line. The set is affected by the size of the label, the value of symbol-spacing, the length of the line segment, etc. Once the anchors are calculated for a given zoom level, that set is fixed and cannot be added to, or have any anchors within shift position.

During the placement pass, when determining collisions between multiple symbols, anchors calculated in the previous step may be rejected. This reduces the number of labels shown along a given line.

In some cases, depending on line length, the size of the label text, and the symbol-spacing value for the layer, there may only be a single anchor available.

The example here is contrived to illustrate the problem. It happens fairly regularly with real city street data, where short roads and long labels regularly lead to sparsely-labeled areas.

symbol-spacing=400

image

If another symbol happens to collide with that line label, that's it - no other position along the line is considered, even though there's plenty of space.

image

Design Alternatives

Use existing symbol-spacing behavior

Individual cases can be worked around by adjusting the symbol-spacing value for a layer.

symbol-spacing=300

image

However, since the text and line geometry determine the available space, crafting the appropriate symbol-spacing rule for a layer as a whole can be difficult, and may lead to over-crowding the line when even smaller values are used.

symbol-spacing=20; also note the weird extra space towards the end of the line

image

Move symbol-spacing evaluation to placement step

Conceptually, it makes sense to only consider symbol-spacing relative to a symbol that has actually been placed. If a collision knocks out a symbol, the placement code shouldn't need to move XX units from that now-unused anchor position before placing another instance of that symbol. The next spot along the line that clears the collision region should be available for label placement.

image

Design

Three parts to be considered to support this: style, layout, and placement.

Style

Need to add one or more properties to opt-in to the new behavior. Need to consider how it interacts with the existing symbol-spacing, [icon|text]-overlap, and [icon|text]-ignore-placement styles.

Layout

symbol-spacing evaluation needs to be removed from consideration when generating the anchor points for line labels. Instead, the layout system generates a larger set of possibly-overlapping anchors, increasing the pool of available placements for a symbol. The rules for how many and how closely spaced these anchors should be need to be figured out.

Placement

The pool of anchors generated by Layout is whittled down by collisions, and by evaluating symbol-spacing at this step. Once a symbol is placed, the anchorIsTooClose function (also moved from Layout to Placement) rejects anchors for the same symbol appearing closer than symbol-spacing allows.

Drawbacks / Concerns

Performance

The additional anchors generated during layout increases the number of collisions to be evaluated during placement. In the example here, two anchors are rejected before finding the third one that avoids colliding with the foreground symbol.

image

Also, this increases the data passed between worker and main thread, so overall performance needs to be evaluated.

User Experience

The layout code has an extremely long history, and the comments discuss an emphasis on label stability across zoom levels. Need to figure out some way of determining if the change in experience is for the better or for the worse.

Since symbols are placed feature-by-feature, labels placed on multi-lane roads (i.e., highway shields) wind up only on a single lane, instead of alternating. This is an issue with the current maplibre code as well.

image

ambientlight commented 2 years ago

@drwestco: your proposal seems to result in tighter placement of labels, which arguably could be of some help, curious, do you see any benefit in the following hypothetical multi-pass approach:

  1. layout phase is untouched - anchors calculated according to the symbol-spacing and other layout style properties as is.
  2. the placement phase that considers collisions can again perform the layout - re-layout. (I don't know how it looks in terms of code as I don't yet have a good mental picture of placement code). The relayout can be enabled per layer in style - when enabled -> all previously produced anchors from layout phase as recalculated within the feature geometries but with occlusions (collisions) taken into account. Having this togglable per layer can also help to control the perf impact, since style designer can decide which layers need relayout.

This might help not to explode the amount of anchors produced during layout. I haven't started prototyping this yet though.