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

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,400 @@
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 {log} from '@deck.gl/core';
10
+ import {getCompiledStyleProperty, type StyleFeature} from './style-expression';
11
+ import {
12
+ type ResolvedSpriteIcon,
13
+ type SpriteAtlas,
14
+ type SpriteIconMappingEntry,
15
+ getSpriteImageNames,
16
+ resolveSpriteIcon
17
+ } from './sprite';
18
+
19
+ type StyleLayerLike = {
20
+ paint?: Record<string, unknown>;
21
+ layout?: Record<string, unknown>;
22
+ };
23
+
24
+ /** The features of a patterned fill whose images come from one sprite. */
25
+ export type FillPatternGroup = {
26
+ atlas: SpriteAtlas;
27
+ /** The atlas's index in the style's sprite list, unique per group. */
28
+ atlasIndex: number;
29
+ features: any[];
30
+ /** The image name in the atlas's mapping, for every feature or per feature. */
31
+ getFillPattern: string | ((feature: StyleFeature) => string);
32
+ /** The image's pixel ratio, device pixels per CSS pixel, for every feature or per feature. */
33
+ getPixelRatio: number | ((feature: StyleFeature) => number);
34
+ /** The names of the group's images, sorted. */
35
+ names: string[];
36
+ /** The group's pattern atlas, made on first use and released with the group. */
37
+ patternAtlas?: PatternAtlas;
38
+ };
39
+
40
+ const warnedMissingPatterns = new Set<string>();
41
+
42
+ function warnMissingPattern(name: string): void {
43
+ if (!warnedMissingPatterns.has(name)) {
44
+ warnedMissingPatterns.add(name);
45
+ log.warn(`[BasemapLayer] fill-pattern "${name}" is not in the style's sprite; skipped`)();
46
+ }
47
+ }
48
+
49
+ /** The latest groups per tile content and style layer, reused while their inputs are unchanged. */
50
+ const patternGroupCache = new WeakMap<
51
+ any[],
52
+ WeakMap<StyleLayerLike, {key: unknown[]; groups: FillPatternGroup[]}>
53
+ >();
54
+
55
+ /**
56
+ * Splits a patterned fill's features by the sprite their `fill-pattern` image is in. Features
57
+ * whose image is in no loaded sprite draw nothing, as in MapLibre GL JS. `fill-pattern` is
58
+ * evaluated at the integer zoom, as the style spec does for cross-faded properties.
59
+ *
60
+ * Returns no groups while the sprites load (`null`) or when the layer has no `fill-pattern`. A
61
+ * pattern that does not depend on feature data keeps the tile's feature array, so deck.gl does not
62
+ * re-tessellate the features at a new zoom step; per-feature groups are cached per tile, style
63
+ * layer, sprite list and zoom.
64
+ */
65
+ export function getFillPatternGroups(
66
+ features: any[],
67
+ styleLayer: StyleLayerLike,
68
+ zoom: number,
69
+ spriteAtlases: SpriteAtlas[] | null | undefined
70
+ ): FillPatternGroup[] {
71
+ const property = getCompiledStyleProperty(styleLayer, 'fill-pattern');
72
+ if (!property || !spriteAtlases?.length) {
73
+ return [];
74
+ }
75
+ const patternZoom = Math.floor(zoom);
76
+ const imageNames = getSpriteImageNames(spriteAtlases);
77
+ const resolve = (feature?: StyleFeature): ResolvedSpriteIcon | null => {
78
+ const value = property.evaluate(patternZoom, feature, imageNames);
79
+ const name = value === null || value === undefined ? '' : String(value);
80
+ if (!name) {
81
+ return null;
82
+ }
83
+ const icon = resolveSpriteIcon(spriteAtlases, name);
84
+ if (!icon) {
85
+ warnMissingPattern(name);
86
+ }
87
+ return icon;
88
+ };
89
+
90
+ if (!property.isFeatureDependent) {
91
+ const icon = resolve();
92
+ return icon
93
+ ? [
94
+ {
95
+ atlas: icon.atlas,
96
+ atlasIndex: spriteAtlases.indexOf(icon.atlas),
97
+ features,
98
+ getFillPattern: icon.name,
99
+ getPixelRatio: icon.entry.pixelRatio,
100
+ names: [icon.name]
101
+ }
102
+ ]
103
+ : [];
104
+ }
105
+
106
+ const key = [spriteAtlases, property.isZoomDependent ? patternZoom : null];
107
+ let byStyleLayer = patternGroupCache.get(features);
108
+ if (!byStyleLayer) {
109
+ byStyleLayer = new WeakMap();
110
+ patternGroupCache.set(features, byStyleLayer);
111
+ }
112
+ const cached = byStyleLayer.get(styleLayer);
113
+ if (cached && cached.key.every((entry, index) => entry === key[index])) {
114
+ return cached.groups;
115
+ }
116
+
117
+ const icons = new Map<StyleFeature, ResolvedSpriteIcon>();
118
+ const groups = new Map<SpriteAtlas, FillPatternGroup>();
119
+ const pixelRatios = new Map<SpriteAtlas, Set<number>>();
120
+ for (const feature of features) {
121
+ const icon = resolve(feature);
122
+ if (!icon) {
123
+ continue;
124
+ }
125
+ icons.set(feature, icon);
126
+ let group = groups.get(icon.atlas);
127
+ if (!group) {
128
+ group = {
129
+ atlas: icon.atlas,
130
+ atlasIndex: spriteAtlases.indexOf(icon.atlas),
131
+ features: [],
132
+ getFillPattern: feature => icons.get(feature)!.name,
133
+ getPixelRatio: feature => icons.get(feature)!.entry.pixelRatio,
134
+ names: []
135
+ };
136
+ groups.set(icon.atlas, group);
137
+ pixelRatios.set(icon.atlas, new Set());
138
+ }
139
+ group.features.push(feature);
140
+ group.names.push(icon.name);
141
+ pixelRatios.get(icon.atlas)!.add(icon.entry.pixelRatio);
142
+ }
143
+ const result = [...groups.values()];
144
+ for (const group of result) {
145
+ group.names = [...new Set(group.names)].sort();
146
+ }
147
+ // A constant pixel ratio, as a sprite normally has, keeps the pattern scale a constant.
148
+ for (const group of result) {
149
+ const ratios = pixelRatios.get(group.atlas)!;
150
+ if (ratios.size === 1) {
151
+ group.getPixelRatio = [...ratios][0];
152
+ }
153
+ }
154
+ byStyleLayer.set(styleLayer, {key, groups: result});
155
+ return result;
156
+ }
157
+
158
+ const PATTERN_TEXTURE_PARAMETERS = {mipmapFilter: 'none'} as const;
159
+
160
+ type PatternAtlas = {
161
+ image: SpriteAtlas['image'] | OffscreenCanvas | HTMLCanvasElement | ImageData;
162
+ mapping: Record<string, SpriteIconMappingEntry>;
163
+ };
164
+
165
+ /**
166
+ * Single-image pattern atlases per sprite. A sprite has a fixed set of images, so this holds at
167
+ * most one small canvas per image a style draws as a pattern, for as long as the sprite is loaded.
168
+ * Atlases of several images depend on a tile's features and are kept on their group instead (see
169
+ * `getFillPatternProps`), so they are released with the tile.
170
+ */
171
+ const patternAtlasCache = new WeakMap<SpriteAtlas, Map<string, PatternAtlas>>();
172
+
173
+ function createCanvas(width: number, height: number): OffscreenCanvas | HTMLCanvasElement | null {
174
+ if (typeof OffscreenCanvas !== 'undefined') {
175
+ return new OffscreenCanvas(width, height);
176
+ }
177
+ if (typeof document !== 'undefined') {
178
+ const canvas = document.createElement('canvas');
179
+ canvas.width = width;
180
+ canvas.height = height;
181
+ return canvas;
182
+ }
183
+ return null;
184
+ }
185
+
186
+ /**
187
+ * An atlas of the named sprite images for a pattern, each with a 1 pixel border copied from its
188
+ * opposite edge, as MapLibre GL JS pads its pattern atlas. A sprite packs its images edge to edge,
189
+ * so a linear filter at an image's edge would blend in the next image and draw a seam between the
190
+ * repeats; with the border, it blends in the image's own wrapped pixels. The atlas holds only
191
+ * these images, so each fill uploads a small texture instead of the whole sprite. Where the
192
+ * sprite image is a URL, or no canvas is available, it returns the sprite itself.
193
+ */
194
+ export function getPatternAtlas(atlas: SpriteAtlas, names: string[]): PatternAtlas {
195
+ if (names.length !== 1) {
196
+ return createPatternAtlas(atlas, names);
197
+ }
198
+ let byName = patternAtlasCache.get(atlas);
199
+ if (!byName) {
200
+ byName = new Map();
201
+ patternAtlasCache.set(atlas, byName);
202
+ }
203
+ let result = byName.get(names[0]);
204
+ if (!result) {
205
+ result = createPatternAtlas(atlas, names);
206
+ byName.set(names[0], result);
207
+ }
208
+ return result;
209
+ }
210
+
211
+ /**
212
+ * The largest width and height of a pattern atlas. WebGL 2 guarantees textures of 2048 pixels; in
213
+ * practice every device that runs it supports 4096, as MapLibre GL JS also assumes for its atlases.
214
+ */
215
+ export const MAX_PATTERN_ATLAS_SIZE = 4096;
216
+
217
+ /**
218
+ * Places images with a 1 pixel border on each side in rows no wider than `maxSize`: shelf packing,
219
+ * tallest images first. Returns each image's top left corner, inside its border, and the atlas
220
+ * size, or `null` when the images do not fit in `maxSize` by `maxSize`.
221
+ */
222
+ export function packPatternImages(
223
+ sizes: {width: number; height: number}[],
224
+ maxSize = MAX_PATTERN_ATLAS_SIZE
225
+ ): {positions: [number, number][]; width: number; height: number} | null {
226
+ const order = sizes.map((_, index) => index).sort((a, b) => sizes[b].height - sizes[a].height);
227
+ const positions: [number, number][] = new Array(sizes.length);
228
+ let x = 0;
229
+ let y = 0;
230
+ let rowHeight = 0;
231
+ let width = 0;
232
+ for (const index of order) {
233
+ const w = sizes[index].width + 2;
234
+ const h = sizes[index].height + 2;
235
+ if (x > 0 && x + w > maxSize) {
236
+ y += rowHeight;
237
+ x = 0;
238
+ rowHeight = 0;
239
+ }
240
+ if (x + w > maxSize || y + h > maxSize) {
241
+ return null;
242
+ }
243
+ positions[index] = [x + 1, y + 1];
244
+ x += w;
245
+ rowHeight = Math.max(rowHeight, h);
246
+ width = Math.max(width, x);
247
+ }
248
+ return {positions, width, height: y + rowHeight};
249
+ }
250
+
251
+ function createPatternAtlas(atlas: SpriteAtlas, names: string[]): PatternAtlas {
252
+ const sprite: PatternAtlas = {image: atlas.image, mapping: atlas.mapping};
253
+ const entries = names.map(name => atlas.mapping[name]);
254
+ const packing = packPatternImages(entries);
255
+ if (!packing) {
256
+ // Too many images for one texture: the sprite itself, which the style's renderer loaded as
257
+ // one texture, draws them, with seams at the image edges.
258
+ log.warn(
259
+ '[BasemapLayer] fill-pattern images exceed the pattern atlas size; using the sprite'
260
+ )();
261
+ return sprite;
262
+ }
263
+ const canvas =
264
+ typeof atlas.image === 'string' ? null : createCanvas(packing.width, packing.height);
265
+ const context = canvas?.getContext('2d') as
266
+ | OffscreenCanvasRenderingContext2D
267
+ | CanvasRenderingContext2D
268
+ | null
269
+ | undefined;
270
+ if (!canvas || !context) {
271
+ return sprite;
272
+ }
273
+ const mapping: Record<string, SpriteIconMappingEntry> = {};
274
+ names.forEach((name, index) => {
275
+ const entry = entries[index];
276
+ const {x, y, width: w, height: h} = entry;
277
+ const [left, top] = packing.positions[index];
278
+ // The image, then each border from the opposite edge, corners included: a 3x3 tiling of the
279
+ // image clipped to its 1 pixel border.
280
+ for (const dx of [-1, 0, 1]) {
281
+ for (const dy of [-1, 0, 1]) {
282
+ const sx = dx === 0 ? x : dx < 0 ? x + w - 1 : x;
283
+ const sy = dy === 0 ? y : dy < 0 ? y + h - 1 : y;
284
+ const sw = dx === 0 ? w : 1;
285
+ const sh = dy === 0 ? h : 1;
286
+ const tx = left + (dx === 0 ? 0 : dx < 0 ? -1 : w);
287
+ const ty = top + (dy === 0 ? 0 : dy < 0 ? -1 : h);
288
+ context.drawImage(atlas.image as CanvasImageSource, sx, sy, sw, sh, tx, ty, sw, sh);
289
+ }
290
+ }
291
+ mapping[name] = {...entry, x: left, y: top};
292
+ });
293
+ const image = context.getImageData(0, 0, packing.width, packing.height);
294
+ // Each image with its border on its own, so that no image takes colors from its neighbour's.
295
+ names.forEach((name, index) => {
296
+ const [left, top] = packing.positions[index];
297
+ const {width: w, height: h} = entries[index];
298
+ bleedIntoTransparentPixels(image, [left - 1, top - 1, left + w + 1, top + h + 1]);
299
+ });
300
+ return {image, mapping};
301
+ }
302
+
303
+ /**
304
+ * Gives each fully transparent pixel of the rectangle `[left, top, right, bottom)` (the whole
305
+ * image by default) next to a visible one the alpha-weighted average color of its neighbors in
306
+ * the rectangle, keeping it transparent. MapLibre GL JS samples its pattern atlas, which is
307
+ * premultiplied, with a linear filter, so a pattern's edges blend toward transparent in its own
308
+ * color. A canvas uploads unpremultiplied, with transparent pixels black: the filter would blend
309
+ * the edges toward black, and draw light patterns with dark outlines. The image is read back from
310
+ * the canvas so that these colors survive (a canvas keeps none for transparent pixels). It is
311
+ * changed in place, with no copy: only transparent pixels change, and their alpha, which weighs
312
+ * every neighbor's color, stays 0, so no pixel's new color depends on another's.
313
+ */
314
+ export function bleedIntoTransparentPixels(
315
+ {data, width, height}: ImageData,
316
+ [left, top, right, bottom]: number[] = [0, 0, width, height]
317
+ ): void {
318
+ for (let y = top; y < bottom; y++) {
319
+ for (let x = left; x < right; x++) {
320
+ const i = (y * width + x) * 4;
321
+ if (data[i + 3] !== 0) {
322
+ continue;
323
+ }
324
+ let r = 0;
325
+ let g = 0;
326
+ let b = 0;
327
+ let count = 0;
328
+ for (let dy = -1; dy <= 1; dy++) {
329
+ for (let dx = -1; dx <= 1; dx++) {
330
+ const nx = x + dx;
331
+ const ny = y + dy;
332
+ if (nx < left || ny < top || nx >= right || ny >= bottom) {
333
+ continue;
334
+ }
335
+ // Weighted by alpha, as a premultiplied filter weighs them.
336
+ const j = (ny * width + nx) * 4;
337
+ const alpha = data[j + 3];
338
+ r += data[j] * alpha;
339
+ g += data[j + 1] * alpha;
340
+ b += data[j + 2] * alpha;
341
+ count += alpha;
342
+ }
343
+ }
344
+ if (count) {
345
+ data[i] = r / count;
346
+ data[i + 1] = g / count;
347
+ data[i + 2] = b / count;
348
+ }
349
+ }
350
+ }
351
+ }
352
+
353
+ /** The features without an image per list of groups. */
354
+ const unpatternedCache = new WeakMap<FillPatternGroup[], any[]>();
355
+
356
+ /**
357
+ * The features of a patterned fill that no group draws, because no loaded sprite has their image.
358
+ * MapLibre GL JS draws no fill for them, but still draws their `fill-outline-color` outline.
359
+ * Cached per list of groups, which `getFillPatternGroups` reuses while its inputs are unchanged.
360
+ */
361
+ export function getFeaturesWithoutPattern(features: any[], groups: FillPatternGroup[]): any[] {
362
+ let result = unpatternedCache.get(groups);
363
+ if (!result) {
364
+ const patterned = new Set(groups.flatMap(group => group.features));
365
+ result = patterned.size === features.length ? [] : features.filter(f => !patterned.has(f));
366
+ unpatternedCache.set(groups, result);
367
+ }
368
+ return result;
369
+ }
370
+
371
+ /**
372
+ * `FillPatternExtension` props for a group: the sprite atlas and mapping, and each image repeated
373
+ * at its CSS-pixel size at the integer zoom, as MapLibre GL JS sizes patterns. Between integer
374
+ * zooms the pattern scales with the map, and it steps back at the next integer zoom. The image
375
+ * supplies the colors, so the fill color is only the opacity.
376
+ */
377
+ export function getFillPatternProps(group: FillPatternGroup, zoom: number) {
378
+ const unitScale = 2 ** -Math.floor(zoom);
379
+ const {getPixelRatio} = group;
380
+ // Kept on the group: a tile's groups, and so their atlases, are released with its features.
381
+ // A layer turns the atlas image into its own texture and keeps the image as a prop, so no live
382
+ // layer loses its texture when an atlas is released.
383
+ group.patternAtlas ??= getPatternAtlas(group.atlas, group.names);
384
+ const {image, mapping} = group.patternAtlas;
385
+ return {
386
+ fillPatternAtlas: image,
387
+ fillPatternMapping: mapping,
388
+ fillPatternMask: false,
389
+ // Sampled without mipmaps, as in MapLibre GL JS: a mipmap level would blend the atlas's
390
+ // images, and an image's own quadrants. Patterns minify only where a sprite's pixel ratio
391
+ // exceeds the screen's.
392
+ textureParameters: PATTERN_TEXTURE_PARAMETERS,
393
+ fillPatternSizeUnits: 'common' as const,
394
+ getFillPattern: group.getFillPattern,
395
+ getFillPatternScale:
396
+ typeof getPixelRatio === 'function'
397
+ ? (feature: StyleFeature) => unitScale / getPixelRatio(feature)
398
+ : unitScale / getPixelRatio
399
+ };
400
+ }
@@ -0,0 +1,177 @@
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, project} from '@deck.gl/core';
10
+ import type {CompositeLayer, Layer} from '@deck.gl/core';
11
+ import {FillStyleExtension} from '@deck.gl/extensions';
12
+
13
+ /** Whether a layer's device runs the GLSL injections of these extensions. */
14
+ function isGLSLDevice(layer: Layer): boolean {
15
+ return layer.context.device.type !== 'webgpu';
16
+ }
17
+
18
+ /**
19
+ * `fill-translate` in common space. The offset is in pixels on the ground plane, as MapLibre GL
20
+ * JS applies it in tile units: x east and y south on a map, along the local east and south on a
21
+ * globe. The vertex moves before later injections run, so a pattern moves with the fill.
22
+ */
23
+ const fillTranslateModule = {
24
+ name: 'fillTranslate',
25
+ dependencies: [project],
26
+ vs: /* glsl */ `\
27
+ layout(std140) uniform fillTranslateUniforms {
28
+ vec2 offset;
29
+ } fillTranslate;
30
+ `,
31
+ uniformTypes: {offset: 'vec2<f32>'},
32
+ inject: {
33
+ 'vs:DECKGL_FILTER_GL_POSITION': {
34
+ order: -1,
35
+ injection: /* glsl */ `
36
+ if (fillTranslate.offset != vec2(0.0)) {
37
+ vec2 fillTranslate_pixels = vec2(fillTranslate.offset.x, -fillTranslate.offset.y);
38
+ if (project.projectionMode == PROJECTION_MODE_GLOBE) {
39
+ // The globe is centred at the origin with its axis along z.
40
+ vec3 fillTranslate_up = normalize(geometry.position.xyz);
41
+ vec3 fillTranslate_east = vec3(-fillTranslate_up.y, fillTranslate_up.x, 0.0);
42
+ fillTranslate_east = length(fillTranslate_east) > 0.0
43
+ ? normalize(fillTranslate_east)
44
+ : vec3(1.0, 0.0, 0.0);
45
+ vec3 fillTranslate_north = cross(fillTranslate_up, fillTranslate_east);
46
+ geometry.position.xyz +=
47
+ (fillTranslate_east * fillTranslate_pixels.x + fillTranslate_north * fillTranslate_pixels.y) /
48
+ project.scale;
49
+ } else {
50
+ geometry.position.xy += fillTranslate_pixels / project.scale;
51
+ }
52
+ position = project_common_position_to_clipspace(geometry.position);
53
+ }
54
+ `
55
+ }
56
+ }
57
+ } as const;
58
+
59
+ /** The anchor of `fill-translate`. */
60
+ export type FillTranslateAnchor = 'map' | 'viewport';
61
+
62
+ type FillTranslateExtensionProps = {
63
+ /** `fill-translate`: the offset in pixels, x right and y down. */
64
+ fillTranslate?: [number, number];
65
+ /** `fill-translate-anchor`: `'map'` turns the offset with the map, `'viewport'` keeps it on screen. */
66
+ fillTranslateAnchor?: FillTranslateAnchor;
67
+ };
68
+
69
+ /**
70
+ * Returns the ground offset of `fill-translate` in pixels, x east and y south, at a map bearing in
71
+ * degrees. A `'viewport'` offset is turned by the bearing, so it keeps its direction on screen,
72
+ * as in MapLibre GL JS.
73
+ */
74
+ export function getFillTranslateOffset(
75
+ translate: readonly [number, number],
76
+ anchor: FillTranslateAnchor,
77
+ bearing: number
78
+ ): [number, number] {
79
+ const [x, y] = translate;
80
+ if (anchor !== 'viewport' || !bearing) {
81
+ return [x, y];
82
+ }
83
+ const angle = (bearing * Math.PI) / 180;
84
+ const cos = Math.cos(angle);
85
+ const sin = Math.sin(angle);
86
+ return [x * cos - y * sin, x * sin + y * cos];
87
+ }
88
+
89
+ /**
90
+ * Offsets a fill and its outline by `fill-translate` pixels, anchored to the map or to the
91
+ * viewport by `fill-translate-anchor`. WebGL only; on WebGPU fills draw untranslated.
92
+ *
93
+ * Added to a `GeoJsonLayer`, it reaches the polygon fill and outline sublayers.
94
+ */
95
+ export class FillTranslateExtension extends LayerExtension {
96
+ static extensionName = 'FillTranslateExtension';
97
+ static defaultProps = {
98
+ fillTranslate: {type: 'array', value: [0, 0], compare: true},
99
+ fillTranslateAnchor: 'map'
100
+ };
101
+
102
+ getShaders(this: Layer) {
103
+ if (!isGLSLDevice(this)) {
104
+ return {};
105
+ }
106
+ return {modules: [fillTranslateModule]};
107
+ }
108
+
109
+ draw(this: Layer<FillTranslateExtensionProps>) {
110
+ if (!isGLSLDevice(this)) {
111
+ return;
112
+ }
113
+ const {fillTranslate = [0, 0], fillTranslateAnchor = 'map'} = this.props;
114
+ const bearing = (this.context.viewport as {bearing?: number}).bearing || 0;
115
+ this.setShaderModuleProps({
116
+ fillTranslate: {
117
+ offset: getFillTranslateOffset(fillTranslate, fillTranslateAnchor, bearing)
118
+ }
119
+ });
120
+ }
121
+ }
122
+
123
+ /**
124
+ * On a globe, places the pattern by the vertex's Web Mercator position instead of `FillStyleExtension`'s
125
+ * common position, whose x and y lie in the globe's equatorial plane: the pattern then keeps its
126
+ * size and shape across the globe, and stays continuous across tiles, as on a map. Runs after
127
+ * `FillStyleExtension`'s injection (`order: 1`), with the pattern size in common units.
128
+ */
129
+ const globePatternModule = {
130
+ name: 'globeFillPattern',
131
+ dependencies: [project],
132
+ inject: {
133
+ 'vs:DECKGL_FILTER_GL_POSITION': {
134
+ order: 1,
135
+ injection: /* glsl */ `
136
+ if (project.projectionMode == PROJECTION_MODE_GLOBE && fill.patternEnabled && !fill.procedural) {
137
+ vec2 globePattern_frame = fill.patternUnitScale * fillPatternScales * fillPatternFrames.zw;
138
+ // Pattern atlases use top-left coordinates; Web Mercator y grows northward.
139
+ fill_uv = project_mercator_(geometry.worldPosition.xy) / globePattern_frame * vec2(1.0, -1.0);
140
+ fill_uv += fillPatternOffsets;
141
+ }
142
+ `
143
+ }
144
+ }
145
+ } as const;
146
+
147
+ /**
148
+ * `FillStyleExtension` with image patterns, run only where its GLSL shader runs. On WebGPU it adds
149
+ * no shader code, and the fills it is added to draw nothing, as before patterns were supported:
150
+ * their fill color is only the pattern's opacity.
151
+ */
152
+ export class FillPatternExtension extends FillStyleExtension {
153
+ static extensionName = 'FillPatternExtension';
154
+
155
+ constructor() {
156
+ super({pattern: true});
157
+ }
158
+
159
+ getShaders(this: Layer, extension: this): any {
160
+ const shaders = super.getShaders.call(this, extension);
161
+ return (
162
+ shaders && {
163
+ ...shaders,
164
+ modules: [...shaders.modules, globePatternModule]
165
+ }
166
+ );
167
+ }
168
+
169
+ isEnabled(layer: Layer): boolean {
170
+ return isGLSLDevice(layer) && super.isEnabled(layer);
171
+ }
172
+
173
+ getSubLayerProps(this: CompositeLayer, extension: this): any {
174
+ const props = LayerExtension.prototype.getSubLayerProps.call(this, extension);
175
+ return isGLSLDevice(this) ? props : {...props, getFillColor: [0, 0, 0, 0]};
176
+ }
177
+ }