@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,131 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {LayerExtension, project32} from '@deck.gl/core';
6
+ import type {Layer} from '@deck.gl/core';
7
+
8
+ /**
9
+ * Turns a billboard drawn along a line on a globe from the line's direction at the anchor (the
10
+ * instance angle, counter-clockwise from east) to that direction as the view draws it, so the label
11
+ * follows its line on every frame, at any bearing, anywhere on the globe, without updating any
12
+ * attribute. The direction on screen is the line's tangent at the anchor, east and north on the
13
+ * globe's surface (the derivatives of `project_globe_`), through the view-projection matrix: exact,
14
+ * with no difference of nearby positions to lose precision.
15
+ *
16
+ * With `keepUpright`, a billboard that would read upside down there is turned by another half turn,
17
+ * as `getUprightAngle` turns it from the bearing alone: away from the middle of the view the bearing
18
+ * does not tell which way the view draws a line.
19
+ *
20
+ * With `pixelOffset`, the name of the layer's pixel offset attribute, that offset is not turned
21
+ * around with the billboard: it is a direction along and across the line (see
22
+ * `withLineDirection`), and stays on the same side of the line.
23
+ *
24
+ * The instance angle and pixel offset are only declared in the layer's own shader source, after the
25
+ * hook functions, so they are copied into globals at the start of `main`.
26
+ */
27
+ const getGlobeLineAngleModule = (keepUpright: boolean, pixelOffset?: string) => ({
28
+ name: `globeLineAngle${keepUpright ? 'Upright' : ''}${pixelOffset ? `_${pixelOffset}` : ''}`,
29
+ dependencies: [project32],
30
+ inject: {
31
+ 'vs:#decl': /* glsl */ `
32
+ float globeLineAngle_angle;
33
+ vec2 globeLineAngle_pixelOffset;
34
+ `,
35
+ 'vs:#main-start': /* glsl */ `
36
+ globeLineAngle_angle = radians(instanceAngles);
37
+ // As the layer adds it to the billboard's offset, y down.
38
+ globeLineAngle_pixelOffset = ${pixelOffset ? `vec2(${pixelOffset}.x, -${pixelOffset}.y)` : 'vec2(0.0)'};
39
+ `,
40
+ 'vs:DECKGL_FILTER_SIZE': /* glsl */ `
41
+ if (project.projectionMode == PROJECTION_MODE_GLOBE) {
42
+ float globeLineAngle_lambda = radians(geometry.worldPosition.x);
43
+ float globeLineAngle_phi = radians(geometry.worldPosition.y);
44
+ vec3 globeLineAngle_east = vec3(cos(globeLineAngle_lambda), sin(globeLineAngle_lambda), 0.0);
45
+ vec3 globeLineAngle_north = vec3(
46
+ -sin(globeLineAngle_lambda) * sin(globeLineAngle_phi),
47
+ cos(globeLineAngle_lambda) * sin(globeLineAngle_phi),
48
+ cos(globeLineAngle_phi)
49
+ );
50
+ vec3 globeLineAngle_tangent = cos(globeLineAngle_angle) * globeLineAngle_east +
51
+ sin(globeLineAngle_angle) * globeLineAngle_north;
52
+ vec4 globeLineAngle_clip = project_common_position_to_clipspace(
53
+ vec4(project_position(geometry.worldPosition), 1.0)
54
+ );
55
+ vec4 globeLineAngle_clipTangent = project.viewProjectionMatrix * vec4(globeLineAngle_tangent, 0.0);
56
+ // The derivative of the anchor's normalized device coordinates, times w squared, in pixels.
57
+ vec2 globeLineAngle_screen = (globeLineAngle_clipTangent.xy * globeLineAngle_clip.w -
58
+ globeLineAngle_clip.xy * globeLineAngle_clipTangent.w) * project.viewportSize;
59
+ float globeLineAngle_turn =
60
+ atan(globeLineAngle_screen.y, globeLineAngle_screen.x) - globeLineAngle_angle;
61
+ float globeLineAngle_cos = cos(globeLineAngle_turn);
62
+ float globeLineAngle_sin = sin(globeLineAngle_turn);
63
+ mat2 globeLineAngle_rotation =
64
+ mat2(globeLineAngle_cos, globeLineAngle_sin, -globeLineAngle_sin, globeLineAngle_cos);
65
+ ${
66
+ keepUpright
67
+ ? ` if (globeLineAngle_screen.x < 0.0) {
68
+ // Turned around, but for the pixel offset: -(size - offset) + offset.
69
+ size.xy = 2.0 * globeLineAngle_pixelOffset - size.xy;
70
+ }
71
+ `
72
+ : ''
73
+ } size.xy = globeLineAngle_rotation * size.xy;
74
+ }
75
+ `
76
+ }
77
+ });
78
+
79
+ const GLOBE_LINE_ANGLE_MODULES = {
80
+ free: getGlobeLineAngleModule(false),
81
+ upright: getGlobeLineAngleModule(true),
82
+ // The pixel offset attributes of `IconLayer` (and the glyphs of `TextLayer`) and of
83
+ // `TextLayer`'s backgrounds.
84
+ instancePixelOffset: getGlobeLineAngleModule(true, 'instancePixelOffset'),
85
+ instancePixelOffsets: getGlobeLineAngleModule(true, 'instancePixelOffsets')
86
+ };
87
+
88
+ /**
89
+ * Keeps billboards drawn along lines (labels and icons whose rotation is aligned to the map) on
90
+ * their lines in a `GlobeView`, which draws them in screen space: their angles are taken from
91
+ * north up, and the shader turns them as the view draws the line, upright with `keepUpright`. In
92
+ * other views it does nothing. Add it only to billboards (the shader would also turn the offset of
93
+ * a layer drawn in the map plane) whose angle is a direction on the map. WebGL only: on WebGPU the
94
+ * extension adds no shader code, and they stay as they are drawn from north up.
95
+ *
96
+ * Added to a composite layer such as `TextLayer`, it reaches the character and background
97
+ * sublayers. With `keepOffsetSide`, text kept upright is turned around without its pixel offset,
98
+ * which must then be a direction along and across the line, as `text-offset` is.
99
+ */
100
+ export class GlobeLineAngleExtension extends LayerExtension<{
101
+ keepUpright: boolean;
102
+ keepOffsetSide?: boolean;
103
+ }> {
104
+ static extensionName = 'GlobeLineAngleExtension';
105
+
106
+ constructor(opts: {keepUpright: boolean; keepOffsetSide?: boolean}) {
107
+ super(opts);
108
+ }
109
+
110
+ getShaders(this: Layer, extension: GlobeLineAngleExtension) {
111
+ if (this.context.device.type === 'webgpu') {
112
+ return {};
113
+ }
114
+ const {keepUpright, keepOffsetSide} = extension.opts;
115
+ if (!keepUpright) {
116
+ return {modules: [GLOBE_LINE_ANGLE_MODULES.free]};
117
+ }
118
+ if (!keepOffsetSide) {
119
+ return {modules: [GLOBE_LINE_ANGLE_MODULES.upright]};
120
+ }
121
+ // `TextLayer`'s backgrounds name the attribute in the plural.
122
+ const attributes = (this.getAttributeManager() as any)?.attributes || {};
123
+ return {
124
+ modules: [
125
+ attributes.instancePixelOffsets
126
+ ? GLOBE_LINE_ANGLE_MODULES.instancePixelOffsets
127
+ : GLOBE_LINE_ANGLE_MODULES.instancePixelOffset
128
+ ]
129
+ };
130
+ }
131
+ }
@@ -0,0 +1,173 @@
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 TinySDF from '@mapbox/tiny-sdf';
10
+ import type {TextLayerProps} from '@deck.gl/layers';
11
+ import {GLYPH_BORDER, GLYPH_EM} from './glyph-pbf';
12
+ import type {Glyph} from './glyph-pbf';
13
+ import {isLocalGlyph} from './glyph-store';
14
+ import type {GlyphStore} from './glyph-store';
15
+
16
+ type GetFontRenderer = NonNullable<TextLayerProps['_getFontRenderer']>;
17
+ type FontRenderer = ReturnType<GetFontRenderer>;
18
+
19
+ /**
20
+ * `TextLayer`'s font settings for glyphs from a style's `glyphs` URL: distance fields one em of
21
+ * {@link GLYPH_EM} pixels high, reaching 8 pixels, with the edge at 0.75, as MapLibre GL JS draws
22
+ * them. Cells are packed `buffer` pixels apart, the border of the glyph bitmaps.
23
+ */
24
+ export const GLYPH_FONT_SETTINGS = {
25
+ sdf: true,
26
+ fontSize: GLYPH_EM,
27
+ buffer: GLYPH_BORDER,
28
+ radius: 8,
29
+ cutoff: 0.25,
30
+ smoothing: 0.1
31
+ };
32
+
33
+ /**
34
+ * MapLibre GL JS sets each line's glyphs this many pixels below the line's top, at one em of
35
+ * {@link GLYPH_EM} pixels (`SHAPING_DEFAULT_OFFSET`, negated).
36
+ */
37
+ const LINE_GLYPH_OFFSET = 17;
38
+
39
+ /**
40
+ * MapLibre GL JS's alignment of a glyph drawn with the browser's fonts (see `isLocalGlyph`) with
41
+ * the glyphs of glyph ranges, whose `top` is measured from above the em box.
42
+ */
43
+ const LOCAL_GLYPH_TOP = 27.5;
44
+ const LOCAL_GLYPH_LEFT = 0.5;
45
+
46
+ /** The font-weight MapLibre GL JS draws local glyphs in, from the font stack's names. */
47
+ function getLocalGlyphWeight(fontStack: string): string {
48
+ if (/bold/i.test(fontStack)) return '900';
49
+ if (/medium/i.test(fontStack)) return '500';
50
+ if (/light/i.test(fontStack)) return '200';
51
+ return '400';
52
+ }
53
+
54
+ const LOCAL_GLYPH_SDFS = new Map<string, TinySDF>();
55
+
56
+ /**
57
+ * A glyph that MapLibre GL JS draws with the browser's fonts, as a glyph of a glyph range. MapLibre
58
+ * draws these at twice the resolution; here they share the one-em atlas of the other glyphs.
59
+ */
60
+ function getLocalGlyph(fontStack: string, codePoint: number): Glyph | null {
61
+ if (typeof document === 'undefined' && typeof OffscreenCanvas === 'undefined') {
62
+ return null;
63
+ }
64
+ const fontWeight = getLocalGlyphWeight(fontStack);
65
+ let sdf = LOCAL_GLYPH_SDFS.get(fontWeight);
66
+ if (!sdf) {
67
+ sdf = new TinySDF({
68
+ fontSize: GLYPH_EM,
69
+ buffer: GLYPH_BORDER,
70
+ radius: GLYPH_FONT_SETTINGS.radius,
71
+ cutoff: GLYPH_FONT_SETTINGS.cutoff,
72
+ fontFamily: 'sans-serif',
73
+ fontWeight
74
+ });
75
+ LOCAL_GLYPH_SDFS.set(fontWeight, sdf);
76
+ }
77
+ const drawn = sdf.draw(String.fromCodePoint(codePoint));
78
+ const width = drawn.glyphWidth;
79
+ const height = drawn.glyphHeight;
80
+ return {
81
+ id: codePoint,
82
+ // TinySDF draws `glyphWidth + 2 * buffer` by `glyphHeight + 2 * buffer`, as a glyph range does.
83
+ bitmap: width && height ? drawn.data : new Uint8Array(0),
84
+ width: width && height ? width : 0,
85
+ height: width && height ? height : 0,
86
+ left: drawn.glyphLeft + LOCAL_GLYPH_LEFT,
87
+ top: drawn.glyphTop - LOCAL_GLYPH_TOP,
88
+ advance: drawn.glyphAdvance
89
+ };
90
+ }
91
+
92
+ /**
93
+ * The glyph a character of `fontStack` is drawn with: from the store's ranges, or with the browser's
94
+ * fonts for the characters MapLibre GL JS draws so. Null while its range loads, and when the range
95
+ * has none: the character is then not drawn, as in MapLibre.
96
+ */
97
+ export function getStyleGlyph(
98
+ store: GlyphStore,
99
+ fontStack: string,
100
+ character: string
101
+ ): Glyph | null {
102
+ const codePoint = character.codePointAt(0);
103
+ if (codePoint === undefined) {
104
+ return null;
105
+ }
106
+ if (isLocalGlyph(codePoint)) {
107
+ return getLocalGlyph(fontStack, codePoint);
108
+ }
109
+ return store.getGlyph(fontStack, codePoint) ?? null;
110
+ }
111
+
112
+ /**
113
+ * A function giving a character's advance in ems: the glyph's advance, as MapLibre GL JS lays text
114
+ * out. A character without a glyph advances nothing.
115
+ */
116
+ export function getGlyphCharacterMeasurer(
117
+ store: GlyphStore,
118
+ fontStack: string
119
+ ): (character: string) => number {
120
+ const widths = new Map<string, number>();
121
+ return character => {
122
+ let width = widths.get(character);
123
+ if (width === undefined) {
124
+ const glyph = getStyleGlyph(store, fontStack, character);
125
+ width = (glyph?.advance ?? 0) / GLYPH_EM;
126
+ // A character whose range is still loading is measured again once it has loaded.
127
+ if (glyph || store.hasGlyphs(fontStack, character)) {
128
+ widths.set(character, width);
129
+ }
130
+ }
131
+ return width;
132
+ };
133
+ }
134
+
135
+ /**
136
+ * `TextLayer`'s font renderer for the glyphs of a font stack. With
137
+ * {@link GLYPH_FONT_SETTINGS}, `TextLayer` lays each character's glyph out as MapLibre GL JS does:
138
+ * the line's baseline offset puts it {@link LINE_GLYPH_OFFSET} pixels below the line's top, and the
139
+ * glyph's bitmap is drawn `top` pixels below that and `left` pixels right of the pen.
140
+ */
141
+ export function getGlyphFontRenderer(store: GlyphStore, fontStack: string): FontRenderer {
142
+ return {
143
+ measure(character?: string) {
144
+ if (character === undefined) {
145
+ // `TextLayer` sets the baseline `(ascent - descent) / 2` from the middle of the line.
146
+ return {advance: 0, width: 0, ascent: 0, descent: 2 * LINE_GLYPH_OFFSET};
147
+ }
148
+ const glyph = getStyleGlyph(store, fontStack, character);
149
+ if (!glyph) {
150
+ return {advance: 0, width: 0, ascent: 0, descent: 0};
151
+ }
152
+ return {
153
+ advance: glyph.advance,
154
+ width: glyph.width,
155
+ ascent: glyph.top,
156
+ descent: glyph.height - glyph.top
157
+ };
158
+ },
159
+ draw(character: string) {
160
+ const glyph = getStyleGlyph(store, fontStack, character);
161
+ if (!glyph || !glyph.bitmap.length) {
162
+ return {data: new ImageData(1, 1), left: 0, top: 0};
163
+ }
164
+ const width = glyph.width + 2 * GLYPH_BORDER;
165
+ const height = glyph.height + 2 * GLYPH_BORDER;
166
+ const data = new ImageData(width, height);
167
+ for (let i = 0; i < glyph.bitmap.length; i++) {
168
+ data.data[4 * i + 3] = glyph.bitmap[i];
169
+ }
170
+ return {data, left: GLYPH_BORDER - glyph.left, top: GLYPH_BORDER};
171
+ }
172
+ };
173
+ }
@@ -0,0 +1,164 @@
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
+ * The pixels of distance field around each glyph of a glyph range, on every side, beyond its
11
+ * `width` and `height`: a bitmap is `width + 6` by `height + 6`.
12
+ */
13
+ export const GLYPH_BORDER = 3;
14
+
15
+ /** The size, in pixels, at which glyph ranges are drawn: one em. */
16
+ export const GLYPH_EM = 24;
17
+
18
+ /** Code points per glyph range: a range file holds `256 * n` to `256 * n + 255`. */
19
+ export const GLYPH_RANGE_SIZE = 256;
20
+
21
+ /**
22
+ * A glyph of a glyph range, as MapLibre GL JS reads it: a signed distance field and its metrics,
23
+ * in pixels at {@link GLYPH_EM}.
24
+ */
25
+ export type Glyph = {
26
+ /** Unicode code point. */
27
+ id: number;
28
+ /**
29
+ * One byte per pixel, `width + 2 * GLYPH_BORDER` by `height + 2 * GLYPH_BORDER`; empty for a
30
+ * glyph without ink, such as a space.
31
+ */
32
+ bitmap: Uint8Array | Uint8ClampedArray;
33
+ /** Width of the ink. */
34
+ width: number;
35
+ /** Height of the ink. */
36
+ height: number;
37
+ /** From the pen position to the left of the ink. */
38
+ left: number;
39
+ /** Vertical bearing of the top of the ink, as MapLibre GL JS reads it. */
40
+ top: number;
41
+ /** From the pen position to the next. */
42
+ advance: number;
43
+ };
44
+
45
+ /** A reader of the protocol buffer encoding: just what glyph ranges use. */
46
+ class Reader {
47
+ position = 0;
48
+ constructor(readonly bytes: Uint8Array) {}
49
+
50
+ readVarint(): number {
51
+ let value = 0;
52
+ let shift = 0;
53
+ for (;;) {
54
+ if (this.position >= this.bytes.length) {
55
+ throw new Error('Truncated glyph range');
56
+ }
57
+ const byte = this.bytes[this.position++];
58
+ // Glyph fields fit in 32 bits; multiplying keeps larger values exact, up to 2^53.
59
+ value += (byte & 0x7f) * 2 ** shift;
60
+ if (byte < 0x80) {
61
+ return value;
62
+ }
63
+ shift += 7;
64
+ if (shift > 49) {
65
+ throw new Error('Invalid varint in glyph range');
66
+ }
67
+ }
68
+ }
69
+
70
+ readSVarint(): number {
71
+ const value = this.readVarint();
72
+ return value % 2 === 1 ? -(value + 1) / 2 : value / 2;
73
+ }
74
+
75
+ /** The bytes of a length-delimited field, which the reader then skips. */
76
+ readBytes(): Uint8Array {
77
+ const length = this.readVarint();
78
+ const end = this.position + length;
79
+ if (end > this.bytes.length) {
80
+ throw new Error('Truncated glyph range');
81
+ }
82
+ const bytes = this.bytes.subarray(this.position, end);
83
+ this.position = end;
84
+ return bytes;
85
+ }
86
+
87
+ skip(wireType: number): void {
88
+ if (wireType === 0) {
89
+ this.readVarint();
90
+ } else if (wireType === 1) {
91
+ this.position += 8;
92
+ } else if (wireType === 2) {
93
+ this.readBytes();
94
+ } else if (wireType === 5) {
95
+ this.position += 4;
96
+ } else {
97
+ throw new Error(`Unsupported wire type ${wireType} in glyph range`);
98
+ }
99
+ }
100
+
101
+ /** Calls `read` with the tag and wire type of each field, which `read` must consume or skip. */
102
+ readFields(read: (tag: number, wireType: number) => void): void {
103
+ while (this.position < this.bytes.length) {
104
+ const key = this.readVarint();
105
+ read(Math.floor(key / 8), key % 8);
106
+ }
107
+ }
108
+ }
109
+
110
+ function readGlyph(bytes: Uint8Array): Glyph | null {
111
+ const reader = new Reader(bytes);
112
+ const glyph: Partial<Glyph> = {};
113
+ reader.readFields((tag, wireType) => {
114
+ if (tag === 1 && wireType === 0) glyph.id = reader.readVarint();
115
+ else if (tag === 2 && wireType === 2) glyph.bitmap = reader.readBytes();
116
+ else if (tag === 3 && wireType === 0) glyph.width = reader.readVarint();
117
+ else if (tag === 4 && wireType === 0) glyph.height = reader.readVarint();
118
+ else if (tag === 5 && wireType === 0) glyph.left = reader.readSVarint();
119
+ else if (tag === 6 && wireType === 0) glyph.top = reader.readSVarint();
120
+ else if (tag === 7 && wireType === 0) glyph.advance = reader.readVarint();
121
+ else reader.skip(wireType);
122
+ });
123
+ const {id, width = 0, height = 0, left = 0, top = 0, advance = 0} = glyph;
124
+ if (id === undefined) {
125
+ return null;
126
+ }
127
+ // MapLibre GL JS reads every bitmap as `width + 6` by `height + 6` (its `AlphaImage`) and fails on
128
+ // any other size. A glyph without ink, such as a space, may leave its bitmap out or carry just
129
+ // the border (CARTO's glyphs do): either way it draws nothing, and only its advance counts.
130
+ const inked = width > 0 && height > 0;
131
+ const size = (width + 2 * GLYPH_BORDER) * (height + 2 * GLYPH_BORDER);
132
+ if (glyph.bitmap ? glyph.bitmap.length !== size : inked) {
133
+ return null;
134
+ }
135
+ const bitmap = inked ? glyph.bitmap! : new Uint8Array(0);
136
+ return {id, bitmap, width, height, left, top, advance};
137
+ }
138
+
139
+ /**
140
+ * Reads a glyph range, the `{fontstack}/{range}.pbf` file a style's `glyphs` URL points to: the
141
+ * glyphs of every font stack it holds. Glyphs whose bitmap does not match their size are skipped.
142
+ */
143
+ export function parseGlyphRange(data: ArrayBuffer | Uint8Array): Glyph[] {
144
+ const glyphs: Glyph[] = [];
145
+ const reader = new Reader(data instanceof Uint8Array ? data : new Uint8Array(data));
146
+ reader.readFields((tag, wireType) => {
147
+ if (tag !== 1 || wireType !== 2) {
148
+ reader.skip(wireType);
149
+ return;
150
+ }
151
+ const fontstack = new Reader(reader.readBytes());
152
+ fontstack.readFields((fontstackTag, fontstackWireType) => {
153
+ if (fontstackTag === 3 && fontstackWireType === 2) {
154
+ const glyph = readGlyph(fontstack.readBytes());
155
+ if (glyph) {
156
+ glyphs.push(glyph);
157
+ }
158
+ } else {
159
+ fontstack.skip(fontstackWireType);
160
+ }
161
+ });
162
+ });
163
+ return glyphs;
164
+ }