@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","names":["DEFAULT_CURSOR: CSSCursorType","getModeOwnerFn: ((mapId: UniqueId) => string | undefined) | null","isRegisteredModeOwnerFn:\n | ((mapId: UniqueId, owner: string) => boolean)\n | null","importPromise: Promise<void> | null","updates: Partial<CursorState>","newState: CursorState"],"sources":["../../src/map-cursor/store.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\n/**\n * Map Cursor Store\n *\n * Manages cursor state with ownership-based priority.\n *\n * Priority order:\n * 1. Mode owner's cursor (if mode is owned)\n * 2. Most recent cursor request\n * 3. Default cursor\n *\n * @example\n * ```tsx\n * import { cursorStore } from '@accelint/map-toolkit/map-cursor';\n *\n * function MapContainer({ mapId }) {\n * const cursor = cursorStore.useSelector(mapId, (s) => getEffectiveCursor(mapId, s));\n * return <div style={{ cursor }}>...</div>;\n * }\n *\n * // Request cursor change from a layer:\n * cursorStore.actions(mapId).requestCursorChange('crosshair', 'draw-layer');\n * ```\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { MapModeEvents } from '../map-mode/events';\nimport { createMapStore, mapDelete, mapSet } from '../shared/create-map-store';\nimport { MapCursorEvents } from './events';\nimport type { UniqueId } from '@accelint/core';\nimport type { ModeChangedEvent } from '../map-mode/types';\nimport type { CSSCursorType, MapCursorEventType } from './types';\n\nconst DEFAULT_CURSOR: CSSCursorType = 'default';\n\n/**\n * State shape for map cursor\n */\ntype CursorState = {\n /** Map of owner -> cursor */\n cursorOwners: Map<string, CSSCursorType>;\n /** Current active cursor (for priority tracking) */\n currentCursor: CSSCursorType | null;\n /** Current cursor owner (for priority tracking) */\n currentOwner: string | null;\n};\n\n/**\n * Actions for cursor management\n */\ntype CursorActions = {\n /** Request a cursor change */\n requestCursorChange: (cursor: CSSCursorType, owner: string) => void;\n /** Clear cursor for an owner */\n clearCursor: (owner: string) => void;\n};\n\nconst cursorBus = Broadcast.getInstance<MapCursorEventType>();\nconst modeBus = Broadcast.getInstance<ModeChangedEvent>();\n\n/**\n * Lazy import to avoid circular dependency between cursor and mode stores.\n * The mode store doesn't depend on cursor store, but importing it synchronously\n * at module load time can cause initialization order issues.\n *\n * This pattern ensures the import is resolved before first use (bus listeners\n * are set up on first subscriber, not at module load time).\n */\nlet getModeOwnerFn: ((mapId: UniqueId) => string | undefined) | null = null;\nlet isRegisteredModeOwnerFn:\n | ((mapId: UniqueId, owner: string) => boolean)\n | null = null;\nlet importPromise: Promise<void> | null = null;\nlet importFailed = false;\n\nfunction ensureModeStoreImported(): void {\n if (getModeOwnerFn !== null || importFailed) {\n return;\n }\n if (importPromise === null) {\n importPromise = import('../map-mode/store')\n .then((mod) => {\n getModeOwnerFn = mod.getCurrentModeOwner;\n isRegisteredModeOwnerFn = mod.isRegisteredModeOwner;\n })\n .catch((error) => {\n importFailed = true;\n // Log error in development only - in production this is a silent fallback\n if (process.env.NODE_ENV !== 'production') {\n console.error('[MapCursor] Failed to import mode store:', error);\n }\n });\n }\n}\n\n// Start the import immediately so it's likely resolved by first use\nensureModeStoreImported();\n\nfunction getModeOwner(mapId: UniqueId): string | undefined {\n // getModeOwnerFn will be available by the time bus listeners are set up\n // (which happens on first React subscriber, not at module load)\n return getModeOwnerFn?.(mapId);\n}\n\nfunction isRegisteredOwner(mapId: UniqueId, owner: string): boolean {\n return isRegisteredModeOwnerFn?.(mapId, owner) ?? false;\n}\n\n/**\n * Calculate effective cursor based on priority\n */\nfunction getEffectiveCursor(\n mapId: UniqueId,\n state: CursorState,\n): CSSCursorType {\n // Priority 1: Mode owner's cursor\n const modeOwner = getModeOwner?.(mapId);\n if (modeOwner) {\n const modeOwnerCursor = state.cursorOwners.get(modeOwner);\n if (modeOwnerCursor) {\n return modeOwnerCursor;\n }\n }\n\n // Priority 2: Current cursor (if owner still has entry)\n if (state.currentCursor && state.currentOwner) {\n if (state.cursorOwners.has(state.currentOwner)) {\n return state.currentCursor;\n }\n }\n\n // Priority 3: Default\n return DEFAULT_CURSOR;\n}\n\n/**\n * Cursor store\n */\nexport const cursorStore = createMapStore<CursorState, CursorActions>({\n defaultState: {\n cursorOwners: new Map(),\n currentCursor: null,\n currentOwner: null,\n },\n\n actions: (mapId, { get, set }) => ({\n requestCursorChange: (cursor: CSSCursorType, owner: string) => {\n const trimmedCursor = cursor.trim() as CSSCursorType;\n const trimmedOwner = owner.trim();\n\n if (!trimmedCursor) {\n throw new Error('requestCursorChange requires non-empty cursor');\n }\n if (!trimmedOwner) {\n throw new Error('requestCursorChange requires non-empty owner');\n }\n\n cursorBus.emit(MapCursorEvents.changeRequest, {\n cursor: trimmedCursor,\n owner: trimmedOwner,\n id: mapId,\n });\n },\n\n clearCursor: (owner: string) => {\n const state = get();\n const hadCursor = state.cursorOwners.has(owner);\n\n if (hadCursor) {\n // Clear current tracking if this was the owner\n const updates: Partial<CursorState> = {\n cursorOwners: mapDelete(state.cursorOwners, owner),\n };\n if (state.currentOwner === owner) {\n updates.currentCursor = null;\n updates.currentOwner = null;\n }\n\n set(updates);\n }\n },\n }),\n\n bus: (mapId, { get, set }) => {\n // Handle cursor change requests\n const unsubRequest = cursorBus.on(\n MapCursorEvents.changeRequest,\n (event) => {\n const { cursor, owner: requestOwner, id } = event.payload;\n if (id !== mapId) {\n return;\n }\n\n const state = get();\n const previousCursor = getEffectiveCursor(mapId, state);\n\n // Skip if same cursor already stored for this owner\n if (state.cursorOwners.get(requestOwner) === cursor) {\n return;\n }\n\n // Create new Map for immutable update\n const newCursorOwners = mapSet(\n state.cursorOwners,\n requestOwner,\n cursor,\n );\n\n // Check ownership\n const currentModeOwner = getModeOwner?.(mapId);\n const isOwnerless = !currentModeOwner;\n const isCurrentModeOwner = requestOwner === currentModeOwner;\n const isAnyModeOwner = isRegisteredOwner(mapId, requestOwner);\n\n if (isOwnerless || isCurrentModeOwner) {\n // Accept: apply cursor with immutable update\n const newState: CursorState = {\n cursorOwners: newCursorOwners,\n currentCursor: cursor,\n currentOwner: requestOwner,\n };\n\n // Calculate new cursor with updated state\n const newCursor = getEffectiveCursor(mapId, newState);\n\n if (previousCursor !== newCursor) {\n set(newState);\n cursorBus.emit(MapCursorEvents.changed, {\n previousCursor,\n currentCursor: newCursor,\n owner: requestOwner,\n id: mapId,\n });\n } else {\n // Still need to update state even if cursor didn't change visually\n set(newState);\n }\n } else if (isAnyModeOwner) {\n // Store but don't apply: requester owns a different mode (pending or not current).\n // When their mode becomes active, getEffectiveCursor will find their cursor.\n set({ cursorOwners: newCursorOwners });\n\n cursorBus.emit(MapCursorEvents.rejected, {\n rejectedCursor: cursor,\n rejectedOwner: requestOwner,\n currentOwner: state.currentOwner || currentModeOwner || 'unknown',\n reason: 'not-current-owner',\n id: mapId,\n });\n } else {\n // Reject: don't store. Non-owners should only set cursor in default mode.\n cursorBus.emit(MapCursorEvents.rejected, {\n rejectedCursor: cursor,\n rejectedOwner: requestOwner,\n currentOwner: state.currentOwner || currentModeOwner || 'unknown',\n reason: 'not-owner',\n id: mapId,\n });\n }\n },\n );\n\n // Handle mode changes\n const unsubMode = modeBus.on(MapModeEvents.changed, (event) => {\n if (event.payload.id !== mapId) {\n return;\n }\n\n const state = get();\n const previousCursor = getEffectiveCursor(mapId, state);\n\n // Clear current tracking on mode change with immutable update\n if (\n event.payload.currentMode === 'default' ||\n event.payload.previousMode !== event.payload.currentMode\n ) {\n set({\n currentCursor: null,\n currentOwner: null,\n });\n }\n\n // Defer check until new mode owner is registered\n queueMicrotask(() => {\n // Re-get state after microtask since it may have changed\n const currentState = get();\n const newCursor = getEffectiveCursor(mapId, currentState);\n if (previousCursor !== newCursor) {\n const newModeOwner = getModeOwner?.(mapId) || 'system';\n cursorBus.emit(MapCursorEvents.changed, {\n previousCursor,\n currentCursor: newCursor,\n owner: newModeOwner,\n id: mapId,\n });\n }\n });\n });\n\n return () => {\n unsubRequest();\n unsubMode();\n };\n },\n});\n\n// =============================================================================\n// Convenience exports\n// =============================================================================\n\n/**\n * Get effective cursor (computed from state)\n */\nexport function getCursor(mapId: UniqueId): CSSCursorType {\n return getEffectiveCursor(mapId, cursorStore.get(mapId));\n}\n\n/**\n * Hook for effective cursor value.\n *\n * **Internal use only** - not exported from the public API.\n * Use `useMapCursor` instead, which provides:\n * - MapContext integration (auto-resolves mapId inside MapProvider)\n * - Actions (requestCursorChange, clearCursor)\n * - Better ergonomics for consumers\n *\n * This hook exists for internal composition (used by useMapCursor).\n */\nexport function useCursor(mapId: UniqueId): CSSCursorType {\n return cursorStore.useSelector(mapId, (state) =>\n getEffectiveCursor(mapId, state),\n );\n}\n\n/**\n * Clear cursor state\n */\nexport function clearCursorState(mapId: UniqueId): void {\n cursorStore.clear(mapId);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"store.js","names":["DEFAULT_CURSOR: CSSCursorType","getModeOwnerFn: ((mapId: UniqueId) => string | undefined) | null","isRegisteredModeOwnerFn:\n | ((mapId: UniqueId, owner: string) => boolean)\n | null","importPromise: Promise<void> | null","updates: Partial<CursorState>","newState: CursorState"],"sources":["../../src/map-cursor/store.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\n/**\n * Map Cursor Store\n *\n * Manages cursor state with ownership-based priority.\n *\n * Priority order:\n * 1. Mode owner's cursor (if mode is owned)\n * 2. Most recent cursor request\n * 3. Default cursor\n *\n * @example\n * ```tsx\n * import { cursorStore } from '@accelint/map-toolkit/map-cursor';\n *\n * function MapContainer({ mapId }) {\n * const cursor = cursorStore.useSelector(mapId, (s) => getEffectiveCursor(mapId, s));\n * return <div style={{ cursor }}>...</div>;\n * }\n *\n * // Request cursor change from a layer:\n * cursorStore.actions(mapId).requestCursorChange('crosshair', 'draw-layer');\n * ```\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { MapModeEvents } from '../map-mode/events';\nimport { createMapStore, mapDelete, mapSet } from '../shared/create-map-store';\nimport { MapCursorEvents } from './events';\nimport type { UniqueId } from '@accelint/core';\nimport type { ModeChangedEvent } from '../map-mode/types';\nimport type { CSSCursorType, MapCursorEventType } from './types';\n\nconst DEFAULT_CURSOR: CSSCursorType = 'default';\n\n/**\n * State shape for map cursor\n */\ntype CursorState = {\n /** Map of owner -> cursor */\n cursorOwners: Map<string, CSSCursorType>;\n /** Current active cursor (for priority tracking) */\n currentCursor: CSSCursorType | null;\n /** Current cursor owner (for priority tracking) */\n currentOwner: string | null;\n};\n\n/**\n * Actions for cursor management\n */\ntype CursorActions = {\n /** Request a cursor change */\n requestCursorChange: (cursor: CSSCursorType, owner: string) => void;\n /** Clear cursor for an owner */\n clearCursor: (owner: string) => void;\n};\n\nconst cursorBus = Broadcast.getInstance<MapCursorEventType>();\nconst modeBus = Broadcast.getInstance<ModeChangedEvent>();\n\n/**\n * Lazy import to avoid circular dependency between cursor and mode stores.\n * The mode store doesn't depend on cursor store, but importing it synchronously\n * at module load time can cause initialization order issues.\n *\n * This pattern ensures the import is resolved before first use (bus listeners\n * are set up on first subscriber, not at module load time).\n */\nlet getModeOwnerFn: ((mapId: UniqueId) => string | undefined) | null = null;\nlet isRegisteredModeOwnerFn:\n | ((mapId: UniqueId, owner: string) => boolean)\n | null = null;\nlet importPromise: Promise<void> | null = null;\nlet importFailed = false;\n\nfunction ensureModeStoreImported(): void {\n if (getModeOwnerFn !== null || importFailed) {\n return;\n }\n if (importPromise === null) {\n importPromise = import('../map-mode/store')\n .then((mod) => {\n getModeOwnerFn = mod.getCurrentModeOwner;\n isRegisteredModeOwnerFn = mod.isRegisteredModeOwner;\n })\n .catch((error) => {\n importFailed = true;\n // Log error in development only - in production this is a silent fallback\n if (process.env.NODE_ENV !== 'production') {\n console.error('[MapCursor] Failed to import mode store:', error);\n }\n });\n }\n}\n\n// Start the import immediately so it's likely resolved by first use\nensureModeStoreImported();\n\nfunction getModeOwner(mapId: UniqueId): string | undefined {\n // getModeOwnerFn will be available by the time bus listeners are set up\n // (which happens on first React subscriber, not at module load)\n return getModeOwnerFn?.(mapId);\n}\n\nfunction isRegisteredOwner(mapId: UniqueId, owner: string): boolean {\n return isRegisteredModeOwnerFn?.(mapId, owner) ?? false;\n}\n\n/**\n * Calculate effective cursor based on priority\n */\nfunction getEffectiveCursor(\n mapId: UniqueId,\n state: CursorState,\n): CSSCursorType {\n // Priority 1: Mode owner's cursor\n const modeOwner = getModeOwner?.(mapId);\n if (modeOwner) {\n const modeOwnerCursor = state.cursorOwners.get(modeOwner);\n if (modeOwnerCursor) {\n return modeOwnerCursor;\n }\n }\n\n // Priority 2: Current cursor (if owner still has entry)\n if (state.currentCursor && state.currentOwner) {\n if (state.cursorOwners.has(state.currentOwner)) {\n return state.currentCursor;\n }\n }\n\n // Priority 3: Default\n return DEFAULT_CURSOR;\n}\n\n/**\n * Cursor store\n */\nexport const cursorStore = createMapStore<CursorState, CursorActions>({\n defaultState: {\n cursorOwners: new Map(),\n currentCursor: null,\n currentOwner: null,\n },\n\n actions: (mapId, { get, set }) => ({\n requestCursorChange: (cursor: CSSCursorType, owner: string) => {\n const trimmedCursor = cursor.trim() as CSSCursorType;\n const trimmedOwner = owner.trim();\n\n if (!trimmedCursor) {\n throw new Error('requestCursorChange requires non-empty cursor');\n }\n if (!trimmedOwner) {\n throw new Error('requestCursorChange requires non-empty owner');\n }\n\n cursorBus.emit(MapCursorEvents.changeRequest, {\n cursor: trimmedCursor,\n owner: trimmedOwner,\n id: mapId,\n });\n },\n\n clearCursor: (owner: string) => {\n const state = get();\n const hadCursor = state.cursorOwners.has(owner);\n\n if (hadCursor) {\n // Clear current tracking if this was the owner\n const updates: Partial<CursorState> = {\n cursorOwners: mapDelete(state.cursorOwners, owner),\n };\n if (state.currentOwner === owner) {\n updates.currentCursor = null;\n updates.currentOwner = null;\n }\n\n set(updates);\n }\n },\n }),\n\n bus: (mapId, { get, set }) => {\n // Handle cursor change requests\n const unsubRequest = cursorBus.on(\n MapCursorEvents.changeRequest,\n (event) => {\n const { cursor, owner: requestOwner, id } = event.payload;\n if (id !== mapId) {\n return;\n }\n\n const state = get();\n const previousCursor = getEffectiveCursor(mapId, state);\n\n // Skip if same cursor already stored for this owner\n if (state.cursorOwners.get(requestOwner) === cursor) {\n return;\n }\n\n // Create new Map for immutable update\n const newCursorOwners = mapSet(\n state.cursorOwners,\n requestOwner,\n cursor,\n );\n\n // Check ownership\n const currentModeOwner = getModeOwner?.(mapId);\n const isOwnerless = !currentModeOwner;\n const isCurrentModeOwner = requestOwner === currentModeOwner;\n const isAnyModeOwner = isRegisteredOwner(mapId, requestOwner);\n\n if (isOwnerless || isCurrentModeOwner) {\n // Accept: apply cursor with immutable update\n const newState: CursorState = {\n cursorOwners: newCursorOwners,\n currentCursor: cursor,\n currentOwner: requestOwner,\n };\n\n // Calculate new cursor with updated state\n const newCursor = getEffectiveCursor(mapId, newState);\n\n if (previousCursor !== newCursor) {\n set(newState);\n cursorBus.emit(MapCursorEvents.changed, {\n previousCursor,\n currentCursor: newCursor,\n owner: requestOwner,\n id: mapId,\n });\n } else {\n // Still need to update state even if cursor didn't change visually\n set(newState);\n }\n } else if (isAnyModeOwner) {\n // Store but don't apply: requester owns a different mode (pending or not current).\n // When their mode becomes active, getEffectiveCursor will find their cursor.\n set({ cursorOwners: newCursorOwners });\n\n cursorBus.emit(MapCursorEvents.rejected, {\n rejectedCursor: cursor,\n rejectedOwner: requestOwner,\n currentOwner: state.currentOwner || currentModeOwner || 'unknown',\n reason: 'not-current-owner',\n id: mapId,\n });\n } else {\n // Reject: don't store. Non-owners should only set cursor in default mode.\n cursorBus.emit(MapCursorEvents.rejected, {\n rejectedCursor: cursor,\n rejectedOwner: requestOwner,\n currentOwner: state.currentOwner || currentModeOwner || 'unknown',\n reason: 'not-owner',\n id: mapId,\n });\n }\n },\n );\n\n // Handle mode changes\n const unsubMode = modeBus.on(MapModeEvents.changed, (event) => {\n if (event.payload.id !== mapId) {\n return;\n }\n\n const state = get();\n const previousCursor = getEffectiveCursor(mapId, state);\n\n // Clear current tracking on mode change with immutable update\n if (\n event.payload.currentMode === 'default' ||\n event.payload.previousMode !== event.payload.currentMode\n ) {\n set({\n currentCursor: null,\n currentOwner: null,\n });\n }\n\n // Defer check until new mode owner is registered\n queueMicrotask(() => {\n // Re-get state after microtask since it may have changed\n const currentState = get();\n const newCursor = getEffectiveCursor(mapId, currentState);\n if (previousCursor !== newCursor) {\n const newModeOwner = getModeOwner?.(mapId) || 'system';\n cursorBus.emit(MapCursorEvents.changed, {\n previousCursor,\n currentCursor: newCursor,\n owner: newModeOwner,\n id: mapId,\n });\n }\n });\n });\n\n return () => {\n unsubRequest();\n unsubMode();\n };\n },\n});\n\n// =============================================================================\n// Convenience exports\n// =============================================================================\n\n/**\n * Get effective cursor (computed from state)\n */\nexport function getCursor(mapId: UniqueId): CSSCursorType {\n return getEffectiveCursor(mapId, cursorStore.get(mapId));\n}\n\n/**\n * Hook for effective cursor value.\n *\n * **Internal use only** - not exported from the public API.\n * Use `useMapCursor` instead, which provides:\n * - MapContext integration (auto-resolves mapId inside MapProvider)\n * - Actions (requestCursorChange, clearCursor)\n * - Better ergonomics for consumers\n *\n * This hook exists for internal composition (used by useMapCursor).\n */\nexport function useCursor(mapId: UniqueId): CSSCursorType {\n return cursorStore.useSelector(mapId, (state) =>\n getEffectiveCursor(mapId, state),\n );\n}\n\n/**\n * Clear cursor state\n */\nexport function clearCursorState(mapId: UniqueId): void {\n cursorStore.clear(mapId);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-map-cursor.js","names":[],"sources":["../../src/map-cursor/use-map-cursor.ts"],"sourcesContent":["/*\n * Copyright
|
|
1
|
+
{"version":3,"file":"use-map-cursor.js","names":[],"sources":["../../src/map-cursor/use-map-cursor.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 { useContext, useEffect, useMemo, useRef } from 'react';\nimport { MapContext } from '../deckgl/base-map/provider';\nimport { cursorStore, useCursor } from './store';\nimport type { UniqueId } from '@accelint/core';\nimport type { CSSCursorType } from './types';\n\n/**\n * Return value for the useMapCursor hook\n */\nexport type UseMapCursorReturn = {\n /** The current active cursor */\n cursor: CSSCursorType;\n /** Function to request a cursor change with ownership */\n requestCursorChange: (cursor: CSSCursorType, requestOwner: string) => void;\n /** Function to clear cursor for an owner */\n clearCursor: (owner: string) => void;\n};\n\n/**\n * Hook to access the map cursor state and actions.\n *\n * This hook uses `useSyncExternalStore` to subscribe to map cursor state changes,\n * providing concurrent-safe cursor state updates. Uses a fan-out pattern where\n * a single bus listener per map instance notifies N React component subscribers.\n *\n * **Owner-based Priority System:**\n * - Mode owners (from MapModeStore) have highest priority\n * - Non-owners can set cursors only in default/ownerless modes\n * - Rejected requests emit events with rejection reasons\n *\n * @param id - Optional map instance ID. If not provided, will use the ID from `MapContext`.\n * @returns The current cursor, requestCursorChange function, and clearCursor function\n * @throws Error if no `id` is provided and hook is used outside of `MapProvider`\n *\n * @example\n * ```tsx\n * // Inside MapProvider (within BaseMap children)\n * function CustomDeckLayer() {\n * const { cursor, requestCursorChange, clearCursor } = useMapCursor();\n *\n * const handleHover = (info: PickingInfo) => {\n * if (info.object) {\n * requestCursorChange('pointer', 'custom-layer-id');\n * } else {\n * clearCursor('custom-layer-id');\n * }\n * };\n *\n * return <ScatterplotLayer onHover={handleHover} />;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Outside MapProvider - pass id directly\n * function ExternalControl({ mapId }: { mapId: UniqueId }) {\n * const { cursor, requestCursorChange } = useMapCursor(mapId);\n *\n * return (\n * <button onClick={() => requestCursorChange('crosshair', 'external-control')}>\n * Set Crosshair (current: {cursor})\n * </button>\n * );\n * }\n * ```\n */\nexport function useMapCursor(id?: UniqueId): UseMapCursorReturn {\n const contextId = useContext(MapContext);\n const actualId = id ?? contextId;\n\n if (!actualId) {\n throw new Error(\n 'useMapCursor requires either an id parameter or to be used within a MapProvider',\n );\n }\n\n // Use the useCursor hook which computes effective cursor with priority\n const cursor = useCursor(actualId);\n\n // Get actions from the store - cursorStore.actions returns stable references\n // (cached per mapId), so we can memoize on actualId only\n const actions = useMemo(() => cursorStore.actions(actualId), [actualId]);\n\n // Memoize the return value to prevent unnecessary re-renders\n return useMemo(\n () => ({\n cursor,\n requestCursorChange: actions.requestCursorChange,\n clearCursor: actions.clearCursor,\n }),\n [cursor, actions],\n );\n}\n\n/**\n * Hook to automatically manage cursor for a component's lifecycle.\n *\n * This hook automatically requests a cursor when mounted and clears it when unmounted.\n * Useful for components that need to consistently show a specific cursor.\n *\n * NOTE: This hook does NOT re-render when cursor state changes. If you need to read\n * the current cursor value, use `useMapCursor` instead.\n *\n * @param cursorType - The cursor to request\n * @param owner - The owner identifier\n * @param id - Optional map instance ID\n *\n * @example\n * ```tsx\n * function DrawingMode() {\n * useMapCursorEffect('crosshair', 'drawing-mode');\n *\n * return <div>Drawing mode active with crosshair cursor</div>;\n * }\n * ```\n */\nexport function useMapCursorEffect(\n cursorType: CSSCursorType,\n owner: string,\n id?: UniqueId,\n): void {\n const contextId = useContext(MapContext);\n const actualId = id ?? contextId;\n\n if (!actualId) {\n throw new Error(\n 'useMapCursorEffect requires either an id parameter or to be used within a MapProvider',\n );\n }\n\n // Store values in refs for stable cleanup function access\n const cursorRef = useRef(cursorType);\n const ownerRef = useRef(owner);\n const idRef = useRef(actualId);\n\n // Update refs on each render\n cursorRef.current = cursorType;\n ownerRef.current = owner;\n idRef.current = actualId;\n\n // Subscribe to store (to ensure bus listeners are set up) on mount only\n useEffect(() => {\n const mapId = idRef.current;\n\n // Subscribe to store to ensure bus listeners are set up\n // The subscription callback is a no-op since we don't want to re-render\n const unsubscribe = cursorStore.subscribe(mapId)(() => {\n // No-op: we don't want cursor state changes to trigger re-renders\n });\n\n return () => {\n // Unsubscribe first to prevent re-render loops\n unsubscribe();\n\n // IMPORTANT: Defer cursor clear using queueMicrotask to avoid the React error:\n // \"Cannot update a component while rendering a different component\"\n //\n // Effect cleanup runs during React's commit phase. If clearCursor() triggers\n // a state update synchronously (via store.set() -> notify subscribers), it can\n // cause React to attempt re-rendering while still committing the current tree.\n //\n // queueMicrotask schedules the cleanup to run after the current commit phase\n // completes, allowing React to finish its work before the state update occurs.\n queueMicrotask(() => {\n const cleanupActions = cursorStore.actions(idRef.current);\n cleanupActions.clearCursor(ownerRef.current);\n });\n };\n }, []);\n\n // Request cursor when cursorType or owner changes\n useEffect(() => {\n const actions = cursorStore.actions(actualId);\n actions.requestCursorChange(cursorType, owner);\n }, [actualId, cursorType, owner]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgFA,SAAgB,aAAa,IAAmC;CAC9D,MAAM,YAAY,WAAW,WAAW;CACxC,MAAM,WAAW,MAAM;AAEvB,KAAI,CAAC,SACH,OAAM,IAAI,MACR,kFACD;CAIH,MAAM,SAAS,UAAU,SAAS;CAIlC,MAAM,UAAU,cAAc,YAAY,QAAQ,SAAS,EAAE,CAAC,SAAS,CAAC;AAGxE,QAAO,eACE;EACL;EACA,qBAAqB,QAAQ;EAC7B,aAAa,QAAQ;EACtB,GACD,CAAC,QAAQ,QAAQ,CAClB;;;;;;;;;;;;;;;;;;;;;;;;AAyBH,SAAgB,mBACd,YACA,OACA,IACM;CACN,MAAM,YAAY,WAAW,WAAW;CACxC,MAAM,WAAW,MAAM;AAEvB,KAAI,CAAC,SACH,OAAM,IAAI,MACR,wFACD;CAIH,MAAM,YAAY,OAAO,WAAW;CACpC,MAAM,WAAW,OAAO,MAAM;CAC9B,MAAM,QAAQ,OAAO,SAAS;AAG9B,WAAU,UAAU;AACpB,UAAS,UAAU;AACnB,OAAM,UAAU;AAGhB,iBAAgB;EACd,MAAM,QAAQ,MAAM;EAIpB,MAAM,cAAc,YAAY,UAAU,MAAM,OAAO,GAErD;AAEF,eAAa;AAEX,gBAAa;AAWb,wBAAqB;AAEnB,IADuB,YAAY,QAAQ,MAAM,QAAQ,CAC1C,YAAY,SAAS,QAAQ;KAC5C;;IAEH,EAAE,CAAC;AAGN,iBAAgB;AAEd,EADgB,YAAY,QAAQ,SAAS,CACrC,oBAAoB,YAAY,MAAM;IAC7C;EAAC;EAAU;EAAY;EAAM,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","names":[],"sources":["../../src/map-mode/events.ts"],"sourcesContent":["/*\n * Copyright
|
|
1
|
+
{"version":3,"file":"events.js","names":[],"sources":["../../src/map-mode/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\n/**\n * Namespace prefix for all map mode events.\n */\nexport const MapModeEventsNamespace = 'map-mode';\n\n/**\n * Event type constants for map mode state management.\n *\n * These events are emitted through the `@accelint/bus` event bus to coordinate\n * map mode changes across components in a decoupled manner.\n *\n * @example\n * ```ts\n * import { useOn, useEmit } from '@accelint/bus/react';\n * import { MapModeEvents } from '@accelint/map-toolkit/map-mode';\n *\n * // Listen for mode changes\n * useOn(MapModeEvents.changed, (event) => {\n * console.log('Mode changed to:', event.payload.currentMode);\n * });\n *\n * // Emit a decision\n * const emitDecision = useEmit(MapModeEvents.changeDecision);\n * emitDecision({ authId, approved: true, owner: 'my-id', id });\n * ```\n */\nexport const MapModeEvents = {\n /** Emitted when the map mode has successfully changed */\n changed: `${MapModeEventsNamespace}:changed`,\n /** Emitted when a component requests a mode change */\n changeRequest: `${MapModeEventsNamespace}:change:request`,\n /** Emitted when authorization is required for a mode change */\n changeAuthorization: `${MapModeEventsNamespace}:change:authorization`,\n /** Emitted when an authorization decision is made (approve/reject) */\n changeDecision: `${MapModeEventsNamespace}:change:decision`,\n} as const;\n"],"mappings":";;;;;;;;;;;;;;;;;AAeA,MAAa,yBAAyB;;;;;;;;;;;;;;;;;;;;;;AAuBtC,MAAa,gBAAgB;CAE3B,SAAS,GAAG,uBAAuB;CAEnC,eAAe,GAAG,uBAAuB;CAEzC,qBAAqB,GAAG,uBAAuB;CAE/C,gBAAgB,GAAG,uBAAuB;CAC3C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","names":["requestsToReject: PendingRequest[]","matchingRequestOwner: string | null","matchingRequest: PendingRequest | null"],"sources":["../../src/map-mode/store.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\n/**\n * Map Mode Store\n *\n * Manages mode state with ownership-based authorization.\n *\n * @example\n * ```tsx\n * import { modeStore } from '@accelint/map-toolkit/map-mode';\n *\n * function MapControls({ mapId }) {\n * const { state, requestModeChange } = modeStore.use(mapId);\n *\n * return (\n * <div>\n * <p>Current mode: {state.mode}</p>\n * <button onClick={() => requestModeChange('draw', 'draw-layer')}>\n * Draw Mode\n * </button>\n * </div>\n * );\n * }\n * ```\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { uuid } from '@accelint/core';\nimport { getLogger } from '@accelint/logger';\nimport {\n createMapStore,\n mapClear,\n mapDelete,\n mapSet,\n} from '../shared/create-map-store';\nimport { MapModeEvents } from './events';\nimport type { UniqueId } from '@accelint/core';\nimport type { StoreHelpers } from '../shared/create-map-store';\nimport type { MapModeEventType, ModeChangeDecisionPayload } from './types';\n\nconst logger = getLogger({\n enabled:\n process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test',\n level: 'warn',\n prefix: '[MapMode]',\n pretty: true,\n});\n\nconst DEFAULT_MODE = 'default';\n\n/**\n * Typed event bus instance for map mode events.\n */\nconst mapModeBus = Broadcast.getInstance<MapModeEventType>();\n\n/**\n * Internal type for tracking pending authorization requests.\n */\ntype PendingRequest = {\n authId: string;\n desiredMode: string;\n currentMode: string;\n requestOwner: string;\n};\n\n/**\n * State shape for map mode\n */\ntype MapModeState = {\n mode: string;\n modeOwners: Map<string, string>;\n pendingRequests: Map<string, PendingRequest>;\n};\n\n/**\n * Actions for map mode\n */\ntype MapModeActions = {\n /** Request a mode change */\n requestModeChange: (desiredMode: string, requestOwner: string) => void;\n};\n\n/**\n * Determine if a mode change request should be auto-accepted without authorization\n */\nfunction shouldAutoAcceptRequest(\n state: MapModeState,\n desiredMode: string,\n requestOwner: string,\n): boolean {\n const currentModeOwner = state.modeOwners.get(state.mode);\n const desiredModeOwner = state.modeOwners.get(desiredMode);\n\n // Owner returning to default mode\n if (desiredMode === DEFAULT_MODE && requestOwner === currentModeOwner) {\n return true;\n }\n\n // Owner switching between their own modes\n if (requestOwner === currentModeOwner) {\n return true;\n }\n\n // No ownership conflicts exist\n if (!(currentModeOwner || desiredModeOwner)) {\n return true;\n }\n\n // Entering an owned mode from default mode\n if (state.mode === DEFAULT_MODE && requestOwner === desiredModeOwner) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Approve a request and reject all others (immutable update)\n */\nfunction approveRequestAndRejectOthers(\n instanceId: UniqueId,\n state: MapModeState,\n approvedRequest: PendingRequest,\n excludeAuthId: string,\n decisionOwner: string,\n reason: string,\n emitApproval: boolean,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n // Collect all other pending requests to emit rejections for\n const requestsToReject: PendingRequest[] = [];\n for (const request of state.pendingRequests.values()) {\n if (request.authId !== excludeAuthId) {\n requestsToReject.push(request);\n }\n }\n\n // Build immutable updates: clear pending requests, update owners\n const newModeOwners =\n approvedRequest.desiredMode !== DEFAULT_MODE\n ? mapSet(\n state.modeOwners,\n approvedRequest.desiredMode,\n approvedRequest.requestOwner,\n )\n : state.modeOwners;\n\n // Immutable update: clear pending requests, update owners, change mode\n set({\n mode: approvedRequest.desiredMode,\n pendingRequests: mapClear<string, PendingRequest>(),\n modeOwners: newModeOwners,\n });\n\n // Emit mode changed event\n mapModeBus.emit(MapModeEvents.changed, {\n previousMode: state.mode,\n currentMode: approvedRequest.desiredMode,\n id: instanceId,\n });\n\n // Emit approval decision if requested\n if (emitApproval) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: approvedRequest.authId,\n approved: true,\n owner: decisionOwner,\n reason,\n id: instanceId,\n });\n }\n\n // Emit rejection events for all other pending requests\n for (const request of requestsToReject) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: decisionOwner,\n reason: 'Request auto-rejected because another request was approved',\n id: instanceId,\n });\n }\n}\n\n/**\n * Handle pending requests when returning to default mode (immutable update)\n */\nfunction handlePendingRequestsOnDefaultMode(\n instanceId: UniqueId,\n state: MapModeState,\n previousMode: string,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const firstEntry = Array.from(state.pendingRequests.values())[0];\n if (!firstEntry) {\n return;\n }\n\n const previousModeOwner = state.modeOwners.get(previousMode);\n\n if (!previousModeOwner) {\n return;\n }\n\n // If the first pending request is for default mode, reject all requests\n if (firstEntry.desiredMode === DEFAULT_MODE) {\n const allRequests = Array.from(state.pendingRequests.values());\n\n // Immutable update: clear pending requests\n set({ pendingRequests: mapClear<string, PendingRequest>() });\n\n for (const request of allRequests) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: previousModeOwner,\n reason: 'Request rejected - already in requested mode',\n id: instanceId,\n } satisfies ModeChangeDecisionPayload);\n }\n } else {\n // Auto-accept the first pending request for a different mode\n approveRequestAndRejectOthers(\n instanceId,\n state,\n firstEntry,\n firstEntry.authId,\n previousModeOwner,\n 'Auto-accepted when mode owner returned to default',\n true,\n set,\n );\n }\n}\n\n/**\n * Handle authorization decision (immutable update)\n */\nfunction handleAuthorizationDecision(\n instanceId: UniqueId,\n state: MapModeState,\n payload: {\n approved: boolean;\n authId: string;\n owner: string;\n },\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const { approved, authId, owner: decisionOwner } = payload;\n\n // Verify decision is from current mode's owner\n const currentModeOwner = state.modeOwners.get(state.mode);\n if (decisionOwner !== currentModeOwner) {\n logger.warn(\n `Authorization decision from \"${decisionOwner}\" ignored - not the owner of mode \"${state.mode}\" (owner: ${currentModeOwner || 'none'})`,\n );\n return;\n }\n\n // Find the request with matching authId\n let matchingRequestOwner: string | null = null;\n let matchingRequest: PendingRequest | null = null;\n\n for (const [requestOwner, request] of state.pendingRequests.entries()) {\n if (request.authId === authId) {\n matchingRequestOwner = requestOwner;\n matchingRequest = request;\n break;\n }\n }\n\n if (!(matchingRequest && matchingRequestOwner)) {\n return;\n }\n\n if (approved) {\n approveRequestAndRejectOthers(\n instanceId,\n state,\n matchingRequest,\n authId,\n decisionOwner,\n '',\n false,\n set,\n );\n } else {\n // Immutable update: remove the rejected request\n set({\n pendingRequests: mapDelete(state.pendingRequests, matchingRequestOwner),\n });\n }\n}\n\n/**\n * Handle mode change request logic (immutable update)\n */\nfunction handleModeChangeRequest(\n instanceId: UniqueId,\n state: MapModeState,\n desiredMode: string,\n requestOwner: string,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const desiredModeOwner = state.modeOwners.get(desiredMode);\n\n // Check if this request should be auto-accepted\n if (shouldAutoAcceptRequest(state, desiredMode, requestOwner)) {\n // Build immutable updates\n const newModeOwners =\n desiredMode !== DEFAULT_MODE && !desiredModeOwner\n ? mapSet(state.modeOwners, desiredMode, requestOwner)\n : state.modeOwners;\n\n // Clear requester's pending request since mode changed successfully\n const newPendingRequests = mapDelete(state.pendingRequests, requestOwner);\n\n const previousMode = state.mode;\n\n // Immutable update\n set({\n mode: desiredMode,\n modeOwners: newModeOwners,\n pendingRequests: newPendingRequests,\n });\n\n mapModeBus.emit(MapModeEvents.changed, {\n previousMode,\n currentMode: desiredMode,\n id: instanceId,\n });\n\n return;\n }\n\n // Otherwise, send authorization request\n const authId = uuid();\n\n // Immutable update: add pending request\n set({\n pendingRequests: mapSet(state.pendingRequests, requestOwner, {\n authId,\n desiredMode,\n currentMode: state.mode,\n requestOwner,\n }),\n });\n\n mapModeBus.emit(MapModeEvents.changeAuthorization, {\n authId,\n desiredMode,\n currentMode: state.mode,\n id: instanceId,\n });\n}\n\n/**\n * Map mode store\n */\nexport const modeStore = createMapStore<MapModeState, MapModeActions>({\n defaultState: {\n mode: DEFAULT_MODE,\n modeOwners: new Map(),\n pendingRequests: new Map(),\n },\n\n actions: (instanceId) => ({\n requestModeChange: (desiredMode: string, requestOwner: string) => {\n const trimmedDesiredMode = desiredMode.trim();\n const trimmedRequestOwner = requestOwner.trim();\n\n if (!trimmedDesiredMode) {\n throw new Error('requestModeChange requires non-empty desiredMode');\n }\n if (!trimmedRequestOwner) {\n throw new Error('requestModeChange requires non-empty requestOwner');\n }\n\n mapModeBus.emit(MapModeEvents.changeRequest, {\n desiredMode: trimmedDesiredMode,\n owner: trimmedRequestOwner,\n id: instanceId,\n });\n },\n }),\n\n bus: (instanceId, { get, set }) => {\n // Listen for mode change requests\n const unsubRequest = mapModeBus.on(MapModeEvents.changeRequest, (event) => {\n const { desiredMode, owner: requestOwner, id } = event.payload;\n\n const state = get();\n // Filter: only handle if targeted at this map\n if (id !== instanceId || desiredMode === state.mode) {\n return;\n }\n\n handleModeChangeRequest(\n instanceId,\n state,\n desiredMode,\n requestOwner,\n set,\n );\n });\n\n // Listen for authorization decisions\n const unsubDecision = mapModeBus.on(\n MapModeEvents.changeDecision,\n (event) => {\n const { id, approved, authId, owner } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== instanceId) {\n return;\n }\n\n handleAuthorizationDecision(\n instanceId,\n get(),\n { approved, authId, owner },\n set,\n );\n },\n );\n\n // Listen for mode changes to handle pending requests\n const unsubChanged = mapModeBus.on(MapModeEvents.changed, (event) => {\n const { currentMode, previousMode, id } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== instanceId) {\n return;\n }\n\n const state = get();\n // When mode owner changes to default mode, handle pending requests\n if (currentMode === DEFAULT_MODE && state.pendingRequests.size > 0) {\n handlePendingRequestsOnDefaultMode(\n instanceId,\n state,\n previousMode,\n set,\n );\n }\n });\n\n return () => {\n unsubRequest();\n unsubDecision();\n unsubChanged();\n };\n },\n});\n\n// =============================================================================\n// Convenience exports\n// =============================================================================\n\n/**\n * Get the current mode for a map instance\n */\nexport function getMode(mapId: UniqueId): string {\n return modeStore.get(mapId).mode;\n}\n\n/**\n * Hook for current mode value\n */\nexport function useMode(mapId: UniqueId): string {\n return modeStore.useSelector(mapId, (state) => state.mode);\n}\n\n/**\n * Get the owner of the current mode for a given map instance\n * @internal - For internal map-toolkit use only\n */\nexport function getCurrentModeOwner(instanceId: UniqueId): string | undefined {\n const state = modeStore.get(instanceId);\n return state.modeOwners.get(state.mode);\n}\n\n/**\n * Check if a given owner is registered as the owner of any mode.\n * This includes both active mode owners and pending mode requests.\n * @internal - For internal map-toolkit use only\n */\nexport function isRegisteredModeOwner(\n instanceId: UniqueId,\n owner: string,\n): boolean {\n const state = modeStore.get(instanceId);\n\n // Check active mode owners\n for (const modeOwner of state.modeOwners.values()) {\n if (modeOwner === owner) {\n return true;\n }\n }\n\n // Check pending mode requests (owner is the key in pendingRequests map)\n if (state.pendingRequests.has(owner)) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Manually clear map mode state for a specific instanceId.\n */\nexport function clearMapModeState(instanceId: UniqueId): void {\n modeStore.clear(instanceId);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,MAAM,SAAS,UAAU;CACvB,SACE,QAAQ,IAAI,aAAa,gBAAgB,QAAQ,IAAI,aAAa;CACpE,OAAO;CACP,QAAQ;CACR,QAAQ;CACT,CAAC;AAEF,MAAM,eAAe;;;;AAKrB,MAAM,aAAa,UAAU,aAA+B;;;;AAgC5D,SAAS,wBACP,OACA,aACA,cACS;CACT,MAAM,mBAAmB,MAAM,WAAW,IAAI,MAAM,KAAK;CACzD,MAAM,mBAAmB,MAAM,WAAW,IAAI,YAAY;AAG1D,KAAI,gBAAgB,gBAAgB,iBAAiB,iBACnD,QAAO;AAIT,KAAI,iBAAiB,iBACnB,QAAO;AAIT,KAAI,EAAE,oBAAoB,kBACxB,QAAO;AAIT,KAAI,MAAM,SAAS,gBAAgB,iBAAiB,iBAClD,QAAO;AAGT,QAAO;;;;;AAMT,SAAS,8BACP,YACA,OACA,iBACA,eACA,eACA,QACA,cACA,KACM;CAEN,MAAMA,mBAAqC,EAAE;AAC7C,MAAK,MAAM,WAAW,MAAM,gBAAgB,QAAQ,CAClD,KAAI,QAAQ,WAAW,cACrB,kBAAiB,KAAK,QAAQ;CAKlC,MAAM,gBACJ,gBAAgB,gBAAgB,eAC5B,OACE,MAAM,YACN,gBAAgB,aAChB,gBAAgB,aACjB,GACD,MAAM;AAGZ,KAAI;EACF,MAAM,gBAAgB;EACtB,iBAAiB,UAAkC;EACnD,YAAY;EACb,CAAC;AAGF,YAAW,KAAK,cAAc,SAAS;EACrC,cAAc,MAAM;EACpB,aAAa,gBAAgB;EAC7B,IAAI;EACL,CAAC;AAGF,KAAI,aACF,YAAW,KAAK,cAAc,gBAAgB;EAC5C,QAAQ,gBAAgB;EACxB,UAAU;EACV,OAAO;EACP;EACA,IAAI;EACL,CAAC;AAIJ,MAAK,MAAM,WAAW,iBACpB,YAAW,KAAK,cAAc,gBAAgB;EAC5C,QAAQ,QAAQ;EAChB,UAAU;EACV,OAAO;EACP,QAAQ;EACR,IAAI;EACL,CAAC;;;;;AAON,SAAS,mCACP,YACA,OACA,cACA,KACM;CACN,MAAM,aAAa,MAAM,KAAK,MAAM,gBAAgB,QAAQ,CAAC,CAAC;AAC9D,KAAI,CAAC,WACH;CAGF,MAAM,oBAAoB,MAAM,WAAW,IAAI,aAAa;AAE5D,KAAI,CAAC,kBACH;AAIF,KAAI,WAAW,gBAAgB,cAAc;EAC3C,MAAM,cAAc,MAAM,KAAK,MAAM,gBAAgB,QAAQ,CAAC;AAG9D,MAAI,EAAE,iBAAiB,UAAkC,EAAE,CAAC;AAE5D,OAAK,MAAM,WAAW,YACpB,YAAW,KAAK,cAAc,gBAAgB;GAC5C,QAAQ,QAAQ;GAChB,UAAU;GACV,OAAO;GACP,QAAQ;GACR,IAAI;GACL,CAAqC;OAIxC,+BACE,YACA,OACA,YACA,WAAW,QACX,mBACA,qDACA,MACA,IACD;;;;;AAOL,SAAS,4BACP,YACA,OACA,SAKA,KACM;CACN,MAAM,EAAE,UAAU,QAAQ,OAAO,kBAAkB;CAGnD,MAAM,mBAAmB,MAAM,WAAW,IAAI,MAAM,KAAK;AACzD,KAAI,kBAAkB,kBAAkB;AACtC,SAAO,KACL,gCAAgC,cAAc,qCAAqC,MAAM,KAAK,YAAY,oBAAoB,OAAO,GACtI;AACD;;CAIF,IAAIC,uBAAsC;CAC1C,IAAIC,kBAAyC;AAE7C,MAAK,MAAM,CAAC,cAAc,YAAY,MAAM,gBAAgB,SAAS,CACnE,KAAI,QAAQ,WAAW,QAAQ;AAC7B,yBAAuB;AACvB,oBAAkB;AAClB;;AAIJ,KAAI,EAAE,mBAAmB,sBACvB;AAGF,KAAI,SACF,+BACE,YACA,OACA,iBACA,QACA,eACA,IACA,OACA,IACD;KAGD,KAAI,EACF,iBAAiB,UAAU,MAAM,iBAAiB,qBAAqB,EACxE,CAAC;;;;;AAON,SAAS,wBACP,YACA,OACA,aACA,cACA,KACM;CACN,MAAM,mBAAmB,MAAM,WAAW,IAAI,YAAY;AAG1D,KAAI,wBAAwB,OAAO,aAAa,aAAa,EAAE;EAE7D,MAAM,gBACJ,gBAAgB,gBAAgB,CAAC,mBAC7B,OAAO,MAAM,YAAY,aAAa,aAAa,GACnD,MAAM;EAGZ,MAAM,qBAAqB,UAAU,MAAM,iBAAiB,aAAa;EAEzE,MAAM,eAAe,MAAM;AAG3B,MAAI;GACF,MAAM;GACN,YAAY;GACZ,iBAAiB;GAClB,CAAC;AAEF,aAAW,KAAK,cAAc,SAAS;GACrC;GACA,aAAa;GACb,IAAI;GACL,CAAC;AAEF;;CAIF,MAAM,SAAS,MAAM;AAGrB,KAAI,EACF,iBAAiB,OAAO,MAAM,iBAAiB,cAAc;EAC3D;EACA;EACA,aAAa,MAAM;EACnB;EACD,CAAC,EACH,CAAC;AAEF,YAAW,KAAK,cAAc,qBAAqB;EACjD;EACA;EACA,aAAa,MAAM;EACnB,IAAI;EACL,CAAC;;;;;AAMJ,MAAa,YAAY,eAA6C;CACpE,cAAc;EACZ,MAAM;EACN,4BAAY,IAAI,KAAK;EACrB,iCAAiB,IAAI,KAAK;EAC3B;CAED,UAAU,gBAAgB,EACxB,oBAAoB,aAAqB,iBAAyB;EAChE,MAAM,qBAAqB,YAAY,MAAM;EAC7C,MAAM,sBAAsB,aAAa,MAAM;AAE/C,MAAI,CAAC,mBACH,OAAM,IAAI,MAAM,mDAAmD;AAErE,MAAI,CAAC,oBACH,OAAM,IAAI,MAAM,oDAAoD;AAGtE,aAAW,KAAK,cAAc,eAAe;GAC3C,aAAa;GACb,OAAO;GACP,IAAI;GACL,CAAC;IAEL;CAED,MAAM,YAAY,EAAE,KAAK,UAAU;EAEjC,MAAM,eAAe,WAAW,GAAG,cAAc,gBAAgB,UAAU;GACzE,MAAM,EAAE,aAAa,OAAO,cAAc,OAAO,MAAM;GAEvD,MAAM,QAAQ,KAAK;AAEnB,OAAI,OAAO,cAAc,gBAAgB,MAAM,KAC7C;AAGF,2BACE,YACA,OACA,aACA,cACA,IACD;IACD;EAGF,MAAM,gBAAgB,WAAW,GAC/B,cAAc,iBACb,UAAU;GACT,MAAM,EAAE,IAAI,UAAU,QAAQ,UAAU,MAAM;AAG9C,OAAI,OAAO,WACT;AAGF,+BACE,YACA,KAAK,EACL;IAAE;IAAU;IAAQ;IAAO,EAC3B,IACD;IAEJ;EAGD,MAAM,eAAe,WAAW,GAAG,cAAc,UAAU,UAAU;GACnE,MAAM,EAAE,aAAa,cAAc,OAAO,MAAM;AAGhD,OAAI,OAAO,WACT;GAGF,MAAM,QAAQ,KAAK;AAEnB,OAAI,gBAAgB,gBAAgB,MAAM,gBAAgB,OAAO,EAC/D,oCACE,YACA,OACA,cACA,IACD;IAEH;AAEF,eAAa;AACX,iBAAc;AACd,kBAAe;AACf,iBAAc;;;CAGnB,CAAC;;;;AASF,SAAgB,QAAQ,OAAyB;AAC/C,QAAO,UAAU,IAAI,MAAM,CAAC;;;;;AAM9B,SAAgB,QAAQ,OAAyB;AAC/C,QAAO,UAAU,YAAY,QAAQ,UAAU,MAAM,KAAK;;;;;;AAO5D,SAAgB,oBAAoB,YAA0C;CAC5E,MAAM,QAAQ,UAAU,IAAI,WAAW;AACvC,QAAO,MAAM,WAAW,IAAI,MAAM,KAAK;;;;;;;AAQzC,SAAgB,sBACd,YACA,OACS;CACT,MAAM,QAAQ,UAAU,IAAI,WAAW;AAGvC,MAAK,MAAM,aAAa,MAAM,WAAW,QAAQ,CAC/C,KAAI,cAAc,MAChB,QAAO;AAKX,KAAI,MAAM,gBAAgB,IAAI,MAAM,CAClC,QAAO;AAGT,QAAO;;;;;AAMT,SAAgB,kBAAkB,YAA4B;AAC5D,WAAU,MAAM,WAAW"}
|
|
1
|
+
{"version":3,"file":"store.js","names":["requestsToReject: PendingRequest[]","matchingRequestOwner: string | null","matchingRequest: PendingRequest | null"],"sources":["../../src/map-mode/store.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\n/**\n * Map Mode Store\n *\n * Manages mode state with ownership-based authorization.\n *\n * @example\n * ```tsx\n * import { modeStore } from '@accelint/map-toolkit/map-mode';\n *\n * function MapControls({ mapId }) {\n * const { state, requestModeChange } = modeStore.use(mapId);\n *\n * return (\n * <div>\n * <p>Current mode: {state.mode}</p>\n * <button onClick={() => requestModeChange('draw', 'draw-layer')}>\n * Draw Mode\n * </button>\n * </div>\n * );\n * }\n * ```\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { uuid } from '@accelint/core';\nimport { getLogger } from '@accelint/logger';\nimport {\n createMapStore,\n mapClear,\n mapDelete,\n mapSet,\n} from '../shared/create-map-store';\nimport { MapModeEvents } from './events';\nimport type { UniqueId } from '@accelint/core';\nimport type { StoreHelpers } from '../shared/create-map-store';\nimport type { MapModeEventType, ModeChangeDecisionPayload } from './types';\n\nconst logger = getLogger({\n enabled:\n process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test',\n level: 'warn',\n prefix: '[MapMode]',\n pretty: true,\n});\n\nconst DEFAULT_MODE = 'default';\n\n/**\n * Typed event bus instance for map mode events.\n */\nconst mapModeBus = Broadcast.getInstance<MapModeEventType>();\n\n/**\n * Internal type for tracking pending authorization requests.\n */\ntype PendingRequest = {\n authId: string;\n desiredMode: string;\n currentMode: string;\n requestOwner: string;\n};\n\n/**\n * State shape for map mode\n */\ntype MapModeState = {\n mode: string;\n modeOwners: Map<string, string>;\n pendingRequests: Map<string, PendingRequest>;\n};\n\n/**\n * Actions for map mode\n */\ntype MapModeActions = {\n /** Request a mode change */\n requestModeChange: (desiredMode: string, requestOwner: string) => void;\n};\n\n/**\n * Determine if a mode change request should be auto-accepted without authorization\n */\nfunction shouldAutoAcceptRequest(\n state: MapModeState,\n desiredMode: string,\n requestOwner: string,\n): boolean {\n const currentModeOwner = state.modeOwners.get(state.mode);\n const desiredModeOwner = state.modeOwners.get(desiredMode);\n\n // Owner returning to default mode\n if (desiredMode === DEFAULT_MODE && requestOwner === currentModeOwner) {\n return true;\n }\n\n // Owner switching between their own modes\n if (requestOwner === currentModeOwner) {\n return true;\n }\n\n // No ownership conflicts exist\n if (!(currentModeOwner || desiredModeOwner)) {\n return true;\n }\n\n // Entering an owned mode from default mode\n if (state.mode === DEFAULT_MODE && requestOwner === desiredModeOwner) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Approve a request and reject all others (immutable update)\n */\nfunction approveRequestAndRejectOthers(\n instanceId: UniqueId,\n state: MapModeState,\n approvedRequest: PendingRequest,\n excludeAuthId: string,\n decisionOwner: string,\n reason: string,\n emitApproval: boolean,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n // Collect all other pending requests to emit rejections for\n const requestsToReject: PendingRequest[] = [];\n for (const request of state.pendingRequests.values()) {\n if (request.authId !== excludeAuthId) {\n requestsToReject.push(request);\n }\n }\n\n // Build immutable updates: clear pending requests, update owners\n const newModeOwners =\n approvedRequest.desiredMode !== DEFAULT_MODE\n ? mapSet(\n state.modeOwners,\n approvedRequest.desiredMode,\n approvedRequest.requestOwner,\n )\n : state.modeOwners;\n\n // Immutable update: clear pending requests, update owners, change mode\n set({\n mode: approvedRequest.desiredMode,\n pendingRequests: mapClear<string, PendingRequest>(),\n modeOwners: newModeOwners,\n });\n\n // Emit mode changed event\n mapModeBus.emit(MapModeEvents.changed, {\n previousMode: state.mode,\n currentMode: approvedRequest.desiredMode,\n id: instanceId,\n });\n\n // Emit approval decision if requested\n if (emitApproval) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: approvedRequest.authId,\n approved: true,\n owner: decisionOwner,\n reason,\n id: instanceId,\n });\n }\n\n // Emit rejection events for all other pending requests\n for (const request of requestsToReject) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: decisionOwner,\n reason: 'Request auto-rejected because another request was approved',\n id: instanceId,\n });\n }\n}\n\n/**\n * Handle pending requests when returning to default mode (immutable update)\n */\nfunction handlePendingRequestsOnDefaultMode(\n instanceId: UniqueId,\n state: MapModeState,\n previousMode: string,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const firstEntry = Array.from(state.pendingRequests.values())[0];\n if (!firstEntry) {\n return;\n }\n\n const previousModeOwner = state.modeOwners.get(previousMode);\n\n if (!previousModeOwner) {\n return;\n }\n\n // If the first pending request is for default mode, reject all requests\n if (firstEntry.desiredMode === DEFAULT_MODE) {\n const allRequests = Array.from(state.pendingRequests.values());\n\n // Immutable update: clear pending requests\n set({ pendingRequests: mapClear<string, PendingRequest>() });\n\n for (const request of allRequests) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: previousModeOwner,\n reason: 'Request rejected - already in requested mode',\n id: instanceId,\n } satisfies ModeChangeDecisionPayload);\n }\n } else {\n // Auto-accept the first pending request for a different mode\n approveRequestAndRejectOthers(\n instanceId,\n state,\n firstEntry,\n firstEntry.authId,\n previousModeOwner,\n 'Auto-accepted when mode owner returned to default',\n true,\n set,\n );\n }\n}\n\n/**\n * Handle authorization decision (immutable update)\n */\nfunction handleAuthorizationDecision(\n instanceId: UniqueId,\n state: MapModeState,\n payload: {\n approved: boolean;\n authId: string;\n owner: string;\n },\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const { approved, authId, owner: decisionOwner } = payload;\n\n // Verify decision is from current mode's owner\n const currentModeOwner = state.modeOwners.get(state.mode);\n if (decisionOwner !== currentModeOwner) {\n logger.warn(\n `Authorization decision from \"${decisionOwner}\" ignored - not the owner of mode \"${state.mode}\" (owner: ${currentModeOwner || 'none'})`,\n );\n return;\n }\n\n // Find the request with matching authId\n let matchingRequestOwner: string | null = null;\n let matchingRequest: PendingRequest | null = null;\n\n for (const [requestOwner, request] of state.pendingRequests.entries()) {\n if (request.authId === authId) {\n matchingRequestOwner = requestOwner;\n matchingRequest = request;\n break;\n }\n }\n\n if (!(matchingRequest && matchingRequestOwner)) {\n return;\n }\n\n if (approved) {\n approveRequestAndRejectOthers(\n instanceId,\n state,\n matchingRequest,\n authId,\n decisionOwner,\n '',\n false,\n set,\n );\n } else {\n // Immutable update: remove the rejected request\n set({\n pendingRequests: mapDelete(state.pendingRequests, matchingRequestOwner),\n });\n }\n}\n\n/**\n * Handle mode change request logic (immutable update)\n */\nfunction handleModeChangeRequest(\n instanceId: UniqueId,\n state: MapModeState,\n desiredMode: string,\n requestOwner: string,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const desiredModeOwner = state.modeOwners.get(desiredMode);\n\n // Check if this request should be auto-accepted\n if (shouldAutoAcceptRequest(state, desiredMode, requestOwner)) {\n // Build immutable updates\n const newModeOwners =\n desiredMode !== DEFAULT_MODE && !desiredModeOwner\n ? mapSet(state.modeOwners, desiredMode, requestOwner)\n : state.modeOwners;\n\n // Clear requester's pending request since mode changed successfully\n const newPendingRequests = mapDelete(state.pendingRequests, requestOwner);\n\n const previousMode = state.mode;\n\n // Immutable update\n set({\n mode: desiredMode,\n modeOwners: newModeOwners,\n pendingRequests: newPendingRequests,\n });\n\n mapModeBus.emit(MapModeEvents.changed, {\n previousMode,\n currentMode: desiredMode,\n id: instanceId,\n });\n\n return;\n }\n\n // Otherwise, send authorization request\n const authId = uuid();\n\n // Immutable update: add pending request\n set({\n pendingRequests: mapSet(state.pendingRequests, requestOwner, {\n authId,\n desiredMode,\n currentMode: state.mode,\n requestOwner,\n }),\n });\n\n mapModeBus.emit(MapModeEvents.changeAuthorization, {\n authId,\n desiredMode,\n currentMode: state.mode,\n id: instanceId,\n });\n}\n\n/**\n * Map mode store\n */\nexport const modeStore = createMapStore<MapModeState, MapModeActions>({\n defaultState: {\n mode: DEFAULT_MODE,\n modeOwners: new Map(),\n pendingRequests: new Map(),\n },\n\n actions: (instanceId) => ({\n requestModeChange: (desiredMode: string, requestOwner: string) => {\n const trimmedDesiredMode = desiredMode.trim();\n const trimmedRequestOwner = requestOwner.trim();\n\n if (!trimmedDesiredMode) {\n throw new Error('requestModeChange requires non-empty desiredMode');\n }\n if (!trimmedRequestOwner) {\n throw new Error('requestModeChange requires non-empty requestOwner');\n }\n\n mapModeBus.emit(MapModeEvents.changeRequest, {\n desiredMode: trimmedDesiredMode,\n owner: trimmedRequestOwner,\n id: instanceId,\n });\n },\n }),\n\n bus: (instanceId, { get, set }) => {\n // Listen for mode change requests\n const unsubRequest = mapModeBus.on(MapModeEvents.changeRequest, (event) => {\n const { desiredMode, owner: requestOwner, id } = event.payload;\n\n const state = get();\n // Filter: only handle if targeted at this map\n if (id !== instanceId || desiredMode === state.mode) {\n return;\n }\n\n handleModeChangeRequest(\n instanceId,\n state,\n desiredMode,\n requestOwner,\n set,\n );\n });\n\n // Listen for authorization decisions\n const unsubDecision = mapModeBus.on(\n MapModeEvents.changeDecision,\n (event) => {\n const { id, approved, authId, owner } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== instanceId) {\n return;\n }\n\n handleAuthorizationDecision(\n instanceId,\n get(),\n { approved, authId, owner },\n set,\n );\n },\n );\n\n // Listen for mode changes to handle pending requests\n const unsubChanged = mapModeBus.on(MapModeEvents.changed, (event) => {\n const { currentMode, previousMode, id } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== instanceId) {\n return;\n }\n\n const state = get();\n // When mode owner changes to default mode, handle pending requests\n if (currentMode === DEFAULT_MODE && state.pendingRequests.size > 0) {\n handlePendingRequestsOnDefaultMode(\n instanceId,\n state,\n previousMode,\n set,\n );\n }\n });\n\n return () => {\n unsubRequest();\n unsubDecision();\n unsubChanged();\n };\n },\n});\n\n// =============================================================================\n// Convenience exports\n// =============================================================================\n\n/**\n * Get the current mode for a map instance\n */\nexport function getMode(mapId: UniqueId): string {\n return modeStore.get(mapId).mode;\n}\n\n/**\n * Hook for current mode value\n */\nexport function useMode(mapId: UniqueId): string {\n return modeStore.useSelector(mapId, (state) => state.mode);\n}\n\n/**\n * Get the owner of the current mode for a given map instance\n * @internal - For internal map-toolkit use only\n */\nexport function getCurrentModeOwner(instanceId: UniqueId): string | undefined {\n const state = modeStore.get(instanceId);\n return state.modeOwners.get(state.mode);\n}\n\n/**\n * Check if a given owner is registered as the owner of any mode.\n * This includes both active mode owners and pending mode requests.\n * @internal - For internal map-toolkit use only\n */\nexport function isRegisteredModeOwner(\n instanceId: UniqueId,\n owner: string,\n): boolean {\n const state = modeStore.get(instanceId);\n\n // Check active mode owners\n for (const modeOwner of state.modeOwners.values()) {\n if (modeOwner === owner) {\n return true;\n }\n }\n\n // Check pending mode requests (owner is the key in pendingRequests map)\n if (state.pendingRequests.has(owner)) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Manually clear map mode state for a specific instanceId.\n */\nexport function clearMapModeState(instanceId: UniqueId): void {\n modeStore.clear(instanceId);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-map-mode.js","names":[],"sources":["../../src/map-mode/use-map-mode.ts"],"sourcesContent":["/*\n * Copyright
|
|
1
|
+
{"version":3,"file":"use-map-mode.js","names":[],"sources":["../../src/map-mode/use-map-mode.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 { useContext, useMemo } from 'react';\nimport { MapContext } from '../deckgl/base-map/provider';\nimport { modeStore } from './store';\nimport type { UniqueId } from '@accelint/core';\n\n/**\n * Return value for the useMapMode hook\n */\nexport type UseMapModeReturn = {\n /** The current active map mode */\n mode: string;\n /** Function to request a mode change with ownership */\n requestModeChange: (desiredMode: string, requestOwner: string) => void;\n};\n\n/**\n * Hook to access the map mode state and actions.\n *\n * This hook uses `useSyncExternalStore` to subscribe to map mode state changes,\n * providing concurrent-safe mode state updates. Uses a fan-out pattern where\n * a single bus listener per map instance notifies N React component subscribers.\n *\n * @param id - Optional map instance ID. If not provided, will use the ID from `MapContext`.\n * @returns The current map mode and requestModeChange function\n * @throws Error if no `id` is provided and hook is used outside of `MapProvider`\n *\n * @example\n * ```tsx\n * // Inside MapProvider (within BaseMap children) - uses context\n * // Only Deck.gl layer components can be children\n * function CustomDeckLayer() {\n * const { mode, requestModeChange } = useMapMode();\n *\n * const handleClick = (info: PickingInfo) => {\n * requestModeChange('editing', 'custom-layer-id');\n * };\n *\n * return <ScatterplotLayer onClick={handleClick} />;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Outside MapProvider - pass id directly\n * function ExternalControl({ mapId }: { mapId: UniqueId }) {\n * const { mode, requestModeChange } = useMapMode(mapId);\n *\n * return <button onClick={() => requestModeChange('default', 'external')}>\n * Reset to Default (current: {mode})\n * </button>;\n * }\n * ```\n */\nexport function useMapMode(id?: UniqueId): UseMapModeReturn {\n const contextId = useContext(MapContext);\n const actualId = id ?? contextId;\n\n if (!actualId) {\n throw new Error(\n 'useMapMode requires either an id parameter or to be used within a MapProvider',\n );\n }\n\n // Use selector to get just the mode string (primitive comparison works with useSyncExternalStore)\n const mode = modeStore.useSelector(actualId, (state) => state.mode);\n\n // Get actions separately (stable reference)\n const { requestModeChange } = modeStore.actions(actualId);\n\n // Memoize the return value to prevent unnecessary re-renders\n return useMemo(\n () => ({\n mode,\n requestModeChange,\n }),\n [mode, requestModeChange],\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEA,SAAgB,WAAW,IAAiC;CAC1D,MAAM,YAAY,WAAW,WAAW;CACxC,MAAM,WAAW,MAAM;AAEvB,KAAI,CAAC,SACH,OAAM,IAAI,MACR,gFACD;CAIH,MAAM,OAAO,UAAU,YAAY,WAAW,UAAU,MAAM,KAAK;CAGnE,MAAM,EAAE,sBAAsB,UAAU,QAAQ,SAAS;AAGzD,QAAO,eACE;EACL;EACA;EACD,GACD,CAAC,MAAM,kBAAkB,CAC1B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-maplibre.js","names":["MapLibre"],"sources":["../../../src/maplibre/hooks/use-maplibre.ts"],"sourcesContent":["/*\n * Copyright
|
|
1
|
+
{"version":3,"file":"use-maplibre.js","names":["MapLibre"],"sources":["../../../src/maplibre/hooks/use-maplibre.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 { type IControl, Map as MapLibre, type MapOptions } from 'maplibre-gl';\nimport { useEffect, useRef } from 'react';\n\n/**\n * Hook to integrate a MapLibre GL map with a Deck.gl instance.\n *\n * This hook manages the lifecycle of a MapLibre map, including initialization,\n * style updates, and cleanup. It ensures the Deck.gl control is properly added\n * to the map and handles cleanup when the component unmounts.\n *\n * @param deck - The Deck.gl IControl instance to add to the map\n * @param styleUrl - The MapLibre style URL to use for the map\n * @param options - MapLibre map options (container, center, zoom, etc.)\n * @returns A ref containing the MapLibre map instance (ref.current may be null before initialization)\n *\n * @example\n * ```tsx\n * function MapComponent() {\n * const deckglInstance = useDeckgl();\n * const container = useId();\n *\n * const mapOptions = useMemo(() => ({\n * container,\n * center: [-122.4, 37.8],\n * zoom: 12,\n * }), [container]);\n *\n * useMapLibre(\n * deckglInstance as IControl,\n * 'https://tiles.example.com/style.json',\n * mapOptions\n * );\n *\n * return <div id={container} />;\n * }\n * ```\n */\nexport function useMapLibre(\n deck: IControl | null,\n styleUrl: string,\n options: MapOptions,\n) {\n const mapRef = useRef<MapLibre | null>(null);\n // Using a ref for options to avoid re-creating the map when options object reference changes\n // The map is only created once on mount, options changes after that are ignored\n const optionsRef = useRef(options);\n // using a ref in the initial setup so that it doesn't cause a re-run of the effect on change\n const styleRef = useRef(styleUrl);\n\n // Initialize MapLibre instance once\n useEffect(() => {\n if (deck && !mapRef.current) {\n mapRef.current = new MapLibre({\n ...optionsRef.current,\n style: styleRef.current,\n });\n\n mapRef.current.once('style.load', () => {\n mapRef.current?.setProjection({ type: 'mercator' });\n mapRef.current?.addControl(deck);\n });\n\n return () => {\n if (mapRef.current) {\n mapRef.current.removeControl(deck);\n mapRef.current.remove();\n mapRef.current = null;\n }\n };\n }\n }, [deck]);\n\n // Update style when it changes\n useEffect(() => {\n if (mapRef.current) {\n mapRef.current.setStyle(styleUrl);\n }\n }, [styleUrl]);\n\n return mapRef;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,SAAgB,YACd,MACA,UACA,SACA;CACA,MAAM,SAAS,OAAwB,KAAK;CAG5C,MAAM,aAAa,OAAO,QAAQ;CAElC,MAAM,WAAW,OAAO,SAAS;AAGjC,iBAAgB;AACd,MAAI,QAAQ,CAAC,OAAO,SAAS;AAC3B,UAAO,UAAU,IAAIA,IAAS;IAC5B,GAAG,WAAW;IACd,OAAO,SAAS;IACjB,CAAC;AAEF,UAAO,QAAQ,KAAK,oBAAoB;AACtC,WAAO,SAAS,cAAc,EAAE,MAAM,YAAY,CAAC;AACnD,WAAO,SAAS,WAAW,KAAK;KAChC;AAEF,gBAAa;AACX,QAAI,OAAO,SAAS;AAClB,YAAO,QAAQ,cAAc,KAAK;AAClC,YAAO,QAAQ,QAAQ;AACvB,YAAO,UAAU;;;;IAItB,CAAC,KAAK,CAAC;AAGV,iBAAgB;AACd,MAAI,OAAO,QACT,QAAO,QAAQ,SAAS,SAAS;IAElC,CAAC,SAAS,CAAC;AAEd,QAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","names":[],"sources":["../../src/shared/constants.ts"],"sourcesContent":["/*\n * Copyright
|
|
1
|
+
{"version":3,"file":"constants.js","names":[],"sources":["../../src/shared/constants.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\n/**\n * Default view state for map initialization.\n * Used by both BaseMap (deck.gl) and MapLibre components.\n *\n * Centered on Washington D.C. at zoom level 4 (continental US view).\n */\nexport const DEFAULT_VIEW_STATE = {\n longitude: -77.0369,\n latitude: 38.9072,\n zoom: 4,\n minZoom: 1,\n maxZoom: 22,\n pitch: 0,\n bearing: 0,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAkBA,MAAa,qBAAqB;CAChC,WAAW;CACX,UAAU;CACV,MAAM;CACN,SAAS;CACT,SAAS;CACT,OAAO;CACP,SAAS;CACV"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-map-store.js","names":[],"sources":["../../src/shared/create-map-store.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\nimport { useRef, useSyncExternalStore } from 'react';\nimport type { UniqueId } from '@accelint/core';\n\n// =============================================================================\n// Immutable Map Helpers\n// =============================================================================\n\n/**\n * Create a new Map with an entry added or updated (immutable).\n *\n * @example\n * ```ts\n * const newMap = mapSet(state.cursorOwners, 'draw-layer', 'crosshair');\n * set({ cursorOwners: newMap });\n * ```\n */\nexport function mapSet<K, V>(map: Map<K, V>, key: K, value: V): Map<K, V> {\n const newMap = new Map(map);\n newMap.set(key, value);\n return newMap;\n}\n\n/**\n * Create a new Map with an entry removed (immutable).\n *\n * @example\n * ```ts\n * const newMap = mapDelete(state.cursorOwners, 'draw-layer');\n * set({ cursorOwners: newMap });\n * ```\n */\nexport function mapDelete<K, V>(map: Map<K, V>, key: K): Map<K, V> {\n const newMap = new Map(map);\n newMap.delete(key);\n return newMap;\n}\n\n/**\n * Create a new empty Map (immutable replacement for Map.clear()).\n *\n * @example\n * ```ts\n * set({ pendingRequests: mapClear<string, PendingRequest>() });\n * ```\n */\nexport function mapClear<K, V>(): Map<K, V> {\n return new Map<K, V>();\n}\n\n/**\n * Helper methods passed to action creators and bus setup functions.\n *\n * This type is exported for consumers building custom store extensions or\n * helper functions that need to interact with store state.\n *\n * @example\n * ```ts\n * import type { StoreHelpers } from '@accelint/map-toolkit/shared';\n *\n * function createCustomAction<T>(helpers: StoreHelpers<T>) {\n * return () => {\n * const current = helpers.get();\n * helpers.set({ ...current, modified: true });\n * };\n * }\n * ```\n */\nexport type StoreHelpers<TState> = {\n /** Get current state */\n get: () => TState;\n /** Update state (partial merge) and notify subscribers */\n set: (updates: Partial<TState>) => void;\n /** Replace entire state and notify subscribers */\n replace: (state: TState) => void;\n /** Notify subscribers without changing state */\n notify: () => void;\n};\n\n/**\n * Configuration for creating a map store\n */\nexport type MapStoreConfig<TState, TActions> = {\n /** Default state for new instances and SSR */\n defaultState: TState;\n\n /**\n * Action creators - receives mapId and helpers, returns action methods.\n * Actions are cached per mapId for referential stability.\n */\n actions: (mapId: UniqueId, helpers: StoreHelpers<TState>) => TActions;\n\n /**\n * Optional bus listener setup. Called when first subscriber mounts.\n * Return cleanup function to unsubscribe.\n */\n bus?: (mapId: UniqueId, helpers: StoreHelpers<TState>) => () => void;\n\n /**\n * Optional cleanup when instance is destroyed (last subscriber unmounts).\n */\n onCleanup?: (mapId: UniqueId, state: TState) => void;\n};\n\n/**\n * Instance data for a single map\n */\ntype Instance<TState, TActions> = {\n state: TState;\n actions?: TActions;\n subscribers: Set<() => void>;\n busCleanup?: () => void;\n};\n\n/**\n * The store object returned by createMapStore\n */\nexport type MapStore<TState, TActions> = {\n /**\n * React hook - the primary way to use the store.\n * Returns state and actions with proper memoization.\n */\n use: (mapId: UniqueId) => { state: TState } & TActions;\n\n /**\n * React hook with selector for derived state.\n * Only re-renders when the underlying state changes.\n *\n * The selector result is memoized - it only recomputes when the **state reference**\n * changes, not on every render or when the selector function changes. This means:\n *\n * - Selectors that create new objects/arrays are safe without additional memoization\n * - Changing the selector function does NOT trigger recomputation (by design)\n * - This prevents infinite re-render loops when using inline arrow functions\n *\n * **Important**: The selector function is intentionally NOT tracked as a dependency.\n * If you need the selector to change dynamically, extract the changing value as a\n * separate dependency and use it within a stable selector, or use the `use()` hook\n * with your own `useMemo` for derived state.\n *\n * @example\n * ```ts\n * // Returns primitive - recomputes when state.count changes\n * const count = store.useSelector(mapId, (s) => s.count);\n *\n * // Returns existing reference - recomputes when state.items ref changes\n * const items = store.useSelector(mapId, (s) => s.items);\n *\n * // Safe: derived object is memoized internally, no infinite loops\n * const derived = store.useSelector(mapId, (s) => ({ doubled: s.count * 2 }));\n *\n * // If you need dynamic selector behavior, use the base hook instead:\n * const { state } = store.use(mapId);\n * const filtered = useMemo(() => filterFn(state.items), [state.items, filterFn]);\n * ```\n */\n useSelector: <TSelected>(\n mapId: UniqueId,\n selector: (state: TState) => TSelected,\n ) => TSelected;\n\n /**\n * Get actions without subscribing to state changes.\n * Useful for event handlers or effects.\n */\n actions: (mapId: UniqueId) => TActions;\n\n /**\n * Get current state (non-reactive, for imperative code).\n */\n get: (mapId: UniqueId) => TState;\n\n /**\n * Update state directly (usually prefer actions).\n */\n set: (mapId: UniqueId, updates: Partial<TState>) => void;\n\n /**\n * Check if instance exists (has been initialized).\n */\n exists: (mapId: UniqueId) => boolean;\n\n /**\n * Clear instance state (for tests or manual cleanup).\n */\n clear: (mapId: UniqueId) => void;\n\n /**\n * Low-level access for custom hooks or useSyncExternalStore.\n */\n subscribe: (mapId: UniqueId) => (callback: () => void) => () => void;\n snapshot: (mapId: UniqueId) => () => TState;\n serverSnapshot: () => TState;\n};\n\n/**\n * Creates a store for managing state across multiple map instances.\n *\n * @example\n * ```ts\n * const cursorStore = createMapStore({\n * defaultState: { cursor: 'default', owner: null },\n *\n * actions: (mapId, { get, set }) => ({\n * setCursor: (cursor: string, owner: string) => {\n * set({ cursor, owner });\n * },\n * clearCursor: () => {\n * set({ cursor: 'default', owner: null });\n * },\n * }),\n *\n * bus: (mapId, { set }) => {\n * return cursorBus.on(CursorEvents.change, (e) => {\n * if (e.payload.id === mapId) {\n * set({ cursor: e.payload.cursor });\n * }\n * });\n * },\n * });\n *\n * // In component:\n * function CursorDisplay({ mapId }) {\n * const { state, setCursor } = cursorStore.use(mapId);\n * return <div style={{ cursor: state.cursor }} />;\n * }\n * ```\n */\nexport function createMapStore<TState, TActions>(\n config: MapStoreConfig<TState, TActions>,\n): MapStore<TState, TActions> {\n const { defaultState, actions: createActions, bus, onCleanup } = config;\n\n const instances = new Map<UniqueId, Instance<TState, TActions>>();\n\n // Cached functions for referential stability\n const subscriptionCache = new Map<\n UniqueId,\n (callback: () => void) => () => void\n >();\n const snapshotCache = new Map<UniqueId, () => TState>();\n\n function getInstance(mapId: UniqueId): Instance<TState, TActions> {\n let instance = instances.get(mapId);\n if (!instance) {\n instance = {\n state: { ...defaultState },\n subscribers: new Set(),\n };\n instances.set(mapId, instance);\n }\n return instance;\n }\n\n function notify(mapId: UniqueId): void {\n const instance = instances.get(mapId);\n if (instance) {\n for (const callback of instance.subscribers) {\n callback();\n }\n }\n }\n\n function getHelpers(mapId: UniqueId): StoreHelpers<TState> {\n return {\n get: () => getInstance(mapId).state,\n set: (updates) => {\n const instance = getInstance(mapId);\n instance.state = { ...instance.state, ...updates };\n notify(mapId);\n },\n replace: (state) => {\n const instance = getInstance(mapId);\n instance.state = state;\n notify(mapId);\n },\n notify: () => notify(mapId),\n };\n }\n\n function getActions(mapId: UniqueId): TActions {\n const instance = getInstance(mapId);\n if (!instance.actions) {\n instance.actions = createActions(mapId, getHelpers(mapId));\n }\n return instance.actions;\n }\n\n /**\n * Clean up instance when last subscriber unmounts\n */\n function cleanupInstance(\n mapId: UniqueId,\n instance: Instance<TState, TActions>,\n ): void {\n if (onCleanup) {\n onCleanup(mapId, instance.state);\n }\n if (instance.busCleanup) {\n instance.busCleanup();\n }\n instances.delete(mapId);\n subscriptionCache.delete(mapId);\n snapshotCache.delete(mapId);\n }\n\n function subscribe(mapId: UniqueId): (callback: () => void) => () => void {\n let cached = subscriptionCache.get(mapId);\n if (!cached) {\n cached = (callback: () => void) => {\n const instance = getInstance(mapId);\n\n // Setup bus on first subscriber\n if (instance.subscribers.size === 0 && bus) {\n instance.busCleanup = bus(mapId, getHelpers(mapId));\n }\n\n instance.subscribers.add(callback);\n\n return () => {\n instance.subscribers.delete(callback);\n\n // Cleanup when last subscriber unmounts\n if (instance.subscribers.size === 0) {\n cleanupInstance(mapId, instance);\n }\n };\n };\n subscriptionCache.set(mapId, cached);\n }\n return cached;\n }\n\n function snapshot(mapId: UniqueId): () => TState {\n let cached = snapshotCache.get(mapId);\n if (!cached) {\n cached = () => {\n // State is already a new object reference when updated via set()\n // which creates { ...instance.state, ...updates }\n return getInstance(mapId).state;\n };\n snapshotCache.set(mapId, cached);\n }\n return cached;\n }\n\n function serverSnapshot(): TState {\n return defaultState;\n }\n\n /**\n * Main hook - returns state and actions\n */\n function use(mapId: UniqueId): { state: TState } & TActions {\n const state = useSyncExternalStore(\n subscribe(mapId),\n snapshot(mapId),\n serverSnapshot,\n );\n\n const actions = getActions(mapId);\n\n // Return merged object with state wrapper for clarity\n return { state, ...actions };\n }\n\n /**\n * Selector hook - only re-renders when selected value changes.\n *\n * Note: The selector function is intentionally NOT tracked as a dependency.\n * This prevents infinite re-render loops when using inline arrow functions.\n * If you need dynamic selector behavior, use the `use()` hook with `useMemo`.\n */\n function useSelector<TSelected>(\n mapId: UniqueId,\n selector: (state: TState) => TSelected,\n ): TSelected {\n // Cache the previous state and selected value to avoid unnecessary re-computation.\n // We intentionally do NOT track selector changes - only state changes trigger\n // recomputation. This prevents infinite loops with inline selectors.\n const cache = useRef<{ state: TState; selected: TSelected } | null>(null);\n\n const state = useSyncExternalStore(\n subscribe(mapId),\n snapshot(mapId),\n serverSnapshot,\n );\n\n // Only recompute if state reference changed (selector changes are ignored)\n if (cache.current === null || cache.current.state !== state) {\n cache.current = {\n state,\n selected: selector(state),\n };\n }\n\n return cache.current.selected;\n }\n\n return {\n use,\n useSelector,\n actions: getActions,\n get: (mapId) => getInstance(mapId).state,\n set: (mapId, updates) => {\n const instance = getInstance(mapId);\n instance.state = { ...instance.state, ...updates };\n notify(mapId);\n },\n exists: (mapId) => instances.has(mapId),\n clear: (mapId) => {\n const instance = instances.get(mapId);\n if (instance) {\n cleanupInstance(mapId, instance);\n }\n },\n subscribe,\n snapshot,\n serverSnapshot,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,OAAa,KAAgB,KAAQ,OAAqB;CACxE,MAAM,SAAS,IAAI,IAAI,IAAI;AAC3B,QAAO,IAAI,KAAK,MAAM;AACtB,QAAO;;;;;;;;;;;AAYT,SAAgB,UAAgB,KAAgB,KAAmB;CACjE,MAAM,SAAS,IAAI,IAAI,IAAI;AAC3B,QAAO,OAAO,IAAI;AAClB,QAAO;;;;;;;;;;AAWT,SAAgB,WAA4B;AAC1C,wBAAO,IAAI,KAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqLxB,SAAgB,eACd,QAC4B;CAC5B,MAAM,EAAE,cAAc,SAAS,eAAe,KAAK,cAAc;CAEjE,MAAM,4BAAY,IAAI,KAA2C;CAGjE,MAAM,oCAAoB,IAAI,KAG3B;CACH,MAAM,gCAAgB,IAAI,KAA6B;CAEvD,SAAS,YAAY,OAA6C;EAChE,IAAI,WAAW,UAAU,IAAI,MAAM;AACnC,MAAI,CAAC,UAAU;AACb,cAAW;IACT,OAAO,EAAE,GAAG,cAAc;IAC1B,6BAAa,IAAI,KAAK;IACvB;AACD,aAAU,IAAI,OAAO,SAAS;;AAEhC,SAAO;;CAGT,SAAS,OAAO,OAAuB;EACrC,MAAM,WAAW,UAAU,IAAI,MAAM;AACrC,MAAI,SACF,MAAK,MAAM,YAAY,SAAS,YAC9B,WAAU;;CAKhB,SAAS,WAAW,OAAuC;AACzD,SAAO;GACL,WAAW,YAAY,MAAM,CAAC;GAC9B,MAAM,YAAY;IAChB,MAAM,WAAW,YAAY,MAAM;AACnC,aAAS,QAAQ;KAAE,GAAG,SAAS;KAAO,GAAG;KAAS;AAClD,WAAO,MAAM;;GAEf,UAAU,UAAU;IAClB,MAAM,WAAW,YAAY,MAAM;AACnC,aAAS,QAAQ;AACjB,WAAO,MAAM;;GAEf,cAAc,OAAO,MAAM;GAC5B;;CAGH,SAAS,WAAW,OAA2B;EAC7C,MAAM,WAAW,YAAY,MAAM;AACnC,MAAI,CAAC,SAAS,QACZ,UAAS,UAAU,cAAc,OAAO,WAAW,MAAM,CAAC;AAE5D,SAAO,SAAS;;;;;CAMlB,SAAS,gBACP,OACA,UACM;AACN,MAAI,UACF,WAAU,OAAO,SAAS,MAAM;AAElC,MAAI,SAAS,WACX,UAAS,YAAY;AAEvB,YAAU,OAAO,MAAM;AACvB,oBAAkB,OAAO,MAAM;AAC/B,gBAAc,OAAO,MAAM;;CAG7B,SAAS,UAAU,OAAuD;EACxE,IAAI,SAAS,kBAAkB,IAAI,MAAM;AACzC,MAAI,CAAC,QAAQ;AACX,aAAU,aAAyB;IACjC,MAAM,WAAW,YAAY,MAAM;AAGnC,QAAI,SAAS,YAAY,SAAS,KAAK,IACrC,UAAS,aAAa,IAAI,OAAO,WAAW,MAAM,CAAC;AAGrD,aAAS,YAAY,IAAI,SAAS;AAElC,iBAAa;AACX,cAAS,YAAY,OAAO,SAAS;AAGrC,SAAI,SAAS,YAAY,SAAS,EAChC,iBAAgB,OAAO,SAAS;;;AAItC,qBAAkB,IAAI,OAAO,OAAO;;AAEtC,SAAO;;CAGT,SAAS,SAAS,OAA+B;EAC/C,IAAI,SAAS,cAAc,IAAI,MAAM;AACrC,MAAI,CAAC,QAAQ;AACX,kBAAe;AAGb,WAAO,YAAY,MAAM,CAAC;;AAE5B,iBAAc,IAAI,OAAO,OAAO;;AAElC,SAAO;;CAGT,SAAS,iBAAyB;AAChC,SAAO;;;;;CAMT,SAAS,IAAI,OAA+C;AAU1D,SAAO;GAAE,OATK,qBACZ,UAAU,MAAM,EAChB,SAAS,MAAM,EACf,eACD;GAKe,GAHA,WAAW,MAAM;GAGL;;;;;;;;;CAU9B,SAAS,YACP,OACA,UACW;EAIX,MAAM,QAAQ,OAAsD,KAAK;EAEzE,MAAM,QAAQ,qBACZ,UAAU,MAAM,EAChB,SAAS,MAAM,EACf,eACD;AAGD,MAAI,MAAM,YAAY,QAAQ,MAAM,QAAQ,UAAU,MACpD,OAAM,UAAU;GACd;GACA,UAAU,SAAS,MAAM;GAC1B;AAGH,SAAO,MAAM,QAAQ;;AAGvB,QAAO;EACL;EACA;EACA,SAAS;EACT,MAAM,UAAU,YAAY,MAAM,CAAC;EACnC,MAAM,OAAO,YAAY;GACvB,MAAM,WAAW,YAAY,MAAM;AACnC,YAAS,QAAQ;IAAE,GAAG,SAAS;IAAO,GAAG;IAAS;AAClD,UAAO,MAAM;;EAEf,SAAS,UAAU,UAAU,IAAI,MAAM;EACvC,QAAQ,UAAU;GAChB,MAAM,WAAW,UAAU,IAAI,MAAM;AACrC,OAAI,SACF,iBAAgB,OAAO,SAAS;;EAGpC;EACA;EACA;EACD"}
|
|
1
|
+
{"version":3,"file":"create-map-store.js","names":[],"sources":["../../src/shared/create-map-store.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 { useRef, useSyncExternalStore } from 'react';\nimport type { UniqueId } from '@accelint/core';\n\n// =============================================================================\n// Immutable Map Helpers\n// =============================================================================\n\n/**\n * Create a new Map with an entry added or updated (immutable).\n *\n * @example\n * ```ts\n * const newMap = mapSet(state.cursorOwners, 'draw-layer', 'crosshair');\n * set({ cursorOwners: newMap });\n * ```\n */\nexport function mapSet<K, V>(map: Map<K, V>, key: K, value: V): Map<K, V> {\n const newMap = new Map(map);\n newMap.set(key, value);\n return newMap;\n}\n\n/**\n * Create a new Map with an entry removed (immutable).\n *\n * @example\n * ```ts\n * const newMap = mapDelete(state.cursorOwners, 'draw-layer');\n * set({ cursorOwners: newMap });\n * ```\n */\nexport function mapDelete<K, V>(map: Map<K, V>, key: K): Map<K, V> {\n const newMap = new Map(map);\n newMap.delete(key);\n return newMap;\n}\n\n/**\n * Create a new empty Map (immutable replacement for Map.clear()).\n *\n * @example\n * ```ts\n * set({ pendingRequests: mapClear<string, PendingRequest>() });\n * ```\n */\nexport function mapClear<K, V>(): Map<K, V> {\n return new Map<K, V>();\n}\n\n/**\n * Helper methods passed to action creators and bus setup functions.\n *\n * This type is exported for consumers building custom store extensions or\n * helper functions that need to interact with store state.\n *\n * @example\n * ```ts\n * import type { StoreHelpers } from '@accelint/map-toolkit/shared';\n *\n * function createCustomAction<T>(helpers: StoreHelpers<T>) {\n * return () => {\n * const current = helpers.get();\n * helpers.set({ ...current, modified: true });\n * };\n * }\n * ```\n */\nexport type StoreHelpers<TState> = {\n /** Get current state */\n get: () => TState;\n /** Update state (partial merge) and notify subscribers */\n set: (updates: Partial<TState>) => void;\n /** Replace entire state and notify subscribers */\n replace: (state: TState) => void;\n /** Notify subscribers without changing state */\n notify: () => void;\n};\n\n/**\n * Configuration for creating a map store\n */\nexport type MapStoreConfig<TState, TActions> = {\n /** Default state for new instances and SSR */\n defaultState: TState;\n\n /**\n * Action creators - receives mapId and helpers, returns action methods.\n * Actions are cached per mapId for referential stability.\n */\n actions: (mapId: UniqueId, helpers: StoreHelpers<TState>) => TActions;\n\n /**\n * Optional bus listener setup. Called when first subscriber mounts.\n * Return cleanup function to unsubscribe.\n */\n bus?: (mapId: UniqueId, helpers: StoreHelpers<TState>) => () => void;\n\n /**\n * Optional cleanup when instance is destroyed (last subscriber unmounts).\n */\n onCleanup?: (mapId: UniqueId, state: TState) => void;\n};\n\n/**\n * Instance data for a single map\n */\ntype Instance<TState, TActions> = {\n state: TState;\n actions?: TActions;\n subscribers: Set<() => void>;\n busCleanup?: () => void;\n};\n\n/**\n * The store object returned by createMapStore\n */\nexport type MapStore<TState, TActions> = {\n /**\n * React hook - the primary way to use the store.\n * Returns state and actions with proper memoization.\n */\n use: (mapId: UniqueId) => { state: TState } & TActions;\n\n /**\n * React hook with selector for derived state.\n * Only re-renders when the underlying state changes.\n *\n * The selector result is memoized - it only recomputes when the **state reference**\n * changes, not on every render or when the selector function changes. This means:\n *\n * - Selectors that create new objects/arrays are safe without additional memoization\n * - Changing the selector function does NOT trigger recomputation (by design)\n * - This prevents infinite re-render loops when using inline arrow functions\n *\n * **Important**: The selector function is intentionally NOT tracked as a dependency.\n * If you need the selector to change dynamically, extract the changing value as a\n * separate dependency and use it within a stable selector, or use the `use()` hook\n * with your own `useMemo` for derived state.\n *\n * @example\n * ```ts\n * // Returns primitive - recomputes when state.count changes\n * const count = store.useSelector(mapId, (s) => s.count);\n *\n * // Returns existing reference - recomputes when state.items ref changes\n * const items = store.useSelector(mapId, (s) => s.items);\n *\n * // Safe: derived object is memoized internally, no infinite loops\n * const derived = store.useSelector(mapId, (s) => ({ doubled: s.count * 2 }));\n *\n * // If you need dynamic selector behavior, use the base hook instead:\n * const { state } = store.use(mapId);\n * const filtered = useMemo(() => filterFn(state.items), [state.items, filterFn]);\n * ```\n */\n useSelector: <TSelected>(\n mapId: UniqueId,\n selector: (state: TState) => TSelected,\n ) => TSelected;\n\n /**\n * Get actions without subscribing to state changes.\n * Useful for event handlers or effects.\n */\n actions: (mapId: UniqueId) => TActions;\n\n /**\n * Get current state (non-reactive, for imperative code).\n */\n get: (mapId: UniqueId) => TState;\n\n /**\n * Update state directly (usually prefer actions).\n */\n set: (mapId: UniqueId, updates: Partial<TState>) => void;\n\n /**\n * Check if instance exists (has been initialized).\n */\n exists: (mapId: UniqueId) => boolean;\n\n /**\n * Clear instance state (for tests or manual cleanup).\n */\n clear: (mapId: UniqueId) => void;\n\n /**\n * Low-level access for custom hooks or useSyncExternalStore.\n */\n subscribe: (mapId: UniqueId) => (callback: () => void) => () => void;\n snapshot: (mapId: UniqueId) => () => TState;\n serverSnapshot: () => TState;\n};\n\n/**\n * Creates a store for managing state across multiple map instances.\n *\n * @example\n * ```ts\n * const cursorStore = createMapStore({\n * defaultState: { cursor: 'default', owner: null },\n *\n * actions: (mapId, { get, set }) => ({\n * setCursor: (cursor: string, owner: string) => {\n * set({ cursor, owner });\n * },\n * clearCursor: () => {\n * set({ cursor: 'default', owner: null });\n * },\n * }),\n *\n * bus: (mapId, { set }) => {\n * return cursorBus.on(CursorEvents.change, (e) => {\n * if (e.payload.id === mapId) {\n * set({ cursor: e.payload.cursor });\n * }\n * });\n * },\n * });\n *\n * // In component:\n * function CursorDisplay({ mapId }) {\n * const { state, setCursor } = cursorStore.use(mapId);\n * return <div style={{ cursor: state.cursor }} />;\n * }\n * ```\n */\nexport function createMapStore<TState, TActions>(\n config: MapStoreConfig<TState, TActions>,\n): MapStore<TState, TActions> {\n const { defaultState, actions: createActions, bus, onCleanup } = config;\n\n const instances = new Map<UniqueId, Instance<TState, TActions>>();\n\n // Cached functions for referential stability\n const subscriptionCache = new Map<\n UniqueId,\n (callback: () => void) => () => void\n >();\n const snapshotCache = new Map<UniqueId, () => TState>();\n\n function getInstance(mapId: UniqueId): Instance<TState, TActions> {\n let instance = instances.get(mapId);\n if (!instance) {\n instance = {\n state: { ...defaultState },\n subscribers: new Set(),\n };\n instances.set(mapId, instance);\n }\n return instance;\n }\n\n function notify(mapId: UniqueId): void {\n const instance = instances.get(mapId);\n if (instance) {\n for (const callback of instance.subscribers) {\n callback();\n }\n }\n }\n\n function getHelpers(mapId: UniqueId): StoreHelpers<TState> {\n return {\n get: () => getInstance(mapId).state,\n set: (updates) => {\n const instance = getInstance(mapId);\n instance.state = { ...instance.state, ...updates };\n notify(mapId);\n },\n replace: (state) => {\n const instance = getInstance(mapId);\n instance.state = state;\n notify(mapId);\n },\n notify: () => notify(mapId),\n };\n }\n\n function getActions(mapId: UniqueId): TActions {\n const instance = getInstance(mapId);\n if (!instance.actions) {\n instance.actions = createActions(mapId, getHelpers(mapId));\n }\n return instance.actions;\n }\n\n /**\n * Clean up instance when last subscriber unmounts\n */\n function cleanupInstance(\n mapId: UniqueId,\n instance: Instance<TState, TActions>,\n ): void {\n if (onCleanup) {\n onCleanup(mapId, instance.state);\n }\n if (instance.busCleanup) {\n instance.busCleanup();\n }\n instances.delete(mapId);\n subscriptionCache.delete(mapId);\n snapshotCache.delete(mapId);\n }\n\n function subscribe(mapId: UniqueId): (callback: () => void) => () => void {\n let cached = subscriptionCache.get(mapId);\n if (!cached) {\n cached = (callback: () => void) => {\n const instance = getInstance(mapId);\n\n // Setup bus on first subscriber\n if (instance.subscribers.size === 0 && bus) {\n instance.busCleanup = bus(mapId, getHelpers(mapId));\n }\n\n instance.subscribers.add(callback);\n\n return () => {\n instance.subscribers.delete(callback);\n\n // Cleanup when last subscriber unmounts\n if (instance.subscribers.size === 0) {\n cleanupInstance(mapId, instance);\n }\n };\n };\n subscriptionCache.set(mapId, cached);\n }\n return cached;\n }\n\n function snapshot(mapId: UniqueId): () => TState {\n let cached = snapshotCache.get(mapId);\n if (!cached) {\n cached = () => {\n // State is already a new object reference when updated via set()\n // which creates { ...instance.state, ...updates }\n return getInstance(mapId).state;\n };\n snapshotCache.set(mapId, cached);\n }\n return cached;\n }\n\n function serverSnapshot(): TState {\n return defaultState;\n }\n\n /**\n * Main hook - returns state and actions\n */\n function use(mapId: UniqueId): { state: TState } & TActions {\n const state = useSyncExternalStore(\n subscribe(mapId),\n snapshot(mapId),\n serverSnapshot,\n );\n\n const actions = getActions(mapId);\n\n // Return merged object with state wrapper for clarity\n return { state, ...actions };\n }\n\n /**\n * Selector hook - only re-renders when selected value changes.\n *\n * Note: The selector function is intentionally NOT tracked as a dependency.\n * This prevents infinite re-render loops when using inline arrow functions.\n * If you need dynamic selector behavior, use the `use()` hook with `useMemo`.\n */\n function useSelector<TSelected>(\n mapId: UniqueId,\n selector: (state: TState) => TSelected,\n ): TSelected {\n // Cache the previous state and selected value to avoid unnecessary re-computation.\n // We intentionally do NOT track selector changes - only state changes trigger\n // recomputation. This prevents infinite loops with inline selectors.\n const cache = useRef<{ state: TState; selected: TSelected } | null>(null);\n\n const state = useSyncExternalStore(\n subscribe(mapId),\n snapshot(mapId),\n serverSnapshot,\n );\n\n // Only recompute if state reference changed (selector changes are ignored)\n if (cache.current === null || cache.current.state !== state) {\n cache.current = {\n state,\n selected: selector(state),\n };\n }\n\n return cache.current.selected;\n }\n\n return {\n use,\n useSelector,\n actions: getActions,\n get: (mapId) => getInstance(mapId).state,\n set: (mapId, updates) => {\n const instance = getInstance(mapId);\n instance.state = { ...instance.state, ...updates };\n notify(mapId);\n },\n exists: (mapId) => instances.has(mapId),\n clear: (mapId) => {\n const instance = instances.get(mapId);\n if (instance) {\n cleanupInstance(mapId, instance);\n }\n },\n subscribe,\n snapshot,\n serverSnapshot,\n };\n}\n"],"mappings":"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"}
|
package/dist/shared/units.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"units.js","names":["DEFAULT_DISTANCE_UNITS: DistanceUnit"],"sources":["../../src/shared/units.ts"],"sourcesContent":["/*\n * Copyright
|
|
1
|
+
{"version":3,"file":"units.js","names":["DEFAULT_DISTANCE_UNITS: DistanceUnit"],"sources":["../../src/shared/units.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\n/**\n * Map of Turf.js unit names to display abbreviations.\n * These are the practical units users would select for map measurements.\n */\nexport const DISTANCE_UNIT_ABBREVIATIONS = {\n kilometers: 'km',\n meters: 'm',\n nauticalmiles: 'nm',\n miles: 'mi',\n feet: 'ft',\n} as const;\n\n/**\n * Turf.js unit name (e.g., 'kilometers', 'nauticalmiles').\n */\nexport type DistanceUnit = keyof typeof DISTANCE_UNIT_ABBREVIATIONS;\n\n/**\n * Display abbreviation (e.g., 'km', 'nm').\n */\nexport type DistanceUnitAbbreviation =\n (typeof DISTANCE_UNIT_ABBREVIATIONS)[DistanceUnit];\n\n/**\n * Default distance units for geographic measurements.\n */\nexport const DEFAULT_DISTANCE_UNITS: DistanceUnit = 'kilometers';\n\n/**\n * Get the full Turf.js unit name from an abbreviation.\n * @param abbrev - The abbreviation (e.g., 'km', 'nm')\n * @returns The full unit name (e.g., 'kilometers', 'nauticalmiles') or undefined\n */\nexport function getDistanceUnitFromAbbreviation(\n abbrev: string,\n): DistanceUnit | undefined {\n const entry = Object.entries(DISTANCE_UNIT_ABBREVIATIONS).find(\n ([, a]) => a === abbrev,\n );\n return entry?.[0] as DistanceUnit | undefined;\n}\n\n/**\n * Get the abbreviation for a Turf.js unit name.\n * @param unit - The full unit name (e.g., 'kilometers')\n * @returns The abbreviation (e.g., 'km') or the input if not found\n */\nexport function getDistanceUnitAbbreviation(unit: string): string {\n return DISTANCE_UNIT_ABBREVIATIONS[unit as DistanceUnit] ?? unit;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAgBA,MAAa,8BAA8B;CACzC,YAAY;CACZ,QAAQ;CACR,eAAe;CACf,OAAO;CACP,MAAM;CACP;;;;AAgBD,MAAaA,yBAAuC;;;;;;AAOpD,SAAgB,gCACd,QAC0B;AAI1B,QAHc,OAAO,QAAQ,4BAA4B,CAAC,MACvD,GAAG,OAAO,MAAM,OAClB,GACc;;;;;;;AAQjB,SAAgB,4BAA4B,MAAsB;AAChE,QAAO,4BAA4B,SAAyB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","names":[],"sources":["../../src/viewport/store.ts"],"sourcesContent":["/*\n * Copyright
|
|
1
|
+
{"version":3,"file":"store.js","names":[],"sources":["../../src/viewport/store.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\n/**\n * Viewport Store\n *\n * Manages viewport state (bounds, lat/lon, zoom, dimensions) per map instance.\n * State is updated automatically from MapEvents.viewport bus events.\n *\n * @example\n * ```tsx\n * import { viewportStore } from '@accelint/map-toolkit/viewport';\n *\n * function MapInfo({ mapId }) {\n * const { state } = viewportStore.use(mapId);\n * return (\n * <div>\n * Lat: {state.latitude.toFixed(2)}, Lon: {state.longitude.toFixed(2)}\n * </div>\n * );\n * }\n * ```\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { MapEvents } from '../deckgl/base-map/events';\nimport { createMapStore } from '../shared/create-map-store';\nimport type { UniqueId } from '@accelint/core';\nimport type {\n MapEventType,\n MapViewportPayload,\n} from '../deckgl/base-map/types';\n\nconst bus = Broadcast.getInstance<MapEventType>();\n\n/**\n * State shape for viewport - mirrors MapViewportPayload but with\n * a placeholder id that gets replaced per-instance\n */\ntype ViewportState = MapViewportPayload;\n\n/**\n * No actions needed - viewport state is read-only from bus events\n */\ntype ViewportActions = Record<string, never>;\n\n/**\n * Create default state for a given mapId.\n * Returns uninitialized values that indicate no viewport data yet.\n */\nfunction createDefaultState(mapId: UniqueId): ViewportState {\n return {\n id: mapId,\n bounds: undefined,\n latitude: Number.NaN,\n longitude: Number.NaN,\n zoom: Number.NaN,\n width: 0,\n height: 0,\n };\n}\n\n/**\n * Viewport store instance.\n *\n * Note: The defaultState uses a placeholder id that gets replaced\n * when instances are created. The actual id comes from the bus event payload.\n */\nexport const viewportStore = createMapStore<ViewportState, ViewportActions>({\n // Placeholder default state - actual instances get proper id from bus events\n defaultState: {\n id: '' as UniqueId,\n bounds: undefined,\n latitude: Number.NaN,\n longitude: Number.NaN,\n zoom: Number.NaN,\n width: 0,\n height: 0,\n },\n\n // No actions - state is read-only from bus events\n actions: () => ({}) as ViewportActions,\n\n bus: (mapId, { replace }) => {\n const unsub = bus.on(MapEvents.viewport, ({ payload }) => {\n if (payload.id === mapId) {\n // Replace entire state with the new payload\n replace(payload);\n }\n });\n\n return unsub;\n },\n});\n\n// =============================================================================\n// Convenience exports\n// =============================================================================\n\n/**\n * Hook to subscribe to viewport state changes for a specific map.\n *\n * @param mapId - Unique identifier for the map instance\n * @returns Current viewport state including bounds, latitude, longitude, zoom, width, height\n *\n * @example\n * ```tsx\n * function MapInfo({ mapId }) {\n * const viewport = useMapViewport(mapId);\n * return (\n * <div>\n * Lat: {viewport.latitude?.toFixed(2)}, Lon: {viewport.longitude?.toFixed(2)}, Zoom: {viewport.zoom}\n * </div>\n * );\n * }\n * ```\n */\nexport function useMapViewport(mapId: UniqueId): ViewportState {\n return viewportStore.useSelector(mapId, (state) => {\n // Return proper default state with correct mapId if uninitialized\n if (state.id === '') {\n return createDefaultState(mapId);\n }\n return state;\n });\n}\n\n/**\n * Get current viewport state (non-reactive, for imperative code).\n *\n * @param mapId - Unique identifier for the map instance\n * @returns Current viewport state\n */\nexport function getViewport(mapId: UniqueId): ViewportState {\n const state = viewportStore.get(mapId);\n // Return proper default state with correct mapId if uninitialized\n if (state.id === '') {\n return createDefaultState(mapId);\n }\n return state;\n}\n\n/**\n * Manually clear viewport state for a specific map instance.\n * This is typically not needed as cleanup happens automatically when all subscribers unmount.\n *\n * @param mapId - The unique identifier for the map instance to clear\n */\nexport function clearViewportState(mapId: UniqueId): void {\n viewportStore.clear(mapId);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAM,MAAM,UAAU,aAA2B;;;;;AAiBjD,SAAS,mBAAmB,OAAgC;AAC1D,QAAO;EACL,IAAI;EACJ,QAAQ;EACR,UAAU;EACV,WAAW;EACX,MAAM;EACN,OAAO;EACP,QAAQ;EACT;;;;;;;;AASH,MAAa,gBAAgB,eAA+C;CAE1E,cAAc;EACZ,IAAI;EACJ,QAAQ;EACR,UAAU;EACV,WAAW;EACX,MAAM;EACN,OAAO;EACP,QAAQ;EACT;CAGD,gBAAgB,EAAE;CAElB,MAAM,OAAO,EAAE,cAAc;AAQ3B,SAPc,IAAI,GAAG,UAAU,WAAW,EAAE,cAAc;AACxD,OAAI,QAAQ,OAAO,MAEjB,SAAQ,QAAQ;IAElB;;CAIL,CAAC;;;;;;;;;;;;;;;;;;;AAwBF,SAAgB,eAAe,OAAgC;AAC7D,QAAO,cAAc,YAAY,QAAQ,UAAU;AAEjD,MAAI,MAAM,OAAO,GACf,QAAO,mBAAmB,MAAM;AAElC,SAAO;GACP;;;;;;;;AASJ,SAAgB,YAAY,OAAgC;CAC1D,MAAM,QAAQ,cAAc,IAAI,MAAM;AAEtC,KAAI,MAAM,OAAO,GACf,QAAO,mBAAmB,MAAM;AAElC,QAAO;;;;;;;;AAST,SAAgB,mBAAmB,OAAuB;AACxD,eAAc,MAAM,MAAM"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","names":[],"sources":["../../src/viewport/utils.ts"],"sourcesContent":["/*\n * Copyright
|
|
1
|
+
{"version":3,"file":"utils.js","names":[],"sources":["../../src/viewport/utils.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 DistanceUnit,\n getDistanceUnitFromAbbreviation,\n} from '../shared/units';\nimport type { GetViewportSizeArgs } from './types';\n\nconst numberFormatter = Intl.NumberFormat('en-US');\n\n/**\n * Web Mercator constant: meters per pixel at zoom 0, equator.\n * This is Earth's circumference (40075016.686m) divided by 256 (tile size).\n */\nconst METERS_PER_PIXEL_AT_ZOOM_0 = 156543.03392;\n\n/**\n * Unit conversion factors from meters.\n */\nconst METERS_TO_UNIT = {\n kilometers: 0.001,\n meters: 1,\n nauticalmiles: 0.000539957,\n miles: 0.000621371,\n feet: 3.28084,\n} as const;\n\n/**\n * Returns a formatted viewport size string i.e. `660 x 1,801 NM`\n *\n * Calculates the geographic distance of the viewport using zoom level and\n * pixel dimensions with the Web Mercator projection formula. This approach\n * provides stable results without the edge cases of bounds-based calculations.\n *\n * @param args - Viewport size calculation arguments\n * @param args.bounds - Geographic bounds [minLon, minLat, maxLon, maxLat]\n * @param args.zoom - Zoom level for meters-per-pixel calculation\n * @param args.width - Viewport width in pixels\n * @param args.height - Viewport height in pixels\n * @param args.unit - Unit of distance measurement: `km | m | nm | mi | ft`. Defaults to `nm`\n * @param args.formatter - Number formatter for localization (defaults to en-US)\n * @returns Formatted string like \"660 x 1,801 NM\" or \"-- x -- NM\" if invalid\n *\n * @example\n * ```typescript\n * getViewportSize({ bounds: [-82, 22, -71, 52], zoom: 5, width: 800, height: 600, unit: 'nm' })\n * // returns \"612 x 459 NM\"\n *\n * getViewportSize({ bounds: [170, 50, -170, 60], zoom: 4, width: 1024, height: 768, unit: 'km' })\n * // returns \"2,050 x 1,538 KM\"\n * ```\n */\nexport function getViewportSize({\n bounds,\n zoom,\n width: pixelWidth,\n height: pixelHeight,\n unit = 'nm',\n formatter = numberFormatter,\n}: GetViewportSizeArgs) {\n const defaultValue = `-- x -- ${unit.toUpperCase()}`;\n\n // Validate inputs\n if (!bounds || bounds.every((b) => Number.isNaN(b))) {\n return defaultValue;\n }\n\n if (Number.isNaN(zoom) || pixelWidth === 0 || pixelHeight === 0) {\n return defaultValue;\n }\n\n const [, minLat, , maxLat] = bounds;\n\n // Validate latitude bounds are within valid geographic ranges\n if (minLat < -90 || minLat > 90 || maxLat < -90 || maxLat > 90) {\n return defaultValue;\n }\n\n // Calculate center latitude for the viewport\n const centerLat = (minLat + maxLat) / 2;\n\n // Web Mercator formula: meters per pixel at given zoom and latitude\n // Resolution = 156543.03392 * cos(latitude * π/180) / 2^zoom\n const metersPerPixel =\n (METERS_PER_PIXEL_AT_ZOOM_0 * Math.cos((centerLat * Math.PI) / 180)) /\n 2 ** zoom;\n\n // Calculate distances in meters\n const widthMeters = pixelWidth * metersPerPixel;\n const heightMeters = pixelHeight * metersPerPixel;\n\n // Convert to requested unit\n const unitKey = getDistanceUnitFromAbbreviation(unit) as DistanceUnit;\n const conversionFactor = METERS_TO_UNIT[unitKey];\n\n const widthDistance = Math.round(widthMeters * conversionFactor);\n const heightDistance = Math.round(heightMeters * conversionFactor);\n\n const width = formatter.format(widthDistance);\n const height = formatter.format(heightDistance);\n\n return `${width} x ${height} ${unit.toUpperCase()}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,MAAM,kBAAkB,KAAK,aAAa,QAAQ;;;;;AAMlD,MAAM,6BAA6B;;;;AAKnC,MAAM,iBAAiB;CACrB,YAAY;CACZ,QAAQ;CACR,eAAe;CACf,OAAO;CACP,MAAM;CACP;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BD,SAAgB,gBAAgB,EAC9B,QACA,MACA,OAAO,YACP,QAAQ,aACR,OAAO,MACP,YAAY,mBACU;CACtB,MAAM,eAAe,WAAW,KAAK,aAAa;AAGlD,KAAI,CAAC,UAAU,OAAO,OAAO,MAAM,OAAO,MAAM,EAAE,CAAC,CACjD,QAAO;AAGT,KAAI,OAAO,MAAM,KAAK,IAAI,eAAe,KAAK,gBAAgB,EAC5D,QAAO;CAGT,MAAM,GAAG,UAAU,UAAU;AAG7B,KAAI,SAAS,OAAO,SAAS,MAAM,SAAS,OAAO,SAAS,GAC1D,QAAO;CAIT,MAAM,aAAa,SAAS,UAAU;CAItC,MAAM,iBACH,6BAA6B,KAAK,IAAK,YAAY,KAAK,KAAM,IAAI,GACnE,KAAK;CAGP,MAAM,cAAc,aAAa;CACjC,MAAM,eAAe,cAAc;CAInC,MAAM,mBAAmB,eADT,gCAAgC,KAAK;CAGrD,MAAM,gBAAgB,KAAK,MAAM,cAAc,iBAAiB;CAChE,MAAM,iBAAiB,KAAK,MAAM,eAAe,iBAAiB;AAKlE,QAAO,GAHO,UAAU,OAAO,cAAc,CAG7B,KAFD,UAAU,OAAO,eAAe,CAEnB,GAAG,KAAK,aAAa"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"viewport-size.js","names":[],"sources":["../../src/viewport/viewport-size.tsx"],"sourcesContent":["/*\n * Copyright
|
|
1
|
+
{"version":3,"file":"viewport-size.js","names":[],"sources":["../../src/viewport/viewport-size.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\nimport { useMapViewport } from './store';\nimport { getViewportSize } from './utils';\nimport type { UniqueId } from '@accelint/core';\nimport type { ComponentPropsWithRef } from 'react';\nimport type { SupportedDistanceUnit } from './types';\n\nexport type ViewportSizeProps = ComponentPropsWithRef<'span'> & {\n instanceId: UniqueId;\n unit?: SupportedDistanceUnit;\n};\n\n/**\n * A span element displaying the current viewport bounds in the specified unit.\n *\n * Displays the viewport dimensions in a format like `660 x 1,801 NM`.\n * Updates automatically as the viewport changes by subscribing to viewport events.\n *\n * @param props - Extends `<span>` props\n * @param props.instanceId - The id of the view to subscribe to\n * @param props.unit - Measure of distance: `km | m | nm | mi | ft`. Defaults to `nm`\n * @param props.className - CSS classes for styling\n *\n * @example\n * ```tsx\n * // Basic usage with default nautical miles\n * <ViewportSize instanceId=\"some-uuid\" />\n *\n * // With custom unit and styling\n * <ViewportSize\n * instanceId=\"some-uuid\"\n * unit=\"km\"\n * className=\"text-sm text-gray-600\"\n * />\n * ```\n */\nexport function ViewportSize({\n instanceId,\n unit = 'nm',\n ...rest\n}: ViewportSizeProps) {\n const { bounds, zoom, width, height } = useMapViewport(instanceId);\n\n return (\n <span {...rest}>\n {getViewportSize({ bounds, unit, zoom, width, height })}\n </span>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,SAAgB,aAAa,EAC3B,YACA,OAAO,MACP,GAAG,QACiB;CACpB,MAAM,EAAE,QAAQ,MAAM,OAAO,WAAW,eAAe,WAAW;AAElE,QACE,oBAAC;EAAK,GAAI;YACP,gBAAgB;GAAE;GAAQ;GAAM;GAAM;GAAO;GAAQ,CAAC;GAClD"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@accelint/map-toolkit",
|
|
3
3
|
"description": "A collection of components and utilities to simplify visualizing and working with geospatial data.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"author": "https://hypergiant.com",
|
|
6
6
|
"$schema": "https://json.schemastore.org/package",
|
|
7
7
|
"devDependencies": {
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"@accelint/biome-config": "1.1.0",
|
|
46
46
|
"@accelint/bus": "3.0.2",
|
|
47
47
|
"@accelint/core": "0.5.2",
|
|
48
|
-
"@accelint/design-foundation": "2.
|
|
49
|
-
"@accelint/design-toolkit": "9.
|
|
50
|
-
"@accelint/geo": "0.5.
|
|
48
|
+
"@accelint/design-foundation": "2.1.0",
|
|
49
|
+
"@accelint/design-toolkit": "9.4.0",
|
|
50
|
+
"@accelint/geo": "0.5.1",
|
|
51
|
+
"@accelint/logger": "0.1.5",
|
|
51
52
|
"@accelint/postcss-tailwind-css-modules": "1.0.1",
|
|
52
|
-
"@accelint/
|
|
53
|
-
"@accelint/smeegl": "0.3.4",
|
|
53
|
+
"@accelint/smeegl": "0.3.5",
|
|
54
54
|
"@accelint/typescript-config": "0.1.4",
|
|
55
55
|
"@accelint/vitest-config": "0.1.6"
|
|
56
56
|
},
|
|
@@ -64,6 +64,8 @@
|
|
|
64
64
|
"./camera/store": "./dist/camera/store.js",
|
|
65
65
|
"./camera/types": "./dist/camera/types.js",
|
|
66
66
|
"./cursor-coordinates": "./dist/cursor-coordinates/index.js",
|
|
67
|
+
"./cursor-coordinates/store": "./dist/cursor-coordinates/store.js",
|
|
68
|
+
"./cursor-coordinates/types": "./dist/cursor-coordinates/types.js",
|
|
67
69
|
"./cursor-coordinates/use-cursor-coordinates": "./dist/cursor-coordinates/use-cursor-coordinates.js",
|
|
68
70
|
"./deckgl": "./dist/deckgl/index.js",
|
|
69
71
|
"./deckgl/base-map": "./dist/deckgl/base-map/index.js",
|
|
@@ -149,16 +151,16 @@
|
|
|
149
151
|
"milsymbol": "^3.0.2",
|
|
150
152
|
"mjolnir.js": "^3.0.0",
|
|
151
153
|
"react-map-gl": "^8.1.0",
|
|
152
|
-
"@accelint/hotkey-manager": "0.
|
|
154
|
+
"@accelint/hotkey-manager": "1.0.0"
|
|
153
155
|
},
|
|
154
156
|
"owner": "default/pathfinder",
|
|
155
157
|
"peerDependencies": {
|
|
156
158
|
"@types/geojson": "^7946.0.14",
|
|
157
159
|
"react": "^19",
|
|
160
|
+
"@accelint/bus": "3.0.2",
|
|
158
161
|
"@accelint/core": "0.5.2",
|
|
159
|
-
"@accelint/geo": "0.5.
|
|
160
|
-
"@accelint/logger": "0.1.
|
|
161
|
-
"@accelint/bus": "3.0.2"
|
|
162
|
+
"@accelint/geo": "0.5.1",
|
|
163
|
+
"@accelint/logger": "0.1.5"
|
|
162
164
|
},
|
|
163
165
|
"private": false,
|
|
164
166
|
"publishConfig": {
|