import { type CameraOptions, type MandatedChromeHosts } from "./runtime-core"; import { type LightingIR, type LightingOptions } from "./scene-lighting"; import type { TerrainIR } from "./terrain"; import { type SnapshotOptions } from "./snapshot"; import { type TokenStore, type ViewOrigin, type ViewportSnapshot, type DataVersionSnapshot } from "./external-store"; import type { LayerIR } from "./ir"; import { type RuntimeContext } from "./ctx"; import type { LayerMetaSnapshot } from "./ctx"; import { type LayerFilterFieldSpec, type LayerCategoryFilterFieldSpec } from "./attribute-resolution"; import { type DataTransportOwner } from "./data-layer"; import type { Selection } from "./selection"; import type { ValidationEntry } from "./validation"; import type { MapViewport } from "./basemap"; import { type LayerIRSnapshot } from "./ir-snapshot"; /** * A layer, declared as a typed value — the programmatic mirror of one * element. First-party callers normally use plain JS accessor * functions. A schema-declared accessor may alternatively be a restricted * OnlyMap expression string; that JSON-safe form is intended for native and * cross-process bridges and uses the same compiler as `get-*` attributes. */ export interface LayerDescriptor { /** Stable identity — same contract as the `id` attribute. Required. */ id: string; /** deck.gl layer class name, e.g. "ScatterplotLayer". */ type: string; /** * Inline rows / GeoJSON / columnar data passed by reference, or a URL * string — URLs ride the Data Layer exactly like the `data` attribute * (format detection, ws(s):// streams, `refresh` polling, draw: stores). * Keep inline references STABLE across commits (memoize in the caller): * the `data:` watch tokens diff by reference. */ data?: unknown; /** Legend metadata (ctx.layers), same as the label/color attributes. */ label?: string; color?: string; /** GPU filter (spec: "Filtering") — mirrors filter-field/filter-range/… (single dimension). */ filterField?: string; filterRange?: [number, number]; filterSoftRange?: [number, number]; /** Multi-dimension GPU filter (up to 4) — mirrors `filter-fields`. Wins over `filterField`/`filterRange` if both are set. */ filterFields?: LayerFilterFieldSpec[]; filterCategoryField?: string; filterCategories?: unknown[]; /** Multi-dimension categorical GPU filter (up to 4) — mirrors `filter-category-fields`. Wins over `filterCategoryField`/`filterCategories` if both are set. */ filterCategoryFields?: LayerCategoryFilterFieldSpec[]; /** Classified symbology (issue #12) — mirrors `classify-by`/`classify-scale`/`classify-classes`/`classify-ramp`; computes class breaks from the data and installs a fill-color accessor + legend. An explicit fill-color prop wins. */ classifyBy?: string; classifyScale?: "quantile" | "equal-interval" | "jenks"; classifyClasses?: number; classifyRamp?: string; /** Dashed lines on a path layer (spec: "PathStyleExtension") — `[dashLength, gapLength]` in line-width units; mirrors the `dash` attribute. */ dash?: [number, number]; /** Stretch each segment's dashes to start and end on a dash (deck `dashJustified`); mirrors `dash-justified`. */ dashJustified?: boolean; /** Stream options for ws(s):// data URLs (spec: "Live & Streaming Data"). */ source?: string; key?: string; flush?: string; /** Polling interval for REST snapshot URLs, e.g. "5s". */ refresh?: string; /** * deck.gl updateTriggers, passed through per accessor prop. deck ignores * accessor FUNCTION identity by design — supply a value here that changes * when an accessor's output changes (the deck-idiomatic contract). */ updateTriggers?: Record; /** deck.gl props (camelCase); accessors may be functions or restricted expression strings. */ props?: Record; /** Per-layer terrain mode (spec: "Terrain") — the `terrain` attribute's twin; absent = the type default. */ terrain?: "drape" | "offset" | "off"; } /** Camera state, both an input (options/setView) and an output (getViewState). */ export interface CameraState { longitude: number; latitude: number; zoom: number; pitch: number; bearing: number; } export interface MapControllerOptions { center?: [number, number]; zoom?: number; pitch?: number; bearing?: number; /** Same contract as the `basemap` attribute — a preset name, "maplibre", a style URL, or omit for standalone deck.gl. */ basemap?: string; /** Publishable provider key for keyed presets (`maptiler-*`) — the `basemap-key` attribute's programmatic twin (configureBasemap covers the global case). */ basemapKey?: string; /** Set false to opt out of the compact attribution control (render your own credits). */ attribution?: boolean; /** Headless mode (spec: "Consumer Testing Surface") — no renderer, real projection math; for jsdom/happy-dom tests. */ headless?: { width?: number; height?: number; }; /** Runtime error boundary — deck.gl-level failures in the structured validation shape. */ onRuntimeError?: (entry: ValidationEntry) => void; /** * Camera settled (trailing-debounced, the `om-view-changed` twin) — for * camera persistence. Per-frame viewport churn goes to `watch(["viewport"])`; * this fires once with the destination. `origin` is "user" when ANY change * in the settled burst came from a canvas gesture (a drag's inertia tail * alone must not relabel the gesture), "programmatic" for pure API/story * moves — the echo-suppression signal for store bridges. */ onViewChange?: (view: CameraState, origin: ViewOrigin) => void; /** Scene lighting (spec: "Scene Lighting") — the lighting* attributes' programmatic twin; omit for deck's default lights. */ lighting?: LightingOptions; /** Every click/hover's map coordinate, including empty-map events — the `om-map-point` DOM event's twin (custom sketch/capture tools). */ onMapPoint?: (coordinate: [number, number] | null, kind: "click" | "hover") => void; /** A Tile3DLayer finished loading its root tileset — the `om-tileset-load` DOM event's twin; `tileset` is the live deck `Tileset3D`. */ onTilesetLoad?: (layerId: string, tileset: unknown) => void; /** A Route layer finished resolving — the `om-route-resolved` DOM event's twin; `route` carries the normalized geometry/distanceMeters/durationSec/legs/bounds. */ onRouteResolved?: (layerId: string, route: { geometry?: { type: "LineString"; coordinates: [number, number][]; }; distanceMeters?: number; durationSec?: number; legs?: { distanceMeters: number; durationSec: number; }[]; bounds?: [[number, number], [number, number]]; }) => void; /** A `pick-features` layer decoded its property table — the DOM front-end's internal `featureTables` bookkeeping, surfaced here since `ctx.features()` depends on it (see `RuntimeCoreCallbacks.onFeatureTable`). */ onFeatureTable?: (layerId: string, rows: Record[]) => void; /** A `carriesGeoreference` layer (BIMLayer) finished loading and read its own file's georeferencing (see `RuntimeCoreCallbacks.onGeoreference`). No front end auto-applies terrain (what-you-write-is-what-you-see) — call `controller.setTerrain(...)` from here if you want a reaction. `approximatePlacement` mirrors the DOM lane's data-quality warning. */ onGeoreference?: (layerId: string, info: { orthogonalHeight?: number; hasFullMapConversion: boolean; approximatePlacement: boolean; }) => void; /** @internal Stable React-owned hosts that place required chrome in managed slots. */ chromeHosts?: MandatedChromeHosts; } /** * Descriptor → layer IR: the programmatic counterpart of one parse-manifest.ts * loop iteration. * * Pass `owner` (any stable object identity — a host controller, an element) to * put a URL-backed descriptor's fetch/poll/socket under descriptor-owned * lifetime; `releaseDataOwner(owner)` then stops everything that owner holds. * Without one the transport is un-owned: it is still shared by identity and * still notifies, but it holds no reference count, so a real owner's release * can stop it. `MapController` owns its own transports and does not need this. */ export declare function descriptorToIR(desc: LayerDescriptor, onDataLoaded: () => void, owner?: DataTransportOwner): LayerIR | null; /** * Resolve JSON-safe/programmatic descriptors to deterministic snapshots. * URL data is represented as an empty pending layer and is never fetched. */ export declare function snapshotDescriptorIR(descriptors: readonly LayerDescriptor[]): LayerIRSnapshot[]; /** rAF-batched overlay positioning hook — same flush contract as the DOM front-end's OverlayHost. */ export type OverlayPositioner = (viewport: MapViewport | undefined, selection: Selection | null, /** Pointer event behind the latest pick — carries the type of an EMPTY pick (`selection` null), which the selection itself can't. */ pickType?: "hover" | "click") => void; /** * Pick stream for framework event handlers. `null` means the pointer left * whatever it was hovering (deck reports an empty pick) — the "hover out" * signal; clicks never arrive as null. */ export type PickListener = (selection: Selection | null) => void; /** * The programmatic map instance — owns a RuntimeCore plus the same * orchestration OmMapElement provides around it (layer IR registry, watch * pub-sub, selection normalization, overlay flush, readiness), minus every * DOM-manifest concern. One controller per map container. */ export declare class MapController { private firstSettleDrawDone; private core; private mount; private layerIRs; /** Decoded property tables per pick-features layer — see RuntimeCoreCallbacks.onFeatureTable and the `` twin's own field. */ private featureTables; private descriptors; private selection; private lastPickType; private watchers; private pickListeners; private overlayFns; private overlayFlushPending; private commitPending; private destroyed; private suspended; private readonly dataOwner; private firstCommitDone; private readonly stores; /** Origin of the most recent viewport change FRAME — latched through the settle tracker for snapshot reads. */ private lastViewOrigin; /** * Shared settle/burst tracker (the `om-view-changed` twin) — ALWAYS armed, * onViewChange or not: the burst latch also feeds the viewport snapshot's * `origin`, so it must reset on settle even for store-only consumers. */ private readonly viewSettle; private onViewChange?; /** Per-layer data version counters (`data:` snapshots) — bumped on every data change, monotonic for the controller's lifetime. */ private readonly dataVersions; private readyFired; private resolveReady; readonly ready: Promise; constructor(container: HTMLElement, options?: MapControllerOptions); /** Replaces the full layer set (z-order = array order) — descriptors are re-resolved on a microtask-coalesced commit. */ setLayers(descriptors: readonly LayerDescriptor[]): void; /** A snapshot of the current layer IRs — the same shape ctx.layers is built from. */ getLayers(): readonly LayerIR[]; private scheduleCommit; private commit; private checkReady; /** Readiness as a poll (the promise form is `ready`). */ isReady(): boolean; /** * Subscribes to watch tokens ("viewport", "selection", "layers", * "data:") — `notify` runs immediately with the current context * (initial render parity with widget registration), then on every matching * change. Returns the unsubscribe function. */ watch(tokens: readonly string[], notify: (ctx: RuntimeContext) => void): () => void; private notifyWatchers; /** * Per-token `{subscribe, getSnapshot}` store — the framework-free surface * for `useSyncExternalStore`, MobX autoruns, Redux listeners, Zustand * mirrors (recipes: docs/external-stores.md). Snapshots are cached * immutable PLAIN DATA: `getSnapshot()` returns the SAME reference until * this token's next change event, and never contains closures — rich * reads (`project`, `data()`, `stats()`) stay on `ctx`. Tokens: * - "viewport" → ViewportSnapshot (camera + bounds + origin) * - "selection" → Selection | null * - "layers" → LayerMetaSnapshot[] (what ctx.layers builds) * - "data:" → {version, rows} — a VERSION STAMP, not the rows * (mirroring row arrays into a store per tick is the anti-pattern the * Redux performance guidance warns about; fetch rows via ctx.data()). * `history` is deliberately NOT a store here — the app owns state on the * programmatic front-end. Calling twice for one token returns the * identical store object. */ getStore(token: "viewport"): TokenStore; getStore(token: "selection"): TokenStore; getStore(token: "layers"): TokenStore; getStore(token: `data:${string}`): TokenStore; getStore(token: string): TokenStore; private buildViewportSnapshot; /** The RuntimeContext contract (HU5) — identical to what HTML widget scripts receive as `ctx`. */ buildCtx(): RuntimeContext; /** Picked object, normalized to flat — same path as OmMapElement (GeoJSON properties lifted; columnar rows materialized). */ private resolvePickedObject; private handleSelectionChange; /** * The pointer event behind the most recent pick — tracked separately from * the selection because an EMPTY pick stores null, losing its type. What * lets a `selectionType`-scoped overlay treat a click on empty space as a * dismissal while ignoring hovers over empty space. */ getLastPickType(): "hover" | "click" | undefined; getSelection(): Selection | null; /** Every hover/click pick (null = hover left) — the stream framework adapters route to per-layer onClick/onHover handlers. */ watchPicks(listener: PickListener): () => void; /** * Synthetic pick injection (spec: "Consumer Testing Surface") — lands on * the SAME selection path real deck.gl picks arrive through, so * flattening, watcher notification, and overlay flush are same-path by * construction. */ injectPick(selection: Selection | null, pickType?: "hover" | "click"): void; /** * Registers an rAF-batched positioner: called with the fresh viewport + * selection whenever either may have moved (spec: "Overlay Renderer" — * one flush per frame across all overlays). Returns unsubscribe. */ registerOverlay(positioner: OverlayPositioner): () => void; /** * Requests a re-flush of every registered positioner — for state changes * a positioner reads from OUTSIDE the flush arguments (framework props * like anchor/visible), which the controller can't observe itself. * rAF-coalesced, so calling it eagerly is free. */ refreshOverlays(): void; private scheduleOverlayFlush; /** Dispatches an action (spec: "Action payload contract") — camera moves, pulse/populate effects, custom registered actions. */ emit(event: string, payload?: Record): void; /** * Live basemap change — a preset name, style URL, or "none". Style-only * switches keep the renderer, camera, and layers; renderer-mode changes * (none ↔ maplibre) remount seeded with the current camera. */ setBasemap(basemap: string | null): void; /** Scene lighting (spec: "Scene Lighting") — swap the LightingEffect live; null restores deck's default lights. */ setLighting(options: LightingOptions | null): void; /** Resolved scene-lighting IR (test inspection) — null = deck default lights. */ getLighting(): LightingIR | null; /** * Terrain (spec: "Terrain") — a resolved TerrainIR, or null to turn the * surface off. Note: terrain suppresses an active basemap while on * (restored on null) — the flat-canvas desync rule. */ setTerrain(ir: TerrainIR | null): void; /** Resolved terrain IR (test inspection) — null = no surface. */ getTerrain(): TerrainIR | null; /** * Canvas-only scene snapshot (spec: "Snapshot API") — basemap + deck * composited at device pixels; framework-rendered widgets/overlays are * NOT captured (render provider credits yourself on exports). Await * `ready` first; headless controllers reject. */ snapshot(opts?: SnapshotOptions): Promise; flyTo(center: [number, number], zoom?: number, opts?: CameraOptions): void; flyToBounds(bounds: [[number, number], [number, number]], padding?: number, opts?: CameraOptions): void; setView(partial: Partial, opts?: CameraOptions): void; getViewState(): CameraState; /** * Pause descriptor-owned live transports while retaining canonical map state. * `retain` because this pause is meant to be reversed: `resume()` repaints the * last rows immediately rather than showing an empty map while it reconnects. */ suspend(): void; /** Reacquire current descriptor transports and reconcile after foregrounding. */ resume(): void; /** Projects a lng/lat through the current viewport — undefined before the viewport resolves. */ project(lngLat: [number, number]): [number, number] | undefined; /** Issue #37 — the controller-lane twin of om-map.whenSettled(): resolves once every live 3D tileset has refined for the current view plus one painted frame; {settled:false} + warning on timeout. */ whenSettled(opts?: { timeout?: number; }): Promise<{ settled: boolean; }>; /** Issue #37 — the determinism switch: while on, camera moves land instantly, effect verbs snap to end state, and GPU transitions are disabled, so a captured frame is a pure function of story time. */ setDeterministic(on: boolean): void; destroy(): void; }