@fusion.dev/helix-map 0.0.0-dev

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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -0
  3. package/dist/connect-map/connect-map.d.ts +317 -0
  4. package/dist/connect-map/connect-map.d.ts.map +1 -0
  5. package/dist/connect-map/connect-map.js +847 -0
  6. package/dist/connect-map/connect-map.js.map +1 -0
  7. package/dist/connect-map/index.d.ts +3 -0
  8. package/dist/connect-map/index.d.ts.map +1 -0
  9. package/dist/connect-map/index.js +2 -0
  10. package/dist/connect-map/index.js.map +1 -0
  11. package/dist/contours/contours.d.ts +71 -0
  12. package/dist/contours/contours.d.ts.map +1 -0
  13. package/dist/contours/contours.js +314 -0
  14. package/dist/contours/contours.js.map +1 -0
  15. package/dist/credit/credit.d.ts +20 -0
  16. package/dist/credit/credit.d.ts.map +1 -0
  17. package/dist/credit/credit.js +65 -0
  18. package/dist/credit/credit.js.map +1 -0
  19. package/dist/credit/index.d.ts +3 -0
  20. package/dist/credit/index.d.ts.map +1 -0
  21. package/dist/credit/index.js +2 -0
  22. package/dist/credit/index.js.map +1 -0
  23. package/dist/index.d.ts +12 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +7 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/leaflet/index.d.ts +3 -0
  28. package/dist/leaflet/index.d.ts.map +1 -0
  29. package/dist/leaflet/index.js +2 -0
  30. package/dist/leaflet/index.js.map +1 -0
  31. package/dist/leaflet/leaflet-driver.d.ts +126 -0
  32. package/dist/leaflet/leaflet-driver.d.ts.map +1 -0
  33. package/dist/leaflet/leaflet-driver.js +241 -0
  34. package/dist/leaflet/leaflet-driver.js.map +1 -0
  35. package/dist/maplibre/index.d.ts +5 -0
  36. package/dist/maplibre/index.d.ts.map +1 -0
  37. package/dist/maplibre/index.js +3 -0
  38. package/dist/maplibre/index.js.map +1 -0
  39. package/dist/maplibre/map-images.d.ts +17 -0
  40. package/dist/maplibre/map-images.d.ts.map +1 -0
  41. package/dist/maplibre/map-images.js +138 -0
  42. package/dist/maplibre/map-images.js.map +1 -0
  43. package/dist/maplibre/maplibre-driver.d.ts +276 -0
  44. package/dist/maplibre/maplibre-driver.d.ts.map +1 -0
  45. package/dist/maplibre/maplibre-driver.js +1114 -0
  46. package/dist/maplibre/maplibre-driver.js.map +1 -0
  47. package/dist/motion/index.d.ts +3 -0
  48. package/dist/motion/index.d.ts.map +1 -0
  49. package/dist/motion/index.js +2 -0
  50. package/dist/motion/index.js.map +1 -0
  51. package/dist/motion/motion.d.ts +41 -0
  52. package/dist/motion/motion.d.ts.map +1 -0
  53. package/dist/motion/motion.js +167 -0
  54. package/dist/motion/motion.js.map +1 -0
  55. package/dist/points/index.d.ts +3 -0
  56. package/dist/points/index.d.ts.map +1 -0
  57. package/dist/points/index.js +2 -0
  58. package/dist/points/index.js.map +1 -0
  59. package/dist/points/points.d.ts +20 -0
  60. package/dist/points/points.d.ts.map +1 -0
  61. package/dist/points/points.js +41 -0
  62. package/dist/points/points.js.map +1 -0
  63. package/dist/snapshot/camera.d.ts +44 -0
  64. package/dist/snapshot/camera.d.ts.map +1 -0
  65. package/dist/snapshot/camera.js +42 -0
  66. package/dist/snapshot/camera.js.map +1 -0
  67. package/dist/snapshot/index.d.ts +5 -0
  68. package/dist/snapshot/index.d.ts.map +1 -0
  69. package/dist/snapshot/index.js +3 -0
  70. package/dist/snapshot/index.js.map +1 -0
  71. package/dist/snapshot/snapshot.d.ts +75 -0
  72. package/dist/snapshot/snapshot.d.ts.map +1 -0
  73. package/dist/snapshot/snapshot.js +290 -0
  74. package/dist/snapshot/snapshot.js.map +1 -0
  75. package/dist/style/index.d.ts +5 -0
  76. package/dist/style/index.d.ts.map +1 -0
  77. package/dist/style/index.js +3 -0
  78. package/dist/style/index.js.map +1 -0
  79. package/dist/style/map-colors.d.ts +69 -0
  80. package/dist/style/map-colors.d.ts.map +1 -0
  81. package/dist/style/map-colors.js +82 -0
  82. package/dist/style/map-colors.js.map +1 -0
  83. package/dist/style/map-style.d.ts +99 -0
  84. package/dist/style/map-style.d.ts.map +1 -0
  85. package/dist/style/map-style.js +440 -0
  86. package/dist/style/map-style.js.map +1 -0
  87. package/dist/style/oklab.d.ts +36 -0
  88. package/dist/style/oklab.d.ts.map +1 -0
  89. package/dist/style/oklab.js +76 -0
  90. package/dist/style/oklab.js.map +1 -0
  91. package/dist/theme/index.d.ts +2 -0
  92. package/dist/theme/index.d.ts.map +1 -0
  93. package/dist/theme/index.js +2 -0
  94. package/dist/theme/index.js.map +1 -0
  95. package/dist/theme/watch-theme.d.ts +14 -0
  96. package/dist/theme/watch-theme.d.ts.map +1 -0
  97. package/dist/theme/watch-theme.js +30 -0
  98. package/dist/theme/watch-theme.js.map +1 -0
  99. package/package.json +70 -0
