@fusion.dev/helix-map 0.110.2 → 0.110.3
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.
- package/dist/connect-map/connect-map.d.ts +17 -0
- package/dist/connect-map/connect-map.js +111 -32
- package/dist/leaflet/leaflet-driver.d.ts +2 -2
- package/dist/leaflet/leaflet-driver.js +51 -1
- package/dist/maplibre/maplibre-driver.d.ts +6 -4
- package/dist/maplibre/maplibre-driver.js +60 -14
- package/dist/style/map-style.js +21 -11
- package/package.json +5 -5
|
@@ -248,6 +248,23 @@ export interface MapLayerSpec {
|
|
|
248
248
|
*/
|
|
249
249
|
points: MapPlace[];
|
|
250
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* The stretch of the world some places cover, and their middle.
|
|
253
|
+
*
|
|
254
|
+
* **One pass rather than `Math.max(...lats)`**, which spreads every place into
|
|
255
|
+
* an argument list and throws past about 65,000 of them in Safari: a points
|
|
256
|
+
* layer of that size made `fit()` throw.
|
|
257
|
+
*
|
|
258
|
+
* **Measured the short way round.** Longitudes are taken within 180 degrees of
|
|
259
|
+
* the first place, so two places either side of the antimeridian are a degree
|
|
260
|
+
* apart and not the whole world: framed naively, 179.5 and -179.5 zoomed out
|
|
261
|
+
* to the globe, and their middle landed at 0. The bounds may run past 180 as
|
|
262
|
+
* a result, which both renderers draw.
|
|
263
|
+
*/
|
|
264
|
+
export declare const reachOf: (places: readonly MapPlace[]) => {
|
|
265
|
+
bounds: MapBounds;
|
|
266
|
+
middle: MapPlace;
|
|
267
|
+
};
|
|
251
268
|
/**
|
|
252
269
|
* How far along a line each of its points is, in meters from the first:
|
|
253
270
|
* degrees with longitude narrowed by the latitude, which is a flat map's idea
|
|
@@ -28,6 +28,37 @@ const sameNames = (had, names) => (had?.length ?? 0) === names.length && names.e
|
|
|
28
28
|
const isMarker = (node) => node?.isMapMarker === true;
|
|
29
29
|
const isPosition = (node) => node?.isMapPosition === true;
|
|
30
30
|
const METERS_PER_DEGREE = 111_320;
|
|
31
|
+
const LEAST_REACH = 0.005;
|
|
32
|
+
export const reachOf = (places) => {
|
|
33
|
+
const first = places[0]?.lng ?? 0;
|
|
34
|
+
let north = -90;
|
|
35
|
+
let south = 90;
|
|
36
|
+
let east = -Infinity;
|
|
37
|
+
let west = Infinity;
|
|
38
|
+
let lats = 0;
|
|
39
|
+
let lngs = 0;
|
|
40
|
+
for (const place of places) {
|
|
41
|
+
const lng = place.lng + 360 * Math.round((first - place.lng) / 360);
|
|
42
|
+
north = Math.max(north, place.lat);
|
|
43
|
+
south = Math.min(south, place.lat);
|
|
44
|
+
east = Math.max(east, lng);
|
|
45
|
+
west = Math.min(west, lng);
|
|
46
|
+
lats += place.lat;
|
|
47
|
+
lngs += lng;
|
|
48
|
+
}
|
|
49
|
+
const count = Math.max(places.length, 1);
|
|
50
|
+
const across = ((lngs / count + 540) % 360) - 180;
|
|
51
|
+
if (north - south < LEAST_REACH && east - west < LEAST_REACH) {
|
|
52
|
+
north += LEAST_REACH;
|
|
53
|
+
south -= LEAST_REACH;
|
|
54
|
+
east += LEAST_REACH;
|
|
55
|
+
west -= LEAST_REACH;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
bounds: { northEast: { lat: north, lng: east }, southWest: { lat: south, lng: west } },
|
|
59
|
+
middle: { lat: lats / count, lng: across },
|
|
60
|
+
};
|
|
61
|
+
};
|
|
31
62
|
const AHEAD = 10;
|
|
32
63
|
export const distancesAlong = (points) => {
|
|
33
64
|
let total = 0;
|
|
@@ -63,6 +94,9 @@ const cut = (points, fraction) => {
|
|
|
63
94
|
return [points, []];
|
|
64
95
|
};
|
|
65
96
|
const WALKED = 100;
|
|
97
|
+
const WALK_LINE = 24;
|
|
98
|
+
const WALK_SETTLES = 250;
|
|
99
|
+
const DRAGGED = 4;
|
|
66
100
|
const sameWalk = (had, walk) => (had?.length ?? 0) === walk.length &&
|
|
67
101
|
walk.every((one, at) => {
|
|
68
102
|
const was = had?.[at];
|
|
@@ -70,6 +104,7 @@ const sameWalk = (had, walk) => (had?.length ?? 0) === walk.length &&
|
|
|
70
104
|
Math.round(was.x) === Math.round(one.x) &&
|
|
71
105
|
Math.round(was.y) === Math.round(one.y) &&
|
|
72
106
|
was.label === one.label &&
|
|
107
|
+
was.value === one.value &&
|
|
73
108
|
was.count === one.count);
|
|
74
109
|
});
|
|
75
110
|
const isLayer = (node) => node?.isMapLayer === true;
|
|
@@ -169,8 +204,10 @@ export const connectMap = (frame, driver, options = {}) => {
|
|
|
169
204
|
types: ['standard', 'hybrid', 'satellite'],
|
|
170
205
|
})
|
|
171
206
|
.then((previews) => {
|
|
172
|
-
|
|
173
|
-
|
|
207
|
+
if (live) {
|
|
208
|
+
;
|
|
209
|
+
frame.mapPreviews = previews;
|
|
210
|
+
}
|
|
174
211
|
})
|
|
175
212
|
.catch(() => {
|
|
176
213
|
stale = true;
|
|
@@ -180,9 +217,18 @@ export const connectMap = (frame, driver, options = {}) => {
|
|
|
180
217
|
});
|
|
181
218
|
};
|
|
182
219
|
const restoreKeyboard = options.keyboard === 'renderer' ? undefined : driver.silenceKeyboard?.();
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
220
|
+
let restoreZoom;
|
|
221
|
+
const replaceZoom = () => {
|
|
222
|
+
if (!restoreZoom && frame.shadowRoot?.querySelector('[part~="zoom-in"]')) {
|
|
223
|
+
restoreZoom = driver.replaceZoom?.();
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
replaceZoom();
|
|
227
|
+
void frame.updateComplete?.then(() => {
|
|
228
|
+
if (live) {
|
|
229
|
+
replaceZoom();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
186
232
|
const focusable = options.keyboard === 'renderer' ? null : (driver.focusTarget?.() ?? null);
|
|
187
233
|
const tabindex = focusable?.getAttribute('tabindex') ?? null;
|
|
188
234
|
focusable?.setAttribute('tabindex', '-1');
|
|
@@ -190,6 +236,19 @@ export const connectMap = (frame, driver, options = {}) => {
|
|
|
190
236
|
let credit;
|
|
191
237
|
let listening;
|
|
192
238
|
let written;
|
|
239
|
+
let crediting = false;
|
|
240
|
+
const creditSoon = () => {
|
|
241
|
+
if (crediting) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
crediting = true;
|
|
245
|
+
queueMicrotask(() => {
|
|
246
|
+
crediting = false;
|
|
247
|
+
if (live) {
|
|
248
|
+
writeCredit();
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
};
|
|
193
252
|
const writeCredit = () => {
|
|
194
253
|
if (!listening || !credit) {
|
|
195
254
|
return;
|
|
@@ -224,7 +283,7 @@ export const connectMap = (frame, driver, options = {}) => {
|
|
|
224
283
|
credit = document.createElement('span');
|
|
225
284
|
credit.setAttribute('slot', 'attribution');
|
|
226
285
|
credit.setAttribute(CREDIT, '');
|
|
227
|
-
listening = driver.credits(
|
|
286
|
+
listening = driver.credits(creditSoon);
|
|
228
287
|
writeCredit();
|
|
229
288
|
}
|
|
230
289
|
const markers = () => [...frame.children].filter(isMarker);
|
|
@@ -290,7 +349,8 @@ export const connectMap = (frame, driver, options = {}) => {
|
|
|
290
349
|
}
|
|
291
350
|
const { names, values } = pointsOf(layer);
|
|
292
351
|
const found = drawn.inView(id).map((one) => ({ at: project(one.place), one }));
|
|
293
|
-
found.sort((a, b) => a.at.y -
|
|
352
|
+
found.sort((a, b) => Math.floor(a.at.y / WALK_LINE) - Math.floor(b.at.y / WALK_LINE) ||
|
|
353
|
+
(rtl ? b.at.x - a.at.x : a.at.x - b.at.x));
|
|
294
354
|
next.set(layer, found.slice(0, WALKED).map(({ one }) => one.cluster !== undefined
|
|
295
355
|
? { count: one.count ?? 0, key: `c:${one.cluster}`, place: one.place }
|
|
296
356
|
: {
|
|
@@ -308,7 +368,11 @@ export const connectMap = (frame, driver, options = {}) => {
|
|
|
308
368
|
seen = next;
|
|
309
369
|
place();
|
|
310
370
|
};
|
|
311
|
-
const readLayers = () =>
|
|
371
|
+
const readLayers = () => {
|
|
372
|
+
pointLayers.clear();
|
|
373
|
+
return readShown();
|
|
374
|
+
};
|
|
375
|
+
const readShown = () => [...frame.children]
|
|
312
376
|
.filter(isLayer)
|
|
313
377
|
.filter((layer) => !layer.hidden)
|
|
314
378
|
.filter((layer) => !(layer.layerKind === 'raster' && layer.ground === true && !frame.mapType))
|
|
@@ -479,9 +543,10 @@ export const connectMap = (frame, driver, options = {}) => {
|
|
|
479
543
|
}
|
|
480
544
|
}
|
|
481
545
|
const standing = everywhere.filter((one) => !one.behind);
|
|
546
|
+
const stood = new Map(standing.map((one) => [one.marker, one]));
|
|
482
547
|
const gathering = frame.cluster === true;
|
|
483
548
|
for (const marker of markers()) {
|
|
484
|
-
if (!
|
|
549
|
+
if (!stood.has(marker) && marker.gatheredBy !== undefined) {
|
|
485
550
|
marker.gatheredBy = undefined;
|
|
486
551
|
}
|
|
487
552
|
}
|
|
@@ -489,8 +554,10 @@ export const connectMap = (frame, driver, options = {}) => {
|
|
|
489
554
|
if (fanned) {
|
|
490
555
|
const scale = scaleOf(fanned.place);
|
|
491
556
|
const members = fanned.members;
|
|
492
|
-
|
|
493
|
-
|
|
557
|
+
const zoomed = fanned.zoom !== undefined && driver.zoom
|
|
558
|
+
? Math.abs(driver.zoom() - fanned.zoom) > 0.01
|
|
559
|
+
: Math.abs(scale - fanned.scale) > fanned.scale * 0.02;
|
|
560
|
+
if (zoomed || !members.every((member) => stood.has(member))) {
|
|
494
561
|
fanned = undefined;
|
|
495
562
|
}
|
|
496
563
|
}
|
|
@@ -517,7 +584,7 @@ export const connectMap = (frame, driver, options = {}) => {
|
|
|
517
584
|
placing.push({
|
|
518
585
|
at: { x: center.x + offset.x, y: center.y + offset.y },
|
|
519
586
|
marker: member,
|
|
520
|
-
reach:
|
|
587
|
+
reach: stood.get(member)?.reach,
|
|
521
588
|
});
|
|
522
589
|
});
|
|
523
590
|
}
|
|
@@ -627,7 +694,12 @@ export const connectMap = (frame, driver, options = {}) => {
|
|
|
627
694
|
}
|
|
628
695
|
}
|
|
629
696
|
};
|
|
630
|
-
|
|
697
|
+
let looking;
|
|
698
|
+
const stopViewing = driver.onView?.(() => {
|
|
699
|
+
place();
|
|
700
|
+
clearTimeout(looking);
|
|
701
|
+
looking = setTimeout(look, WALK_SETTLES);
|
|
702
|
+
});
|
|
631
703
|
const away = (snapshot) => {
|
|
632
704
|
snapshot.hidden = true;
|
|
633
705
|
snapshot.style.display = 'none';
|
|
@@ -735,22 +807,19 @@ export const connectMap = (frame, driver, options = {}) => {
|
|
|
735
807
|
if (!members || options.clusterZoom === false) {
|
|
736
808
|
return;
|
|
737
809
|
}
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
810
|
+
const { bounds, middle } = reachOf(members.map((one) => ({ lat: one.lat, lng: one.lng })));
|
|
811
|
+
let spread = 0;
|
|
812
|
+
for (const one of members) {
|
|
813
|
+
const across = (((one.lng - middle.lng + 540) % 360) - 180) *
|
|
814
|
+
Math.cos((middle.lat * Math.PI) / 180);
|
|
815
|
+
spread = Math.max(spread, Math.hypot(one.lat - middle.lat, across) * METERS_PER_DEGREE);
|
|
816
|
+
}
|
|
745
817
|
if (spread <= SAME_SPOT || !driver.fit) {
|
|
746
|
-
fanned = { members, place: middle, scale: scaleOf(middle) };
|
|
818
|
+
fanned = { members, place: middle, scale: scaleOf(middle), zoom: driver.zoom?.() };
|
|
747
819
|
place();
|
|
748
820
|
return;
|
|
749
821
|
}
|
|
750
|
-
driver.fit(
|
|
751
|
-
northEast: { lat: Math.max(...lats), lng: Math.max(...lngs) },
|
|
752
|
-
southWest: { lat: Math.min(...lats), lng: Math.min(...lngs) },
|
|
753
|
-
}, 96, motionOf(frame, 'travel'));
|
|
822
|
+
driver.fit(bounds, 96, motionOf(frame, 'travel'));
|
|
754
823
|
};
|
|
755
824
|
frame.addEventListener(HelixMapClusterEvent.eventName, handleCluster);
|
|
756
825
|
const closeFan = () => {
|
|
@@ -765,18 +834,26 @@ export const connectMap = (frame, driver, options = {}) => {
|
|
|
765
834
|
closeFan()?.focus();
|
|
766
835
|
}
|
|
767
836
|
};
|
|
837
|
+
let pressedAt;
|
|
838
|
+
const handleFanDown = (event) => {
|
|
839
|
+
pressedAt = { x: event.clientX, y: event.clientY };
|
|
840
|
+
};
|
|
768
841
|
const handleFanPress = (event) => {
|
|
769
|
-
|
|
842
|
+
const dragged = pressedAt !== undefined &&
|
|
843
|
+
Math.hypot(event.clientX - pressedAt.x, event.clientY - pressedAt.y) > DRAGGED;
|
|
844
|
+
if (fanned && !dragged && !event.composedPath().some(isMarker)) {
|
|
770
845
|
closeFan();
|
|
771
846
|
}
|
|
772
847
|
};
|
|
773
848
|
frame.addEventListener('keydown', handleFanKey);
|
|
849
|
+
frame.addEventListener('pointerdown', handleFanDown);
|
|
774
850
|
frame.addEventListener('click', handleFanPress);
|
|
775
851
|
return {
|
|
776
852
|
disconnect() {
|
|
777
853
|
live = false;
|
|
778
854
|
drawn?.stop();
|
|
779
855
|
stopViewing?.();
|
|
856
|
+
clearTimeout(looking);
|
|
780
857
|
stopWaiting?.();
|
|
781
858
|
stopTheming?.();
|
|
782
859
|
styled.disconnect();
|
|
@@ -785,6 +862,7 @@ export const connectMap = (frame, driver, options = {}) => {
|
|
|
785
862
|
frame.removeEventListener('focusin', reveal);
|
|
786
863
|
frame.removeEventListener(HelixMapClusterEvent.eventName, handleCluster);
|
|
787
864
|
frame.removeEventListener('keydown', handleFanKey);
|
|
865
|
+
frame.removeEventListener('pointerdown', handleFanDown);
|
|
788
866
|
frame.removeEventListener('click', handleFanPress);
|
|
789
867
|
for (const marker of markers()) {
|
|
790
868
|
marker.style.removeProperty('--_map-marker-x');
|
|
@@ -798,6 +876,12 @@ export const connectMap = (frame, driver, options = {}) => {
|
|
|
798
876
|
if (marker.fanned === true) {
|
|
799
877
|
marker.fanned = false;
|
|
800
878
|
}
|
|
879
|
+
if (marker.behind === true) {
|
|
880
|
+
marker.behind = false;
|
|
881
|
+
}
|
|
882
|
+
if (marker.side === 'start') {
|
|
883
|
+
marker.side = 'end';
|
|
884
|
+
}
|
|
801
885
|
marker.style.removeProperty('--_map-marker-leg');
|
|
802
886
|
marker.style.removeProperty('--_map-marker-leg-angle');
|
|
803
887
|
}
|
|
@@ -847,12 +931,7 @@ export const connectMap = (frame, driver, options = {}) => {
|
|
|
847
931
|
if (places.length === 0) {
|
|
848
932
|
return;
|
|
849
933
|
}
|
|
850
|
-
|
|
851
|
-
const lngs = places.map((place) => place.lng);
|
|
852
|
-
driver.fit({
|
|
853
|
-
northEast: { lat: Math.max(...lats), lng: Math.max(...lngs) },
|
|
854
|
-
southWest: { lat: Math.min(...lats), lng: Math.min(...lngs) },
|
|
855
|
-
}, options.padding ?? 48, options.animate === false ? null : motionOf(frame, 'travel'));
|
|
934
|
+
driver.fit(reachOf(places).bounds, options.padding ?? 48, options.animate === false ? null : motionOf(frame, 'travel'));
|
|
856
935
|
},
|
|
857
936
|
};
|
|
858
937
|
};
|
|
@@ -43,8 +43,8 @@ export interface LeafletMap {
|
|
|
43
43
|
x: number;
|
|
44
44
|
y: number;
|
|
45
45
|
};
|
|
46
|
-
off(type: 'layeradd layerremove' | 'move zoom resize', listener: () => void): unknown;
|
|
47
|
-
on(type: 'layeradd layerremove' | 'move zoom resize', listener: () => void): unknown;
|
|
46
|
+
off(type: 'layeradd layerremove' | 'move zoom resize' | 'moveend', listener: () => void): unknown;
|
|
47
|
+
on(type: 'layeradd layerremove' | 'move zoom resize' | 'moveend', listener: () => void): unknown;
|
|
48
48
|
fitBounds(bounds: [[number, number], [number, number]], options?: LeafletAnimation & {
|
|
49
49
|
paddingBottomRight?: [number, number];
|
|
50
50
|
paddingTopLeft?: [number, number];
|
|
@@ -75,6 +75,8 @@ export const leafletDriver = (map, shapes) => ({
|
|
|
75
75
|
? {
|
|
76
76
|
layers: (read, events) => {
|
|
77
77
|
let drawn = [];
|
|
78
|
+
const circles = new Map();
|
|
79
|
+
let placed = new Map();
|
|
78
80
|
const rasters = new Map();
|
|
79
81
|
const drawRasters = (specs) => {
|
|
80
82
|
const make = shapes.tileLayer;
|
|
@@ -122,6 +124,8 @@ export const leafletDriver = (map, shapes) => ({
|
|
|
122
124
|
shape.remove();
|
|
123
125
|
}
|
|
124
126
|
const specs = read();
|
|
127
|
+
const kept = new Set();
|
|
128
|
+
placed = new Map();
|
|
125
129
|
drawRasters(specs.filter((spec) => spec.kind === 'raster'));
|
|
126
130
|
drawn = specs.flatMap((layer) => {
|
|
127
131
|
if (layer.kind === 'buildings' ||
|
|
@@ -137,7 +141,19 @@ export const leafletDriver = (map, shapes) => ({
|
|
|
137
141
|
if (!circle) {
|
|
138
142
|
return [];
|
|
139
143
|
}
|
|
140
|
-
|
|
144
|
+
kept.add(layer.id);
|
|
145
|
+
placed.set(layer.id, layer.points);
|
|
146
|
+
const had = circles.get(layer.id);
|
|
147
|
+
if (had &&
|
|
148
|
+
had.points === layer.points &&
|
|
149
|
+
had.color === layer.color &&
|
|
150
|
+
had.casing === layer.casing) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
for (const shape of had?.shapes ?? []) {
|
|
154
|
+
shape.remove();
|
|
155
|
+
}
|
|
156
|
+
const made = latlngs.map((latlng, at) => {
|
|
141
157
|
const point = circle(latlng, {
|
|
142
158
|
color: layer.casing ?? layer.color,
|
|
143
159
|
fillColor: layer.color,
|
|
@@ -150,6 +166,13 @@ export const leafletDriver = (map, shapes) => ({
|
|
|
150
166
|
point.addTo(map);
|
|
151
167
|
return point;
|
|
152
168
|
});
|
|
169
|
+
circles.set(layer.id, {
|
|
170
|
+
casing: layer.casing,
|
|
171
|
+
color: layer.color,
|
|
172
|
+
points: layer.points,
|
|
173
|
+
shapes: made,
|
|
174
|
+
});
|
|
175
|
+
return [];
|
|
153
176
|
}
|
|
154
177
|
const casing = layer.kind === 'route' && layer.casing
|
|
155
178
|
? [
|
|
@@ -185,14 +208,41 @@ export const leafletDriver = (map, shapes) => ({
|
|
|
185
208
|
}
|
|
186
209
|
return [...casing, shape];
|
|
187
210
|
});
|
|
211
|
+
for (const [id, had] of [...circles]) {
|
|
212
|
+
if (!kept.has(id)) {
|
|
213
|
+
for (const shape of had.shapes) {
|
|
214
|
+
shape.remove();
|
|
215
|
+
}
|
|
216
|
+
circles.delete(id);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
events?.settled?.();
|
|
188
220
|
};
|
|
221
|
+
const settled = () => events?.settled?.();
|
|
222
|
+
map.on('moveend', settled);
|
|
189
223
|
draw();
|
|
190
224
|
return {
|
|
225
|
+
inView: (id) => {
|
|
226
|
+
const box = map.getContainer();
|
|
227
|
+
const width = box.clientWidth;
|
|
228
|
+
const height = box.clientHeight;
|
|
229
|
+
return (placed.get(id) ?? []).flatMap((place, at) => {
|
|
230
|
+
const { x, y } = map.latLngToContainerPoint([place.lat, place.lng]);
|
|
231
|
+
return x >= 0 && y >= 0 && x <= width && y <= height ? [{ at, place }] : [];
|
|
232
|
+
});
|
|
233
|
+
},
|
|
191
234
|
stop: () => {
|
|
235
|
+
map.off('moveend', settled);
|
|
192
236
|
for (const shape of drawn) {
|
|
193
237
|
shape.remove();
|
|
194
238
|
}
|
|
195
239
|
drawn = [];
|
|
240
|
+
for (const had of circles.values()) {
|
|
241
|
+
for (const shape of had.shapes) {
|
|
242
|
+
shape.remove();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
circles.clear();
|
|
196
246
|
drawRasters([]);
|
|
197
247
|
},
|
|
198
248
|
update: draw,
|
|
@@ -97,8 +97,10 @@ interface MapLibreAnimation {
|
|
|
97
97
|
export interface MapLibreMap {
|
|
98
98
|
/** Whether every tile the view needs has loaded. */
|
|
99
99
|
areTilesLoaded?(): boolean;
|
|
100
|
-
/** Whether the style
|
|
100
|
+
/** Whether the style and every tile have loaded, and nothing is waiting to be drawn. */
|
|
101
101
|
loaded?(): boolean;
|
|
102
|
+
/** Whether the style has loaded: `true`, and anything else is not yet. MapLibre 6 types it loosely. */
|
|
103
|
+
isStyleLoaded?(): unknown;
|
|
102
104
|
/** The canvas, which MapLibre makes focusable. */
|
|
103
105
|
getCanvas(): HTMLElement;
|
|
104
106
|
/** The middle of the view, which a map-type picture is drawn around. */
|
|
@@ -186,8 +188,8 @@ export interface MapLibreMap {
|
|
|
186
188
|
enable(): void;
|
|
187
189
|
isEnabled(): boolean;
|
|
188
190
|
};
|
|
189
|
-
off(type: 'click' | 'idle' | 'load' | 'mousemove' | 'move' | 'resize' | 'sourcedata' | 'moveend' | 'styledata' | 'styleimagemissing', listener: (event?: MapLibreEvent) => void): unknown;
|
|
190
|
-
on(type: 'click' | 'idle' | 'load' | 'mousemove' | 'move' | 'resize' | 'sourcedata' | 'moveend' | 'styledata' | 'styleimagemissing', listener: (event?: MapLibreEvent) => void): unknown;
|
|
191
|
+
off(type: 'click' | 'idle' | 'load' | 'mousemove' | 'move' | 'resize' | 'sourcedata' | 'moveend' | 'render' | 'styledata' | 'styleimagemissing', listener: (event?: MapLibreEvent) => void): unknown;
|
|
192
|
+
on(type: 'click' | 'idle' | 'load' | 'mousemove' | 'move' | 'resize' | 'sourcedata' | 'moveend' | 'render' | 'styledata' | 'styleimagemissing', listener: (event?: MapLibreEvent) => void): unknown;
|
|
191
193
|
removeLayer(id: string): unknown;
|
|
192
194
|
removeSource(id: string): unknown;
|
|
193
195
|
panBy(offset: [number, number], options?: MapLibreAnimation): unknown;
|
|
@@ -239,7 +241,7 @@ export interface MapLibreMap {
|
|
|
239
241
|
};
|
|
240
242
|
};
|
|
241
243
|
/** Draws each image a style names as it is asked for. MapLibre 6. */
|
|
242
|
-
setMissingStyleImageResolver?(resolver: ((id: string) =>
|
|
244
|
+
setMissingStyleImageResolver?(resolver: ((id: string) => unknown) | null): unknown;
|
|
243
245
|
/** Sets one paint property of one layer, which is how a points layer takes a new color. */
|
|
244
246
|
setPaintProperty(layer: string, name: string, value: unknown): unknown;
|
|
245
247
|
/** Sets one layout property of one layer, which is how the buildings are shown and hidden. */
|
|
@@ -202,11 +202,22 @@ const showLabels = (map, shown) => {
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
};
|
|
205
|
+
const described = new WeakMap();
|
|
206
|
+
const describedOf = (holder) => {
|
|
207
|
+
const had = described.get(holder);
|
|
208
|
+
if (had) {
|
|
209
|
+
return had;
|
|
210
|
+
}
|
|
211
|
+
const style = (holder.getStyle() ?? {});
|
|
212
|
+
described.set(holder, style);
|
|
213
|
+
queueMicrotask(() => described.delete(holder));
|
|
214
|
+
return style;
|
|
215
|
+
};
|
|
205
216
|
const supplyImage = (holder, id) => {
|
|
206
217
|
if (!id?.startsWith('helix-') || holder.hasImage?.(id)) {
|
|
207
218
|
return;
|
|
208
219
|
}
|
|
209
|
-
const style = holder
|
|
220
|
+
const style = describedOf(holder);
|
|
210
221
|
const [named = '', text = ''] = id.split('|');
|
|
211
222
|
const image = style?.metadata?.['helix:images']?.[named];
|
|
212
223
|
const drawn = image ? drawMapImage(image, text) : undefined;
|
|
@@ -216,8 +227,10 @@ const supplyImage = (holder, id) => {
|
|
|
216
227
|
};
|
|
217
228
|
const answerImages = (holder) => {
|
|
218
229
|
if (typeof holder.setMissingStyleImageResolver === 'function') {
|
|
219
|
-
|
|
220
|
-
|
|
230
|
+
const before = holder
|
|
231
|
+
._missingStyleImageResolver ?? null;
|
|
232
|
+
holder.setMissingStyleImageResolver((id) => id.startsWith('helix-') ? supplyImage(holder, id) : before?.(id));
|
|
233
|
+
return () => holder.setMissingStyleImageResolver?.(before);
|
|
221
234
|
}
|
|
222
235
|
const missing = (event) => supplyImage(holder, event?.id);
|
|
223
236
|
holder.on('styleimagemissing', missing);
|
|
@@ -265,10 +278,18 @@ export const mapLibreDriver = (map) => ({
|
|
|
265
278
|
bared.set(map, !shown);
|
|
266
279
|
showLabels(map, shown);
|
|
267
280
|
if (!heard) {
|
|
281
|
+
let pending = false;
|
|
268
282
|
map.on('styledata', () => {
|
|
269
|
-
if (bared.get(map)) {
|
|
270
|
-
|
|
283
|
+
if (!bared.get(map) || pending) {
|
|
284
|
+
return;
|
|
271
285
|
}
|
|
286
|
+
pending = true;
|
|
287
|
+
queueMicrotask(() => {
|
|
288
|
+
pending = false;
|
|
289
|
+
if (bared.get(map)) {
|
|
290
|
+
showLabels(map, false);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
272
293
|
});
|
|
273
294
|
}
|
|
274
295
|
},
|
|
@@ -344,7 +365,8 @@ export const mapLibreDriver = (map) => ({
|
|
|
344
365
|
const lines = new Map();
|
|
345
366
|
let tracing = 0;
|
|
346
367
|
const heightsOf = (url, tile, encoding) => {
|
|
347
|
-
const
|
|
368
|
+
const key = `${url}|${encoding}`;
|
|
369
|
+
const had = heights.get(key);
|
|
348
370
|
if (had) {
|
|
349
371
|
return had;
|
|
350
372
|
}
|
|
@@ -366,11 +388,17 @@ export const mapLibreDriver = (map) => ({
|
|
|
366
388
|
const pixels = context.getImageData(0, 0, bitmap.width, bitmap.height).data;
|
|
367
389
|
return { ...tile, heights: heightsFrom(pixels, encoding), size: bitmap.width };
|
|
368
390
|
})
|
|
369
|
-
.catch(() => undefined)
|
|
391
|
+
.catch(() => undefined)
|
|
392
|
+
.then((tile) => {
|
|
393
|
+
if (tile === undefined && heights.get(key) === read) {
|
|
394
|
+
heights.delete(key);
|
|
395
|
+
}
|
|
396
|
+
return tile;
|
|
397
|
+
});
|
|
370
398
|
if (heights.size >= 100) {
|
|
371
399
|
heights.delete(heights.keys().next().value);
|
|
372
400
|
}
|
|
373
|
-
heights.set(
|
|
401
|
+
heights.set(key, read);
|
|
374
402
|
return read;
|
|
375
403
|
};
|
|
376
404
|
const trace = async () => {
|
|
@@ -411,10 +439,13 @@ export const mapLibreDriver = (map) => ({
|
|
|
411
439
|
const known = new Map(read.filter((tile) => tile !== undefined).map((tile) => [`${tile.x}/${tile.y}`, tile]));
|
|
412
440
|
const traced = [];
|
|
413
441
|
for (const tile of known.values()) {
|
|
414
|
-
const
|
|
442
|
+
const across = 2 ** tile.z;
|
|
443
|
+
const beside = [-1, 0, 1]
|
|
444
|
+
.flatMap((dy) => [-1, 0, 1].map((dx) => known.has(`${(tile.x + dx + across) % across}/${tile.y + dy}`) ? '1' : '0'))
|
|
445
|
+
.join('');
|
|
446
|
+
const key = `${spec.src}|${spec.encoding ?? ''}|${tile.z}/${tile.x}/${tile.y}|${interval}|${unit}|${beside}`;
|
|
415
447
|
let found = lines.get(key);
|
|
416
448
|
if (!found) {
|
|
417
|
-
const across = 2 ** tile.z;
|
|
418
449
|
found = contoursOf(tile, interval, unit, (dx, dy) => known.get(`${(tile.x + dx + across) % across}/${tile.y + dy}`));
|
|
419
450
|
if (lines.size >= 200) {
|
|
420
451
|
lines.delete(lines.keys().next().value);
|
|
@@ -1029,12 +1060,27 @@ export const mapLibreDriver = (map) => ({
|
|
|
1029
1060
|
done();
|
|
1030
1061
|
return () => undefined;
|
|
1031
1062
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1063
|
+
let finished = false;
|
|
1064
|
+
const finish = () => {
|
|
1065
|
+
if (finished) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
finished = true;
|
|
1069
|
+
stop();
|
|
1034
1070
|
done();
|
|
1035
1071
|
};
|
|
1036
|
-
|
|
1037
|
-
|
|
1072
|
+
const drawn = () => {
|
|
1073
|
+
if (map.isStyleLoaded?.() === true && map.areTilesLoaded?.() === true) {
|
|
1074
|
+
finish();
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
const stop = () => {
|
|
1078
|
+
map.off('load', finish);
|
|
1079
|
+
map.off('render', drawn);
|
|
1080
|
+
};
|
|
1081
|
+
map.on('load', finish);
|
|
1082
|
+
map.on('render', drawn);
|
|
1083
|
+
return stop;
|
|
1038
1084
|
},
|
|
1039
1085
|
rotate: ({ by, to }, motion) => {
|
|
1040
1086
|
const { bearing, pitch } = aimFrom(map);
|
package/dist/style/map-style.js
CHANGED
|
@@ -57,7 +57,13 @@ const SIGNS = {
|
|
|
57
57
|
'us-interstate': 'interstate',
|
|
58
58
|
'us-state': 'state',
|
|
59
59
|
};
|
|
60
|
-
const
|
|
60
|
+
const stamp = (drawn) => {
|
|
61
|
+
let hash = 0x811c9dc5;
|
|
62
|
+
for (const char of JSON.stringify(drawn)) {
|
|
63
|
+
hash = Math.imul(hash ^ (char.codePointAt(0) ?? 0), 0x01000193) >>> 0;
|
|
64
|
+
}
|
|
65
|
+
return hash.toString(36);
|
|
66
|
+
};
|
|
61
67
|
const widen = (from, to) => [
|
|
62
68
|
'interpolate',
|
|
63
69
|
['exponential', 1.5],
|
|
@@ -147,13 +153,14 @@ export const mapLibreStyle = (colors, options = {}) => {
|
|
|
147
153
|
const landmarkColor = ['match', ['get', 'class']];
|
|
148
154
|
for (const { classes, group, hue, icon } of LANDMARKS) {
|
|
149
155
|
const color = cssColor(hued(hue));
|
|
150
|
-
const
|
|
151
|
-
images[id] = {
|
|
156
|
+
const image = {
|
|
152
157
|
color,
|
|
153
158
|
icon: { content: icon.content, viewBox: icon.viewBox },
|
|
154
159
|
ink: halo,
|
|
155
160
|
kind: 'landmark',
|
|
156
161
|
};
|
|
162
|
+
const id = `helix-landmark-${group}-${stamp(image)}`;
|
|
163
|
+
images[id] = image;
|
|
157
164
|
landmarkImage.push([...classes], id);
|
|
158
165
|
landmarkColor.push([...classes], legible(hued(hue), 0));
|
|
159
166
|
}
|
|
@@ -161,27 +168,30 @@ export const mapLibreStyle = (colors, options = {}) => {
|
|
|
161
168
|
landmarkColor.push(legible(land, 0.6));
|
|
162
169
|
const classesOf = (everyday) => LANDMARKS.filter((one) => (one.everyday === true) === everyday).flatMap(({ classes }) => classes);
|
|
163
170
|
const signFont = face ? `"${face}", sans-serif` : 'sans-serif';
|
|
164
|
-
|
|
171
|
+
const signs = `helix-sign-${stamp(signFont)}-`;
|
|
172
|
+
images[`${signs}interstate`] = {
|
|
165
173
|
design: 'interstate',
|
|
166
174
|
font: signFont,
|
|
167
175
|
ink: '#ffffff',
|
|
168
176
|
kind: 'sign',
|
|
169
177
|
};
|
|
170
|
-
images[
|
|
171
|
-
images[
|
|
172
|
-
const plate = {
|
|
173
|
-
images[`helix-sign-plate-${idOf(plate.fill + plate.edge)}`] = {
|
|
174
|
-
...plate,
|
|
178
|
+
images[`${signs}us`] = { design: 'us', font: signFont, ink: '#1a1a1a', kind: 'sign' };
|
|
179
|
+
images[`${signs}state`] = { design: 'state', font: signFont, ink: '#1a1a1a', kind: 'sign' };
|
|
180
|
+
const plate = {
|
|
175
181
|
design: 'plate',
|
|
182
|
+
edge: toward(0.5),
|
|
183
|
+
fill: halo,
|
|
176
184
|
font: signFont,
|
|
177
185
|
ink: cssColor(ink),
|
|
178
186
|
kind: 'sign',
|
|
179
187
|
};
|
|
188
|
+
const plateShape = `plate-${stamp(plate)}`;
|
|
189
|
+
images[`${signs}${plateShape}`] = plate;
|
|
180
190
|
const signShape = ['match', ['get', 'network']];
|
|
181
191
|
for (const [network, shape] of Object.entries(SIGNS)) {
|
|
182
192
|
signShape.push(network, shape);
|
|
183
193
|
}
|
|
184
|
-
signShape.push(
|
|
194
|
+
signShape.push(plateShape);
|
|
185
195
|
return {
|
|
186
196
|
...(glyphs ? { glyphs } : {}),
|
|
187
197
|
layers: [
|
|
@@ -355,7 +365,7 @@ export const mapLibreStyle = (colors, options = {}) => {
|
|
|
355
365
|
],
|
|
356
366
|
id: 'label-shield',
|
|
357
367
|
layout: {
|
|
358
|
-
'icon-image': ['concat',
|
|
368
|
+
'icon-image': ['concat', signs, signShape, '|', ['get', 'ref']],
|
|
359
369
|
'icon-rotation-alignment': 'viewport',
|
|
360
370
|
'symbol-placement': 'line',
|
|
361
371
|
'symbol-spacing': 320,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion.dev/helix-map",
|
|
3
3
|
"description": "Connects the Helix Map frame to a map renderer: MapLibre, Leaflet, or a driver of your own.",
|
|
4
|
-
"version": "0.110.
|
|
4
|
+
"version": "0.110.3",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -43,14 +43,14 @@
|
|
|
43
43
|
},
|
|
44
44
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@fusion.dev/helix-core": "0.110.
|
|
47
|
-
"@fusion.dev/helix-icons": "0.110.
|
|
46
|
+
"@fusion.dev/helix-core": "0.110.3",
|
|
47
|
+
"@fusion.dev/helix-icons": "0.110.3",
|
|
48
48
|
"@mapbox/vector-tile": "^3.0.0",
|
|
49
49
|
"pbf": "^5.1.2"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@fusion.dev/helix-headless": "0.110.
|
|
53
|
-
"@fusion.dev/helix-testing": "0.110.
|
|
52
|
+
"@fusion.dev/helix-headless": "0.110.3",
|
|
53
|
+
"@fusion.dev/helix-testing": "0.110.3",
|
|
54
54
|
"@maplibre/maplibre-gl-style-spec": "^26.4.4",
|
|
55
55
|
"@types/bun": "^1.3.0",
|
|
56
56
|
"@types/leaflet": "^1.9.20",
|