@accelint/map-toolkit 0.3.1 → 0.4.1

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.
Files changed (104) hide show
  1. package/CHANGELOG.md +49 -24
  2. package/catalog-info.yaml +5 -7
  3. package/dist/cursor-coordinates/index.d.ts +14 -3
  4. package/dist/cursor-coordinates/index.js +16 -3
  5. package/dist/cursor-coordinates/use-cursor-coordinates.d.ts +20 -6
  6. package/dist/cursor-coordinates/use-cursor-coordinates.js +247 -128
  7. package/dist/cursor-coordinates/use-cursor-coordinates.js.map +1 -1
  8. package/dist/deckgl/base-map/constants.d.ts +14 -12
  9. package/dist/deckgl/base-map/constants.js +26 -12
  10. package/dist/deckgl/base-map/constants.js.map +1 -1
  11. package/dist/deckgl/base-map/events.d.ts +6 -4
  12. package/dist/deckgl/base-map/events.js +18 -4
  13. package/dist/deckgl/base-map/events.js.map +1 -1
  14. package/dist/deckgl/base-map/index.d.ts +45 -18
  15. package/dist/deckgl/base-map/index.js +216 -148
  16. package/dist/deckgl/base-map/index.js.map +1 -1
  17. package/dist/deckgl/base-map/provider.d.ts +48 -32
  18. package/dist/deckgl/base-map/provider.js +122 -11
  19. package/dist/deckgl/base-map/provider.js.map +1 -1
  20. package/dist/deckgl/base-map/types.d.ts +49 -39
  21. package/dist/deckgl/base-map/types.js +11 -2
  22. package/dist/deckgl/index.d.ts +18 -13
  23. package/dist/deckgl/index.js +19 -6
  24. package/dist/deckgl/symbol-layer/fiber.d.ts +21 -10
  25. package/dist/deckgl/symbol-layer/fiber.js +18 -3
  26. package/dist/deckgl/symbol-layer/fiber.js.map +1 -1
  27. package/dist/deckgl/symbol-layer/index.d.ts +68 -54
  28. package/dist/deckgl/symbol-layer/index.js +105 -85
  29. package/dist/deckgl/symbol-layer/index.js.map +1 -1
  30. package/dist/deckgl/text-layer/character-sets.d.ts +19 -17
  31. package/dist/deckgl/text-layer/character-sets.js +40 -19
  32. package/dist/deckgl/text-layer/character-sets.js.map +1 -1
  33. package/dist/deckgl/text-layer/default-settings.d.ts +16 -2
  34. package/dist/deckgl/text-layer/default-settings.js +42 -18
  35. package/dist/deckgl/text-layer/default-settings.js.map +1 -1
  36. package/dist/deckgl/text-layer/fiber.d.ts +38 -27
  37. package/dist/deckgl/text-layer/fiber.js +18 -3
  38. package/dist/deckgl/text-layer/fiber.js.map +1 -1
  39. package/dist/deckgl/text-layer/index.d.ts +39 -25
  40. package/dist/deckgl/text-layer/index.js +47 -29
  41. package/dist/deckgl/text-layer/index.js.map +1 -1
  42. package/dist/decorators/deckgl.d.ts +16 -2
  43. package/dist/decorators/deckgl.js +25 -7
  44. package/dist/decorators/deckgl.js.map +1 -1
  45. package/dist/map-cursor/events.d.ts +16 -0
  46. package/dist/map-cursor/events.js +27 -0
  47. package/dist/map-cursor/events.js.map +1 -0
  48. package/dist/map-cursor/index.d.ts +17 -0
  49. package/dist/map-cursor/index.js +18 -0
  50. package/dist/map-cursor/store.d.ts +93 -0
  51. package/dist/map-cursor/store.js +351 -0
  52. package/dist/map-cursor/store.js.map +1 -0
  53. package/dist/map-cursor/types.d.ts +81 -0
  54. package/dist/map-cursor/types.js +12 -0
  55. package/dist/map-cursor/use-map-cursor.d.ts +99 -0
  56. package/dist/map-cursor/use-map-cursor.js +116 -0
  57. package/dist/map-cursor/use-map-cursor.js.map +1 -0
  58. package/dist/map-mode/events.d.ts +11 -9
  59. package/dist/map-mode/events.js +43 -9
  60. package/dist/map-mode/events.js.map +1 -1
  61. package/dist/map-mode/index.d.ts +17 -6
  62. package/dist/map-mode/index.js +18 -5
  63. package/dist/map-mode/store.d.ts +26 -3
  64. package/dist/map-mode/store.js +329 -265
  65. package/dist/map-mode/store.js.map +1 -1
  66. package/dist/map-mode/types.d.ts +49 -35
  67. package/dist/map-mode/types.js +11 -2
  68. package/dist/map-mode/use-map-mode.d.ts +21 -7
  69. package/dist/map-mode/use-map-mode.js +66 -23
  70. package/dist/map-mode/use-map-mode.js.map +1 -1
  71. package/dist/maplibre/constants.d.ts +10 -8
  72. package/dist/maplibre/constants.js +22 -8
  73. package/dist/maplibre/constants.js.map +1 -1
  74. package/dist/maplibre/hooks/use-maplibre.d.ts +17 -2
  75. package/dist/maplibre/hooks/use-maplibre.js +77 -31
  76. package/dist/maplibre/hooks/use-maplibre.js.map +1 -1
  77. package/dist/maplibre/index.d.ts +15 -3
  78. package/dist/maplibre/index.js +17 -4
  79. package/dist/viewport/constants.d.ts +8 -6
  80. package/dist/viewport/constants.js +20 -6
  81. package/dist/viewport/constants.js.map +1 -1
  82. package/dist/viewport/index.d.ts +18 -13
  83. package/dist/viewport/index.js +19 -6
  84. package/dist/viewport/types.d.ts +27 -17
  85. package/dist/viewport/types.js +11 -2
  86. package/dist/viewport/use-viewport-state.d.ts +29 -14
  87. package/dist/viewport/use-viewport-state.js +200 -87
  88. package/dist/viewport/use-viewport-state.js.map +1 -1
  89. package/dist/viewport/utils.d.ts +25 -10
  90. package/dist/viewport/utils.js +67 -37
  91. package/dist/viewport/utils.js.map +1 -1
  92. package/dist/viewport/viewport-size.d.ts +27 -15
  93. package/dist/viewport/viewport-size.js +54 -11
  94. package/dist/viewport/viewport-size.js.map +1 -1
  95. package/package.json +107 -78
  96. package/dist/cursor-coordinates/index.js.map +0 -1
  97. package/dist/deckgl/base-map/types.js.map +0 -1
  98. package/dist/deckgl/index.js.map +0 -1
  99. package/dist/map-mode/index.js.map +0 -1
  100. package/dist/map-mode/types.js.map +0 -1
  101. package/dist/maplibre/index.js.map +0 -1
  102. package/dist/metafile-esm.json +0 -1
  103. package/dist/viewport/index.js.map +0 -1
  104. package/dist/viewport/types.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @accelint/map-toolkit
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - bb73a1e: Ensure dependencies all follow the same semver range across devtk, maptk, and designtk.
8
+ - Updated dependencies [bb73a1e]
9
+ - @accelint/core@0.5.2
10
+ - @accelint/bus@3.0.2
11
+ - @accelint/geo@0.4.2
12
+
13
+ ## 0.4.0
14
+
15
+ ### Minor Changes
16
+
17
+ - 8802d14: Add map-cursor controller to map-toolkit.
18
+
19
+ ### Patch Changes
20
+
21
+ - 34c42a0: Swap bundling to tsdown and auto generate exports entries in package.json.
22
+ - Updated dependencies [34c42a0]
23
+ - @accelint/core@0.5.1
24
+ - @accelint/bus@3.0.1
25
+ - @accelint/geo@0.4.1
26
+
3
27
  ## 0.3.1