@@ -0,0 +1,1114 @@
1
+ import { contourInterval, contoursOf, heightsFrom, joinedAcross, tilesCovering, } from '../contours/contours.js';
2
+ import { colorsNow, mapColorsFrom, PATIENCE } from '../style/map-colors.js';
3
+ import { cssColor, mix, oklch, parseColor } from '../style/oklab.js';
4
+ import { watchTheme } from '../theme/watch-theme.js';
5
+ import { drawMapImage, IMAGE_RATIO } from './map-images.js';
6
+ const SOURCE = 'helix-layers';
7
+ export const LAYERS = [
8
+ {
9
+ filter: ['==', ['get', 'kind'], 'area'],
10
+ id: 'helix-areas',
11
+ paint: { 'fill-color': ['get', 'color'], 'fill-opacity': 0.16 },
12
+ source: SOURCE,
13
+ type: 'fill',
14
+ },
15
+ {
16
+ filter: ['==', ['get', 'kind'], 'area'],
17
+ id: 'helix-area-edges',
18
+ layout: { 'line-join': 'round' },
19
+ paint: { 'line-color': ['get', 'color'], 'line-width': 1.5 },
20
+ source: SOURCE,
21
+ type: 'line',
22
+ },
23
+ {
24
+ filter: ['all', ['==', ['get', 'kind'], 'route'], ['has', 'casing']],
25
+ id: 'helix-route-casings',
26
+ layout: { 'line-cap': 'round', 'line-join': 'round' },
27
+ paint: {
28
+ 'line-color': ['get', 'casing'],
29
+ 'line-opacity': ['case', ['boolean', ['get', 'faded'], false], 0.4, 1],
30
+ 'line-width': ['interpolate', ['linear'], ['zoom'], 4, 5.5, 14, 9],
31
+ },
32
+ source: SOURCE,
33
+ type: 'line',
34
+ },
35
+ {
36
+ filter: ['all', ['==', ['get', 'kind'], 'route'], ['==', ['get', 'pattern'], 'dotted']],
37
+ id: 'helix-route-dots',
38
+ layout: { 'line-cap': 'round', 'line-join': 'round' },
39
+ paint: {
40
+ 'line-color': ['get', 'color'],
41
+ 'line-dasharray': [0, 2],
42
+ 'line-opacity': ['case', ['boolean', ['get', 'faded'], false], 0.4, 1],
43
+ 'line-width': ['interpolate', ['linear'], ['zoom'], 4, 2.5, 14, 5],
44
+ },
45
+ source: SOURCE,
46
+ type: 'line',
47
+ },
48
+ {
49
+ filter: ['all', ['==', ['get', 'kind'], 'route'], ['!=', ['get', 'pattern'], 'dotted']],
50
+ id: 'helix-routes',
51
+ layout: { 'line-cap': 'round', 'line-join': 'round' },
52
+ paint: {
53
+ 'line-color': ['get', 'color'],
54
+ 'line-opacity': ['case', ['boolean', ['get', 'faded'], false], 0.4, 1],
55
+ 'line-width': ['interpolate', ['linear'], ['zoom'], 4, 2.5, 14, 5],
56
+ },
57
+ source: SOURCE,
58
+ type: 'line',
59
+ },
60
+ ];
61
+ export const geoJson = (layers) => ({
62
+ features: layers.flatMap((layer) => {
63
+ if (layer.kind !== 'area' && layer.kind !== 'route') {
64
+ return [];
65
+ }
66
+ const coordinates = layer.points.map(({ lat, lng }) => [lng, lat]);
67
+ return {
68
+ geometry: layer.kind === 'route'
69
+ ? { coordinates, type: 'LineString' }
70
+ : { coordinates: [[...coordinates, coordinates[0]]], type: 'Polygon' },
71
+ properties: {
72
+ ...(layer.casing ? { casing: layer.casing } : {}),
73
+ color: layer.color,
74
+ ...(layer.faded ? { faded: true } : {}),
75
+ ...(layer.pattern ? { pattern: layer.pattern } : {}),
76
+ kind: layer.kind,
77
+ label: layer.label,
78
+ },
79
+ type: 'Feature',
80
+ };
81
+ }),
82
+ type: 'FeatureCollection',
83
+ });
84
+ const buildingsLayer = (id) => `helix-buildings-${id}`;
85
+ const EXTRAS = new Set([
86
+ 'buildings',
87
+ 'contours',
88
+ 'landcover',
89
+ 'raster',
90
+ 'terrain',
91
+ ]);
92
+ const contoursSource = (id) => `helix-contours-${id}`;
93
+ const landcoverLayer = (id) => `helix-landcover-${id}`;
94
+ const COVERS = [
95
+ ['wood', oklch(0.55, 0.11, 145), 0.42],
96
+ ['grass', oklch(0.68, 0.1, 130), 0.3],
97
+ ['farmland', oklch(0.75, 0.08, 90), 0.32],
98
+ ['wetland', oklch(0.62, 0.07, 185), 0.35],
99
+ ['sand', oklch(0.8, 0.06, 80), 0.4],
100
+ ['rock', oklch(0.62, 0.015, 60), 0.35],
101
+ ['ice', oklch(0.93, 0.03, 230), 0.55],
102
+ ];
103
+ const NO_LINES = { features: [], type: 'FeatureCollection' };
104
+ const terrainLayer = (id) => `helix-terrain-${id}`;
105
+ const terrainSource = (id, use) => `helix-terrain-${id}-${use}`;
106
+ const rasterFrame = (id, at) => `helix-raster-${id}-${at}`;
107
+ export const buildingsExtrusion = (spec, source) => ({
108
+ filter: ['!=', ['get', 'hide_3d'], true],
109
+ id: buildingsLayer(spec.id),
110
+ minzoom: 13,
111
+ paint: {
112
+ 'fill-extrusion-base': ['coalesce', ['get', 'render_min_height'], 0],
113
+ 'fill-extrusion-color': spec.color,
114
+ 'fill-extrusion-height': ['coalesce', ['get', 'render_height'], 0],
115
+ 'fill-extrusion-opacity': 0.9,
116
+ },
117
+ source,
118
+ 'source-layer': 'building',
119
+ type: 'fill-extrusion',
120
+ });
121
+ const pointsSource = (id) => `helix-points-${id}`;
122
+ const pointsJson = (points) => ({
123
+ features: points.map(({ lat, lng }, at) => ({
124
+ geometry: { coordinates: [lng, lat], type: 'Point' },
125
+ properties: { at },
126
+ type: 'Feature',
127
+ })),
128
+ type: 'FeatureCollection',
129
+ });
130
+ export const pointsLayers = (id, spec, font) => {
131
+ const source = pointsSource(id);
132
+ const clustered = ['has', 'point_count'];
133
+ const size = ['step', ['get', 'point_count'], 14, 25, 17, 100, 21, 1000, 26];
134
+ return [
135
+ {
136
+ id: `${source}-halo`,
137
+ paint: {
138
+ 'circle-color': spec.color,
139
+ 'circle-opacity': 0.22,
140
+ 'circle-radius': ['+', size, 6],
141
+ },
142
+ type: 'circle',
143
+ },
144
+ {
145
+ id: `${source}-clusters`,
146
+ paint: {
147
+ 'circle-color': spec.color,
148
+ 'circle-radius': size,
149
+ 'circle-stroke-color': spec.casing ?? spec.color,
150
+ 'circle-stroke-width': 2,
151
+ },
152
+ type: 'circle',
153
+ },
154
+ {
155
+ id: `${source}-count`,
156
+ layout: {
157
+ 'text-allow-overlap': true,
158
+ 'text-field': ['get', 'point_count_abbreviated'],
159
+ 'text-font': font,
160
+ 'text-size': 12,
161
+ },
162
+ paint: { 'text-color': spec.ink ?? '#ffffff' },
163
+ type: 'symbol',
164
+ },
165
+ {
166
+ id: `${source}-points`,
167
+ paint: {
168
+ 'circle-color': spec.color,
169
+ 'circle-radius': ['interpolate', ['linear'], ['zoom'], 8, 3, 15, 6],
170
+ 'circle-stroke-color': spec.casing ?? spec.color,
171
+ 'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 8, 1, 15, 1.5],
172
+ },
173
+ type: 'circle',
174
+ },
175
+ ].map((layer, at) => ({
176
+ ...layer,
177
+ filter: at === 3 ? ['!', clustered] : clustered,
178
+ source,
179
+ }));
180
+ };
181
+ const CONTROL = '.maplibregl-ctrl-attrib';
182
+ const animation = (motion) => motion ? { duration: motion.duration, easing: motion.easing } : { animate: false };
183
+ const aims = new WeakMap();
184
+ const aimFrom = (map) => {
185
+ const aim = aims.get(map);
186
+ return aim && performance.now() < aim.until
187
+ ? aim
188
+ : { bearing: map.getBearing(), pitch: map.getPitch() };
189
+ };
190
+ const turn = (map, bearing, pitch, motion) => {
191
+ aims.set(map, { bearing, pitch, until: performance.now() + (motion?.duration ?? 0) });
192
+ map.easeTo({ bearing, pitch, ...animation(motion) });
193
+ };
194
+ const bared = new WeakMap();
195
+ const isLabel = (layer) => !layer.id.startsWith('helix-') && (layer.type === 'line' || layer.type === 'symbol');
196
+ const showLabels = (map, shown) => {
197
+ const visibility = shown ? 'visible' : 'none';
198
+ for (const layer of map.getStyle()?.layers ?? []) {
199
+ if (isLabel(layer) &&
200
+ (map.getLayoutProperty?.(layer.id, 'visibility') ?? 'visible') !== visibility) {
201
+ map.setLayoutProperty(layer.id, 'visibility', visibility);
202
+ }
203
+ }
204
+ };
205
+ const supplyImage = (holder, id) => {
206
+ if (!id?.startsWith('helix-') || holder.hasImage?.(id)) {
207
+ return;
208
+ }
209
+ const style = holder.getStyle();
210
+ const [named = '', text = ''] = id.split('|');
211
+ const image = style?.metadata?.['helix:images']?.[named];
212
+ const drawn = image ? drawMapImage(image, text) : undefined;
213
+ if (drawn) {
214
+ holder.addImage?.(id, drawn, { pixelRatio: IMAGE_RATIO });
215
+ }
216
+ };
217
+ const PICTURE = { height: 90, width: 120 };
218
+ export const mapLibreDriver = (map) => ({
219
+ bearing: () => map.getBearing(),
220
+ behind: ({ lat, lng }) => map.transform?.isLocationOccluded?.({ lat, lng }) === true,
221
+ credits(changed) {
222
+ const control = map.getContainer().querySelector(CONTROL);
223
+ const display = control?.style.display ?? '';
224
+ if (control) {
225
+ control.style.display = 'none';
226
+ }
227
+ const learned = (event) => {
228
+ if (event?.sourceDataType === 'metadata') {
229
+ changed();
230
+ }
231
+ };
232
+ map.on('styledata', changed);
233
+ map.on('sourcedata', learned);
234
+ return {
235
+ read: () => Object.keys(map.getStyle()?.sources ?? {})
236
+ .map((id) => map.getSource(id)?.attribution ?? '')
237
+ .filter((one) => one !== ''),
238
+ stop: () => {
239
+ map.off('styledata', changed);
240
+ map.off('sourcedata', learned);
241
+ if (control) {
242
+ control.style.display = display;
243
+ }
244
+ },
245
+ };
246
+ },
247
+ fit: ({ northEast, southWest }, padding, motion) => {
248
+ map.fitBounds([
249
+ [southWest.lng, southWest.lat],
250
+ [northEast.lng, northEast.lat],
251
+ ], { ...animation(motion), padding });
252
+ },
253
+ focusTarget: () => map.getCanvas(),
254
+ labels: (shown) => {
255
+ const heard = bared.has(map);
256
+ bared.set(map, !shown);
257
+ showLabels(map, shown);
258
+ if (!heard) {
259
+ map.on('styledata', () => {
260
+ if (bared.get(map)) {
261
+ showLabels(map, false);
262
+ }
263
+ });
264
+ }
265
+ },
266
+ layers: (read, events) => {
267
+ const drawnPoints = new Map();
268
+ const takeOff = (id) => {
269
+ const source = pointsSource(id);
270
+ for (const suffix of ['points', 'count', 'clusters', 'halo']) {
271
+ if (map.getLayer(`${source}-${suffix}`)) {
272
+ map.removeLayer(`${source}-${suffix}`);
273
+ }
274
+ }
275
+ if (map.getSource(source)) {
276
+ map.removeSource(source);
277
+ }
278
+ };
279
+ const drawPoints = (specs) => {
280
+ const faces = (map.getStyle()?.layers ?? [])
281
+ .filter((layer) => layer.type === 'symbol')
282
+ .map((layer) => layer.layout?.['text-font'])
283
+ .filter((face) => Array.isArray(face) && face.every((one) => typeof one === 'string'))
284
+ .filter((face) => !face.some((one) => /italic/i.test(one)));
285
+ const font = faces.find((face) => face.some((one) => /bold/i.test(one))) ??
286
+ faces[0] ?? ['Noto Sans Regular'];
287
+ const wanted = new Set(specs.map((spec) => spec.id));
288
+ for (const id of [...drawnPoints.keys()]) {
289
+ if (!wanted.has(id)) {
290
+ takeOff(id);
291
+ drawnPoints.delete(id);
292
+ }
293
+ }
294
+ for (const spec of specs) {
295
+ const id = pointsSource(spec.id);
296
+ const cluster = spec.cluster === true;
297
+ const had = drawnPoints.get(spec.id);
298
+ if (had && had.cluster !== cluster) {
299
+ takeOff(spec.id);
300
+ }
301
+ const source = map.getSource(id);
302
+ if (source?.setData) {
303
+ if (had?.points !== spec.points) {
304
+ source.setData(pointsJson(spec.points));
305
+ }
306
+ for (const layer of pointsLayers(spec.id, spec, font)) {
307
+ for (const [name, value] of Object.entries(layer.paint)) {
308
+ map.setPaintProperty(layer.id, name, value);
309
+ }
310
+ }
311
+ }
312
+ else {
313
+ try {
314
+ map.addSource(id, {
315
+ cluster,
316
+ clusterMaxZoom: 14,
317
+ clusterRadius: 44,
318
+ data: pointsJson(spec.points),
319
+ type: 'geojson',
320
+ });
321
+ }
322
+ catch {
323
+ continue;
324
+ }
325
+ for (const layer of pointsLayers(spec.id, spec, font)) {
326
+ map.addLayer(layer);
327
+ }
328
+ }
329
+ drawnPoints.set(spec.id, { cluster, points: spec.points });
330
+ }
331
+ };
332
+ const drawnExtras = new Map();
333
+ const traced = new Map();
334
+ const heights = new Map();
335
+ const lines = new Map();
336
+ let tracing = 0;
337
+ const heightsOf = (url, tile, encoding) => {
338
+ const had = heights.get(url);
339
+ if (had) {
340
+ return had;
341
+ }
342
+ const read = fetch(url)
343
+ .then((response) => (response.ok ? response.blob() : undefined))
344
+ .then((blob) => (blob ? createImageBitmap(blob) : undefined))
345
+ .then((bitmap) => {
346
+ if (!bitmap) {
347
+ return undefined;
348
+ }
349
+ const canvas = document.createElement('canvas');
350
+ canvas.width = bitmap.width;
351
+ canvas.height = bitmap.height;
352
+ const context = canvas.getContext('2d', { willReadFrequently: true });
353
+ if (!context) {
354
+ return undefined;
355
+ }
356
+ context.drawImage(bitmap, 0, 0);
357
+ const pixels = context.getImageData(0, 0, bitmap.width, bitmap.height).data;
358
+ return { ...tile, heights: heightsFrom(pixels, encoding), size: bitmap.width };
359
+ })
360
+ .catch(() => undefined);
361
+ if (heights.size >= 100) {
362
+ heights.delete(heights.keys().next().value);
363
+ }
364
+ heights.set(url, read);
365
+ return read;
366
+ };
367
+ const trace = async () => {
368
+ const run = ++tracing;
369
+ const zoom = map.getZoom?.() ?? 0;
370
+ const view = map.getBounds?.();
371
+ for (const [id, spec] of traced) {
372
+ const source = map.getSource(contoursSource(id));
373
+ if (!source?.setData || !spec.src) {
374
+ continue;
375
+ }
376
+ if (zoom < 9 || !view) {
377
+ source.setData(NO_LINES);
378
+ continue;
379
+ }
380
+ const bounds = {
381
+ east: view.getEast(),
382
+ north: view.getNorth(),
383
+ south: view.getSouth(),
384
+ west: view.getWest(),
385
+ };
386
+ let z = Math.min(spec.maxZoom ?? 15, Math.max(0, Math.floor(zoom) - 1));
387
+ let tiles = tilesCovering(bounds, z);
388
+ while (tiles.length > 24 && z > 0) {
389
+ z -= 1;
390
+ tiles = tilesCovering(bounds, z);
391
+ }
392
+ const unit = spec.unit ?? 'meters';
393
+ const interval = contourInterval(zoom, unit);
394
+ const src = spec.src;
395
+ const read = await Promise.all(tiles.map((tile) => heightsOf(src
396
+ .replace('{z}', `${tile.z}`)
397
+ .replace('{x}', `${tile.x}`)
398
+ .replace('{y}', `${tile.y}`), tile, spec.encoding ?? 'terrarium')));
399
+ if (run !== tracing) {
400
+ return;
401
+ }
402
+ const known = new Map(read.filter((tile) => tile !== undefined).map((tile) => [`${tile.x}/${tile.y}`, tile]));
403
+ const traced = [];
404
+ for (const tile of known.values()) {
405
+ const key = `${spec.src}|${tile.z}/${tile.x}/${tile.y}|${interval}|${unit}`;
406
+ let found = lines.get(key);
407
+ if (!found) {
408
+ const across = 2 ** tile.z;
409
+ found = contoursOf(tile, interval, unit, (dx, dy) => known.get(`${(tile.x + dx + across) % across}/${tile.y + dy}`));
410
+ if (lines.size >= 200) {
411
+ lines.delete(lines.keys().next().value);
412
+ }
413
+ lines.set(key, found);
414
+ await new Promise((next) => setTimeout(next, 0));
415
+ if (run !== tracing) {
416
+ return;
417
+ }
418
+ }
419
+ traced.push(...found);
420
+ }
421
+ const features = joinedAcross(traced).map((line) => ({
422
+ geometry: { coordinates: line.coordinates, type: 'LineString' },
423
+ properties: { elevation: line.elevation, index: line.index },
424
+ type: 'Feature',
425
+ }));
426
+ source.setData({ features, type: 'FeatureCollection' });
427
+ }
428
+ };
429
+ const takeOffExtra = (id) => {
430
+ const had = drawnExtras.get(id);
431
+ if (had?.kind === 'contours') {
432
+ for (const layer of [contoursSource(id), `${contoursSource(id)}-label`]) {
433
+ if (map.getLayer(layer)) {
434
+ map.removeLayer(layer);
435
+ }
436
+ }
437
+ if (map.getSource(contoursSource(id))) {
438
+ map.removeSource(contoursSource(id));
439
+ }
440
+ traced.delete(id);
441
+ }
442
+ else if (had?.kind === 'landcover') {
443
+ if (map.getLayer(landcoverLayer(id))) {
444
+ map.removeLayer(landcoverLayer(id));
445
+ }
446
+ }
447
+ else if (had?.kind === 'terrain') {
448
+ if (map.getLayer(terrainLayer(id))) {
449
+ map.removeLayer(terrainLayer(id));
450
+ }
451
+ if (map.getTerrain?.()?.source === terrainSource(id, 'dem')) {
452
+ map.setTerrain?.(null);
453
+ }
454
+ lifts.delete(id);
455
+ for (const use of ['dem', 'shade']) {
456
+ if (map.getSource(terrainSource(id, use))) {
457
+ map.removeSource(terrainSource(id, use));
458
+ }
459
+ }
460
+ }
461
+ else if (had?.kind === 'buildings') {
462
+ if (map.getLayer(buildingsLayer(id))) {
463
+ map.removeLayer(buildingsLayer(id));
464
+ }
465
+ }
466
+ else if (had?.kind === 'raster') {
467
+ for (let at = 0; at < had.key.split(' ').length; at += 1) {
468
+ const name = rasterFrame(id, at);
469
+ if (map.getLayer(name)) {
470
+ map.removeLayer(name);
471
+ }
472
+ if (map.getSource(name)) {
473
+ map.removeSource(name);
474
+ }
475
+ }
476
+ }
477
+ drawnExtras.delete(id);
478
+ };
479
+ const under = () => map.getLayer(LAYERS[0]?.id ?? '')
480
+ ? LAYERS[0]?.id
481
+ : map.getStyle()?.layers?.find((layer) => layer.type === 'symbol')?.id;
482
+ const underRoads = () => {
483
+ const layers = map.getStyle()?.layers ?? [];
484
+ const lastFill = layers.findLastIndex((layer) => layer.type === 'fill' && !layer.id.startsWith('helix-'));
485
+ return layers
486
+ .slice(lastFill + 1)
487
+ .find((layer) => ['fill-extrusion', 'line', 'symbol'].includes(layer.type))?.id;
488
+ };
489
+ let standing = map.getPitch() > 0;
490
+ const lifts = new Map();
491
+ const lift = () => {
492
+ const [ground] = [...lifts.values()].slice(-1);
493
+ if (standing && ground) {
494
+ map.setTerrain?.(ground);
495
+ }
496
+ else if (map.getTerrain?.()?.source.startsWith('helix-terrain-')) {
497
+ map.setTerrain?.(null);
498
+ }
499
+ };
500
+ const stood = () => {
501
+ const now = map.getPitch() > 0;
502
+ if (now === standing) {
503
+ return;
504
+ }
505
+ standing = now;
506
+ for (const [id, had] of drawnExtras) {
507
+ if (had.kind === 'buildings' && map.getLayer(buildingsLayer(id))) {
508
+ map.setLayoutProperty(buildingsLayer(id), 'visibility', standing ? 'visible' : 'none');
509
+ }
510
+ }
511
+ lift();
512
+ };
513
+ const drawExtras = (specs) => {
514
+ const wanted = new Set(specs.map((spec) => spec.id));
515
+ for (const id of [...drawnExtras.keys()]) {
516
+ if (!wanted.has(id)) {
517
+ takeOffExtra(id);
518
+ }
519
+ }
520
+ for (const spec of specs) {
521
+ if (spec.kind === 'buildings') {
522
+ const source = map
523
+ .getStyle()
524
+ ?.layers?.find((layer) => layer['source-layer'] === 'building')?.source;
525
+ if (!source) {
526
+ continue;
527
+ }
528
+ if (map.getLayer(buildingsLayer(spec.id))) {
529
+ map.setPaintProperty(buildingsLayer(spec.id), 'fill-extrusion-color', spec.color);
530
+ }
531
+ else {
532
+ try {
533
+ map.addLayer({
534
+ ...buildingsExtrusion(spec, source),
535
+ layout: { visibility: standing ? 'visible' : 'none' },
536
+ }, under());
537
+ }
538
+ catch {
539
+ continue;
540
+ }
541
+ }
542
+ drawnExtras.set(spec.id, { key: spec.color, kind: 'buildings' });
543
+ continue;
544
+ }
545
+ if (spec.kind === 'landcover') {
546
+ const style = map.getStyle()?.layers ?? [];
547
+ const own = style.findIndex((layer) => layer['source-layer'] === 'landcover');
548
+ const source = style[own]?.source;
549
+ const water = style.findIndex((layer) => layer['source-layer'] === 'water');
550
+ const land = parseColor(spec.color);
551
+ if (!source || !land) {
552
+ continue;
553
+ }
554
+ const fill = ['match', ['get', 'class']];
555
+ for (const [cover, color, amount] of COVERS) {
556
+ fill.push(cover, cssColor(mix(land, color, amount)));
557
+ }
558
+ fill.push('rgba(0, 0, 0, 0)');
559
+ const layer = landcoverLayer(spec.id);
560
+ if (map.getLayer(layer)) {
561
+ map.setPaintProperty(layer, 'fill-color', fill);
562
+ }
563
+ else {
564
+ try {
565
+ map.addLayer({
566
+ filter: ['match', ['get', 'class'], COVERS.map(([cover]) => cover), true, false],
567
+ id: layer,
568
+ paint: { 'fill-color': fill },
569
+ source,
570
+ 'source-layer': 'landcover',
571
+ type: 'fill',
572
+ }, water >= 0 ? style[water]?.id : underRoads());
573
+ }
574
+ catch {
575
+ continue;
576
+ }
577
+ }
578
+ drawnExtras.set(spec.id, { key: spec.color, kind: 'landcover' });
579
+ continue;
580
+ }
581
+ if (spec.kind === 'contours') {
582
+ const key = `${spec.src ?? ''}|${spec.encoding ?? ''}|${spec.maxZoom ?? ''}|${spec.unit ?? ''}`;
583
+ if (drawnExtras.get(spec.id)?.key !== key) {
584
+ takeOffExtra(spec.id);
585
+ }
586
+ const source = contoursSource(spec.id);
587
+ const style = map.getStyle()?.layers ?? [];
588
+ const face = style
589
+ .filter((layer) => layer.type === 'symbol')
590
+ .map((layer) => layer.layout?.['text-font'])
591
+ .find((one) => Array.isArray(one) && !one.some((name) => /italic|bold/i.test(String(name)))) ?? ['Noto Sans Regular'];
592
+ const land = style.find((layer) => layer.type === 'background');
593
+ const halo = String(land?.paint?.['background-color'] ?? 'rgba(0, 0, 0, 0)');
594
+ if (map.getSource(source)) {
595
+ map.setPaintProperty(source, 'line-color', spec.color);
596
+ map.setPaintProperty(`${source}-label`, 'text-color', spec.color);
597
+ }
598
+ else {
599
+ try {
600
+ map.addSource(source, { data: NO_LINES, type: 'geojson' });
601
+ const beneath = underRoads();
602
+ map.addLayer({
603
+ id: source,
604
+ layout: { 'line-join': 'round' },
605
+ paint: {
606
+ 'line-color': spec.color,
607
+ 'line-opacity': ['case', ['get', 'index'], 0.85, 0.55],
608
+ 'line-width': ['case', ['get', 'index'], 1.1, 0.6],
609
+ },
610
+ source,
611
+ type: 'line',
612
+ }, beneath);
613
+ map.addLayer({
614
+ filter: ['get', 'index'],
615
+ id: `${source}-label`,
616
+ layout: {
617
+ 'symbol-placement': 'line',
618
+ 'symbol-spacing': 200,
619
+ 'text-field': [
620
+ 'concat',
621
+ ['to-string', ['get', 'elevation']],
622
+ spec.unit === 'feet' ? ' ft' : ' m',
623
+ ],
624
+ 'text-font': face,
625
+ 'text-max-angle': 40,
626
+ 'text-size': 10,
627
+ },
628
+ paint: {
629
+ 'text-color': spec.color,
630
+ 'text-halo-color': halo,
631
+ 'text-halo-width': 1.5,
632
+ },
633
+ source,
634
+ type: 'symbol',
635
+ }, beneath);
636
+ }
637
+ catch {
638
+ continue;
639
+ }
640
+ }
641
+ traced.set(spec.id, spec);
642
+ drawnExtras.set(spec.id, { key, kind: 'contours' });
643
+ void trace();
644
+ continue;
645
+ }
646
+ if (spec.kind === 'terrain') {
647
+ const key = `${spec.src ?? ''}|${spec.encoding ?? ''}|${spec.maxZoom ?? ''}|${spec.raised ? (spec.exaggeration ?? 1) : ''}`;
648
+ if (drawnExtras.get(spec.id)?.key !== key) {
649
+ takeOffExtra(spec.id);
650
+ }
651
+ const paint = {
652
+ 'hillshade-accent-color': spec.color,
653
+ 'hillshade-highlight-color': spec.highlight ?? spec.color,
654
+ 'hillshade-shadow-color': spec.color,
655
+ };
656
+ const shade = terrainLayer(spec.id);
657
+ if (map.getLayer(shade)) {
658
+ for (const [name, value] of Object.entries(paint)) {
659
+ map.setPaintProperty(shade, name, value);
660
+ }
661
+ }
662
+ else {
663
+ try {
664
+ const uses = spec.raised ? ['shade', 'dem'] : ['shade'];
665
+ for (const use of uses) {
666
+ if (!map.getSource(terrainSource(spec.id, use))) {
667
+ map.addSource(terrainSource(spec.id, use), {
668
+ ...(spec.attribution && use === 'shade'
669
+ ? { attribution: spec.attribution }
670
+ : {}),
671
+ encoding: spec.encoding ?? 'terrarium',
672
+ ...(spec.maxZoom === undefined ? {} : { maxzoom: spec.maxZoom }),
673
+ tileSize: 256,
674
+ tiles: [spec.src],
675
+ type: 'raster-dem',
676
+ });
677
+ }
678
+ }
679
+ map.addLayer({
680
+ id: shade,
681
+ paint,
682
+ source: terrainSource(spec.id, 'shade'),
683
+ type: 'hillshade',
684
+ }, underRoads());
685
+ if (spec.raised) {
686
+ lifts.set(spec.id, {
687
+ exaggeration: spec.exaggeration ?? 1,
688
+ source: terrainSource(spec.id, 'dem'),
689
+ });
690
+ lift();
691
+ }
692
+ }
693
+ catch {
694
+ continue;
695
+ }
696
+ }
697
+ drawnExtras.set(spec.id, { key, kind: 'terrain' });
698
+ continue;
699
+ }
700
+ const frames = spec.frames ?? [];
701
+ const key = `${frames.join(' ')}|${spec.ground ? 'ground' : ''}|${spec.maxZoom ?? ''}`;
702
+ if (drawnExtras.get(spec.id)?.key !== key) {
703
+ takeOffExtra(spec.id);
704
+ }
705
+ for (const [at, url] of frames.entries()) {
706
+ const name = rasterFrame(spec.id, at);
707
+ const opacity = at === spec.frame ? (spec.opacity ?? 0.7) : 0;
708
+ if (map.getLayer(name)) {
709
+ map.setPaintProperty(name, 'raster-opacity', opacity);
710
+ continue;
711
+ }
712
+ try {
713
+ if (!map.getSource(name)) {
714
+ map.addSource(name, {
715
+ ...(spec.attribution ? { attribution: spec.attribution } : {}),
716
+ ...(spec.maxZoom === undefined ? {} : { maxzoom: spec.maxZoom }),
717
+ tileSize: 256,
718
+ tiles: [url],
719
+ type: 'raster',
720
+ });
721
+ }
722
+ map.addLayer({
723
+ id: name,
724
+ paint: { 'raster-fade-duration': 0, 'raster-opacity': opacity },
725
+ source: name,
726
+ type: 'raster',
727
+ }, spec.ground ? underRoads() : under());
728
+ }
729
+ catch {
730
+ }
731
+ }
732
+ drawnExtras.set(spec.id, { key, kind: 'raster' });
733
+ }
734
+ };
735
+ let drawnData = '';
736
+ const draw = () => {
737
+ const specs = read();
738
+ drawPoints(specs.filter((spec) => spec.kind === 'points'));
739
+ const data = geoJson(specs);
740
+ const source = map.getSource(SOURCE);
741
+ if (source?.setData) {
742
+ const json = JSON.stringify(data);
743
+ if (json !== drawnData) {
744
+ source.setData(data);
745
+ drawnData = json;
746
+ }
747
+ drawExtras(specs.filter((spec) => EXTRAS.has(spec.kind)));
748
+ return;
749
+ }
750
+ try {
751
+ map.addSource(SOURCE, { data, tolerance: 1, type: 'geojson' });
752
+ }
753
+ catch {
754
+ return;
755
+ }
756
+ drawnData = JSON.stringify(data);
757
+ const labels = map.getStyle()?.layers?.find((layer) => layer.type === 'symbol')?.id;
758
+ for (const layer of LAYERS) {
759
+ map.addLayer(layer, labels);
760
+ }
761
+ drawExtras(specs.filter((spec) => EXTRAS.has(spec.kind)));
762
+ };
763
+ const restore = () => {
764
+ const lostExtras = [...drawnExtras.entries()].filter(([id, had]) => had.kind === 'buildings'
765
+ ? !map.getLayer(buildingsLayer(id))
766
+ : had.kind === 'terrain'
767
+ ? !map.getLayer(terrainLayer(id))
768
+ : had.kind === 'contours'
769
+ ? !map.getLayer(contoursSource(id))
770
+ : had.kind === 'landcover'
771
+ ? !map.getLayer(landcoverLayer(id))
772
+ : !map.getLayer(rasterFrame(id, 0)));
773
+ for (const [id] of lostExtras) {
774
+ drawnExtras.delete(id);
775
+ }
776
+ if (!map.getSource(SOURCE) ||
777
+ lostExtras.length > 0 ||
778
+ [...drawnPoints.keys()].some((id) => !map.getSource(pointsSource(id)))) {
779
+ if (!map.getSource(SOURCE)) {
780
+ drawnPoints.clear();
781
+ drawnData = '';
782
+ }
783
+ else {
784
+ for (const id of drawnPoints.keys()) {
785
+ if (!map.getSource(pointsSource(id))) {
786
+ drawnPoints.delete(id);
787
+ }
788
+ }
789
+ }
790
+ draw();
791
+ }
792
+ };
793
+ const expand = (id, cluster, center) => {
794
+ const at = center ??
795
+ map
796
+ .queryRenderedFeatures([
797
+ [0, 0],
798
+ [map.getCanvas().clientWidth, map.getCanvas().clientHeight],
799
+ ], { layers: [`${pointsSource(id)}-clusters`].filter((layer) => map.getLayer(layer)) })
800
+ .find((one) => one.properties?.cluster_id === cluster)?.geometry.coordinates;
801
+ if (!at) {
802
+ return;
803
+ }
804
+ void map
805
+ .getSource(pointsSource(id))
806
+ ?.getClusterExpansionZoom?.(cluster)
807
+ .then((zoom) => {
808
+ const motion = events?.motion() ?? null;
809
+ if (motion) {
810
+ map.easeTo({ center: at, zoom, ...animation(motion) });
811
+ }
812
+ else {
813
+ map.jumpTo({ center: at, zoom });
814
+ }
815
+ });
816
+ };
817
+ const inView = (id) => {
818
+ const layers = [`${pointsSource(id)}-clusters`, `${pointsSource(id)}-points`].filter((layer) => map.getLayer(layer));
819
+ if (layers.length === 0) {
820
+ return [];
821
+ }
822
+ const canvas = map.getCanvas();
823
+ const seen = new Set();
824
+ return map
825
+ .queryRenderedFeatures([
826
+ [0, 0],
827
+ [canvas.clientWidth, canvas.clientHeight],
828
+ ], { layers })
829
+ .flatMap((one) => {
830
+ const cluster = one.properties?.cluster_id;
831
+ const at = one.properties?.at;
832
+ const key = typeof cluster === 'number' ? `c:${cluster}` : typeof at === 'number' ? `p:${at}` : '';
833
+ const coordinates = one.geometry.coordinates;
834
+ if (key === '' || seen.has(key) || !Array.isArray(coordinates)) {
835
+ return [];
836
+ }
837
+ seen.add(key);
838
+ const [lng, lat] = coordinates;
839
+ return [
840
+ typeof cluster === 'number'
841
+ ? { cluster, count: Number(one.properties?.point_count ?? 0), place: { lat, lng } }
842
+ : { at: at, place: { lat, lng } },
843
+ ];
844
+ });
845
+ };
846
+ const settled = () => {
847
+ events?.settled?.();
848
+ };
849
+ const hit = (event) => {
850
+ if (!event?.point || drawnPoints.size === 0) {
851
+ return undefined;
852
+ }
853
+ const layers = [...drawnPoints.keys()].flatMap((id) => [`${pointsSource(id)}-clusters`, `${pointsSource(id)}-points`].filter((layer) => map.getLayer(layer)));
854
+ const { x, y } = event.point;
855
+ return map.queryRenderedFeatures([
856
+ [x - 6, y - 6],
857
+ [x + 6, y + 6],
858
+ ], { layers })[0];
859
+ };
860
+ const pressed = (event) => {
861
+ const found = hit(event);
862
+ if (!found) {
863
+ return;
864
+ }
865
+ const id = found.layer.id.replace(/^helix-points-/, '').replace(/-(clusters|points)$/, '');
866
+ const cluster = found.properties?.cluster_id;
867
+ if (typeof cluster === 'number') {
868
+ const [lng, lat] = found.geometry.coordinates;
869
+ expand(id, cluster, [lng, lat]);
870
+ return;
871
+ }
872
+ const at = found.properties?.at;
873
+ if (typeof at === 'number') {
874
+ events?.press(id, at);
875
+ }
876
+ };
877
+ const pointing = (event) => {
878
+ map.getCanvas().style.cursor = hit(event) ? 'pointer' : '';
879
+ };
880
+ map.on('styledata', restore);
881
+ map.on('load', restore);
882
+ map.on('click', pressed);
883
+ map.on('mousemove', pointing);
884
+ map.on('idle', settled);
885
+ map.on('move', stood);
886
+ const missing = (event) => supplyImage(map, event?.id);
887
+ map.on('styleimagemissing', missing);
888
+ const moved = () => {
889
+ if (traced.size > 0) {
890
+ void trace();
891
+ }
892
+ };
893
+ map.on('moveend', moved);
894
+ draw();
895
+ return {
896
+ expand: (id, cluster) => expand(id, cluster),
897
+ inView,
898
+ stop: () => {
899
+ map.off('styledata', restore);
900
+ map.off('load', restore);
901
+ map.off('click', pressed);
902
+ map.off('mousemove', pointing);
903
+ map.off('idle', settled);
904
+ map.off('move', stood);
905
+ map.off('styleimagemissing', missing);
906
+ map.off('moveend', moved);
907
+ for (const id of drawnPoints.keys()) {
908
+ takeOff(id);
909
+ }
910
+ drawnPoints.clear();
911
+ for (const id of [...drawnExtras.keys()]) {
912
+ takeOffExtra(id);
913
+ }
914
+ for (const layer of [...LAYERS].reverse()) {
915
+ if (map.getLayer(layer.id)) {
916
+ map.removeLayer(layer.id);
917
+ }
918
+ }
919
+ if (map.getSource(SOURCE)) {
920
+ map.removeSource(SOURCE);
921
+ }
922
+ },
923
+ update: draw,
924
+ };
925
+ },
926
+ onView: (moved) => {
927
+ map.on('move', moved);
928
+ map.on('resize', moved);
929
+ return () => {
930
+ map.off('move', moved);
931
+ map.off('resize', moved);
932
+ };
933
+ },
934
+ panBy: (x, y, motion) => {
935
+ map.panBy([x, y], animation(motion));
936
+ },
937
+ pitch: () => map.getPitch(),
938
+ previews: async ({ imagery, types }) => {
939
+ const center = map.getCenter?.();
940
+ const zoom = map.getZoom?.();
941
+ const style = map.getStyle();
942
+ if (!center || zoom === undefined || !style) {
943
+ return {};
944
+ }
945
+ const Picture = map.constructor;
946
+ const box = document.createElement('div');
947
+ box.style.cssText = `position:fixed;left:-10000px;top:0;width:${PICTURE.width}px;height:${PICTURE.height}px`;
948
+ document.body.append(box);
949
+ const base = JSON.parse(JSON.stringify(style));
950
+ delete base.terrain;
951
+ const ours = (id) => id.startsWith('helix-');
952
+ base.layers = (base.layers ?? []).filter((layer) => !ours(layer.id));
953
+ base.sources = Object.fromEntries(Object.entries(base.sources ?? {}).filter(([id]) => !ours(id)));
954
+ const pictures = {};
955
+ try {
956
+ for (const type of types) {
957
+ if (type !== 'standard' && !imagery) {
958
+ continue;
959
+ }
960
+ const layers = base.layers.map((layer) => isLabel(layer)
961
+ ? {
962
+ ...layer,
963
+ layout: { ...layer.layout, visibility: type === 'satellite' ? 'none' : 'visible' },
964
+ }
965
+ : layer);
966
+ if (type !== 'standard' && imagery) {
967
+ const lastFill = layers.findLastIndex((layer) => layer.type === 'fill' && !layer.id.startsWith('helix-'));
968
+ layers.splice(lastFill + 1, 0, {
969
+ id: 'helix-picture-imagery',
970
+ source: 'helix-picture-imagery',
971
+ type: 'raster',
972
+ });
973
+ }
974
+ const sources = type !== 'standard' && imagery
975
+ ? {
976
+ ...base.sources,
977
+ 'helix-picture-imagery': {
978
+ ...(imagery.maxZoom === undefined ? {} : { maxzoom: imagery.maxZoom }),
979
+ tileSize: 256,
980
+ tiles: [imagery.src],
981
+ type: 'raster',
982
+ },
983
+ }
984
+ : base.sources;
985
+ const picture = new Picture({
986
+ attributionControl: false,
987
+ bearing: map.getBearing(),
988
+ center: [center.lng, center.lat],
989
+ container: box,
990
+ fadeDuration: 0,
991
+ interactive: false,
992
+ pitch: 0,
993
+ pixelRatio: 2,
994
+ preserveDrawingBuffer: true,
995
+ style: { ...base, layers, sources },
996
+ zoom: Math.max(0, zoom - 1.5),
997
+ });
998
+ picture.on('styleimagemissing', (event) => supplyImage(picture, event?.id));
999
+ await new Promise((done) => {
1000
+ const late = setTimeout(done, 6000);
1001
+ picture.once('idle', () => {
1002
+ clearTimeout(late);
1003
+ done();
1004
+ });
1005
+ });
1006
+ pictures[type] = picture.getCanvas().toDataURL('image/jpeg', 0.85);
1007
+ picture.remove();
1008
+ }
1009
+ }
1010
+ finally {
1011
+ box.remove();
1012
+ }
1013
+ return pictures;
1014
+ },
1015
+ project: ({ lat, lng }) => {
1016
+ const { x, y } = map.project([lng, lat]);
1017
+ return { x, y };
1018
+ },
1019
+ ready: (done) => {
1020
+ if (map.loaded?.() && map.areTilesLoaded?.()) {
1021
+ done();
1022
+ return () => undefined;
1023
+ }
1024
+ const heard = () => {
1025
+ map.off('load', heard);
1026
+ done();
1027
+ };
1028
+ map.on('load', heard);
1029
+ return () => map.off('load', heard);
1030
+ },
1031
+ rotate: ({ by, to }, motion) => {
1032
+ const { bearing, pitch } = aimFrom(map);
1033
+ turn(map, to ?? bearing + (by ?? 0), pitch, motion);
1034
+ },
1035
+ show: ({ lat, lng, zoom }, motion) => {
1036
+ const options = {
1037
+ center: [lng, lat],
1038
+ ...(zoom === undefined ? {} : { zoom }),
1039
+ };
1040
+ if (motion) {
1041
+ map.easeTo({ ...options, ...animation(motion) });
1042
+ }
1043
+ else {
1044
+ map.jumpTo(options);
1045
+ }
1046
+ },
1047
+ silenceKeyboard: () => {
1048
+ const was = map.keyboard.isEnabled();
1049
+ map.keyboard.disable();
1050
+ return () => {
1051
+ if (was) {
1052
+ map.keyboard.enable();
1053
+ }
1054
+ };
1055
+ },
1056
+ tilt: ({ by, to }, motion) => {
1057
+ const { bearing, pitch } = aimFrom(map);
1058
+ turn(map, bearing, Math.max(0, to ?? pitch + (by ?? 0)), motion);
1059
+ },
1060
+ zoomBy: (direction, motion) => {
1061
+ if (direction > 0) {
1062
+ map.zoomIn(animation(motion));
1063
+ }
1064
+ else {
1065
+ map.zoomOut(animation(motion));
1066
+ }
1067
+ },
1068
+ });
1069
+ export const followTheme = (map, element, style) => {
1070
+ const first = colorsNow(element);
1071
+ let last = first ? JSON.stringify(style(first)) : '';
1072
+ let pending = 0;
1073
+ let waited = 0;
1074
+ const read = () => {
1075
+ pending = requestAnimationFrame(() => {
1076
+ const colors = colorsNow(element);
1077
+ if (!colors) {
1078
+ if (waited++ < PATIENCE) {
1079
+ read();
1080
+ }
1081
+ else {
1082
+ try {
1083
+ mapColorsFrom(element);
1084
+ }
1085
+ catch (error) {
1086
+ console.error(error);
1087
+ }
1088
+ }
1089
+ return;
1090
+ }
1091
+ waited = 0;
1092
+ const next = style(colors);
1093
+ const said = JSON.stringify(next);
1094
+ if (said !== last) {
1095
+ last = said;
1096
+ map.setStyle(next, { diff: true });
1097
+ }
1098
+ });
1099
+ };
1100
+ const update = () => {
1101
+ cancelAnimationFrame(pending);
1102
+ waited = 0;
1103
+ read();
1104
+ };
1105
+ const stopWatching = watchTheme(element, update);
1106
+ return {
1107
+ stop: () => {
1108
+ cancelAnimationFrame(pending);
1109
+ stopWatching();
1110
+ },
1111
+ update,
1112
+ };
1113
+ };
1114
+ //# sourceMappingURL=maplibre-driver.js.map