@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 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/map-plan/use-plan-selection.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useMemo, useState } from \"react\";\n\nexport interface UsePlanSelectionOptions {\n /** Allow more than one region to be selected at a time (default false). */\n multiple?: boolean;\n /** The ids selected to begin with. */\n initialSelectedIds?: readonly string[];\n /** Called whenever the selection changes. */\n onChange?: (selectedIds: string[]) => void;\n}\n\nexport interface PlanSelection {\n /** The single selected id, or the last of a multiple selection; `null` for none. */\n selectedId: string | null;\n /** Every selected id, in selection order. */\n selectedIds: string[];\n isSelected: (id: string) => boolean;\n /** Toggle a region; `null` clears everything. */\n select: (id: string | null) => void;\n clear: () => void;\n}\n\n/**\n * Selection state for a plan, keyed by region **id** rather than index — a\n * status tick that re-orders the regions must not move the selection with it.\n *\n * Hand `selectedIds` to `<MapGeoJSON selectedId>` for the paint and to\n * `<MapPlanOverlay selectedId>` for the pressed state, so both read one source.\n */\nexport function usePlanSelection({\n multiple = false,\n initialSelectedIds,\n onChange,\n}: UsePlanSelectionOptions = {}): PlanSelection {\n const [selectedIds, setSelectedIds] = useState<string[]>(() => [...(initialSelectedIds ?? [])]);\n\n const commit = useCallback(\n (next: string[]) => {\n setSelectedIds(next);\n onChange?.(next);\n },\n [onChange],\n );\n\n const select = useCallback(\n (id: string | null) => {\n if (id === null) {\n commit([]);\n return;\n }\n setSelectedIds((current) => {\n const next = current.includes(id)\n ? current.filter((entry) => entry !== id)\n : multiple\n ? [...current, id]\n : [id];\n onChange?.(next);\n return next;\n });\n },\n [commit, multiple, onChange],\n );\n\n const clear = useCallback(() => commit([]), [commit]);\n\n return useMemo(\n () => ({\n selectedId: selectedIds.at(-1) ?? null,\n selectedIds,\n isSelected: (id: string) => selectedIds.includes(id),\n select,\n clear,\n }),\n [selectedIds, select, clear],\n );\n}\n"],"mappings":";;;AAEA,SAAS,aAAa,SAAS,gBAAgB;AA6BxC,SAAS,iBAAiB;AAAA,EAC/B,WAAW;AAAA,EACX;AAAA,EACA;AACF,IAA6B,CAAC,GAAkB;AAC9C,QAAM,CAAC,aAAa,cAAc,IAAI,SAAmB,MAAM,CAAC,GAAI,sBAAsB,CAAC,CAAE,CAAC;AAE9F,QAAM,SAAS;AAAA,IACb,CAAC,SAAmB;AAClB,qBAAe,IAAI;AACnB,iBAAW,IAAI;AAAA,IACjB;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,SAAS;AAAA,IACb,CAAC,OAAsB;AACrB,UAAI,OAAO,MAAM;AACf,eAAO,CAAC,CAAC;AACT;AAAA,MACF;AACA,qBAAe,CAAC,YAAY;AAC1B,cAAM,OAAO,QAAQ,SAAS,EAAE,IAC5B,QAAQ,OAAO,CAAC,UAAU,UAAU,EAAE,IACtC,WACE,CAAC,GAAG,SAAS,EAAE,IACf,CAAC,EAAE;AACT,mBAAW,IAAI;AACf,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,IACA,CAAC,QAAQ,UAAU,QAAQ;AAAA,EAC7B;AAEA,QAAM,QAAQ,YAAY,MAAM,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;AAEpD,SAAO;AAAA,IACL,OAAO;AAAA,MACL,YAAY,YAAY,GAAG,EAAE,KAAK;AAAA,MAClC;AAAA,MACA,YAAY,CAAC,OAAe,YAAY,SAAS,EAAE;AAAA,MACnD;AAAA,MACA;AAAA,IACF;AAAA,IACA,CAAC,aAAa,QAAQ,KAAK;AAAA,EAC7B;AACF;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
useTokenColor
|
|
4
|
+
} from "./chunk-Y44MEKJV.js";
|
|
5
|
+
import {
|
|
6
|
+
useMap
|
|
7
|
+
} from "./chunk-JYFERXZT.js";
|
|
8
|
+
import {
|
|
9
|
+
warnMapOnce
|
|
10
|
+
} from "./chunk-G7JDXDJN.js";
|
|
11
|
+
|
|
12
|
+
// src/map-cluster-layer/map-cluster-layer.tsx
|
|
13
|
+
import { useEffect, useId, useMemo } from "react";
|
|
14
|
+
var DEFAULT_CLUSTER_THRESHOLDS = [100, 750];
|
|
15
|
+
function MapClusterLayer({
|
|
16
|
+
data,
|
|
17
|
+
clusterMaxZoom = 14,
|
|
18
|
+
clusterRadius = 50,
|
|
19
|
+
clusterColors,
|
|
20
|
+
clusterThresholds = DEFAULT_CLUSTER_THRESHOLDS,
|
|
21
|
+
pointColor,
|
|
22
|
+
onPointClick,
|
|
23
|
+
onClusterClick
|
|
24
|
+
}) {
|
|
25
|
+
const { map, isLoaded, plan } = useMap();
|
|
26
|
+
const id = useId();
|
|
27
|
+
const sourceId = `cluster-source-${id}`;
|
|
28
|
+
const clusterLayerId = `clusters-${id}`;
|
|
29
|
+
const clusterCountLayerId = `cluster-count-${id}`;
|
|
30
|
+
const unclusteredLayerId = `unclustered-point-${id}`;
|
|
31
|
+
if (plan) {
|
|
32
|
+
warnMapOnce(
|
|
33
|
+
"cluster-layer-plan",
|
|
34
|
+
"<MapClusterLayer> is not for a plan map: its count label is a symbol text-field, and a plan's blank style has no glyph endpoint, so the counts render as nothing."
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const success = useTokenColor("--success");
|
|
38
|
+
const warning = useTokenColor("--warning");
|
|
39
|
+
const destructive = useTokenColor("--destructive");
|
|
40
|
+
const primary = useTokenColor("--primary");
|
|
41
|
+
const surface = useTokenColor("--background");
|
|
42
|
+
const resolvedClusterColors = useMemo(
|
|
43
|
+
() => clusterColors ?? [success, warning, destructive],
|
|
44
|
+
[clusterColors, success, warning, destructive]
|
|
45
|
+
);
|
|
46
|
+
const resolvedPointColor = pointColor ?? primary;
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!isLoaded || !map) return;
|
|
49
|
+
map.addSource(sourceId, {
|
|
50
|
+
type: "geojson",
|
|
51
|
+
data,
|
|
52
|
+
cluster: true,
|
|
53
|
+
clusterMaxZoom,
|
|
54
|
+
clusterRadius
|
|
55
|
+
});
|
|
56
|
+
map.addLayer({
|
|
57
|
+
id: clusterLayerId,
|
|
58
|
+
type: "circle",
|
|
59
|
+
source: sourceId,
|
|
60
|
+
filter: ["has", "point_count"],
|
|
61
|
+
paint: {
|
|
62
|
+
"circle-color": [
|
|
63
|
+
"step",
|
|
64
|
+
["get", "point_count"],
|
|
65
|
+
resolvedClusterColors[0],
|
|
66
|
+
clusterThresholds[0],
|
|
67
|
+
resolvedClusterColors[1],
|
|
68
|
+
clusterThresholds[1],
|
|
69
|
+
resolvedClusterColors[2]
|
|
70
|
+
],
|
|
71
|
+
"circle-radius": [
|
|
72
|
+
"step",
|
|
73
|
+
["get", "point_count"],
|
|
74
|
+
20,
|
|
75
|
+
clusterThresholds[0],
|
|
76
|
+
30,
|
|
77
|
+
clusterThresholds[1],
|
|
78
|
+
40
|
|
79
|
+
],
|
|
80
|
+
"circle-stroke-width": 1,
|
|
81
|
+
"circle-stroke-color": surface,
|
|
82
|
+
"circle-opacity": 0.85
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
map.addLayer({
|
|
86
|
+
id: clusterCountLayerId,
|
|
87
|
+
type: "symbol",
|
|
88
|
+
source: sourceId,
|
|
89
|
+
filter: ["has", "point_count"],
|
|
90
|
+
layout: {
|
|
91
|
+
"text-field": "{point_count_abbreviated}",
|
|
92
|
+
"text-font": ["Open Sans"],
|
|
93
|
+
"text-size": 12
|
|
94
|
+
},
|
|
95
|
+
paint: {
|
|
96
|
+
"text-color": surface
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
map.addLayer({
|
|
100
|
+
id: unclusteredLayerId,
|
|
101
|
+
type: "circle",
|
|
102
|
+
source: sourceId,
|
|
103
|
+
filter: ["!", ["has", "point_count"]],
|
|
104
|
+
paint: {
|
|
105
|
+
"circle-color": resolvedPointColor,
|
|
106
|
+
"circle-radius": 5,
|
|
107
|
+
"circle-stroke-width": 2,
|
|
108
|
+
"circle-stroke-color": surface
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
return () => {
|
|
112
|
+
try {
|
|
113
|
+
if (map.getLayer(clusterCountLayerId)) map.removeLayer(clusterCountLayerId);
|
|
114
|
+
if (map.getLayer(unclusteredLayerId)) map.removeLayer(unclusteredLayerId);
|
|
115
|
+
if (map.getLayer(clusterLayerId)) map.removeLayer(clusterLayerId);
|
|
116
|
+
if (map.getSource(sourceId)) map.removeSource(sourceId);
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}, [isLoaded, map, sourceId]);
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!isLoaded || !map || typeof data === "string") return;
|
|
123
|
+
const source = map.getSource(sourceId);
|
|
124
|
+
if (source) {
|
|
125
|
+
source.setData(data);
|
|
126
|
+
}
|
|
127
|
+
}, [isLoaded, map, data, sourceId]);
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (!isLoaded || !map) return;
|
|
130
|
+
if (map.getLayer(clusterLayerId)) {
|
|
131
|
+
map.setPaintProperty(clusterLayerId, "circle-color", [
|
|
132
|
+
"step",
|
|
133
|
+
["get", "point_count"],
|
|
134
|
+
resolvedClusterColors[0],
|
|
135
|
+
clusterThresholds[0],
|
|
136
|
+
resolvedClusterColors[1],
|
|
137
|
+
clusterThresholds[1],
|
|
138
|
+
resolvedClusterColors[2]
|
|
139
|
+
]);
|
|
140
|
+
map.setPaintProperty(clusterLayerId, "circle-radius", [
|
|
141
|
+
"step",
|
|
142
|
+
["get", "point_count"],
|
|
143
|
+
20,
|
|
144
|
+
clusterThresholds[0],
|
|
145
|
+
30,
|
|
146
|
+
clusterThresholds[1],
|
|
147
|
+
40
|
|
148
|
+
]);
|
|
149
|
+
map.setPaintProperty(clusterLayerId, "circle-stroke-color", surface);
|
|
150
|
+
}
|
|
151
|
+
if (map.getLayer(clusterCountLayerId)) {
|
|
152
|
+
map.setPaintProperty(clusterCountLayerId, "text-color", surface);
|
|
153
|
+
}
|
|
154
|
+
if (map.getLayer(unclusteredLayerId)) {
|
|
155
|
+
map.setPaintProperty(unclusteredLayerId, "circle-color", resolvedPointColor);
|
|
156
|
+
map.setPaintProperty(unclusteredLayerId, "circle-stroke-color", surface);
|
|
157
|
+
}
|
|
158
|
+
}, [
|
|
159
|
+
isLoaded,
|
|
160
|
+
map,
|
|
161
|
+
clusterLayerId,
|
|
162
|
+
clusterCountLayerId,
|
|
163
|
+
unclusteredLayerId,
|
|
164
|
+
resolvedClusterColors,
|
|
165
|
+
clusterThresholds,
|
|
166
|
+
resolvedPointColor,
|
|
167
|
+
surface
|
|
168
|
+
]);
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (!isLoaded || !map) return;
|
|
171
|
+
const handleClusterClick = async (e) => {
|
|
172
|
+
const features = map.queryRenderedFeatures(e.point, {
|
|
173
|
+
layers: [clusterLayerId]
|
|
174
|
+
});
|
|
175
|
+
const feature = features[0];
|
|
176
|
+
if (!feature) return;
|
|
177
|
+
const clusterId = feature.properties?.cluster_id;
|
|
178
|
+
const pointCount = feature.properties?.point_count;
|
|
179
|
+
const coordinates = feature.geometry.coordinates;
|
|
180
|
+
if (onClusterClick) {
|
|
181
|
+
onClusterClick(clusterId, coordinates, pointCount);
|
|
182
|
+
} else {
|
|
183
|
+
const source = map.getSource(sourceId);
|
|
184
|
+
const zoom = await source.getClusterExpansionZoom(clusterId);
|
|
185
|
+
map.easeTo({
|
|
186
|
+
center: coordinates,
|
|
187
|
+
zoom
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const handlePointClick = (e) => {
|
|
192
|
+
const feature = e.features?.[0];
|
|
193
|
+
if (!onPointClick || !feature) return;
|
|
194
|
+
const coordinates = feature.geometry.coordinates.slice();
|
|
195
|
+
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
|
|
196
|
+
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
|
|
197
|
+
}
|
|
198
|
+
onPointClick(feature, coordinates);
|
|
199
|
+
};
|
|
200
|
+
const handleMouseEnterCluster = () => {
|
|
201
|
+
map.getCanvas().style.cursor = "pointer";
|
|
202
|
+
};
|
|
203
|
+
const handleMouseLeaveCluster = () => {
|
|
204
|
+
map.getCanvas().style.cursor = "";
|
|
205
|
+
};
|
|
206
|
+
const handleMouseEnterPoint = () => {
|
|
207
|
+
if (onPointClick) {
|
|
208
|
+
map.getCanvas().style.cursor = "pointer";
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const handleMouseLeavePoint = () => {
|
|
212
|
+
map.getCanvas().style.cursor = "";
|
|
213
|
+
};
|
|
214
|
+
map.on("click", clusterLayerId, handleClusterClick);
|
|
215
|
+
map.on("click", unclusteredLayerId, handlePointClick);
|
|
216
|
+
map.on("mouseenter", clusterLayerId, handleMouseEnterCluster);
|
|
217
|
+
map.on("mouseleave", clusterLayerId, handleMouseLeaveCluster);
|
|
218
|
+
map.on("mouseenter", unclusteredLayerId, handleMouseEnterPoint);
|
|
219
|
+
map.on("mouseleave", unclusteredLayerId, handleMouseLeavePoint);
|
|
220
|
+
return () => {
|
|
221
|
+
map.off("click", clusterLayerId, handleClusterClick);
|
|
222
|
+
map.off("click", unclusteredLayerId, handlePointClick);
|
|
223
|
+
map.off("mouseenter", clusterLayerId, handleMouseEnterCluster);
|
|
224
|
+
map.off("mouseleave", clusterLayerId, handleMouseLeaveCluster);
|
|
225
|
+
map.off("mouseenter", unclusteredLayerId, handleMouseEnterPoint);
|
|
226
|
+
map.off("mouseleave", unclusteredLayerId, handleMouseLeavePoint);
|
|
227
|
+
};
|
|
228
|
+
}, [isLoaded, map, clusterLayerId, unclusteredLayerId, sourceId, onClusterClick, onPointClick]);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export {
|
|
233
|
+
MapClusterLayer
|
|
234
|
+
};
|
|
235
|
+
//# sourceMappingURL=chunk-JGJ6442I.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/map-cluster-layer/map-cluster-layer.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 { warnMapOnce } from \"../lib/warn-once\";\n\nexport type MapClusterLayerProps<P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties> =\n {\n /** GeoJSON FeatureCollection data or a URL to fetch GeoJSON from. */\n data: string | GeoJSON.FeatureCollection<GeoJSON.Point, P>;\n /** Maximum zoom level to cluster points on (default: 14). */\n clusterMaxZoom?: number;\n /** Radius of each cluster when clustering points, in pixels (default: 50). */\n clusterRadius?: number;\n /**\n * Colors for cluster circles: [small, medium, large] based on point count.\n * Defaults to the theme's `--success`/`--warning`/`--destructive` tokens.\n */\n clusterColors?: [string, string, string];\n /** Point-count thresholds for the color/size steps: [medium, large] (default: [100, 750]). */\n clusterThresholds?: [number, number];\n /** Color for unclustered individual points. Defaults to the theme's `--primary` token. */\n pointColor?: string;\n /** Callback when an unclustered point is clicked. */\n onPointClick?: (\n feature: GeoJSON.Feature<GeoJSON.Point, P>,\n coordinates: [number, number],\n ) => void;\n /** Callback when a cluster is clicked. If not provided, zooms into the cluster. */\n onClusterClick?: (clusterId: number, coordinates: [number, number], pointCount: number) => void;\n };\n\nconst DEFAULT_CLUSTER_THRESHOLDS: [number, number] = [100, 750];\n\n/**\n * Clustered point rendering for large point datasets. Cluster circles step\n * through the status tokens (success → warning → destructive) as the point\n * count grows; strokes and count labels use the page surface for contrast.\n *\n * NOT for a plan map. The count inside each circle is a `symbol` `text-field`,\n * and a blank style — which every plan uses — ships no glyph endpoint, so the\n * counts would silently render as nothing at all. Cluster plan points yourself\n * and draw the groups with `MapGeoJSON`, or put the count in the DOM overlay.\n */\nexport function MapClusterLayer<P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties>({\n data,\n clusterMaxZoom = 14,\n clusterRadius = 50,\n clusterColors,\n clusterThresholds = DEFAULT_CLUSTER_THRESHOLDS,\n pointColor,\n onPointClick,\n onClusterClick,\n}: MapClusterLayerProps<P>) {\n const { map, isLoaded, plan } = useMap();\n const id = useId();\n const sourceId = `cluster-source-${id}`;\n const clusterLayerId = `clusters-${id}`;\n const clusterCountLayerId = `cluster-count-${id}`;\n const unclusteredLayerId = `unclustered-point-${id}`;\n\n if (plan) {\n warnMapOnce(\n \"cluster-layer-plan\",\n \"<MapClusterLayer> is not for a plan map: its count label is a symbol text-field, and a plan's blank style has no glyph endpoint, so the counts render as nothing.\",\n );\n }\n\n const success = useTokenColor(\"--success\");\n const warning = useTokenColor(\"--warning\");\n const destructive = useTokenColor(\"--destructive\");\n const primary = useTokenColor(\"--primary\");\n const surface = useTokenColor(\"--background\");\n\n const resolvedClusterColors = useMemo<[string, string, string]>(\n () => clusterColors ?? [success, warning, destructive],\n [clusterColors, success, warning, destructive],\n );\n const resolvedPointColor = pointColor ?? primary;\n\n // Add source and layers on mount.\n useEffect(() => {\n if (!isLoaded || !map) return;\n\n map.addSource(sourceId, {\n type: \"geojson\",\n data,\n cluster: true,\n clusterMaxZoom,\n clusterRadius,\n });\n\n map.addLayer({\n id: clusterLayerId,\n type: \"circle\",\n source: sourceId,\n filter: [\"has\", \"point_count\"],\n paint: {\n \"circle-color\": [\n \"step\",\n [\"get\", \"point_count\"],\n resolvedClusterColors[0],\n clusterThresholds[0],\n resolvedClusterColors[1],\n clusterThresholds[1],\n resolvedClusterColors[2],\n ],\n \"circle-radius\": [\n \"step\",\n [\"get\", \"point_count\"],\n 20,\n clusterThresholds[0],\n 30,\n clusterThresholds[1],\n 40,\n ],\n \"circle-stroke-width\": 1,\n \"circle-stroke-color\": surface,\n \"circle-opacity\": 0.85,\n },\n });\n\n map.addLayer({\n id: clusterCountLayerId,\n type: \"symbol\",\n source: sourceId,\n filter: [\"has\", \"point_count\"],\n layout: {\n \"text-field\": \"{point_count_abbreviated}\",\n \"text-font\": [\"Open Sans\"],\n \"text-size\": 12,\n },\n paint: {\n \"text-color\": surface,\n },\n });\n\n map.addLayer({\n id: unclusteredLayerId,\n type: \"circle\",\n source: sourceId,\n filter: [\"!\", [\"has\", \"point_count\"]],\n paint: {\n \"circle-color\": resolvedPointColor,\n \"circle-radius\": 5,\n \"circle-stroke-width\": 2,\n \"circle-stroke-color\": surface,\n },\n });\n\n return () => {\n try {\n if (map.getLayer(clusterCountLayerId)) map.removeLayer(clusterCountLayerId);\n if (map.getLayer(unclusteredLayerId)) map.removeLayer(unclusteredLayerId);\n if (map.getLayer(clusterLayerId)) map.removeLayer(clusterLayerId);\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/layers created once per map; data + paint are synced by the effects below\n }, [isLoaded, map, sourceId]);\n\n // Update source data when the data prop changes (only for non-URL data).\n useEffect(() => {\n if (!isLoaded || !map || typeof data === \"string\") return;\n\n const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource;\n if (source) {\n source.setData(data);\n }\n }, [isLoaded, map, data, sourceId]);\n\n // Sync layer styles when props (or the resolved theme colors) change.\n useEffect(() => {\n if (!isLoaded || !map) return;\n\n if (map.getLayer(clusterLayerId)) {\n map.setPaintProperty(clusterLayerId, \"circle-color\", [\n \"step\",\n [\"get\", \"point_count\"],\n resolvedClusterColors[0],\n clusterThresholds[0],\n resolvedClusterColors[1],\n clusterThresholds[1],\n resolvedClusterColors[2],\n ]);\n map.setPaintProperty(clusterLayerId, \"circle-radius\", [\n \"step\",\n [\"get\", \"point_count\"],\n 20,\n clusterThresholds[0],\n 30,\n clusterThresholds[1],\n 40,\n ]);\n map.setPaintProperty(clusterLayerId, \"circle-stroke-color\", surface);\n }\n\n if (map.getLayer(clusterCountLayerId)) {\n map.setPaintProperty(clusterCountLayerId, \"text-color\", surface);\n }\n\n if (map.getLayer(unclusteredLayerId)) {\n map.setPaintProperty(unclusteredLayerId, \"circle-color\", resolvedPointColor);\n map.setPaintProperty(unclusteredLayerId, \"circle-stroke-color\", surface);\n }\n }, [\n isLoaded,\n map,\n clusterLayerId,\n clusterCountLayerId,\n unclusteredLayerId,\n resolvedClusterColors,\n clusterThresholds,\n resolvedPointColor,\n surface,\n ]);\n\n // Handle click events.\n useEffect(() => {\n if (!isLoaded || !map) return;\n\n // Cluster click handler — zoom into the cluster by default.\n const handleClusterClick = async (e: MapLibreGL.MapMouseEvent) => {\n const features = map.queryRenderedFeatures(e.point, {\n layers: [clusterLayerId],\n });\n const feature = features[0];\n if (!feature) return;\n const clusterId = feature.properties?.cluster_id as number;\n const pointCount = feature.properties?.point_count as number;\n const coordinates = (feature.geometry as GeoJSON.Point).coordinates as [number, number];\n\n if (onClusterClick) {\n onClusterClick(clusterId, coordinates, pointCount);\n } else {\n const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource;\n const zoom = await source.getClusterExpansionZoom(clusterId);\n map.easeTo({\n center: coordinates,\n zoom,\n });\n }\n };\n\n // Unclustered point click handler.\n const handlePointClick = (\n e: MapLibreGL.MapMouseEvent & {\n features?: MapLibreGL.MapGeoJSONFeature[];\n },\n ) => {\n const feature = e.features?.[0];\n if (!onPointClick || !feature) return;\n const coordinates = (feature.geometry as GeoJSON.Point).coordinates.slice() as [\n number,\n number,\n ];\n\n // Handle world copies.\n while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {\n coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;\n }\n\n onPointClick(feature as unknown as GeoJSON.Feature<GeoJSON.Point, P>, coordinates);\n };\n\n // Cursor style handlers.\n const handleMouseEnterCluster = () => {\n map.getCanvas().style.cursor = \"pointer\";\n };\n const handleMouseLeaveCluster = () => {\n map.getCanvas().style.cursor = \"\";\n };\n const handleMouseEnterPoint = () => {\n if (onPointClick) {\n map.getCanvas().style.cursor = \"pointer\";\n }\n };\n const handleMouseLeavePoint = () => {\n map.getCanvas().style.cursor = \"\";\n };\n\n map.on(\"click\", clusterLayerId, handleClusterClick);\n map.on(\"click\", unclusteredLayerId, handlePointClick);\n map.on(\"mouseenter\", clusterLayerId, handleMouseEnterCluster);\n map.on(\"mouseleave\", clusterLayerId, handleMouseLeaveCluster);\n map.on(\"mouseenter\", unclusteredLayerId, handleMouseEnterPoint);\n map.on(\"mouseleave\", unclusteredLayerId, handleMouseLeavePoint);\n\n return () => {\n map.off(\"click\", clusterLayerId, handleClusterClick);\n map.off(\"click\", unclusteredLayerId, handlePointClick);\n map.off(\"mouseenter\", clusterLayerId, handleMouseEnterCluster);\n map.off(\"mouseleave\", clusterLayerId, handleMouseLeaveCluster);\n map.off(\"mouseenter\", unclusteredLayerId, handleMouseEnterPoint);\n map.off(\"mouseleave\", unclusteredLayerId, handleMouseLeavePoint);\n };\n }, [isLoaded, map, clusterLayerId, unclusteredLayerId, sourceId, onClusterClick, onPointClick]);\n\n return null;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/map-canvas/map-context.ts
|
|
4
|
+
import { createContext, use } from "react";
|
|
5
|
+
var MapContext = createContext(null);
|
|
6
|
+
function useMap() {
|
|
7
|
+
const context = use(MapContext);
|
|
8
|
+
if (!context) {
|
|
9
|
+
throw new Error("useMap must be used within a <MapCanvas>");
|
|
10
|
+
}
|
|
11
|
+
return context;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
MapContext,
|
|
16
|
+
useMap
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=chunk-JYFERXZT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/map-canvas/map-context.ts"],"sourcesContent":["\"use client\";\n\nimport type MapLibreGL from \"maplibre-gl\";\nimport { createContext, use } from \"react\";\n\nimport type { PlanCrs } from \"../lib/plan-crs\";\n\n/** Light-or-dark flavor of the active basemap (derived from the brand theme). */\nexport type BasemapTheme = \"light\" | \"dark\";\n\nexport interface MapContextValue {\n /** The live MapLibre map instance (`null` until the map has mounted). */\n map: MapLibreGL.Map | null;\n /** True once the map AND its style are fully loaded — gate layer operations on this. */\n isLoaded: boolean;\n /** Which basemap flavor is active. */\n resolvedTheme: BasemapTheme;\n /**\n * Changes whenever the active brand theme changes. Layer components use it\n * as a dependency key to re-resolve semantic token colors for WebGL paint.\n */\n themeKey: string;\n /**\n * The plan coordinate system when this canvas is a CUSTOM (non-geographic)\n * plan map — a floor plan, factory layout or carriage — and `null` for an\n * ordinary geographic map. Layer components read it to convert the plan\n * coordinates they are handed, and to report plan coordinates back.\n */\n plan: PlanCrs | null;\n /** True while the consumer has asked for a loading overlay over the map. */\n loading: boolean;\n}\n\nexport const MapContext = createContext<MapContextValue | null>(null);\n\n/** Access the map instance + load state from any descendant of `<MapCanvas>`. */\nexport function useMap(): MapContextValue {\n const context = use(MapContext);\n if (!context) {\n throw new Error(\"useMap must be used within a <MapCanvas>\");\n }\n return context;\n}\n"],"mappings":";;;AAGA,SAAS,eAAe,WAAW;AA8B5B,IAAM,aAAa,cAAsC,IAAI;AAG7D,SAAS,SAA0B;AACxC,QAAM,UAAU,IAAI,UAAU;AAC9B,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,0CAA0C;AAAA,EAC5D;AACA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/map-plan/map-plan-status.tsx
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
var DEFAULT_COALESCE_MS = 2e3;
|
|
8
|
+
function MapPlanStatus({
|
|
9
|
+
message,
|
|
10
|
+
coalesceMs = DEFAULT_COALESCE_MS,
|
|
11
|
+
visible = false,
|
|
12
|
+
className
|
|
13
|
+
}) {
|
|
14
|
+
const [announced, setAnnounced] = useState(message);
|
|
15
|
+
const lastAtRef = useRef(0);
|
|
16
|
+
const pendingRef = useRef(null);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (message === announced) return;
|
|
19
|
+
const elapsed = Date.now() - lastAtRef.current;
|
|
20
|
+
if (elapsed >= coalesceMs) {
|
|
21
|
+
lastAtRef.current = Date.now();
|
|
22
|
+
setAnnounced(message);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (pendingRef.current) clearTimeout(pendingRef.current);
|
|
26
|
+
pendingRef.current = setTimeout(() => {
|
|
27
|
+
lastAtRef.current = Date.now();
|
|
28
|
+
pendingRef.current = null;
|
|
29
|
+
setAnnounced(message);
|
|
30
|
+
}, coalesceMs - elapsed);
|
|
31
|
+
return () => {
|
|
32
|
+
if (pendingRef.current) {
|
|
33
|
+
clearTimeout(pendingRef.current);
|
|
34
|
+
pendingRef.current = null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}, [message, announced, coalesceMs]);
|
|
38
|
+
return /* @__PURE__ */ jsx(
|
|
39
|
+
"p",
|
|
40
|
+
{
|
|
41
|
+
"data-slot": "map-plan-status",
|
|
42
|
+
role: "status",
|
|
43
|
+
"aria-live": "polite",
|
|
44
|
+
className: cn(visible ? "text-caption text-muted-foreground" : "sr-only", className),
|
|
45
|
+
children: announced
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
MapPlanStatus
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=chunk-LU2UEKFM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/map-plan/map-plan-status.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\n/** Nothing is announced more often than this, however fast the data changes. */\nconst DEFAULT_COALESCE_MS = 2000;\n\nexport interface MapPlanStatusProps {\n /**\n * The sentence to announce. Change it when something a person needs to hear\n * changed — a selection, a status transition — never on every tick of a number.\n */\n message: string;\n /** Milliseconds between announcements (default 2000). */\n coalesceMs?: number;\n /** Also show the message (default false: it is an announcement, not chrome). */\n visible?: boolean;\n className?: string;\n}\n\n/**\n * ONE live region per plan. A WebGL canvas announces nothing by itself, and a\n * plan that changes — a machine goes down, a room frees up — has to say so\n * without stealing focus.\n *\n * Announcements are coalesced: a live region that re-reads a ticking number is\n * worse than silence, because it talks over everything else the person is doing.\n */\nexport function MapPlanStatus({\n message,\n coalesceMs = DEFAULT_COALESCE_MS,\n visible = false,\n className,\n}: MapPlanStatusProps) {\n const [announced, setAnnounced] = useState(message);\n const lastAtRef = useRef(0);\n const pendingRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n if (message === announced) return;\n\n const elapsed = Date.now() - lastAtRef.current;\n if (elapsed >= coalesceMs) {\n lastAtRef.current = Date.now();\n setAnnounced(message);\n return;\n }\n\n // Inside the quiet window: hold the newest message and say it when the\n // window closes, so a burst of changes becomes one sentence.\n if (pendingRef.current) clearTimeout(pendingRef.current);\n pendingRef.current = setTimeout(() => {\n lastAtRef.current = Date.now();\n pendingRef.current = null;\n setAnnounced(message);\n }, coalesceMs - elapsed);\n\n return () => {\n if (pendingRef.current) {\n clearTimeout(pendingRef.current);\n pendingRef.current = null;\n }\n };\n }, [message, announced, coalesceMs]);\n\n return (\n <p\n data-slot=\"map-plan-status\"\n role=\"status\"\n aria-live=\"polite\"\n className={cn(visible ? \"text-caption text-muted-foreground\" : \"sr-only\", className)}\n >\n {announced}\n </p>\n );\n}\n"],"mappings":";;;AAEA,SAAS,WAAW,QAAQ,gBAAgB;AAC5C,SAAS,UAAU;AAgEf;AA7DJ,IAAM,sBAAsB;AAuBrB,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA,aAAa;AAAA,EACb,UAAU;AAAA,EACV;AACF,GAAuB;AACrB,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,OAAO;AAClD,QAAM,YAAY,OAAO,CAAC;AAC1B,QAAM,aAAa,OAA6C,IAAI;AAEpE,YAAU,MAAM;AACd,QAAI,YAAY,UAAW;AAE3B,UAAM,UAAU,KAAK,IAAI,IAAI,UAAU;AACvC,QAAI,WAAW,YAAY;AACzB,gBAAU,UAAU,KAAK,IAAI;AAC7B,mBAAa,OAAO;AACpB;AAAA,IACF;AAIA,QAAI,WAAW,QAAS,cAAa,WAAW,OAAO;AACvD,eAAW,UAAU,WAAW,MAAM;AACpC,gBAAU,UAAU,KAAK,IAAI;AAC7B,iBAAW,UAAU;AACrB,mBAAa,OAAO;AAAA,IACtB,GAAG,aAAa,OAAO;AAEvB,WAAO,MAAM;AACX,UAAI,WAAW,SAAS;AACtB,qBAAa,WAAW,OAAO;AAC/B,mBAAW,UAAU;AAAA,MACvB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,SAAS,WAAW,UAAU,CAAC;AAEnC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAK;AAAA,MACL,aAAU;AAAA,MACV,WAAW,GAAG,UAAU,uCAAuC,WAAW,SAAS;AAAA,MAElF;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/lib/use-plan-projection.ts
|
|
4
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
5
|
+
function usePlanProjection({
|
|
6
|
+
map,
|
|
7
|
+
plan,
|
|
8
|
+
regions,
|
|
9
|
+
enabled = true,
|
|
10
|
+
onFrame
|
|
11
|
+
}) {
|
|
12
|
+
const elementsRef = useRef(/* @__PURE__ */ new Map());
|
|
13
|
+
const boxesRef = useRef(/* @__PURE__ */ new Map());
|
|
14
|
+
const frameRef = useRef(null);
|
|
15
|
+
const latestRef = useRef({ map, plan, regions, enabled, onFrame });
|
|
16
|
+
latestRef.current = { map, plan, regions, enabled, onFrame };
|
|
17
|
+
const write = useCallback(() => {
|
|
18
|
+
frameRef.current = null;
|
|
19
|
+
const {
|
|
20
|
+
map: currentMap,
|
|
21
|
+
plan: currentPlan,
|
|
22
|
+
regions: currentRegions,
|
|
23
|
+
enabled: isEnabled,
|
|
24
|
+
onFrame: notify
|
|
25
|
+
} = latestRef.current;
|
|
26
|
+
if (!currentMap || !currentPlan || !isEnabled) return;
|
|
27
|
+
const boxes = boxesRef.current;
|
|
28
|
+
boxes.clear();
|
|
29
|
+
for (const region of currentRegions) {
|
|
30
|
+
const element = elementsRef.current.get(region.id);
|
|
31
|
+
const { x, y, width, height } = region.bounds;
|
|
32
|
+
const corners = [
|
|
33
|
+
[x, y],
|
|
34
|
+
[x + width, y],
|
|
35
|
+
[x + width, y + height],
|
|
36
|
+
[x, y + height]
|
|
37
|
+
];
|
|
38
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
39
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
40
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
41
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
42
|
+
for (const [cornerX, cornerY] of corners) {
|
|
43
|
+
const point = currentMap.project(currentPlan.toLngLat({ x: cornerX, y: cornerY }));
|
|
44
|
+
if (point.x < minX) minX = point.x;
|
|
45
|
+
if (point.y < minY) minY = point.y;
|
|
46
|
+
if (point.x > maxX) maxX = point.x;
|
|
47
|
+
if (point.y > maxY) maxY = point.y;
|
|
48
|
+
}
|
|
49
|
+
const box = {
|
|
50
|
+
x: minX,
|
|
51
|
+
y: minY,
|
|
52
|
+
width: Math.max(1, maxX - minX),
|
|
53
|
+
height: Math.max(1, maxY - minY)
|
|
54
|
+
};
|
|
55
|
+
boxes.set(region.id, box);
|
|
56
|
+
if (!element) continue;
|
|
57
|
+
element.style.left = "0px";
|
|
58
|
+
element.style.top = "0px";
|
|
59
|
+
element.style.width = `${Math.round(box.width)}px`;
|
|
60
|
+
element.style.height = `${Math.round(box.height)}px`;
|
|
61
|
+
element.style.transform = `translate(${Math.round(box.x)}px, ${Math.round(box.y)}px)`;
|
|
62
|
+
}
|
|
63
|
+
notify?.(boxes);
|
|
64
|
+
}, []);
|
|
65
|
+
const refresh = useCallback(() => {
|
|
66
|
+
if (!latestRef.current.enabled) return;
|
|
67
|
+
if (frameRef.current != null) return;
|
|
68
|
+
if (typeof requestAnimationFrame !== "function") {
|
|
69
|
+
write();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
frameRef.current = requestAnimationFrame(write);
|
|
73
|
+
}, [write]);
|
|
74
|
+
const register = useCallback(
|
|
75
|
+
(id, element) => {
|
|
76
|
+
if (element) elementsRef.current.set(id, element);
|
|
77
|
+
else elementsRef.current.delete(id);
|
|
78
|
+
refresh();
|
|
79
|
+
},
|
|
80
|
+
[refresh]
|
|
81
|
+
);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!map || !plan || !enabled) return;
|
|
84
|
+
write();
|
|
85
|
+
map.on("move", refresh);
|
|
86
|
+
map.on("moveend", refresh);
|
|
87
|
+
map.on("resize", refresh);
|
|
88
|
+
return () => {
|
|
89
|
+
map.off("move", refresh);
|
|
90
|
+
map.off("moveend", refresh);
|
|
91
|
+
map.off("resize", refresh);
|
|
92
|
+
if (frameRef.current != null) {
|
|
93
|
+
cancelAnimationFrame(frameRef.current);
|
|
94
|
+
frameRef.current = null;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}, [map, plan, enabled, regions, refresh, write]);
|
|
98
|
+
return { register, refresh, boxes: boxesRef };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export {
|
|
102
|
+
usePlanProjection
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=chunk-MIMDP3EU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/use-plan-projection.ts"],"sourcesContent":["\"use client\";\n\nimport type MapLibreGL from \"maplibre-gl\";\nimport { useCallback, useEffect, useRef } from \"react\";\n\nimport type { PlanCrs } from \"./plan-crs\";\nimport type { MapPlanRegion } from \"./plan-regions\";\n\n/** Where a region ended up on screen, in CSS pixels relative to the canvas. */\nexport interface PlanScreenBox {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface UsePlanProjectionOptions {\n map: MapLibreGL.Map | null;\n plan: PlanCrs | null;\n /**\n * The regions to track. Keep this array MEMOIZED: it is an effect dependency,\n * and a fresh array every render re-binds the map listeners every render.\n */\n regions: readonly MapPlanRegion[];\n /** Turn tracking off (while the map is still loading, say). */\n enabled?: boolean;\n /** Called after each frame's DOM writes, with the boxes that were written. */\n onFrame?: (boxes: ReadonlyMap<string, PlanScreenBox>) => void;\n}\n\n/**\n * ONE projection pass for every plan overlay element, coalesced into a single\n * `requestAnimationFrame` and writing straight to the DOM — no React state per\n * frame, no per-element listener. A MapLibre `Marker` binds its own `move`\n * listener and projects itself, so forty rooms would mean eighty listeners and\n * forty uncoordinated writes; this is one listener set and one write pass.\n *\n * Each region is projected from its FOUR plan corners to a screen box, so the\n * box stays right even where the projection is not a plain scale.\n */\nexport function usePlanProjection({\n map,\n plan,\n regions,\n enabled = true,\n onFrame,\n}: UsePlanProjectionOptions) {\n const elementsRef = useRef(new Map<string, HTMLElement>());\n const boxesRef = useRef(new Map<string, PlanScreenBox>());\n const frameRef = useRef<number | null>(null);\n const latestRef = useRef({ map, plan, regions, enabled, onFrame });\n latestRef.current = { map, plan, regions, enabled, onFrame };\n\n /** The whole write pass. Reads refs only, so it never needs re-creating. */\n const write = useCallback(() => {\n frameRef.current = null;\n const {\n map: currentMap,\n plan: currentPlan,\n regions: currentRegions,\n enabled: isEnabled,\n onFrame: notify,\n } = latestRef.current;\n if (!currentMap || !currentPlan || !isEnabled) return;\n\n const boxes = boxesRef.current;\n boxes.clear();\n\n for (const region of currentRegions) {\n const element = elementsRef.current.get(region.id);\n const { x, y, width, height } = region.bounds;\n const corners: [number, number][] = [\n [x, y],\n [x + width, y],\n [x + width, y + height],\n [x, y + height],\n ];\n\n let minX = Number.POSITIVE_INFINITY;\n let minY = Number.POSITIVE_INFINITY;\n let maxX = Number.NEGATIVE_INFINITY;\n let maxY = Number.NEGATIVE_INFINITY;\n for (const [cornerX, cornerY] of corners) {\n const point = currentMap.project(currentPlan.toLngLat({ x: cornerX, y: cornerY }));\n if (point.x < minX) minX = point.x;\n if (point.y < minY) minY = point.y;\n if (point.x > maxX) maxX = point.x;\n if (point.y > maxY) maxY = point.y;\n }\n\n const box: PlanScreenBox = {\n x: minX,\n y: minY,\n width: Math.max(1, maxX - minX),\n height: Math.max(1, maxY - minY),\n };\n boxes.set(region.id, box);\n\n if (!element) continue;\n // Physical `left`/`top`, deliberately: `map.project` returns screen\n // pixels, which do not mirror in RTL — a logical inset would put every\n // region on the wrong side of the canvas.\n element.style.left = \"0px\";\n element.style.top = \"0px\";\n element.style.width = `${Math.round(box.width)}px`;\n element.style.height = `${Math.round(box.height)}px`;\n element.style.transform = `translate(${Math.round(box.x)}px, ${Math.round(box.y)}px)`;\n }\n\n notify?.(boxes);\n }, []);\n\n /** Ask for a frame. Repeated calls inside one frame collapse into one pass. */\n const refresh = useCallback(() => {\n if (!latestRef.current.enabled) return;\n if (frameRef.current != null) return;\n if (typeof requestAnimationFrame !== \"function\") {\n write();\n return;\n }\n frameRef.current = requestAnimationFrame(write);\n }, [write]);\n\n /** `ref` callback for a region's element. */\n const register = useCallback(\n (id: string, element: HTMLElement | null) => {\n if (element) elementsRef.current.set(id, element);\n else elementsRef.current.delete(id);\n refresh();\n },\n [refresh],\n );\n\n useEffect(() => {\n if (!map || !plan || !enabled) return;\n\n // Write once synchronously, so the first paint already has the geometry\n // rather than a stack of boxes at the origin.\n write();\n\n map.on(\"move\", refresh);\n map.on(\"moveend\", refresh);\n map.on(\"resize\", refresh);\n\n return () => {\n map.off(\"move\", refresh);\n map.off(\"moveend\", refresh);\n map.off(\"resize\", refresh);\n if (frameRef.current != null) {\n cancelAnimationFrame(frameRef.current);\n frameRef.current = null;\n }\n };\n }, [map, plan, enabled, regions, refresh, write]);\n\n return { register, refresh, boxes: boxesRef };\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":[]}
|