@elabs-ai/components-maps 4.2.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/chunk-2FPB53NG.js +36 -0
- package/dist/chunk-2FPB53NG.js.map +1 -0
- package/dist/chunk-2VVJRT66.js +37 -0
- package/dist/chunk-2VVJRT66.js.map +1 -0
- package/dist/chunk-3HXFONRP.js +2 -0
- package/dist/chunk-3HXFONRP.js.map +1 -0
- package/dist/chunk-3SJN3IYT.js +2 -0
- package/dist/chunk-3SJN3IYT.js.map +1 -0
- package/dist/chunk-4DANBD5B.js +55 -0
- package/dist/chunk-4DANBD5B.js.map +1 -0
- package/dist/chunk-5PDVG53B.js +2 -0
- package/dist/chunk-5PDVG53B.js.map +1 -0
- package/dist/chunk-643R7IJC.js +2 -0
- package/dist/chunk-643R7IJC.js.map +1 -0
- package/dist/chunk-B3ZDBQ6I.js +164 -0
- package/dist/chunk-B3ZDBQ6I.js.map +1 -0
- package/dist/chunk-BGCE3RR5.js +35 -0
- package/dist/chunk-BGCE3RR5.js.map +1 -0
- package/dist/chunk-C4VI37TD.js +485 -0
- package/dist/chunk-C4VI37TD.js.map +1 -0
- package/dist/chunk-CJJT6AEI.js +179 -0
- package/dist/chunk-CJJT6AEI.js.map +1 -0
- package/dist/chunk-D2RCPTRO.js +2 -0
- package/dist/chunk-D2RCPTRO.js.map +1 -0
- package/dist/chunk-DTV7JZVN.js +2 -0
- package/dist/chunk-DTV7JZVN.js.map +1 -0
- package/dist/chunk-E65Z3MSV.js +80 -0
- package/dist/chunk-E65Z3MSV.js.map +1 -0
- package/dist/chunk-E7VPNINZ.js +70 -0
- package/dist/chunk-E7VPNINZ.js.map +1 -0
- package/dist/chunk-FHV5IA6N.js +100 -0
- package/dist/chunk-FHV5IA6N.js.map +1 -0
- package/dist/chunk-FQ6V7BFZ.js +347 -0
- package/dist/chunk-FQ6V7BFZ.js.map +1 -0
- package/dist/chunk-G7JDXDJN.js +19 -0
- package/dist/chunk-G7JDXDJN.js.map +1 -0
- package/dist/chunk-GWULZOMW.js +21 -0
- package/dist/chunk-GWULZOMW.js.map +1 -0
- package/dist/chunk-GYDTTAVD.js +120 -0
- package/dist/chunk-GYDTTAVD.js.map +1 -0
- package/dist/chunk-HCOBWIM4.js +48 -0
- package/dist/chunk-HCOBWIM4.js.map +1 -0
- package/dist/chunk-IAHK5MVT.js +2 -0
- package/dist/chunk-IAHK5MVT.js.map +1 -0
- package/dist/chunk-JGJ6442I.js +235 -0
- package/dist/chunk-JGJ6442I.js.map +1 -0
- package/dist/chunk-JYFERXZT.js +18 -0
- package/dist/chunk-JYFERXZT.js.map +1 -0
- package/dist/chunk-LU2UEKFM.js +53 -0
- package/dist/chunk-LU2UEKFM.js.map +1 -0
- package/dist/chunk-LVYM6C5C.js +2 -0
- package/dist/chunk-LVYM6C5C.js.map +1 -0
- package/dist/chunk-MIMDP3EU.js +104 -0
- package/dist/chunk-MIMDP3EU.js.map +1 -0
- package/dist/chunk-NNBURJHY.js +2 -0
- package/dist/chunk-NNBURJHY.js.map +1 -0
- package/dist/chunk-NPUC3V37.js +2 -0
- package/dist/chunk-NPUC3V37.js.map +1 -0
- package/dist/chunk-OEVVNPN3.js +201 -0
- package/dist/chunk-OEVVNPN3.js.map +1 -0
- package/dist/chunk-PF3R2BXM.js +200 -0
- package/dist/chunk-PF3R2BXM.js.map +1 -0
- package/dist/chunk-Q2I5YODW.js +2 -0
- package/dist/chunk-Q2I5YODW.js.map +1 -0
- package/dist/chunk-QDWWP3N7.js +247 -0
- package/dist/chunk-QDWWP3N7.js.map +1 -0
- package/dist/chunk-QU33KPMP.js +2 -0
- package/dist/chunk-QU33KPMP.js.map +1 -0
- package/dist/chunk-RK4BUMHS.js +2 -0
- package/dist/chunk-RK4BUMHS.js.map +1 -0
- package/dist/chunk-RNSLG64F.js +26 -0
- package/dist/chunk-RNSLG64F.js.map +1 -0
- package/dist/chunk-RV673TB4.js +68 -0
- package/dist/chunk-RV673TB4.js.map +1 -0
- package/dist/chunk-S7N2WMVT.js +180 -0
- package/dist/chunk-S7N2WMVT.js.map +1 -0
- package/dist/chunk-T3Z6TCDD.js +2 -0
- package/dist/chunk-T3Z6TCDD.js.map +1 -0
- package/dist/chunk-T4TJTVVA.js +105 -0
- package/dist/chunk-T4TJTVVA.js.map +1 -0
- package/dist/chunk-UMOYRCDX.js +103 -0
- package/dist/chunk-UMOYRCDX.js.map +1 -0
- package/dist/chunk-V2REM6UT.js +185 -0
- package/dist/chunk-V2REM6UT.js.map +1 -0
- package/dist/chunk-VFBRFGID.js +89 -0
- package/dist/chunk-VFBRFGID.js.map +1 -0
- package/dist/chunk-W76NLFER.js +103 -0
- package/dist/chunk-W76NLFER.js.map +1 -0
- package/dist/chunk-WNU4IOGL.js +116 -0
- package/dist/chunk-WNU4IOGL.js.map +1 -0
- package/dist/chunk-WS4Z7ENS.js +494 -0
- package/dist/chunk-WS4Z7ENS.js.map +1 -0
- package/dist/chunk-XQT7YLJY.js +497 -0
- package/dist/chunk-XQT7YLJY.js.map +1 -0
- package/dist/chunk-Y44MEKJV.js +21 -0
- package/dist/chunk-Y44MEKJV.js.map +1 -0
- package/dist/chunk-Z4EPSOB4.js +53 -0
- package/dist/chunk-Z4EPSOB4.js.map +1 -0
- package/dist/chunk-Z5OPDBDC.js +2 -0
- package/dist/chunk-Z5OPDBDC.js.map +1 -0
- package/dist/index.d.ts +1241 -46
- package/dist/index.js +189 -1471
- package/dist/index.js.map +1 -1
- package/dist/lib/arc-math.js +8 -0
- package/dist/lib/arc-math.js.map +1 -0
- package/dist/lib/map-position.js +11 -0
- package/dist/lib/map-position.js.map +1 -0
- package/dist/lib/merge-hover-paint.js +10 -0
- package/dist/lib/merge-hover-paint.js.map +1 -0
- package/dist/lib/plan-crs.js +14 -0
- package/dist/lib/plan-crs.js.map +1 -0
- package/dist/lib/plan-patterns.js +14 -0
- package/dist/lib/plan-patterns.js.map +1 -0
- package/dist/lib/plan-regions.js +18 -0
- package/dist/lib/plan-regions.js.map +1 -0
- package/dist/lib/plan-status.js +14 -0
- package/dist/lib/plan-status.js.map +1 -0
- package/dist/lib/route-arrow.js +12 -0
- package/dist/lib/route-arrow.js.map +1 -0
- package/dist/lib/use-map-breakpoint.js +33 -0
- package/dist/lib/use-map-breakpoint.js.map +1 -0
- package/dist/lib/use-plan-projection.js +9 -0
- package/dist/lib/use-plan-projection.js.map +1 -0
- package/dist/lib/use-token-color.js +10 -0
- package/dist/lib/use-token-color.js.map +1 -0
- package/dist/lib/warn-once.js +10 -0
- package/dist/lib/warn-once.js.map +1 -0
- package/dist/map-annotation/anchor.js +14 -0
- package/dist/map-annotation/anchor.js.map +1 -0
- package/dist/map-annotation/index.js +21 -0
- package/dist/map-annotation/index.js.map +1 -0
- package/dist/map-annotation/map-annotation.js +18 -0
- package/dist/map-annotation/map-annotation.js.map +1 -0
- package/dist/map-arc/index.js +16 -0
- package/dist/map-arc/index.js.map +1 -0
- package/dist/map-arc/map-arc.js +13 -0
- package/dist/map-arc/map-arc.js.map +1 -0
- package/dist/map-canvas/index.css +11 -0
- package/dist/map-canvas/index.css.map +1 -0
- package/dist/map-canvas/index.js +20 -0
- package/dist/map-canvas/index.js.map +1 -0
- package/dist/map-canvas/map-canvas.css +11 -0
- package/dist/map-canvas/map-canvas.css.map +1 -0
- package/dist/map-canvas/map-canvas.js +17 -0
- package/dist/map-canvas/map-canvas.js.map +1 -0
- package/dist/map-canvas/map-context.js +11 -0
- package/dist/map-canvas/map-context.js.map +1 -0
- package/dist/map-canvas/use-resolved-basemap-theme.js +9 -0
- package/dist/map-canvas/use-resolved-basemap-theme.js.map +1 -0
- package/dist/map-cluster-layer/index.js +12 -0
- package/dist/map-cluster-layer/index.js.map +1 -0
- package/dist/map-cluster-layer/map-cluster-layer.js +12 -0
- package/dist/map-cluster-layer/map-cluster-layer.js.map +1 -0
- package/dist/map-controls/index.js +11 -0
- package/dist/map-controls/index.js.map +1 -0
- package/dist/map-controls/map-controls.js +11 -0
- package/dist/map-controls/map-controls.js.map +1 -0
- package/dist/map-geojson/index.js +13 -0
- package/dist/map-geojson/index.js.map +1 -0
- package/dist/map-geojson/map-geojson.js +13 -0
- package/dist/map-geojson/map-geojson.js.map +1 -0
- package/dist/map-inset/index.css +11 -0
- package/dist/map-inset/index.css.map +1 -0
- package/dist/map-inset/index.js +20 -0
- package/dist/map-inset/index.js.map +1 -0
- package/dist/map-inset/map-inset.css +11 -0
- package/dist/map-inset/map-inset.css.map +1 -0
- package/dist/map-inset/map-inset.js +20 -0
- package/dist/map-inset/map-inset.js.map +1 -0
- package/dist/map-legend/index.js +14 -0
- package/dist/map-legend/index.js.map +1 -0
- package/dist/map-legend/map-corner.js +10 -0
- package/dist/map-legend/map-corner.js.map +1 -0
- package/dist/map-legend/map-legend.js +11 -0
- package/dist/map-legend/map-legend.js.map +1 -0
- package/dist/map-marker/index.js +22 -0
- package/dist/map-marker/index.js.map +1 -0
- package/dist/map-marker/map-marker.js +22 -0
- package/dist/map-marker/map-marker.js.map +1 -0
- package/dist/map-north-arrow/index.js +11 -0
- package/dist/map-north-arrow/index.js.map +1 -0
- package/dist/map-north-arrow/map-north-arrow.js +11 -0
- package/dist/map-north-arrow/map-north-arrow.js.map +1 -0
- package/dist/map-plan/index.js +30 -0
- package/dist/map-plan/index.js.map +1 -0
- package/dist/map-plan/map-plan-legend.js +10 -0
- package/dist/map-plan/map-plan-legend.js.map +1 -0
- package/dist/map-plan/map-plan-overlay.js +13 -0
- package/dist/map-plan/map-plan-overlay.js.map +1 -0
- package/dist/map-plan/map-plan-status.js +9 -0
- package/dist/map-plan/map-plan-status.js.map +1 -0
- package/dist/map-plan/map-plan-table.js +10 -0
- package/dist/map-plan/map-plan-table.js.map +1 -0
- package/dist/map-plan/use-plan-selection.js +9 -0
- package/dist/map-plan/use-plan-selection.js.map +1 -0
- package/dist/map-plan-image/index.js +11 -0
- package/dist/map-plan-image/index.js.map +1 -0
- package/dist/map-plan-image/map-plan-image.js +11 -0
- package/dist/map-plan-image/map-plan-image.js.map +1 -0
- package/dist/map-popup/index.js +12 -0
- package/dist/map-popup/index.js.map +1 -0
- package/dist/map-popup/map-popup.js +12 -0
- package/dist/map-popup/map-popup.js.map +1 -0
- package/dist/map-route/index.js +12 -0
- package/dist/map-route/index.js.map +1 -0
- package/dist/map-route/map-route.js +12 -0
- package/dist/map-route/map-route.js.map +1 -0
- package/dist/map-scale-bar/index.js +18 -0
- package/dist/map-scale-bar/index.js.map +1 -0
- package/dist/map-scale-bar/map-scale-bar.js +18 -0
- package/dist/map-scale-bar/map-scale-bar.js.map +1 -0
- package/package.json +5 -5
- package/src/index.ts +83 -0
- package/src/lib/arc-math.test.ts +19 -0
- package/src/lib/arc-math.ts +6 -1
- package/src/lib/map-position.ts +77 -0
- package/src/lib/merge-hover-paint.ts +44 -13
- package/src/lib/plan-crs.parity.test.ts +39 -0
- package/src/lib/plan-crs.test.ts +248 -0
- package/src/lib/plan-crs.ts +252 -0
- package/src/lib/plan-patterns.test.ts +255 -0
- package/src/lib/plan-patterns.ts +160 -0
- package/src/lib/plan-regions.test.ts +186 -0
- package/src/lib/plan-regions.ts +209 -0
- package/src/lib/plan-status.test.ts +87 -0
- package/src/lib/plan-status.ts +147 -0
- package/src/lib/route-arrow.test.ts +88 -0
- package/src/lib/route-arrow.ts +59 -0
- package/src/lib/use-map-breakpoint.test.ts +83 -0
- package/src/lib/use-map-breakpoint.ts +238 -0
- package/src/lib/use-plan-projection.ts +157 -0
- package/src/lib/warn-once.ts +21 -0
- package/src/map-annotation/anchor.ts +92 -0
- package/src/map-annotation/index.ts +15 -0
- package/src/map-annotation/map-annotation.stories.tsx +113 -0
- package/src/map-annotation/map-annotation.test.tsx +106 -0
- package/src/map-annotation/map-annotation.tsx +356 -0
- package/src/map-arc/map-arc.tsx +26 -5
- package/src/map-canvas/map-canvas.stories.tsx +211 -0
- package/src/map-canvas/map-canvas.test.tsx +389 -0
- package/src/map-canvas/map-canvas.tsx +457 -17
- package/src/map-canvas/map-context.ts +11 -0
- package/src/map-cluster-layer/map-cluster-layer.tsx +14 -1
- package/src/map-controls/map-controls.stories.tsx +16 -0
- package/src/map-controls/map-controls.test.tsx +145 -1
- package/src/map-controls/map-controls.tsx +57 -12
- package/src/map-geojson/map-geojson.stories.tsx +153 -3
- package/src/map-geojson/map-geojson.test.tsx +453 -0
- package/src/map-geojson/map-geojson.tsx +591 -51
- package/src/map-inset/index.ts +1 -0
- package/src/map-inset/map-inset.stories.tsx +61 -0
- package/src/map-inset/map-inset.test.tsx +117 -0
- package/src/map-inset/map-inset.tsx +264 -0
- package/src/map-legend/index.ts +10 -0
- package/src/map-legend/map-corner.ts +23 -0
- package/src/map-legend/map-legend.stories.tsx +171 -0
- package/src/map-legend/map-legend.test.tsx +136 -0
- package/src/map-legend/map-legend.tsx +264 -0
- package/src/map-marker/map-marker.stories.tsx +136 -4
- package/src/map-marker/map-marker.test.tsx +328 -3
- package/src/map-marker/map-marker.tsx +370 -33
- package/src/map-north-arrow/index.ts +1 -0
- package/src/map-north-arrow/map-north-arrow.stories.tsx +65 -0
- package/src/map-north-arrow/map-north-arrow.test.tsx +39 -0
- package/src/map-north-arrow/map-north-arrow.tsx +82 -0
- package/src/map-plan/index.ts +13 -0
- package/src/map-plan/map-plan-legend.test.tsx +61 -0
- package/src/map-plan/map-plan-legend.tsx +83 -0
- package/src/map-plan/map-plan-overlay.stories.tsx +260 -0
- package/src/map-plan/map-plan-overlay.test.tsx +302 -0
- package/src/map-plan/map-plan-overlay.tsx +531 -0
- package/src/map-plan/map-plan-status.test.tsx +71 -0
- package/src/map-plan/map-plan-status.tsx +77 -0
- package/src/map-plan/map-plan-table.test.tsx +86 -0
- package/src/map-plan/map-plan-table.tsx +153 -0
- package/src/map-plan/use-plan-selection.ts +78 -0
- package/src/map-plan-image/index.ts +1 -0
- package/src/map-plan-image/map-plan-image.stories.tsx +301 -0
- package/src/map-plan-image/map-plan-image.test.tsx +176 -0
- package/src/map-plan-image/map-plan-image.tsx +279 -0
- package/src/map-popup/map-popup.tsx +15 -10
- package/src/map-route/index.ts +1 -1
- package/src/map-route/map-route.test.tsx +216 -0
- package/src/map-route/map-route.tsx +105 -6
- package/src/map-scale-bar/index.ts +9 -0
- package/src/map-scale-bar/map-scale-bar.stories.tsx +63 -0
- package/src/map-scale-bar/map-scale-bar.test.tsx +65 -0
- package/src/map-scale-bar/map-scale-bar.tsx +145 -0
- package/src/test-utils/locator-fixture.ts +75 -0
- package/src/test-utils/maplibre-mock.ts +256 -19
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
createRouteArrowImage,
|
|
4
|
+
routeArrowImageId
|
|
5
|
+
} from "./chunk-2VVJRT66.js";
|
|
6
|
+
import {
|
|
7
|
+
useTokenColor
|
|
8
|
+
} from "./chunk-Y44MEKJV.js";
|
|
9
|
+
import {
|
|
10
|
+
useMap
|
|
11
|
+
} from "./chunk-JYFERXZT.js";
|
|
12
|
+
|
|
13
|
+
// src/map-route/map-route.tsx
|
|
14
|
+
import { useEffect, useId, useMemo } from "react";
|
|
15
|
+
function MapRoute({
|
|
16
|
+
id: propId,
|
|
17
|
+
coordinates,
|
|
18
|
+
color,
|
|
19
|
+
width = 3,
|
|
20
|
+
opacity = 0.8,
|
|
21
|
+
dashArray,
|
|
22
|
+
direction = "none",
|
|
23
|
+
directionSpacing = 80,
|
|
24
|
+
onClick,
|
|
25
|
+
onMouseEnter,
|
|
26
|
+
onMouseLeave,
|
|
27
|
+
interactive = true
|
|
28
|
+
}) {
|
|
29
|
+
const { map, isLoaded, plan } = useMap();
|
|
30
|
+
const autoId = useId();
|
|
31
|
+
const id = propId ?? autoId;
|
|
32
|
+
const sourceId = `route-source-${id}`;
|
|
33
|
+
const layerId = `route-layer-${id}`;
|
|
34
|
+
const arrowLayerId = `route-arrows-${id}`;
|
|
35
|
+
const arrowImageId = routeArrowImageId(id);
|
|
36
|
+
const primary = useTokenColor("--primary");
|
|
37
|
+
const lineColor = color ?? primary;
|
|
38
|
+
const resolvedCoordinates = useMemo(
|
|
39
|
+
() => plan ? coordinates.map((point) => plan.toLngLat(point)) : coordinates,
|
|
40
|
+
[plan, coordinates]
|
|
41
|
+
);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!isLoaded || !map) return;
|
|
44
|
+
map.addSource(sourceId, {
|
|
45
|
+
type: "geojson",
|
|
46
|
+
data: {
|
|
47
|
+
type: "Feature",
|
|
48
|
+
properties: {},
|
|
49
|
+
geometry: { type: "LineString", coordinates: [] }
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
map.addLayer({
|
|
53
|
+
id: layerId,
|
|
54
|
+
type: "line",
|
|
55
|
+
source: sourceId,
|
|
56
|
+
layout: { "line-join": "round", "line-cap": "round" },
|
|
57
|
+
paint: {
|
|
58
|
+
"line-color": lineColor,
|
|
59
|
+
"line-width": width,
|
|
60
|
+
"line-opacity": opacity,
|
|
61
|
+
...dashArray && { "line-dasharray": dashArray }
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return () => {
|
|
65
|
+
try {
|
|
66
|
+
if (map.getLayer(layerId)) map.removeLayer(layerId);
|
|
67
|
+
if (map.getSource(sourceId)) map.removeSource(sourceId);
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}, [isLoaded, map]);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!isLoaded || !map || resolvedCoordinates.length < 2) return;
|
|
74
|
+
const source = map.getSource(sourceId);
|
|
75
|
+
if (source) {
|
|
76
|
+
source.setData({
|
|
77
|
+
type: "Feature",
|
|
78
|
+
properties: {},
|
|
79
|
+
geometry: { type: "LineString", coordinates: resolvedCoordinates }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}, [isLoaded, map, resolvedCoordinates, sourceId]);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!isLoaded || !map || direction === "none") return;
|
|
85
|
+
const image = createRouteArrowImage(lineColor);
|
|
86
|
+
if (!image) return;
|
|
87
|
+
let cancelled = false;
|
|
88
|
+
const register = () => {
|
|
89
|
+
try {
|
|
90
|
+
if (map.hasImage(arrowImageId)) {
|
|
91
|
+
map.updateImage(arrowImageId, image);
|
|
92
|
+
} else {
|
|
93
|
+
map.addImage(arrowImageId, image, { pixelRatio: 2 });
|
|
94
|
+
}
|
|
95
|
+
if (!map.getLayer(arrowLayerId)) {
|
|
96
|
+
map.addLayer({
|
|
97
|
+
id: arrowLayerId,
|
|
98
|
+
type: "symbol",
|
|
99
|
+
source: sourceId,
|
|
100
|
+
layout: {
|
|
101
|
+
"symbol-placement": "line",
|
|
102
|
+
"symbol-spacing": directionSpacing,
|
|
103
|
+
"icon-image": arrowImageId,
|
|
104
|
+
"icon-rotate": direction === "backward" ? 180 : 0,
|
|
105
|
+
"icon-rotation-alignment": "map",
|
|
106
|
+
"icon-allow-overlap": true,
|
|
107
|
+
"icon-ignore-placement": true
|
|
108
|
+
},
|
|
109
|
+
paint: { "icon-opacity": opacity }
|
|
110
|
+
});
|
|
111
|
+
} else {
|
|
112
|
+
map.setLayoutProperty(arrowLayerId, "symbol-spacing", directionSpacing);
|
|
113
|
+
map.setLayoutProperty(arrowLayerId, "icon-rotate", direction === "backward" ? 180 : 0);
|
|
114
|
+
map.setPaintProperty(arrowLayerId, "icon-opacity", opacity);
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
register();
|
|
120
|
+
const onStyleData = () => {
|
|
121
|
+
if (!cancelled) register();
|
|
122
|
+
};
|
|
123
|
+
map.on("styledata", onStyleData);
|
|
124
|
+
return () => {
|
|
125
|
+
cancelled = true;
|
|
126
|
+
map.off("styledata", onStyleData);
|
|
127
|
+
try {
|
|
128
|
+
if (map.getLayer(arrowLayerId)) map.removeLayer(arrowLayerId);
|
|
129
|
+
if (map.hasImage(arrowImageId)) map.removeImage(arrowImageId);
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}, [
|
|
134
|
+
isLoaded,
|
|
135
|
+
map,
|
|
136
|
+
direction,
|
|
137
|
+
directionSpacing,
|
|
138
|
+
lineColor,
|
|
139
|
+
opacity,
|
|
140
|
+
sourceId,
|
|
141
|
+
arrowLayerId,
|
|
142
|
+
arrowImageId
|
|
143
|
+
]);
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (!isLoaded || !map || !map.getLayer(layerId)) return;
|
|
146
|
+
map.setPaintProperty(layerId, "line-color", lineColor);
|
|
147
|
+
map.setPaintProperty(layerId, "line-width", width);
|
|
148
|
+
map.setPaintProperty(layerId, "line-opacity", opacity);
|
|
149
|
+
map.setPaintProperty(layerId, "line-dasharray", dashArray);
|
|
150
|
+
}, [isLoaded, map, layerId, lineColor, width, opacity, dashArray]);
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (!isLoaded || !map || !interactive) return;
|
|
153
|
+
const handleClick = () => {
|
|
154
|
+
onClick?.();
|
|
155
|
+
};
|
|
156
|
+
const handleMouseEnter = () => {
|
|
157
|
+
map.getCanvas().style.cursor = "pointer";
|
|
158
|
+
onMouseEnter?.();
|
|
159
|
+
};
|
|
160
|
+
const handleMouseLeave = () => {
|
|
161
|
+
map.getCanvas().style.cursor = "";
|
|
162
|
+
onMouseLeave?.();
|
|
163
|
+
};
|
|
164
|
+
map.on("click", layerId, handleClick);
|
|
165
|
+
map.on("mouseenter", layerId, handleMouseEnter);
|
|
166
|
+
map.on("mouseleave", layerId, handleMouseLeave);
|
|
167
|
+
return () => {
|
|
168
|
+
map.off("click", layerId, handleClick);
|
|
169
|
+
map.off("mouseenter", layerId, handleMouseEnter);
|
|
170
|
+
map.off("mouseleave", layerId, handleMouseLeave);
|
|
171
|
+
};
|
|
172
|
+
}, [isLoaded, map, layerId, onClick, onMouseEnter, onMouseLeave, interactive]);
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export {
|
|
177
|
+
MapRoute
|
|
178
|
+
};
|
|
179
|
+
//# sourceMappingURL=chunk-CJJT6AEI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/map-route/map-route.tsx"],"sourcesContent":["\"use client\";\n\nimport type MapLibreGL from \"maplibre-gl\";\nimport { useEffect, useId, useMemo } from \"react\";\n\nimport { useMap } from \"../map-canvas/map-context\";\nimport { useTokenColor } from \"../lib/use-token-color\";\nimport { createRouteArrowImage, routeArrowImageId } from \"../lib/route-arrow\";\n\n/** Which way the route runs, drawn as chevrons along the line. */\nexport type MapRouteDirection = \"none\" | \"forward\" | \"backward\";\n\nexport interface MapRouteProps {\n /** Optional unique identifier for the route layer. */\n id?: string;\n /**\n * Array of [longitude, latitude] coordinate pairs defining the route — or, in\n * a `<MapCanvas plan>`, [x, y] pairs in plan units.\n */\n coordinates: [number, number][];\n /** Line color as a CSS color value. Defaults to the theme's `--primary` token. */\n color?: string;\n /** Line width in pixels (default: 3). */\n width?: number;\n /** Line opacity from 0 to 1 (default: 0.8). */\n opacity?: number;\n /** Dash pattern [dash length, gap length] for dashed lines. */\n dashArray?: [number, number];\n /**\n * Draw chevrons along the line to show which way it runs (default: \"none\").\n * A conveyor, a one-way aisle and a walking route all need this, and an arrow\n * is a second channel that greyscale keeps.\n */\n direction?: MapRouteDirection;\n /** Distance between chevrons in px (default: 80). */\n directionSpacing?: number;\n /** Callback when the route line is clicked. */\n onClick?: () => void;\n /** Callback when the mouse enters the route line. */\n onMouseEnter?: () => void;\n /** Callback when the mouse leaves the route line. */\n onMouseLeave?: () => void;\n /** Whether the route is interactive — shows a pointer cursor on hover (default: true). */\n interactive?: boolean;\n}\n\n/** A GeoJSON line layer for routes/paths. Renders nothing itself — it draws on the map. */\nexport function MapRoute({\n id: propId,\n coordinates,\n color,\n width = 3,\n opacity = 0.8,\n dashArray,\n direction = \"none\",\n directionSpacing = 80,\n onClick,\n onMouseEnter,\n onMouseLeave,\n interactive = true,\n}: MapRouteProps) {\n const { map, isLoaded, plan } = useMap();\n const autoId = useId();\n const id = propId ?? autoId;\n const sourceId = `route-source-${id}`;\n const layerId = `route-layer-${id}`;\n const arrowLayerId = `route-arrows-${id}`;\n const arrowImageId = routeArrowImageId(id);\n\n const primary = useTokenColor(\"--primary\");\n const lineColor = color ?? primary;\n\n // On a plan map the pairs are plan units; on a geographic one they are handed\n // through untouched, identity included.\n const resolvedCoordinates = useMemo(\n () => (plan ? coordinates.map((point) => plan.toLngLat(point)) : coordinates),\n [plan, coordinates],\n );\n\n // Add source and layer on mount.\n useEffect(() => {\n if (!isLoaded || !map) return;\n\n map.addSource(sourceId, {\n type: \"geojson\",\n data: {\n type: \"Feature\",\n properties: {},\n geometry: { type: \"LineString\", coordinates: [] },\n },\n });\n\n map.addLayer({\n id: layerId,\n type: \"line\",\n source: sourceId,\n layout: { \"line-join\": \"round\", \"line-cap\": \"round\" },\n paint: {\n \"line-color\": lineColor,\n \"line-width\": width,\n \"line-opacity\": opacity,\n ...(dashArray && { \"line-dasharray\": dashArray }),\n },\n });\n\n return () => {\n try {\n if (map.getLayer(layerId)) map.removeLayer(layerId);\n if (map.getSource(sourceId)) map.removeSource(sourceId);\n } catch {\n // style may be mid-reload\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- source/layer created once per map; data + paint are synced by the effects below\n }, [isLoaded, map]);\n\n // When coordinates change, update the source data.\n useEffect(() => {\n if (!isLoaded || !map || resolvedCoordinates.length < 2) return;\n\n const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource;\n if (source) {\n source.setData({\n type: \"Feature\",\n properties: {},\n geometry: { type: \"LineString\", coordinates: resolvedCoordinates },\n });\n }\n }, [isLoaded, map, resolvedCoordinates, sourceId]);\n\n // Direction chevrons: a generated icon, re-registered whenever the ink\n // changes, with the layer added only once the image really exists.\n useEffect(() => {\n if (!isLoaded || !map || direction === \"none\") return;\n\n const image = createRouteArrowImage(lineColor);\n if (!image) return;\n\n let cancelled = false;\n\n const register = () => {\n try {\n if (map.hasImage(arrowImageId)) {\n map.updateImage(arrowImageId, image as unknown as ImageData);\n } else {\n map.addImage(arrowImageId, image as unknown as ImageData, { pixelRatio: 2 });\n }\n if (!map.getLayer(arrowLayerId)) {\n map.addLayer({\n id: arrowLayerId,\n type: \"symbol\",\n source: sourceId,\n layout: {\n \"symbol-placement\": \"line\",\n \"symbol-spacing\": directionSpacing,\n \"icon-image\": arrowImageId,\n \"icon-rotate\": direction === \"backward\" ? 180 : 0,\n \"icon-rotation-alignment\": \"map\",\n \"icon-allow-overlap\": true,\n \"icon-ignore-placement\": true,\n },\n paint: { \"icon-opacity\": opacity },\n });\n } else {\n map.setLayoutProperty(arrowLayerId, \"symbol-spacing\", directionSpacing);\n map.setLayoutProperty(arrowLayerId, \"icon-rotate\", direction === \"backward\" ? 180 : 0);\n map.setPaintProperty(arrowLayerId, \"icon-opacity\", opacity);\n }\n } catch {\n // style mid-reload; `styledata` re-runs this\n }\n };\n\n register();\n // A theme swap calls setStyle, which drops every style image AND every layer\n // this component added.\n const onStyleData = () => {\n if (!cancelled) register();\n };\n map.on(\"styledata\", onStyleData);\n\n return () => {\n cancelled = true;\n map.off(\"styledata\", onStyleData);\n try {\n if (map.getLayer(arrowLayerId)) map.removeLayer(arrowLayerId);\n if (map.hasImage(arrowImageId)) map.removeImage(arrowImageId);\n } catch {\n // style may be mid-reload\n }\n };\n }, [\n isLoaded,\n map,\n direction,\n directionSpacing,\n lineColor,\n opacity,\n sourceId,\n arrowLayerId,\n arrowImageId,\n ]);\n\n // Sync paint when styling (or the resolved theme color) changes.\n useEffect(() => {\n if (!isLoaded || !map || !map.getLayer(layerId)) return;\n\n map.setPaintProperty(layerId, \"line-color\", lineColor);\n map.setPaintProperty(layerId, \"line-width\", width);\n map.setPaintProperty(layerId, \"line-opacity\", opacity);\n map.setPaintProperty(layerId, \"line-dasharray\", dashArray);\n }, [isLoaded, map, layerId, lineColor, width, opacity, dashArray]);\n\n // Handle click and hover events.\n useEffect(() => {\n if (!isLoaded || !map || !interactive) return;\n\n const handleClick = () => {\n onClick?.();\n };\n const handleMouseEnter = () => {\n map.getCanvas().style.cursor = \"pointer\";\n onMouseEnter?.();\n };\n const handleMouseLeave = () => {\n map.getCanvas().style.cursor = \"\";\n onMouseLeave?.();\n };\n\n map.on(\"click\", layerId, handleClick);\n map.on(\"mouseenter\", layerId, handleMouseEnter);\n map.on(\"mouseleave\", layerId, handleMouseLeave);\n\n return () => {\n map.off(\"click\", layerId, handleClick);\n map.off(\"mouseenter\", layerId, handleMouseEnter);\n map.off(\"mouseleave\", layerId, handleMouseLeave);\n };\n }, [isLoaded, map, layerId, onClick, onMouseEnter, onMouseLeave, interactive]);\n\n return null;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/lib/plan-status.ts
|
|
4
|
+
import { STATUS_TONE_ICONS } from "@elabs-ai/components-ui";
|
|
5
|
+
var PLAN_STATUSES = ["free", "occupied", "warning", "down"];
|
|
6
|
+
var DASH_ARRAYS = {
|
|
7
|
+
solid: void 0,
|
|
8
|
+
dotted: [1, 1.5],
|
|
9
|
+
dashed: [3, 2],
|
|
10
|
+
"dot-dash": [4, 1.5, 1, 1.5]
|
|
11
|
+
};
|
|
12
|
+
var PLAN_STATUS_ENCODING = Object.freeze(
|
|
13
|
+
{
|
|
14
|
+
free: {
|
|
15
|
+
tone: "success",
|
|
16
|
+
pattern: "none",
|
|
17
|
+
dash: "solid",
|
|
18
|
+
dashArray: DASH_ARRAYS.solid,
|
|
19
|
+
icon: void 0,
|
|
20
|
+
glyph: "none",
|
|
21
|
+
colorToken: "--success",
|
|
22
|
+
markClass: "bg-success",
|
|
23
|
+
textClass: "text-success-text"
|
|
24
|
+
},
|
|
25
|
+
occupied: {
|
|
26
|
+
tone: "neutral",
|
|
27
|
+
pattern: "diagonal",
|
|
28
|
+
dash: "dotted",
|
|
29
|
+
dashArray: DASH_ARRAYS.dotted,
|
|
30
|
+
icon: void 0,
|
|
31
|
+
glyph: "none",
|
|
32
|
+
colorToken: "--muted-foreground",
|
|
33
|
+
markClass: "bg-muted-foreground",
|
|
34
|
+
textClass: "text-muted-foreground"
|
|
35
|
+
},
|
|
36
|
+
warning: {
|
|
37
|
+
tone: "warning",
|
|
38
|
+
pattern: "cross",
|
|
39
|
+
dash: "dashed",
|
|
40
|
+
dashArray: DASH_ARRAYS.dashed,
|
|
41
|
+
icon: STATUS_TONE_ICONS.warning,
|
|
42
|
+
glyph: "clock",
|
|
43
|
+
colorToken: "--warning",
|
|
44
|
+
markClass: "bg-warning",
|
|
45
|
+
textClass: "text-warning-text"
|
|
46
|
+
},
|
|
47
|
+
down: {
|
|
48
|
+
tone: "destructive",
|
|
49
|
+
pattern: "dense",
|
|
50
|
+
dash: "dot-dash",
|
|
51
|
+
dashArray: DASH_ARRAYS["dot-dash"],
|
|
52
|
+
icon: STATUS_TONE_ICONS.destructive,
|
|
53
|
+
glyph: "alert",
|
|
54
|
+
colorToken: "--destructive",
|
|
55
|
+
markClass: "bg-destructive",
|
|
56
|
+
textClass: "text-destructive-text"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
var PLAN_FILL_OPACITY = Object.freeze({
|
|
61
|
+
rest: 0.16,
|
|
62
|
+
hover: 0.28,
|
|
63
|
+
selected: 0.36
|
|
64
|
+
});
|
|
65
|
+
function planStatusMatch(property, value, fallback) {
|
|
66
|
+
const expression = ["match", ["get", property]];
|
|
67
|
+
for (const status of PLAN_STATUSES) {
|
|
68
|
+
expression.push(status, value(PLAN_STATUS_ENCODING[status], status));
|
|
69
|
+
}
|
|
70
|
+
expression.push(fallback ?? value(PLAN_STATUS_ENCODING.occupied, "occupied"));
|
|
71
|
+
return expression;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
PLAN_STATUSES,
|
|
76
|
+
PLAN_STATUS_ENCODING,
|
|
77
|
+
PLAN_FILL_OPACITY,
|
|
78
|
+
planStatusMatch
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=chunk-E65Z3MSV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/plan-status.ts"],"sourcesContent":["/**\n * Status on a plan — free desk, busy machine, line in alarm — without leaning on\n * colour (WCAG 1.4.1).\n *\n * The MapLibre style spec decides how the channels split, and the split is not a\n * matter of taste: `fill-pattern` and `line-dasharray` accept `zoom` and\n * `feature` expressions but **not `feature-state`**, while `fill-color`,\n * `fill-opacity`, `line-color` and `line-width` accept all three. So:\n *\n * - **status** rides feature PROPERTIES — pattern and dash, which cannot read\n * feature-state anyway;\n * - **hover and selection** ride feature STATE — opacity, width and colour.\n *\n * The two can therefore never collide, and selection must never be expressed as\n * a dash change: a selected free room would read as an occupied one.\n *\n * In greyscale each status still differs by four textures, four outline styles\n * and, on the two states that must never be confused, a glyph — with the legend\n * and the table repeating all of it as words.\n */\nimport { STATUS_TONE_ICONS } from \"@elabs-ai/components-ui\";\nimport type { LucideIcon } from \"lucide-react\";\n\n/** What a region on a plan can be. */\nexport type PlanStatus = \"free\" | \"occupied\" | \"warning\" | \"down\";\n\n/** Every {@link PlanStatus}, in legend order. */\nexport const PLAN_STATUSES: readonly PlanStatus[] = [\"free\", \"occupied\", \"warning\", \"down\"];\n\n/** The generated hatch tile a status is textured with — see `lib/plan-patterns.ts`. */\nexport type PlanPatternKind = \"none\" | \"diagonal\" | \"cross\" | \"dense\";\n\n/** The outline style a status is drawn with: the second non-colour channel. */\nexport type PlanDash = \"solid\" | \"dotted\" | \"dashed\" | \"dot-dash\";\n\n/** MapLibre `line-dasharray` values, in line widths. `undefined` means solid. */\nconst DASH_ARRAYS: Record<PlanDash, number[] | undefined> = {\n solid: undefined,\n dotted: [1, 1.5],\n dashed: [3, 2],\n \"dot-dash\": [4, 1.5, 1, 1.5],\n};\n\nexport interface PlanStatusEncoding {\n /** The status tone, for the token rungs below. */\n tone: \"success\" | \"neutral\" | \"warning\" | \"destructive\";\n /** The hatch tile registered with `map.addImage`. */\n pattern: PlanPatternKind;\n /** The outline style. */\n dash: PlanDash;\n /** `line-dasharray` for the outline; `undefined` keeps it solid. */\n dashArray: number[] | undefined;\n /**\n * A glyph, on the two states that must never be confused with a working one.\n * `undefined` elsewhere: a glyph on every neutral region is noise.\n */\n icon: LucideIcon | undefined;\n /** Stable glyph name, so a test can assert the four states read apart. */\n glyph: \"none\" | \"clock\" | \"alert\";\n /** The token the shape's own ink resolves from (the ≥3:1 mark rung). */\n colorToken: string;\n /** Mark (fill-rung) utility, for a legend swatch. */\n markClass: string;\n /** Ink (`-text` rung) utility, for coloured text on an ordinary surface. */\n textClass: string;\n}\n\n/**\n * The one table every plan surface reads status from — map paint, legend, table\n * and readout alike. Frozen, so a surface cannot quietly drift from the others.\n */\nexport const PLAN_STATUS_ENCODING: Readonly<Record<PlanStatus, PlanStatusEncoding>> = Object.freeze(\n {\n free: {\n tone: \"success\",\n pattern: \"none\",\n dash: \"solid\",\n dashArray: DASH_ARRAYS.solid,\n icon: undefined,\n glyph: \"none\",\n colorToken: \"--success\",\n markClass: \"bg-success\",\n textClass: \"text-success-text\",\n },\n occupied: {\n tone: \"neutral\",\n pattern: \"diagonal\",\n dash: \"dotted\",\n dashArray: DASH_ARRAYS.dotted,\n icon: undefined,\n glyph: \"none\",\n colorToken: \"--muted-foreground\",\n markClass: \"bg-muted-foreground\",\n textClass: \"text-muted-foreground\",\n },\n warning: {\n tone: \"warning\",\n pattern: \"cross\",\n dash: \"dashed\",\n dashArray: DASH_ARRAYS.dashed,\n icon: STATUS_TONE_ICONS.warning,\n glyph: \"clock\",\n colorToken: \"--warning\",\n markClass: \"bg-warning\",\n textClass: \"text-warning-text\",\n },\n down: {\n tone: \"destructive\",\n pattern: \"dense\",\n dash: \"dot-dash\",\n dashArray: DASH_ARRAYS[\"dot-dash\"],\n icon: STATUS_TONE_ICONS.destructive,\n glyph: \"alert\",\n colorToken: \"--destructive\",\n markClass: \"bg-destructive\",\n textClass: \"text-destructive-text\",\n },\n },\n);\n\n/** Fill opacity per interaction state — the feature-state channel, in one place. */\nexport const PLAN_FILL_OPACITY = Object.freeze({\n rest: 0.16,\n hover: 0.28,\n selected: 0.36,\n});\n\n/**\n * A MapLibre `match` expression over a status property, so one paint value can\n * serve every status without a layer per status.\n *\n * ```ts\n * \"line-dasharray\": planStatusMatch(\"status\", (encoding) => encoding.dashArray ?? [1, 0])\n * ```\n */\nexport function planStatusMatch<T>(\n property: string,\n value: (encoding: PlanStatusEncoding, status: PlanStatus) => T,\n fallback?: T,\n): unknown[] {\n const expression: unknown[] = [\"match\", [\"get\", property]];\n for (const status of PLAN_STATUSES) {\n expression.push(status, value(PLAN_STATUS_ENCODING[status], status));\n }\n expression.push(fallback ?? value(PLAN_STATUS_ENCODING.occupied, \"occupied\"));\n return expression;\n}\n"],"mappings":";;;AAoBA,SAAS,yBAAyB;AAO3B,IAAM,gBAAuC,CAAC,QAAQ,YAAY,WAAW,MAAM;AAS1F,IAAM,cAAsD;AAAA,EAC1D,OAAO;AAAA,EACP,QAAQ,CAAC,GAAG,GAAG;AAAA,EACf,QAAQ,CAAC,GAAG,CAAC;AAAA,EACb,YAAY,CAAC,GAAG,KAAK,GAAG,GAAG;AAC7B;AA8BO,IAAM,uBAAyE,OAAO;AAAA,EAC3F;AAAA,IACE,MAAM;AAAA,MACJ,MAAM;AAAA,MACN,SAAS;AAAA,MACT,MAAM;AAAA,MACN,WAAW,YAAY;AAAA,MACvB,MAAM;AAAA,MACN,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,WAAW;AAAA,IACb;AAAA,IACA,UAAU;AAAA,MACR,MAAM;AAAA,MACN,SAAS;AAAA,MACT,MAAM;AAAA,MACN,WAAW,YAAY;AAAA,MACvB,MAAM;AAAA,MACN,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,WAAW;AAAA,IACb;AAAA,IACA,SAAS;AAAA,MACP,MAAM;AAAA,MACN,SAAS;AAAA,MACT,MAAM;AAAA,MACN,WAAW,YAAY;AAAA,MACvB,MAAM,kBAAkB;AAAA,MACxB,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,WAAW;AAAA,IACb;AAAA,IACA,MAAM;AAAA,MACJ,MAAM;AAAA,MACN,SAAS;AAAA,MACT,MAAM;AAAA,MACN,WAAW,YAAY,UAAU;AAAA,MACjC,MAAM,kBAAkB;AAAA,MACxB,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,WAAW;AAAA,IACb;AAAA,EACF;AACF;AAGO,IAAM,oBAAoB,OAAO,OAAO;AAAA,EAC7C,MAAM;AAAA,EACN,OAAO;AAAA,EACP,UAAU;AACZ,CAAC;AAUM,SAAS,gBACd,UACA,OACA,UACW;AACX,QAAM,aAAwB,CAAC,SAAS,CAAC,OAAO,QAAQ,CAAC;AACzD,aAAW,UAAU,eAAe;AAClC,eAAW,KAAK,QAAQ,MAAM,qBAAqB,MAAM,GAAG,MAAM,CAAC;AAAA,EACrE;AACA,aAAW,KAAK,YAAY,MAAM,qBAAqB,UAAU,UAAU,CAAC;AAC5E,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
MAP_CORNER_CLASSES
|
|
4
|
+
} from "./chunk-GWULZOMW.js";
|
|
5
|
+
import {
|
|
6
|
+
useMap
|
|
7
|
+
} from "./chunk-JYFERXZT.js";
|
|
8
|
+
|
|
9
|
+
// src/map-north-arrow/map-north-arrow.tsx
|
|
10
|
+
import { forwardRef, useEffect, useRef, useState } from "react";
|
|
11
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
12
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
var NORTH_UP_TOLERANCE = 0.5;
|
|
14
|
+
function normalizeBearing(bearing) {
|
|
15
|
+
const b = (bearing % 360 + 360) % 360;
|
|
16
|
+
return b > 180 ? b - 360 : b;
|
|
17
|
+
}
|
|
18
|
+
var MapNorthArrow = forwardRef(function MapNorthArrow2({ position = "top-left", label = "North", className, ...props }, ref) {
|
|
19
|
+
const { map } = useMap();
|
|
20
|
+
const glyphRef = useRef(null);
|
|
21
|
+
const [rotated, setRotated] = useState(false);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!map) return void 0;
|
|
24
|
+
const update = () => {
|
|
25
|
+
const bearing = normalizeBearing(map.getBearing());
|
|
26
|
+
setRotated(Math.abs(bearing) > NORTH_UP_TOLERANCE);
|
|
27
|
+
if (glyphRef.current) glyphRef.current.style.transform = `rotate(${-bearing}deg)`;
|
|
28
|
+
};
|
|
29
|
+
update();
|
|
30
|
+
map.on("rotate", update);
|
|
31
|
+
return () => {
|
|
32
|
+
map.off("rotate", update);
|
|
33
|
+
};
|
|
34
|
+
}, [map]);
|
|
35
|
+
return /* @__PURE__ */ jsx(
|
|
36
|
+
"div",
|
|
37
|
+
{
|
|
38
|
+
ref,
|
|
39
|
+
role: "img",
|
|
40
|
+
"aria-label": label,
|
|
41
|
+
"data-slot": "map-north-arrow",
|
|
42
|
+
hidden: !rotated,
|
|
43
|
+
className: cn(
|
|
44
|
+
"pointer-events-none absolute z-10 flex size-9 items-center justify-center rounded-full bg-background/80 text-foreground",
|
|
45
|
+
MAP_CORNER_CLASSES[position],
|
|
46
|
+
className
|
|
47
|
+
),
|
|
48
|
+
...props,
|
|
49
|
+
children: /* @__PURE__ */ jsxs("svg", { ref: glyphRef, viewBox: "0 0 24 24", "aria-hidden": "true", className: "size-7", children: [
|
|
50
|
+
/* @__PURE__ */ jsx("path", { d: "M12 3 L16 13 L12 11 Z", fill: "currentColor" }),
|
|
51
|
+
/* @__PURE__ */ jsx("path", { d: "M12 3 L8 13 L12 11 Z", fill: "currentColor", opacity: 0.55 }),
|
|
52
|
+
/* @__PURE__ */ jsx(
|
|
53
|
+
"path",
|
|
54
|
+
{
|
|
55
|
+
d: "M9.5 21 V15.5 L14.5 21 V15.5",
|
|
56
|
+
fill: "none",
|
|
57
|
+
stroke: "currentColor",
|
|
58
|
+
strokeWidth: 1.5,
|
|
59
|
+
strokeLinejoin: "round"
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
] })
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export {
|
|
68
|
+
MapNorthArrow
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=chunk-E7VPNINZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/map-north-arrow/map-north-arrow.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, useEffect, useRef, useState, type HTMLAttributes } from \"react\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nimport { useMap } from \"../map-canvas/map-context\";\nimport { MAP_CORNER_CLASSES, type MapCorner } from \"../map-legend/map-corner\";\n\n/** Below this rotation (degrees) the map counts as north-up and the arrow stays away. */\nconst NORTH_UP_TOLERANCE = 0.5;\n\n/** A bearing folded into (-180, 180]. */\nfunction normalizeBearing(bearing: number): number {\n const b = ((bearing % 360) + 360) % 360;\n return b > 180 ? b - 360 : b;\n}\n\nexport interface MapNorthArrowProps extends HTMLAttributes<HTMLDivElement> {\n /** Corner of the map (default `\"top-left\"`). */\n position?: MapCorner;\n /** The arrow's accessible name (default `\"North\"`). */\n label?: string;\n}\n\n/**\n * A north arrow that appears only while the map is rotated (bearing ≠ 0) and\n * turns with it — a north-up map needs none. Static furniture, not a control:\n * `<MapControls showCompass>` is the button that resets the bearing. Render\n * inside `<MapCanvas>`.\n */\nexport const MapNorthArrow = forwardRef<HTMLDivElement, MapNorthArrowProps>(function MapNorthArrow(\n { position = \"top-left\", label = \"North\", className, ...props },\n ref,\n) {\n const { map } = useMap();\n const glyphRef = useRef<SVGSVGElement>(null);\n const [rotated, setRotated] = useState(false);\n\n useEffect(() => {\n if (!map) return undefined;\n const update = () => {\n const bearing = normalizeBearing(map.getBearing());\n setRotated(Math.abs(bearing) > NORTH_UP_TOLERANCE);\n if (glyphRef.current) glyphRef.current.style.transform = `rotate(${-bearing}deg)`;\n };\n update();\n map.on(\"rotate\", update);\n return () => {\n map.off(\"rotate\", update);\n };\n }, [map]);\n\n return (\n <div\n ref={ref}\n role=\"img\"\n aria-label={label}\n data-slot=\"map-north-arrow\"\n hidden={!rotated}\n className={cn(\n \"pointer-events-none absolute z-10 flex size-9 items-center justify-center rounded-full bg-background/80 text-foreground\",\n MAP_CORNER_CLASSES[position],\n className,\n )}\n {...props}\n >\n <svg ref={glyphRef} viewBox=\"0 0 24 24\" aria-hidden=\"true\" className=\"size-7\">\n {/* The arrow: dark north half, hollow south half. */}\n <path d=\"M12 3 L16 13 L12 11 Z\" fill=\"currentColor\" />\n <path d=\"M12 3 L8 13 L12 11 Z\" fill=\"currentColor\" opacity={0.55} />\n {/* The cartographic \"N\", drawn as strokes so it is not locale text. */}\n <path\n d=\"M9.5 21 V15.5 L14.5 21 V15.5\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.5}\n strokeLinejoin=\"round\"\n />\n </svg>\n </div>\n );\n});\n"],"mappings":";;;;;;;;;AAEA,SAAS,YAAY,WAAW,QAAQ,gBAAqC;AAC7E,SAAS,UAAU;AA+Db,SAEE,KAFF;AAzDN,IAAM,qBAAqB;AAG3B,SAAS,iBAAiB,SAAyB;AACjD,QAAM,KAAM,UAAU,MAAO,OAAO;AACpC,SAAO,IAAI,MAAM,IAAI,MAAM;AAC7B;AAeO,IAAM,gBAAgB,WAA+C,SAASA,eACnF,EAAE,WAAW,YAAY,QAAQ,SAAS,WAAW,GAAG,MAAM,GAC9D,KACA;AACA,QAAM,EAAE,IAAI,IAAI,OAAO;AACvB,QAAM,WAAW,OAAsB,IAAI;AAC3C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAE5C,YAAU,MAAM;AACd,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,SAAS,MAAM;AACnB,YAAM,UAAU,iBAAiB,IAAI,WAAW,CAAC;AACjD,iBAAW,KAAK,IAAI,OAAO,IAAI,kBAAkB;AACjD,UAAI,SAAS,QAAS,UAAS,QAAQ,MAAM,YAAY,UAAU,CAAC,OAAO;AAAA,IAC7E;AACA,WAAO;AACP,QAAI,GAAG,UAAU,MAAM;AACvB,WAAO,MAAM;AACX,UAAI,IAAI,UAAU,MAAM;AAAA,IAC1B;AAAA,EACF,GAAG,CAAC,GAAG,CAAC;AAER,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,aAAU;AAAA,MACV,QAAQ,CAAC;AAAA,MACT,WAAW;AAAA,QACT;AAAA,QACA,mBAAmB,QAAQ;AAAA,QAC3B;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ,+BAAC,SAAI,KAAK,UAAU,SAAQ,aAAY,eAAY,QAAO,WAAU,UAEnE;AAAA,4BAAC,UAAK,GAAE,yBAAwB,MAAK,gBAAe;AAAA,QACpD,oBAAC,UAAK,GAAE,wBAAuB,MAAK,gBAAe,SAAS,MAAM;AAAA,QAElE;AAAA,UAAC;AAAA;AAAA,YACC,GAAE;AAAA,YACF,MAAK;AAAA,YACL,QAAO;AAAA,YACP,aAAa;AAAA,YACb,gBAAe;AAAA;AAAA,QACjB;AAAA,SACF;AAAA;AAAA,EACF;AAEJ,CAAC;","names":["MapNorthArrow"]}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
PLAN_STATUSES,
|
|
4
|
+
PLAN_STATUS_ENCODING
|
|
5
|
+
} from "./chunk-E65Z3MSV.js";
|
|
6
|
+
|
|
7
|
+
// src/lib/plan-patterns.ts
|
|
8
|
+
import { useEffect } from "react";
|
|
9
|
+
var TILE = 16;
|
|
10
|
+
var INK_ALPHA = 0.55;
|
|
11
|
+
function planPatternImageId(kind) {
|
|
12
|
+
return `plan-pattern-${kind}`;
|
|
13
|
+
}
|
|
14
|
+
var OVERSHOOT = TILE / 2;
|
|
15
|
+
function RISING(spacing) {
|
|
16
|
+
const lines = [];
|
|
17
|
+
for (let c = 0; c <= TILE * 2; c += spacing) {
|
|
18
|
+
lines.push([-OVERSHOOT, c + OVERSHOOT, TILE + OVERSHOOT, c - TILE - OVERSHOOT]);
|
|
19
|
+
}
|
|
20
|
+
return lines;
|
|
21
|
+
}
|
|
22
|
+
function FALLING(spacing) {
|
|
23
|
+
return RISING(spacing).map(
|
|
24
|
+
([x1, y1, x2, y2]) => [x1, TILE - y1, x2, TILE - y2]
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
function strokeLines(context, lines, width) {
|
|
28
|
+
context.lineWidth = width;
|
|
29
|
+
context.lineCap = "square";
|
|
30
|
+
for (const [x1, y1, x2, y2] of lines) {
|
|
31
|
+
context.beginPath();
|
|
32
|
+
context.moveTo(x1, y1);
|
|
33
|
+
context.lineTo(x2, y2);
|
|
34
|
+
context.stroke();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function createPlanPatternTile(kind, ink) {
|
|
38
|
+
if (kind === "none") return null;
|
|
39
|
+
if (typeof document === "undefined") return null;
|
|
40
|
+
const canvas = document.createElement("canvas");
|
|
41
|
+
canvas.width = TILE;
|
|
42
|
+
canvas.height = TILE;
|
|
43
|
+
const context = canvas.getContext("2d");
|
|
44
|
+
if (!context) return null;
|
|
45
|
+
context.clearRect(0, 0, TILE, TILE);
|
|
46
|
+
context.globalAlpha = INK_ALPHA;
|
|
47
|
+
context.strokeStyle = ink;
|
|
48
|
+
if (kind === "diagonal") {
|
|
49
|
+
strokeLines(context, RISING(TILE), 1.5);
|
|
50
|
+
} else if (kind === "cross") {
|
|
51
|
+
strokeLines(context, [...RISING(TILE), ...FALLING(TILE)], 1.25);
|
|
52
|
+
} else {
|
|
53
|
+
strokeLines(context, RISING(TILE / 2), 1.25);
|
|
54
|
+
}
|
|
55
|
+
return canvas;
|
|
56
|
+
}
|
|
57
|
+
function usedPatternKinds() {
|
|
58
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
59
|
+
for (const status of PLAN_STATUSES) kinds.add(PLAN_STATUS_ENCODING[status].pattern);
|
|
60
|
+
kinds.delete("none");
|
|
61
|
+
return [...kinds];
|
|
62
|
+
}
|
|
63
|
+
function usePlanPatterns(map, isLoaded, ink) {
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!map || !isLoaded) return;
|
|
66
|
+
const register = () => {
|
|
67
|
+
for (const kind of usedPatternKinds()) {
|
|
68
|
+
const tile = createPlanPatternTile(kind, ink);
|
|
69
|
+
if (!tile) continue;
|
|
70
|
+
const id = planPatternImageId(kind);
|
|
71
|
+
const image = {
|
|
72
|
+
width: tile.width,
|
|
73
|
+
height: tile.height,
|
|
74
|
+
data: tile.getContext("2d")?.getImageData(0, 0, tile.width, tile.height).data
|
|
75
|
+
};
|
|
76
|
+
if (!image.data) continue;
|
|
77
|
+
try {
|
|
78
|
+
if (map.hasImage(id)) {
|
|
79
|
+
map.updateImage(id, image);
|
|
80
|
+
} else {
|
|
81
|
+
map.addImage(id, image, { pixelRatio: 2 });
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
register();
|
|
88
|
+
map.on("styledata", register);
|
|
89
|
+
return () => {
|
|
90
|
+
map.off("styledata", register);
|
|
91
|
+
};
|
|
92
|
+
}, [map, isLoaded, ink]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
planPatternImageId,
|
|
97
|
+
createPlanPatternTile,
|
|
98
|
+
usePlanPatterns
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=chunk-FHV5IA6N.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/plan-patterns.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect } from \"react\";\nimport type MapLibreGL from \"maplibre-gl\";\n\nimport { PLAN_STATUSES, PLAN_STATUS_ENCODING, type PlanPatternKind } from \"./plan-status\";\n\n/**\n * Seamless hatch tiles for the status channel, generated on a canvas and handed\n * to `map.addImage`. Generated rather than fetched: a blank plan style has no\n * sprite and no glyph endpoint, and a texture that needs the network is a\n * texture that sometimes is not there.\n *\n * The tile geometry mirrors `SeriesPatternKind` in\n * `packages/charts/src/charts/series-pattern.tsx`, so a factory plan and a\n * chart of the same data are textured alike. It is deliberately a copy: the\n * one-way dependency graph forbids maps importing charts.\n */\n\n/** Tile edge, in px. Small enough to read at fit zoom, large enough not to moiré. */\nconst TILE = 16;\n/** Ink alpha. The pattern is a texture over a tone wash, not a second fill. */\nconst INK_ALPHA = 0.55;\n\n/** Image ids are stable and namespaced, so two plans on one map cannot collide. */\nexport function planPatternImageId(kind: PlanPatternKind): string {\n return `plan-pattern-${kind}`;\n}\n\n/**\n * A hatch at `spacing` px, as segments that overshoot the tile on both ends.\n *\n * The lines are laid out so the set repeats under a TILE-sized shift in either\n * axis — that, not the overshoot alone, is what makes the tile seamless. Each\n * line is written as the two points where it meets the tile's own axes, so one\n * of them always crosses the tile's middle rather than clipping a corner: a\n * corner-only line paints nothing and leaves the status untextured.\n *\n * `RISING` runs bottom-left to top-right (canvas y grows downward, so these are\n * the `x + y = c` lines); `FALLING` is its mirror, and the two together are the\n * crosshatch.\n */\nconst OVERSHOOT = TILE / 2;\n\nfunction RISING(spacing: number): [number, number, number, number][] {\n const lines: [number, number, number, number][] = [];\n for (let c = 0; c <= TILE * 2; c += spacing) {\n lines.push([-OVERSHOOT, c + OVERSHOOT, TILE + OVERSHOOT, c - TILE - OVERSHOOT]);\n }\n return lines;\n}\n\nfunction FALLING(spacing: number): [number, number, number, number][] {\n return RISING(spacing).map(\n ([x1, y1, x2, y2]) => [x1, TILE - y1, x2, TILE - y2] as [number, number, number, number],\n );\n}\n\nfunction strokeLines(\n context: CanvasRenderingContext2D,\n lines: [number, number, number, number][],\n width: number,\n) {\n context.lineWidth = width;\n context.lineCap = \"square\";\n for (const [x1, y1, x2, y2] of lines) {\n context.beginPath();\n context.moveTo(x1, y1);\n context.lineTo(x2, y2);\n context.stroke();\n }\n}\n\n/**\n * Draw one seamless tile. Returns `null` for `\"none\"`, and for any environment\n * with no 2D context (jsdom) — the dash channel then carries the meaning alone,\n * which is why status has two non-colour channels rather than one.\n */\nexport function createPlanPatternTile(\n kind: PlanPatternKind,\n ink: string,\n): HTMLCanvasElement | null {\n if (kind === \"none\") return null;\n if (typeof document === \"undefined\") return null;\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = TILE;\n canvas.height = TILE;\n const context = canvas.getContext(\"2d\");\n if (!context) return null;\n\n context.clearRect(0, 0, TILE, TILE);\n context.globalAlpha = INK_ALPHA;\n context.strokeStyle = ink;\n\n if (kind === \"diagonal\") {\n strokeLines(context, RISING(TILE), 1.5);\n } else if (kind === \"cross\") {\n strokeLines(context, [...RISING(TILE), ...FALLING(TILE)], 1.25);\n } else {\n // dense: half the spacing of `diagonal`, so the two never read alike.\n strokeLines(context, RISING(TILE / 2), 1.25);\n }\n\n return canvas;\n}\n\n/** Every pattern kind the status table uses, deduplicated. */\nfunction usedPatternKinds(): PlanPatternKind[] {\n const kinds = new Set<PlanPatternKind>();\n for (const status of PLAN_STATUSES) kinds.add(PLAN_STATUS_ENCODING[status].pattern);\n kinds.delete(\"none\");\n return [...kinds];\n}\n\n/**\n * Register (and re-register) the status textures on a map.\n *\n * Re-registration is not optional: `map.setStyle` — which a theme swap performs —\n * drops every image the style holds, and a `fill-pattern` naming a missing image\n * renders nothing at all. So the images are (re)added on `styledata` as well as\n * whenever the ink changes.\n *\n * @param ink The colour to draw the hatch in, already resolved from a token.\n */\nexport function usePlanPatterns(map: MapLibreGL.Map | null, isLoaded: boolean, ink: string) {\n useEffect(() => {\n if (!map || !isLoaded) return;\n\n const register = () => {\n for (const kind of usedPatternKinds()) {\n const tile = createPlanPatternTile(kind, ink);\n if (!tile) continue;\n const id = planPatternImageId(kind);\n const image = {\n width: tile.width,\n height: tile.height,\n data: tile.getContext(\"2d\")?.getImageData(0, 0, tile.width, tile.height).data,\n };\n if (!image.data) continue;\n try {\n if (map.hasImage(id)) {\n map.updateImage(id, image as unknown as ImageData);\n } else {\n map.addImage(id, image as unknown as ImageData, { pixelRatio: 2 });\n }\n } catch {\n // style mid-reload; the `styledata` handler below re-runs this\n }\n }\n };\n\n register();\n map.on(\"styledata\", register);\n\n return () => {\n map.off(\"styledata\", register);\n };\n }, [map, isLoaded, ink]);\n}\n"],"mappings":"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","names":[]}
|