@tangram.gl/basemap-layers 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +34 -2
  2. package/THIRD-PARTY-NOTICES.md +205 -0
  3. package/dist/geojson-worker.js +2160 -0
  4. package/dist/geojson-worker.js.map +6 -0
  5. package/dist/index.cjs +11388 -0
  6. package/dist/index.cjs.map +6 -0
  7. package/dist/index.js +11369 -0
  8. package/dist/index.js.map +6 -0
  9. package/dist/map-style.cjs +445 -0
  10. package/dist/map-style.cjs.map +6 -0
  11. package/dist/map-style.js +431 -0
  12. package/dist/map-style.js.map +6 -0
  13. package/dist/types/arabic-shaping.d.ts +7 -0
  14. package/dist/types/atmosphere-layer.d.ts +10 -0
  15. package/dist/types/basemap-layer.d.ts +150 -0
  16. package/dist/types/bidi.d.ts +8 -0
  17. package/dist/types/circle-layer.d.ts +49 -0
  18. package/dist/types/fill-extrusion-lighting-extension.d.ts +49 -0
  19. package/dist/types/fill-outline-extension.d.ts +22 -0
  20. package/dist/types/fill-pattern.d.ts +99 -0
  21. package/dist/types/fill-style-extensions.d.ts +100 -0
  22. package/dist/types/geojson-source.d.ts +29 -0
  23. package/dist/types/geojson-tiler.d.ts +48 -0
  24. package/dist/types/geojson-worker.d.ts +40 -0
  25. package/dist/types/globe-horizon-extension.d.ts +61 -0
  26. package/dist/types/globe-layers.d.ts +239 -0
  27. package/dist/types/globe-line-angle-extension.d.ts +37 -0
  28. package/dist/types/glyph-font-renderer.d.ts +37 -0
  29. package/dist/types/glyph-pbf.d.ts +37 -0
  30. package/dist/types/glyph-store.d.ts +103 -0
  31. package/dist/types/grid-index.d.ts +47 -0
  32. package/dist/types/hillshade-layer.d.ts +154 -0
  33. package/dist/types/index.d.ts +9 -0
  34. package/dist/types/is-globe-viewport.d.ts +2 -0
  35. package/dist/types/label-font-renderer.d.ts +18 -0
  36. package/dist/types/label-placement.d.ts +247 -0
  37. package/dist/types/label-text-layer.d.ts +231 -0
  38. package/dist/types/line-blur-extension.d.ts +73 -0
  39. package/dist/types/line-glyphs.d.ts +60 -0
  40. package/dist/types/line-placement.d.ts +93 -0
  41. package/dist/types/map-style-loader.d.ts +48 -0
  42. package/dist/types/map-style-schema.d.ts +72 -0
  43. package/dist/types/map-style.d.ts +6 -0
  44. package/dist/types/mapbox-style.d.ts +40 -0
  45. package/dist/types/mvt-label-layer.d.ts +923 -0
  46. package/dist/types/polylabel.d.ts +17 -0
  47. package/dist/types/raster-color-extension.d.ts +146 -0
  48. package/dist/types/shared-texture.d.ts +34 -0
  49. package/dist/types/sort-key.d.ts +10 -0
  50. package/dist/types/sprite.d.ts +82 -0
  51. package/dist/types/style-accessor.d.ts +45 -0
  52. package/dist/types/style-expression.d.ts +83 -0
  53. package/dist/types/style-resolver.d.ts +94 -0
  54. package/dist/types/text-font.d.ts +36 -0
  55. package/dist/types/text-halo-blur-extension.d.ts +55 -0
  56. package/dist/types/text-layout.d.ts +71 -0
  57. package/dist/types/util.d.ts +22 -0
  58. package/dist/types/zoom-paint-extension.d.ts +72 -0
  59. package/package.json +80 -3
  60. package/src/arabic-shaping.ts +366 -0
  61. package/src/atmosphere-layer.ts +19 -0
  62. package/src/basemap-layer.ts +533 -0
  63. package/src/bidi.ts +199 -0
  64. package/src/circle-layer.ts +191 -0
  65. package/src/fill-extrusion-lighting-extension.ts +206 -0
  66. package/src/fill-outline-extension.ts +70 -0
  67. package/src/fill-pattern.ts +400 -0
  68. package/src/fill-style-extensions.ts +177 -0
  69. package/src/geojson-source.ts +318 -0
  70. package/src/geojson-tiler.ts +185 -0
  71. package/src/geojson-worker.ts +98 -0
  72. package/src/globe-horizon-extension.ts +64 -0
  73. package/src/globe-layers.ts +3487 -0
  74. package/src/globe-line-angle-extension.ts +131 -0
  75. package/src/glyph-font-renderer.ts +173 -0
  76. package/src/glyph-pbf.ts +164 -0
  77. package/src/glyph-store.ts +326 -0
  78. package/src/grid-index.ts +177 -0
  79. package/src/hillshade-layer.ts +775 -0
  80. package/src/index.ts +18 -0
  81. package/src/is-globe-viewport.ts +24 -0
  82. package/src/label-font-renderer.ts +159 -0
  83. package/src/label-placement.ts +727 -0
  84. package/src/label-text-layer.ts +913 -0
  85. package/src/line-blur-extension.ts +237 -0
  86. package/src/line-glyphs.ts +364 -0
  87. package/src/line-placement.ts +446 -0
  88. package/src/map-style-loader.ts +56 -0
  89. package/src/map-style-schema.ts +83 -0
  90. package/src/map-style.ts +23 -0
  91. package/src/mapbox-style.ts +168 -0
  92. package/src/mvt-label-layer.ts +3744 -0
  93. package/src/polylabel.ts +312 -0
  94. package/src/raster-color-extension.ts +245 -0
  95. package/src/shared-texture.ts +153 -0
  96. package/src/sort-key.ts +63 -0
  97. package/src/sprite.ts +244 -0
  98. package/src/style-accessor.ts +95 -0
  99. package/src/style-expression.ts +327 -0
  100. package/src/style-resolver.ts +301 -0
  101. package/src/text-font.ts +150 -0
  102. package/src/text-halo-blur-extension.ts +115 -0
  103. package/src/text-layout.ts +441 -0
  104. package/src/util.ts +47 -0
  105. package/src/zoom-paint-extension.ts +207 -0
