@elabs-ai/components-maps 4.1.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 -1456
- 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 +421 -1
- package/src/map-canvas/map-canvas.tsx +466 -20
- 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 +383 -3
- package/src/map-marker/map-marker.tsx +414 -44
- 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 +18 -11
- 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 +267 -20
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/use-map-breakpoint.ts"],"sourcesContent":["\"use client\";\n\n/**\n * The map's responsive contract: container tiers, `MapResponsive<T>` and the\n * frame context `<MapCanvas>` provides to its furniture (RM-125).\n *\n * A DELIBERATE COPY of `@elabs-ai/components-charts`' `chart-breakpoint.ts`\n * (ADR 0039), with the same thresholds and the same desktop-first\n * `{ base, medium?, narrow? }` shape. `maps` may not import `charts` (both are\n * layer-2 leaves — `pnpm check --rule dep-direction`), so the few lines live\n * here too. Change a threshold in both files or in neither.\n *\n * - Three container tiers, measured on the MAP's own width, never the\n * viewport: `narrow` (< 480 px), `medium` (< 768 px), `wide`.\n * - `MapResponsive<T>`: one value, or `{ base, medium?, narrow? }` — an\n * override applies at its tier and every narrower tier that sets nothing.\n * A plain value structurally matches `charts`' `Responsive<T>`, so one\n * object can drive a chart and a map.\n */\n\nimport {\n type CSSProperties,\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useState,\n} from \"react\";\n\n// ── Tiers ────────────────────────────────────────────────────────────────────\n\n/** A map container's width tier — the same three tiers as a chart's. */\nexport type MapBreakpoint = \"narrow\" | \"medium\" | \"wide\";\n\n/** Every tier, narrowest first. */\nexport const MAP_BREAKPOINTS: readonly MapBreakpoint[] = [\"narrow\", \"medium\", \"wide\"];\n\n/**\n * A width (CSS px) below `narrow` is narrow; below `medium` is medium; anything\n * else is wide. A boundary belongs to the wider tier (480 → medium). Same\n * numbers as `CHART_BREAKPOINT_THRESHOLDS`.\n */\nexport const MAP_BREAKPOINT_THRESHOLDS = { narrow: 480, medium: 768 } as const;\n\n/**\n * The tier for a measured container width. `0` or a non-finite width means\n * \"not measured yet\" (server render, first pass, `display: none`) and\n * resolves to `wide`, so a first paint matches the pre-tier layout.\n */\nexport function mapBreakpointForWidth(width: number): MapBreakpoint {\n if (!Number.isFinite(width) || width <= 0) return \"wide\";\n if (width < MAP_BREAKPOINT_THRESHOLDS.narrow) return \"narrow\";\n if (width < MAP_BREAKPOINT_THRESHOLDS.medium) return \"medium\";\n return \"wide\";\n}\n\n// ── MapResponsive<T> ─────────────────────────────────────────────────────────\n\n/**\n * Per-tier values, desktop-first: `base` is the WIDE value (the opposite of\n * Tailwind's unprefixed, mobile-first class); overrides go down.\n */\nexport interface MapResponsiveByBreakpoint<T> {\n /** The value at `wide`, and the fallback for every tier that sets nothing. */\n base: T;\n /** At `medium` — and at `narrow` too, unless `narrow` is set. */\n medium?: T;\n /** At `narrow` only. */\n narrow?: T;\n}\n\n/**\n * One value for every tier, or per-tier values. `T` must never be an object\n * type with its own `base` key (that key marks the per-tier form). Read it\n * through {@link resolveMapResponsive} / {@link useMapResponsive}.\n */\nexport type MapResponsive<T> = T | MapResponsiveByBreakpoint<T>;\n\n/** True for the per-tier form: a plain object with its own `base` key. */\nexport function isMapResponsiveByBreakpoint<T>(\n value: MapResponsive<T>,\n): value is MapResponsiveByBreakpoint<T> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n Object.prototype.hasOwnProperty.call(value, \"base\")\n );\n}\n\n/**\n * The value for `breakpoint`: `wide` → `base`; `medium` → `medium ?? base`;\n * `narrow` → `narrow ?? medium ?? base`. \"Not set\" means `undefined`.\n */\nexport function resolveMapResponsive<T>(value: MapResponsive<T>, breakpoint: MapBreakpoint): T {\n if (!isMapResponsiveByBreakpoint(value)) return value as T;\n if (breakpoint === \"wide\") return value.base;\n if (breakpoint === \"narrow\" && value.narrow !== undefined) return value.narrow;\n return value.medium !== undefined ? value.medium : value.base;\n}\n\n// ── Height ───────────────────────────────────────────────────────────────────\n\n/** CSS px, or the map's width ÷ height (`{ aspect: 2 }` = twice as wide as tall). */\nexport type MapHeight = number | { aspect: number };\n\n/**\n * The height a `<MapCanvas>` falls back to when its parent gives it none:\n * 1.6 : 1, and square at `narrow` — an aspect-locked map at 380 px would\n * otherwise shrink to a strip nobody can read.\n */\nexport const DEFAULT_MAP_HEIGHT: MapResponsiveByBreakpoint<MapHeight> = {\n base: { aspect: 1.6 },\n narrow: { aspect: 1 },\n};\n\nfunction validHeight(value: MapHeight | undefined): MapHeight | undefined {\n if (typeof value === \"number\") return Number.isFinite(value) && value > 0 ? value : undefined;\n if (value && Number.isFinite(value.aspect) && value.aspect > 0) return value;\n return undefined;\n}\n\n/**\n * The map box's geometry is LAYOUT THE ENGINE MEASURES, so it must never\n * interpolate. `transition-property` defaults to `all`, and the house\n * reduced-motion clamp sets `transition-duration: 0.01ms !important` on every\n * element — which turns a height / aspect-ratio change into a (very short)\n * transition that still lands one animation frame later. Under a busy WebGL\n * first paint that frame arrived ~0.7 s late, so MapLibre measured the box\n * mid-transition (348×218 on a box that ends up 348×348) and fitted `bounds`\n * to a rectangle that never existed. `none` beats any stylesheet from the\n * inline style, and the clamp only forces DURATIONS, never the property list.\n */\nconst NO_GEOMETRY_TRANSITION = { transitionProperty: \"none\" } as const;\n\n/**\n * The container style for a `height` at `breakpoint`. An explicit `height`\n * sets the box outright (`height: auto` releases the `h-full` class for an\n * aspect). Without one, only an `aspect-ratio` is set — CSS ignores it while\n * the parent gives the map a definite height, so a map in a sized parent\n * looks exactly as before; a map in an unsized parent stops collapsing to 0.\n * Every branch pins {@link NO_GEOMETRY_TRANSITION} so the box resizes in the\n * same frame the rule changes.\n */\nexport function resolveMapHeightStyle(\n height: MapResponsive<MapHeight> | undefined,\n breakpoint: MapBreakpoint,\n): CSSProperties {\n const own =\n height === undefined ? undefined : validHeight(resolveMapResponsive(height, breakpoint));\n if (typeof own === \"number\") return { ...NO_GEOMETRY_TRANSITION, height: own };\n if (own) return { ...NO_GEOMETRY_TRANSITION, height: \"auto\", aspectRatio: `${own.aspect} / 1` };\n const fallback = resolveMapResponsive(DEFAULT_MAP_HEIGHT, breakpoint);\n return typeof fallback === \"number\"\n ? { ...NO_GEOMETRY_TRANSITION }\n : { ...NO_GEOMETRY_TRANSITION, aspectRatio: `${fallback.aspect} / 1` };\n}\n\n// ── Measuring ────────────────────────────────────────────────────────────────\n\n/**\n * Measures `node` with a `ResizeObserver` and returns its tier. Re-renders\n * only when the TIER changes.\n */\nexport function useMeasuredMapBreakpoint(node: Element | null): MapBreakpoint {\n const [breakpoint, setBreakpoint] = useState<MapBreakpoint>(\"wide\");\n // Layout effect: the first measure lands before paint, so a narrow map never\n // flashes its wide furniture.\n useLayoutEffect(() => {\n if (!node) return undefined;\n const update = (width: number) => {\n const next = mapBreakpointForWidth(width);\n setBreakpoint((prev) => (prev === next ? prev : next));\n };\n update(node.getBoundingClientRect().width);\n if (typeof ResizeObserver === \"undefined\") return undefined;\n const observer = new ResizeObserver((entries) => {\n const entry = entries[entries.length - 1];\n if (entry) update(entry.contentRect.width);\n });\n observer.observe(node);\n return () => observer.disconnect();\n }, [node]);\n return breakpoint;\n}\n\n// ── Frame context ────────────────────────────────────────────────────────────\n\n/** Where furniture outside the map box goes: the strip above or below it. */\nexport type MapFrameSide = \"above\" | \"below\";\n\nexport interface MapFrameContextValue {\n /** The map container's measured tier. */\n breakpoint: MapBreakpoint;\n /** `false` when the canvas is static (`<MapCanvas interactive={false}>`). */\n interactive: boolean;\n /** The strips around the map box; `null` until something asks for one. */\n slots: Record<MapFrameSide, HTMLElement | null>;\n /** Ask the canvas to render a strip; returns the release function. */\n requestSlot: (side: MapFrameSide) => () => void;\n}\n\nconst noopRelease = () => {};\n\nconst DEFAULT_FRAME: MapFrameContextValue = {\n breakpoint: \"wide\",\n interactive: true,\n slots: { above: null, below: null },\n requestSlot: () => noopRelease,\n};\n\nexport const MapFrameContext = createContext<MapFrameContextValue>(DEFAULT_FRAME);\n\n/** The frame `<MapCanvas>` provides: tier, static flag, outside strips. */\nexport function useMapFrame(): MapFrameContextValue {\n return useContext(MapFrameContext);\n}\n\n/** The enclosing map's tier (`wide` outside a `<MapCanvas>`). */\nexport function useMapBreakpoint(): MapBreakpoint {\n return useContext(MapFrameContext).breakpoint;\n}\n\n/** `value` resolved at the enclosing map's tier. */\nexport function useMapResponsive<T>(value: MapResponsive<T>): T {\n return resolveMapResponsive(value, useMapBreakpoint());\n}\n\n/**\n * The strip above / below the map box to portal into, or `null` while it is\n * not needed (`side` is `null`) or not mounted yet. Asking renders the strip;\n * the canvas drops it again when nothing asks.\n */\nexport function useMapFrameSlot(side: MapFrameSide | null): HTMLElement | null {\n const { slots, requestSlot } = useContext(MapFrameContext);\n useEffect(() => (side ? requestSlot(side) : undefined), [side, requestSlot]);\n return side ? slots[side] : null;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/lib/plan-regions.ts
|
|
4
|
+
function planRegionCentre(bounds) {
|
|
5
|
+
return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
|
|
6
|
+
}
|
|
7
|
+
function walkCoordinates(input, visit) {
|
|
8
|
+
if (!Array.isArray(input)) return;
|
|
9
|
+
if (typeof input[0] === "number" && typeof input[1] === "number") {
|
|
10
|
+
visit(input[0], input[1]);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
for (const entry of input) walkCoordinates(entry, visit);
|
|
14
|
+
}
|
|
15
|
+
function planBoundsOfGeometry(geometry) {
|
|
16
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
17
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
18
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
19
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
20
|
+
const visit = (x, y) => {
|
|
21
|
+
if (x < minX) minX = x;
|
|
22
|
+
if (y < minY) minY = y;
|
|
23
|
+
if (x > maxX) maxX = x;
|
|
24
|
+
if (y > maxY) maxY = y;
|
|
25
|
+
};
|
|
26
|
+
if (geometry.type === "GeometryCollection") {
|
|
27
|
+
for (const member of geometry.geometries) {
|
|
28
|
+
walkCoordinates(member.coordinates, visit);
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
walkCoordinates(geometry.coordinates, visit);
|
|
32
|
+
}
|
|
33
|
+
if (!Number.isFinite(minX) || !Number.isFinite(minY)) return null;
|
|
34
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
35
|
+
}
|
|
36
|
+
function read(field, properties, index) {
|
|
37
|
+
if (field === void 0) return void 0;
|
|
38
|
+
if (typeof field === "function") return field(properties, index);
|
|
39
|
+
return properties?.[field];
|
|
40
|
+
}
|
|
41
|
+
function planRegionsFromGeoJSON(data, fields) {
|
|
42
|
+
const regions = [];
|
|
43
|
+
data.features.forEach((feature, index) => {
|
|
44
|
+
const bounds = planBoundsOfGeometry(feature.geometry);
|
|
45
|
+
if (!bounds) return;
|
|
46
|
+
const properties = feature.properties;
|
|
47
|
+
const id = read(fields.id, properties, index) ?? (feature.id != null ? String(feature.id) : null);
|
|
48
|
+
if (id == null) return;
|
|
49
|
+
const shortLabel = read(fields.shortLabel, properties, index);
|
|
50
|
+
const description = read(fields.description, properties, index);
|
|
51
|
+
const group = read(fields.group, properties, index);
|
|
52
|
+
const groupLabel = read(fields.groupLabel, properties, index);
|
|
53
|
+
regions.push({
|
|
54
|
+
id: String(id),
|
|
55
|
+
label: String(read(fields.label, properties, index) ?? id),
|
|
56
|
+
...shortLabel !== void 0 ? { shortLabel: String(shortLabel) } : {},
|
|
57
|
+
...description !== void 0 ? { description: String(description) } : {},
|
|
58
|
+
...group !== void 0 ? { group: String(group) } : {},
|
|
59
|
+
...groupLabel !== void 0 ? { groupLabel: String(groupLabel) } : {},
|
|
60
|
+
bounds
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
return regions;
|
|
64
|
+
}
|
|
65
|
+
function planGroupId(group) {
|
|
66
|
+
return `group:${group}`;
|
|
67
|
+
}
|
|
68
|
+
function planBoundsUnion(boxes) {
|
|
69
|
+
if (boxes.length === 0) return null;
|
|
70
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
71
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
72
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
73
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
74
|
+
for (const box of boxes) {
|
|
75
|
+
if (box.x < minX) minX = box.x;
|
|
76
|
+
if (box.y < minY) minY = box.y;
|
|
77
|
+
if (box.x + box.width > maxX) maxX = box.x + box.width;
|
|
78
|
+
if (box.y + box.height > maxY) maxY = box.y + box.height;
|
|
79
|
+
}
|
|
80
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
81
|
+
}
|
|
82
|
+
function planGroupsFromRegions(regions) {
|
|
83
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
84
|
+
for (const region of regions) {
|
|
85
|
+
const key = region.group ?? region.id;
|
|
86
|
+
const members = byKey.get(key);
|
|
87
|
+
if (members) members.push(region);
|
|
88
|
+
else byKey.set(key, [region]);
|
|
89
|
+
}
|
|
90
|
+
const groups = [];
|
|
91
|
+
for (const [key, members] of byKey) {
|
|
92
|
+
const bounds = planBoundsUnion(members.map((member) => member.bounds));
|
|
93
|
+
if (!bounds) continue;
|
|
94
|
+
const first = members[0];
|
|
95
|
+
groups.push({
|
|
96
|
+
key,
|
|
97
|
+
region: {
|
|
98
|
+
id: planGroupId(key),
|
|
99
|
+
label: first.groupLabel ?? key,
|
|
100
|
+
bounds
|
|
101
|
+
},
|
|
102
|
+
members
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return groups;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
planRegionCentre,
|
|
110
|
+
planBoundsOfGeometry,
|
|
111
|
+
planRegionsFromGeoJSON,
|
|
112
|
+
planGroupId,
|
|
113
|
+
planBoundsUnion,
|
|
114
|
+
planGroupsFromRegions
|
|
115
|
+
};
|
|
116
|
+
//# sourceMappingURL=chunk-WNU4IOGL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/plan-regions.ts"],"sourcesContent":["import type { PlanPoint } from \"./plan-crs\";\n\n/** An axis-aligned rectangle in PLAN units. */\nexport interface PlanRegionBounds {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * One interactive area of a plan — a room, a machine cell, a seat. The overlay\n * needs a box and a name, not the geometry: a room's shape is drawn by WebGL,\n * while the keyboard proxy, the label and the readout all work from its box.\n */\nexport interface MapPlanRegion {\n /** Stable id. Must match the feature id the shape layer promotes. */\n id: string;\n /** The visible label, and the start of the button's accessible name. */\n label: string;\n /** Shown instead of `label` when the box is too small for the full one. */\n shortLabel?: string;\n /** Appended to the accessible name — a status word, a seat count, an owner. */\n description?: string;\n /** The region's box, in plan units. */\n bounds: PlanRegionBounds;\n /** Group id, for plans with more regions than a keyboard can walk (a carriage, a wing). */\n group?: string;\n /** The group's own label, used by group mode. */\n groupLabel?: string;\n}\n\n/** The centre of a region's box, in plan units. */\nexport function planRegionCentre(bounds: PlanRegionBounds): PlanPoint {\n return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };\n}\n\nfunction walkCoordinates(input: unknown, visit: (x: number, y: number) => void): void {\n if (!Array.isArray(input)) return;\n if (typeof input[0] === \"number\" && typeof input[1] === \"number\") {\n visit(input[0], input[1]);\n return;\n }\n for (const entry of input) walkCoordinates(entry, visit);\n}\n\n/** The bounding box of any geometry, in the units its coordinates are written in. */\nexport function planBoundsOfGeometry(geometry: GeoJSON.Geometry): PlanRegionBounds | null {\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\n const visit = (x: number, y: number) => {\n if (x < minX) minX = x;\n if (y < minY) minY = y;\n if (x > maxX) maxX = x;\n if (y > maxY) maxY = y;\n };\n\n if (geometry.type === \"GeometryCollection\") {\n for (const member of geometry.geometries) {\n walkCoordinates((member as { coordinates?: unknown }).coordinates, visit);\n }\n } else {\n walkCoordinates(geometry.coordinates, visit);\n }\n\n if (!Number.isFinite(minX) || !Number.isFinite(minY)) return null;\n return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n}\n\n/** A field read either from a feature property or computed from the properties. */\nexport type PlanRegionField<P, T> = string | ((properties: P, index: number) => T);\n\nexport interface PlanRegionFields<P extends GeoJSON.GeoJsonProperties> {\n /** Property name, or a function, giving each region's id. */\n id: PlanRegionField<P, string>;\n /** Property name, or a function, giving each region's visible label. */\n label: PlanRegionField<P, string>;\n /** Optional short label for a small box. */\n shortLabel?: PlanRegionField<P, string | undefined>;\n /** Optional extra words for the accessible name. */\n description?: PlanRegionField<P, string | undefined>;\n /** Optional group id. */\n group?: PlanRegionField<P, string | undefined>;\n /** Optional group label. */\n groupLabel?: PlanRegionField<P, string | undefined>;\n}\n\nfunction read<P extends GeoJSON.GeoJsonProperties, T>(\n field: PlanRegionField<P, T> | undefined,\n properties: P,\n index: number,\n): T | undefined {\n if (field === undefined) return undefined;\n if (typeof field === \"function\") return field(properties, index);\n return (properties as Record<string, unknown> | null)?.[field] as T | undefined;\n}\n\n/**\n * Derive the overlay's regions from the same GeoJSON the shape layer draws, so\n * the keyboard proxy can never drift out of step with the picture.\n *\n * ```ts\n * const regions = planRegionsFromGeoJSON(rooms, { id: \"id\", label: \"name\" });\n * ```\n */\nexport function planRegionsFromGeoJSON<P extends GeoJSON.GeoJsonProperties>(\n data: GeoJSON.FeatureCollection<GeoJSON.Geometry, P>,\n fields: PlanRegionFields<P>,\n): MapPlanRegion[] {\n const regions: MapPlanRegion[] = [];\n\n data.features.forEach((feature, index) => {\n const bounds = planBoundsOfGeometry(feature.geometry);\n if (!bounds) return;\n const properties = feature.properties;\n const id =\n read(fields.id, properties, index) ?? (feature.id != null ? String(feature.id) : null);\n if (id == null) return;\n\n const shortLabel = read(fields.shortLabel, properties, index);\n const description = read(fields.description, properties, index);\n const group = read(fields.group, properties, index);\n const groupLabel = read(fields.groupLabel, properties, index);\n\n regions.push({\n id: String(id),\n label: String(read(fields.label, properties, index) ?? id),\n ...(shortLabel !== undefined ? { shortLabel: String(shortLabel) } : {}),\n ...(description !== undefined ? { description: String(description) } : {}),\n ...(group !== undefined ? { group: String(group) } : {}),\n ...(groupLabel !== undefined ? { groupLabel: String(groupLabel) } : {}),\n bounds,\n });\n });\n\n return regions;\n}\n\n/** The id the overlay gives a group's own button, namespaced away from region ids. */\nexport function planGroupId(group: string): string {\n return `group:${group}`;\n}\n\n/** One group of regions, with the synthetic region that stands for the whole of it. */\nexport interface MapPlanGroup {\n /** The group's key, as it appeared on its regions. */\n key: string;\n /** A region covering every member's box — what group mode puts a button on. */\n region: MapPlanRegion;\n /** The group's regions, in reading order. */\n members: readonly MapPlanRegion[];\n}\n\n/** The smallest box containing all of them. `null` for an empty list. */\nexport function planBoundsUnion(boxes: readonly PlanRegionBounds[]): PlanRegionBounds | null {\n if (boxes.length === 0) return null;\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\n for (const box of boxes) {\n if (box.x < minX) minX = box.x;\n if (box.y < minY) minY = box.y;\n if (box.x + box.width > maxX) maxX = box.x + box.width;\n if (box.y + box.height > maxY) maxY = box.y + box.height;\n }\n\n return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n}\n\n/**\n * Bundle regions by their `group`, in first-seen order.\n *\n * A region with no `group` becomes a group of one, keyed by its own id: group\n * mode then still shows everything, instead of quietly dropping whatever was\n * left ungrouped.\n */\nexport function planGroupsFromRegions(regions: readonly MapPlanRegion[]): MapPlanGroup[] {\n const byKey = new Map<string, MapPlanRegion[]>();\n\n for (const region of regions) {\n const key = region.group ?? region.id;\n const members = byKey.get(key);\n if (members) members.push(region);\n else byKey.set(key, [region]);\n }\n\n const groups: MapPlanGroup[] = [];\n for (const [key, members] of byKey) {\n const bounds = planBoundsUnion(members.map((member) => member.bounds));\n if (!bounds) continue;\n const first = members[0]!;\n groups.push({\n key,\n region: {\n id: planGroupId(key),\n label: first.groupLabel ?? key,\n bounds,\n },\n members,\n });\n }\n\n return groups;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,494 @@
|
|
|
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
|
+
import {
|
|
12
|
+
mergeFeatureStatePaint
|
|
13
|
+
} from "./chunk-RNSLG64F.js";
|
|
14
|
+
|
|
15
|
+
// src/map-geojson/map-geojson.tsx
|
|
16
|
+
import { useEffect, useId, useMemo, useRef } from "react";
|
|
17
|
+
import { jsx } from "react/jsx-runtime";
|
|
18
|
+
var DEFAULT_HIT_WIDTH = 16;
|
|
19
|
+
var DEFAULT_PLAN_PICK_PADDING = 8;
|
|
20
|
+
function MapGeoJSON({
|
|
21
|
+
data,
|
|
22
|
+
id: propId,
|
|
23
|
+
promoteId,
|
|
24
|
+
fillPaint,
|
|
25
|
+
linePaint,
|
|
26
|
+
fillHoverPaint,
|
|
27
|
+
fillOpacity,
|
|
28
|
+
pattern,
|
|
29
|
+
vignette,
|
|
30
|
+
lineHoverPaint,
|
|
31
|
+
fillSelectedPaint,
|
|
32
|
+
lineSelectedPaint,
|
|
33
|
+
selectedId,
|
|
34
|
+
hoveredId,
|
|
35
|
+
onClick,
|
|
36
|
+
onHover,
|
|
37
|
+
interactive: interactiveProp,
|
|
38
|
+
featureLabel,
|
|
39
|
+
pickPadding,
|
|
40
|
+
hitWidth = DEFAULT_HIT_WIDTH,
|
|
41
|
+
beforeId
|
|
42
|
+
}) {
|
|
43
|
+
const { map, isLoaded, plan } = useMap();
|
|
44
|
+
const autoId = useId();
|
|
45
|
+
const id = propId ?? autoId;
|
|
46
|
+
const sourceId = `geojson-source-${id}`;
|
|
47
|
+
const fillLayerId = `geojson-fill-${id}`;
|
|
48
|
+
const lineLayerId = `geojson-line-${id}`;
|
|
49
|
+
const patternLayerId = `geojson-pattern-${id}`;
|
|
50
|
+
const vignetteLayerId = `geojson-vignette-${id}`;
|
|
51
|
+
const patternImageId = `geojson-pattern-image-${id}`;
|
|
52
|
+
const hitLayerId = `geojson-hit-${id}`;
|
|
53
|
+
const interactive = interactiveProp ?? plan != null;
|
|
54
|
+
const pickPad = pickPadding ?? (plan ? DEFAULT_PLAN_PICK_PADDING : 0);
|
|
55
|
+
const defaultFill = useTokenColor("--border");
|
|
56
|
+
const defaultLine = useTokenColor("--background");
|
|
57
|
+
const defaultInk = useTokenColor("--foreground");
|
|
58
|
+
const showFill = fillPaint !== false;
|
|
59
|
+
const showLine = linePaint !== false;
|
|
60
|
+
const showHit = interactive && !showFill;
|
|
61
|
+
if (plan && typeof data === "string") {
|
|
62
|
+
warnMapOnce(
|
|
63
|
+
"geojson-plan-url",
|
|
64
|
+
"<MapGeoJSON data={url}> cannot be converted to plan units. Fetch the GeoJSON yourself and pass the object, or pass coordinates already in lng/lat."
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (interactive && !promoteId) {
|
|
68
|
+
warnMapOnce(
|
|
69
|
+
"geojson-promote-id",
|
|
70
|
+
"<MapGeoJSON interactive> needs `promoteId` to tell features apart: without it hover and selection paint nothing."
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const resolvedData = useMemo(
|
|
74
|
+
() => plan && typeof data !== "string" ? plan.toGeoJSON(data) : data,
|
|
75
|
+
[plan, data]
|
|
76
|
+
);
|
|
77
|
+
const baseOpacity = fillOpacity ?? (pattern ? 0.25 : void 0);
|
|
78
|
+
const mergedFillPaint = useMemo(
|
|
79
|
+
() => mergeFeatureStatePaint(
|
|
80
|
+
{
|
|
81
|
+
"fill-color": defaultFill,
|
|
82
|
+
...baseOpacity !== void 0 ? { "fill-opacity": baseOpacity } : {},
|
|
83
|
+
...fillPaint || {}
|
|
84
|
+
},
|
|
85
|
+
{ hover: fillHoverPaint, selected: fillSelectedPaint }
|
|
86
|
+
),
|
|
87
|
+
[defaultFill, baseOpacity, fillPaint, fillHoverPaint, fillSelectedPaint]
|
|
88
|
+
);
|
|
89
|
+
const configuredFill = fillPaint ? fillPaint["fill-color"] : void 0;
|
|
90
|
+
const areaColor = configuredFill === void 0 ? defaultFill : typeof configuredFill === "string" ? configuredFill : defaultInk;
|
|
91
|
+
const stripeWidth = Math.max(1, pattern?.width ?? 2);
|
|
92
|
+
const stripeGap = Math.max(0, pattern?.gap ?? 4);
|
|
93
|
+
const vignetteWidth = Math.max(0, vignette?.width ?? 12);
|
|
94
|
+
const vignetteOpacity = Math.min(1, Math.max(0, vignette?.opacity ?? 0.35));
|
|
95
|
+
const hasPattern = pattern !== void 0;
|
|
96
|
+
const hasVignette = vignette !== void 0;
|
|
97
|
+
const patternDrawnRef = useRef(null);
|
|
98
|
+
const mergedLinePaint = useMemo(
|
|
99
|
+
() => mergeFeatureStatePaint(
|
|
100
|
+
{
|
|
101
|
+
"line-color": defaultLine,
|
|
102
|
+
"line-width": 0.5,
|
|
103
|
+
...linePaint || {}
|
|
104
|
+
},
|
|
105
|
+
{ hover: lineHoverPaint, selected: lineSelectedPaint }
|
|
106
|
+
),
|
|
107
|
+
[defaultLine, linePaint, lineHoverPaint, lineSelectedPaint]
|
|
108
|
+
);
|
|
109
|
+
const latestRef = useRef({ onClick, onHover, plan });
|
|
110
|
+
latestRef.current = { onClick, onHover, plan };
|
|
111
|
+
const setHoverRef = useRef(null);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (!isLoaded || !map) return;
|
|
114
|
+
map.addSource(sourceId, {
|
|
115
|
+
type: "geojson",
|
|
116
|
+
data: resolvedData,
|
|
117
|
+
...promoteId ? { promoteId } : {}
|
|
118
|
+
});
|
|
119
|
+
return () => {
|
|
120
|
+
try {
|
|
121
|
+
if (map.getLayer(hitLayerId)) map.removeLayer(hitLayerId);
|
|
122
|
+
if (map.getLayer(lineLayerId)) map.removeLayer(lineLayerId);
|
|
123
|
+
if (map.getLayer(patternLayerId)) map.removeLayer(patternLayerId);
|
|
124
|
+
if (map.getLayer(fillLayerId)) map.removeLayer(fillLayerId);
|
|
125
|
+
if (map.getLayer(vignetteLayerId)) map.removeLayer(vignetteLayerId);
|
|
126
|
+
if (map.hasImage(patternImageId)) map.removeImage(patternImageId);
|
|
127
|
+
if (map.getSource(sourceId)) map.removeSource(sourceId);
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}, [isLoaded, map]);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!isLoaded || !map) return;
|
|
134
|
+
const source = map.getSource(sourceId);
|
|
135
|
+
source?.setData(resolvedData);
|
|
136
|
+
}, [isLoaded, map, resolvedData, sourceId]);
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!isLoaded || !map) return;
|
|
139
|
+
const source = map.getSource(sourceId);
|
|
140
|
+
if (!source) return;
|
|
141
|
+
if (showFill && !map.getLayer(fillLayerId)) {
|
|
142
|
+
map.addLayer(
|
|
143
|
+
{
|
|
144
|
+
id: fillLayerId,
|
|
145
|
+
type: "fill",
|
|
146
|
+
source: sourceId,
|
|
147
|
+
paint: mergedFillPaint
|
|
148
|
+
},
|
|
149
|
+
beforeId
|
|
150
|
+
);
|
|
151
|
+
} else if (!showFill && map.getLayer(fillLayerId)) {
|
|
152
|
+
map.removeLayer(fillLayerId);
|
|
153
|
+
}
|
|
154
|
+
if (showLine && !map.getLayer(lineLayerId)) {
|
|
155
|
+
map.addLayer(
|
|
156
|
+
{
|
|
157
|
+
id: lineLayerId,
|
|
158
|
+
type: "line",
|
|
159
|
+
source: sourceId,
|
|
160
|
+
paint: mergedLinePaint
|
|
161
|
+
},
|
|
162
|
+
beforeId
|
|
163
|
+
);
|
|
164
|
+
} else if (!showLine && map.getLayer(lineLayerId)) {
|
|
165
|
+
map.removeLayer(lineLayerId);
|
|
166
|
+
}
|
|
167
|
+
if (showHit && !map.getLayer(hitLayerId)) {
|
|
168
|
+
map.addLayer(
|
|
169
|
+
{
|
|
170
|
+
id: hitLayerId,
|
|
171
|
+
type: "line",
|
|
172
|
+
source: sourceId,
|
|
173
|
+
paint: { "line-color": defaultLine, "line-opacity": 0, "line-width": hitWidth }
|
|
174
|
+
},
|
|
175
|
+
beforeId
|
|
176
|
+
);
|
|
177
|
+
} else if (!showHit && map.getLayer(hitLayerId)) {
|
|
178
|
+
map.removeLayer(hitLayerId);
|
|
179
|
+
}
|
|
180
|
+
if (showFill && map.getLayer(fillLayerId)) {
|
|
181
|
+
for (const [key, value] of Object.entries(mergedFillPaint)) {
|
|
182
|
+
map.setPaintProperty(fillLayerId, key, value);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (showLine && map.getLayer(lineLayerId)) {
|
|
186
|
+
for (const [key, value] of Object.entries(mergedLinePaint)) {
|
|
187
|
+
map.setPaintProperty(lineLayerId, key, value);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const vignettePaint = {
|
|
191
|
+
"line-color": areaColor,
|
|
192
|
+
"line-width": vignetteWidth * 2,
|
|
193
|
+
"line-blur": vignetteWidth,
|
|
194
|
+
"line-opacity": vignetteOpacity
|
|
195
|
+
};
|
|
196
|
+
if (hasVignette && !map.getLayer(vignetteLayerId)) {
|
|
197
|
+
const before = map.getLayer(fillLayerId) ? fillLayerId : map.getLayer(lineLayerId) ? lineLayerId : beforeId;
|
|
198
|
+
map.addLayer(
|
|
199
|
+
{ id: vignetteLayerId, type: "line", source: sourceId, paint: vignettePaint },
|
|
200
|
+
before
|
|
201
|
+
);
|
|
202
|
+
} else if (hasVignette) {
|
|
203
|
+
for (const [key, value] of Object.entries(vignettePaint)) {
|
|
204
|
+
map.setPaintProperty(vignetteLayerId, key, value);
|
|
205
|
+
}
|
|
206
|
+
} else if (map.getLayer(vignetteLayerId)) {
|
|
207
|
+
map.removeLayer(vignetteLayerId);
|
|
208
|
+
}
|
|
209
|
+
if (hasPattern) {
|
|
210
|
+
const drawKey = `${areaColor}|${stripeWidth}|${stripeGap}`;
|
|
211
|
+
if (patternDrawnRef.current !== drawKey || !map.hasImage(patternImageId)) {
|
|
212
|
+
const image = stripeImage(areaColor, stripeWidth, stripeGap);
|
|
213
|
+
if (image) {
|
|
214
|
+
if (map.hasImage(patternImageId)) map.removeImage(patternImageId);
|
|
215
|
+
map.addImage(patternImageId, image.data, { pixelRatio: image.pixelRatio });
|
|
216
|
+
patternDrawnRef.current = drawKey;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (map.hasImage(patternImageId)) {
|
|
220
|
+
if (!map.getLayer(patternLayerId)) {
|
|
221
|
+
map.addLayer(
|
|
222
|
+
{
|
|
223
|
+
id: patternLayerId,
|
|
224
|
+
type: "fill",
|
|
225
|
+
source: sourceId,
|
|
226
|
+
paint: { "fill-pattern": patternImageId }
|
|
227
|
+
},
|
|
228
|
+
map.getLayer(lineLayerId) ? lineLayerId : beforeId
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
if (map.getLayer(patternLayerId)) map.removeLayer(patternLayerId);
|
|
234
|
+
if (map.hasImage(patternImageId)) map.removeImage(patternImageId);
|
|
235
|
+
patternDrawnRef.current = null;
|
|
236
|
+
}
|
|
237
|
+
if (showHit && map.getLayer(hitLayerId)) {
|
|
238
|
+
map.setPaintProperty(hitLayerId, "line-width", hitWidth);
|
|
239
|
+
}
|
|
240
|
+
}, [
|
|
241
|
+
areaColor,
|
|
242
|
+
hasPattern,
|
|
243
|
+
stripeWidth,
|
|
244
|
+
stripeGap,
|
|
245
|
+
hasVignette,
|
|
246
|
+
vignetteWidth,
|
|
247
|
+
vignetteOpacity,
|
|
248
|
+
patternLayerId,
|
|
249
|
+
vignetteLayerId,
|
|
250
|
+
patternImageId,
|
|
251
|
+
isLoaded,
|
|
252
|
+
map,
|
|
253
|
+
sourceId,
|
|
254
|
+
fillLayerId,
|
|
255
|
+
lineLayerId,
|
|
256
|
+
hitLayerId,
|
|
257
|
+
showFill,
|
|
258
|
+
showLine,
|
|
259
|
+
showHit,
|
|
260
|
+
hitWidth,
|
|
261
|
+
defaultLine,
|
|
262
|
+
mergedFillPaint,
|
|
263
|
+
mergedLinePaint,
|
|
264
|
+
beforeId
|
|
265
|
+
]);
|
|
266
|
+
const selectedKey = useMemo(() => {
|
|
267
|
+
if (selectedId == null) return "";
|
|
268
|
+
return (Array.isArray(selectedId) ? selectedId : [selectedId]).join("\0");
|
|
269
|
+
}, [selectedId]);
|
|
270
|
+
const appliedSelectionRef = useRef([]);
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
if (!isLoaded || !map || !map.getSource(sourceId)) return;
|
|
273
|
+
const next = selectedId == null ? [] : Array.isArray(selectedId) ? [...selectedId] : [selectedId];
|
|
274
|
+
for (const previous of appliedSelectionRef.current) {
|
|
275
|
+
if (!next.includes(previous)) {
|
|
276
|
+
map.setFeatureState({ source: sourceId, id: previous }, { selected: false });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const current of next) {
|
|
280
|
+
map.setFeatureState({ source: sourceId, id: current }, { selected: true });
|
|
281
|
+
}
|
|
282
|
+
appliedSelectionRef.current = next;
|
|
283
|
+
}, [isLoaded, map, sourceId, selectedKey]);
|
|
284
|
+
const hoverIsControlled = hoveredId !== void 0;
|
|
285
|
+
const appliedHoverRef = useRef(null);
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
if (!hoverIsControlled || !isLoaded || !map || !map.getSource(sourceId)) return;
|
|
288
|
+
const previous = appliedHoverRef.current;
|
|
289
|
+
if (previous != null && previous !== hoveredId) {
|
|
290
|
+
map.setFeatureState({ source: sourceId, id: previous }, { hover: false });
|
|
291
|
+
}
|
|
292
|
+
if (hoveredId != null) {
|
|
293
|
+
map.setFeatureState({ source: sourceId, id: hoveredId }, { hover: true });
|
|
294
|
+
}
|
|
295
|
+
appliedHoverRef.current = hoveredId ?? null;
|
|
296
|
+
}, [hoverIsControlled, hoveredId, isLoaded, map, sourceId]);
|
|
297
|
+
const pickLayerId = showFill ? fillLayerId : hitLayerId;
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
if (!isLoaded || !map || !interactive) return;
|
|
300
|
+
if (!showFill && !showHit) return;
|
|
301
|
+
let pointerHoverId = null;
|
|
302
|
+
const setHover = (next) => {
|
|
303
|
+
if (hoverIsControlled) return;
|
|
304
|
+
if (next === pointerHoverId) return;
|
|
305
|
+
const sourceExists = !!map.getSource(sourceId);
|
|
306
|
+
if (pointerHoverId != null && sourceExists) {
|
|
307
|
+
map.setFeatureState({ source: sourceId, id: pointerHoverId }, { hover: false });
|
|
308
|
+
}
|
|
309
|
+
pointerHoverId = next;
|
|
310
|
+
if (next != null && sourceExists) {
|
|
311
|
+
map.setFeatureState({ source: sourceId, id: next }, { hover: true });
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
const payload = (feature, e) => {
|
|
315
|
+
const planCrs = latestRef.current.plan;
|
|
316
|
+
return {
|
|
317
|
+
feature,
|
|
318
|
+
longitude: e.lngLat.lng,
|
|
319
|
+
latitude: e.lngLat.lat,
|
|
320
|
+
plan: planCrs ? planCrs.toPlan([e.lngLat.lng, e.lngLat.lat]) : null,
|
|
321
|
+
originalEvent: e
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
const handleMouseMove = (e) => {
|
|
325
|
+
const feature = e.features?.[0];
|
|
326
|
+
if (!feature) return;
|
|
327
|
+
map.getCanvas().style.cursor = "pointer";
|
|
328
|
+
const featureId = feature.id;
|
|
329
|
+
if (featureId === pointerHoverId) return;
|
|
330
|
+
setHover(featureId ?? null);
|
|
331
|
+
latestRef.current.onHover?.(payload(feature, e));
|
|
332
|
+
};
|
|
333
|
+
const handleMouseLeave = () => {
|
|
334
|
+
setHover(null);
|
|
335
|
+
map.getCanvas().style.cursor = "";
|
|
336
|
+
latestRef.current.onHover?.(null);
|
|
337
|
+
};
|
|
338
|
+
const pickPadded = (e) => {
|
|
339
|
+
const exact = map.queryRenderedFeatures(e.point, { layers: [pickLayerId] });
|
|
340
|
+
if (exact.length > 0) return exact[0];
|
|
341
|
+
const { x, y } = e.point;
|
|
342
|
+
const box = [
|
|
343
|
+
[x - pickPad, y - pickPad],
|
|
344
|
+
[x + pickPad, y + pickPad]
|
|
345
|
+
];
|
|
346
|
+
return map.queryRenderedFeatures(box, { layers: [pickLayerId] })[0];
|
|
347
|
+
};
|
|
348
|
+
setHoverRef.current = setHover;
|
|
349
|
+
const handleClick = (e) => {
|
|
350
|
+
const feature = pickPad > 0 ? pickPadded(e) : e.features?.[0];
|
|
351
|
+
if (!feature) return;
|
|
352
|
+
latestRef.current.onClick?.(payload(feature, e));
|
|
353
|
+
};
|
|
354
|
+
map.on("mousemove", pickLayerId, handleMouseMove);
|
|
355
|
+
map.on("mouseleave", pickLayerId, handleMouseLeave);
|
|
356
|
+
if (pickPad > 0) {
|
|
357
|
+
map.on("click", handleClick);
|
|
358
|
+
} else {
|
|
359
|
+
map.on("click", pickLayerId, handleClick);
|
|
360
|
+
}
|
|
361
|
+
return () => {
|
|
362
|
+
map.off("mousemove", pickLayerId, handleMouseMove);
|
|
363
|
+
map.off("mouseleave", pickLayerId, handleMouseLeave);
|
|
364
|
+
if (pickPad > 0) {
|
|
365
|
+
map.off("click", handleClick);
|
|
366
|
+
} else {
|
|
367
|
+
map.off("click", pickLayerId, handleClick);
|
|
368
|
+
}
|
|
369
|
+
setHover(null);
|
|
370
|
+
setHoverRef.current = null;
|
|
371
|
+
map.getCanvas().style.cursor = "";
|
|
372
|
+
};
|
|
373
|
+
}, [
|
|
374
|
+
isLoaded,
|
|
375
|
+
map,
|
|
376
|
+
pickLayerId,
|
|
377
|
+
sourceId,
|
|
378
|
+
interactive,
|
|
379
|
+
showFill,
|
|
380
|
+
showHit,
|
|
381
|
+
pickPad,
|
|
382
|
+
hoverIsControlled
|
|
383
|
+
]);
|
|
384
|
+
const keyboardFeatures = useMemo(
|
|
385
|
+
() => interactive && showFill ? listKeyboardFeatures(data, promoteId) : [],
|
|
386
|
+
[interactive, showFill, data, promoteId]
|
|
387
|
+
);
|
|
388
|
+
const reach = (entry) => {
|
|
389
|
+
setHoverRef.current?.(entry.id ?? null);
|
|
390
|
+
latestRef.current.onHover?.({
|
|
391
|
+
feature: entry.feature,
|
|
392
|
+
longitude: entry.longitude,
|
|
393
|
+
latitude: entry.latitude,
|
|
394
|
+
// A plan map's keyboard path is `MapPlanOverlay`'s job, so this list
|
|
395
|
+
// never has plan units to hand back.
|
|
396
|
+
plan: null,
|
|
397
|
+
originalEvent: null
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
if (keyboardFeatures.length === 0) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
return /* @__PURE__ */ jsx("div", { className: "sr-only", "data-slot": "map-geojson-keyboard-list", children: /* @__PURE__ */ jsx("ul", { children: keyboardFeatures.map((entry, index) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
404
|
+
"button",
|
|
405
|
+
{
|
|
406
|
+
onBlur: () => {
|
|
407
|
+
setHoverRef.current?.(null);
|
|
408
|
+
latestRef.current.onHover?.(null);
|
|
409
|
+
},
|
|
410
|
+
onClick: () => latestRef.current.onClick?.({
|
|
411
|
+
feature: entry.feature,
|
|
412
|
+
longitude: entry.longitude,
|
|
413
|
+
latitude: entry.latitude,
|
|
414
|
+
plan: null,
|
|
415
|
+
originalEvent: null
|
|
416
|
+
}),
|
|
417
|
+
onFocus: () => reach(entry),
|
|
418
|
+
type: "button",
|
|
419
|
+
children: featureLabel?.(entry.feature, index) ?? entry.fallbackLabel
|
|
420
|
+
}
|
|
421
|
+
) }, entry.key)) }) });
|
|
422
|
+
}
|
|
423
|
+
function geometryPositions(geometry) {
|
|
424
|
+
if (geometry.type === "GeometryCollection") {
|
|
425
|
+
return geometry.geometries.flatMap(geometryPositions);
|
|
426
|
+
}
|
|
427
|
+
if (geometry.type === "Point") {
|
|
428
|
+
return [geometry.coordinates];
|
|
429
|
+
}
|
|
430
|
+
const flatten = (value) => Array.isArray(value) && typeof value[0] === "number" ? [value] : Array.isArray(value) ? value.flatMap(flatten) : [];
|
|
431
|
+
return flatten(geometry.coordinates);
|
|
432
|
+
}
|
|
433
|
+
function geometryCentre(geometry) {
|
|
434
|
+
const positions = geometryPositions(geometry);
|
|
435
|
+
if (positions.length === 0) return [0, 0];
|
|
436
|
+
let west = Number.POSITIVE_INFINITY;
|
|
437
|
+
let east = Number.NEGATIVE_INFINITY;
|
|
438
|
+
let south = Number.POSITIVE_INFINITY;
|
|
439
|
+
let north = Number.NEGATIVE_INFINITY;
|
|
440
|
+
for (const [lng, lat] of positions) {
|
|
441
|
+
west = Math.min(west, lng ?? 0);
|
|
442
|
+
east = Math.max(east, lng ?? 0);
|
|
443
|
+
south = Math.min(south, lat ?? 0);
|
|
444
|
+
north = Math.max(north, lat ?? 0);
|
|
445
|
+
}
|
|
446
|
+
return [(west + east) / 2, (south + north) / 2];
|
|
447
|
+
}
|
|
448
|
+
function listKeyboardFeatures(data, promoteId) {
|
|
449
|
+
if (typeof data === "string") return [];
|
|
450
|
+
const features = data.type === "FeatureCollection" ? data.features : data.type === "Feature" ? [data] : [];
|
|
451
|
+
return features.map((feature, index) => {
|
|
452
|
+
const promoted = promoteId ? feature.properties?.[promoteId] : void 0;
|
|
453
|
+
const id = typeof promoted === "string" || typeof promoted === "number" ? promoted : typeof feature.id === "string" || typeof feature.id === "number" ? feature.id : void 0;
|
|
454
|
+
const [longitude, latitude] = geometryCentre(feature.geometry);
|
|
455
|
+
return {
|
|
456
|
+
key: `${id ?? index}`,
|
|
457
|
+
id,
|
|
458
|
+
feature,
|
|
459
|
+
longitude,
|
|
460
|
+
latitude,
|
|
461
|
+
fallbackLabel: `${id ?? index + 1}`
|
|
462
|
+
};
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
function stripeImage(color, width, gap) {
|
|
466
|
+
if (typeof document === "undefined") return null;
|
|
467
|
+
const pixelRatio = Math.max(1, Math.round(globalThis.devicePixelRatio || 1));
|
|
468
|
+
const size = Math.max(2, Math.round((width + gap) * Math.SQRT2 * pixelRatio));
|
|
469
|
+
const canvas = document.createElement("canvas");
|
|
470
|
+
canvas.width = size;
|
|
471
|
+
canvas.height = size;
|
|
472
|
+
let context = null;
|
|
473
|
+
try {
|
|
474
|
+
context = canvas.getContext("2d");
|
|
475
|
+
} catch {
|
|
476
|
+
context = null;
|
|
477
|
+
}
|
|
478
|
+
if (!context) return null;
|
|
479
|
+
context.strokeStyle = color;
|
|
480
|
+
context.lineWidth = width * pixelRatio;
|
|
481
|
+
context.lineCap = "square";
|
|
482
|
+
context.beginPath();
|
|
483
|
+
for (const k of [0, 1, 2]) {
|
|
484
|
+
context.moveTo(k * size - size, size);
|
|
485
|
+
context.lineTo(k * size, 0);
|
|
486
|
+
}
|
|
487
|
+
context.stroke();
|
|
488
|
+
return { data: context.getImageData(0, 0, size, size), pixelRatio };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export {
|
|
492
|
+
MapGeoJSON
|
|
493
|
+
};
|
|
494
|
+
//# sourceMappingURL=chunk-WS4Z7ENS.js.map
|