@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,847 @@
1
+ import { HelixActivateEvent, HelixMapClusterEvent, HelixMapPanEvent, HelixMapPartChangeEvent, HelixMapRotateEvent, HelixMapTiltEvent, HelixMapZoomEvent, isRtl, } from '@fusion.dev/helix-core';
2
+ import { creditParts } from '../credit/credit.js';
3
+ import { motionOf } from '../motion/motion.js';
4
+ import { pointsFrom } from '../points/points.js';
5
+ import { settledColor } from '../style/map-colors.js';
6
+ import { watchTheme } from '../theme/watch-theme.js';
7
+ const SAME_SPOT = 2;
8
+ const fanOut = (count, spacing) => {
9
+ if (count <= 8) {
10
+ const radius = Math.max(spacing, (count * spacing) / (2 * Math.PI));
11
+ return Array.from({ length: count }, (_, at) => {
12
+ const angle = -Math.PI / 2 + (at * 2 * Math.PI) / count;
13
+ return { x: radius * Math.cos(angle), y: radius * Math.sin(angle) };
14
+ });
15
+ }
16
+ let angle = 0;
17
+ let length = spacing;
18
+ return Array.from({ length: count }, () => {
19
+ angle += spacing / length;
20
+ length = spacing + (spacing / (2 * Math.PI)) * angle;
21
+ return {
22
+ x: length * Math.cos(angle - Math.PI / 2),
23
+ y: length * Math.sin(angle - Math.PI / 2),
24
+ };
25
+ });
26
+ };
27
+ const sameNames = (had, names) => (had?.length ?? 0) === names.length && names.every((name, at) => had?.[at] === name);
28
+ const isMarker = (node) => node?.isMapMarker === true;
29
+ const isPosition = (node) => node?.isMapPosition === true;
30
+ const METERS_PER_DEGREE = 111_320;
31
+ const AHEAD = 10;
32
+ export const distancesAlong = (points) => {
33
+ let total = 0;
34
+ return points.map((point, at) => {
35
+ const from = points[at - 1];
36
+ if (from) {
37
+ total +=
38
+ Math.hypot(point.lat - from.lat, (point.lng - from.lng) * Math.cos((from.lat * Math.PI) / 180)) * METERS_PER_DEGREE;
39
+ }
40
+ return total;
41
+ });
42
+ };
43
+ const cut = (points, fraction) => {
44
+ const along = distancesAlong(points);
45
+ const legs = along.slice(1).map((distance, at) => distance - along[at]);
46
+ let left = (along.at(-1) ?? 0) * fraction;
47
+ for (let at = 0; at < legs.length; at++) {
48
+ const leg = legs[at];
49
+ if (left <= leg) {
50
+ const [from, to] = [points[at], points[at + 1]];
51
+ const part = leg > 0 ? left / leg : 0;
52
+ const middle = {
53
+ lat: from.lat + (to.lat - from.lat) * part,
54
+ lng: from.lng + (to.lng - from.lng) * part,
55
+ };
56
+ return [
57
+ [...points.slice(0, at + 1), middle],
58
+ [middle, ...points.slice(at + 1)],
59
+ ];
60
+ }
61
+ left -= leg;
62
+ }
63
+ return [points, []];
64
+ };
65
+ const WALKED = 100;
66
+ const sameWalk = (had, walk) => (had?.length ?? 0) === walk.length &&
67
+ walk.every((one, at) => {
68
+ const was = had?.[at];
69
+ return (was?.key === one.key &&
70
+ Math.round(was.x) === Math.round(one.x) &&
71
+ Math.round(was.y) === Math.round(one.y) &&
72
+ was.label === one.label &&
73
+ was.count === one.count);
74
+ });
75
+ const isLayer = (node) => node?.isMapLayer === true;
76
+ const ids = new WeakMap();
77
+ let minted = 0;
78
+ const idOf = (element) => {
79
+ let id = ids.get(element);
80
+ if (!id) {
81
+ minted += 1;
82
+ id = `helix-layer-${minted}`;
83
+ ids.set(element, id);
84
+ }
85
+ return id;
86
+ };
87
+ const CREDIT = 'data-helix-map-credit';
88
+ const viewportOf = (frame) => {
89
+ const box = (frame.shadowRoot?.querySelector('[part="viewport"]') ?? frame).getBoundingClientRect();
90
+ return { height: box.height, width: box.width };
91
+ };
92
+ export const connectMap = (frame, driver, options = {}) => {
93
+ const handlePan = (event) => {
94
+ const { by, to } = event.detail;
95
+ const { height, width } = viewportOf(frame);
96
+ const inline = (to === 'end' ? 1 : to === 'start' ? -1 : 0) * (isRtl(frame) ? -1 : 1);
97
+ const block = to === 'down' ? 1 : to === 'up' ? -1 : 0;
98
+ driver.panBy(inline * width * by, block * height * by, motionOf(frame, 'step'));
99
+ };
100
+ const handleZoom = (event) => {
101
+ const { by } = event.detail;
102
+ if (by !== 0) {
103
+ driver.zoomBy(by > 0 ? 1 : -1, motionOf(frame, 'step'));
104
+ }
105
+ };
106
+ frame.addEventListener(HelixMapPanEvent.eventName, handlePan);
107
+ frame.addEventListener(HelixMapZoomEvent.eventName, handleZoom);
108
+ const handleRotate = (event) => {
109
+ const { by, to } = event.detail;
110
+ driver.rotate?.({ by, to }, motionOf(frame, 'step'));
111
+ };
112
+ const handleTilt = (event) => {
113
+ const { by, to } = event.detail;
114
+ driver.tilt?.({ by, to }, motionOf(frame, 'step'));
115
+ };
116
+ frame.addEventListener(HelixMapRotateEvent.eventName, handleRotate);
117
+ frame.addEventListener(HelixMapTiltEvent.eventName, handleTilt);
118
+ const holder = frame;
119
+ const syncBearing = () => {
120
+ const bearing = Math.round((driver.bearing?.() ?? 0) * 10) / 10;
121
+ if ('bearing' in holder && holder.bearing !== bearing) {
122
+ holder.bearing = bearing;
123
+ }
124
+ const pitch = Math.round((driver.pitch?.() ?? 0) * 10) / 10;
125
+ if ('pitch' in holder && holder.pitch !== pitch) {
126
+ holder.pitch = pitch;
127
+ }
128
+ };
129
+ syncBearing();
130
+ const stopBearing = driver.bearing || driver.pitch ? driver.onView?.(syncBearing) : undefined;
131
+ let bare = false;
132
+ const syncType = () => {
133
+ const next = frame.mapType === 'satellite';
134
+ if (next !== bare) {
135
+ bare = next;
136
+ driver.labels?.(!next);
137
+ }
138
+ };
139
+ syncType();
140
+ let stale = true;
141
+ const stopWatchingPreviews = driver.previews
142
+ ? driver.onView?.(() => {
143
+ stale = true;
144
+ })
145
+ : undefined;
146
+ let picturing;
147
+ const drawPreviews = () => {
148
+ const imagery = [...frame.children]
149
+ .filter(isLayer)
150
+ .find((layer) => layer.layerKind === 'raster' && layer.ground === true);
151
+ const src = imagery?.src ?? imagery?.frames?.at(-1);
152
+ if (!driver.previews || !src || !stale || picturing) {
153
+ return;
154
+ }
155
+ stale = false;
156
+ picturing = driver
157
+ .previews({
158
+ imagery: { maxZoom: imagery?.maxZoom, src },
159
+ types: ['standard', 'hybrid', 'satellite'],
160
+ })
161
+ .then((previews) => {
162
+ ;
163
+ frame.mapPreviews = previews;
164
+ })
165
+ .catch(() => {
166
+ stale = true;
167
+ })
168
+ .finally(() => {
169
+ picturing = undefined;
170
+ });
171
+ };
172
+ const restoreKeyboard = options.keyboard === 'renderer' ? undefined : driver.silenceKeyboard?.();
173
+ const restoreZoom = frame.shadowRoot?.querySelector('[part~="zoom-in"]')
174
+ ? driver.replaceZoom?.()
175
+ : undefined;
176
+ const focusable = options.keyboard === 'renderer' ? null : (driver.focusTarget?.() ?? null);
177
+ const tabindex = focusable?.getAttribute('tabindex') ?? null;
178
+ focusable?.setAttribute('tabindex', '-1');
179
+ const owned = [...frame.children].some((child) => child.getAttribute('slot') === 'attribution' && !child.hasAttribute(CREDIT));
180
+ let credit;
181
+ let listening;
182
+ let written;
183
+ const writeCredit = () => {
184
+ if (!listening || !credit) {
185
+ return;
186
+ }
187
+ const parts = creditParts(listening.read());
188
+ const said = JSON.stringify(parts);
189
+ if (said === written) {
190
+ return;
191
+ }
192
+ written = said;
193
+ const nodes = parts.map((part) => {
194
+ if (part.href === undefined) {
195
+ return document.createTextNode(part.text);
196
+ }
197
+ const link = document.createElement('a');
198
+ link.href = part.href;
199
+ link.rel = 'noopener noreferrer';
200
+ link.target = '_blank';
201
+ link.dataset.link = 'quiet';
202
+ link.textContent = part.text;
203
+ return link;
204
+ });
205
+ credit.replaceChildren(...nodes);
206
+ if (nodes.length === 0) {
207
+ credit.remove();
208
+ }
209
+ else if (credit.parentNode !== frame) {
210
+ frame.append(credit);
211
+ }
212
+ };
213
+ if (options.credit !== false && !owned && driver.credits) {
214
+ credit = document.createElement('span');
215
+ credit.setAttribute('slot', 'attribution');
216
+ credit.setAttribute(CREDIT, '');
217
+ listening = driver.credits(writeCredit);
218
+ writeCredit();
219
+ }
220
+ const markers = () => [...frame.children].filter(isMarker);
221
+ let clusters = new Map();
222
+ let fanned;
223
+ const scaleOf = (place) => {
224
+ if (!driver.project) {
225
+ return 0;
226
+ }
227
+ const at = driver.project(place);
228
+ const north = driver.project({ lat: place.lat + 10 / METERS_PER_DEGREE, lng: place.lng });
229
+ return Math.hypot(north.x - at.x, north.y - at.y);
230
+ };
231
+ const positions = () => [...frame.children].filter(isPosition);
232
+ const fetched = new WeakMap();
233
+ const read = new WeakMap();
234
+ const dataOf = (layer) => {
235
+ if (layer.data !== undefined || !layer.src) {
236
+ return layer.data;
237
+ }
238
+ const had = fetched.get(layer);
239
+ if (had?.src === layer.src) {
240
+ return had.data;
241
+ }
242
+ const asked = { src: layer.src };
243
+ fetched.set(layer, asked);
244
+ fetch(layer.src)
245
+ .then((response) => response.ok
246
+ ? response.json()
247
+ : Promise.reject(new Error(`${asked.src}: ${response.status}`)))
248
+ .then((data) => {
249
+ if (fetched.get(layer) === asked && live) {
250
+ asked.data = data;
251
+ soon({ draw: true });
252
+ }
253
+ })
254
+ .catch((error) => console.error(error));
255
+ return undefined;
256
+ };
257
+ const pointsOf = (layer) => {
258
+ const data = dataOf(layer);
259
+ const nameKey = layer.nameKey ?? 'name';
260
+ const had = read.get(layer);
261
+ if (had && had.data === data && had.nameKey === nameKey) {
262
+ return had.points;
263
+ }
264
+ const points = pointsFrom(data, nameKey);
265
+ read.set(layer, { data, nameKey, points });
266
+ return points;
267
+ };
268
+ const pointLayers = new Map();
269
+ let seen = new Map();
270
+ const look = () => {
271
+ if (!live || !drawn?.inView || !driver.project) {
272
+ return;
273
+ }
274
+ const project = driver.project;
275
+ const rtl = isRtl(frame);
276
+ const next = new Map();
277
+ for (const [id, layer] of pointLayers) {
278
+ if (layer.parentElement !== frame) {
279
+ continue;
280
+ }
281
+ const { names, values } = pointsOf(layer);
282
+ const found = drawn.inView(id).map((one) => ({ at: project(one.place), one }));
283
+ found.sort((a, b) => a.at.y - b.at.y || (rtl ? b.at.x - a.at.x : a.at.x - b.at.x));
284
+ next.set(layer, found.slice(0, WALKED).map(({ one }) => one.cluster !== undefined
285
+ ? { count: one.count ?? 0, key: `c:${one.cluster}`, place: one.place }
286
+ : {
287
+ key: `p:${one.at}`,
288
+ label: names[one.at ?? -1] || undefined,
289
+ place: one.place,
290
+ value: values[one.at ?? -1],
291
+ }));
292
+ }
293
+ for (const layer of seen.keys()) {
294
+ if (!next.has(layer) && (layer.walk?.length ?? 0) > 0) {
295
+ layer.walk = [];
296
+ }
297
+ }
298
+ seen = next;
299
+ place();
300
+ };
301
+ const readLayers = () => [...frame.children]
302
+ .filter(isLayer)
303
+ .filter((layer) => !layer.hidden)
304
+ .flatMap((layer) => {
305
+ const color = settledColor(layer);
306
+ if (layer.layerKind === 'contours') {
307
+ if (!layer.src) {
308
+ return [];
309
+ }
310
+ return [
311
+ {
312
+ ...(layer.attribution ? { attribution: layer.attribution } : {}),
313
+ color,
314
+ encoding: layer.encoding === 'mapbox' ? 'mapbox' : 'terrarium',
315
+ id: idOf(layer),
316
+ kind: 'contours',
317
+ label: layer.label ?? '',
318
+ ...(typeof layer.maxZoom === 'number' ? { maxZoom: layer.maxZoom } : {}),
319
+ points: [],
320
+ src: layer.src,
321
+ unit: layer.unit === 'feet' ? 'feet' : 'meters',
322
+ },
323
+ ];
324
+ }
325
+ if (layer.layerKind === 'landcover') {
326
+ return [
327
+ { color, id: idOf(layer), kind: 'landcover', label: layer.label ?? '', points: [] },
328
+ ];
329
+ }
330
+ if (layer.layerKind === 'terrain') {
331
+ if (!layer.src) {
332
+ return [];
333
+ }
334
+ return [
335
+ {
336
+ ...(layer.attribution ? { attribution: layer.attribution } : {}),
337
+ color,
338
+ encoding: layer.encoding === 'mapbox' ? 'mapbox' : 'terrarium',
339
+ exaggeration: Math.max(0, layer.exaggeration ?? 1),
340
+ highlight: settledColor(layer, 'border-top-color'),
341
+ id: idOf(layer),
342
+ kind: 'terrain',
343
+ label: layer.label ?? '',
344
+ ...(typeof layer.maxZoom === 'number' ? { maxZoom: layer.maxZoom } : {}),
345
+ points: [],
346
+ raised: layer.raised === true,
347
+ src: layer.src,
348
+ },
349
+ ];
350
+ }
351
+ if (layer.layerKind === 'buildings') {
352
+ return [
353
+ { color, id: idOf(layer), kind: 'buildings', label: layer.label ?? '', points: [] },
354
+ ];
355
+ }
356
+ if (layer.layerKind === 'raster') {
357
+ const frames = layer.frames?.length ? [...layer.frames] : layer.src ? [layer.src] : [];
358
+ if (frames.length === 0) {
359
+ return [];
360
+ }
361
+ const last = frames.length - 1;
362
+ return [
363
+ {
364
+ ...(layer.attribution ? { attribution: layer.attribution } : {}),
365
+ color,
366
+ frame: Math.min(Math.max(0, layer.frame ?? last), last),
367
+ frames,
368
+ ...(layer.ground ? { ground: true } : {}),
369
+ id: idOf(layer),
370
+ kind: 'raster',
371
+ label: layer.label ?? '',
372
+ ...(typeof layer.maxZoom === 'number' ? { maxZoom: layer.maxZoom } : {}),
373
+ opacity: Math.min(Math.max(0, layer.opacity ?? 0.7), 1),
374
+ points: [],
375
+ },
376
+ ];
377
+ }
378
+ if (layer.layerKind === 'points') {
379
+ const { points } = pointsOf(layer);
380
+ if (points.length === 0) {
381
+ return [];
382
+ }
383
+ const id = idOf(layer);
384
+ pointLayers.set(id, layer);
385
+ return [
386
+ {
387
+ casing: settledColor(layer, 'border-top-color'),
388
+ cluster: layer.cluster === true,
389
+ color,
390
+ id,
391
+ ink: settledColor(layer, 'outline-color'),
392
+ kind: 'points',
393
+ label: layer.label ?? '',
394
+ points,
395
+ },
396
+ ];
397
+ }
398
+ if ((layer.points?.length ?? 0) < (layer.layerKind === 'area' ? 3 : 2)) {
399
+ return [];
400
+ }
401
+ const edge = layer.layerKind === 'route' ? settledColor(layer, 'border-top-color') : color;
402
+ const dotted = layer.layerKind === 'route' && layer.variant === 'dotted';
403
+ const spec = {
404
+ ...(edge && edge !== color && !dotted ? { casing: edge } : {}),
405
+ color,
406
+ id: idOf(layer),
407
+ kind: layer.layerKind,
408
+ label: layer.label ?? '',
409
+ ...(dotted ? { pattern: 'dotted' } : {}),
410
+ points: (layer.points ?? []).map(({ lat, lng }) => ({ lat, lng })),
411
+ };
412
+ const progress = layer.layerKind === 'route' ? layer.progress : undefined;
413
+ if (typeof progress !== 'number' || progress <= 0) {
414
+ return [spec];
415
+ }
416
+ if (progress >= 1) {
417
+ return [{ ...spec, faded: true }];
418
+ }
419
+ const [behind, ahead] = cut(spec.points, progress);
420
+ return [
421
+ { ...spec, faded: true, id: `${spec.id}-behind`, points: behind },
422
+ { ...spec, points: ahead },
423
+ ];
424
+ });
425
+ const drawn = driver.layers?.(readLayers, {
426
+ motion: () => motionOf(frame, 'travel'),
427
+ press: (id, index) => {
428
+ const layer = pointLayers.get(id);
429
+ const value = layer ? pointsOf(layer).values[index] : undefined;
430
+ if (layer && value !== undefined) {
431
+ layer.dispatchEvent(new HelixActivateEvent({ value }));
432
+ }
433
+ },
434
+ settled: () => look(),
435
+ });
436
+ const place = () => {
437
+ if (!driver.project) {
438
+ return;
439
+ }
440
+ const project = driver.project;
441
+ const { width } = viewportOf(frame);
442
+ const rtl = isRtl(frame);
443
+ const bearing = driver.bearing?.() ?? 0;
444
+ const pitch = driver.pitch?.() ?? 0;
445
+ const everywhere = markers()
446
+ .filter((marker) => typeof marker.lat === 'number' && typeof marker.lng === 'number')
447
+ .map((marker) => {
448
+ const where = { lat: marker.lat, lng: marker.lng };
449
+ return {
450
+ at: project(where),
451
+ behind: driver.behind?.(where) === true,
452
+ marker,
453
+ reach: typeof marker.extent === 'function' ? marker.extent() : undefined,
454
+ };
455
+ });
456
+ for (const one of everywhere) {
457
+ if (one.marker.behind !== one.behind) {
458
+ one.marker.behind = one.behind;
459
+ }
460
+ }
461
+ const hidden = everywhere.filter((one) => one.behind);
462
+ for (const { marker } of hidden) {
463
+ if (marker.gatheredBy !== undefined) {
464
+ marker.gatheredBy = undefined;
465
+ }
466
+ if ((marker.gathered?.length ?? 0) > 0) {
467
+ marker.gathered = [];
468
+ }
469
+ }
470
+ const standing = everywhere.filter((one) => !one.behind);
471
+ const gathering = frame.cluster === true;
472
+ for (const marker of markers()) {
473
+ if (!standing.some((one) => one.marker === marker) && marker.gatheredBy !== undefined) {
474
+ marker.gatheredBy = undefined;
475
+ }
476
+ }
477
+ const reach = options.clusterDistance ?? 40;
478
+ if (fanned) {
479
+ const scale = scaleOf(fanned.place);
480
+ const members = fanned.members;
481
+ if (Math.abs(scale - fanned.scale) > fanned.scale * 0.02 ||
482
+ !members.every((member) => standing.some((one) => one.marker === member))) {
483
+ fanned = undefined;
484
+ }
485
+ }
486
+ const fan = new Set(fanned?.members);
487
+ const taken = new Set(fan);
488
+ clusters = new Map();
489
+ const placing = [];
490
+ if (fanned) {
491
+ const center = project(fanned.place);
492
+ const offsets = fanOut(fanned.members.length, reach);
493
+ fanned.members.forEach((member, at) => {
494
+ const offset = offsets[at];
495
+ if (member.gatheredBy !== undefined) {
496
+ member.gatheredBy = undefined;
497
+ }
498
+ if ((member.gathered?.length ?? 0) > 0) {
499
+ member.gathered = [];
500
+ }
501
+ if (member.fanned !== true) {
502
+ member.fanned = true;
503
+ }
504
+ member.style.setProperty('--_map-marker-leg', `${Math.hypot(offset.x, offset.y)}px`);
505
+ member.style.setProperty('--_map-marker-leg-angle', `${Math.atan2(-offset.y, -offset.x)}rad`);
506
+ placing.push({
507
+ at: { x: center.x + offset.x, y: center.y + offset.y },
508
+ marker: member,
509
+ reach: standing.find((one) => one.marker === member)?.reach,
510
+ });
511
+ });
512
+ }
513
+ for (const marker of markers()) {
514
+ if (!fan.has(marker) && marker.fanned === true) {
515
+ marker.fanned = false;
516
+ marker.style.removeProperty('--_map-marker-leg');
517
+ marker.style.removeProperty('--_map-marker-leg-angle');
518
+ }
519
+ }
520
+ for (const one of standing) {
521
+ if (taken.has(one.marker)) {
522
+ continue;
523
+ }
524
+ const group = gathering
525
+ ? standing.filter((other) => !taken.has(other.marker) &&
526
+ Math.hypot(other.at.x - one.at.x, other.at.y - one.at.y) <= reach)
527
+ : [one];
528
+ for (const member of group) {
529
+ taken.add(member.marker);
530
+ }
531
+ const [lead, ...rest] = group.map((member) => member.marker);
532
+ const names = rest.length > 0 ? [lead, ...rest].map((one) => one.label || one.name || '') : [];
533
+ if (!sameNames(lead.gathered, names)) {
534
+ lead.gathered = names;
535
+ }
536
+ if (lead.gatheredBy !== undefined) {
537
+ lead.gatheredBy = undefined;
538
+ }
539
+ for (const member of rest) {
540
+ if (member.gatheredBy !== lead) {
541
+ member.gatheredBy = lead;
542
+ }
543
+ if ((member.gathered?.length ?? 0) > 0) {
544
+ member.gathered = [];
545
+ }
546
+ }
547
+ if (rest.length > 0) {
548
+ clusters.set(lead, [lead, ...rest]);
549
+ }
550
+ placing.push({
551
+ at: {
552
+ x: group.reduce((sum, member) => sum + member.at.x, 0) / group.length,
553
+ y: group.reduce((sum, member) => sum + member.at.y, 0) / group.length,
554
+ },
555
+ marker: lead,
556
+ reach: one.reach,
557
+ });
558
+ }
559
+ placing.push(...hidden.map(({ at, marker, reach }) => ({ at, marker, reach })));
560
+ for (const { at, marker, reach } of placing) {
561
+ marker.style.setProperty('--_map-marker-x', `${at.x}px`);
562
+ marker.style.setProperty('--_map-marker-y', `${at.y}px`);
563
+ if (reach !== undefined) {
564
+ const ahead = rtl ? at.x : width - at.x;
565
+ const behind = rtl ? width - at.x : at.x;
566
+ marker.side = reach <= ahead || ahead >= behind ? 'end' : 'start';
567
+ }
568
+ }
569
+ const across = ((bearing + 90) * Math.PI) / 180;
570
+ const tilted = Math.max(Math.cos((pitch * Math.PI) / 180), 0.05);
571
+ const headingOf = (position, at) => {
572
+ const heading = position.heading ?? 0;
573
+ const toward = (heading * Math.PI) / 180;
574
+ const ahead = project({
575
+ lat: position.lat + (AHEAD * Math.cos(toward)) / METERS_PER_DEGREE,
576
+ lng: position.lng +
577
+ (AHEAD * Math.sin(toward)) /
578
+ (METERS_PER_DEGREE * Math.cos((position.lat * Math.PI) / 180)),
579
+ });
580
+ const [dx, dy] = [ahead.x - at.x, ahead.y - at.y];
581
+ if (Math.hypot(dx, dy) < 1e-6) {
582
+ return heading - bearing;
583
+ }
584
+ return Math.round(((Math.atan2(dx, -dy / tilted) * 180) / Math.PI) * 100) / 100;
585
+ };
586
+ for (const position of positions()) {
587
+ if (typeof position.lat !== 'number' || typeof position.lng !== 'number') {
588
+ continue;
589
+ }
590
+ if (driver.behind?.({ lat: position.lat, lng: position.lng })) {
591
+ for (const name of ['heading', 'pitch', 'radius', 'x', 'y']) {
592
+ position.style.removeProperty(`--_map-position-${name}`);
593
+ }
594
+ continue;
595
+ }
596
+ const at = project({ lat: position.lat, lng: position.lng });
597
+ const meters = position.accuracy ?? 0;
598
+ const edge = meters > 0
599
+ ? project({
600
+ lat: position.lat + (meters * Math.cos(across)) / METERS_PER_DEGREE,
601
+ lng: position.lng +
602
+ (meters * Math.sin(across)) /
603
+ (METERS_PER_DEGREE * Math.cos((position.lat * Math.PI) / 180)),
604
+ })
605
+ : at;
606
+ position.style.setProperty('--_map-position-x', `${at.x}px`);
607
+ position.style.setProperty('--_map-position-y', `${at.y}px`);
608
+ position.style.setProperty('--_map-position-heading', `${headingOf({ heading: position.heading, lat: position.lat, lng: position.lng }, at)}deg`);
609
+ position.style.setProperty('--_map-position-radius', `${Math.hypot(edge.x - at.x, edge.y - at.y)}px`);
610
+ position.style.setProperty('--_map-position-pitch', `${pitch}deg`);
611
+ }
612
+ for (const [layer, found] of seen) {
613
+ const walk = found.map(({ place, ...one }) => ({ ...one, ...project(place) }));
614
+ if (!sameWalk(layer.walk, walk)) {
615
+ layer.walk = walk;
616
+ }
617
+ }
618
+ };
619
+ const stopViewing = driver.onView?.(place);
620
+ const away = (snapshot) => {
621
+ snapshot.hidden = true;
622
+ snapshot.style.display = 'none';
623
+ };
624
+ const snapshots = [...frame.querySelectorAll(':scope > [data-helix-map-snapshot]')];
625
+ const stopWaiting = snapshots.length > 0
626
+ ? driver.ready?.(() => {
627
+ const motion = motionOf(frame, 'step');
628
+ for (const snapshot of snapshots) {
629
+ if (!motion || typeof snapshot.animate !== 'function') {
630
+ away(snapshot);
631
+ continue;
632
+ }
633
+ snapshot
634
+ .animate([{ opacity: 1 }, { opacity: 0 }], {
635
+ duration: motion.duration,
636
+ easing: `linear(${Array.from({ length: 13 }, (_, step) => motion.easing(step / 12).toFixed(3)).join(', ')})`,
637
+ })
638
+ .finished.then(() => away(snapshot), () => away(snapshot));
639
+ }
640
+ })
641
+ : undefined;
642
+ let live = true;
643
+ let placing = false;
644
+ let drawing = false;
645
+ let queued = false;
646
+ const flush = () => {
647
+ queued = false;
648
+ if (!live) {
649
+ return;
650
+ }
651
+ if (placing) {
652
+ placing = false;
653
+ place();
654
+ }
655
+ if (drawing) {
656
+ drawing = false;
657
+ drawn?.update();
658
+ }
659
+ };
660
+ const soon = (what) => {
661
+ placing ||= what.place === true;
662
+ drawing ||= what.draw === true;
663
+ if (!queued) {
664
+ queued = true;
665
+ queueMicrotask(flush);
666
+ }
667
+ };
668
+ const handlePartChange = (event) => {
669
+ const { kind } = event.detail;
670
+ if (kind === 'menu') {
671
+ drawPreviews();
672
+ return;
673
+ }
674
+ if (kind === 'layer') {
675
+ syncType();
676
+ }
677
+ if (kind !== 'cluster') {
678
+ soon(kind === 'layer' ? { draw: true } : { place: true });
679
+ }
680
+ };
681
+ frame.addEventListener(HelixMapPartChangeEvent.eventName, handlePartChange);
682
+ const styled = new MutationObserver(() => soon({ draw: true }));
683
+ const watchLayers = () => {
684
+ styled.disconnect();
685
+ for (const layer of [...frame.children].filter(isLayer)) {
686
+ styled.observe(layer, {
687
+ attributeFilter: ['class', 'hidden', 'style', 'tone', 'variant'],
688
+ attributes: true,
689
+ });
690
+ }
691
+ };
692
+ watchLayers();
693
+ const children = new MutationObserver(() => {
694
+ watchLayers();
695
+ soon({ draw: true, place: true });
696
+ });
697
+ children.observe(frame, { childList: true });
698
+ const stopTheming = drawn ? watchTheme(frame, () => drawn.update()) : undefined;
699
+ place();
700
+ const reveal = (event) => {
701
+ const marker = event.composedPath().find(isMarker);
702
+ if (!marker || !driver.project || !driver.show) {
703
+ return;
704
+ }
705
+ if (typeof marker.lat !== 'number' || typeof marker.lng !== 'number') {
706
+ return;
707
+ }
708
+ const where = { lat: marker.lat, lng: marker.lng };
709
+ const at = driver.project(where);
710
+ const { height, width } = viewportOf(frame);
711
+ if (driver.behind?.(where) || at.x < 0 || at.y < 0 || at.x > width || at.y > height) {
712
+ driver.show({ lat: marker.lat, lng: marker.lng }, motionOf(frame, 'travel'));
713
+ }
714
+ };
715
+ frame.addEventListener('focusin', reveal);
716
+ const handleCluster = (event) => {
717
+ const layer = event.target;
718
+ const key = event.detail.key;
719
+ if (isLayer(layer) && layer.layerKind === 'points' && key?.startsWith('c:')) {
720
+ drawn?.expand?.(idOf(layer), Number(key.slice(2)));
721
+ return;
722
+ }
723
+ const members = clusters.get(event.target);
724
+ if (!members || options.clusterZoom === false) {
725
+ return;
726
+ }
727
+ const lats = members.map((one) => one.lat);
728
+ const lngs = members.map((one) => one.lng);
729
+ const middle = {
730
+ lat: lats.reduce((sum, lat) => sum + lat, 0) / lats.length,
731
+ lng: lngs.reduce((sum, lng) => sum + lng, 0) / lngs.length,
732
+ };
733
+ const spread = Math.max(...members.map((one) => Math.hypot(one.lat - middle.lat, (one.lng - middle.lng) * Math.cos((middle.lat * Math.PI) / 180)) * METERS_PER_DEGREE));
734
+ if (spread <= SAME_SPOT || !driver.fit) {
735
+ fanned = { members, place: middle, scale: scaleOf(middle) };
736
+ place();
737
+ return;
738
+ }
739
+ driver.fit({
740
+ northEast: { lat: Math.max(...lats), lng: Math.max(...lngs) },
741
+ southWest: { lat: Math.min(...lats), lng: Math.min(...lngs) },
742
+ }, 96, motionOf(frame, 'travel'));
743
+ };
744
+ frame.addEventListener(HelixMapClusterEvent.eventName, handleCluster);
745
+ const closeFan = () => {
746
+ const lead = fanned?.members[0];
747
+ fanned = undefined;
748
+ place();
749
+ return lead;
750
+ };
751
+ const handleFanKey = (event) => {
752
+ if (fanned && event.key === 'Escape') {
753
+ event.preventDefault();
754
+ closeFan()?.focus();
755
+ }
756
+ };
757
+ const handleFanPress = (event) => {
758
+ if (fanned && !event.composedPath().some(isMarker)) {
759
+ closeFan();
760
+ }
761
+ };
762
+ frame.addEventListener('keydown', handleFanKey);
763
+ frame.addEventListener('click', handleFanPress);
764
+ return {
765
+ disconnect() {
766
+ live = false;
767
+ drawn?.stop();
768
+ stopViewing?.();
769
+ stopWaiting?.();
770
+ stopTheming?.();
771
+ styled.disconnect();
772
+ children.disconnect();
773
+ frame.removeEventListener(HelixMapPartChangeEvent.eventName, handlePartChange);
774
+ frame.removeEventListener('focusin', reveal);
775
+ frame.removeEventListener(HelixMapClusterEvent.eventName, handleCluster);
776
+ frame.removeEventListener('keydown', handleFanKey);
777
+ frame.removeEventListener('click', handleFanPress);
778
+ for (const marker of markers()) {
779
+ marker.style.removeProperty('--_map-marker-x');
780
+ marker.style.removeProperty('--_map-marker-y');
781
+ if (marker.gatheredBy !== undefined) {
782
+ marker.gatheredBy = undefined;
783
+ }
784
+ if ((marker.gathered?.length ?? 0) > 0) {
785
+ marker.gathered = [];
786
+ }
787
+ if (marker.fanned === true) {
788
+ marker.fanned = false;
789
+ }
790
+ marker.style.removeProperty('--_map-marker-leg');
791
+ marker.style.removeProperty('--_map-marker-leg-angle');
792
+ }
793
+ for (const layer of seen.keys()) {
794
+ layer.walk = [];
795
+ }
796
+ for (const position of positions()) {
797
+ for (const name of ['heading', 'pitch', 'radius', 'x', 'y']) {
798
+ position.style.removeProperty(`--_map-position-${name}`);
799
+ }
800
+ }
801
+ frame.removeEventListener(HelixMapPanEvent.eventName, handlePan);
802
+ frame.removeEventListener(HelixMapZoomEvent.eventName, handleZoom);
803
+ frame.removeEventListener(HelixMapRotateEvent.eventName, handleRotate);
804
+ frame.removeEventListener(HelixMapTiltEvent.eventName, handleTilt);
805
+ stopBearing?.();
806
+ stopWatchingPreviews?.();
807
+ if (bare) {
808
+ driver.labels?.(true);
809
+ }
810
+ restoreKeyboard?.();
811
+ restoreZoom?.();
812
+ if (focusable) {
813
+ if (tabindex === null) {
814
+ focusable.removeAttribute('tabindex');
815
+ }
816
+ else {
817
+ focusable.setAttribute('tabindex', tabindex);
818
+ }
819
+ }
820
+ listening?.stop();
821
+ credit?.remove();
822
+ },
823
+ fit(options = {}) {
824
+ if (!driver.fit) {
825
+ return;
826
+ }
827
+ const places = [
828
+ ...[...markers(), ...positions()]
829
+ .filter((one) => typeof one.lat === 'number' && typeof one.lng === 'number')
830
+ .map((one) => ({ lat: one.lat, lng: one.lng })),
831
+ ...[...frame.children]
832
+ .filter(isLayer)
833
+ .flatMap((layer) => layer.layerKind === 'points' ? pointsOf(layer).points : (layer.points ?? [])),
834
+ ];
835
+ if (places.length === 0) {
836
+ return;
837
+ }
838
+ const lats = places.map((place) => place.lat);
839
+ const lngs = places.map((place) => place.lng);
840
+ driver.fit({
841
+ northEast: { lat: Math.max(...lats), lng: Math.max(...lngs) },
842
+ southWest: { lat: Math.min(...lats), lng: Math.min(...lngs) },
843
+ }, options.padding ?? 48, options.animate === false ? null : motionOf(frame, 'travel'));
844
+ },
845
+ };
846
+ };
847
+ //# sourceMappingURL=connect-map.js.map