@@ -0,0 +1,237 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ // deck.gl-community
6
+ // SPDX-License-Identifier: MIT
7
+ // Copyright (c) vis.gl contributors
8
+
9
+ import {LayerExtension} from '@deck.gl/core';
10
+ import type {Layer, UpdateParameters} from '@deck.gl/core';
11
+ import type {ShaderModule} from '@luma.gl/shadertools';
12
+
13
+ export type LineBlurExtensionOptions = {
14
+ /**
15
+ * Reads `getBlur` per path, from an attribute. Otherwise `getBlur` must be a constant, which a
16
+ * uniform carries: a `PathLayer` with `PathStyleExtension`'s path dashes and offsets already
17
+ * uses all 16 vertex attributes that WebGL2 guarantees, and does not link with one more.
18
+ */
19
+ perFeature: boolean;
20
+ };
21
+
22
+ type LineBlurProps = {
23
+ blur: number;
24
+ gapOffset: number;
25
+ gapSide: number;
26
+ };
27
+
28
+ const UNIFORMS = `\
29
+ layout(std140) uniform lineBlurUniforms {
30
+ float blur;
31
+ float gapOffset;
32
+ float gapSide;
33
+ } lineBlur;
34
+ `;
35
+
36
+ /**
37
+ * Adds `getBlur` to the `PathLayer`s of a layer drawn with `antialiasing` (for a `GeoJsonLayer`,
38
+ * `lineAntialiasing`): per path, the CSS pixels over which the edges fade out, as MapLibre GL JS
39
+ * draws `line-blur`, and, with a blur of 0, every line's antialiasing. MapLibre fades each edge
40
+ * linearly from the edge, which its antialiasing moves out by half a device pixel, to `blur` plus
41
+ * one device pixel inside it. On deck.gl's antialiased stroke alone, that fade leaves a line
42
+ * narrower than a pixel too faint, so lines without an offset (which is measured in widths) are
43
+ * drawn half a device pixel wider on each side, with their dashes scaled back to the line's width,
44
+ * and faded from their own edge. The fade follows round caps and joins, as in MapLibre. Other
45
+ * sublayers, and
46
+ * layers drawn without `antialiasing`, are left unchanged. WebGL only; on WebGPU lines draw
47
+ * without blur.
48
+ *
49
+ * `blurGapSide` serves the two lines of a `line-gap-width`, drawn with `PathStyleExtension`'s
50
+ * offsets: 1 or -1, the side of the gap's center that this line is on (as `getLineOffset`'s
51
+ * `side`), with `blurGapOffset` the CSS pixels of the gap's center from the path (`line-offset`).
52
+ * The line draws nothing past the gap's center, so that where a blur wider than half the gap makes
53
+ * the two lines overlap, their fades do not draw over each other. MapLibre draws the gap as one
54
+ * stroke whose fade is symmetric about its center, so each line draws its half. The default 0
55
+ * clips nothing, and neither does a path whose gap is 0: its one line is centered on the gap's
56
+ * center (`getLineOffset`), which the shader tells per path from the offset and width it already
57
+ * has, so a gap that is 0 at a zoom or for a feature needs no attribute of its own.
58
+ */
59
+ export class LineBlurExtension extends LayerExtension<LineBlurExtensionOptions> {
60
+ static extensionName = 'LineBlurExtension';
61
+ static defaultProps = {
62
+ getBlur: {type: 'accessor', value: 0},
63
+ blurGapSide: 0,
64
+ blurGapOffset: 0
65
+ };
66
+
67
+ constructor({perFeature = false}: Partial<LineBlurExtensionOptions> = {}) {
68
+ super({perFeature});
69
+ }
70
+
71
+ initializeState(this: Layer, context: unknown, extension: this) {
72
+ if (!isBlurredPathLayer(this) || !extension.opts.perFeature) {
73
+ return;
74
+ }
75
+ this.getAttributeManager()?.addInstanced({
76
+ instanceLineBlur: {size: 1, accessor: 'getBlur', defaultValue: 0}
77
+ });
78
+ }
79
+
80
+ updateState(this: Layer<any>, params: UpdateParameters<Layer<any>>, extension: this) {
81
+ if (!isBlurredPathLayer(this)) {
82
+ return;
83
+ }
84
+ const {getBlur, blurGapSide, blurGapOffset} = this.props;
85
+ const lineBlur: LineBlurProps = {
86
+ blur: extension.opts.perFeature ? 0 : Math.max(0, Number(getBlur) || 0),
87
+ gapOffset: Number(blurGapOffset) || 0,
88
+ gapSide: Math.sign(Number(blurGapSide) || 0)
89
+ };
90
+ this.setShaderModuleProps({lineBlur});
91
+ }
92
+
93
+ getShaders(this: Layer, extension: this) {
94
+ if (!isBlurredPathLayer(this)) {
95
+ return {};
96
+ }
97
+ // deck.gl appends these to the layer's and other extensions' code for the same hooks.
98
+ // `edgePixels` is PathLayer's signed distance to the stroke's edge in device pixels, and
99
+ // `fragColor.a` already carries its `smoothedge(0.0, edgePixels)` coverage, which this
100
+ // replaces with MapLibre's linear fade. `smoothedge` is 0 only where MapLibre's fade is 0.
101
+ // The fade measures its own distances, as `edgePixels` does but in Euclidean pixels: the
102
+ // length of the coordinate's screen gradient rather than `fwidth`, which is up to 1.41 times
103
+ // longer on diagonal lines and round caps, where it would shorten the fade. The body uses
104
+ // `vPathPosition.x` rather than its absolute value, whose gradient falls to 0 across the
105
+ // middle of the stroke, where a blur wider than the line keeps it translucent in MapLibre.
106
+ // Gradients are taken before any fragment of the quad is discarded.
107
+ const lineBlur: ShaderModule<LineBlurProps> = {
108
+ name: 'lineBlur',
109
+ inject: {
110
+ 'vs:#decl': `${UNIFORMS}
111
+ #ifdef LINE_BLUR_PER_FEATURE
112
+ in float instanceLineBlur;
113
+ #endif
114
+ out float lineBlur_blur;
115
+ out float lineBlur_gapCenter;
116
+ out float lineBlur_gapSide;
117
+ out float lineBlur_padPixels;
118
+ // The line's width over the drawn stroke's (see DECKGL_FILTER_SIZE).
119
+ float lineBlur_widthRatio = 1.0;
120
+ `,
121
+ // MapLibre's fade reaches half a device pixel past the line. The stroke is drawn that much
122
+ // wider on each side, and the fade measured from the line's own edge, so that a line
123
+ // narrower than a pixel draws as in MapLibre: with multisampling, the fade would otherwise
124
+ // be cut at the stroke's edge. Dashes, measured in the drawn width, are scaled back in
125
+ // #main-end. Offsets are measured in it too, so offset lines keep their width.
126
+ 'vs:DECKGL_FILTER_SIZE': `
127
+ lineBlur_padPixels = 0.0;
128
+ #ifndef PATH_STYLE_OFFSET
129
+ if (size.x > 0.0) {
130
+ // \`size\` is half the stroke's width.
131
+ vec2 lineBlur_pad = vec2(0.5 / project.devicePixelRatio);
132
+ lineBlur_pad = path.billboard ? lineBlur_pad : project_pixel_size(lineBlur_pad);
133
+ lineBlur_widthRatio = size.x / (size.x + lineBlur_pad.x);
134
+ size.xy += lineBlur_pad;
135
+ lineBlur_padPixels = 0.5;
136
+ }
137
+ #endif
138
+ `,
139
+ 'vs:#main-end': `
140
+ #if defined(DASH_ENABLED) && !defined(PATH_STYLE_OFFSET)
141
+ // PathStyleExtension measured the dashes in the drawn stroke's widths, MapLibre in the line's.
142
+ // Offset lines are not widened, and keep the dash phase that the offset shader corrects.
143
+ vDashArray *= lineBlur_widthRatio;
144
+ #ifdef HIGH_PRECISION_DASH
145
+ float lineBlur_dashUnit = vDashArray.x + vDashArray.y;
146
+ float lineBlur_dashPeriod = pathStyle.dashAlignMode == 0.0
147
+ ? lineBlur_dashUnit
148
+ : vDashPathLength / max(round(vDashPathLength / max(lineBlur_dashUnit, 0.0001)), 1.0);
149
+ vDashOffset = lineBlur_dashPeriod > 0.0 ? mod(dashOffsetAndLength.x, lineBlur_dashPeriod) : 0.0;
150
+ #endif
151
+ #endif
152
+ #ifdef LINE_BLUR_PER_FEATURE
153
+ lineBlur_blur = instanceLineBlur;
154
+ #else
155
+ lineBlur_blur = lineBlur.blur;
156
+ #endif
157
+ // In device pixels.
158
+ lineBlur_blur *= project.devicePixelRatio;
159
+ // The gap's center across the stroke, in the units of vPathPosition.x: half the drawn width
160
+ // from the stroke's center. PathStyleExtension puts the path at 2 * instanceOffsets, and
161
+ // vPathPosition.x runs the other way to the offset.
162
+ lineBlur_gapCenter = 0.0;
163
+ lineBlur_gapSide = 0.0;
164
+ #ifdef PATH_STYLE_OFFSET
165
+ if (widthPixels.x > 0.0) {
166
+ lineBlur_gapCenter = 2.0 * instanceOffsets - lineBlur.gapOffset / widthPixels.x;
167
+ // A path without a gap draws one line, centered on the gap's center: it is not clipped. A
168
+ // line of a gap is always off the center, by at least 0.01 pixels (MIN_GAP_LINE_CENTER).
169
+ if (abs(lineBlur_gapCenter) * widthPixels.x > 0.001) {
170
+ lineBlur_gapSide = lineBlur.gapSide;
171
+ }
172
+ }
173
+ #endif
174
+ `,
175
+ 'fs:#decl': `${UNIFORMS}
176
+ in float lineBlur_blur;
177
+ in float lineBlur_gapCenter;
178
+ in float lineBlur_gapSide;
179
+ in float lineBlur_padPixels;
180
+ `,
181
+ 'fs:#main-start': `
182
+ float lineBlur_bodyGradient = max(
183
+ length(vec2(dFdx(vPathPosition.x), dFdy(vPathPosition.x))), 1e-6
184
+ );
185
+ float lineBlur_cornerCoord = length(vCornerOffset);
186
+ float lineBlur_cornerGradient = max(
187
+ length(vec2(dFdx(lineBlur_cornerCoord), dFdy(lineBlur_cornerCoord))), 1e-6
188
+ );
189
+ `,
190
+ 'fs:#main-end': `
191
+ #ifdef ANTIALIASING
192
+ // The gap's other line draws past its center.
193
+ if (lineBlur_gapSide * (vPathPosition.x - lineBlur_gapCenter) > 0.0) {
194
+ discard;
195
+ }
196
+ float lineBlur_coverage = smoothedge(0.0, edgePixels);
197
+ // In device pixels, as edgePixels. A round cap or join is bounded by both its circle and the
198
+ // stroke's width, as PathLayer bounds it.
199
+ float lineBlur_edgePixels = (1.0 - abs(vPathPosition.x)) / lineBlur_bodyGradient;
200
+ if (isRound && isCorner) {
201
+ lineBlur_edgePixels = min(
202
+ lineBlur_edgePixels, (1.0 - lineBlur_cornerCoord) / lineBlur_cornerGradient
203
+ );
204
+ }
205
+ lineBlur_edgePixels -= lineBlur_padPixels;
206
+ // Picking colors keep the coverage PathLayer gave them.
207
+ if (lineBlur_coverage > 0.0 && !bool(picking.isActive)) {
208
+ fragColor.a *= clamp(
209
+ (lineBlur_edgePixels + 0.5) / (lineBlur_blur + 1.0), 0.0, 1.0
210
+ ) / lineBlur_coverage;
211
+ }
212
+ #endif
213
+ `
214
+ },
215
+ uniformTypes: {blur: 'f32', gapOffset: 'f32', gapSide: 'f32'}
216
+ };
217
+ return {
218
+ modules: [lineBlur],
219
+ defines: extension.opts.perFeature ? {LINE_BLUR_PER_FEATURE: true} : {}
220
+ };
221
+ }
222
+ }
223
+
224
+ function isBlurredPathLayer(layer: Layer): boolean {
225
+ return layer.context.device.type !== 'webgpu' && 'pathTesselator' in (layer.state || {});
226
+ }
227
+
228
+ /**
229
+ * The width to draw each of a gap's two lines with, so that their fades fall where MapLibre's do:
230
+ * MapLibre draws the two lines as one stroke whose inner edges fade into the gap and whose outer
231
+ * edges fade inward, each over the same `blur`. Drawing each line `blur` wider, and `blur / 2`
232
+ * closer to the gap (see `getLineOffset`), turns that into a fade inward from both of the line's
233
+ * edges. Lines without a gap, or without width, keep their width.
234
+ */
235
+ export function getBlurredLineWidth(width: number, gapWidth: number, blur: number): number {
236
+ return width > 0 && gapWidth > 0 ? width + Math.max(0, blur) : width;
237
+ }
@@ -0,0 +1,364 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ // deck.gl-community
6
+ // SPDX-License-Identifier: MIT
7
+ // Copyright (c) vis.gl contributors
8
+
9
+ /**
10
+ * Glyphs of a label along a line, each placed on the line and turned to the segment it lands on,
11
+ * as MapLibre GL JS's `placeGlyphsAlongLine`, `placeFirstAndLastGlyph` and `placeGlyphAlongLine`
12
+ * (symbol/projection.ts) place them in the label plane. Here the label plane is the map plane at
13
+ * the current zoom: the line, measured in pixels at the placement zoom, is scaled about the anchor.
14
+ *
15
+ * Coordinates are pixels with y pointing down. Angles returned are counter-clockwise on screen, in
16
+ * degrees, as deck.gl's `getAngle` takes them.
17
+ */
18
+
19
+ /** A line a label is placed along, and where on it the label's anchor lies. */
20
+ export type LabelLine = {
21
+ /** The line's vertices, `x, y` pairs in pixels at the placement zoom. */
22
+ points: ArrayLike<number>;
23
+ /** The segment the anchor lies on: from vertex `segment` to `segment + 1`. */
24
+ segment: number;
25
+ /** The anchor's position along that segment, from 0 to 1. */
26
+ t: number;
27
+ };
28
+
29
+ export type LineGlyphOptions = {
30
+ /**
31
+ * Each glyph's advance centre along the label, in pixels at the current zoom, from the anchor
32
+ * (MapLibre's `glyphOffsetArray` times the font scale).
33
+ */
34
+ offsets: ArrayLike<number>;
35
+ /** The label's `text-offset` in pixels: along the line, and across it (MapLibre's `lineOffset`). */
36
+ lineOffsetX?: number;
37
+ lineOffsetY?: number;
38
+ /** Pixels at the current zoom per pixel of `line.points`. */
39
+ scale: number;
40
+ /** Whether the label reads against the line's direction (turned upright). */
41
+ flip: boolean;
42
+ };
43
+
44
+ /** Where each glyph of a label is drawn, from the anchor, and the path its collision follows. */
45
+ export type LineGlyphs = {
46
+ /** For each glyph: `dx, dy` in pixels from the anchor, and its angle. */
47
+ glyphs: Float32Array;
48
+ /**
49
+ * The path from the first glyph to the last along the line, `x, y` pairs in pixels from the
50
+ * anchor, as MapLibre builds the collision circles of a label along a line.
51
+ */
52
+ path: number[];
53
+ };
54
+
55
+ type PlacedGlyph = {x: number; y: number; angle: number; path: number[]};
56
+
57
+ /** The anchor's position and the projected line, relative to the anchor and scaled. */
58
+ type Projection = {
59
+ vertex: (index: number) => [number, number];
60
+ count: number;
61
+ };
62
+
63
+ function getProjection(line: LabelLine, scale: number): Projection {
64
+ const {points, segment, t} = line;
65
+ const ax = points[segment * 2] + (points[segment * 2 + 2] - points[segment * 2]) * t;
66
+ const ay = points[segment * 2 + 1] + (points[segment * 2 + 3] - points[segment * 2 + 1]) * t;
67
+ return {
68
+ vertex: index => [(points[index * 2] - ax) * scale, (points[index * 2 + 1] - ay) * scale],
69
+ count: points.length / 2
70
+ };
71
+ }
72
+
73
+ /** `transformToOffsetNormal`: the segment's normal, of length `offset * direction`. */
74
+ function getOffsetNormal(
75
+ dx: number,
76
+ dy: number,
77
+ offset: number,
78
+ direction: number
79
+ ): [number, number] {
80
+ const length = Math.hypot(dx, dy);
81
+ const k = (offset * direction) / length;
82
+ // `Point.perp()` is (-y, x).
83
+ return [-dy * k, dx * k];
84
+ }
85
+
86
+ /** `findLineIntersection`: where lines a1-a2 and b1-b2 meet, or null if parallel. */
87
+ function intersectLines(
88
+ a1: number[],
89
+ a2: number[],
90
+ b1: number[],
91
+ b2: number[]
92
+ ): [number, number] | null {
93
+ const aDx = a2[0] - a1[0];
94
+ const aDy = a2[1] - a1[1];
95
+ const bDx = b2[0] - b1[0];
96
+ const bDy = b2[1] - b1[1];
97
+ const denominator = bDy * aDx - bDx * aDy;
98
+ if (denominator === 0) {
99
+ return null;
100
+ }
101
+ const ua = (bDx * (a1[1] - b1[1]) - bDy * (a1[0] - b1[0])) / denominator;
102
+ return [a1[0] + ua * aDx, a1[1] + ua * aDy];
103
+ }
104
+
105
+ /**
106
+ * `placeGlyphAlongLine`: walks the line from the anchor until the distance travelled equals the
107
+ * glyph's offset, on the line moved `lineOffsetY` across it when that is set. Returns null when the
108
+ * glyph does not fit on the line.
109
+ */
110
+ function placeGlyph(
111
+ projection: Projection,
112
+ anchorSegment: number,
113
+ offsetX: number,
114
+ lineOffsetX: number,
115
+ lineOffsetY: number,
116
+ flip: boolean,
117
+ offsets: Map<number, [number, number]>
118
+ ): PlacedGlyph | null {
119
+ const combinedOffsetX = flip ? offsetX - lineOffsetX : offsetX + lineOffsetX;
120
+ let direction = combinedOffsetX > 0 ? 1 : -1;
121
+ let angle = 0;
122
+ if (flip) {
123
+ direction *= -1;
124
+ angle = Math.PI;
125
+ }
126
+ if (direction < 0) {
127
+ angle += Math.PI;
128
+ }
129
+ let currentIndex = direction > 0 ? anchorSegment : anchorSegment + 1;
130
+ const anchor: [number, number] = [0, 0];
131
+ let currentVertex = anchor;
132
+ let previousVertex = anchor;
133
+ let offsetIntersectionPoint: [number, number] | undefined;
134
+ let offsetPreviousVertex: [number, number] | undefined;
135
+ let distanceFromAnchor = 0;
136
+ let currentSegmentDistance = 0;
137
+ const absOffsetX = Math.abs(combinedOffsetX);
138
+ const path: number[] = [];
139
+ let segmentX = 0;
140
+ let segmentY = 0;
141
+
142
+ while (distanceFromAnchor + currentSegmentDistance <= absOffsetX) {
143
+ currentIndex += direction;
144
+ if (currentIndex < 0 || currentIndex >= projection.count) {
145
+ return null;
146
+ }
147
+ distanceFromAnchor += currentSegmentDistance;
148
+ previousVertex = currentVertex;
149
+ offsetPreviousVertex = offsetIntersectionPoint;
150
+ currentVertex = projection.vertex(currentIndex);
151
+ if (lineOffsetY === 0) {
152
+ path.push(previousVertex[0], previousVertex[1]);
153
+ segmentX = currentVertex[0] - previousVertex[0];
154
+ segmentY = currentVertex[1] - previousVertex[1];
155
+ } else {
156
+ let normal: [number, number];
157
+ const dx = currentVertex[0] - previousVertex[0];
158
+ const dy = currentVertex[1] - previousVertex[1];
159
+ if (dx === 0 && dy === 0) {
160
+ // The anchor lies on a vertex: the next segment gives the normal.
161
+ const next = projection.vertex(currentIndex + direction);
162
+ normal = getOffsetNormal(
163
+ next[0] - currentVertex[0],
164
+ next[1] - currentVertex[1],
165
+ lineOffsetY,
166
+ direction
167
+ );
168
+ } else {
169
+ normal = getOffsetNormal(dx, dy, lineOffsetY, direction);
170
+ }
171
+ offsetPreviousVertex ||= [previousVertex[0] + normal[0], previousVertex[1] + normal[1]];
172
+ offsetIntersectionPoint = getOffsetIntersection(
173
+ projection,
174
+ currentIndex,
175
+ normal,
176
+ currentVertex,
177
+ offsetPreviousVertex,
178
+ lineOffsetY,
179
+ direction,
180
+ offsets
181
+ );
182
+ path.push(offsetPreviousVertex[0], offsetPreviousVertex[1]);
183
+ segmentX = offsetIntersectionPoint[0] - offsetPreviousVertex[0];
184
+ segmentY = offsetIntersectionPoint[1] - offsetPreviousVertex[1];
185
+ }
186
+ currentSegmentDistance = Math.hypot(segmentX, segmentY);
187
+ }
188
+
189
+ const k = (absOffsetX - distanceFromAnchor) / currentSegmentDistance;
190
+ const start = offsetPreviousVertex || previousVertex;
191
+ const x = start[0] + segmentX * k;
192
+ const y = start[1] + segmentY * k;
193
+ path.push(x, y);
194
+ return {
195
+ x,
196
+ y,
197
+ angle:
198
+ angle +
199
+ Math.atan2(currentVertex[1] - previousVertex[1], currentVertex[0] - previousVertex[0]),
200
+ path
201
+ };
202
+ }
203
+
204
+ /** `findOffsetIntersectionPoint`, with MapLibre's per-vertex cache of offset vertices. */
205
+ function getOffsetIntersection(
206
+ projection: Projection,
207
+ index: number,
208
+ normal: [number, number],
209
+ currentVertex: [number, number],
210
+ offsetPreviousVertex: [number, number],
211
+ lineOffsetY: number,
212
+ direction: number,
213
+ offsets: Map<number, [number, number]>
214
+ ): [number, number] {
215
+ const cached = offsets.get(index);
216
+ if (cached) {
217
+ return cached;
218
+ }
219
+ const offsetCurrentVertex: [number, number] = [
220
+ currentVertex[0] + normal[0],
221
+ currentVertex[1] + normal[1]
222
+ ];
223
+ let result = offsetCurrentVertex;
224
+ const nextIndex = index + direction;
225
+ if (nextIndex >= 0 && nextIndex < projection.count) {
226
+ const next = projection.vertex(nextIndex);
227
+ const nextNormal = getOffsetNormal(
228
+ next[0] - currentVertex[0],
229
+ next[1] - currentVertex[1],
230
+ lineOffsetY,
231
+ direction
232
+ );
233
+ const begin = [currentVertex[0] + nextNormal[0], currentVertex[1] + nextNormal[1]];
234
+ const end = [next[0] + nextNormal[0], next[1] + nextNormal[1]];
235
+ result = intersectLines(offsetPreviousVertex, offsetCurrentVertex, begin, end) || result;
236
+ }
237
+ offsets.set(index, result);
238
+ return result;
239
+ }
240
+
241
+ /** Radians, clockwise on screen with y down, to deck.gl degrees, counter-clockwise. */
242
+ function toDeckAngle(radians: number): number {
243
+ return (-radians * 180) / Math.PI;
244
+ }
245
+
246
+ /**
247
+ * Places each glyph of a label along `line`, as MapLibre GL JS draws text along a line. Returns
248
+ * null when the first or the last glyph does not fit on the line: MapLibre then hides the label.
249
+ * `flip` is decided by the caller (see {@link needsFlip}); MapLibre's own flip test is on screen.
250
+ */
251
+ export function placeLineGlyphs(line: LabelLine, options: LineGlyphOptions): LineGlyphs | null {
252
+ const {offsets, flip, scale} = options;
253
+ const lineOffsetX = options.lineOffsetX || 0;
254
+ const lineOffsetY = options.lineOffsetY || 0;
255
+ const count = offsets.length;
256
+ const {segment} = line;
257
+ if (
258
+ !count ||
259
+ !(scale > 0) ||
260
+ !(segment >= 0) ||
261
+ segment * 2 + 3 >= line.points.length ||
262
+ !(line.t >= 0 && line.t <= 1)
263
+ ) {
264
+ return null;
265
+ }
266
+ const projection = getProjection(line, scale);
267
+ // MapLibre keeps one cache of offset vertices per label, whichever way a glyph walks: either way
268
+ // the offset vertex is where the same two offset segments meet.
269
+ const offsetVertices = new Map<number, [number, number]>();
270
+ const place = (offsetX: number) =>
271
+ placeGlyph(projection, segment, offsetX, lineOffsetX, lineOffsetY, flip, offsetVertices);
272
+ const first = place(offsets[0]);
273
+ const last = count > 1 ? place(offsets[count - 1]) : first;
274
+ if (!first || !last) {
275
+ return null;
276
+ }
277
+ const glyphs = new Float32Array(count * 3);
278
+ const write = (i: number, glyph: PlacedGlyph) => {
279
+ glyphs[i * 3] = glyph.x;
280
+ glyphs[i * 3 + 1] = glyph.y;
281
+ glyphs[i * 3 + 2] = toDeckAngle(glyph.angle);
282
+ };
283
+ write(0, first);
284
+ for (let i = 1; i < count - 1; i++) {
285
+ const glyph = place(offsets[i]);
286
+ if (!glyph) {
287
+ return null;
288
+ }
289
+ write(i, glyph);
290
+ }
291
+ write(count - 1, last);
292
+ // The collision path: the first glyph's path reversed, then the last glyph's, without the anchor
293
+ // each starts from (MapLibre's `placeCollisionCircles`).
294
+ const path: number[] = [];
295
+ for (let i = first.path.length - 2; i >= 2; i -= 2) {
296
+ path.push(first.path[i], first.path[i + 1]);
297
+ }
298
+ for (let i = 2; i < last.path.length; i += 2) {
299
+ path.push(last.path[i], last.path[i + 1]);
300
+ }
301
+ return {glyphs, path};
302
+ }
303
+
304
+ /**
305
+ * MapLibre's `requiresOrientationChange` for horizontal text: whether a label placed unflipped reads
306
+ * right to left on screen, its first glyph right of its last, on a flat map turned by `bearing`.
307
+ */
308
+ export function needsFlip(unflipped: LineGlyphs, bearing: number): boolean {
309
+ const {glyphs} = unflipped;
310
+ const n = glyphs.length / 3;
311
+ const dx = glyphs[(n - 1) * 3] - glyphs[0];
312
+ const dy = glyphs[(n - 1) * 3 + 1] - glyphs[1];
313
+ // A direction of the map plane is drawn turned counter-clockwise by the bearing on screen, as in
314
+ // `getUprightAngle`: the chord's x on screen.
315
+ const radians = (bearing * Math.PI) / 180;
316
+ const x = dx * Math.cos(radians) + dy * Math.sin(radians);
317
+ return x < 0;
318
+ }
319
+
320
+ /**
321
+ * Collision circles along a path on screen, as MapLibre GL JS's `placeCollisionCircles` lays them
322
+ * out with its `PathInterpolator`: the path's ends inset by a quarter radius, circles at most 2.5
323
+ * radii apart, and one circle on a path shorter than half a radius (or at the middle of the path
324
+ * for a radius of 0). Returns `x, y, radius` triples.
325
+ */
326
+ export function getPathCircles(path: ArrayLike<number>, radius: number): number[] {
327
+ const count = path.length / 2;
328
+ if (!count) {
329
+ return [];
330
+ }
331
+ const distances = [0];
332
+ for (let i = 1; i < count; i++) {
333
+ distances[i] =
334
+ distances[i - 1] +
335
+ Math.hypot(path[i * 2] - path[i * 2 - 2], path[i * 2 + 1] - path[i * 2 - 1]);
336
+ }
337
+ const length = distances[count - 1];
338
+ const padding = Math.min(radius * 0.25, length * 0.5);
339
+ const paddedLength = length - padding * 2;
340
+ // A label without height has one circle, at the middle of its path.
341
+ const circleCount =
342
+ !(radius > 0) || length <= 0.5 * radius ? 1 : Math.ceil(paddedLength / (radius * 2.5)) + 1;
343
+ const circles: number[] = [];
344
+ for (let k = 0; k < circleCount; k++) {
345
+ const t = circleCount === 1 && !(radius > 0) ? 0.5 : k / Math.max(circleCount - 1, 1);
346
+ if (count === 1) {
347
+ circles.push(path[0], path[1], radius);
348
+ continue;
349
+ }
350
+ const target = t * paddedLength + padding;
351
+ let index = 1;
352
+ while (distances[index] < target && index < count - 1) {
353
+ index++;
354
+ }
355
+ const segment = distances[index] - distances[index - 1];
356
+ const u = segment > 0 ? (target - distances[index - 1]) / segment : 0;
357
+ circles.push(
358
+ path[index * 2 - 2] * (1 - u) + path[index * 2] * u,
359
+ path[index * 2 - 1] * (1 - u) + path[index * 2 + 1] * u,
360
+ radius
361
+ );
362
+ }
363
+ return circles;
364
+ }