@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.
@@ -0,0 +1,68 @@
1
+ export type { GeoJsonFeature, GeoJsonFeatureCollection, GeoJsonGeometry } from "@trackunit/geo-json-utils";
2
+ export type { AdaptiveMarkerResolution, AdaptiveRenderConfig, AdaptiveRenderState, AdaptiveResolutionContext, CircleSymbolDescriptor, ClientClusterConfig, ClusterConfig, ClusterDomRenderConfig, ClusterInfo, ClusterRenderConfig, ClusterRenderState, ClusterSymbolRenderConfig, ClusterSymbolStyle, CommonRenderState, DomPortalStackingInput, DomPortalStackingResolver, DomPortalStackingResult, DomRenderConfig, DomRenderState, MarkerAnchor, MarkerDomPortalStackGeometry, MarkerDomPortalStackPhase, PixelOffset, RenderConfig, RenderMedium, ResolutionContext, ServerClusterConfig, SymbolDescriptor, SymbolRenderConfig, SymbolRenderState, } from "./markerTypes";
3
+ export { computeMarkerDomPortalZIndex } from "./markerTypes";
4
+ /**
5
+ * Controls which geometric parts of a shape respond to click/hover/dblclick.
6
+ *
7
+ * - `"stroke"` — only line layers (Mapbox) or polyline overlays (Google)
8
+ * respond to interaction. Polygon fills are inert.
9
+ * - `"stroke-and-fill"` — all layers respond, including polygon fills.
10
+ * - `"none"` — no interaction at all.
11
+ *
12
+ * **Points/circles are always interactive** regardless of mode (when not
13
+ * `"none"`). They have no meaningful fill-vs-stroke distinction and are too
14
+ * small for stroke-only targeting to be usable.
15
+ */
16
+ export type ShapeInteractiveMode = "stroke" | "stroke-and-fill" | "none";
17
+ /**
18
+ * The five visual properties that can be overridden per interaction state
19
+ * (hovered, selected). Excludes `pointRadius` because radius should not
20
+ * change on hover/select.
21
+ */
22
+ export type ShapeStyleOverrides = Readonly<{
23
+ /** Fill color (CSS color string). Only applies to polygons and points. */
24
+ fill?: string;
25
+ /** Fill opacity from 0 to 1. Adapter applies geometry-aware defaults (polygon 0.05, point 0.2). */
26
+ fillOpacity?: number;
27
+ /** Stroke color (CSS color string) */
28
+ stroke?: string;
29
+ /** Stroke opacity from 0 to 1. Default: 1 */
30
+ strokeOpacity?: number;
31
+ /** Stroke width in pixels */
32
+ strokeWidth?: number;
33
+ }>;
34
+ /**
35
+ * Provider-agnostic style for shapes (polygons, lines, points).
36
+ * Each adapter maps these to its own style format.
37
+ *
38
+ * Prefer opaque colors for `fill` and `stroke`; use `fillOpacity` and
39
+ * `strokeOpacity` to control transparency. Alpha channels in color strings
40
+ * are still respected but multiply with these opacity values.
41
+ *
42
+ * `hovered` and `selected` allow per-shape override of visual properties
43
+ * when the shape is in that interaction state. Unset properties fall back
44
+ * to adapter-computed defaults (e.g. darkened stroke for hover).
45
+ */
46
+ export interface ShapeStyle extends ShapeStyleOverrides {
47
+ /** Radius of point features in pixels. Default: 5 */
48
+ readonly pointRadius?: number;
49
+ /** Visual overrides applied when this shape is hovered */
50
+ readonly hovered?: ShapeStyleOverrides;
51
+ /** Visual overrides applied when this shape is selected */
52
+ readonly selected?: ShapeStyleOverrides;
53
+ }
54
+ /**
55
+ * Provider-agnostic style for routes/polylines.
56
+ */
57
+ export type RouteStyle = Readonly<{
58
+ /** Line color (CSS color string) */
59
+ color?: string;
60
+ /** Line width in pixels */
61
+ width?: number;
62
+ /** Opacity from 0 to 1 */
63
+ opacity?: number;
64
+ /** Dash pattern. Omit for solid lines. */
65
+ dashArray?: ReadonlyArray<number>;
66
+ /** Whether to show directional arrows along the route */
67
+ showDirectionArrows?: boolean;
68
+ }>;
@@ -0,0 +1,259 @@
1
+ import type { AdaptiveRenderState, CircleSymbolDescriptor, GeoJsonFeature, GeoJsonFeatureCollection, MarkerAnchor, RenderMedium, SymbolDescriptor, SymbolRenderState } from "./layerApiTypes";
2
+ import type { DomPortalDescriptor, MarkerSourceConfig } from "./LayerPort";
3
+ type ReactNode = import("react").ReactNode;
4
+ /** Simple {lng, lat} coordinate pair used across adapters */
5
+ export type LngLat = Readonly<{
6
+ lng: number;
7
+ lat: number;
8
+ }>;
9
+ /**
10
+ * Extract {lng, lat} coordinates from a GeoJSON Point feature's geometry.
11
+ * Returns null if the geometry is null (unlocated feature) or not a Point.
12
+ */
13
+ export declare const extractPointCoordinates: (feature: GeoJsonFeature) => LngLat | null;
14
+ /**
15
+ * Extract coordinates array from a GeoJSON LineString feature.
16
+ * Returns an array of {lat, lng} objects, or null if empty.
17
+ * Returns null if the geometry is null (unlocated feature) or not a LineString.
18
+ */
19
+ export declare const extractLineCoordinates: (feature: GeoJsonFeature) => Array<LngLat> | null;
20
+ /**
21
+ * Extract coordinates from GeoJSON Polygon coordinates.
22
+ * Returns an array of rings (outer ring + holes), each an array of {lat, lng}.
23
+ */
24
+ export declare const extractPolygonPaths: (coordinates: unknown) => Array<Array<LngLat>> | null;
25
+ /**
26
+ * Check whether two GeoJSON feature collections contain the same features
27
+ * by comparing the count and individual feature IDs. Returns true if both
28
+ * are null, or if they have the same number of features with matching IDs
29
+ * in the same order.
30
+ *
31
+ * Used by adapters to skip unnecessary full marker rebuilds when a new config
32
+ * object is created but the underlying data hasn't changed (e.g. only callback
33
+ * references differ due to a React re-render).
34
+ */
35
+ export declare const hasSameFeatureIds: (a: GeoJsonFeatureCollection | null, b: GeoJsonFeatureCollection | null) => boolean;
36
+ /**
37
+ * Determines whether an existing marker source can be patched in place
38
+ * (position + portal update) instead of requiring a full teardown/rebuild.
39
+ *
40
+ * A full rebuild is required when the Mapbox/Google marker `anchor` or
41
+ * `pixelOffset` changes, because both are immutable after marker construction.
42
+ * Without this check the DOM marker keeps a stale anchor/offset while React
43
+ * renders with the new values.
44
+ */
45
+ export declare const canPatchMarkerInPlace: (existing: MarkerSourceConfig, incoming: MarkerSourceConfig) => boolean;
46
+ /**
47
+ * Extract the original source data from GeoJSON feature properties.
48
+ * useMarkers stores the original TItem / TCluster in properties.__data
49
+ * so that style functions receive the full typed object, not raw GeoJSON properties.
50
+ */
51
+ export declare const extractSourceData: (properties: Readonly<Record<string, unknown>> | null) => unknown;
52
+ type PatchPortalDescriptorsResult = Readonly<{
53
+ descriptors: Array<DomPortalDescriptor>;
54
+ changed: boolean;
55
+ }>;
56
+ /**
57
+ * Patch portal descriptors for features whose data or render function changed,
58
+ * preserving the existing container and key so React can update in-place
59
+ * without a remove-then-add cycle (which causes visible flickering).
60
+ *
61
+ * Pure function: returns a new descriptors array and a `changed` flag.
62
+ * Both adapters delegate to this from their `patchDomMarkers` method.
63
+ */
64
+ export declare const patchPortalDescriptors: (descriptors: ReadonlyArray<DomPortalDescriptor>, sourceId: string, features: ReadonlyArray<GeoJsonFeature>, render: DomPortalDescriptor["renderFn"]) => PatchPortalDescriptorsResult;
65
+ /**
66
+ * Defaults for a {@link CircleSymbolDescriptor} (per ADR-0011).
67
+ * - `diameterPx`: 16
68
+ * - `opacity`: 1
69
+ * - `borderColor`: null (no border)
70
+ * - `borderWidthPx`: 0 (no border)
71
+ */
72
+ export declare const CIRCLE_SYMBOL_DEFAULT_DIAMETER_PX = 16;
73
+ export declare const CIRCLE_SYMBOL_DEFAULT_OPACITY = 1;
74
+ /**
75
+ * Resolved circle-symbol style with all defaults filled and `radiusPx` /
76
+ * `hasBorder` precomputed for adapter consumption (Mapbox `circle-radius`,
77
+ * Google canvas `ctx.arc`, conditional stroke).
78
+ */
79
+ export type ResolvedCircleSymbol = Readonly<{
80
+ color: string;
81
+ diameterPx: number;
82
+ radiusPx: number;
83
+ opacity: number;
84
+ borderColor: string | null;
85
+ borderWidthPx: number;
86
+ hasBorder: boolean;
87
+ }>;
88
+ /**
89
+ * Fill {@link CircleSymbolDescriptor} defaults and compute `radiusPx`
90
+ * (`= diameterPx / 2`) and `hasBorder` (`= borderColor !== null && borderWidthPx > 0`).
91
+ *
92
+ * Pure function — adapters use this once per feature to derive the values they
93
+ * pass to their style format (Mapbox paint expression or canvas `ctx.arc`).
94
+ */
95
+ export declare const resolveCircleSymbolDefaults: (descriptor: CircleSymbolDescriptor) => ResolvedCircleSymbol;
96
+ /**
97
+ * Resolve a {@link SymbolDescriptor} by calling the style function with the
98
+ * source data extracted from `properties.__data`.
99
+ */
100
+ export declare const resolveSymbolDescriptor: (style: (item: unknown) => SymbolDescriptor, properties: Readonly<Record<string, unknown>> | null) => SymbolDescriptor;
101
+ /**
102
+ * Result of discriminating a unified `render(item, state)` return value
103
+ * (ADR-0009). One render callback can return either a {@link SymbolDescriptor}
104
+ * for map-native symbol rendering or a {@link ReactNode} for DOM portal
105
+ * rendering; adapters route on the discriminated `kind`.
106
+ */
107
+ export type DiscriminatedRenderResult = Readonly<{
108
+ kind: "symbol";
109
+ symbol: SymbolDescriptor;
110
+ }> | Readonly<{
111
+ kind: "dom";
112
+ node: ReactNode;
113
+ }>;
114
+ /**
115
+ * Discriminate the result of a unified `render(item, state)` callback into a
116
+ * symbol descriptor or a DOM node (ADR-0009).
117
+ *
118
+ * Rules:
119
+ * - Plain object with `color: string` → `{ kind: "symbol", symbol }`.
120
+ * - Anything else (strings, numbers, `null`, fragments, iterables) → treated
121
+ * as a DOM node so consumers can return primitives like text directly.
122
+ *
123
+ * When the optional `expectedMedium` argument is set (adaptive routing), a dev-only
124
+ * `console.warn` runs if the discriminated `kind` disagrees with it — e.g.
125
+ * the consumer returned a `ReactNode` while `state.medium` was `"symbol"`.
126
+ * Omitted in production builds (`NODE_ENV === "production"`).
127
+ */
128
+ export declare const discriminateRenderResult: (result: SymbolDescriptor | ReactNode, expectedMedium?: RenderMedium) => DiscriminatedRenderResult;
129
+ /**
130
+ * Build a `(item) → SymbolDescriptor` closure from an adaptive
131
+ * {@link AdaptiveRenderConfig.render} callback (ADR-0009).
132
+ *
133
+ * Adapters that render a feature in symbol medium under `mode: "adaptive"`
134
+ * call render with the supplied `SymbolRenderState`, then route the result
135
+ * via {@link discriminateRenderResult}. When the discriminated kind is
136
+ * `"symbol"`, the descriptor is returned; when it disagrees with the
137
+ * requested medium (the consumer accidentally returned a `ReactNode`), a
138
+ * neutral fallback descriptor is returned so rendering can proceed (with a
139
+ * dev-only `console.warn` via {@link discriminateRenderResult}).
140
+ */
141
+ export declare const buildAdaptiveSymbolStyleFn: (render: (item: unknown, state: AdaptiveRenderState) => SymbolDescriptor | ReactNode, state: SymbolRenderState) => ((item: unknown) => SymbolDescriptor);
142
+ /**
143
+ * Build a `DomPortalDescriptor["renderFn"]` from an adaptive
144
+ * {@link AdaptiveRenderConfig.render} callback (ADR-0009).
145
+ *
146
+ * Adapters wrap the consumer's unified render with this when registering a
147
+ * DOM portal for an adaptive feature: the closure forwards the
148
+ * `DomRenderState` (a member of `AdaptiveRenderState`) into render, then
149
+ * routes the result via {@link discriminateRenderResult}, returning the
150
+ * `ReactNode` for `kind === "dom"` and `null` for the mismatch case where
151
+ * the consumer accidentally returned a `SymbolDescriptor` (with a dev-only
152
+ * `console.warn` via {@link discriminateRenderResult}).
153
+ */
154
+ export declare const buildAdaptiveDomRenderFn: (render: (item: unknown, state: AdaptiveRenderState) => SymbolDescriptor | ReactNode) => DomPortalDescriptor["renderFn"];
155
+ /**
156
+ * Create a styled cluster pin DOM element.
157
+ * Used by both Google Maps and Mapbox adapters for symbol-mode cluster rendering.
158
+ *
159
+ * Cluster pins keep their always-on white border (visual distinction from
160
+ * individual markers); only `color` is read from the descriptor.
161
+ */
162
+ export declare const createClusterPinElement: (descriptor: CircleSymbolDescriptor, count: unknown) => HTMLElement;
163
+ /**
164
+ * Create a default cluster element with the default blue color.
165
+ */
166
+ export declare const createDefaultClusterElement: (count: unknown) => HTMLElement;
167
+ /**
168
+ * Create a styled dot element for symbol-mode markers (e.g. client-clustering
169
+ * paths that emit per-marker DOM elements). Uses the resolved descriptor's
170
+ * `diameterPx` for sizing and applies the border only when `hasBorder` is true.
171
+ */
172
+ export declare const createSymbolDotElement: (resolved: ResolvedCircleSymbol) => HTMLElement;
173
+ /**
174
+ * Feature IDs that render as DOM markers in adaptive mode (i.e. resolved
175
+ * medium is `"dom"`).
176
+ */
177
+ export declare const getAdaptiveDomFeatureIds: (config: MarkerSourceConfig) => Set<string>;
178
+ /**
179
+ * Returns true when a render mode requires the WebGL/canvas circle layer
180
+ * (symbol or adaptive). Use to branch between the canvas path and
181
+ * the individual DOM-marker-per-feature path.
182
+ */
183
+ export declare const isCanvasMarkerMode: (mode: "symbol" | "dom" | "adaptive") => boolean;
184
+ /**
185
+ * Shared loop body for adaptive DOM overlay creation. Handles guards, feature
186
+ * iteration, and renderFn construction, then delegates marker creation to the
187
+ * adapter via `createMarker`.
188
+ *
189
+ * Returns one entry per DOM-rendered feature so each adapter can apply its own
190
+ * post-creation work (zIndex, interaction listeners, tracked-marker bookkeeping).
191
+ */
192
+ export declare const buildAdaptiveDomEntries: <TMarker>(config: MarkerSourceConfig, createMarker: (featureId: string, coords: LngLat, sourceId: string, data: unknown, renderFn: DomPortalDescriptor["renderFn"]) => TMarker) => ReadonlyArray<{
193
+ featureId: string;
194
+ coords: LngLat;
195
+ domMarker: TMarker;
196
+ }>;
197
+ /**
198
+ * Collect the set of non-empty feature IDs from a GeoJSON feature collection.
199
+ * Returns an empty set when `collection` is null.
200
+ *
201
+ * Used by adapters to diff feature ID sets between config updates so they can
202
+ * remove gone markers and add new ones without a full teardown/rebuild.
203
+ */
204
+ export declare const collectFeatureIdSet: (collection: GeoJsonFeatureCollection | null) => Set<string>;
205
+ /**
206
+ * Returns true when an adaptive marker source can be patched incrementally
207
+ * after a viewport refetch — i.e. structural config (render mode, anchor,
208
+ * clustering mode, cluster render mode) matches between existing and incoming,
209
+ * but marker/cluster feature IDs may differ.
210
+ *
211
+ * When this returns true, adapters should diff by feature ID (add new,
212
+ * remove gone) instead of doing a full teardown/rebuild. This prevents
213
+ * existing DOM markers from flickering when only the set of visible IDs changes.
214
+ *
215
+ * Callback references (render functions) are intentionally not compared:
216
+ * they change on every React render and are always captured fresh during the patch.
217
+ */
218
+ export declare const canPatchAdaptiveViewport: (existing: MarkerSourceConfig, incoming: MarkerSourceConfig) => boolean;
219
+ /**
220
+ * Returns true when an adaptive marker source can be patched incrementally for
221
+ * mode transitions / portal updates where feature IDs and cluster data are stable.
222
+ */
223
+ export declare const canPatchAdaptiveMarker: (existing: MarkerSourceConfig, incoming: MarkerSourceConfig) => boolean;
224
+ type RemoveGoneIndexedMarkersInput<TMarker> = Readonly<{
225
+ incomingFeatureIds: ReadonlySet<string>;
226
+ markerIndex: Map<string, TMarker>;
227
+ markers: Array<TMarker>;
228
+ portalDescriptors: Array<DomPortalDescriptor>;
229
+ sourceId: string;
230
+ detachMarker: (marker: TMarker) => void;
231
+ }>;
232
+ type RemoveGoneIndexedMarkersResult<TMarker> = Readonly<{
233
+ markerIndex: Map<string, TMarker>;
234
+ markers: Array<TMarker>;
235
+ portalDescriptors: Array<DomPortalDescriptor>;
236
+ markersChanged: boolean;
237
+ portalDescriptorsChanged: boolean;
238
+ }>;
239
+ /**
240
+ * Removes indexed native markers whose feature IDs are no longer present and
241
+ * removes the matching DOM portal descriptors. The marker index is mutated in
242
+ * place because adapters keep it as their long-lived native marker registry.
243
+ */
244
+ export declare const removeGoneIndexedMarkers: <TMarker>({ incomingFeatureIds, markerIndex, markers, portalDescriptors, sourceId, detachMarker, }: RemoveGoneIndexedMarkersInput<TMarker>) => RemoveGoneIndexedMarkersResult<TMarker>;
245
+ /**
246
+ * Convert a MarkerAnchor value to a CSS transform string.
247
+ * Assumes the element's **bottom-center** is at the coordinate by default.
248
+ * Google Maps AdvancedMarkerElement positions custom DOM content at
249
+ * bottom-center, so all Google DOM elements (symbol dots, shape point dots,
250
+ * and DOM markers) need this to correct anchoring.
251
+ */
252
+ export declare const anchorFromBottomCenter: (anchor: MarkerAnchor) => string;
253
+ export declare const LAYER_FADE_DURATION_MS = 200;
254
+ /**
255
+ * Start an element at opacity 0 and animate it in via CSS transition.
256
+ * Used for DOM-based markers in both Mapbox and Google adapters.
257
+ */
258
+ export declare const fadeInElement: (el: HTMLElement) => void;
259
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { MapEvent, MapEventOfType } from "./primitiveMapTypes";
2
+ /** Type predicate to narrow a MapEvent to a specific event type */
3
+ export declare const isEventOfType: <TEventType extends MapEvent["type"]>(mapEvent: MapEvent, type: TEventType) => mapEvent is MapEventOfType<TEventType>;
@@ -0,0 +1,11 @@
1
+ import { type CameraState, type GeoJsonBbox, type InitialViewport, type MapAppearance, type MapState } from "./primitiveMapTypes";
2
+ /**
3
+ * Compute the initial MapState from a config's appearance and optional viewport.
4
+ * Shared by both Google Maps and Mapbox adapters.
5
+ */
6
+ export declare const computeInitialState: (appearance: MapAppearance, initialViewport?: InitialViewport) => MapState;
7
+ export declare const bboxEquals: (a: Readonly<GeoJsonBbox> | null, b: Readonly<GeoJsonBbox> | null) => boolean;
8
+ /** Field-wise equality for camera state (center, zoom, bounds, isIdle). */
9
+ export declare const cameraStateEquals: (a: CameraState, b: CameraState) => boolean;
10
+ /** Field-wise equality for the full combined MapState. */
11
+ export declare const mapStateEquals: (a: MapState, b: MapState) => boolean;
@@ -0,0 +1,36 @@
1
+ import type { MapTheme } from "./primitiveMapTypes";
2
+ /**
3
+ * Visual role of a DOM portal marker for **global** stacking (z-index) on the map.
4
+ *
5
+ * Ordering (back → front): `cluster` &lt; `circle` &lt; `pill` &lt; `stick`.
6
+ * Cluster DOM is kept behind individual markers so promoted pins stay clickable.
7
+ */
8
+ export type MarkerDomPortalStackGeometry = "circle" | "pill" | "stick" | "cluster";
9
+ /**
10
+ * Interaction-driven band within a geometry tier. Back → front:
11
+ * `expanded` &lt; `idle` &lt; `hovered` &lt; `selected`.
12
+ */
13
+ export type MarkerDomPortalStackPhase = "expanded" | "idle" | "hovered" | "selected";
14
+ /**
15
+ * Inputs threaded from {@link DomRenderState} for portal stacking decisions.
16
+ */
17
+ export type DomPortalStackingInput = Readonly<{
18
+ selected: boolean;
19
+ hovered: boolean;
20
+ labelVisible: boolean;
21
+ isMounting: boolean;
22
+ theme: MapTheme;
23
+ }>;
24
+ export type DomPortalStackingResult = Readonly<{
25
+ geometry: MarkerDomPortalStackGeometry;
26
+ phase: MarkerDomPortalStackPhase;
27
+ }>;
28
+ export type DomPortalStackingResolver = (item: unknown, input: DomPortalStackingInput) => DomPortalStackingResult;
29
+ /**
30
+ * Single source of truth for DOM marker portal `z-index` values.
31
+ *
32
+ * Higher geometry tiers always sort above lower tiers regardless of phase
33
+ * (e.g. any `pill` stacks above any `circle`). Within a tier, phase orders
34
+ * expanded → idle → hovered → selected.
35
+ */
36
+ export declare const computeMarkerDomPortalZIndex: (geometry: MarkerDomPortalStackGeometry, phase: MarkerDomPortalStackPhase) => number;