UNPKG

maplibre-gl

Version:

BSD licensed community fork of mapbox-gl, a WebGL interactive maps library

164 lines (138 loc) 5.91 kB
import {VariableAnchorOffsetCollection, type VariableAnchorOffsetCollectionSpecification} from '@maplibre/maplibre-gl-style-spec'; import {type SymbolFeature} from '../../data/bucket/symbol_bucket'; import {type CanonicalTileID} from '../../source/tile_id'; import ONE_EM from '../../symbol/one_em'; import {type SymbolStyleLayer} from './symbol_style_layer'; export enum TextAnchorEnum { 'center' = 1, 'left' = 2, 'right' = 3, 'top' = 4, 'bottom' = 5, 'top-left' = 6, 'top-right' = 7, 'bottom-left' = 8, 'bottom-right' = 9 } export type TextAnchor = keyof typeof TextAnchorEnum; // The radial offset is to the edge of the text box // In the horizontal direction, the edge of the text box is where glyphs start // But in the vertical direction, the glyphs appear to "start" at the baseline // We don't actually load baseline data, but we assume an offset of ONE_EM - 17 // (see "yOffset" in shaping.js) const baselineOffset = 7; export const INVALID_TEXT_OFFSET = Number.POSITIVE_INFINITY; export function evaluateVariableOffset(anchor: TextAnchor, offset: [number, number]): [number, number] { function fromRadialOffset(anchor: TextAnchor, radialOffset: number): [number, number] { let x = 0, y = 0; if (radialOffset < 0) radialOffset = 0; // Ignore negative offset. // solve for r where r^2 + r^2 = radialOffset^2 const hypotenuse = radialOffset / Math.SQRT2; switch (anchor) { case 'top-right': case 'top-left': y = hypotenuse - baselineOffset; break; case 'bottom-right': case 'bottom-left': y = -hypotenuse + baselineOffset; break; case 'bottom': y = -radialOffset + baselineOffset; break; case 'top': y = radialOffset - baselineOffset; break; } switch (anchor) { case 'top-right': case 'bottom-right': x = -hypotenuse; break; case 'top-left': case 'bottom-left': x = hypotenuse; break; case 'left': x = radialOffset; break; case 'right': x = -radialOffset; break; } return [x, y]; } function fromTextOffset(anchor: TextAnchor, offsetX: number, offsetY: number): [number, number] { let x = 0, y = 0; // Use absolute offset values. offsetX = Math.abs(offsetX); offsetY = Math.abs(offsetY); switch (anchor) { case 'top-right': case 'top-left': case 'top': y = offsetY - baselineOffset; break; case 'bottom-right': case 'bottom-left': case 'bottom': y = -offsetY + baselineOffset; break; } switch (anchor) { case 'top-right': case 'bottom-right': case 'right': x = -offsetX; break; case 'top-left': case 'bottom-left': case 'left': x = offsetX; break; } return [x, y]; } return (offset[1] !== INVALID_TEXT_OFFSET) ? fromTextOffset(anchor, offset[0], offset[1]) : fromRadialOffset(anchor, offset[0]); } // Helper to support both text-variable-anchor and text-variable-anchor-offset. Offset values converted from EMs to PXs export function getTextVariableAnchorOffset(layer: SymbolStyleLayer, feature: SymbolFeature, canonical: CanonicalTileID): VariableAnchorOffsetCollection | null { const layout = layer.layout; // If style specifies text-variable-anchor-offset, just return it const variableAnchorOffset = layout.get('text-variable-anchor-offset')?.evaluate(feature, {}, canonical); if (variableAnchorOffset) { const sourceValues = variableAnchorOffset.values; const destValues: VariableAnchorOffsetCollectionSpecification = []; // Convert offsets from EM to PX, and apply baseline shift for (let i = 0; i < sourceValues.length; i += 2) { const anchor = destValues[i] = sourceValues[i] as TextAnchor; const offset = (sourceValues[i + 1] as [number, number]).map(t => t * ONE_EM) as [number, number]; if (anchor.startsWith('top')) { offset[1] -= baselineOffset; } else if (anchor.startsWith('bottom')) { offset[1] += baselineOffset; } destValues[i + 1] = offset; } return new VariableAnchorOffsetCollection(destValues); } // If style specifies text-variable-anchor, convert to the new format const variableAnchor = layout.get('text-variable-anchor'); if (variableAnchor) { let textOffset: [number, number]; const unevaluatedLayout = layer._unevaluatedLayout; // The style spec says don't use `text-offset` and `text-radial-offset` together // but doesn't actually specify what happens if you use both. We go with the radial offset. if (unevaluatedLayout.getValue('text-radial-offset') !== undefined) { textOffset = [layout.get('text-radial-offset').evaluate(feature, {}, canonical) * ONE_EM, INVALID_TEXT_OFFSET]; } else { textOffset = layout.get('text-offset').evaluate(feature, {}, canonical).map(t => t * ONE_EM) as [number, number]; } const anchorOffsets: VariableAnchorOffsetCollectionSpecification = []; for (const anchor of variableAnchor) { anchorOffsets.push(anchor, evaluateVariableOffset(anchor, textOffset)); } return new VariableAnchorOffsetCollection(anchorOffsets); } return null; }