@accelint/map-toolkit 1.0.0 → 1.2.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/CHANGELOG.md +19 -0
- package/catalog-info.yaml +4 -4
- package/dist/camera/events.js.map +1 -1
- package/dist/camera/store.js.map +1 -1
- package/dist/cursor-coordinates/index.d.ts +4 -2
- package/dist/cursor-coordinates/index.js +3 -2
- package/dist/cursor-coordinates/store.d.ts +48 -0
- package/dist/cursor-coordinates/store.js +92 -0
- package/dist/cursor-coordinates/store.js.map +1 -0
- package/dist/cursor-coordinates/types.d.ts +87 -0
- package/dist/cursor-coordinates/types.js +12 -0
- package/dist/cursor-coordinates/use-cursor-coordinates.d.ts +41 -37
- package/dist/cursor-coordinates/use-cursor-coordinates.js +131 -202
- package/dist/cursor-coordinates/use-cursor-coordinates.js.map +1 -1
- package/dist/deckgl/base-map/controls.d.ts +1 -1
- package/dist/deckgl/base-map/controls.js.map +1 -1
- package/dist/deckgl/base-map/events.js.map +1 -1
- package/dist/deckgl/base-map/index.d.ts +2 -2
- package/dist/deckgl/base-map/index.js +3 -3
- package/dist/deckgl/base-map/provider.d.ts +2 -2
- package/dist/deckgl/base-map/provider.js.map +1 -1
- package/dist/deckgl/index.js +1 -1
- package/dist/deckgl/saved-viewports/index.js.map +1 -1
- package/dist/deckgl/saved-viewports/storage.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/constants.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/fiber.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/index.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/shape-label-layer.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/store.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/use-select-shape.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/utils/display-style.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/utils/labels.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/constants.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/events.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/fiber.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/index.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-ellipse-mode-with-tooltip.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/index.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/store.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/use-draw-shape.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/utils/feature-conversion.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/constants.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/events.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/index.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/base-transform-mode.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/circle-transform-mode.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/index.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/rotate-mode-with-snap.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/scale-mode-with-free-transform.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/vertex-transform-mode.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/store.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/use-edit-shape.js.map +1 -1
- package/dist/deckgl/shapes/index.d.ts +1 -1
- package/dist/deckgl/shapes/shared/constants.js.map +1 -1
- package/dist/deckgl/shapes/shared/events.js.map +1 -1
- package/dist/deckgl/shapes/shared/hooks/use-shift-zoom-disable.js.map +1 -1
- package/dist/deckgl/shapes/shared/types.js.map +1 -1
- package/dist/deckgl/shapes/shared/utils/geometry-measurements.js.map +1 -1
- package/dist/deckgl/shapes/shared/utils/layer-config.js.map +1 -1
- package/dist/deckgl/shapes/shared/utils/mode-utils.js.map +1 -1
- package/dist/deckgl/shapes/shared/utils/pick-filtering.js.map +1 -1
- package/dist/deckgl/shapes/shared/utils/style-utils.js.map +1 -1
- package/dist/deckgl/symbol-layer/fiber.js.map +1 -1
- package/dist/deckgl/symbol-layer/index.js.map +1 -1
- package/dist/deckgl/text-layer/character-sets.js.map +1 -1
- package/dist/deckgl/text-layer/default-settings.js.map +1 -1
- package/dist/deckgl/text-layer/fiber.js.map +1 -1
- package/dist/deckgl/text-layer/index.js.map +1 -1
- package/dist/deckgl/text-settings.js.map +1 -1
- package/dist/map-cursor/events.js.map +1 -1
- package/dist/map-cursor/store.js.map +1 -1
- package/dist/map-cursor/use-map-cursor.js.map +1 -1
- package/dist/map-mode/events.js.map +1 -1
- package/dist/map-mode/store.js.map +1 -1
- package/dist/map-mode/use-map-mode.js.map +1 -1
- package/dist/maplibre/hooks/use-maplibre.js.map +1 -1
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/create-map-store.js.map +1 -1
- package/dist/shared/units.js.map +1 -1
- package/dist/viewport/store.js.map +1 -1
- package/dist/viewport/utils.js.map +1 -1
- package/dist/viewport/viewport-size.js.map +1 -1
- package/package.json +12 -10
|
@@ -13,210 +13,88 @@
|
|
|
13
13
|
|
|
14
14
|
'use client';
|
|
15
15
|
|
|
16
|
-
import { MapEvents } from "../deckgl/base-map/events.js";
|
|
17
16
|
import { MapContext } from "../deckgl/base-map/provider.js";
|
|
18
|
-
import {
|
|
19
|
-
import { useContext, useMemo
|
|
20
|
-
import {
|
|
17
|
+
import { cursorCoordinateStore } from "./store.js";
|
|
18
|
+
import { useContext, useMemo } from "react";
|
|
19
|
+
import { getLogger } from "@accelint/logger";
|
|
20
|
+
import { coordinateSystems, createCoordinate, formatDecimalDegrees, formatDegreesDecimalMinutes, formatDegreesMinutesSeconds } from "@accelint/geo";
|
|
21
21
|
|
|
22
22
|
//#region src/cursor-coordinates/use-cursor-coordinates.ts
|
|
23
|
-
const
|
|
24
|
-
|
|
23
|
+
const logger = getLogger({
|
|
24
|
+
enabled: process.env.NODE_ENV !== "production" && process.env.NODE_ENV !== "test",
|
|
25
|
+
level: "warn",
|
|
26
|
+
prefix: "[CursorCoordinates]",
|
|
27
|
+
pretty: true
|
|
28
|
+
});
|
|
25
29
|
const MAX_LONGITUDE = 180;
|
|
26
30
|
const LONGITUDE_RANGE = 360;
|
|
27
|
-
const COORDINATE_PRECISION = 8;
|
|
28
31
|
const DEFAULT_COORDINATE = "--, --";
|
|
29
32
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* compass directions (E/W for longitude, N/S for latitude).
|
|
33
|
+
* Normalizes longitude to -180 to 180 range.
|
|
34
|
+
* Handles wraparound including multi-revolution values.
|
|
33
35
|
*
|
|
34
|
-
* @param
|
|
35
|
-
* @returns
|
|
36
|
+
* @param lon - Longitude value in degrees
|
|
37
|
+
* @returns Normalized longitude between -180 and 180
|
|
36
38
|
*/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
lon = ((lon + MAX_LONGITUDE) % LONGITUDE_RANGE + LONGITUDE_RANGE) % LONGITUDE_RANGE - MAX_LONGITUDE;
|
|
40
|
-
const lat = coord[1];
|
|
41
|
-
return `${`${Math.abs(lon).toFixed(COORDINATE_PRECISION)} ${lon < 0 ? "W" : "E"}`} / ${`${Math.abs(lat).toFixed(COORDINATE_PRECISION)} ${lat < 0 ? "S" : "N"}`}`;
|
|
42
|
-
};
|
|
43
|
-
/**
|
|
44
|
-
* Type guard to validate that a value is a proper coordinate tuple.
|
|
45
|
-
* Checks that the value is an array with exactly two finite numbers.
|
|
46
|
-
*
|
|
47
|
-
* @param value - Value to validate as a coordinate
|
|
48
|
-
* @returns True if value is a valid [longitude, latitude] tuple
|
|
49
|
-
*/
|
|
50
|
-
function isValidCoordinate(value) {
|
|
51
|
-
return Array.isArray(value) && value.length === 2 && value.every(Number.isFinite);
|
|
39
|
+
function normalizeLongitude(lon) {
|
|
40
|
+
return ((lon + MAX_LONGITUDE) % LONGITUDE_RANGE + LONGITUDE_RANGE) % LONGITUDE_RANGE - MAX_LONGITUDE;
|
|
52
41
|
}
|
|
53
42
|
/**
|
|
54
|
-
*
|
|
55
|
-
*/
|
|
56
|
-
const coordinateStore = /* @__PURE__ */ new Map();
|
|
57
|
-
/**
|
|
58
|
-
* Track React component subscribers per instanceId (for fan-out notifications).
|
|
59
|
-
* Each Set contains onStoreChange callbacks from useSyncExternalStore.
|
|
60
|
-
*/
|
|
61
|
-
const componentSubscribers = /* @__PURE__ */ new Map();
|
|
62
|
-
/**
|
|
63
|
-
* Cache of bus unsubscribe functions (1 per instanceId).
|
|
64
|
-
* This ensures we only have one bus listener per map, regardless of
|
|
65
|
-
* how many React components subscribe to it.
|
|
66
|
-
*/
|
|
67
|
-
const busUnsubscribers = /* @__PURE__ */ new Map();
|
|
68
|
-
/**
|
|
69
|
-
* Cache of subscription functions per instanceId to avoid recreating on every render
|
|
70
|
-
*/
|
|
71
|
-
const subscriptionCache = /* @__PURE__ */ new Map();
|
|
72
|
-
/**
|
|
73
|
-
* Cache of snapshot functions per instanceId to maintain referential stability
|
|
74
|
-
*/
|
|
75
|
-
const snapshotCache = /* @__PURE__ */ new Map();
|
|
76
|
-
/**
|
|
77
|
-
* Cache of server snapshot functions per instanceId to maintain referential stability.
|
|
78
|
-
* Server snapshots always return default coordinate since coordinate state is client-only.
|
|
79
|
-
*/
|
|
80
|
-
const serverSnapshotCache = /* @__PURE__ */ new Map();
|
|
81
|
-
/**
|
|
82
|
-
* Ensures a single bus listener exists for the given instanceId.
|
|
83
|
-
* All React subscribers will be notified via fan-out when the bus event fires.
|
|
84
|
-
* This prevents creating N bus listeners for N React components.
|
|
43
|
+
* Builds a RawCoordinate object from a coordinate tuple.
|
|
85
44
|
*
|
|
86
|
-
* @param
|
|
45
|
+
* @param coord - Coordinate tuple [longitude, latitude] or null
|
|
46
|
+
* @returns RawCoordinate object or null
|
|
87
47
|
*/
|
|
88
|
-
function
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (state) {
|
|
95
|
-
if (isValidCoordinate(coords)) state.coordinate = coords;
|
|
96
|
-
else state.coordinate = null;
|
|
97
|
-
const subscribers = componentSubscribers.get(instanceId);
|
|
98
|
-
if (subscribers) for (const onStoreChange of subscribers) onStoreChange();
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
busUnsubscribers.set(instanceId, unsub);
|
|
48
|
+
function buildRawCoordinate(coord) {
|
|
49
|
+
if (!coord) return null;
|
|
50
|
+
return {
|
|
51
|
+
longitude: normalizeLongitude(coord[0]),
|
|
52
|
+
latitude: coord[1]
|
|
53
|
+
};
|
|
102
54
|
}
|
|
103
55
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
56
|
+
* Formats a coordinate using the specified format.
|
|
57
|
+
* Uses @accelint/geo formatters which match CoordinateField precision:
|
|
58
|
+
* - DD: 6 decimal places
|
|
59
|
+
* - DDM: 4 decimal places for minutes
|
|
60
|
+
* - DMS: 2 decimal places for seconds
|
|
61
|
+
*
|
|
62
|
+
* @param coord - Coordinate tuple [longitude, latitude]
|
|
63
|
+
* @param format - Coordinate format type
|
|
64
|
+
* @returns Formatted coordinate string
|
|
65
|
+
*/
|
|
66
|
+
function formatCoordinate(coord, format) {
|
|
67
|
+
const normalizedLon = normalizeLongitude(coord[0]);
|
|
68
|
+
const latLon = [coord[1], normalizedLon];
|
|
69
|
+
switch (format) {
|
|
70
|
+
case "dd": return formatDecimalDegrees(latLon, {
|
|
71
|
+
withOrdinal: true,
|
|
72
|
+
separator: " / ",
|
|
73
|
+
prefix: "",
|
|
74
|
+
suffix: ""
|
|
75
|
+
});
|
|
76
|
+
case "ddm": return formatDegreesDecimalMinutes(latLon, {
|
|
77
|
+
withOrdinal: true,
|
|
78
|
+
separator: " / ",
|
|
79
|
+
prefix: "",
|
|
80
|
+
suffix: ""
|
|
81
|
+
});
|
|
82
|
+
case "dms": return formatDegreesMinutesSeconds(latLon, {
|
|
83
|
+
withOrdinal: true,
|
|
84
|
+
separator: " / ",
|
|
85
|
+
prefix: "",
|
|
86
|
+
suffix: ""
|
|
87
|
+
});
|
|
88
|
+
case "mgrs":
|
|
89
|
+
case "utm": {
|
|
90
|
+
const lat = latLon[0];
|
|
91
|
+
const lon = latLon[1];
|
|
92
|
+
const latOrdinal = lat >= 0 ? "N" : "S";
|
|
93
|
+
const lonOrdinal = lon >= 0 ? "E" : "W";
|
|
94
|
+
const formattedInput = `${Math.abs(lon).toFixed(10)} ${lonOrdinal} / ${Math.abs(lat).toFixed(10)} ${latOrdinal}`;
|
|
95
|
+
return createCoordinate(coordinateSystems.dd, "LONLAT")(formattedInput)[format]();
|
|
138
96
|
}
|
|
139
|
-
subscriberSet.add(onStoreChange);
|
|
140
|
-
return () => {
|
|
141
|
-
const currentSubscriberSet = componentSubscribers.get(instanceId);
|
|
142
|
-
if (currentSubscriberSet) currentSubscriberSet.delete(onStoreChange);
|
|
143
|
-
cleanupBusListenerIfNeeded(instanceId);
|
|
144
|
-
};
|
|
145
|
-
});
|
|
146
|
-
subscriptionCache.set(instanceId, subscription);
|
|
147
|
-
return subscription;
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Creates or retrieves a cached snapshot function for a given instanceId.
|
|
151
|
-
* The function must read from the store on every call to get current state.
|
|
152
|
-
*
|
|
153
|
-
* @param instanceId - The unique identifier for the map
|
|
154
|
-
* @returns A snapshot function for useSyncExternalStore that returns formatted coordinate string
|
|
155
|
-
*/
|
|
156
|
-
function getOrCreateSnapshot(instanceId) {
|
|
157
|
-
let cached = snapshotCache.get(instanceId);
|
|
158
|
-
if (!cached) {
|
|
159
|
-
cached = () => {
|
|
160
|
-
const state = coordinateStore.get(instanceId);
|
|
161
|
-
if (!state) return DEFAULT_COORDINATE;
|
|
162
|
-
if (!state.coordinate) return DEFAULT_COORDINATE;
|
|
163
|
-
return create(prepareCoord(state.coordinate))[state.format]();
|
|
164
|
-
};
|
|
165
|
-
snapshotCache.set(instanceId, cached);
|
|
166
97
|
}
|
|
167
|
-
return cached;
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Creates or retrieves a cached server snapshot function for a given instanceId.
|
|
171
|
-
* Server snapshots always return the default coordinate since coordinate state is client-only.
|
|
172
|
-
* Required for SSR/RSC compatibility with useSyncExternalStore.
|
|
173
|
-
*
|
|
174
|
-
* @param instanceId - The unique identifier for the map
|
|
175
|
-
* @returns A server snapshot function for useSyncExternalStore
|
|
176
|
-
*/
|
|
177
|
-
function getOrCreateServerSnapshot(instanceId) {
|
|
178
|
-
const serverSnapshot = serverSnapshotCache.get(instanceId) ?? (() => DEFAULT_COORDINATE);
|
|
179
|
-
serverSnapshotCache.set(instanceId, serverSnapshot);
|
|
180
|
-
return serverSnapshot;
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Updates the format for a given map instance and notifies subscribers.
|
|
184
|
-
*
|
|
185
|
-
* @param instanceId - The unique identifier for the map
|
|
186
|
-
* @param format - The new coordinate format to use
|
|
187
|
-
*/
|
|
188
|
-
function setFormatForInstance(instanceId, format) {
|
|
189
|
-
const state = coordinateStore.get(instanceId);
|
|
190
|
-
if (state) {
|
|
191
|
-
state.format = format;
|
|
192
|
-
const subscribers = componentSubscribers.get(instanceId);
|
|
193
|
-
if (subscribers) for (const onStoreChange of subscribers) onStoreChange();
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Manually clear cursor coordinate state for a specific instanceId.
|
|
198
|
-
* This is typically not needed as cleanup happens automatically when all subscribers unmount.
|
|
199
|
-
* Use this only in advanced scenarios where manual cleanup is required.
|
|
200
|
-
*
|
|
201
|
-
* @param instanceId - The unique identifier for the map to clear
|
|
202
|
-
*
|
|
203
|
-
* @example
|
|
204
|
-
* ```tsx
|
|
205
|
-
* // Manual cleanup (rarely needed)
|
|
206
|
-
* clearCursorCoordinateState('my-map-instance');
|
|
207
|
-
* ```
|
|
208
|
-
*/
|
|
209
|
-
function clearCursorCoordinateState(instanceId) {
|
|
210
|
-
const unsub = busUnsubscribers.get(instanceId);
|
|
211
|
-
if (unsub) {
|
|
212
|
-
unsub();
|
|
213
|
-
busUnsubscribers.delete(instanceId);
|
|
214
|
-
}
|
|
215
|
-
coordinateStore.delete(instanceId);
|
|
216
|
-
componentSubscribers.delete(instanceId);
|
|
217
|
-
subscriptionCache.delete(instanceId);
|
|
218
|
-
snapshotCache.delete(instanceId);
|
|
219
|
-
serverSnapshotCache.delete(instanceId);
|
|
220
98
|
}
|
|
221
99
|
/**
|
|
222
100
|
* React hook that tracks and formats the cursor hover position coordinates on a map.
|
|
@@ -225,17 +103,15 @@ function clearCursorCoordinateState(instanceId) {
|
|
|
225
103
|
* geographic formats (Decimal Degrees, DMS, MGRS, UTM, etc.). The hook automatically
|
|
226
104
|
* filters events to only process those from the specified map instance.
|
|
227
105
|
*
|
|
228
|
-
* Uses
|
|
229
|
-
* where multiple components can subscribe to the same map's coordinates with a single
|
|
230
|
-
* bus listener.
|
|
106
|
+
* Uses the shared store factory for efficient state management and automatic cleanup.
|
|
231
107
|
*
|
|
232
108
|
* @param id - Optional map instance ID. If not provided, attempts to use the ID from MapProvider context.
|
|
233
|
-
* @
|
|
234
|
-
* @
|
|
235
|
-
* @property setFormat - Function to change the coordinate format system
|
|
109
|
+
* @param options - Optional configuration options
|
|
110
|
+
* @returns Object containing the formatted coordinate string, raw coordinate, format setter, and current format
|
|
236
111
|
* @throws {Error} When no id is provided and hook is used outside MapProvider context
|
|
237
112
|
*
|
|
238
113
|
* @example
|
|
114
|
+
* Basic usage:
|
|
239
115
|
* ```tsx
|
|
240
116
|
* import { uuid } from '@accelint/core';
|
|
241
117
|
* import { useCursorCoordinates } from '@accelint/map-toolkit/cursor-coordinates';
|
|
@@ -259,22 +135,75 @@ function clearCursorCoordinateState(instanceId) {
|
|
|
259
135
|
* );
|
|
260
136
|
* }
|
|
261
137
|
* ```
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* With custom formatter:
|
|
141
|
+
* ```tsx
|
|
142
|
+
* function CustomCoordinateDisplay() {
|
|
143
|
+
* const { formattedCoord, rawCoord } = useCursorCoordinates(MAP_ID, {
|
|
144
|
+
* formatter: (coord) =>
|
|
145
|
+
* `Lat: ${coord.latitude.toFixed(6)}° Lng: ${coord.longitude.toFixed(6)}°`,
|
|
146
|
+
* });
|
|
147
|
+
*
|
|
148
|
+
* return <div>{formattedCoord}</div>;
|
|
149
|
+
* }
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* Accessing raw coordinates:
|
|
154
|
+
* ```tsx
|
|
155
|
+
* function RawCoordinateDisplay() {
|
|
156
|
+
* const { rawCoord, currentFormat } = useCursorCoordinates(MAP_ID);
|
|
157
|
+
*
|
|
158
|
+
* if (!rawCoord) {
|
|
159
|
+
* return <div>Move cursor over map</div>;
|
|
160
|
+
* }
|
|
161
|
+
*
|
|
162
|
+
* return (
|
|
163
|
+
* <div>
|
|
164
|
+
* <div>Longitude: {rawCoord.longitude}</div>
|
|
165
|
+
* <div>Latitude: {rawCoord.latitude}</div>
|
|
166
|
+
* <div>Format: {currentFormat}</div>
|
|
167
|
+
* </div>
|
|
168
|
+
* );
|
|
169
|
+
* }
|
|
170
|
+
* ```
|
|
262
171
|
*/
|
|
263
|
-
function useCursorCoordinates(id) {
|
|
172
|
+
function useCursorCoordinates(id, options) {
|
|
264
173
|
const contextId = useContext(MapContext);
|
|
265
174
|
const actualId = id ?? contextId;
|
|
266
175
|
if (!actualId) throw new Error("useCursorCoordinates requires either an id parameter or to be used within a MapProvider");
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
176
|
+
const customFormatter = options?.formatter;
|
|
177
|
+
const { state, setFormat } = cursorCoordinateStore.use(actualId);
|
|
178
|
+
const rawCoord = useMemo(() => buildRawCoordinate(state.coordinate), [state.coordinate]);
|
|
179
|
+
const formattedCoord = useMemo(() => {
|
|
180
|
+
if (!rawCoord) return DEFAULT_COORDINATE;
|
|
181
|
+
if (customFormatter) try {
|
|
182
|
+
return customFormatter(rawCoord);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
logger.error(`Custom formatter failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
185
|
+
return DEFAULT_COORDINATE;
|
|
186
|
+
}
|
|
187
|
+
return formatCoordinate(state.coordinate, state.format);
|
|
188
|
+
}, [
|
|
189
|
+
rawCoord,
|
|
190
|
+
customFormatter,
|
|
191
|
+
state.format,
|
|
192
|
+
state.coordinate
|
|
193
|
+
]);
|
|
272
194
|
return useMemo(() => ({
|
|
273
195
|
formattedCoord,
|
|
274
|
-
setFormat
|
|
275
|
-
|
|
196
|
+
setFormat,
|
|
197
|
+
rawCoord,
|
|
198
|
+
currentFormat: state.format
|
|
199
|
+
}), [
|
|
200
|
+
formattedCoord,
|
|
201
|
+
setFormat,
|
|
202
|
+
rawCoord,
|
|
203
|
+
state.format
|
|
204
|
+
]);
|
|
276
205
|
}
|
|
277
206
|
|
|
278
207
|
//#endregion
|
|
279
|
-
export {
|
|
208
|
+
export { useCursorCoordinates };
|
|
280
209
|
//# sourceMappingURL=use-cursor-coordinates.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-cursor-coordinates.js","names":[],"sources":["../../src/cursor-coordinates/use-cursor-coordinates.ts"],"sourcesContent":["/*\n * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n'use client';\n\nimport 'client-only';\nimport { Broadcast } from '@accelint/bus';\nimport { coordinateSystems, createCoordinate } from '@accelint/geo';\nimport { useContext, useMemo, useSyncExternalStore } from 'react';\nimport { MapEvents } from '../deckgl/base-map/events';\nimport { MapContext } from '../deckgl/base-map/provider';\nimport type { UniqueId } from '@accelint/core';\nimport type { MapEventType, MapHoverEvent } from '../deckgl/base-map/types';\n\n/**\n * Supported coordinate format types for displaying map coordinates.\n *\n * @typedef {'dd' | 'ddm' | 'dms' | 'mgrs' | 'utm'} CoordinateFormatTypes\n * @property dd - Decimal Degrees (e.g., \"45.50000000 E / 30.25000000 N\")\n * @property ddm - Degrees Decimal Minutes (e.g., \"45° 30' E / 30° 15' N\")\n * @property dms - Degrees Minutes Seconds (e.g., \"45° 30' 0\\\" E / 30° 15' 0\\\" N\")\n * @property mgrs - Military Grid Reference System (e.g., \"31U DQ 48251 11932\")\n * @property utm - Universal Transverse Mercator (e.g., \"31N 448251 5411932\")\n */\nexport type CoordinateFormatTypes = keyof typeof coordinateSystems;\n\nconst bus = Broadcast.getInstance<MapEventType>();\nconst create = createCoordinate(coordinateSystems.dd, 'LONLAT');\n\nconst MAX_LONGITUDE = 180;\nconst LONGITUDE_RANGE = 360;\nconst COORDINATE_PRECISION = 8;\nconst DEFAULT_COORDINATE = '--, --';\n\n/**\n * Prepares coordinates for display by normalizing longitude and formatting with cardinal directions.\n * Normalizes longitude to -180 to 180 range and formats both longitude and latitude with\n * compass directions (E/W for longitude, N/S for latitude).\n *\n * @param coord - Tuple of [longitude, latitude] coordinates\n * @returns Formatted string in the format \"LON.NNNNNNNN E/W / LAT.NNNNNNNN N/S\"\n */\nconst prepareCoord = (coord: [number, number]) => {\n // Normalize longitude to -180 to 180 range (handles wraparound including multi-revolution values)\n let lon = coord[0];\n lon =\n ((((lon + MAX_LONGITUDE) % LONGITUDE_RANGE) + LONGITUDE_RANGE) %\n LONGITUDE_RANGE) -\n MAX_LONGITUDE;\n\n const lat = coord[1];\n const lonStr = `${Math.abs(lon).toFixed(COORDINATE_PRECISION)} ${lon < 0 ? 'W' : 'E'}`;\n const latStr = `${Math.abs(lat).toFixed(COORDINATE_PRECISION)} ${lat < 0 ? 'S' : 'N'}`;\n\n return `${lonStr} / ${latStr}`;\n};\n\n/**\n * Type guard to validate that a value is a proper coordinate tuple.\n * Checks that the value is an array with exactly two finite numbers.\n *\n * @param value - Value to validate as a coordinate\n * @returns True if value is a valid [longitude, latitude] tuple\n */\nfunction isValidCoordinate(value?: number[]): value is [number, number] {\n return (\n Array.isArray(value) && value.length === 2 && value.every(Number.isFinite)\n );\n}\n\n/**\n * State stored for each map instance's cursor coordinates\n */\ntype CursorCoordinateState = {\n coordinate: [number, number] | null;\n format: CoordinateFormatTypes;\n};\n\n/**\n * Store for cursor coordinate state keyed by instanceId\n */\nconst coordinateStore = new Map<UniqueId, CursorCoordinateState>();\n\n/**\n * Track React component subscribers per instanceId (for fan-out notifications).\n * Each Set contains onStoreChange callbacks from useSyncExternalStore.\n */\nconst componentSubscribers = new Map<UniqueId, Set<() => void>>();\n\n/**\n * Cache of bus unsubscribe functions (1 per instanceId).\n * This ensures we only have one bus listener per map, regardless of\n * how many React components subscribe to it.\n */\nconst busUnsubscribers = new Map<UniqueId, () => void>();\n\ntype Subscription = (onStoreChange: () => void) => () => void;\n/**\n * Cache of subscription functions per instanceId to avoid recreating on every render\n */\nconst subscriptionCache = new Map<UniqueId, Subscription>();\n\n/**\n * Cache of snapshot functions per instanceId to maintain referential stability\n */\nconst snapshotCache = new Map<UniqueId, () => string>();\n\n/**\n * Cache of server snapshot functions per instanceId to maintain referential stability.\n * Server snapshots always return default coordinate since coordinate state is client-only.\n */\nconst serverSnapshotCache = new Map<UniqueId, () => string>();\n\n/**\n * Ensures a single bus listener exists for the given instanceId.\n * All React subscribers will be notified via fan-out when the bus event fires.\n * This prevents creating N bus listeners for N React components.\n *\n * @param instanceId - The unique identifier for the map\n */\nfunction ensureBusListener(instanceId: UniqueId): void {\n if (busUnsubscribers.has(instanceId)) {\n return; // Already listening\n }\n\n const unsub = bus.on(MapEvents.hover, (data: MapHoverEvent) => {\n const eventId = data.payload.id;\n\n // Ignore hover events from other possible map instances\n if (instanceId !== eventId) {\n return;\n }\n\n const coords = data.payload.info.coordinate;\n const state = coordinateStore.get(instanceId);\n\n // Update coordinate if valid, or clear if invalid\n if (state) {\n if (isValidCoordinate(coords)) {\n state.coordinate = coords as [number, number];\n } else {\n state.coordinate = null;\n }\n\n // Fan-out: notify all React subscribers\n const subscribers = componentSubscribers.get(instanceId);\n if (subscribers) {\n for (const onStoreChange of subscribers) {\n onStoreChange();\n }\n }\n }\n });\n\n busUnsubscribers.set(instanceId, unsub);\n}\n\n/**\n * Cleans up the bus listener if no React subscribers remain.\n *\n * @param instanceId - The unique identifier for the map\n */\nfunction cleanupBusListenerIfNeeded(instanceId: UniqueId): void {\n const subscribers = componentSubscribers.get(instanceId);\n\n if (!subscribers || subscribers.size === 0) {\n // No more React subscribers - clean up bus listener\n const unsub = busUnsubscribers.get(instanceId);\n if (unsub) {\n unsub();\n busUnsubscribers.delete(instanceId);\n }\n\n // Clean up all state\n coordinateStore.delete(instanceId);\n componentSubscribers.delete(instanceId);\n subscriptionCache.delete(instanceId);\n snapshotCache.delete(instanceId);\n serverSnapshotCache.delete(instanceId);\n }\n}\n\n/**\n * Creates or retrieves a cached subscription function for a given instanceId.\n * Uses a fan-out pattern: 1 bus listener -> N React subscribers.\n * Automatically cleans up coordinate state when the last subscriber unsubscribes.\n *\n * @param instanceId - The unique identifier for the map\n * @returns A subscription function for useSyncExternalStore\n */\nfunction getOrCreateSubscription(\n instanceId: UniqueId,\n): (onStoreChange: () => void) => () => void {\n const subscription =\n subscriptionCache.get(instanceId) ??\n ((onStoreChange: () => void) => {\n // Ensure single bus listener exists for this instanceId\n ensureBusListener(instanceId);\n\n // Get or create the subscriber set for this map instance, then add this component's callback\n let subscriberSet = componentSubscribers.get(instanceId);\n if (!subscriberSet) {\n subscriberSet = new Set();\n componentSubscribers.set(instanceId, subscriberSet);\n }\n subscriberSet.add(onStoreChange);\n\n // Return cleanup function to remove this component's subscription\n return () => {\n const currentSubscriberSet = componentSubscribers.get(instanceId);\n if (currentSubscriberSet) {\n currentSubscriberSet.delete(onStoreChange);\n }\n\n // Clean up bus listener if this was the last React subscriber\n cleanupBusListenerIfNeeded(instanceId);\n };\n });\n\n subscriptionCache.set(instanceId, subscription);\n\n return subscription;\n}\n\n/**\n * Creates or retrieves a cached snapshot function for a given instanceId.\n * The function must read from the store on every call to get current state.\n *\n * @param instanceId - The unique identifier for the map\n * @returns A snapshot function for useSyncExternalStore that returns formatted coordinate string\n */\nfunction getOrCreateSnapshot(instanceId: UniqueId): () => string {\n let cached = snapshotCache.get(instanceId);\n\n if (!cached) {\n // Create a snapshot function that always reads current state from the store\n cached = () => {\n const state = coordinateStore.get(instanceId);\n\n if (!state) {\n return DEFAULT_COORDINATE;\n }\n\n if (!state.coordinate) {\n return DEFAULT_COORDINATE;\n }\n\n const coord = create(prepareCoord(state.coordinate));\n return coord[state.format]();\n };\n\n snapshotCache.set(instanceId, cached);\n }\n\n return cached;\n}\n\n/**\n * Creates or retrieves a cached server snapshot function for a given instanceId.\n * Server snapshots always return the default coordinate since coordinate state is client-only.\n * Required for SSR/RSC compatibility with useSyncExternalStore.\n *\n * @param instanceId - The unique identifier for the map\n * @returns A server snapshot function for useSyncExternalStore\n */\nfunction getOrCreateServerSnapshot(instanceId: UniqueId): () => string {\n const serverSnapshot =\n serverSnapshotCache.get(instanceId) ?? (() => DEFAULT_COORDINATE);\n\n serverSnapshotCache.set(instanceId, serverSnapshot);\n\n return serverSnapshot;\n}\n\n/**\n * Updates the format for a given map instance and notifies subscribers.\n *\n * @param instanceId - The unique identifier for the map\n * @param format - The new coordinate format to use\n */\nfunction setFormatForInstance(\n instanceId: UniqueId,\n format: CoordinateFormatTypes,\n): void {\n const state = coordinateStore.get(instanceId);\n if (state) {\n state.format = format;\n\n // Notify all subscribers of the format change\n // The coordinate remains unchanged; only the display format changes\n const subscribers = componentSubscribers.get(instanceId);\n if (subscribers) {\n for (const onStoreChange of subscribers) {\n onStoreChange();\n }\n }\n }\n}\n\n/**\n * Manually clear cursor coordinate state for a specific instanceId.\n * This is typically not needed as cleanup happens automatically when all subscribers unmount.\n * Use this only in advanced scenarios where manual cleanup is required.\n *\n * @param instanceId - The unique identifier for the map to clear\n *\n * @example\n * ```tsx\n * // Manual cleanup (rarely needed)\n * clearCursorCoordinateState('my-map-instance');\n * ```\n */\nexport function clearCursorCoordinateState(instanceId: UniqueId): void {\n // Unsubscribe from bus if listening\n const unsub = busUnsubscribers.get(instanceId);\n if (unsub) {\n unsub();\n busUnsubscribers.delete(instanceId);\n }\n\n // Clear all state\n coordinateStore.delete(instanceId);\n componentSubscribers.delete(instanceId);\n subscriptionCache.delete(instanceId);\n snapshotCache.delete(instanceId);\n serverSnapshotCache.delete(instanceId);\n}\n\n/**\n * React hook that tracks and formats the cursor hover position coordinates on a map.\n *\n * Subscribes to map hover events via the event bus and converts coordinates to various\n * geographic formats (Decimal Degrees, DMS, MGRS, UTM, etc.). The hook automatically\n * filters events to only process those from the specified map instance.\n *\n * Uses `useSyncExternalStore` for concurrent-safe updates and efficient fan-out pattern\n * where multiple components can subscribe to the same map's coordinates with a single\n * bus listener.\n *\n * @param id - Optional map instance ID. If not provided, attempts to use the ID from MapProvider context.\n * @returns Object containing the formatted coordinate string and format setter function\n * @property formattedCoord - The formatted coordinate string (defaults to \"--, --\" when no position)\n * @property setFormat - Function to change the coordinate format system\n * @throws {Error} When no id is provided and hook is used outside MapProvider context\n *\n * @example\n * ```tsx\n * import { uuid } from '@accelint/core';\n * import { useCursorCoordinates } from '@accelint/map-toolkit/cursor-coordinates';\n *\n * const MAP_ID = uuid();\n *\n * function CoordinateDisplay() {\n * const { formattedCoord, setFormat } = useCursorCoordinates(MAP_ID);\n *\n * return (\n * <div>\n * <select onChange={(e) => setFormat(e.target.value as CoordinateFormatTypes)}>\n * <option value=\"dd\">Decimal Degrees</option>\n * <option value=\"ddm\">Degrees Decimal Minutes</option>\n * <option value=\"dms\">Degrees Minutes Seconds</option>\n * <option value=\"mgrs\">MGRS</option>\n * <option value=\"utm\">UTM</option>\n * </select>\n * <div>{formattedCoord}</div>\n * </div>\n * );\n * }\n * ```\n */\nexport function useCursorCoordinates(id?: UniqueId) {\n const contextId = useContext(MapContext);\n const actualId = id ?? contextId;\n\n if (!actualId) {\n throw new Error(\n 'useCursorCoordinates requires either an id parameter or to be used within a MapProvider',\n );\n }\n\n // Initialize state for this map instance BEFORE subscribing\n // This ensures the bus listener has a store to write to\n if (!coordinateStore.has(actualId)) {\n coordinateStore.set(actualId, {\n coordinate: null,\n format: 'dd',\n });\n }\n\n // Subscribe to coordinate changes using useSyncExternalStore\n // This must happen after store initialization\n // Third parameter provides server snapshot for SSR/RSC compatibility\n const formattedCoord = useSyncExternalStore<string>(\n getOrCreateSubscription(actualId),\n getOrCreateSnapshot(actualId),\n getOrCreateServerSnapshot(actualId),\n );\n\n // Memoize the return value to prevent unnecessary re-renders\n return useMemo(\n () => ({\n formattedCoord,\n setFormat: (format: CoordinateFormatTypes) =>\n setFormatForInstance(actualId, format),\n }),\n [formattedCoord, actualId],\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-cursor-coordinates.js","names":["latLon: [number, number]"],"sources":["../../src/cursor-coordinates/use-cursor-coordinates.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n'use client';\n\nimport 'client-only';\nimport {\n coordinateSystems,\n createCoordinate,\n formatDecimalDegrees,\n formatDegreesDecimalMinutes,\n formatDegreesMinutesSeconds,\n} from '@accelint/geo';\nimport { getLogger } from '@accelint/logger';\nimport { useContext, useMemo } from 'react';\nimport { MapContext } from '../deckgl/base-map/provider';\nimport { cursorCoordinateStore } from './store';\nimport type { UniqueId } from '@accelint/core';\nimport type {\n CoordinateFormatTypes,\n RawCoordinate,\n UseCursorCoordinatesOptions,\n UseCursorCoordinatesReturn,\n} from './types';\n\nconst logger = getLogger({\n enabled:\n process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test',\n level: 'warn',\n prefix: '[CursorCoordinates]',\n pretty: true,\n});\n\nconst MAX_LONGITUDE = 180;\nconst LONGITUDE_RANGE = 360;\nconst DEFAULT_COORDINATE = '--, --';\n\n/**\n * Normalizes longitude to -180 to 180 range.\n * Handles wraparound including multi-revolution values.\n *\n * @param lon - Longitude value in degrees\n * @returns Normalized longitude between -180 and 180\n */\nfunction normalizeLongitude(lon: number): number {\n return (\n ((((lon + MAX_LONGITUDE) % LONGITUDE_RANGE) + LONGITUDE_RANGE) %\n LONGITUDE_RANGE) -\n MAX_LONGITUDE\n );\n}\n\n/**\n * Builds a RawCoordinate object from a coordinate tuple.\n *\n * @param coord - Coordinate tuple [longitude, latitude] or null\n * @returns RawCoordinate object or null\n */\nfunction buildRawCoordinate(coord: [number, number] | null): RawCoordinate {\n if (!coord) {\n return null;\n }\n\n const normalizedLon = normalizeLongitude(coord[0]);\n\n return {\n longitude: normalizedLon,\n latitude: coord[1],\n };\n}\n\n/**\n * Formats a coordinate using the specified format.\n * Uses @accelint/geo formatters which match CoordinateField precision:\n * - DD: 6 decimal places\n * - DDM: 4 decimal places for minutes\n * - DMS: 2 decimal places for seconds\n *\n * @param coord - Coordinate tuple [longitude, latitude]\n * @param format - Coordinate format type\n * @returns Formatted coordinate string\n */\nfunction formatCoordinate(\n coord: [number, number],\n format: CoordinateFormatTypes,\n): string {\n // Normalize longitude and convert to [lat, lon] for geo formatters\n const normalizedLon = normalizeLongitude(coord[0]);\n const latLon: [number, number] = [coord[1], normalizedLon];\n\n switch (format) {\n case 'dd':\n return formatDecimalDegrees(latLon, {\n withOrdinal: true,\n separator: ' / ',\n prefix: '',\n suffix: '',\n });\n case 'ddm':\n return formatDegreesDecimalMinutes(latLon, {\n withOrdinal: true,\n separator: ' / ',\n prefix: '',\n suffix: '',\n });\n case 'dms':\n return formatDegreesMinutesSeconds(latLon, {\n withOrdinal: true,\n separator: ' / ',\n prefix: '',\n suffix: '',\n });\n case 'mgrs':\n case 'utm': {\n // Use createCoordinate for grid-based formats\n // Input format: \"lon E / lat N\" for LONLAT (matching geo package DD tests)\n // Limit to 10 decimal places (geo parser max) and avoid floating point precision issues\n const lat = latLon[0];\n const lon = latLon[1];\n const latOrdinal = lat >= 0 ? 'N' : 'S';\n const lonOrdinal = lon >= 0 ? 'E' : 'W';\n // Use LONLAT format: longitude first, then latitude\n // toFixed(10) ensures we stay within the parser's regex limits\n const formattedInput = `${Math.abs(lon).toFixed(10)} ${lonOrdinal} / ${Math.abs(lat).toFixed(10)} ${latOrdinal}`;\n\n const geoCoord = createCoordinate(\n coordinateSystems.dd,\n 'LONLAT',\n )(formattedInput);\n\n return geoCoord[format]();\n }\n }\n}\n\n/**\n * React hook that tracks and formats the cursor hover position coordinates on a map.\n *\n * Subscribes to map hover events via the event bus and converts coordinates to various\n * geographic formats (Decimal Degrees, DMS, MGRS, UTM, etc.). The hook automatically\n * filters events to only process those from the specified map instance.\n *\n * Uses the shared store factory for efficient state management and automatic cleanup.\n *\n * @param id - Optional map instance ID. If not provided, attempts to use the ID from MapProvider context.\n * @param options - Optional configuration options\n * @returns Object containing the formatted coordinate string, raw coordinate, format setter, and current format\n * @throws {Error} When no id is provided and hook is used outside MapProvider context\n *\n * @example\n * Basic usage:\n * ```tsx\n * import { uuid } from '@accelint/core';\n * import { useCursorCoordinates } from '@accelint/map-toolkit/cursor-coordinates';\n *\n * const MAP_ID = uuid();\n *\n * function CoordinateDisplay() {\n * const { formattedCoord, setFormat } = useCursorCoordinates(MAP_ID);\n *\n * return (\n * <div>\n * <select onChange={(e) => setFormat(e.target.value as CoordinateFormatTypes)}>\n * <option value=\"dd\">Decimal Degrees</option>\n * <option value=\"ddm\">Degrees Decimal Minutes</option>\n * <option value=\"dms\">Degrees Minutes Seconds</option>\n * <option value=\"mgrs\">MGRS</option>\n * <option value=\"utm\">UTM</option>\n * </select>\n * <div>{formattedCoord}</div>\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * With custom formatter:\n * ```tsx\n * function CustomCoordinateDisplay() {\n * const { formattedCoord, rawCoord } = useCursorCoordinates(MAP_ID, {\n * formatter: (coord) =>\n * `Lat: ${coord.latitude.toFixed(6)}° Lng: ${coord.longitude.toFixed(6)}°`,\n * });\n *\n * return <div>{formattedCoord}</div>;\n * }\n * ```\n *\n * @example\n * Accessing raw coordinates:\n * ```tsx\n * function RawCoordinateDisplay() {\n * const { rawCoord, currentFormat } = useCursorCoordinates(MAP_ID);\n *\n * if (!rawCoord) {\n * return <div>Move cursor over map</div>;\n * }\n *\n * return (\n * <div>\n * <div>Longitude: {rawCoord.longitude}</div>\n * <div>Latitude: {rawCoord.latitude}</div>\n * <div>Format: {currentFormat}</div>\n * </div>\n * );\n * }\n * ```\n */\nexport function useCursorCoordinates(\n id?: UniqueId,\n options?: UseCursorCoordinatesOptions,\n): UseCursorCoordinatesReturn {\n const contextId = useContext(MapContext);\n const actualId = id ?? contextId;\n\n if (!actualId) {\n throw new Error(\n 'useCursorCoordinates requires either an id parameter or to be used within a MapProvider',\n );\n }\n\n const customFormatter = options?.formatter;\n\n // Use the store hook to get state and actions\n const { state, setFormat } = cursorCoordinateStore.use(actualId);\n\n // Build raw coordinate object\n const rawCoord = useMemo(\n () => buildRawCoordinate(state.coordinate),\n [state.coordinate],\n );\n\n // Compute formatted coordinate string\n const formattedCoord = useMemo(() => {\n if (!rawCoord) {\n return DEFAULT_COORDINATE;\n }\n\n // Use custom formatter if provided\n if (customFormatter) {\n try {\n return customFormatter(rawCoord);\n } catch (error) {\n logger.error(\n `Custom formatter failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n return DEFAULT_COORDINATE;\n }\n }\n\n // Use built-in formatter\n return formatCoordinate(state.coordinate!, state.format);\n }, [rawCoord, customFormatter, state.format, state.coordinate]);\n\n // Memoize the return value to prevent unnecessary re-renders\n return useMemo(\n () => ({\n formattedCoord,\n setFormat,\n rawCoord,\n currentFormat: state.format,\n }),\n [formattedCoord, setFormat, rawCoord, state.format],\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAiCA,MAAM,SAAS,UAAU;CACvB,SACE,QAAQ,IAAI,aAAa,gBAAgB,QAAQ,IAAI,aAAa;CACpE,OAAO;CACP,QAAQ;CACR,QAAQ;CACT,CAAC;AAEF,MAAM,gBAAgB;AACtB,MAAM,kBAAkB;AACxB,MAAM,qBAAqB;;;;;;;;AAS3B,SAAS,mBAAmB,KAAqB;AAC/C,UACM,MAAM,iBAAiB,kBAAmB,mBAC5C,kBACF;;;;;;;;AAUJ,SAAS,mBAAmB,OAA+C;AACzE,KAAI,CAAC,MACH,QAAO;AAKT,QAAO;EACL,WAHoB,mBAAmB,MAAM,GAAG;EAIhD,UAAU,MAAM;EACjB;;;;;;;;;;;;;AAcH,SAAS,iBACP,OACA,QACQ;CAER,MAAM,gBAAgB,mBAAmB,MAAM,GAAG;CAClD,MAAMA,SAA2B,CAAC,MAAM,IAAI,cAAc;AAE1D,SAAQ,QAAR;EACE,KAAK,KACH,QAAO,qBAAqB,QAAQ;GAClC,aAAa;GACb,WAAW;GACX,QAAQ;GACR,QAAQ;GACT,CAAC;EACJ,KAAK,MACH,QAAO,4BAA4B,QAAQ;GACzC,aAAa;GACb,WAAW;GACX,QAAQ;GACR,QAAQ;GACT,CAAC;EACJ,KAAK,MACH,QAAO,4BAA4B,QAAQ;GACzC,aAAa;GACb,WAAW;GACX,QAAQ;GACR,QAAQ;GACT,CAAC;EACJ,KAAK;EACL,KAAK,OAAO;GAIV,MAAM,MAAM,OAAO;GACnB,MAAM,MAAM,OAAO;GACnB,MAAM,aAAa,OAAO,IAAI,MAAM;GACpC,MAAM,aAAa,OAAO,IAAI,MAAM;GAGpC,MAAM,iBAAiB,GAAG,KAAK,IAAI,IAAI,CAAC,QAAQ,GAAG,CAAC,GAAG,WAAW,KAAK,KAAK,IAAI,IAAI,CAAC,QAAQ,GAAG,CAAC,GAAG;AAOpG,UALiB,iBACf,kBAAkB,IAClB,SACD,CAAC,eAAe,CAED,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8E/B,SAAgB,qBACd,IACA,SAC4B;CAC5B,MAAM,YAAY,WAAW,WAAW;CACxC,MAAM,WAAW,MAAM;AAEvB,KAAI,CAAC,SACH,OAAM,IAAI,MACR,0FACD;CAGH,MAAM,kBAAkB,SAAS;CAGjC,MAAM,EAAE,OAAO,cAAc,sBAAsB,IAAI,SAAS;CAGhE,MAAM,WAAW,cACT,mBAAmB,MAAM,WAAW,EAC1C,CAAC,MAAM,WAAW,CACnB;CAGD,MAAM,iBAAiB,cAAc;AACnC,MAAI,CAAC,SACH,QAAO;AAIT,MAAI,gBACF,KAAI;AACF,UAAO,gBAAgB,SAAS;WACzB,OAAO;AACd,UAAO,MACL,4BAA4B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,GACnF;AACD,UAAO;;AAKX,SAAO,iBAAiB,MAAM,YAAa,MAAM,OAAO;IACvD;EAAC;EAAU;EAAiB,MAAM;EAAQ,MAAM;EAAW,CAAC;AAG/D,QAAO,eACE;EACL;EACA;EACA;EACA,eAAe,MAAM;EACtB,GACD;EAAC;EAAgB;EAAW;EAAU,MAAM;EAAO,CACpD"}
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { RefObject } from "react";
|
|
14
|
-
import { UniqueId } from "@accelint/core";
|
|
15
14
|
import { MapRef } from "react-map-gl/maplibre";
|
|
15
|
+
import { UniqueId } from "@accelint/core";
|
|
16
16
|
|
|
17
17
|
//#region src/deckgl/base-map/controls.d.ts
|
|
18
18
|
type MapControlsProps = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controls.js","names":[],"sources":["../../../src/deckgl/base-map/controls.tsx"],"sourcesContent":["/*\n * Copyright
|
|
1
|
+
{"version":3,"file":"controls.js","names":[],"sources":["../../../src/deckgl/base-map/controls.tsx"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\n'use client';\n\nimport 'client-only';\nimport { useOn } from '@accelint/bus/react';\nimport { MapEvents } from './events';\nimport type { UniqueId } from '@accelint/core';\nimport type { RefObject } from 'react';\nimport type { MapRef } from 'react-map-gl/maplibre';\nimport type {\n MapDisablePanEvent,\n MapDisableZoomEvent,\n MapEnablePanEvent,\n MapEnableZoomEvent,\n} from './types';\n\ntype MapControlsProps = {\n id: UniqueId;\n mapRef: RefObject<MapRef | null>;\n};\n\n/**\n * Headless component that listens for map control events and applies them to the MapLibre instance.\n *\n * This component is rendered inside BaseMap to wire up event listeners\n * for pan and zoom control events.\n */\nexport function MapControls({ id, mapRef }: MapControlsProps) {\n useOn<MapEnablePanEvent>(MapEvents.enablePan, (event) => {\n if (event.payload.id === id) {\n mapRef.current?.getMap().dragPan.enable();\n }\n });\n\n useOn<MapDisablePanEvent>(MapEvents.disablePan, (event) => {\n if (event.payload.id === id) {\n mapRef.current?.getMap().dragPan.disable();\n }\n });\n\n useOn<MapEnableZoomEvent>(MapEvents.enableZoom, (event) => {\n if (event.payload.id === id) {\n mapRef.current?.getMap().scrollZoom.enable();\n mapRef.current?.getMap().doubleClickZoom.enable();\n mapRef.current?.getMap().boxZoom.enable();\n }\n });\n\n useOn<MapDisableZoomEvent>(MapEvents.disableZoom, (event) => {\n if (event.payload.id === id) {\n mapRef.current?.getMap().scrollZoom.disable();\n mapRef.current?.getMap().doubleClickZoom.disable();\n mapRef.current?.getMap().boxZoom.disable();\n }\n });\n\n return null;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,YAAY,EAAE,IAAI,UAA4B;AAC5D,OAAyB,UAAU,YAAY,UAAU;AACvD,MAAI,MAAM,QAAQ,OAAO,GACvB,QAAO,SAAS,QAAQ,CAAC,QAAQ,QAAQ;GAE3C;AAEF,OAA0B,UAAU,aAAa,UAAU;AACzD,MAAI,MAAM,QAAQ,OAAO,GACvB,QAAO,SAAS,QAAQ,CAAC,QAAQ,SAAS;GAE5C;AAEF,OAA0B,UAAU,aAAa,UAAU;AACzD,MAAI,MAAM,QAAQ,OAAO,IAAI;AAC3B,UAAO,SAAS,QAAQ,CAAC,WAAW,QAAQ;AAC5C,UAAO,SAAS,QAAQ,CAAC,gBAAgB,QAAQ;AACjD,UAAO,SAAS,QAAQ,CAAC,QAAQ,QAAQ;;GAE3C;AAEF,OAA2B,UAAU,cAAc,UAAU;AAC3D,MAAI,MAAM,QAAQ,OAAO,IAAI;AAC3B,UAAO,SAAS,QAAQ,CAAC,WAAW,SAAS;AAC7C,UAAO,SAAS,QAAQ,CAAC,gBAAgB,SAAS;AAClD,UAAO,SAAS,QAAQ,CAAC,QAAQ,SAAS;;GAE5C;AAEF,QAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","names":[],"sources":["../../../src/deckgl/base-map/events.ts"],"sourcesContent":["/*\n * Copyright
|
|
1
|
+
{"version":3,"file":"events.js","names":[],"sources":["../../../src/deckgl/base-map/events.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nexport const MapEventsNamespace = 'map';\n\nexport const MapEvents = {\n click: `${MapEventsNamespace}:click`,\n hover: `${MapEventsNamespace}:hover`,\n viewport: `${MapEventsNamespace}:viewport`,\n // Control events\n enablePan: `${MapEventsNamespace}:enablePan`,\n disablePan: `${MapEventsNamespace}:disablePan`,\n enableZoom: `${MapEventsNamespace}:enableZoom`,\n disableZoom: `${MapEventsNamespace}:disableZoom`,\n} as const;\n"],"mappings":";;;;;;;;;;;;;;AAYA,MAAa,qBAAqB;AAElC,MAAa,YAAY;CACvB,OAAO,GAAG,mBAAmB;CAC7B,OAAO,GAAG,mBAAmB;CAC7B,UAAU,GAAG,mBAAmB;CAEhC,WAAW,GAAG,mBAAmB;CACjC,YAAY,GAAG,mBAAmB;CAClC,YAAY,GAAG,mBAAmB;CAClC,aAAa,GAAG,mBAAmB;CACpC"}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { BaseMapProps } from "./types.js";
|
|
14
|
-
import * as
|
|
14
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
15
15
|
|
|
16
16
|
//#region src/deckgl/base-map/index.d.ts
|
|
17
17
|
|
|
@@ -106,7 +106,7 @@ declare function BaseMap({
|
|
|
106
106
|
onViewStateChange,
|
|
107
107
|
pickingRadius,
|
|
108
108
|
...rest
|
|
109
|
-
}: BaseMapProps):
|
|
109
|
+
}: BaseMapProps): react_jsx_runtime0.JSX.Element;
|
|
110
110
|
//#endregion
|
|
111
111
|
export { BaseMap };
|
|
112
112
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -14,17 +14,17 @@
|
|
|
14
14
|
'use client';
|
|
15
15
|
|
|
16
16
|
import { useMapCamera } from "../../camera/store.js";
|
|
17
|
-
import { MapEvents } from "./events.js";
|
|
18
17
|
import { getCursor } from "../../map-cursor/store.js";
|
|
19
|
-
import { MapProvider } from "./provider.js";
|
|
20
18
|
import { DEFAULT_VIEW_STATE } from "../../shared/constants.js";
|
|
21
19
|
import { DARK_BASE_MAP_STYLE, PARAMETERS, PICKING_RADIUS } from "./constants.js";
|
|
20
|
+
import { MapEvents } from "./events.js";
|
|
22
21
|
import { MapControls } from "./controls.js";
|
|
22
|
+
import { MapProvider } from "./provider.js";
|
|
23
23
|
import { useCallback, useId, useMemo, useRef } from "react";
|
|
24
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
25
24
|
import { useEffectEvent, useEmit } from "@accelint/bus/react";
|
|
26
25
|
import { Deckgl, useDeckgl } from "@deckgl-fiber-renderer/dom";
|
|
27
26
|
import { Map, useControl } from "react-map-gl/maplibre";
|
|
27
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
28
28
|
|
|
29
29
|
//#region src/deckgl/base-map/index.tsx
|
|
30
30
|
/**
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { ReactNode } from "react";
|
|
14
14
|
import { UniqueId } from "@accelint/core";
|
|
15
|
-
import * as
|
|
15
|
+
import * as react_jsx_runtime1 from "react/jsx-runtime";
|
|
16
16
|
|
|
17
17
|
//#region src/deckgl/base-map/provider.d.ts
|
|
18
18
|
/**
|
|
@@ -139,7 +139,7 @@ type MapProviderProps = {
|
|
|
139
139
|
declare function MapProvider({
|
|
140
140
|
children,
|
|
141
141
|
id
|
|
142
|
-
}: MapProviderProps):
|
|
142
|
+
}: MapProviderProps): react_jsx_runtime1.JSX.Element;
|
|
143
143
|
//#endregion
|
|
144
144
|
export { MapContext, MapProvider, MapProviderProps };
|
|
145
145
|
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","names":[],"sources":["../../../src/deckgl/base-map/provider.tsx"],"sourcesContent":["/*\n * Copyright
|
|
1
|
+
{"version":3,"file":"provider.js","names":[],"sources":["../../../src/deckgl/base-map/provider.tsx"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\n'use client';\n\nimport 'client-only';\nimport { createContext, type ReactNode, useEffect } from 'react';\nimport { clearCursorState } from '../../map-cursor/store';\nimport { clearMapModeState } from '../../map-mode/store';\nimport type { UniqueId } from '@accelint/core';\n\n/**\n * React context for map ID.\n * Use the `useMapMode` hook to access the map mode state.\n */\nexport const MapContext = createContext<UniqueId | null>(null);\n\n/**\n * Props for the MapProvider component.\n */\nexport type MapProviderProps = {\n /** Child components that will have access to map mode context */\n children: ReactNode;\n /**\n * Unique identifier for this map instance.\n *\n * Used to isolate mode changes between different map instances (e.g., main map vs minimap).\n * This is required and should be provided by the parent component (typically BaseMap).\n *\n * @example\n * ```tsx\n * // Multiple independent map instances\n * const mainMapId = uuid();\n * const minimapId = uuid();\n *\n * <MapProvider id={mainMapId}>\n * // Map layers and components\n * </MapProvider>\n *\n * <MapProvider id={minimapId}>\n * // Minimap layers and components\n * </MapProvider>\n * ```\n */\n id: UniqueId;\n};\n\n/**\n * Provider component for managing map modes with ownership and authorization.\n *\n * **Note**: This provider is used internally by `BaseMap` and should not be used directly.\n * Consumers should pass the `id` prop to `BaseMap`, which will create this provider automatically.\n *\n * This component uses a hybrid architecture combining React Context (for map instance identity)\n * with module-level state management (for map mode state). The provider:\n * - Provides a unique `id` via Context\n * - Cleans up map mode state when unmounted\n * - Allows components to subscribe to mode changes via `useMapMode` hook (which uses `useSyncExternalStore`)\n *\n * The module-level state management system implements a state machine for map modes where\n * components can request mode changes with ownership. When a mode is owned by a component,\n * other components must request authorization to change to a different mode. The system handles:\n *\n * - Automatic mode changes when no ownership conflicts exist\n * - Authorization flow when switching between owned modes\n * - Per-mode ownership tracking that persists throughout the session\n * - Pending request management (one pending request per requester)\n * - Auto-acceptance of first pending request when mode owner returns to default\n * - Auto-rejection of other pending requests when one is approved\n * - Event emission through a centralized event bus\n * - Instance isolation for multiple map scenarios (main map + minimap)\n * - Always initializes in 'default' mode\n *\n * ## Instance Isolation\n *\n * Each MapProvider instance operates independently. Mode changes in one instance\n * do not affect other instances, even when multiple maps are rendered on the same page.\n * This enables scenarios like:\n * - Main map in \"drawing\" mode while minimap stays in \"view\" mode\n * - Multiple independent map views with different interaction modes\n *\n * Events are scoped to specific instances using the `id` prop. The event bus\n * filters events to ensure each provider only responds to events for its own instance.\n *\n * ## Pending Request Behavior\n *\n * - Pending requests are stored by requester ID (not mode owner)\n * - Each requester can have only one pending request at a time\n * - New requests from the same requester auto-replace previous requests\n * - Pending requests persist when mode owner switches between their own modes\n * - When any request is approved, all other pending requests are auto-rejected\n * - When mode owner returns to default mode:\n * - If first pending request is for default mode, all pending requests are rejected (already in requested mode)\n * - If first pending request is for a different mode, that request is auto-approved and others are rejected\n *\n * ## Instance ID Stability and Lifecycle\n *\n * The provider's cleanup mechanism (via `useEffect`) ensures proper state management:\n * - Map mode state is cleaned up when the provider unmounts\n * - Changing the `id` prop will trigger cleanup of the old state via the effect dependency\n * - State is lazily initialized on first subscription (no manual creation needed)\n *\n * While the `id` prop should typically remain stable (created as a module-level constant\n * or with `useState`), changing it will work correctly due to the cleanup mechanism.\n *\n * @param props - Provider props including children and required id\n * @returns Provider component that wraps children with map instance identity context\n *\n * @example\n * Internal usage within BaseMap:\n * ```tsx\n * // BaseMap automatically creates the provider\n * function BaseMap({ id, children, ...props }: BaseMapProps) {\n * return (\n * <div>\n * <MapProvider id={id}>\n * <Deckgl {...props}>\n * {children}\n * </Deckgl>\n * </MapProvider>\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * With authorization handling - use id in event payloads:\n * ```tsx\n * useOn(MapModeEvents.changeAuthorization, (event) => {\n * const { authId, id } = event.payload;\n * emitDecision({ authId, approved: true, owner: 'tool', id });\n * });\n * ```\n */\nexport function MapProvider({ children, id }: MapProviderProps) {\n // Cleanup when component unmounts\n // State is created automatically on first subscription in useMapMode/useMapCursor\n useEffect(() => {\n return () => {\n clearMapModeState(id);\n clearCursorState(id);\n };\n }, [id]);\n\n return <MapContext.Provider value={id}>{children}</MapContext.Provider>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAwBA,MAAa,aAAa,cAA+B,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuH9D,SAAgB,YAAY,EAAE,UAAU,MAAwB;AAG9D,iBAAgB;AACd,eAAa;AACX,qBAAkB,GAAG;AACrB,oBAAiB,GAAG;;IAErB,CAAC,GAAG,CAAC;AAER,QAAO,oBAAC,WAAW;EAAS,OAAO;EAAK;GAA+B"}
|
package/dist/deckgl/index.js
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
import { MapEvents, MapEventsNamespace } from "./base-map/events.js";
|
|
15
14
|
import { DARK_BASE_MAP_STYLE, LIGHT_BASE_MAP_STYLE, PARAMETERS } from "./base-map/constants.js";
|
|
15
|
+
import { MapEvents, MapEventsNamespace } from "./base-map/events.js";
|
|
16
16
|
import { BaseMap } from "./base-map/index.js";
|
|
17
17
|
import { createSavedViewport } from "./saved-viewports/index.js";
|
|
18
18
|
import { DASH_ARRAYS, DEFAULT_COLORS, DEFAULT_STYLE_PROPERTIES, LINE_PATTERNS, LINE_WIDTHS, SHAPE_LAYER_IDS } from "./shapes/shared/constants.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/deckgl/saved-viewports/index.ts"],"sourcesContent":["/*\n * Copyright
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/deckgl/saved-viewports/index.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {\n type HotkeyOptions,\n type KeyCombination,\n type KeyCombinationId,\n Keycode,\n registerHotkey,\n} from '@accelint/hotkey-manager';\nimport { persist, retrieve, STORAGE_ID } from './storage';\nimport type { MapViewState } from '@deck.gl/core';\nimport type { RequireAllOrNone } from 'type-fest';\n\ntype BaseOptions = {\n uniqueIdentifier?: string;\n threshold?: number;\n getCurrentViewport: () => MapViewState;\n setCurrentViewport: (viewport: MapViewState) => void;\n};\n\ntype PersistOptions = RequireAllOrNone<{\n getSavedViewport: (\n id: KeyCombinationId,\n uniqueIdentifier?: string,\n ) => MapViewState;\n setSavedViewport: (\n id: KeyCombinationId,\n viewport: MapViewState,\n uniqueIdentifier?: string,\n ) => void;\n}>;\n\nexport type SavedViewportOptions = Partial<HotkeyOptions> &\n BaseOptions &\n PersistOptions;\n\nexport const createSavedViewport = (\n options: SavedViewportOptions,\n): ReturnType<typeof registerHotkey> => {\n const setFn = options.setSavedViewport ?? persist;\n const getFn = options.getSavedViewport ?? retrieve;\n\n return registerHotkey({\n id: STORAGE_ID,\n heldThresholdMs: options.threshold,\n key: options.key ?? [\n {\n code: Keycode.Digit0,\n },\n {\n code: Keycode.Digit1,\n },\n {\n code: Keycode.Digit2,\n },\n {\n code: Keycode.Digit3,\n },\n {\n code: Keycode.Digit4,\n },\n {\n code: Keycode.Digit5,\n },\n {\n code: Keycode.Digit6,\n },\n {\n code: Keycode.Digit7,\n },\n {\n code: Keycode.Digit8,\n },\n {\n code: Keycode.Digit9,\n },\n ],\n onKeyHeld: (e: KeyboardEvent, key: KeyCombination) => {\n e.preventDefault();\n const viewport = options.getCurrentViewport();\n setFn(key.id, viewport, options.uniqueIdentifier);\n },\n onKeyUp: (e: KeyboardEvent, key: KeyCombination) => {\n e.preventDefault();\n const viewport = getFn(key.id, options.uniqueIdentifier);\n if (viewport) {\n options.setCurrentViewport(viewport);\n }\n },\n });\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AA8CA,MAAa,uBACX,YACsC;CACtC,MAAM,QAAQ,QAAQ,oBAAoB;CAC1C,MAAM,QAAQ,QAAQ,oBAAoB;AAE1C,QAAO,eAAe;EACpB,IAAI;EACJ,iBAAiB,QAAQ;EACzB,KAAK,QAAQ,OAAO;GAClB,EACE,MAAM,QAAQ,QACf;GACD,EACE,MAAM,QAAQ,QACf;GACD,EACE,MAAM,QAAQ,QACf;GACD,EACE,MAAM,QAAQ,QACf;GACD,EACE,MAAM,QAAQ,QACf;GACD,EACE,MAAM,QAAQ,QACf;GACD,EACE,MAAM,QAAQ,QACf;GACD,EACE,MAAM,QAAQ,QACf;GACD,EACE,MAAM,QAAQ,QACf;GACD,EACE,MAAM,QAAQ,QACf;GACF;EACD,YAAY,GAAkB,QAAwB;AACpD,KAAE,gBAAgB;GAClB,MAAM,WAAW,QAAQ,oBAAoB;AAC7C,SAAM,IAAI,IAAI,UAAU,QAAQ,iBAAiB;;EAEnD,UAAU,GAAkB,QAAwB;AAClD,KAAE,gBAAgB;GAClB,MAAM,WAAW,MAAM,IAAI,IAAI,QAAQ,iBAAiB;AACxD,OAAI,SACF,SAAQ,mBAAmB,SAAS;;EAGzC,CAAC"}
|