@trackunit/react-map-adapter-shared 0.0.4-alpha-9d327375fc1.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/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@trackunit/react-map-adapter-shared",
3
+ "version": "0.0.4-alpha-9d327375fc1.0",
4
+ "repository": "https://github.com/Trackunit/manager",
5
+ "license": "SEE LICENSE IN LICENSE.txt",
6
+ "engines": {
7
+ "node": ">=24.x"
8
+ },
9
+ "dependencies": {
10
+ "@trackunit/geo-json-utils": "1.14.25-alpha-9d327375fc1.0",
11
+ "@trackunit/ui-design-tokens": "1.13.24-alpha-9d327375fc1.0",
12
+ "es-toolkit": "^1.39.10",
13
+ "zod": "^3.25.76"
14
+ },
15
+ "peerDependencies": {
16
+ "react": "^19.0.0"
17
+ },
18
+ "module": "./index.esm.js",
19
+ "main": "./index.cjs.js",
20
+ "types": "./index.d.ts"
21
+ }
@@ -0,0 +1,234 @@
1
+ import type { GeoJsonBbox } from "@trackunit/geo-json-utils";
2
+ import type { Entity } from "./interactionTypes";
3
+ import type { AdaptiveMarkerResolution, ClusterConfig, ClusterRenderConfig, ClusterRenderState, DomRenderState, GeoJsonFeatureCollection, RenderConfig, RouteStyle, ShapeInteractiveMode, ShapeStyle } from "./layerApiTypes";
4
+ type ReactNode = import("react").ReactNode;
5
+ /**
6
+ * Full interactive marker with clustering and adapter-managed entity events.
7
+ */
8
+ export type InteractiveMarkerConfig = Readonly<{
9
+ kind: "marker";
10
+ id: string;
11
+ features: GeoJsonFeatureCollection;
12
+ markerRender: RenderConfig<unknown>;
13
+ clusterConfig: ClusterConfig<unknown> | null;
14
+ clusterRender: ClusterRenderConfig<unknown, unknown> | null;
15
+ clusterFeatures: GeoJsonFeatureCollection | null;
16
+ /**
17
+ * Populated by `<Layers>` when `markerRender.mode === "adaptive"` so adapters
18
+ * can split symbol vs DOM per feature (plan §8).
19
+ */
20
+ adaptiveResolution?: AdaptiveMarkerResolution;
21
+ }>;
22
+ /**
23
+ * Positioned DOM content anchor — adapter only handles positioning,
24
+ * content manages its own interaction.
25
+ */
26
+ export type AnchoredContentConfig = Readonly<{
27
+ kind: "anchor";
28
+ id: string;
29
+ features: GeoJsonFeatureCollection;
30
+ markerRender: RenderConfig<unknown>;
31
+ }>;
32
+ /**
33
+ * Configuration for a marker source.
34
+ * The adapter uses this to render markers and clusters on the map.
35
+ *
36
+ * - `"marker"` — interactive marker with clustering and entity events
37
+ * - `"anchor"` — positioned DOM content; content handles its own interaction
38
+ */
39
+ export type MarkerSourceConfig = InteractiveMarkerConfig | AnchoredContentConfig;
40
+ /**
41
+ * Configuration for a shape source.
42
+ * The adapter uses this to render polygons, multipolygons, and lines.
43
+ */
44
+ export type ShapeSourceConfig = Readonly<{
45
+ id: string;
46
+ features: GeoJsonFeatureCollection;
47
+ style: ShapeStyle;
48
+ /** Per-feature resolved styles, keyed by `String(feature.id)`. */
49
+ featureStyles?: ReadonlyMap<string, ShapeStyle>;
50
+ interactive: ShapeInteractiveMode;
51
+ }>;
52
+ /**
53
+ * Configuration for a route source.
54
+ */
55
+ export type RouteSourceConfig = Readonly<{
56
+ id: string;
57
+ features: GeoJsonFeatureCollection;
58
+ style: RouteStyle;
59
+ interactive: boolean;
60
+ }>;
61
+ /**
62
+ * Configuration for an image overlay.
63
+ */
64
+ export type ImageOverlayConfig = Readonly<{
65
+ id: string;
66
+ url: string;
67
+ imageBounds: GeoJsonBbox;
68
+ opacity: number;
69
+ }>;
70
+ /**
71
+ * A typed layer description — the declarative unit passed to `setSnapshot`.
72
+ *
73
+ * Discriminated on `layerType`, matching `LayerHandle.layerType`.
74
+ * Adding a new layer type is additive: extend this union, add a case in each
75
+ * adapter's layer-type handling (for example, its internal snapshot-application
76
+ * switch), and TypeScript's exhaustive check catches any adapter that missed
77
+ * the new type.
78
+ */
79
+ export type MapLayer = ({
80
+ readonly layerType: "markers";
81
+ } & MarkerSourceConfig) | ({
82
+ readonly layerType: "shapes";
83
+ } & ShapeSourceConfig) | ({
84
+ readonly layerType: "route";
85
+ } & RouteSourceConfig) | ({
86
+ readonly layerType: "image-overlay";
87
+ } & ImageOverlayConfig);
88
+ /**
89
+ * Shape interaction state — which shape feature is currently selected/hovered.
90
+ * Both fields are null when nothing is selected/hovered.
91
+ */
92
+ export type ShapeInteractionState = Readonly<{
93
+ handleId: string | null;
94
+ featureId: string | null;
95
+ }>;
96
+ /**
97
+ * Full desired rendering state passed to `LayerPort.setSnapshot`.
98
+ *
99
+ * The adapter owns the diff between the previous snapshot and this one,
100
+ * applying only the minimal changes needed to bring the map up to date.
101
+ */
102
+ export type LayerSnapshot = Readonly<{
103
+ /** All active layers in desired render order. */
104
+ layers: ReadonlyArray<MapLayer>;
105
+ /** Currently selected shape feature. Both fields null when nothing selected. */
106
+ shapeSelection: ShapeInteractionState;
107
+ /** Currently hovered shape feature. Both fields null when nothing hovered. */
108
+ shapeHover: ShapeInteractionState;
109
+ }>;
110
+ /**
111
+ * Entity interaction event types emitted by the map adapter.
112
+ */
113
+ export type EntityInteractionEvent = Readonly<{
114
+ type: "click";
115
+ entity: Entity;
116
+ }> | Readonly<{
117
+ type: "dblclick";
118
+ entity: Entity;
119
+ }> | Readonly<{
120
+ type: "hover-start";
121
+ entity: Entity;
122
+ }> | Readonly<{
123
+ type: "hover-end";
124
+ entity: Entity;
125
+ }>;
126
+ /**
127
+ * Handler for entity interaction events.
128
+ */
129
+ export type EntityInteractionHandler = (event: EntityInteractionEvent) => void;
130
+ /**
131
+ * Describes a React portal target for DOM-rendered markers/clusters.
132
+ *
133
+ * The adapter creates an `AdvancedMarkerElement` (or equivalent) with an
134
+ * empty container div as its content. `<Layers>` subscribes to these
135
+ * descriptors and renders user-provided React content into each container
136
+ * via `createPortal()`. Routing to `markerRender` vs `clusterRender` is
137
+ * derived structurally from which {@link DomPortalStore} the descriptor
138
+ * came from (`markerPortals` vs `clusterPortals` on {@link LayerPort}).
139
+ */
140
+ export type DomPortalDescriptor = Readonly<{
141
+ /** Unique React key for list rendering. Format: `${sourceId}:${featureId}`. */
142
+ key: string;
143
+ /** Owning marker layer id. Avoids parsing composite React keys. */
144
+ sourceId: string;
145
+ /** The marker's content element (portal target) */
146
+ container: HTMLElement;
147
+ /**
148
+ * Optional adapter hook for map-native marker stacking.
149
+ *
150
+ * Some adapters wrap `container` in their own marker object whose z-index
151
+ * controls sibling ordering, so the computed portal z-index must be applied
152
+ * outside the React portal target as well.
153
+ */
154
+ setZIndex?: (zIndex: number) => void;
155
+ /** Feature ID for computing `DomRenderState` (selected/hovered) */
156
+ featureId: string;
157
+ /** Original data item (TItem or TCluster extracted from properties.__data) */
158
+ sourceData: unknown;
159
+ /**
160
+ * User-provided render function. For marker portals this is a
161
+ * `DomRenderConfig`/`AdaptiveRenderConfig.render` (state =
162
+ * {@link DomRenderState}); for cluster portals it is a
163
+ * {@link ClusterRenderConfig} `render` (state =
164
+ * {@link ClusterRenderState}). `<Layers>` constructs the matching state
165
+ * shape based on which store the descriptor came from (`markerPortals`
166
+ * vs `clusterPortals`) before invoking this function.
167
+ *
168
+ * Method syntax: parameter variance is bivariant so the narrower
169
+ * marker-only and cluster-only render signatures both assign into this
170
+ * field without re-wrapping.
171
+ */
172
+ renderFn(item: unknown, state: DomRenderState | ClusterRenderState<unknown>): ReactNode;
173
+ }>;
174
+ /**
175
+ * Optional subscription interface for DOM portal rendering.
176
+ * Compatible with `useSyncExternalStore(subscribe, getSnapshot)`.
177
+ */
178
+ export type DomPortalStore = Readonly<{
179
+ subscribe: (callback: () => void) => () => void;
180
+ getSnapshot: () => ReadonlyArray<DomPortalDescriptor>;
181
+ }>;
182
+ /**
183
+ * The adapter extension point for layer rendering.
184
+ *
185
+ * Each adapter provides a `LayerPort` that the `<Layers>` component uses
186
+ * to communicate the full desired map state. The adapter receives a
187
+ * `LayerSnapshot` describing all active layers and interaction state, and
188
+ * is responsible for diffing against its previous state to apply minimal
189
+ * changes to the underlying map engine.
190
+ */
191
+ export type LayerPort = Readonly<{
192
+ /**
193
+ * Push the full desired rendering state to the adapter.
194
+ *
195
+ * Called whenever layers or interaction state changes. The adapter diffs
196
+ * the new snapshot against its previous state and applies only the
197
+ * necessary changes to the map engine. Passing an empty `layers` array
198
+ * removes all active sources.
199
+ */
200
+ setSnapshot: (snapshot: LayerSnapshot) => void;
201
+ /** Subscribe to entity interaction events from the map (click, hover) */
202
+ onEntityInteraction: (handler: EntityInteractionHandler) => () => void;
203
+ /** Subscribe to clicks on the map background (no entity hit). */
204
+ onBackgroundClick: (handler: () => void) => () => void;
205
+ /**
206
+ * Subscribe to know when a specific source has been loaded and is ready
207
+ * for rendering. Used by `<Layers>` to defer showing edge labels until
208
+ * the parent shape source has finished drawing.
209
+ *
210
+ * Mapbox: fires after `sourcedata` with `isSourceLoaded` for the source.
211
+ * Google Maps: fires immediately (shapes are added synchronously).
212
+ *
213
+ * @param configId - The same `id` that was passed in a shape layer's config.
214
+ * The adapter maps this to its internal source identifier.
215
+ * @param callback - Called once when the source data is loaded and ready.
216
+ * @returns Unsubscribe function.
217
+ */
218
+ onSourceReady: (configId: string, callback: () => void) => () => void;
219
+ /**
220
+ * Subscription for DOM-rendered individual markers (including adaptive DOM
221
+ * features). `<Layers>` routes these descriptors to `markerRender`.
222
+ *
223
+ * Null or undefined if the adapter doesn't support DOM rendering.
224
+ */
225
+ markerPortals?: DomPortalStore | null;
226
+ /**
227
+ * Subscription for DOM-rendered clusters (server clusters and client-side
228
+ * clusters alike). `<Layers>` routes these descriptors to `clusterRender`.
229
+ *
230
+ * Null or undefined if the adapter doesn't support DOM rendering.
231
+ */
232
+ clusterPortals?: DomPortalStore | null;
233
+ }>;
234
+ export {};
@@ -0,0 +1,189 @@
1
+ import type { LayerPort } from "./LayerPort";
2
+ import type { CameraState, FitBoundsOptions, GeoJsonBbox, GeoJsonPosition, InitialViewport, MapComponentProps, MapEvent, MapEventHandler, MapStatus, MapTheme, MapType } from "./primitiveMapTypes";
3
+ type ComponentType<TProps> = import("react").ComponentType<TProps>;
4
+ /** Viewport overrides for adapter switching - both adapters support these */
5
+ type ViewportOverrides = Readonly<{
6
+ initialViewport: InitialViewport;
7
+ }>;
8
+ /**
9
+ * Props passed to adapter renderer components
10
+ */
11
+ export type AdapterRendererProps = MapComponentProps & {
12
+ /** Adapter instance to connect to */
13
+ readonly adapterInstance: AdapterInstance;
14
+ };
15
+ /**
16
+ * Adapter instance - the runtime interface for controlling the map
17
+ * Created by adapter factory, used internally by useMap
18
+ * Generic over the config type for type-safe config access
19
+ */
20
+ export type AdapterInstance<TConfig extends Record<string, unknown> = Record<string, unknown>> = Readonly<{
21
+ /**
22
+ * Get the adapter configuration
23
+ */
24
+ getConfig: () => TConfig;
25
+ /**
26
+ * Get current camera state (center, zoom, bounds, isIdle).
27
+ * This snapshot belongs to the high-frequency camera channel and can change
28
+ * continuously during panning/zooming.
29
+ */
30
+ getCameraState: () => CameraState;
31
+ /**
32
+ * Get current map status (isReady, initializationFailed, appearance, tileSize).
33
+ * This snapshot belongs to the low-frequency status channel used by `api.state`.
34
+ */
35
+ getStatus: () => MapStatus;
36
+ /**
37
+ * Subscribe to camera state changes only.
38
+ * Adapters should notify this channel only after replacing the cached
39
+ * CameraState snapshot with a field-wise different value.
40
+ */
41
+ subscribeCamera: (listener: () => void) => () => void;
42
+ /**
43
+ * Subscribe to status changes only.
44
+ * Adapters should notify this channel only after replacing the cached MapStatus
45
+ * snapshot with a field-wise different value.
46
+ */
47
+ subscribeStatus: (listener: () => void) => () => void;
48
+ /**
49
+ * Set the map center
50
+ */
51
+ setCenter: (center: GeoJsonPosition) => Promise<void>;
52
+ /**
53
+ * Set the zoom level (absolute)
54
+ */
55
+ setZoom: (zoom: number) => Promise<void>;
56
+ /**
57
+ * Adjust zoom by delta (relative)
58
+ */
59
+ zoomBy: (delta: number) => Promise<void>;
60
+ /**
61
+ * Fit the map to bounds
62
+ */
63
+ fitBounds: (bounds: GeoJsonBbox, options?: FitBoundsOptions) => Promise<void>;
64
+ /**
65
+ * Pan to a position with animation
66
+ */
67
+ panTo: (center: GeoJsonPosition) => Promise<void>;
68
+ /**
69
+ * Pan the map by pixel offsets (for keyboard navigation)
70
+ * Uses screen coordinate convention where origin (0,0) is top-left:
71
+ *
72
+ * @param deltaX - Horizontal pan amount in pixels (positive = right, negative = left)
73
+ * @param deltaY - Vertical pan amount in pixels (positive = down, negative = up)
74
+ */
75
+ panBy: (deltaX: number, deltaY: number) => Promise<void>;
76
+ /**
77
+ * Set the map type (base layer)
78
+ */
79
+ setMapType: (type: MapType) => Promise<void>;
80
+ /**
81
+ * Set the map theme (visual appearance)
82
+ */
83
+ setTheme: (theme: MapTheme) => Promise<void>;
84
+ /**
85
+ * Set whether road labels are shown over satellite/hybrid imagery
86
+ */
87
+ setShowRoads: (showRoads: boolean) => Promise<void>;
88
+ /**
89
+ * Subscribe to map events
90
+ */
91
+ on: <TEventType extends MapEvent["type"]>(event: TEventType, handler: MapEventHandler<TEventType>) => () => void;
92
+ /**
93
+ * Mark map initialization as permanently failed (e.g. provider API could not load).
94
+ * Sets `initializationFailed` on state and `isReady` false, then notifies subscribers.
95
+ */
96
+ notifyInitializationFailed: () => void;
97
+ /**
98
+ * Cleanup resources
99
+ */
100
+ destroy: () => void;
101
+ /**
102
+ * Layer rendering capabilities.
103
+ * Null if this adapter does not yet support layer rendering.
104
+ * Adapters can implement this incrementally.
105
+ */
106
+ layers: LayerPort | null;
107
+ }>;
108
+ /**
109
+ * Pixel insets representing areas reserved by the map provider's own UI
110
+ * (logo, attribution links, etc.). Controls use these to avoid overlapping
111
+ * provider chrome.
112
+ */
113
+ export type SafeAreaInsets = Readonly<{
114
+ top: number;
115
+ right: number;
116
+ bottom: number;
117
+ left: number;
118
+ }>;
119
+ /**
120
+ * Base adapter configuration fields returned by adapter factory functions.
121
+ * This is the shape that adapter factories produce before `defineAdapter`
122
+ * injects the `derive` method.
123
+ */
124
+ export type AdapterConfigBase<TConfig extends Record<string, unknown> = Record<string, unknown>> = Readonly<{
125
+ /** Unique name for this adapter */
126
+ name: string;
127
+ /** Adapter-specific configuration */
128
+ config: TConfig;
129
+ /**
130
+ * Safe area insets declared by the map provider.
131
+ * Provide pixel values for sides where the provider renders its own UI,
132
+ * or `null` to indicate no safe area concerns.
133
+ */
134
+ safeAreaInsets: SafeAreaInsets | null;
135
+ /** Factory function to create the adapter instance */
136
+ createInstance: () => AdapterInstance;
137
+ /** React component that renders the map */
138
+ Renderer: ComponentType<AdapterRendererProps>;
139
+ }>;
140
+ /**
141
+ * Full adapter configuration with the `derive` method for creating variants.
142
+ * Generic over the config type for type-safe adapter-specific options.
143
+ *
144
+ * Use `derive(overrides)` to create a new adapter config with merged settings,
145
+ * e.g. for spawning preview map instances with a different theme or map type.
146
+ */
147
+ export type AdapterConfig<TConfig extends Record<string, unknown> = Record<string, unknown>> = AdapterConfigBase<TConfig> & {
148
+ /**
149
+ * Create a variant of this adapter config with merged overrides.
150
+ * Returns a new `AdapterConfig` that uses the same adapter factory
151
+ * but with the overridden config values merged in.
152
+ *
153
+ * Uses method syntax intentionally — bivariant method parameters
154
+ * let `AdapterConfig<TConfig>` remain assignable to `AdapterConfig`
155
+ * (default `Record<string, unknown>`) so we can store it on `MapApi`
156
+ * without requiring a generic on `MapApi` itself.
157
+ *
158
+ * @param overrides - Partial config to merge into the existing config
159
+ * @returns A new AdapterConfig with the merged configuration
160
+ * @example
161
+ * ```typescript
162
+ * // Create a dark-themed variant for a preview map
163
+ * const darkPreview = api.adapterConfig.derive({ theme: "dark" });
164
+ * ```
165
+ */
166
+ derive(overrides: Partial<TConfig> | ViewportOverrides): AdapterConfig<TConfig>;
167
+ };
168
+ /**
169
+ * Helper to define an adapter factory with proper type inference.
170
+ *
171
+ * Automatically injects the `derive` method on every `AdapterConfig` it produces.
172
+ * Adapter implementations don't need to handle `derive` -- it's added transparently
173
+ * by closing over the original factory function.
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * export const googleMapsAdapter = defineAdapter((config: GoogleMapsConfig) => ({
178
+ * name: "google",
179
+ * config,
180
+ * createInstance: () => createGoogleMapsInstance(config),
181
+ * Renderer: GoogleMapsRenderer,
182
+ * }));
183
+ *
184
+ * // derive is available automatically:
185
+ * const darkAdapter = googleMapsAdapter({ apiKey, theme: "light" }).derive({ theme: "dark" });
186
+ * ```
187
+ */
188
+ export declare const defineAdapter: <TConfig extends Record<string, unknown>>(factory: (config: TConfig) => AdapterConfigBase<TConfig>) => ((config: TConfig) => AdapterConfig<TConfig>);
189
+ export {};
@@ -0,0 +1,22 @@
1
+ import type { GeoJsonFeatureCollection } from "@trackunit/geo-json-utils";
2
+ /**
3
+ * Pre-process a GeoJSON FeatureCollection for antimeridian-aware rendering.
4
+ *
5
+ * - MultiPolygon features that represent a single polygon split at the antimeridian
6
+ * (per RFC 7946 Section 3.1.9) are merged back into single Polygons with
7
+ * unwrapped coordinates (lng values may exceed 180). Interior rings that are also
8
+ * split at ±180° with matching seam latitudes are stitched the same way as exteriors.
9
+ *
10
+ * - MultiLineString features that represent one route split at ±180° are merged
11
+ * into a single LineString with the same unwrapping. This eliminates visible strokes
12
+ * at the antimeridian and ensures hover/selection treats each shape as one entity.
13
+ *
14
+ * The input collection is never mutated. Features that don't need merging
15
+ * are returned by reference.
16
+ *
17
+ * **Important:** The output contains coordinates outside [-180, 180] and is
18
+ * intended only for rendering via Google Maps or Mapbox, which handle
19
+ * world-wrapped coordinates natively. Do not pass the output to GeoJSON
20
+ * validators or store it.
21
+ */
22
+ export declare const mergeAntimeridianFeatures: (features: GeoJsonFeatureCollection) => GeoJsonFeatureCollection;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Mix two CSS colors using the browser's `color-mix(in srgb)` function.
3
+ *
4
+ * @param color1 - First color (any valid CSS color string)
5
+ * @param color2 - Second color (any valid CSS color string)
6
+ * @param percentage - Percentage of `color1` in the mix (0–100)
7
+ * @returns Resolved color as an `rgb()` string, or `color1` if resolution fails
8
+ */
9
+ export declare const mixColor: (color1: string, color2: string, percentage: number) => string;
10
+ /**
11
+ * Darken a CSS color by mixing it with black.
12
+ *
13
+ * @param color - Any valid CSS color string
14
+ * @param amount - Darkening intensity from 0 (no change) to 100 (pure black)
15
+ */
16
+ export declare const darkenColor: (color: string, amount: number) => string;
17
+ /**
18
+ * Lighten a CSS color by mixing it with white.
19
+ *
20
+ * @param color - Any valid CSS color string
21
+ * @param amount - Lightening intensity from 0 (no change) to 100 (pure white)
22
+ */
23
+ export declare const lightenColor: (color: string, amount: number) => string;
24
+ /**
25
+ * Resolve a CSS color and apply an opacity multiplier to its alpha channel.
26
+ *
27
+ * Unlike setting `element.style.opacity`, this only affects the individual
28
+ * color value — useful when fill and stroke need independent opacity.
29
+ *
30
+ * @param color - Any valid CSS color string
31
+ * @param opacity - Opacity multiplier from 0 to 1 (multiplied with existing alpha)
32
+ * @returns `rgba()` string with the combined alpha, or the original color if resolution fails
33
+ */
34
+ export declare const colorWithOpacity: (color: string, opacity: number) => string;
35
+ /**
36
+ * Clear the resolved-color cache and detach the probe element.
37
+ * Exposed for test teardown only — not part of the public API.
38
+ */
39
+ export declare const resetColorUtilsForTesting: () => void;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * World bounds for Web Mercator projection.
3
+ * GeoJSON format: [minLng, minLat, maxLng, maxLat]
4
+ */
5
+ export declare const WORLD_BBOX: readonly [-180, number, 180, number];
6
+ /**
7
+ * Default zoom level when not specified
8
+ */
9
+ export declare const DEFAULT_ZOOM = 2;
10
+ /**
11
+ * Default center when not specified (Atlantic Ocean)
12
+ * Position format: { lat: number, lng: number }
13
+ */
14
+ export declare const DEFAULT_CENTER: {
15
+ readonly lat: 0;
16
+ readonly lng: 0;
17
+ };
18
+ /**
19
+ * Minimum zoom level supported across providers
20
+ * Both Mapbox and Google Maps support zoom level 0
21
+ */
22
+ export declare const MIN_ZOOM = 0;
23
+ /**
24
+ * Maximum zoom level supported across providers
25
+ * Both Mapbox and Google Maps support zoom level 22
26
+ */
27
+ export declare const MAX_ZOOM = 22;
28
+ /**
29
+ * Amount to pan the map when using arrow keys (in pixels)
30
+ */
31
+ export declare const KEYBOARD_PAN_AMOUNT = 100;
32
+ /**
33
+ * Amount to zoom when using +/- keys
34
+ */
35
+ export declare const KEYBOARD_ZOOM_AMOUNT = 1;
36
+ /**
37
+ * CSS cursor values used across map adapters.
38
+ * Centralised so both Mapbox and Google stay in sync.
39
+ */
40
+ export declare const MAP_CURSORS: {
41
+ readonly default: "default";
42
+ readonly interactive: "pointer";
43
+ };
44
+ /**
45
+ * Estimate zoom level from a bounding box using Web Mercator projection.
46
+ * Both Google Maps and Mapbox use the same 256px tile / 2^zoom formula,
47
+ * so a simple `log2(360 / lonSpan)` gives a close approximation without
48
+ * needing the container size. Used by adapter constructors to provide
49
+ * a reasonable initial zoom before the native map instance is created.
50
+ */
51
+ export declare const estimateZoomFromBounds: (bounds: readonly [number, number, number, number]) => number;
52
+ /**
53
+ * Compute the visual center of a bounding box in Web Mercator projection.
54
+ * The arithmetic mean of lat/lng gives a wrong center because Mercator
55
+ * stretches latitudes non-linearly. This converts to Mercator Y, averages
56
+ * there, and converts back — matching what Google Maps and Mapbox render.
57
+ */
58
+ export declare const mercatorCenterFromBounds: (bounds: readonly [number, number, number, number]) => readonly [number, number];
package/src/index.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ export * from "./adapterContract";
2
+ export * from "./antimeridianMerge";
3
+ export * from "./colorUtils";
4
+ export * from "./constants";
5
+ export * from "./initialViewportValidation";
6
+ export * from "./interactionTypes";
7
+ export * from "./layerApiTypes";
8
+ export * from "./LayerPort";
9
+ export * from "./layerPortHelpers";
10
+ export * from "./mapEventUtils";
11
+ export * from "./mapStateUtils";
12
+ export * from "./primitiveMapTypes";
13
+ export * from "./restrictBounds";
14
+ export * from "./safeArea/attachSafeAreaHoverListeners";
15
+ export * from "./safeArea/markerContract";
16
+ export * from "./safeArea/polygon";
17
+ export * from "./safeArea/safeAreaDebug";
18
+ export * from "./safeArea/safePolygon";
19
+ export * from "./safeArea/watchSafeAreaLeave";
20
+ export * from "./shapeStyleDefaults";
21
+ export * from "./types";
@@ -0,0 +1,5 @@
1
+ import type { InitialViewport } from "./primitiveMapTypes";
2
+ /**
3
+ * Validates InitialViewport and returns undefined for invalid values.
4
+ */
5
+ export declare const validateInitialViewport: (value: unknown) => InitialViewport | undefined;
@@ -0,0 +1,79 @@
1
+ import type { GeoJsonBbox, GeoJsonGeometry, GeoJsonPosition } from "@trackunit/geo-json-utils";
2
+ /**
3
+ * A marker entity -- a single point of interest on the map.
4
+ * May be contained within a cluster when clustering is active.
5
+ */
6
+ export type MarkerEntity = Readonly<{
7
+ type: "marker";
8
+ id: string;
9
+ position: GeoJsonPosition | null;
10
+ /** If this marker is inside a cluster, the cluster's ID. Null when unclustered. */
11
+ clusterId: string | null;
12
+ }>;
13
+ /**
14
+ * A cluster entity -- a group of markers aggregated into a single point.
15
+ * Produced by either client-side or server-side clustering.
16
+ */
17
+ export type ClusterEntity = Readonly<{
18
+ type: "cluster";
19
+ id: string;
20
+ position: GeoJsonPosition;
21
+ /** IDs of markers contained in this cluster */
22
+ markerIds: ReadonlyArray<string>;
23
+ /**
24
+ * Geographic extent as GeoJSON bbox when known (eg. server clusters from backend)
25
+ */
26
+ bbox: GeoJsonBbox | null;
27
+ }>;
28
+ /**
29
+ * The kind of geometry a shape entity represents.
30
+ * Derived from GeoJSON geometry types when constructing entities in adapters.
31
+ */
32
+ export type ShapeType = "polygon" | "line" | "point";
33
+ /**
34
+ * A shape entity -- a polygon, line, or point on the map.
35
+ * When the interaction originates from an anchor (e.g. edge label),
36
+ * `handleId` carries the layer handle the feature belongs to.
37
+ */
38
+ export type ShapeEntity = Readonly<{
39
+ type: "shape";
40
+ id: string;
41
+ shapeType: ShapeType;
42
+ handleId?: string;
43
+ }>;
44
+ /**
45
+ * A route entity -- a polyline with waypoints.
46
+ */
47
+ export type RouteEntity = Readonly<{
48
+ type: "route";
49
+ id: string;
50
+ /** The waypoints that define this route */
51
+ waypoints: ReadonlyArray<GeoJsonPosition>;
52
+ }>;
53
+ /**
54
+ * Discriminated union of all interactive map entities.
55
+ * Used for hover/select interaction across all layer types.
56
+ */
57
+ export type Entity = Readonly<MarkerEntity | ClusterEntity | ShapeEntity | RouteEntity>;
58
+ /**
59
+ * Shared interaction state for all layers.
60
+ * At most one entity can be selected and one hovered at any time.
61
+ * Selecting a marker clears any previously selected polygon, and vice versa.
62
+ */
63
+ export type MapInteractionState = Readonly<{
64
+ selectedEntity: Entity | null;
65
+ hoveredEntity: Entity | null;
66
+ }>;
67
+ /**
68
+ * GeoJSON geometry types excluding GeometryCollection, which is a
69
+ * container of mixed primitives rather than a renderable shape itself.
70
+ */
71
+ export type GeoJsonPrimitiveGeometryType = Exclude<GeoJsonGeometry["type"], "GeometryCollection">;
72
+ /**
73
+ * Maps a primitive GeoJSON geometry type to the ShapeType discriminant.
74
+ */
75
+ export declare const geometryTypeToShapeType: (geometryType: GeoJsonPrimitiveGeometryType) => ShapeType;
76
+ /**
77
+ * Initial interaction state -- nothing selected or hovered.
78
+ */
79
+ export declare const INITIAL_INTERACTION_STATE: MapInteractionState;