@elabs-ai/components-maps 4.2.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,176 @@
|
|
|
1
|
+
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
vi.mock("maplibre-gl", async () => {
|
|
5
|
+
const { createMaplibreMock } = await import("../test-utils/maplibre-mock");
|
|
6
|
+
return createMaplibreMock();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
import { MockMap, resetMaplibreMock } from "../test-utils/maplibre-mock";
|
|
10
|
+
import { MapCanvas } from "../map-canvas";
|
|
11
|
+
import { createPlanCrs } from "../lib/plan-crs";
|
|
12
|
+
import { resetMapWarnings } from "../lib/warn-once";
|
|
13
|
+
import { MapPlanImage } from "./map-plan-image";
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
cleanup();
|
|
17
|
+
resetMaplibreMock();
|
|
18
|
+
resetMapWarnings();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const PLAN = { width: 1600, height: 900 };
|
|
22
|
+
const SRC = "https://plans.example.com/level-3.png";
|
|
23
|
+
|
|
24
|
+
function lastMap() {
|
|
25
|
+
return MockMap.instances.at(-1)!;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("MapPlanImage", () => {
|
|
29
|
+
it("covers the whole plan extent without being told any coordinates", async () => {
|
|
30
|
+
render(
|
|
31
|
+
<MapCanvas plan={PLAN}>
|
|
32
|
+
<MapPlanImage id="floor" src={SRC} alt="Level 3 floor plan" />
|
|
33
|
+
</MapCanvas>,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const map = await waitFor(() => {
|
|
37
|
+
const instance = lastMap();
|
|
38
|
+
expect(instance.getSource("plan-image-source-floor")).toBeDefined();
|
|
39
|
+
return instance;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const source = map.getSource("plan-image-source-floor")!;
|
|
43
|
+
expect(source.spec.type).toBe("image");
|
|
44
|
+
expect(source.spec.url).toBe(SRC);
|
|
45
|
+
expect(source.spec.coordinates).toEqual(createPlanCrs(PLAN).imageCoordinates);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("places one wing of a plan from an extent in plan units", async () => {
|
|
49
|
+
render(
|
|
50
|
+
<MapCanvas plan={PLAN}>
|
|
51
|
+
<MapPlanImage id="wing" src={SRC} extent={{ x: 100, y: 50, width: 400, height: 200 }} />
|
|
52
|
+
</MapCanvas>,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const map = await waitFor(() => {
|
|
56
|
+
const instance = lastMap();
|
|
57
|
+
expect(instance.getSource("plan-image-source-wing")).toBeDefined();
|
|
58
|
+
return instance;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const crs = createPlanCrs(PLAN);
|
|
62
|
+
expect(map.getSource("plan-image-source-wing")!.spec.coordinates).toEqual([
|
|
63
|
+
crs.toLngLat({ x: 100, y: 50 }),
|
|
64
|
+
crs.toLngLat({ x: 500, y: 50 }),
|
|
65
|
+
crs.toLngLat({ x: 500, y: 250 }),
|
|
66
|
+
crs.toLngLat({ x: 100, y: 250 }),
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("draws the picture under the data and never cross-fades a floor swap", async () => {
|
|
71
|
+
render(
|
|
72
|
+
<MapCanvas plan={PLAN}>
|
|
73
|
+
<MapPlanImage id="floor" src={SRC} opacity={0.35} resampling="nearest" />
|
|
74
|
+
</MapCanvas>,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const map = await waitFor(() => {
|
|
78
|
+
const instance = lastMap();
|
|
79
|
+
expect(instance.getLayer("plan-image-layer-floor")).toBeDefined();
|
|
80
|
+
return instance;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const layer = map.getLayer("plan-image-layer-floor");
|
|
84
|
+
expect(layer.type).toBe("raster");
|
|
85
|
+
expect(layer.paint["raster-opacity"]).toBe(0.35);
|
|
86
|
+
expect(layer.paint["raster-fade-duration"]).toBe(0);
|
|
87
|
+
expect(layer.paint["raster-resampling"]).toBe("nearest");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("swaps the picture in place instead of tearing the source down", async () => {
|
|
91
|
+
const { rerender } = render(
|
|
92
|
+
<MapCanvas plan={PLAN}>
|
|
93
|
+
<MapPlanImage id="floor" src={SRC} />
|
|
94
|
+
</MapCanvas>,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const map = await waitFor(() => {
|
|
98
|
+
const instance = lastMap();
|
|
99
|
+
expect(instance.getSource("plan-image-source-floor")).toBeDefined();
|
|
100
|
+
return instance;
|
|
101
|
+
});
|
|
102
|
+
const source = map.getSource("plan-image-source-floor")!;
|
|
103
|
+
|
|
104
|
+
rerender(
|
|
105
|
+
<MapCanvas plan={PLAN}>
|
|
106
|
+
<MapPlanImage id="floor" src="https://plans.example.com/level-4.png" />
|
|
107
|
+
</MapCanvas>,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
await waitFor(() => expect(source.updateImageCalls).toHaveLength(1));
|
|
111
|
+
expect((source.updateImageCalls[0] as { url: string }).url).toBe(
|
|
112
|
+
"https://plans.example.com/level-4.png",
|
|
113
|
+
);
|
|
114
|
+
expect(map.getSource("plan-image-source-floor")).toBe(source);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("hides the picture with a visibility change, keeping the source mounted", async () => {
|
|
118
|
+
const { rerender } = render(
|
|
119
|
+
<MapCanvas plan={PLAN}>
|
|
120
|
+
<MapPlanImage id="floor" src={SRC} />
|
|
121
|
+
</MapCanvas>,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const map = await waitFor(() => {
|
|
125
|
+
const instance = lastMap();
|
|
126
|
+
expect(instance.getLayer("plan-image-layer-floor")).toBeDefined();
|
|
127
|
+
return instance;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
rerender(
|
|
131
|
+
<MapCanvas plan={PLAN}>
|
|
132
|
+
<MapPlanImage id="floor" src={SRC} visible={false} />
|
|
133
|
+
</MapCanvas>,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
await waitFor(() => expect(map.layout.get("plan-image-layer-floor:visibility")).toBe("none"));
|
|
137
|
+
expect(map.getSource("plan-image-source-floor")).toBeDefined();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("reports a failed picture on the source, not the map", async () => {
|
|
141
|
+
const onError = vi.fn();
|
|
142
|
+
const onLoad = vi.fn();
|
|
143
|
+
render(
|
|
144
|
+
<MapCanvas plan={PLAN}>
|
|
145
|
+
<MapPlanImage id="floor" src={SRC} onLoad={onLoad} onError={onError} />
|
|
146
|
+
</MapCanvas>,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const source = await waitFor(() => {
|
|
150
|
+
const instance = lastMap().getSource("plan-image-source-floor");
|
|
151
|
+
expect(instance).toBeDefined();
|
|
152
|
+
return instance!;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
source.emit("data", { sourceDataType: "content" });
|
|
156
|
+
expect(onLoad).not.toHaveBeenCalled();
|
|
157
|
+
|
|
158
|
+
source.emit("data", { sourceDataType: "metadata" });
|
|
159
|
+
expect(onLoad).toHaveBeenCalledTimes(1);
|
|
160
|
+
|
|
161
|
+
source.emit("error", { error: new Error("404") });
|
|
162
|
+
expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: "404" }));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("describes a picture assistive technology cannot reach", async () => {
|
|
166
|
+
render(
|
|
167
|
+
<MapCanvas plan={PLAN}>
|
|
168
|
+
<MapPlanImage src={SRC} alt="Level 3 floor plan, 14 rooms around a central core" />
|
|
169
|
+
</MapCanvas>,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
expect(
|
|
173
|
+
await screen.findByText("Level 3 floor plan, 14 rooms around a central core"),
|
|
174
|
+
).toHaveClass("sr-only");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type MapLibreGL from "maplibre-gl";
|
|
4
|
+
import { useEffect, useId, useMemo, useRef } from "react";
|
|
5
|
+
|
|
6
|
+
import { useMap } from "../map-canvas/map-context";
|
|
7
|
+
import { warnMapOnce } from "../lib/warn-once";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The picture under a plan: a URL, or an already-decoded image, bitmap or
|
|
11
|
+
* canvas. A `{ light, dark }` pair swaps with the active theme — a plan drawing
|
|
12
|
+
* has its own baked-in ink, and one asset rarely reads well on both surfaces.
|
|
13
|
+
*/
|
|
14
|
+
export type MapPlanImageSource = string | HTMLImageElement | ImageBitmap | HTMLCanvasElement;
|
|
15
|
+
|
|
16
|
+
export type MapPlanImageProps = {
|
|
17
|
+
/** The plan picture. Pass `{ light, dark }` to swap it with the theme. */
|
|
18
|
+
src: MapPlanImageSource | { light: MapPlanImageSource; dark: MapPlanImageSource };
|
|
19
|
+
/**
|
|
20
|
+
* What the picture shows, for anyone who cannot see it ("Level 3 floor plan,
|
|
21
|
+
* 14 rooms around a central core"). A plan drawn into WebGL has no accessible
|
|
22
|
+
* name of its own, so this is the only description of it.
|
|
23
|
+
*/
|
|
24
|
+
alt?: string;
|
|
25
|
+
/** Optional identifier for the source/layer. Auto-generated if not provided. */
|
|
26
|
+
id?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Where the picture sits, in PLAN units. Defaults to the whole plan extent, so
|
|
29
|
+
* a single full-bleed plan needs nothing here. Use it to place one wing, one
|
|
30
|
+
* machine cell or one carriage inside a larger plan.
|
|
31
|
+
*/
|
|
32
|
+
extent?: { x: number; y: number; width: number; height: number };
|
|
33
|
+
/**
|
|
34
|
+
* The four corners in lng/lat (top-left, top-right, bottom-right, bottom-left)
|
|
35
|
+
* — an escape hatch that also works on an ordinary geographic canvas, like
|
|
36
|
+
* Leaflet's `imageOverlay`. Wins over `extent`.
|
|
37
|
+
*/
|
|
38
|
+
coordinates?: MapLibreGL.Coordinates;
|
|
39
|
+
/**
|
|
40
|
+
* Opacity, 0–1 (default 1). A photographic or ink-heavy plan usually reads
|
|
41
|
+
* best held back around 0.35 so the data drawn on top stays the focus.
|
|
42
|
+
*/
|
|
43
|
+
opacity?: number;
|
|
44
|
+
/** Hide the picture without tearing the source down — a multi-floor switch (default true). */
|
|
45
|
+
visible?: boolean;
|
|
46
|
+
/** `"nearest"` keeps CAD line art crisp when zoomed past its native resolution. */
|
|
47
|
+
resampling?: "linear" | "nearest";
|
|
48
|
+
/** MapLibre layer id to insert this layer before (z-order control). */
|
|
49
|
+
beforeId?: string;
|
|
50
|
+
/** Fired once the picture has been fetched and decoded. */
|
|
51
|
+
onLoad?: () => void;
|
|
52
|
+
/** Fired when the picture cannot be loaded. The plan itself keeps working without it. */
|
|
53
|
+
onError?: (error: Error) => void;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function isThemedSource(
|
|
57
|
+
src: MapPlanImageProps["src"],
|
|
58
|
+
): src is { light: MapPlanImageSource; dark: MapPlanImageSource } {
|
|
59
|
+
return typeof src === "object" && src !== null && "light" in src && "dark" in src;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Draw a decoded image or bitmap into a canvas MapLibre can read. Returns
|
|
64
|
+
* `null` when there is no 2D context (jsdom, a zero-sized canvas) — the plan
|
|
65
|
+
* then renders without its picture rather than throwing.
|
|
66
|
+
*/
|
|
67
|
+
function toCanvas(source: Exclude<MapPlanImageSource, string>): HTMLCanvasElement | null {
|
|
68
|
+
if (typeof HTMLCanvasElement !== "undefined" && source instanceof HTMLCanvasElement) {
|
|
69
|
+
return source.width > 0 && source.height > 0 ? source : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const width = "naturalWidth" in source ? source.naturalWidth : source.width;
|
|
73
|
+
const height = "naturalHeight" in source ? source.naturalHeight : source.height;
|
|
74
|
+
if (!width || !height) return null;
|
|
75
|
+
|
|
76
|
+
const canvas = document.createElement("canvas");
|
|
77
|
+
canvas.width = width;
|
|
78
|
+
canvas.height = height;
|
|
79
|
+
const context = canvas.getContext("2d");
|
|
80
|
+
if (!context) return null;
|
|
81
|
+
context.drawImage(source as CanvasImageSource, 0, 0);
|
|
82
|
+
return canvas;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The plan picture under the shapes — a floor plan, a factory layout, a deck
|
|
87
|
+
* plan. The map equivalent of Qlik Sense's image background layer and Leaflet's
|
|
88
|
+
* `imageOverlay`: by default it covers the whole plan extent declared on
|
|
89
|
+
* `<MapCanvas plan>`, so its corners need no coordinates at all.
|
|
90
|
+
*
|
|
91
|
+
* Render it FIRST among the canvas children so the picture sits under the data,
|
|
92
|
+
* or pin the order with `beforeId`.
|
|
93
|
+
*
|
|
94
|
+
* ```tsx
|
|
95
|
+
* <MapCanvas blank plan={{ width: 2400, height: 1600, unit: "cm" }}>
|
|
96
|
+
* <MapPlanImage src={floorPlanUrl} alt="Level 3 floor plan" opacity={0.35} />
|
|
97
|
+
* <MapGeoJSON data={roomsInPlanUnits} promoteId="id" />
|
|
98
|
+
* </MapCanvas>
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* A picture is optional and often worth skipping: a plan drawn as GeoJSON in
|
|
102
|
+
* plan units paints from tokens, themes correctly, and never blurs.
|
|
103
|
+
*/
|
|
104
|
+
export function MapPlanImage({
|
|
105
|
+
src,
|
|
106
|
+
alt,
|
|
107
|
+
id: propId,
|
|
108
|
+
extent,
|
|
109
|
+
coordinates,
|
|
110
|
+
opacity = 1,
|
|
111
|
+
visible = true,
|
|
112
|
+
resampling = "linear",
|
|
113
|
+
beforeId,
|
|
114
|
+
onLoad,
|
|
115
|
+
onError,
|
|
116
|
+
}: MapPlanImageProps) {
|
|
117
|
+
const { map, isLoaded, plan, resolvedTheme } = useMap();
|
|
118
|
+
const autoId = useId();
|
|
119
|
+
const id = propId ?? autoId;
|
|
120
|
+
const sourceId = `plan-image-source-${id}`;
|
|
121
|
+
const layerId = `plan-image-layer-${id}`;
|
|
122
|
+
|
|
123
|
+
const resolvedSrc = isThemedSource(src) ? (resolvedTheme === "dark" ? src.dark : src.light) : src;
|
|
124
|
+
const url = typeof resolvedSrc === "string" ? resolvedSrc : null;
|
|
125
|
+
const canvas = useMemo(
|
|
126
|
+
() => (typeof resolvedSrc === "string" ? null : toCanvas(resolvedSrc)),
|
|
127
|
+
[resolvedSrc],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const corners = useMemo<MapLibreGL.Coordinates | null>(() => {
|
|
131
|
+
if (coordinates) return coordinates;
|
|
132
|
+
if (!plan) return null;
|
|
133
|
+
if (!extent) return plan.imageCoordinates;
|
|
134
|
+
const { x, y, width, height } = extent;
|
|
135
|
+
return [
|
|
136
|
+
plan.toLngLat({ x, y }),
|
|
137
|
+
plan.toLngLat({ x: x + width, y }),
|
|
138
|
+
plan.toLngLat({ x: x + width, y: y + height }),
|
|
139
|
+
plan.toLngLat({ x, y: y + height }),
|
|
140
|
+
];
|
|
141
|
+
}, [coordinates, extent, plan]);
|
|
142
|
+
|
|
143
|
+
// Keyed on the values, so an inline `extent` literal doesn't re-place the
|
|
144
|
+
// picture on every render.
|
|
145
|
+
const cornersKey = corners ? JSON.stringify(corners) : null;
|
|
146
|
+
|
|
147
|
+
const latestRef = useRef({ corners, url, onLoad, onError });
|
|
148
|
+
latestRef.current = { corners, url, onLoad, onError };
|
|
149
|
+
|
|
150
|
+
if (!corners) {
|
|
151
|
+
warnMapOnce(
|
|
152
|
+
"plan-image-coordinates",
|
|
153
|
+
"<MapPlanImage> needs somewhere to sit: set `plan` on <MapCanvas>, or pass `coordinates`.",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (!url && !canvas) {
|
|
157
|
+
warnMapOnce(
|
|
158
|
+
"plan-image-source",
|
|
159
|
+
"<MapPlanImage> could not read its `src`. Pass a URL, or an image/bitmap/canvas that has already decoded.",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Read paint through refs so changing it never re-creates the source below.
|
|
164
|
+
const opacityRef = useRef(opacity);
|
|
165
|
+
opacityRef.current = opacity;
|
|
166
|
+
const resamplingRef = useRef(resampling);
|
|
167
|
+
resamplingRef.current = resampling;
|
|
168
|
+
|
|
169
|
+
// Add the source + layer. Deliberately NOT keyed on the url or the corners:
|
|
170
|
+
// those are updated in place below, so a floor swap never refetches.
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (!isLoaded || !map) return;
|
|
173
|
+
const { corners: initialCorners, url: initialUrl } = latestRef.current;
|
|
174
|
+
if (!initialCorners || (!initialUrl && !canvas)) return;
|
|
175
|
+
|
|
176
|
+
map.addSource(
|
|
177
|
+
sourceId,
|
|
178
|
+
canvas
|
|
179
|
+
? { type: "canvas", canvas, coordinates: initialCorners, animate: false }
|
|
180
|
+
: { type: "image", url: initialUrl as string, coordinates: initialCorners },
|
|
181
|
+
);
|
|
182
|
+
map.addLayer(
|
|
183
|
+
{
|
|
184
|
+
id: layerId,
|
|
185
|
+
type: "raster",
|
|
186
|
+
source: sourceId,
|
|
187
|
+
paint: {
|
|
188
|
+
"raster-opacity": opacityRef.current,
|
|
189
|
+
// A plan swap should replace the picture, not cross-fade it.
|
|
190
|
+
"raster-fade-duration": 0,
|
|
191
|
+
"raster-resampling": resamplingRef.current,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
beforeId,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return () => {
|
|
198
|
+
try {
|
|
199
|
+
if (map.getLayer(layerId)) map.removeLayer(layerId);
|
|
200
|
+
if (map.getSource(sourceId)) map.removeSource(sourceId);
|
|
201
|
+
} catch {
|
|
202
|
+
// style may be mid-reload
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
// Deps deliberately exclude url/corners/paint: all three are read from
|
|
206
|
+
// `latestRef` or refs and synced in place by the effects below.
|
|
207
|
+
}, [isLoaded, map, sourceId, layerId, canvas, beforeId]);
|
|
208
|
+
|
|
209
|
+
// Move the picture when its placement changes.
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (!isLoaded || !map || !cornersKey) return;
|
|
212
|
+
const source = map.getSource(sourceId) as MapLibreGL.ImageSource | undefined;
|
|
213
|
+
source?.setCoordinates?.(latestRef.current.corners as MapLibreGL.Coordinates);
|
|
214
|
+
}, [isLoaded, map, sourceId, cornersKey]);
|
|
215
|
+
|
|
216
|
+
// Swap the picture in place — no teardown, no flash, no refetch of the rest.
|
|
217
|
+
const mountedUrlRef = useRef<string | null>(url);
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
if (!isLoaded || !map || !url) return;
|
|
220
|
+
if (mountedUrlRef.current === url) return;
|
|
221
|
+
mountedUrlRef.current = url;
|
|
222
|
+
const source = map.getSource(sourceId) as MapLibreGL.ImageSource | undefined;
|
|
223
|
+
source?.updateImage?.({
|
|
224
|
+
url,
|
|
225
|
+
...(latestRef.current.corners ? { coordinates: latestRef.current.corners } : {}),
|
|
226
|
+
});
|
|
227
|
+
}, [isLoaded, map, sourceId, url]);
|
|
228
|
+
|
|
229
|
+
// Paint and visibility.
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
if (!isLoaded || !map || !map.getLayer(layerId)) return;
|
|
232
|
+
map.setPaintProperty(layerId, "raster-opacity", opacity);
|
|
233
|
+
map.setPaintProperty(layerId, "raster-resampling", resampling);
|
|
234
|
+
map.setLayoutProperty(layerId, "visibility", visible ? "visible" : "none");
|
|
235
|
+
}, [isLoaded, map, layerId, opacity, resampling, visible]);
|
|
236
|
+
|
|
237
|
+
// Load and failure are reported by the SOURCE, not the map: a map-level error
|
|
238
|
+
// carries no source id. The plan keeps working without its picture, so the
|
|
239
|
+
// consumer owns whatever the failure should look like.
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (!isLoaded || !map) return;
|
|
242
|
+
const source = map.getSource(sourceId) as
|
|
243
|
+
| (MapLibreGL.ImageSource & {
|
|
244
|
+
on?: (event: string, handler: (payload: unknown) => void) => void;
|
|
245
|
+
off?: (event: string, handler: (payload: unknown) => void) => void;
|
|
246
|
+
})
|
|
247
|
+
| undefined;
|
|
248
|
+
if (!source?.on) return;
|
|
249
|
+
|
|
250
|
+
const handleData = (payload: unknown) => {
|
|
251
|
+
if ((payload as { sourceDataType?: string } | undefined)?.sourceDataType !== "metadata") {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
latestRef.current.onLoad?.();
|
|
255
|
+
};
|
|
256
|
+
const handleError = (payload: unknown) => {
|
|
257
|
+
const error = (payload as { error?: Error } | undefined)?.error;
|
|
258
|
+
latestRef.current.onError?.(error instanceof Error ? error : new Error("Plan image failed"));
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
source.on("data", handleData);
|
|
262
|
+
source.on("error", handleError);
|
|
263
|
+
|
|
264
|
+
return () => {
|
|
265
|
+
source.off?.("data", handleData);
|
|
266
|
+
source.off?.("error", handleError);
|
|
267
|
+
};
|
|
268
|
+
}, [isLoaded, map, sourceId]);
|
|
269
|
+
|
|
270
|
+
// The picture lives in WebGL, where assistive technology cannot reach it. This
|
|
271
|
+
// span is its only description — and the module's stable selector seam.
|
|
272
|
+
return alt ? (
|
|
273
|
+
<span data-slot="map-plan-image" className="sr-only">
|
|
274
|
+
{alt}
|
|
275
|
+
</span>
|
|
276
|
+
) : (
|
|
277
|
+
<span data-slot="map-plan-image" hidden />
|
|
278
|
+
);
|
|
279
|
+
}
|
|
@@ -8,12 +8,9 @@ import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
|
8
8
|
import { useLocale } from "@elabs-ai/components-ui";
|
|
9
9
|
|
|
10
10
|
import { useMap } from "../map-canvas/map-context";
|
|
11
|
+
import { resolveMapPosition, type MapPosition } from "../lib/map-position";
|
|
11
12
|
|
|
12
|
-
export type MapPopupProps = {
|
|
13
|
-
/** Longitude coordinate for the popup position. */
|
|
14
|
-
longitude: number;
|
|
15
|
-
/** Latitude coordinate for the popup position. */
|
|
16
|
-
latitude: number;
|
|
13
|
+
export type MapPopupProps = MapPosition & {
|
|
17
14
|
/** Callback when the popup is closed. */
|
|
18
15
|
onClose?: () => void;
|
|
19
16
|
/** Popup content. */
|
|
@@ -25,20 +22,28 @@ export type MapPopupProps = {
|
|
|
25
22
|
} & Omit<PopupOptions, "className" | "closeButton">;
|
|
26
23
|
|
|
27
24
|
/**
|
|
28
|
-
* A standalone popup anchored at a lng/lat
|
|
29
|
-
*
|
|
25
|
+
* A standalone popup anchored at a lng/lat — or, on a canvas with a `plan`
|
|
26
|
+
* extent, at a plan `x`/`y`. Not attached to a marker; typically rendered
|
|
27
|
+
* conditionally from app state (e.g. after a layer click).
|
|
30
28
|
*/
|
|
31
29
|
export function MapPopup({
|
|
32
|
-
longitude,
|
|
33
|
-
latitude,
|
|
30
|
+
longitude: longitudeProp,
|
|
31
|
+
latitude: latitudeProp,
|
|
32
|
+
x,
|
|
33
|
+
y,
|
|
34
34
|
onClose,
|
|
35
35
|
children,
|
|
36
36
|
className,
|
|
37
37
|
closeButton = false,
|
|
38
38
|
...popupOptions
|
|
39
39
|
}: MapPopupProps) {
|
|
40
|
-
const { map } = useMap();
|
|
40
|
+
const { map, plan } = useMap();
|
|
41
41
|
const { t } = useLocale();
|
|
42
|
+
const [longitude, latitude] = resolveMapPosition(
|
|
43
|
+
{ longitude: longitudeProp, latitude: latitudeProp, x, y },
|
|
44
|
+
plan,
|
|
45
|
+
"MapPopup",
|
|
46
|
+
);
|
|
42
47
|
const onCloseRef = useRef(onClose);
|
|
43
48
|
onCloseRef.current = onClose;
|
|
44
49
|
const container = useMemo(() => document.createElement("div"), []);
|
package/src/map-route/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { MapRoute, type MapRouteProps } from "./map-route";
|
|
1
|
+
export { MapRoute, type MapRouteProps, type MapRouteDirection } from "./map-route";
|