4
28
 
5
29
  ### Patch Changes
@@ -8,62 +32,62 @@
8
32
  - @accelint/geo@0.4.0
9
33
 
10
34
  ## 0.3.0
35
+
11
36
  ### Minor Changes
12
37
 
13
38
  - 7131cc0: Add `useCursorCoordinates`, a hook to retrieve the current coordinates for the mouse hovered position
14
39
  - 874edd5: ## Breaking Change: Structured Clone Constraint
15
-
40
+
16
41
  The event bus payload is now constrained to values that are serializable by the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). This constraint aligns the TypeScript types with the actual runtime behavior of the `BroadcastChannel` API.
17
-
42
+
18
43
  ### What this means
19
-
44
+
20
45
  You can no longer pass the following types in event payloads:
21
-
22
46
  - Functions
23
47
  - Symbols
24
48
  - DOM nodes
25
49
  - Prototype chains (class instances lose their methods)
26
50
  - Properties with `undefined` values (they're omitted)
27
-
51
+
28
52
  ### How to migrate
29
-
53
+
30
54
  **❌ Before:**
31
-
55
+
32
56
  ```typescript
33
- bus.emit('user-action', {
34
- callback: () => console.log('done'), // ❌ Function
57
+ bus.emit("user-action", {
58
+ callback: () => console.log("done"), // ❌ Function
35
59
  userData: userClass, // ❌ Class instance with methods
36
- element: document.getElementById('foo'), // ❌ DOM node
60
+ element: document.getElementById("foo"), // ❌ DOM node
37
61
  });
38
62
  ```
39
-
63
+
40
64
  **✅ After:**
41
-
65
+
42
66
  ```typescript
43
67
  // Option 1: Send only data, handle logic separately
44
- bus.emit('user-action', {
45
- actionType: 'complete', // ✅ Primitive
68
+ bus.emit("user-action", {
69
+ actionType: "complete", // ✅ Primitive
46
70
  userData: { id: userClass.id, name: userClass.name }, // ✅ Plain object
47
- elementId: 'foo', // ✅ Reference by ID
71
+ elementId: "foo", // ✅ Reference by ID
48
72
  });
49
-
73
+
50
74
  // Option 2: Use event types to trigger behavior
51
- bus.on('user-action-complete', () => {
52
- console.log('done'); // Handle callback logic in listener
75
+ bus.on("user-action-complete", () => {
76
+ console.log("done"); // Handle callback logic in listener
53
77
  });
54
78
  ```
55
-
79
+
56
80
  ### Supported types
57
-
58
81
  - Primitives (string, number, boolean, null, BigInt)
59
82
  - Plain objects and arrays
60
83
  - Date, RegExp, Map, Set
61
84
  - Typed arrays (Uint8Array, etc.)
62
85
  - ArrayBuffer, Blob, File
63
-
86
+
64
87
  ### Finding issues
65
-
88
+
66
89
  TypeScript will now catch most violations at compile time. Runtime errors from `BroadcastChannel.postMessage()` indicate non-serializable values that slipped through.
90
+
67
91
  - e8535c4: Adds ViewportSize component that shows the width and height of the viewport and updates with changes.
68
92
  Adds useViewportState for more fine-grained control of viewport data (lat/lon/zoom/bounds). Exports `useEffectEvent` ponyfill from `@accelint/bus/react`.
69
93
 
@@ -73,10 +97,11 @@
73
97
  - 99f6cd5: Add map-mode/store getCurrentModeOwner method to access the current map mode owner.
74
98
  - 80db585: BREAKING CHANGES:
75
99
  Design Toolkit no longer exports `tv`, this is now available from Design Foundation
76
-
100
+
77
101
  Created new package @accelint/design-foundation that houses all of the Tailwind tokens, variants, base styles and utilities for easier reuse without having a dependency on the larger Design Toolkit
78
-
102
+
79
103
  Updated Map Toolkit Storybook and NextJS demo app styles to import the new Design Foundation
104
+
80
105
  - f157e42: update picking info to remove more unserializable properties in map-toolkit
81
106
  - 7539bbb: update vite config in map-toolkit to re-enable base-map render in storybook preview
82
107
  - 86a95cf: refactor map mode to use module useSyncExternalStore pattern instead of class.
package/catalog-info.yaml CHANGED
@@ -11,15 +11,14 @@ metadata:
11
11
 
12
12
  Dependencies:
13
13
 
14
- accelint_biome-config@1.0.2, accelint_bus@3.0.0,
15
- accelint_constellation-tracker@1.0.1, accelint_core@0.5.0,
16
- accelint_design-foundation@1.0.0, accelint_design-toolkit@8.1.0,
17
- accelint_geo@0.4.0, accelint_typescript-config@0.1.4,
18
- accelint_vitest-config@0.1.5
14
+ accelint_biome-config@1.0.2, accelint_bus@3.0.2, accelint_core@0.5.2,
15
+ accelint_design-foundation@2.0.0, accelint_design-toolkit@9.0.0,
16
+ accelint_geo@0.4.1, accelint_typescript-config@0.1.4,
17
+ accelint_vitest-config@0.1.6
19
18
  annotations:
20
19
  backstage.io/edit-url: https://github.com/gohypergiant/standard-toolkit/blob/main/packages/map-toolkit/catalog-info.yaml
21
20
  backstage.io/techdocs-ref: dir:.
22
- package/version: 0.3.1
21
+ package/version: 0.4.1
23
22
  github.com/project-slug: gohypergiant/standard-toolkit
24
23
  links:
25
24
  - url: https://github.com/gohypergiant/standard-toolkit/tree/main/packages/map-toolkit
@@ -38,7 +37,6 @@ spec:
38
37
  dependsOn:
39
38
  - component:accelint_biome-config
40
39
  - component:accelint_bus
41
- - component:accelint_constellation-tracker
42
40
  - component:accelint_core
43
41
  - component:accelint_design-foundation
44
42
  - component:accelint_design-toolkit
@@ -1,3 +1,14 @@
1
- export { CoordinateFormatTypes, clearCursorCoordinateState, useCursorCoordinates } from './use-cursor-coordinates.js';
2
- import '@accelint/geo';
3
- import '@accelint/core';
1
+ /*
2
+ * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at https://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { CoordinateFormatTypes, clearCursorCoordinateState, useCursorCoordinates } from "./use-cursor-coordinates.js";
14
+ export { type CoordinateFormatTypes, clearCursorCoordinateState, useCursorCoordinates };
@@ -1,3 +1,16 @@
1
- export { clearCursorCoordinateState, useCursorCoordinates } from './use-cursor-coordinates.js';
2
- //# sourceMappingURL=index.js.map
3
- //# sourceMappingURL=index.js.map
1
+ /*
2
+ * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at https://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+
14
+ import { clearCursorCoordinateState, useCursorCoordinates } from "./use-cursor-coordinates.js";
15
+
16
+ export { clearCursorCoordinateState, useCursorCoordinates };
@@ -1,6 +1,19 @@
1
- import { coordinateSystems } from '@accelint/geo';
2
- import { UniqueId } from '@accelint/core';
1
+ /*
2
+ * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at https://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
3
12
 
13
+ import { coordinateSystems } from "@accelint/geo";
14
+ import { UniqueId } from "@accelint/core";
15
+
16
+ //#region src/cursor-coordinates/use-cursor-coordinates.d.ts
4
17
  /**
5
18
  * Supported coordinate format types for displaying map coordinates.
6
19
  *
@@ -69,8 +82,9 @@ declare function clearCursorCoordinateState(instanceId: UniqueId): void;
69
82
  * ```
70
83
  */
71
84
  declare function useCursorCoordinates(id?: UniqueId): {
72
- formattedCoord: string;
73
- setFormat: (format: CoordinateFormatTypes) => void;
85
+ formattedCoord: string;
86
+ setFormat: (format: CoordinateFormatTypes) => void;
74
87
  };
75
-
76
- export { type CoordinateFormatTypes, clearCursorCoordinateState, useCursorCoordinates };
88
+ //#endregion
89
+ export { CoordinateFormatTypes, clearCursorCoordinateState, useCursorCoordinates };
90
+ //# sourceMappingURL=use-cursor-coordinates.d.ts.map
@@ -1,161 +1,280 @@
1
- import 'client-only';
2
- import { Broadcast } from '@accelint/bus';
3
- import { createCoordinate, coordinateSystems } from '@accelint/geo';
4
- import { useContext, useSyncExternalStore, useMemo } from 'react';
5
- import { MapEvents } from '../deckgl/base-map/events.js';
6
- import { MapContext } from '../deckgl/base-map/provider.js';
1
+ /*
2
+ * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at https://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
7
12
 
13
+
14
+ 'use client';
15
+
16
+ import { MapEvents } from "../deckgl/base-map/events.js";
17
+ import { MapContext } from "../deckgl/base-map/provider.js";
18
+ import { Broadcast } from "@accelint/bus";
19
+ import { coordinateSystems, createCoordinate } from "@accelint/geo";
20
+ import { useContext, useMemo, useSyncExternalStore } from "react";
21
+
22
+ //#region src/cursor-coordinates/use-cursor-coordinates.ts
8
23
  const bus = Broadcast.getInstance();
9
24
  const create = createCoordinate(coordinateSystems.dd, "LONLAT");
10
25
  const MAX_LONGITUDE = 180;
11
26
  const LONGITUDE_RANGE = 360;
12
27
  const COORDINATE_PRECISION = 8;
13
28
  const DEFAULT_COORDINATE = "--, --";
29
+ /**
30
+ * Prepares coordinates for display by normalizing longitude and formatting with cardinal directions.
31
+ * Normalizes longitude to -180 to 180 range and formats both longitude and latitude with
32
+ * compass directions (E/W for longitude, N/S for latitude).
33
+ *
34
+ * @param coord - Tuple of [longitude, latitude] coordinates
35
+ * @returns Formatted string in the format "LON.NNNNNNNN E/W / LAT.NNNNNNNN N/S"
36
+ */
14
37
  const prepareCoord = (coord) => {
15
- let lon = coord[0];
16
- lon = ((lon + MAX_LONGITUDE) % LONGITUDE_RANGE + LONGITUDE_RANGE) % LONGITUDE_RANGE - MAX_LONGITUDE;
17
- const lat = coord[1];
18
- const lonStr = `${Math.abs(lon).toFixed(COORDINATE_PRECISION)} ${lon < 0 ? "W" : "E"}`;
19
- const latStr = `${Math.abs(lat).toFixed(COORDINATE_PRECISION)} ${lat < 0 ? "S" : "N"}`;
20
- return `${lonStr} / ${latStr}`;
38
+ let lon = coord[0];
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"}`}`;
21
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
+ */
22
50
  function isValidCoordinate(value) {
23
- return Array.isArray(value) && value.length === 2 && value.every(Number.isFinite);
51
+ return Array.isArray(value) && value.length === 2 && value.every(Number.isFinite);
24
52
  }
53
+ /**
54
+ * Store for cursor coordinate state keyed by instanceId
55
+ */
25
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
+ */
26
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
+ */
27
67
  const busUnsubscribers = /* @__PURE__ */ new Map();
68
+ /**
69
+ * Cache of subscription functions per instanceId to avoid recreating on every render
70
+ */
28
71
  const subscriptionCache = /* @__PURE__ */ new Map();
72
+ /**
73
+ * Cache of snapshot functions per instanceId to maintain referential stability
74
+ */
29
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.
85
+ *
86
+ * @param instanceId - The unique identifier for the map
87
+ */
30
88
  function ensureBusListener(instanceId) {
31
- if (busUnsubscribers.has(instanceId)) {
32
- return;
33
- }
34
- const unsub = bus.on(MapEvents.hover, (data) => {
35
- const eventId = data.payload.id;
36
- if (instanceId !== eventId) {
37
- return;
38
- }
39
- const coords = data.payload.info.coordinate;
40
- const state = coordinateStore.get(instanceId);
41
- if (state) {
42
- if (isValidCoordinate(coords)) {
43
- state.coordinate = coords;
44
- } else {
45
- state.coordinate = null;
46
- }
47
- const subscribers = componentSubscribers.get(instanceId);
48
- if (subscribers) {
49
- for (const onStoreChange of subscribers) {
50
- onStoreChange();
51
- }
52
- }
53
- }
54
- });
55
- busUnsubscribers.set(instanceId, unsub);
89
+ if (busUnsubscribers.has(instanceId)) return;
90
+ const unsub = bus.on(MapEvents.hover, (data) => {
91
+ if (instanceId !== data.payload.id) return;
92
+ const coords = data.payload.info.coordinate;
93
+ const state = coordinateStore.get(instanceId);
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);
56
102
  }
103
+ /**
104
+ * Cleans up the bus listener if no React subscribers remain.
105
+ *
106
+ * @param instanceId - The unique identifier for the map
107
+ */
57
108
  function cleanupBusListenerIfNeeded(instanceId) {
58
- const subscribers = componentSubscribers.get(instanceId);
59
- if (!subscribers || subscribers.size === 0) {
60
- const unsub = busUnsubscribers.get(instanceId);
61
- if (unsub) {
62
- unsub();
63
- busUnsubscribers.delete(instanceId);
64
- }
65
- coordinateStore.delete(instanceId);
66
- componentSubscribers.delete(instanceId);
67
- subscriptionCache.delete(instanceId);
68
- snapshotCache.delete(instanceId);
69
- }
109
+ const subscribers = componentSubscribers.get(instanceId);
110
+ if (!subscribers || subscribers.size === 0) {
111
+ const unsub = busUnsubscribers.get(instanceId);
112
+ if (unsub) {
113
+ unsub();
114
+ busUnsubscribers.delete(instanceId);
115
+ }
116
+ coordinateStore.delete(instanceId);
117
+ componentSubscribers.delete(instanceId);
118
+ subscriptionCache.delete(instanceId);
119
+ snapshotCache.delete(instanceId);
120
+ serverSnapshotCache.delete(instanceId);
121
+ }
70
122
  }
123
+ /**
124
+ * Creates or retrieves a cached subscription function for a given instanceId.
125
+ * Uses a fan-out pattern: 1 bus listener -> N React subscribers.
126
+ * Automatically cleans up coordinate state when the last subscriber unsubscribes.
127
+ *
128
+ * @param instanceId - The unique identifier for the map
129
+ * @returns A subscription function for useSyncExternalStore
130
+ */
71
131
  function getOrCreateSubscription(instanceId) {
72
- const subscription = subscriptionCache.get(instanceId) ?? ((onStoreChange) => {
73
- ensureBusListener(instanceId);
74
- let subscriberSet = componentSubscribers.get(instanceId);
75
- if (!subscriberSet) {
76
- subscriberSet = /* @__PURE__ */ new Set();
77
- componentSubscribers.set(instanceId, subscriberSet);
78
- }
79
- subscriberSet.add(onStoreChange);
80
- return () => {
81
- const currentSubscriberSet = componentSubscribers.get(instanceId);
82
- if (currentSubscriberSet) {
83
- currentSubscriberSet.delete(onStoreChange);
84
- }
85
- cleanupBusListenerIfNeeded(instanceId);
86
- };
87
- });
88
- subscriptionCache.set(instanceId, subscription);
89
- return subscription;
132
+ const subscription = subscriptionCache.get(instanceId) ?? ((onStoreChange) => {
133
+ ensureBusListener(instanceId);
134
+ let subscriberSet = componentSubscribers.get(instanceId);
135
+ if (!subscriberSet) {
136
+ subscriberSet = /* @__PURE__ */ new Set();
137
+ componentSubscribers.set(instanceId, subscriberSet);
138
+ }
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;
90
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
+ */
91
156
  function getOrCreateSnapshot(instanceId) {
92
- let cached = snapshotCache.get(instanceId);
93
- if (!cached) {
94
- cached = () => {
95
- const state = coordinateStore.get(instanceId);
96
- if (!state) {
97
- return DEFAULT_COORDINATE;
98
- }
99
- if (!state.coordinate) {
100
- return DEFAULT_COORDINATE;
101
- }
102
- const coord = create(prepareCoord(state.coordinate));
103
- return coord[state.format]();
104
- };
105
- snapshotCache.set(instanceId, cached);
106
- }
107
- return cached;
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
+ }
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;
108
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
+ */
109
188
  function setFormatForInstance(instanceId, format) {
110
- const state = coordinateStore.get(instanceId);
111
- if (state) {
112
- state.format = format;
113
- const subscribers = componentSubscribers.get(instanceId);
114
- if (subscribers) {
115
- for (const onStoreChange of subscribers) {
116
- onStoreChange();
117
- }
118
- }
119
- }
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
+ }
120
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
+ */
121
209
  function clearCursorCoordinateState(instanceId) {
122
- const unsub = busUnsubscribers.get(instanceId);
123
- if (unsub) {
124
- unsub();
125
- busUnsubscribers.delete(instanceId);
126
- }
127
- coordinateStore.delete(instanceId);
128
- componentSubscribers.delete(instanceId);
129
- subscriptionCache.delete(instanceId);
130
- snapshotCache.delete(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);
131
220
  }
221
+ /**
222
+ * React hook that tracks and formats the cursor hover position coordinates on a map.
223
+ *
224
+ * Subscribes to map hover events via the event bus and converts coordinates to various
225
+ * geographic formats (Decimal Degrees, DMS, MGRS, UTM, etc.). The hook automatically
226
+ * filters events to only process those from the specified map instance.
227
+ *
228
+ * Uses `useSyncExternalStore` for concurrent-safe updates and efficient fan-out pattern
229
+ * where multiple components can subscribe to the same map's coordinates with a single
230
+ * bus listener.
231
+ *
232
+ * @param id - Optional map instance ID. If not provided, attempts to use the ID from MapProvider context.
233
+ * @returns Object containing the formatted coordinate string and format setter function
234
+ * @property formattedCoord - The formatted coordinate string (defaults to "--, --" when no position)
235
+ * @property setFormat - Function to change the coordinate format system
236
+ * @throws {Error} When no id is provided and hook is used outside MapProvider context
237
+ *
238
+ * @example
239
+ * ```tsx
240
+ * import { uuid } from '@accelint/core';
241
+ * import { useCursorCoordinates } from '@accelint/map-toolkit/cursor-coordinates';
242
+ *
243
+ * const MAP_ID = uuid();
244
+ *
245
+ * function CoordinateDisplay() {
246
+ * const { formattedCoord, setFormat } = useCursorCoordinates(MAP_ID);
247
+ *
248
+ * return (
249
+ * <div>
250
+ * <select onChange={(e) => setFormat(e.target.value as CoordinateFormatTypes)}>
251
+ * <option value="dd">Decimal Degrees</option>
252
+ * <option value="ddm">Degrees Decimal Minutes</option>
253
+ * <option value="dms">Degrees Minutes Seconds</option>
254
+ * <option value="mgrs">MGRS</option>
255
+ * <option value="utm">UTM</option>
256
+ * </select>
257
+ * <div>{formattedCoord}</div>
258
+ * </div>
259
+ * );
260
+ * }
261
+ * ```
262
+ */
132
263
  function useCursorCoordinates(id) {
133
- const contextId = useContext(MapContext);
134
- const actualId = id ?? contextId;
135
- if (!actualId) {
136
- throw new Error(
137
- "useCursorCoordinates requires either an id parameter or to be used within a MapProvider"
138
- );
139
- }
140
- if (!coordinateStore.has(actualId)) {
141
- coordinateStore.set(actualId, {
142
- coordinate: null,
143
- format: "dd"
144
- });
145
- }
146
- const formattedCoord = useSyncExternalStore(
147
- getOrCreateSubscription(actualId),
148
- getOrCreateSnapshot(actualId)
149
- );
150
- return useMemo(
151
- () => ({
152
- formattedCoord,
153
- setFormat: (format) => setFormatForInstance(actualId, format)
154
- }),
155
- [formattedCoord, actualId]
156
- );
264
+ const contextId = useContext(MapContext);
265
+ const actualId = id ?? contextId;
266
+ if (!actualId) throw new Error("useCursorCoordinates requires either an id parameter or to be used within a MapProvider");
267
+ if (!coordinateStore.has(actualId)) coordinateStore.set(actualId, {
268
+ coordinate: null,
269
+ format: "dd"
270
+ });
271
+ const formattedCoord = useSyncExternalStore(getOrCreateSubscription(actualId), getOrCreateSnapshot(actualId), getOrCreateServerSnapshot(actualId));
272
+ return useMemo(() => ({
273
+ formattedCoord,
274
+ setFormat: (format) => setFormatForInstance(actualId, format)
275
+ }), [formattedCoord, actualId]);
157
276
  }
158
277
 
278
+ //#endregion
159
279
  export { clearCursorCoordinateState, useCursorCoordinates };
160
- //# sourceMappingURL=use-cursor-coordinates.js.map
161
280
  //# sourceMappingURL=use-cursor-coordinates.js.map