@12-apps/ui 6.98.1 → 6.100.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/dist/_internal/components/data-display/Alert/Alert.content.js +2 -2
- package/dist/_internal/components/data-display/Alert/Alert.js +3 -3
- package/dist/_internal/components/data-display/CardKit/card-actions-context.js +4 -4
- package/dist/_internal/components/data-display/CardKit/use-remove-confirm.js +5 -5
- package/dist/_internal/components/data-display/Carousel/Carousel.js +2 -2
- package/dist/_internal/components/data-display/Chart/SpecChart.js +3 -3
- package/dist/_internal/components/data-display/DataGrid/DataGrid.js +1 -1
- package/dist/_internal/components/data-display/DataViews/DataViewsGrid.js +17 -17
- package/dist/_internal/components/data-display/DataViews/DataViewsTableBase.js +22 -22
- package/dist/_internal/components/data-display/DataViews/base-list-card-geometry.js +1 -1
- package/dist/_internal/components/data-display/DataViews/base-list-card-layout.js +1 -1
- package/dist/_internal/components/data-display/DataViews/base-list-card-slots.js +1 -1
- package/dist/_internal/components/data-display/DataViews/base-list-card.js +4 -4
- package/dist/_internal/components/data-display/DataViews/data-views-grid-bodies.js +2 -2
- package/dist/_internal/components/data-display/DataViews/data-views-grid-parts.js +14 -14
- package/dist/_internal/components/data-display/DataViews/data-views-inline-bar.js +6 -6
- package/dist/_internal/components/data-display/DataViews/data-views-more-fields.js +2 -2
- package/dist/_internal/components/data-display/DataViews/data-views-more-filters.js +3 -3
- package/dist/_internal/components/data-display/DataViews/data-views-table-parts.js +4 -4
- package/dist/_internal/components/data-display/DataViews/list-card-rails.js +1 -1
- package/dist/_internal/components/data-display/DataViews/use-data-views-state.js +2 -2
- package/dist/_internal/components/data-display/DayRuler/DayRuler.footer.js +2 -2
- package/dist/_internal/components/data-display/DayRuler/DayRuler.js +7 -7
- package/dist/_internal/components/data-display/DayRuler/DayRuler.keyboard.js +2 -2
- package/dist/_internal/components/data-display/DayRuler/DayRuler.parts.js +3 -3
- package/dist/_internal/components/data-display/DayRuler/useRulerDrag.js +2 -2
- package/dist/_internal/components/data-display/FleetMap/FleetBody.js +6 -6
- package/dist/_internal/components/data-display/FleetMap/FleetCanvas.js +5 -5
- package/dist/_internal/components/data-display/FleetMap/FleetMap.js +7 -7
- package/dist/_internal/components/data-display/HoverCard/HoverCard.js +2 -2
- package/dist/_internal/components/data-display/Lightbox/Lightbox.js +3 -3
- package/dist/_internal/components/data-display/MapPreview/MapLayers.js +2 -2
- package/dist/_internal/components/data-display/MapPreview/MapPreview.js +4 -4
- package/dist/_internal/components/data-display/Progress/Progress.js +2 -2
- package/dist/_internal/components/data-display/RegionMap/RegionMap.fixtures.js +1 -1
- package/dist/_internal/components/data-display/RegionMap/RegionMap.helpers.js +3 -1
- package/dist/_internal/components/data-display/RegionMap/RegionMap.hooks.js +3 -3
- package/dist/_internal/components/data-display/RegionMap/RegionMap.js +6 -5
- package/dist/_internal/components/data-display/RegionMap/RegionMap.view.js +2 -2
- package/dist/_internal/components/data-display/RegionMap/RegionMapDrawing.js +3 -2
- package/dist/_internal/components/data-display/RegionMap/RegionMapPin.js +26 -0
- package/dist/_internal/components/data-display/index.js +51 -45
- package/dist/_internal/components/feedback/NotificationStatusNotice/NotificationStatusNotice.js +4 -4
- package/dist/_internal/components/feedback/SequenceResolver/SequenceResolver.frame.js +4 -4
- package/dist/_internal/components/feedback/SequenceResolver/SequenceResolver.header.js +3 -3
- package/dist/_internal/components/feedback/SequenceResolver/SequenceResolver.js +10 -10
- package/dist/_internal/components/feedback/SequenceResolver/SequenceResolver.overview.js +5 -5
- package/dist/_internal/components/feedback/SequenceResolver/SequenceResolver.parts.js +4 -4
- package/dist/_internal/components/feedback/index.js +11 -11
- package/dist/_internal/components/index.js +57 -51
- package/dist/charts.js +11 -11
- package/dist/{chunk-WYNGH2WL.js → chunk-255WTBZ5.js} +8 -8
- package/dist/{chunk-CGLMWS7K.js → chunk-37MJZLQU.js} +2 -2
- package/dist/{chunk-PNAKRHRT.js → chunk-3I7KE2N6.js} +2 -2
- package/dist/{chunk-L4FWEI2P.js → chunk-3V3Z4EML.js} +5 -5
- package/dist/{chunk-6RYSSNOO.js → chunk-54DOLCJ3.js} +2 -2
- package/dist/{chunk-74IH3ZGD.js → chunk-643YFWNK.js} +2 -2
- package/dist/{chunk-FRQGZIYU.js → chunk-6WAP7BJS.js} +2 -2
- package/dist/chunk-7XVT4RME.js +293 -0
- package/dist/chunk-7XVT4RME.js.map +1 -0
- package/dist/{chunk-JO6D7YGN.js → chunk-CWRQGA2T.js} +2 -2
- package/dist/{chunk-ZW7Q3IRL.js → chunk-FYJL432J.js} +4 -4
- package/dist/{chunk-G6TKWNF2.js → chunk-GZITNT7O.js} +11 -11
- package/dist/{chunk-RFMQBIE6.js → chunk-HBHBRRSW.js} +4 -4
- package/dist/{chunk-U4Z6GZDW.js → chunk-HN36UVOE.js} +4 -4
- package/dist/{chunk-WK577LQL.js → chunk-J2IQ6FJ5.js} +4 -4
- package/dist/{chunk-HCTHY66U.js → chunk-KAGXV3ID.js} +4 -4
- package/dist/{chunk-FWAY2SAI.js → chunk-KAZWBDIH.js} +2 -2
- package/dist/{chunk-DKPELKGL.js → chunk-KV7LTOWC.js} +5 -5
- package/dist/{chunk-UC7DLGMP.js → chunk-KVP5A3YK.js} +2 -2
- package/dist/{chunk-3CXRHRGE.js → chunk-LCKUEYU2.js} +7 -3
- package/dist/chunk-LCKUEYU2.js.map +1 -0
- package/dist/{chunk-EGBQR5GH.js → chunk-LOLYBFYY.js} +5 -5
- package/dist/{chunk-SOXO2VN3.js → chunk-MEMVZW64.js} +3 -3
- package/dist/{chunk-COOSJKZL.js → chunk-MNJZPJ5S.js} +2 -1
- package/dist/chunk-MNJZPJ5S.js.map +1 -0
- package/dist/{chunk-G2NVBCF7.js → chunk-NWUMGE6T.js} +3 -3
- package/dist/{chunk-RKAX37F4.js → chunk-Q5SCBQLY.js} +7 -7
- package/dist/{chunk-3R5MCSYO.js → chunk-QLKUWDMQ.js} +4 -4
- package/dist/{chunk-MPVH2GM5.js → chunk-RAQD5QCM.js} +4 -4
- package/dist/{chunk-KXDLLS62.js → chunk-REHGWN5M.js} +4 -4
- package/dist/{chunk-3N2HWNKT.js → chunk-UIJLDAV7.js} +2 -2
- package/dist/{chunk-GDWU7WF4.js → chunk-UN5WU2OX.js} +4 -4
- package/dist/{chunk-A27J4CIZ.js → chunk-UTIYB6VE.js} +11 -11
- package/dist/{chunk-5V7IZBRY.js → chunk-VRSW7PIO.js} +5 -5
- package/dist/{chunk-AESKKFA5.js → chunk-WDEVKND3.js} +3 -3
- package/dist/chunk-WGKLBVDZ.js +1 -0
- package/dist/chunk-WGKLBVDZ.js.map +1 -0
- package/dist/{chunk-67GTAPK6.js → chunk-XFOKLWJZ.js} +2 -2
- package/dist/{chunk-OV5NM4FY.js → chunk-XQAFSC66.js} +2 -2
- package/dist/{chunk-6FGP7RMV.js → chunk-XR474YYX.js} +2 -2
- package/dist/{chunk-E4MJA52O.js → chunk-YHFLI2FD.js} +16 -18
- package/dist/chunk-YHFLI2FD.js.map +1 -0
- package/dist/{chunk-4N4M237A.js → chunk-YQ5GQ32T.js} +2 -2
- package/dist/{chunk-YLZXIR6B.js → chunk-Z5V5E3JW.js} +4 -4
- package/dist/data-display/Alert.js +3 -3
- package/dist/data-display/CardKit.js +5 -5
- package/dist/data-display/Carousel.js +2 -2
- package/dist/data-display/Chart.js +11 -11
- package/dist/data-display/DataGrid.js +1 -1
- package/dist/data-display/DataViews.js +29 -29
- package/dist/data-display/DayRuler.js +7 -7
- package/dist/data-display/FleetMap.js +7 -7
- package/dist/data-display/HoverCard.js +2 -2
- package/dist/data-display/Lightbox.js +3 -3
- package/dist/data-display/MapPreview.js +4 -4
- package/dist/data-display/Progress.js +2 -2
- package/dist/data-display/RegionMap.js +14 -8
- package/dist/feedback/NotificationStatusNotice.js +4 -4
- package/dist/feedback/SequenceResolver.js +10 -10
- package/dist/types/components/data-display/RegionMap/RegionMap.d.ts +1 -1
- package/dist/types/components/data-display/RegionMap/RegionMap.d.ts.map +1 -1
- package/dist/types/components/data-display/RegionMap/RegionMap.helpers.d.ts +9 -0
- package/dist/types/components/data-display/RegionMap/RegionMap.helpers.d.ts.map +1 -1
- package/dist/types/components/data-display/RegionMap/RegionMap.types.d.ts +45 -1
- package/dist/types/components/data-display/RegionMap/RegionMap.types.d.ts.map +1 -1
- package/dist/types/components/data-display/RegionMap/RegionMapDrawing.d.ts +4 -1
- package/dist/types/components/data-display/RegionMap/RegionMapDrawing.d.ts.map +1 -1
- package/dist/types/components/data-display/RegionMap/RegionMapPin.d.ts +102 -0
- package/dist/types/components/data-display/RegionMap/RegionMapPin.d.ts.map +1 -0
- package/dist/types/components/data-display/RegionMap/index.d.ts +2 -1
- package/dist/types/components/data-display/RegionMap/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/data-display/RegionMap/RegionMap.helpers.ts +1 -1
- package/src/components/data-display/RegionMap/RegionMap.md +19 -2
- package/src/components/data-display/RegionMap/RegionMap.tsx +4 -0
- package/src/components/data-display/RegionMap/RegionMap.types.ts +49 -1
- package/src/components/data-display/RegionMap/RegionMapDrawing.tsx +11 -13
- package/src/components/data-display/RegionMap/RegionMapPin.tsx +388 -0
- package/src/components/data-display/RegionMap/index.ts +5 -0
- package/dist/chunk-3CXRHRGE.js.map +0 -1
- package/dist/chunk-COOSJKZL.js.map +0 -1
- package/dist/chunk-DY2I6LMX.js +0 -1
- package/dist/chunk-E4MJA52O.js.map +0 -1
- /package/dist/{chunk-DY2I6LMX.js.map → _internal/components/data-display/RegionMap/RegionMapPin.js.map} +0 -0
- /package/dist/{chunk-WYNGH2WL.js.map → chunk-255WTBZ5.js.map} +0 -0
- /package/dist/{chunk-CGLMWS7K.js.map → chunk-37MJZLQU.js.map} +0 -0
- /package/dist/{chunk-PNAKRHRT.js.map → chunk-3I7KE2N6.js.map} +0 -0
- /package/dist/{chunk-L4FWEI2P.js.map → chunk-3V3Z4EML.js.map} +0 -0
- /package/dist/{chunk-6RYSSNOO.js.map → chunk-54DOLCJ3.js.map} +0 -0
- /package/dist/{chunk-74IH3ZGD.js.map → chunk-643YFWNK.js.map} +0 -0
- /package/dist/{chunk-FRQGZIYU.js.map → chunk-6WAP7BJS.js.map} +0 -0
- /package/dist/{chunk-JO6D7YGN.js.map → chunk-CWRQGA2T.js.map} +0 -0
- /package/dist/{chunk-ZW7Q3IRL.js.map → chunk-FYJL432J.js.map} +0 -0
- /package/dist/{chunk-G6TKWNF2.js.map → chunk-GZITNT7O.js.map} +0 -0
- /package/dist/{chunk-RFMQBIE6.js.map → chunk-HBHBRRSW.js.map} +0 -0
- /package/dist/{chunk-U4Z6GZDW.js.map → chunk-HN36UVOE.js.map} +0 -0
- /package/dist/{chunk-WK577LQL.js.map → chunk-J2IQ6FJ5.js.map} +0 -0
- /package/dist/{chunk-HCTHY66U.js.map → chunk-KAGXV3ID.js.map} +0 -0
- /package/dist/{chunk-FWAY2SAI.js.map → chunk-KAZWBDIH.js.map} +0 -0
- /package/dist/{chunk-DKPELKGL.js.map → chunk-KV7LTOWC.js.map} +0 -0
- /package/dist/{chunk-UC7DLGMP.js.map → chunk-KVP5A3YK.js.map} +0 -0
- /package/dist/{chunk-EGBQR5GH.js.map → chunk-LOLYBFYY.js.map} +0 -0
- /package/dist/{chunk-SOXO2VN3.js.map → chunk-MEMVZW64.js.map} +0 -0
- /package/dist/{chunk-G2NVBCF7.js.map → chunk-NWUMGE6T.js.map} +0 -0
- /package/dist/{chunk-RKAX37F4.js.map → chunk-Q5SCBQLY.js.map} +0 -0
- /package/dist/{chunk-3R5MCSYO.js.map → chunk-QLKUWDMQ.js.map} +0 -0
- /package/dist/{chunk-MPVH2GM5.js.map → chunk-RAQD5QCM.js.map} +0 -0
- /package/dist/{chunk-KXDLLS62.js.map → chunk-REHGWN5M.js.map} +0 -0
- /package/dist/{chunk-3N2HWNKT.js.map → chunk-UIJLDAV7.js.map} +0 -0
- /package/dist/{chunk-GDWU7WF4.js.map → chunk-UN5WU2OX.js.map} +0 -0
- /package/dist/{chunk-A27J4CIZ.js.map → chunk-UTIYB6VE.js.map} +0 -0
- /package/dist/{chunk-5V7IZBRY.js.map → chunk-VRSW7PIO.js.map} +0 -0
- /package/dist/{chunk-AESKKFA5.js.map → chunk-WDEVKND3.js.map} +0 -0
- /package/dist/{chunk-67GTAPK6.js.map → chunk-XFOKLWJZ.js.map} +0 -0
- /package/dist/{chunk-OV5NM4FY.js.map → chunk-XQAFSC66.js.map} +0 -0
- /package/dist/{chunk-6FGP7RMV.js.map → chunk-XR474YYX.js.map} +0 -0
- /package/dist/{chunk-4N4M237A.js.map → chunk-YQ5GQ32T.js.map} +0 -0
- /package/dist/{chunk-YLZXIR6B.js.map → chunk-Z5V5E3JW.js.map} +0 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import Box from '@mui/material/Box/index.js';
|
|
2
|
+
import { alpha, type Theme } from '@mui/material/styles/index.js';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { rem } from '../../../tokens/relative';
|
|
6
|
+
|
|
7
|
+
import { METRES_PER_DEGREE, type PlaneBox, type PlanePoint, type Projection } from './RegionMap.helpers';
|
|
8
|
+
import type { RegionMapModel } from './RegionMap.hooks';
|
|
9
|
+
import type { RegionMapCenter, RegionMapCopy, RegionMapDot, RegionMapGhost, RegionMapPin } from './RegionMap.types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The movable centre of a `RegionMap` (`pin`, `ghost`, `dots`): a pin the
|
|
13
|
+
* viewer drags with a mouse or one finger, or focuses and nudges with the
|
|
14
|
+
* arrow keys, while the VIEW stays where `center` put it — so the areas never
|
|
15
|
+
* slide under a dragging finger. The circles follow the pin; a dashed ring
|
|
16
|
+
* and dot stay at `ghost`; `dots` are small points (filled or hollow).
|
|
17
|
+
*
|
|
18
|
+
* The pin is HTML over the drawing, at its point by percent like the labels,
|
|
19
|
+
* carrying `data-region-pin` so a host that zooms the drawing's layers can
|
|
20
|
+
* move it with them. Its moves are reported, never applied: the host passes
|
|
21
|
+
* the new point back.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** The movable centre and what is drawn with it, as the drawing receives it. */
|
|
25
|
+
export interface Moving {
|
|
26
|
+
center: RegionMapCenter;
|
|
27
|
+
pin?: RegionMapPin;
|
|
28
|
+
ghost?: RegionMapGhost;
|
|
29
|
+
dots?: readonly RegionMapDot[];
|
|
30
|
+
copy: RegionMapCopy;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type DrawnModel = Pick<RegionMapModel, 'projection' | 'radii'> & { view: PlaneBox };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* What the movable centre draws inside the SVG: the circles around the pin
|
|
37
|
+
* (else `fallback`, the centre's own circles), the ghost, and the dots.
|
|
38
|
+
*/
|
|
39
|
+
export function PinShapes({ model, moving, testId, theme, fallback }: { model: DrawnModel; moving: Moving | undefined; testId: string; theme: Theme; fallback: React.ReactNode }): React.JSX.Element {
|
|
40
|
+
const { projection, view } = model;
|
|
41
|
+
if (!moving || !projection) return <>{fallback}</>;
|
|
42
|
+
const pinAt = moving.pin ? projection.project([moving.pin.at.latitude, moving.pin.at.longitude]) : null;
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
{pinAt ? <PinCircles at={pinAt} radii={model.radii} theme={theme} /> : fallback}
|
|
46
|
+
{pinAt && moving.ghost ? <GhostShapes ghost={moving.ghost} pinAt={pinAt} projection={projection} view={view} testId={testId} theme={theme} /> : null}
|
|
47
|
+
{moving.dots ? <Dots dots={moving.dots} projection={projection} view={view} testId={testId} theme={theme} /> : null}
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** What the movable centre draws over the SVG, in HTML: the pin and the scale bar. */
|
|
53
|
+
export function PinOverlay({
|
|
54
|
+
model,
|
|
55
|
+
moving,
|
|
56
|
+
svgRef,
|
|
57
|
+
testId,
|
|
58
|
+
theme,
|
|
59
|
+
}: {
|
|
60
|
+
model: DrawnModel;
|
|
61
|
+
moving: Moving | undefined;
|
|
62
|
+
svgRef: React.RefObject<SVGSVGElement | null>;
|
|
63
|
+
testId: string;
|
|
64
|
+
theme: Theme;
|
|
65
|
+
}): React.JSX.Element | null {
|
|
66
|
+
if (!moving || !model.projection) return null;
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
{moving.pin ? <PinMarker pin={moving.pin} center={moving.center} projection={model.projection} view={model.view} svgRef={svgRef} testId={testId} theme={theme} /> : null}
|
|
70
|
+
<ScaleBar view={model.view} copy={moving.copy} svgRef={svgRef} testId={testId} theme={theme} />
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The flat projection's inverse around `center` — it is linear. */
|
|
76
|
+
export function latLngAt(center: RegionMapCenter, point: PlanePoint): RegionMapCenter {
|
|
77
|
+
const kx = METRES_PER_DEGREE * Math.cos((center.latitude * Math.PI) / 180);
|
|
78
|
+
return { latitude: center.latitude - point.y / METRES_PER_DEGREE, longitude: center.longitude + point.x / kx };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A point of the plane as fractions of the view (0..1 across, 0..1 down). */
|
|
82
|
+
function fractionOf(view: PlaneBox, point: PlanePoint): { x: number; y: number } {
|
|
83
|
+
return { x: (point.x - view.minX) / view.spanX, y: (point.y - view.minY) / view.spanY };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Keep a fraction inside the view, a hair in from its edge (the mock's clamp). */
|
|
87
|
+
const clampFraction = (value: number): number => Math.min(0.995, Math.max(0.005, value));
|
|
88
|
+
|
|
89
|
+
/** A plane point kept inside the view by the same clamp a drag gets. */
|
|
90
|
+
function clampToView(view: PlaneBox, point: PlanePoint): PlanePoint {
|
|
91
|
+
const at = fractionOf(view, point);
|
|
92
|
+
return { x: view.minX + clampFraction(at.x) * view.spanX, y: view.minY + clampFraction(at.y) * view.spanY };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Arrow keys, as metres east and north per press. */
|
|
96
|
+
const ARROWS: Record<string, [number, number]> = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, 1], ArrowDown: [0, -1] };
|
|
97
|
+
|
|
98
|
+
/** One arrow key: 50 m unless the host says otherwise. */
|
|
99
|
+
const DEFAULT_STEP_METRES = 50;
|
|
100
|
+
|
|
101
|
+
interface PinLayerProps {
|
|
102
|
+
pin: RegionMapPin;
|
|
103
|
+
center: RegionMapCenter;
|
|
104
|
+
projection: Projection;
|
|
105
|
+
view: PlaneBox;
|
|
106
|
+
/** The drawing's SVG, whose on-screen box maps a pointer to the plane (zoomed or not). */
|
|
107
|
+
svgRef: React.RefObject<SVGSVGElement | null>;
|
|
108
|
+
testId: string;
|
|
109
|
+
theme: Theme;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The draggable pin, as HTML over the drawing. */
|
|
113
|
+
export function PinMarker({ pin, center, projection, view, svgRef, testId, theme }: PinLayerProps): React.JSX.Element {
|
|
114
|
+
const ref = React.useRef<HTMLDivElement>(null);
|
|
115
|
+
const live = React.useRef({ pin, center, view });
|
|
116
|
+
live.current = { pin, center, view };
|
|
117
|
+
usePinDrag(ref, svgRef, live);
|
|
118
|
+
const at = fractionOf(view, projection.project([pin.at.latitude, pin.at.longitude]));
|
|
119
|
+
const onKeyDown = (event: React.KeyboardEvent): void => {
|
|
120
|
+
const arrow = ARROWS[event.key];
|
|
121
|
+
if (!arrow || pin.disabled) return;
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
const step = pin.stepMetres ?? DEFAULT_STEP_METRES;
|
|
124
|
+
const plane = projection.project([pin.at.latitude, pin.at.longitude]);
|
|
125
|
+
const next = latLngAt(center, clampToView(view, { x: plane.x + arrow[0] * step, y: plane.y - arrow[1] * step }));
|
|
126
|
+
pin.onMove?.(next);
|
|
127
|
+
pin.onDrop(next);
|
|
128
|
+
};
|
|
129
|
+
return (
|
|
130
|
+
<Box
|
|
131
|
+
ref={ref}
|
|
132
|
+
role="button"
|
|
133
|
+
tabIndex={pin.disabled ? -1 : 0}
|
|
134
|
+
aria-label={pin.ariaLabel}
|
|
135
|
+
aria-disabled={pin.disabled || undefined}
|
|
136
|
+
onKeyDown={onKeyDown}
|
|
137
|
+
data-testid={`${testId}-pin`}
|
|
138
|
+
data-region-pin=""
|
|
139
|
+
style={{ left: `${(at.x * 100).toFixed(3)}%`, top: `${(at.y * 100).toFixed(3)}%` }}
|
|
140
|
+
sx={{
|
|
141
|
+
position: 'absolute',
|
|
142
|
+
transform: 'translate(-50%, -100%)',
|
|
143
|
+
width: rem(theme, 44),
|
|
144
|
+
height: rem(theme, 48),
|
|
145
|
+
display: 'flex',
|
|
146
|
+
alignItems: 'flex-end',
|
|
147
|
+
justifyContent: 'center',
|
|
148
|
+
cursor: pin.disabled ? 'default' : 'grab',
|
|
149
|
+
touchAction: 'none',
|
|
150
|
+
zIndex: 2,
|
|
151
|
+
borderRadius: 1,
|
|
152
|
+
'&:focus-visible': { outline: `${rem(theme, 2)} solid`, outlineColor: 'primary.main', outlineOffset: rem(theme, 2) },
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
<Box component="svg" viewBox="0 0 32 42" aria-hidden="true" sx={{ width: rem(theme, 32), height: rem(theme, 42) }}>
|
|
156
|
+
<path
|
|
157
|
+
d="M16 41C16 41 3 25 3 15.5A13 13 0 1 1 29 15.5C29 25 16 41 16 41Z"
|
|
158
|
+
fill={theme.palette.primary.main}
|
|
159
|
+
stroke={theme.palette.background.paper}
|
|
160
|
+
strokeWidth={2}
|
|
161
|
+
/>
|
|
162
|
+
<circle cx="16" cy="15.5" r="5" fill={theme.palette.background.paper} />
|
|
163
|
+
</Box>
|
|
164
|
+
<Box
|
|
165
|
+
component="span"
|
|
166
|
+
aria-hidden
|
|
167
|
+
sx={{
|
|
168
|
+
position: 'absolute',
|
|
169
|
+
left: '88%',
|
|
170
|
+
top: 0,
|
|
171
|
+
px: 0.75,
|
|
172
|
+
borderRadius: 0.5,
|
|
173
|
+
border: 1,
|
|
174
|
+
borderColor: 'divider',
|
|
175
|
+
bgcolor: 'background.paper',
|
|
176
|
+
color: 'text.primary',
|
|
177
|
+
fontSize: rem(theme, 12),
|
|
178
|
+
fontWeight: 600,
|
|
179
|
+
whiteSpace: 'nowrap',
|
|
180
|
+
pointerEvents: 'none',
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
{pin.label}
|
|
184
|
+
</Box>
|
|
185
|
+
</Box>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The pointer drag: native listeners on the pin, so a host's own listeners on
|
|
191
|
+
* the drawing (a pan, a tap) never see the press; captured, so a finger that
|
|
192
|
+
* leaves the pin keeps dragging it; clamped inside the view.
|
|
193
|
+
*/
|
|
194
|
+
function usePinDrag(
|
|
195
|
+
ref: React.RefObject<HTMLDivElement | null>,
|
|
196
|
+
svgRef: React.RefObject<SVGSVGElement | null>,
|
|
197
|
+
live: React.RefObject<{ pin: RegionMapPin; center: RegionMapCenter; view: PlaneBox }>,
|
|
198
|
+
): void {
|
|
199
|
+
React.useEffect(() => {
|
|
200
|
+
const element = ref.current;
|
|
201
|
+
if (!element) return undefined;
|
|
202
|
+
let dragging: number | null = null;
|
|
203
|
+
// Where the last move put the pin: a cancelled gesture (a browser may
|
|
204
|
+
// report 0,0 for it) rests there rather than jumping to the corner.
|
|
205
|
+
let last: RegionMapCenter | null = null;
|
|
206
|
+
const pointAt = (event: PointerEvent): RegionMapCenter | null => {
|
|
207
|
+
const box = svgRef.current?.getBoundingClientRect();
|
|
208
|
+
if (!box || box.width === 0 || box.height === 0) return null;
|
|
209
|
+
const { view, center } = live.current;
|
|
210
|
+
if (!Number.isFinite(event.clientX) || !Number.isFinite(event.clientY)) return null;
|
|
211
|
+
const fx = clampFraction((event.clientX - box.left) / box.width);
|
|
212
|
+
const fy = clampFraction((event.clientY - box.top) / box.height);
|
|
213
|
+
return latLngAt(center, { x: view.minX + fx * view.spanX, y: view.minY + fy * view.spanY });
|
|
214
|
+
};
|
|
215
|
+
const down = (event: PointerEvent): void => {
|
|
216
|
+
event.stopPropagation();
|
|
217
|
+
if (live.current.pin.disabled) return;
|
|
218
|
+
event.preventDefault();
|
|
219
|
+
dragging = event.pointerId;
|
|
220
|
+
last = null;
|
|
221
|
+
element.setPointerCapture?.(event.pointerId);
|
|
222
|
+
};
|
|
223
|
+
const move = (event: PointerEvent): void => {
|
|
224
|
+
if (dragging !== event.pointerId) return;
|
|
225
|
+
event.stopPropagation();
|
|
226
|
+
const at = pointAt(event);
|
|
227
|
+
if (!at) return;
|
|
228
|
+
last = at;
|
|
229
|
+
live.current.pin.onMove?.(at);
|
|
230
|
+
};
|
|
231
|
+
const up = (event: PointerEvent): void => {
|
|
232
|
+
if (dragging !== event.pointerId) return;
|
|
233
|
+
event.stopPropagation();
|
|
234
|
+
dragging = null;
|
|
235
|
+
const at = event.type === 'pointercancel' ? last : (pointAt(event) ?? last);
|
|
236
|
+
if (at) live.current.pin.onDrop(at);
|
|
237
|
+
};
|
|
238
|
+
// A click that ends a drag is the drag's, not a tap on the area under it.
|
|
239
|
+
const click = (event: MouseEvent): void => event.stopPropagation();
|
|
240
|
+
element.addEventListener('pointerdown', down);
|
|
241
|
+
element.addEventListener('pointermove', move);
|
|
242
|
+
element.addEventListener('pointerup', up);
|
|
243
|
+
element.addEventListener('pointercancel', up);
|
|
244
|
+
element.addEventListener('click', click);
|
|
245
|
+
return () => {
|
|
246
|
+
element.removeEventListener('pointerdown', down);
|
|
247
|
+
element.removeEventListener('pointermove', move);
|
|
248
|
+
element.removeEventListener('pointerup', up);
|
|
249
|
+
element.removeEventListener('pointercancel', up);
|
|
250
|
+
element.removeEventListener('click', click);
|
|
251
|
+
};
|
|
252
|
+
}, [ref, svgRef, live]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The circles around the pin: solid in the primary colour, faintly filled. */
|
|
256
|
+
export function PinCircles({ at, radii, theme }: { at: PlanePoint; radii: readonly number[]; theme: Theme }): React.JSX.Element {
|
|
257
|
+
return (
|
|
258
|
+
<>
|
|
259
|
+
{radii.map((radius) => (
|
|
260
|
+
<circle
|
|
261
|
+
key={radius}
|
|
262
|
+
cx={at.x}
|
|
263
|
+
cy={at.y}
|
|
264
|
+
r={radius}
|
|
265
|
+
fill={alpha(theme.palette.primary.main, 0.06)}
|
|
266
|
+
stroke={theme.palette.primary.main}
|
|
267
|
+
strokeWidth={2.25}
|
|
268
|
+
vectorEffect="non-scaling-stroke"
|
|
269
|
+
pointerEvents="none"
|
|
270
|
+
/>
|
|
271
|
+
))}
|
|
272
|
+
</>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Where the circles stood: a dashed ring and dot at `ghost`, and a dashed line from it to the pin. */
|
|
277
|
+
export function GhostShapes({
|
|
278
|
+
ghost,
|
|
279
|
+
pinAt,
|
|
280
|
+
projection,
|
|
281
|
+
view,
|
|
282
|
+
testId,
|
|
283
|
+
theme,
|
|
284
|
+
}: {
|
|
285
|
+
ghost: RegionMapGhost;
|
|
286
|
+
pinAt: PlanePoint;
|
|
287
|
+
projection: Projection;
|
|
288
|
+
view: PlaneBox;
|
|
289
|
+
testId: string;
|
|
290
|
+
theme: Theme;
|
|
291
|
+
}): React.JSX.Element {
|
|
292
|
+
const at = projection.project([ghost.at.latitude, ghost.at.longitude]);
|
|
293
|
+
const stroke = theme.palette.text.secondary;
|
|
294
|
+
const common = { fill: 'none', stroke, vectorEffect: 'non-scaling-stroke', pointerEvents: 'none' } as const;
|
|
295
|
+
return (
|
|
296
|
+
<g data-testid={`${testId}-ghost`}>
|
|
297
|
+
<circle cx={at.x} cy={at.y} r={ghost.radiusMetres} strokeWidth={1.5} strokeDasharray="6 5" {...common} />
|
|
298
|
+
<line x1={at.x} y1={at.y} x2={pinAt.x} y2={pinAt.y} strokeWidth={1.5} strokeDasharray="4 4" {...common} />
|
|
299
|
+
<circle cx={at.x} cy={at.y} r={view.spanX / 110} strokeWidth={2} strokeDasharray="3 2" {...common} fill={alpha(theme.palette.background.paper, 0.7)} />
|
|
300
|
+
</g>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The dots: filled with the ink, or hollow (paper with an ink ring). */
|
|
305
|
+
export function Dots({ dots, projection, view, testId, theme }: { dots: readonly RegionMapDot[]; projection: Projection; view: PlaneBox; testId: string; theme: Theme }): React.JSX.Element {
|
|
306
|
+
return (
|
|
307
|
+
<g data-testid={`${testId}-dots`}>
|
|
308
|
+
{dots.map((dot) => {
|
|
309
|
+
const at = projection.project([dot.at.latitude, dot.at.longitude]);
|
|
310
|
+
return (
|
|
311
|
+
<circle
|
|
312
|
+
key={dot.id}
|
|
313
|
+
cx={at.x}
|
|
314
|
+
cy={at.y}
|
|
315
|
+
r={view.spanX / 150}
|
|
316
|
+
fill={dot.filled ? theme.palette.text.primary : theme.palette.background.paper}
|
|
317
|
+
stroke={theme.palette.text.primary}
|
|
318
|
+
strokeWidth={2}
|
|
319
|
+
vectorEffect="non-scaling-stroke"
|
|
320
|
+
pointerEvents="none"
|
|
321
|
+
data-filled={dot.filled ? 'true' : 'false'}
|
|
322
|
+
/>
|
|
323
|
+
);
|
|
324
|
+
})}
|
|
325
|
+
</g>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Nice lengths a scale bar may take, in metres. */
|
|
330
|
+
const SCALE_STEPS = [50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000];
|
|
331
|
+
|
|
332
|
+
/** The longest nice length that fits in a quarter of the drawing's width. */
|
|
333
|
+
export function scaleMetres(view: PlaneBox): number {
|
|
334
|
+
const quarter = view.spanX / 4;
|
|
335
|
+
return SCALE_STEPS.filter((step) => step <= quarter).at(-1) ?? SCALE_STEPS[0]!;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* A scale bar, bottom-left, when the host gives `copy.scale`. It reads what is
|
|
340
|
+
* on screen: a host that zooms the drawing's layers (a CSS scale on the SVG)
|
|
341
|
+
* shows fewer metres per pixel, so after every render the bar compares the
|
|
342
|
+
* SVG's drawn width with its laid-out width — both off the SVG itself — and
|
|
343
|
+
* spans the drawing's own width, so its share of it is a share of the view.
|
|
344
|
+
*/
|
|
345
|
+
export function ScaleBar({
|
|
346
|
+
view,
|
|
347
|
+
copy,
|
|
348
|
+
svgRef,
|
|
349
|
+
testId,
|
|
350
|
+
theme,
|
|
351
|
+
}: {
|
|
352
|
+
view: PlaneBox;
|
|
353
|
+
copy: RegionMapCopy;
|
|
354
|
+
svgRef: React.RefObject<SVGSVGElement | null>;
|
|
355
|
+
testId: string;
|
|
356
|
+
theme: Theme;
|
|
357
|
+
}): React.JSX.Element | null {
|
|
358
|
+
// What the drawing shows on screen: its laid-out width, and its width as
|
|
359
|
+
// drawn (a host's CSS scale on the SVG), both read off the SVG itself.
|
|
360
|
+
const [screen, setScreen] = React.useState({ laidOut: 0, drawn: 0 });
|
|
361
|
+
const wanted = copy.scale !== undefined;
|
|
362
|
+
React.useLayoutEffect(() => {
|
|
363
|
+
if (!wanted) return;
|
|
364
|
+
const svg = svgRef.current;
|
|
365
|
+
const next = { laidOut: svg?.clientWidth ?? 0, drawn: svg?.getBoundingClientRect().width ?? 0 };
|
|
366
|
+
if (next.laidOut !== screen.laidOut || Math.abs(next.drawn - screen.drawn) > 0.5) setScreen(next);
|
|
367
|
+
});
|
|
368
|
+
if (!copy.scale) return null;
|
|
369
|
+
const zoom = screen.laidOut > 0 && screen.drawn > 0 ? screen.drawn / screen.laidOut : 1;
|
|
370
|
+
const metres = scaleMetres({ ...view, spanX: view.spanX / zoom });
|
|
371
|
+
// The bar's share of the drawing's width: metres on screen over the metres the width shows.
|
|
372
|
+
const share = (metres * zoom) / view.spanX;
|
|
373
|
+
return (
|
|
374
|
+
<Box
|
|
375
|
+
data-testid={`${testId}-scale`}
|
|
376
|
+
aria-hidden
|
|
377
|
+
sx={{ position: 'absolute', left: 0, right: 0, bottom: rem(theme, 10), pointerEvents: 'none', zIndex: 1 }}
|
|
378
|
+
>
|
|
379
|
+
<Box component="span" sx={{ display: 'block', ml: rem(theme, 10), fontSize: rem(theme, 12), color: 'text.secondary' }}>
|
|
380
|
+
{copy.scale(metres)}
|
|
381
|
+
</Box>
|
|
382
|
+
<Box
|
|
383
|
+
component="span"
|
|
384
|
+
sx={{ display: 'block', height: rem(theme, 4), ml: rem(theme, 10), width: `${(share * 100).toFixed(2)}%`, borderStyle: 'solid', borderWidth: `0 ${rem(theme, 1.5)} ${rem(theme, 1.5)}`, borderColor: 'text.secondary' }}
|
|
385
|
+
/>
|
|
386
|
+
</Box>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
export { RegionMap } from './RegionMap';
|
|
2
2
|
export { decodePolyline, hitRegion, projectionAt } from './RegionMap.helpers';
|
|
3
|
+
export { latLngAt, scaleMetres } from './RegionMapPin';
|
|
3
4
|
export { boundsOfRings, regionMapView } from './RegionMap.view';
|
|
4
5
|
export type { AreaGeometry, PlaneBox, PlanePoint, PlaneRegion, Projection } from './RegionMap.helpers';
|
|
5
6
|
export type {
|
|
6
7
|
RegionMapBounds,
|
|
7
8
|
RegionMapCircle,
|
|
9
|
+
RegionMapCenter,
|
|
8
10
|
RegionMapCopy,
|
|
11
|
+
RegionMapDot,
|
|
12
|
+
RegionMapGhost,
|
|
9
13
|
RegionMapLayer,
|
|
10
14
|
RegionMapListMode,
|
|
11
15
|
RegionMapListSort,
|
|
16
|
+
RegionMapPin,
|
|
12
17
|
RegionMapPoint,
|
|
13
18
|
RegionMapProps,
|
|
14
19
|
RegionMapReach,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/data-display/RegionMap/RegionMap.tsx"],"sourcesContent":["import Box from '@mui/material/Box/index.js';\nimport { useTheme } from '@mui/material/styles/index.js';\nimport React from 'react';\n\nimport { useRegionMap } from './RegionMap.hooks';\nimport type { RegionMapProps } from './RegionMap.types';\nimport { RegionMapDrawing } from './RegionMapDrawing';\nimport { RegionMapLegend } from './RegionMapLegend';\nimport { RegionMapList } from './RegionMapList';\n\n/** One empty list, so the default does not change identity per render. */\nconst NO_IDS: readonly string[] = [];\n\n/**\n * A hand-drawn map of named areas around a centre; a tap toggles an area.\n *\n * Drawn as inline SVG over a plain background — no tile server, no API key,\n * nothing fetched — because the question it answers is \"which of THESE areas\",\n * not \"what is the street called\". Four choices shape it:\n *\n * - **The list is the accessible way in.** The drawing is one `role=\"img\"`\n * with a name; its areas are not tab stops (dozens of them, many too small to\n * tap on a phone). Every area is also a toggle button in the list beneath,\n * with `aria-pressed`, which is what a keyboard and a screen reader use —\n * and what reaches a sliver no finger can.\n * - **Excluded is hatched AND named, never only coloured,** and never in the\n * error colour: leaving an area out is a choice, not a failure.\n * - **The hit test is ours, not only the browser's.** A mouse click on a path\n * is that path. A finger's tap takes the SMALLEST area within a few pixels'\n * worth of metres (`hitRegion`), so a sliver beside a big area can be hit\n * from the map; one too thin even for that is still on the list.\n * - **The host can draw inside it.** `reach` widens the view past the largest\n * circle and `overlay` draws a layer in the SVG at the map's own projection,\n * tap-transparent; `regionMapView` gives the same view to HTML laid over it.\n * - **A toggle is cheap.** Geometry is projected once per `rings` identity,\n * and the base layer is memoised, so a tap re-renders the hatch and labels.\n */\nexport function RegionMap({\n regions,\n center,\n circles,\n reach,\n overlay,\n onToggle,\n highlightedIds = NO_IDS,\n copy,\n listMode = 'collapsed',\n listSort = 'given',\n maxHeight,\n className,\n dataTestId = 'region-map',\n}: RegionMapProps): React.JSX.Element | null {\n const theme = useTheme();\n const model = useRegionMap({ regions, center, circles, reach, onToggle });\n const { view } = model;\n if (view === null && regions.length === 0) return null;\n return (\n <Box className={className} data-testid={dataTestId}>\n {view && (\n <>\n <RegionMapDrawing\n model={{ ...model, view }}\n overlay={overlay}\n showCenter={center !== null}\n highlightedIds={highlightedIds}\n maxHeight={maxHeight}\n label={copy.mapLabel}\n tappable={onToggle !== undefined}\n testId={dataTestId}\n theme={theme}\n />\n <RegionMapLegend\n copy={copy}\n outermostMetres={model.outermostMetres}\n showCenter={center !== null}\n theme={theme}\n testId={dataTestId}\n />\n </>\n )}\n {listMode !== 'none' && (\n <RegionMapList\n regions={regions}\n copy={copy}\n mode={listMode}\n sort={listSort}\n onToggle={onToggle}\n testId={dataTestId}\n theme={theme}\n />\n )}\n </Box>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA,OAAO,SAAS;AAChB,SAAS,gBAAgB;AA0DjB,mBACE,KADF;AAhDR,IAAM,SAA4B,CAAC;AA0B5B,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB;AAAA,EACA,WAAW;AAAA,EACX,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,aAAa;AACf,GAA6C;AAC3C,QAAM,QAAQ,SAAS;AACvB,QAAM,QAAQ,aAAa,EAAE,SAAS,QAAQ,SAAS,OAAO,SAAS,CAAC;AACxE,QAAM,EAAE,KAAK,IAAI;AACjB,MAAI,SAAS,QAAQ,QAAQ,WAAW,EAAG,QAAO;AAClD,SACE,qBAAC,OAAI,WAAsB,eAAa,YACrC;AAAA,YACC,iCACE;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,EAAE,GAAG,OAAO,KAAK;AAAA,UACxB;AAAA,UACA,YAAY,WAAW;AAAA,UACvB;AAAA,UACA;AAAA,UACA,OAAO,KAAK;AAAA,UACZ,UAAU,aAAa;AAAA,UACvB,QAAQ;AAAA,UACR;AAAA;AAAA,MACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA,iBAAiB,MAAM;AAAA,UACvB,YAAY,WAAW;AAAA,UACvB;AAAA,UACA,QAAQ;AAAA;AAAA,MACV;AAAA,OACF;AAAA,IAED,aAAa,UACZ;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,MAAM;AAAA,QACN,MAAM;AAAA,QACN;AAAA,QACA,QAAQ;AAAA,QACR;AAAA;AAAA,IACF;AAAA,KAEJ;AAEJ;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/data-display/RegionMap/RegionMap.helpers.ts"],"sourcesContent":["import type { RegionMapPoint, RegionMapRegion, RegionMapRing } from './RegionMap.types';\n\n/**\n * The geometry behind `RegionMap`, kept pure so it is tested without a DOM.\n *\n * Coordinates are projected to METRES on a plane around the centre\n * (equirectangular): within the tens of kilometres a map like this spans, its\n * error is far below any outline's own. `y` grows SOUTH, as SVG's does.\n */\n\n/** Metres per degree of latitude on the mean sphere. */\nconst METRES_PER_DEGREE = 111_195;\n\n/** A projected point, metres east (`x`) and south (`y`) of the centre. */\nexport interface PlanePoint {\n x: number;\n y: number;\n}\n\n/** An SVG viewBox in projected metres. */\nexport interface PlaneBox {\n /** West edge, metres east of the centre. */\n minX: number;\n /** North edge, metres south of the centre. */\n minY: number;\n /** East-west extent, metres. */\n spanX: number;\n /** North-south extent, metres. */\n spanY: number;\n}\n\nexport interface Projection {\n project: (point: RegionMapPoint) => PlanePoint;\n}\n\n/** The projection around a centre. */\nexport function projectionAt(center: { latitude: number; longitude: number }): Projection {\n const kx = METRES_PER_DEGREE * Math.cos((center.latitude * Math.PI) / 180);\n return {\n project: ([latitude, longitude]) => ({\n x: (longitude - center.longitude) * kx,\n y: -(latitude - center.latitude) * METRES_PER_DEGREE,\n }),\n };\n}\n\n/** A ring as an SVG path segment, closed. */\nfunction ringPath(ring: readonly PlanePoint[]): string {\n if (ring.length === 0) return '';\n const [first, ...rest] = ring;\n return `M${first!.x.toFixed(1)} ${first!.y.toFixed(1)}${rest.map((p) => `L${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join('')}Z`;\n}\n\n/** Every ring of an area as one path, to be filled with `fill-rule=\"evenodd\"`. */\nexport function regionPath(rings: readonly (readonly PlanePoint[])[]): string {\n return rings.map(ringPath).join('');\n}\n\n/** The smallest and largest of some numbers, in one pass (no spread). */\nexport function extent(values: Iterable<number>): { min: number; max: number } | null {\n let min = Infinity;\n let max = -Infinity;\n for (const value of values) {\n if (value < min) min = value;\n if (value > max) max = value;\n }\n return min === Infinity ? null : { min, max };\n}\n\n/** The box around projected rings. */\nexport function planeBounds(rings: readonly (readonly PlanePoint[])[]): PlaneBox | null {\n const points = rings.flat();\n const xs = extent(points.map((p) => p.x));\n const ys = extent(points.map((p) => p.y));\n if (xs === null || ys === null) return null;\n return { minX: xs.min, minY: ys.min, spanX: xs.max - xs.min, spanY: ys.max - ys.min };\n}\n\n/**\n * The square the map shows: the largest circle with a margin, else every area,\n * else nothing. Square so the drawing keeps its proportions at every width.\n */\nexport function viewBoxFor(\n largestRadius: number | null,\n areaBounds: PlaneBox | null,\n margin = 0.12,\n): PlaneBox | null {\n if (largestRadius !== null && largestRadius > 0) {\n const half = largestRadius * (1 + margin);\n return { minX: -half, minY: -half, spanX: half * 2, spanY: half * 2 };\n }\n if (areaBounds === null) return null;\n const side = Math.max(areaBounds.spanX, areaBounds.spanY) * (1 + margin) || 1;\n const cx = areaBounds.minX + areaBounds.spanX / 2;\n const cy = areaBounds.minY + areaBounds.spanY / 2;\n return { minX: cx - side / 2, minY: cy - side / 2, spanX: side, spanY: side };\n}\n\n/** Do two boxes overlap? Used to skip areas wholly outside the view. */\nexport function boxesOverlap(a: PlaneBox, b: PlaneBox): boolean {\n return (\n a.minX <= b.minX + b.spanX &&\n b.minX <= a.minX + a.spanX &&\n a.minY <= b.minY + b.spanY &&\n b.minY <= a.minY + a.spanY\n );\n}\n\n/** How many of one ring's edges a ray from the point, eastward, crosses. */\nfunction ringCrossings(ring: readonly PlanePoint[], point: PlanePoint): number {\n let crossings = 0;\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {\n const a = ring[i]!;\n const b = ring[j]!;\n if (a.y > point.y !== b.y > point.y && point.x < ((b.x - a.x) * (point.y - a.y)) / (b.y - a.y) + a.x) {\n crossings += 1;\n }\n }\n return crossings;\n}\n\n/** Is the point inside the rings, by the even-odd rule? */\nexport function ringsContain(rings: readonly (readonly PlanePoint[])[], point: PlanePoint): boolean {\n return rings.reduce((sum, ring) => sum + ringCrossings(ring, point), 0) % 2 === 1;\n}\n\n/** The point's distance to one ring's nearest edge. */\nfunction ringDistance(ring: readonly PlanePoint[], point: PlanePoint): number {\n let best = Infinity;\n for (let i = 1; i <= ring.length; i++) {\n const a = ring[i - 1]!;\n const b = ring[i % ring.length]!;\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const lengthSquared = dx * dx + dy * dy;\n const t =\n lengthSquared === 0\n ? 0\n : Math.max(0, Math.min(1, ((point.x - a.x) * dx + (point.y - a.y) * dy) / lengthSquared));\n best = Math.min(best, Math.hypot(a.x + t * dx - point.x, a.y + t * dy - point.y));\n }\n return best;\n}\n\n/** The point's distance to the rings: 0 inside, else to the nearest edge. */\nexport function ringsDistance(rings: readonly (readonly PlanePoint[])[], point: PlanePoint): number {\n if (ringsContain(rings, point)) return 0;\n let best = Infinity;\n for (const ring of rings) best = Math.min(best, ringDistance(ring, point));\n return best;\n}\n\n/** An area projected once: what drawing it and hit-testing it read. */\nexport interface AreaGeometry {\n rings: readonly (readonly PlanePoint[])[];\n /** Null for an area with no points. */\n bounds: PlaneBox | null;\n /** Square metres of the largest ring — orders hits and draw order. */\n area: number;\n /** The SVG path, every ring, for `fill-rule=\"evenodd\"`. */\n path: string;\n}\n\n/** A projected area, as the hit test reads it. */\nexport interface PlaneRegion {\n id: string;\n geometry: AreaGeometry;\n}\n\n/** Is the point within `margin` of the box? */\nfunction nearBox(box: PlaneBox, point: PlanePoint, margin: number): boolean {\n return (\n point.x >= box.minX - margin &&\n point.x <= box.minX + box.spanX + margin &&\n point.y >= box.minY - margin &&\n point.y <= box.minY + box.spanY + margin\n );\n}\n\n/**\n * Which area a tap at this point means: the SMALLEST area within `tolerance`\n * of it, the ones holding it included (distance 0).\n *\n * Smallest, because a fingertip is wider than a sliver: a tap that lands on a\n * big area a few pixels from a 10 m strip almost always meant the strip, and\n * the big one stays easy to hit everywhere else. An area in another's hole is\n * smaller than the one around it, so it wins too. Nothing within the\n * tolerance — empty map — is nothing: a tap there is not a guess. Areas whose\n * box is not near the point are skipped before any ray is cast.\n */\nexport function hitRegion(\n regions: readonly PlaneRegion[],\n point: PlanePoint,\n tolerance: number,\n): string | null {\n let best: { id: string; area: number; distance: number } | null = null;\n for (const { id, geometry } of regions) {\n const distance = reach(geometry, point, tolerance);\n if (distance !== null && beats(geometry.area, distance, best)) {\n best = { id, area: geometry.area, distance };\n }\n }\n return best?.id ?? null;\n}\n\n/** The point's distance to the area, or null when it is beyond the tolerance. */\nfunction reach(geometry: AreaGeometry, point: PlanePoint, tolerance: number): number | null {\n if (geometry.bounds === null || !nearBox(geometry.bounds, point, tolerance)) return null;\n const distance = ringsDistance(geometry.rings, point);\n return distance <= tolerance ? distance : null;\n}\n\n/** Smaller wins; between two the same size, the nearer. */\nfunction beats(area: number, distance: number, best: { area: number; distance: number } | null): boolean {\n if (best === null || area < best.area) return true;\n return area === best.area && distance < best.distance;\n}\n\n/** Signed area and centroid of one ring (shoelace). */\nfunction ringCentroid(ring: readonly PlanePoint[]): { area: number; x: number; y: number } {\n let area = 0;\n let cx = 0;\n let cy = 0;\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {\n const a = ring[j]!;\n const b = ring[i]!;\n const cross = a.x * b.y - b.x * a.y;\n area += cross;\n cx += (a.x + b.x) * cross;\n cy += (a.y + b.y) * cross;\n }\n area /= 2;\n if (area === 0) return { area: 0, x: ring[0]?.x ?? 0, y: ring[0]?.y ?? 0 };\n return { area, x: cx / (6 * area), y: cy / (6 * area) };\n}\n\n/**\n * The area's size in square metres, read from its LARGEST ring. Rough on\n * purpose — holes and second parts are ignored — because it only orders\n * things: which of two areas holding a tap is the smaller, and which is drawn\n * first so a small one is never painted over by its neighbour.\n */\nexport function planeArea(rings: readonly (readonly PlanePoint[])[]): number {\n let largest = 0;\n for (const ring of rings) largest = Math.max(largest, Math.abs(ringCentroid(ring).area));\n return largest;\n}\n\n/**\n * Where an area's label goes: the centroid of its largest ring, pulled back\n * inside the view, as fractions of the view (0..1) — the label is HTML laid\n * over the drawing, so its type stays the theme's size at every width.\n */\nexport function labelPosition(\n rings: readonly (readonly PlanePoint[])[],\n view: PlaneBox,\n inset = 0.04,\n): { fractionX: number; fractionY: number } {\n const largest = rings\n .map(ringCentroid)\n .reduce((a, b) => (Math.abs(b.area) > Math.abs(a.area) ? b : a), { area: 0, x: 0, y: 0 });\n const clamp = (value: number): number => Math.min(1 - inset, Math.max(inset, value));\n return {\n fractionX: clamp((largest.x - view.minX) / view.spanX),\n fractionY: clamp((largest.y - view.minY) / view.spanY),\n };\n}\n\n/** Every ring of an area, projected. */\nexport function projectRings(rings: readonly RegionMapRing[], projection: Projection): PlanePoint[][] {\n return rings.map((ring) => ring.map(projection.project));\n}\n\n/** One area's geometry, projected and pathed. */\nexport function areaGeometry(rings: readonly RegionMapRing[], projection: Projection): AreaGeometry {\n const plane = projectRings(rings, projection);\n return { rings: plane, bounds: planeBounds(plane), area: planeArea(plane), path: regionPath(plane) };\n}\n\n/**\n * A cache of geometry per `rings` IDENTITY, for one projection. A host that\n * re-creates its region objects on every toggle (`{ ...area, excluded }`)\n * keeps the same `rings` arrays, so a tap re-projects nothing.\n */\nexport function geometryCache(projection: Projection): (rings: readonly RegionMapRing[]) => AreaGeometry {\n const cache = new WeakMap<readonly RegionMapRing[], AreaGeometry>();\n return (rings) => {\n let geometry = cache.get(rings);\n if (geometry === undefined) {\n geometry = areaGeometry(rings, projection);\n cache.set(rings, geometry);\n }\n return geometry;\n };\n}\n\n/** An area as the drawing reads it. */\nexport type DrawnRegion = RegionMapRegion & { geometry: AreaGeometry };\n\n/** The areas worth drawing — any part in the view — largest first. */\nexport function drawOrder(\n regions: readonly RegionMapRegion[],\n geometryOf: (rings: readonly RegionMapRing[]) => AreaGeometry,\n view: PlaneBox,\n): DrawnRegion[] {\n return regions\n .map((region) => ({ ...region, geometry: geometryOf(region.rings) }))\n .filter(({ geometry }) => geometry.bounds !== null && boxesOverlap(geometry.bounds, view))\n .sort((a, b) => b.geometry.area - a.geometry.area);\n}\n\n/**\n * A Google encoded polyline into `[latitude, longitude]` points, at 1e-5 — the\n * compact form an outline usually travels in. Product-free, so it lives here.\n */\nexport function decodePolyline(encoded: string, precision = 1e5): RegionMapPoint[] {\n const points: RegionMapPoint[] = [];\n let index = 0;\n let lat = 0;\n let lng = 0;\n const next = (): number => {\n let shift = 0;\n let result = 0;\n let byte: number;\n do {\n byte = encoded.charCodeAt(index++) - 63;\n result |= (byte & 0x1f) << shift;\n shift += 5;\n } while (byte >= 0x20);\n return result & 1 ? ~(result >> 1) : result >> 1;\n };\n while (index < encoded.length) {\n lat += next();\n lng += next();\n points.push([lat / precision, lng / precision]);\n }\n return points;\n}\n"],"mappings":"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","names":[]}
|
package/dist/chunk-DY2I6LMX.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
//# sourceMappingURL=chunk-DY2I6LMX.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/data-display/RegionMap/RegionMapDrawing.tsx"],"sourcesContent":["import Box from '@mui/material/Box/index.js';\nimport Typography from '@mui/material/Typography/index.js';\nimport { alpha, type Theme } from '@mui/material/styles/index.js';\nimport React from 'react';\n\nimport { rem, remPx } from '../../../tokens/relative';\n\nimport { labelPosition, type AreaGeometry, type DrawnRegion, type PlaneBox } from './RegionMap.helpers';\nimport type { RegionMapModel } from './RegionMap.hooks';\nimport { labelOrder, placeLabels } from './RegionMap.labels';\nimport type { RegionMapLayer } from './RegionMap.types';\n\nexport interface RegionMapDrawingProps {\n model: RegionMapModel & { view: PlaneBox };\n /** The host's layer, drawn inside the SVG under the circles (`RegionMapProps.overlay`). */\n overlay?: (layer: RegionMapLayer) => React.ReactNode;\n showCenter: boolean;\n highlightedIds: readonly string[];\n maxHeight: string | undefined;\n label: string;\n tappable: boolean;\n testId: string;\n theme: Theme;\n}\n\n/**\n * The drawing: one `role=\"img\"`, square, with HTML labels laid over it.\n *\n * Strokes are `non-scaling-stroke` and labels are HTML at theme sizes, so the\n * picture reads the same at 320 px and 1600 px; `maxHeight` caps it on a wide\n * screen. Where it sits in a wider box is the host's call.\n *\n * The base areas are a memoised layer: a toggle changes `excluded`, not the\n * geometry, so only the hatch, highlight and label layers re-render.\n */\nexport function RegionMapDrawing({\n model,\n overlay,\n showCenter,\n highlightedIds,\n maxHeight,\n label,\n tappable,\n testId,\n theme,\n}: RegionMapDrawingProps): React.JSX.Element {\n const hatchId = `${React.useId().replace(/:/g, '')}-hatch`;\n const { view, drawn } = model;\n const wanted = new Set(highlightedIds);\n const highlighted = drawn.filter((d) => wanted.has(d.id));\n // Highlighted, then excluded, then larger, then by name: the order labels\n // claim their place in, so a collision hides the less important one.\n const labelled = labelOrder(drawn.filter((d) => d.excluded || wanted.has(d.id)), wanted);\n const boxRef = React.useRef<HTMLDivElement>(null);\n useLabelPlacement(boxRef, labelled.map((d) => d.id).join('|'), remPx(theme, 4));\n const base = drawn.map((d) => ({ id: d.id, name: d.name, geometry: d.geometry, outside: d.outside === true }));\n return (\n <Box\n sx={{\n position: 'relative',\n width: '100%',\n maxWidth: maxHeight,\n maxHeight,\n aspectRatio: '1 / 1',\n border: 1,\n borderColor: 'divider',\n borderRadius: 1,\n overflow: 'hidden',\n bgcolor: 'background.paper',\n }}\n data-testid={`${testId}-canvas`}\n ref={boxRef}\n >\n <svg\n viewBox={`${view.minX} ${view.minY} ${view.spanX} ${view.spanY}`}\n role=\"img\"\n aria-label={label}\n onPointerDown={model.onPointerDown}\n onClick={model.onClick}\n style={{ display: 'block', width: '100%', height: '100%', cursor: tappable ? 'pointer' : 'default' }}\n >\n <Hatch id={hatchId} pitch={view.spanX / 70} theme={theme} />\n <BaseAreas areas={base} testId={testId} theme={theme} />\n <ExcludedAreas drawn={drawn} hatchId={hatchId} testId={testId} theme={theme} />\n {overlay && model.projection && (\n // `pointer-events` is inherited in SVG: nothing the host draws here\n // takes a tap from the areas beneath.\n <g pointerEvents=\"none\" data-testid={`${testId}-overlay`}>\n {overlay({ view, projection: model.projection })}\n </g>\n )}\n <Circles radii={model.radii} theme={theme} />\n <Highlights areas={highlighted} testId={testId} theme={theme} />\n {showCenter && (\n <circle\n cx={0}\n cy={0}\n r={view.spanX / 90}\n fill={theme.palette.primary.main}\n stroke={theme.palette.background.paper}\n strokeWidth={2}\n vectorEffect=\"non-scaling-stroke\"\n pointerEvents=\"none\"\n data-testid={`${testId}-center`}\n />\n )}\n </svg>\n {labelled.map((d) => (\n <AreaLabel key={`l-${d.id}`} area={d} view={view} testId={testId} theme={theme} />\n ))}\n </Box>\n );\n}\n\n/** The highlighted areas, drawn over everything but the centre. */\nfunction Highlights({\n areas,\n testId,\n theme,\n}: {\n areas: readonly DrawnRegion[];\n testId: string;\n theme: Theme;\n}): React.JSX.Element {\n return (\n <>\n {areas.map((d) => (\n <path\n key={`h-${d.id}`}\n d={d.geometry.path}\n fillRule=\"evenodd\"\n vectorEffect=\"non-scaling-stroke\"\n fill={alpha(theme.palette.primary.main, 0.3)}\n stroke={theme.palette.primary.main}\n strokeWidth={2.5}\n pointerEvents=\"none\"\n data-testid={`${testId}-highlight-${d.id}`}\n />\n ))}\n </>\n );\n}\n\n/** The hatch an excluded area is filled with: lines, not a colour. */\nfunction Hatch({ id, pitch, theme }: { id: string; pitch: number; theme: Theme }): React.JSX.Element {\n return (\n <defs>\n <pattern id={id} width={pitch} height={pitch} patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate(45)\">\n <rect width={pitch} height={pitch} fill={alpha(theme.palette.text.primary, 0.1)} />\n <rect width={pitch / 3} height={pitch} fill={theme.palette.text.secondary} />\n </pattern>\n </defs>\n );\n}\n\ninterface BaseArea {\n id: string;\n name: string;\n geometry: AreaGeometry;\n outside: boolean;\n}\n\ninterface BaseAreasProps {\n areas: readonly BaseArea[];\n testId: string;\n theme: Theme;\n}\n\n/** Same areas, same geometry objects, same look: nothing to re-render. */\nfunction sameBase(a: BaseAreasProps, b: BaseAreasProps): boolean {\n if (a.theme !== b.theme || a.testId !== b.testId || a.areas.length !== b.areas.length) return false;\n return a.areas.every((area, i) => {\n const other = b.areas[i]!;\n return (\n area.id === other.id &&\n area.geometry === other.geometry &&\n area.outside === other.outside &&\n area.name === other.name\n );\n });\n}\n\n/** Every area's fill and border — the tap targets. Memoised: see `sameBase`. */\nconst BaseAreas = React.memo(function BaseAreas({ areas, testId, theme }: BaseAreasProps): React.JSX.Element {\n const { palette } = theme;\n return (\n <>\n {areas.map((d) => (\n <path\n key={d.id}\n d={d.geometry.path}\n fillRule=\"evenodd\"\n vectorEffect=\"non-scaling-stroke\"\n fill={d.outside ? palette.background.paper : palette.action.hover}\n stroke={palette.divider}\n strokeWidth={1}\n data-region-id={d.id}\n data-testid={`${testId}-area-${d.id}`}\n >\n <title>{d.name}</title>\n </path>\n ))}\n </>\n );\n}, sameBase);\n\n/** The hatch over every excluded area. */\nfunction ExcludedAreas({\n drawn,\n hatchId,\n testId,\n theme,\n}: {\n drawn: readonly DrawnRegion[];\n hatchId: string;\n testId: string;\n theme: Theme;\n}): React.JSX.Element {\n return (\n <>\n {drawn\n .filter((d) => d.excluded)\n .map((d) => (\n <path\n key={`x-${d.id}`}\n d={d.geometry.path}\n fillRule=\"evenodd\"\n vectorEffect=\"non-scaling-stroke\"\n fill={`url(#${hatchId})`}\n stroke={theme.palette.text.secondary}\n strokeWidth={1.5}\n pointerEvents=\"none\"\n data-testid={`${testId}-excluded-${d.id}`}\n />\n ))}\n </>\n );\n}\n\n/** The dashed circles around the centre. */\nfunction Circles({ radii, theme }: { radii: readonly number[]; theme: Theme }): React.JSX.Element {\n return (\n <>\n {radii.map((radius) => (\n <circle\n key={radius}\n cx={0}\n cy={0}\n r={radius}\n fill=\"none\"\n stroke={theme.palette.info.main}\n strokeWidth={1.75}\n strokeDasharray=\"7 5\"\n vectorEffect=\"non-scaling-stroke\"\n pointerEvents=\"none\"\n />\n ))}\n </>\n );\n}\n\n/**\n * Places every label the drawing carries, in their DOM order (the priority\n * order), and hides one that would overlap a label already drawn\n * (`placeLabels`). Measured by a ResizeObserver, never in a layout effect: its\n * first call comes after the browser's own layout, so mounting a label on a\n * tap forces no synchronous reflow of the drawing.\n */\nfunction useLabelPlacement(boxRef: React.RefObject<HTMLDivElement | null>, key: string, gap: number): void {\n React.useEffect(() => {\n const box = boxRef.current;\n if (!box || typeof ResizeObserver === 'undefined') return undefined;\n const labels = Array.from(box.querySelectorAll<HTMLElement>('[data-region-label]'));\n const place = (): void => {\n const spanX = box.clientWidth;\n const spanY = box.clientHeight;\n if (spanX === 0 || spanY === 0) return;\n const placed = placeLabels(\n labels.map((label) => ({\n fractionX: Number(label.dataset.fractionX),\n fractionY: Number(label.dataset.fractionY),\n spanX: label.offsetWidth,\n spanY: label.offsetHeight,\n })),\n { spanX, spanY },\n gap,\n );\n placed.forEach(({ x, y, visible }, index) => {\n const label = labels[index]!;\n label.style.left = `${(x / spanX) * 100}%`;\n label.style.top = `${(y / spanY) * 100}%`;\n label.style.visibility = visible ? 'visible' : 'hidden';\n label.dataset.hidden = visible ? 'false' : 'true';\n });\n };\n const observer = new ResizeObserver(place);\n observer.observe(box);\n for (const label of labels) observer.observe(label);\n return () => observer.disconnect();\n }, [boxRef, key, gap]);\n}\n\n/**\n * An area's name over the drawing, at the theme's type size whatever the\n * width. Placed — and hidden on a collision — by {@link useLabelPlacement}.\n * Decorative (`aria-hidden`): the area's name is its `<title>` and its row in\n * the list.\n */\nfunction AreaLabel({\n area,\n view,\n testId,\n theme,\n}: {\n area: DrawnRegion;\n view: PlaneBox;\n testId: string;\n theme: Theme;\n}): React.JSX.Element {\n const at = labelPosition(area.geometry.rings, view);\n const halo = `0 0 ${rem(theme, 3)} ${theme.palette.background.paper}`;\n return (\n <Typography\n component=\"span\"\n aria-hidden\n data-testid={`${testId}-label-${area.id}`}\n data-region-label=\"\"\n data-fraction-x={at.fractionX}\n data-fraction-y={at.fractionY}\n // Position as an inline style, not `sx`: a per-label class would insert\n // a new CSS rule (and a style recalculation) on every tap.\n style={{\n left: `${(at.fractionX * 100).toFixed(2)}%`,\n top: `${(at.fractionY * 100).toFixed(2)}%`,\n }}\n sx={{\n position: 'absolute',\n transform: 'translate(-50%, -50%)',\n fontSize: rem(theme, 12),\n fontWeight: 700,\n lineHeight: 1.1,\n textAlign: 'center',\n maxWidth: '45%',\n color: 'text.primary',\n pointerEvents: 'none',\n textShadow: `${halo}, ${halo}`,\n }}\n >\n {area.name}\n </Typography>\n );\n}\n"],"mappings":"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","names":["BaseAreas"]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|