import { HTMLElementBase } from "../env";
import { type RuntimeContext } from "../ctx";
import { type LightingIR } from "../scene-lighting";
import { type ClipBoxIR } from "../clip-box";
import { type TerrainIR } from "../terrain";
import { type WidgetSlot } from "../widget-layout";
import { type SnapshotOptions } from "../snapshot";
import { type ValidationEntry } from "../validation";
import type { LayerIR } from "../ir";
import type { Selection } from "../selection";
import type { MapViewport } from "../basemap";
interface OverlayHost {
updatePosition(viewport: MapViewport | undefined, selection: Selection | null, pickType?: "hover" | "click"): void;
/**
* Screen rect for collision-dim (spec: "Overlap avoidance") — non-null
* only when the overlay is VISIBLE and participates in dimming. Returns
* null when hidden OR when it's transient runtime chrome (the internal
* hover tooltip / trace / draw preview): those glide constantly and must
* not flicker widgets. The map reads this off its registered host set
* rather than re-querying the DOM.
*/
getVisibleRect(): DOMRect | null;
}
export declare class OmMapElement extends HTMLElementBase {
private core;
private observer;
private history;
private keydownHandler?;
private keyupHandler?;
private dblclickHandler?;
private contextmenuHandler?;
private mount;
private reconcilePending;
private readonly dataOwner;
private dataReleasePending;
/** Decoded property tables per pick-features layer — see RuntimeCoreCallbacks.onFeatureTable. */
private featureTables;
private layerIRs;
private selection;
/**
* The pointer event behind the most recent pick — kept SEPARATELY from
* `selection` because empty picks store `selection = null`, losing their
* type, yet `selection-type`-scoped overlays must tell a click on empty
* space (dismisses a click popup) from a hover over empty space (inert).
*/
private lastPickType;
private widgets;
private overlays;
private overlayFlushPending;
private errorPanel;
private runtimeErrors;
/** Dedup guard for the approximate-BIM-georeferencing warning — same shape as applyLicenseGates' own `warned` set, keyed per layer id. */
private warnedApproximatePlacement;
/** Dedup guard for the no-terrain-for-elevation error — same shape as `warnedApproximatePlacement`. */
private erroredNoTerrainForElevation;
private readyFired;
private firstSettleDrawDone;
/** Custom-state holder for the `om-collapsed` height-floor marker — see applyHeightFloor. */
private internals;
private resolveReady;
readonly ready: Promise;
private readonly viewSettle;
private firstReconcileDone;
private rendererLoadPending;
private loadDispatched;
connectedCallback(): void;
disconnectedCallback(): void;
/** Library-owned stacking layer: one absolutely-positioned flex container per managed slot, created lazily. Rendering plumbing, never authored state. */
private widgetLayer;
private readonly slotContainers;
private readonly mandatedChromeHosts;
/** Slots that host mandated chrome (badge/attribution) — set at host creation, so the collision-dim exemption is a slot-level flag, not a per-flush DOM scan. */
private readonly slotsWithMandatedChrome;
private widgetResizeObserver;
private widgetsFolded;
private foldPassPending;
private inFoldPass;
private readonly foldMapId;
private readonly foldedWidgetSlots;
private readonly foldDrawers;
/**
* Is this node layout plumbing (the widget layer, slot containers, or
* cluster wrappers)? Their childList churn is rendering, not manifest
* edits. om-widget elements themselves are NOT plumbing — their attribute
* edits stay manifest-visible.
*/
private isWidgetLayoutPlumbing;
/**
* Places an into its managed slot container (creating layer/
* container on demand), or back into the light DOM for the manual tier.
* The reparent is RENDERING PLUMBING: history-suppressed (undo must never
* replay it) and filtered out of manifest-change detection. The widget's
* authored state — its `position` attribute — never changes here.
*/
/** Per-widget comment anchors marking the AUTHORED light-DOM position — a managed→manual flip restores the widget where the author wrote it, not at the end. */
private readonly slotAnchors;
/** The widget layer div — lazy, shared by slot containers AND the layout tokens (both lanes put tokens on the layer, never on an authored element). */
private ensureWidgetLayer;
private ensureSlotContainer;
/**
* Provider attribution and the free-license badge are immutable members of
* managed slots. Their stable wrappers stay front-end-owned while renderer
* adapters mount/unmount the actual controls inside.
*/
private ensureMandatedChromeHost;
/** True while an internal reparent (slotting/manual-flip) is moving a widget — its transient disconnect must NOT be read as an author removal. */
private inSlotReparent;
/** Wrap an internal reparent: suppress history AND flag it as a move so unregisterWidgetInternal keeps the fold/anchor bookkeeping. */
private reparent;
slotWidgetInternal(el: HTMLElement, slot: WidgetSlot): void;
/** Called by when its live `fold` attribute changes. */
refreshWidgetFoldInternal(): void;
private startWidgetFoldObserver;
private foldBreakpointRaw?;
private foldBreakpointCache;
/** Cached breakpoint resolution — re-probes only when the token string actually changes, not on every ResizeObserver tick. */
private foldBreakpointPx;
private measureWidgetFold;
private updateWidgetFoldForWidth;
/** Lift the bottom-end row clear of the bottom drawer toggle while folded. Applied here AND in the fold pass so a lazily-created bottom-end container still gets it. */
private syncFoldBottomOffset;
private scheduleFoldPass;
private shouldFoldWidget;
private ensureFoldDrawer;
private setFoldDrawerOpen;
private focusFirstIn;
private restoreFoldedWidget;
private runFoldPass;
private clusterPassPending;
/** Re-entry guard: the pass's own reparenting fires connect callbacks that call back into scheduleClusterPass — self-inflicted, never re-schedule. */
private inClusterPass;
/** Microtask-coalesced: one pass per placement burst (boot slots N widgets). */
private scheduleClusterPass;
/**
* Re-derives cluster wrappers in every slot container: runs of ≥2
* adjacent compact widgets (in VISUAL order — `order` attr, then DOM)
* merge into one `data-om-cluster` wrapper div carrying the group's
* radius/shadow/divider look; the widgets' own shells flatten via the
* `clustered` class they toggle at render. Unwrap-then-rewrap per pass:
* wrappers are cheap plumbing divs, and idempotence beats bookkeeping.
* Members hidden by hide-all are excluded — a hidden widget can't cluster,
* so a group never leaves a ghost wrapper pill around invisible members.
* The re-entry guard is inline (the pass's own reparenting fires connect
* callbacks that call back into scheduleClusterPass — self-inflicted).
*/
private runClusterPass;
/**
* `widgets-hidden` — visibility:hidden + pointer-events:none on every
* widget EXCEPT attribution and the toggle itself (attribution never
* hides — license compliance). Never removal: an open listbox or
* mid-scrub slider survives. Inline styles are history-excluded plumbing.
*/
private applyWidgetsHidden;
private applyHiddenToWidget;
/** Custom properties applied from widget-style — tracked so an edit reverts removed pairs to their defaults (CSSStyleDeclaration iteration of custom props is not portable). */
private appliedWidgetStyleProps;
/**
* `widget-style` sugar → custom properties on the WIDGET LAYER, never the
* authored element (matching the React lane): inline style on an
* authored element would collide with author styles and leak into any
* outerHTML serialization. Unparseable/out-of-range pairs are skipped
* live; validation carries the loud version. Author page-CSS tokens on
* `om-map { --om-widget-*: … }` still cascade INTO the layer; the sugar
* (being inline on the layer) wins over page CSS, matching attr > CSS.
*/
private applyWidgetStyle;
registerWidgetInternal(el: Element, watch: string[], notify: (ctx: RuntimeContext) => void): void;
unregisterWidgetInternal(el: Element): void;
registerOverlayInternal(overlay: OverlayHost): void;
unregisterOverlayInternal(overlay: OverlayHost): void;
/**
* rAF-batched (spec: "Overlay Renderer" — the callback only stores the
* latest state and schedules one requestAnimationFrame; every overlay's
* `viewport.project()` + DOM write happens in that one flush, coalescing
* multiple viewport/selection changes per frame into one DOM pass).
*/
private scheduleOverlayFlush;
/**
* Dynamic overlap avoidance (spec: "Widget Layout Manager / Overlap
* avoidance") — a slot container whose rect intersects an OPEN
* `` popup is dimmed (`--om-widget-opacity-dimmed`, default
* 0.35) rather than repositioned. Own overlays only (issue Q1: foreign
* absolutely-positioned DOM is unknowable). `widgets-dim="off"` opts out.
* Real-geometry only — verified behaviorally in the layout audit, never
* modeled (headless has no rects; this no-ops there).
*/
private dimSlotsAgainstOverlays;
/** widgets-toggle can sit in any author-chosen slot, so still scan for it (attribution/badge are covered by the slot flag). */
private slotHasNeverHidesWidget;
emit(event: string, payload?: Record): void;
/** Recenters (and optionally rezooms) the map — instant, not an animated fly. */
flyTo(coords: [number, number], zoom?: number, opts?: {
duration?: number;
curve?: boolean;
}): void;
setLayerVisible(id: string, visible: boolean): void;
/** A snapshot of the current layer IRs — the same shape `ctx.layers` is built from. */
getLayers(): readonly LayerIR[];
private notifyWidgets;
private buildCurrentCtx;
/** Picked object, normalized to flat — GeoJSON properties lifted; columnar rows materialized from columns at the pick index. */
private resolvePickedObject;
/**
* The single selection path — deck.gl's real picks (via RuntimeCore's
* onSelectionChange callback) and synthetic harness picks
* (injectPickInternal) both land here, so flattening, columnar row
* materialization, widget notification, overlay flush, and behavior
* dispatch are same-path by construction.
*
* Flattening happens here (not in toSelection()) — this is the one place
* with access to the picked layer's shape, needed for GeoJSON `{{field}}`
* template interpolation against ctx.selection.object (spec: "Because the
* picked object is already normalized to flat..."). Columnar layers pick
* with no object at all (deck.gl's non-iterable data path only reports the
* index) — materialized here, the one place with both the index and the
* layer's columns.
*/
private handleSelectionChange;
/**
* The single drag-pick path — deck.gl's continuous drag picks (via
* RuntimeCore's onDragPick callback) and synthetic harness drags
* (injectDragPickInternal) both land here. Deliberately does NOT touch
* ctx.selection (spec: a widget watching `selection` shouldn't re-render
* on every drag-move) — it only dispatches `on="drag"` behaviors.
*/
private handleDragPick;
/**
* Synthetic pick entries for the test harness (spec: "Consumer Testing
* Surface") — internal-suffix API like registerWidgetInternal, consumed
* by mountForTest's pick()/clearSelection(), not part of the public
* authoring surface.
*/
injectPickInternal(selection: Selection | null, pickType?: "hover" | "click"): void;
injectDragPickInternal(selection: Selection): void;
/** Every click/hover map coordinate (spec: "Manual Drawing"): drives the
* internal draw controller AND fires the public `om-map-point` event, so
* consumer capture tools the built-in draw widget doesn't cover
* (rectangle/circle AOIs) can subscribe. The two consumers are
* DECOUPLED: the event dispatches first (dispatchEvent isolates a
* throwing listener), then the draw controller runs — so neither a
* throwing draw session nor a throwing listener can starve the other.
* Fires at pointer rate on hover (no debounce — vertex capture needs
* every point); heavy listeners should throttle their own work. */
private handleMapPoint;
/**
* XY snapping's snap-tip (spec: issue #34 Part A — "a snap tip shows
* which agent fired on which layer, otherwise users can't tell why a
* vertex jumped"). A SEPARATE runtime overlay from the shared
* `show-tooltip` singleton (`actions.ts`'s `getOrCreateTooltipOverlay`):
* that one is `anchor-from="selection"` (tied to `ctx.selection`) and
* shared across every `show-tooltip` behavior a page authors (this
* page's own z-tooltip, for one) — reusing it here would fight over the
* same element on every hover. This one anchors at a plain, static
* `anchor="[lng,lat]"` instead, since a snap position has nothing to do
* with whatever `ctx.selection` currently holds.
*/
private ensureSnapTipOverlay;
private handleSnapPoint;
/** Harness map-point injection (spec: "Consumer Testing Surface") — the
* same path a real deck click/hover coordinate takes, so the om-map-point
* event and custom capture tools are testable without a GPU. */
/**
* The map's "done drawing" promise (issue #37): resolves once every live
* 3D tileset has refined for the CURRENT view and one further animation
* frame has been painted — the await a pull-model frame renderer (or a
* consumer Playwright test) needs before screenshotting. Resolves
* `{settled:false}` with a console warning on timeout rather than
* rejecting (a late frame is ordinary pop-in, not an error). v1 gates 3D
* tilesets only — basemap raster tiles are not yet awaited.
*/
whenSettled(opts?: {
timeout?: number;
}): Promise<{
settled: boolean;
}>;
/** Test surface: is the determinism switch active on the core (issue #37 — must be true BEFORE the first render when data-om-recording is authored; the basemap adapter reads it in its constructor). */
isDeterministicInternal(): boolean;
injectMapPointInternal(coordinate: [number, number] | null, kind?: "click" | "hover", pointerType?: string): void;
/** Harness setView (spec: "Consumer Testing Surface") — the one path that reaches pitch/bearing. */
setViewInternal(partial: {
longitude?: number;
latitude?: number;
zoom?: number;
pitch?: number;
bearing?: number;
}): void;
/** Cancels all channel-driven effects (trace/pulse), clears their per-frame patches, and removes runtime temp trace layers — story scrub restore. */
clearEffectsInternal(): void;
/** A layer's current per-frame channel patch (tests/e2e assert trace progress through this). */
getAnimatedPropsInternal(layerId: string): Record | undefined;
/** Current camera state (story initial-state capture) — internal-suffix API. */
getViewStateInternal(): {
longitude: number;
latitude: number;
zoom: number;
pitch: number;
bearing: number;
} | undefined;
/** Live Tileset3D per currently-present 3D-tiles layer — the paced-flyby driver's isLoaded() gate (same surface the warm-tiles action reads). */
getLiveTilesetsInternal(): Map;
/** Rendered viewport size — the paced driver's fly-to arc math needs real dims (undefined until the renderer has one). */
getViewportSizeInternal(): {
width: number;
height: number;
} | undefined;
/** Resolved scene-lighting IR (headless test inspection) — null = deck default lights. */
getLightingInternal(): LightingIR | null;
/** Resolved terrain IR (headless test inspection) — null = no surface. */
getTerrainInternal(): TerrainIR | null;
/** Resolved clip-box IR (headless test inspection) — null = no active box. */
getClipBoxInternal(): ClipBoxIR | null;
/**
* Canvas-only scene snapshot (spec: "Snapshot API") — basemap + deck
* composited at device pixels. DOM widgets/overlays/badge/attribution are
* NOT captured: consumers exporting imagery must render provider credits
* themselves. Await `ready` first; headless maps reject.
*/
snapshot(opts?: SnapshotOptions): Promise;
/** terrain* attributes → IR, key-substituted like basemap presets; resolution problems log once per apply. */
private resolveTerrainAttr;
/** Projects a lng/lat through the current viewport (harness pixel derivation) — undefined before the viewport resolves. */
projectInternal(lngLat: [number, number]): [number, number] | undefined;
/** Renderer finished async init — defer to the first reconcile if it hasn't run yet (see the field comment). */
private handleRendererLoad;
private dispatchRendererLoad;
/** Fires `om-map-ready` / resolves `.ready` once the first reconcile ran, the renderer is up, and no declared data URL is still loading. */
private checkReady;
/**
* Per-element half of the base-layout guardrail: mark the host `om-collapsed`
* so the layered `min-height` floor applies, but ONLY while it genuinely
* measures zero. Gating the floor this way is what lets an explicit author
* height win outright — a map with any height never carries the state, so the
* floor rule never matches it and a 300px map stays 300px.
*
* Clear-then-re-measure makes it idempotent and self-correcting: the connect-
* time call runs mid-parse, where a parent sized by later siblings still
* measures zero, and the post-ready call drops a floor that turned out to be
* unnecessary.
*
* A custom state rather than an attribute or inline style, deliberately: the
* host's own MutationObserver treats attribute writes as manifest edits (a
* reconcile), history.ts records them as undo steps, and either would
* serialize into a saved manifest. A custom state touches none of that.
*/
private applyHeightFloor;
/**
* First-shot guardrail: warn (once) if the map has no visible size once it is
* ready. The base-layout default covers a bare ``, but a constrained
* parent (a 0-height flex/grid cell, `height:100%` under an unsized ancestor)
* can still collapse it — a silent blank map otherwise.
* Skipped headless: jsdom/happy-dom report 0×0 by design, and `mountForTest`
* sets the `headless` attribute, so this never fires in the test harness.
*/
private checkVisibleSize;
private pickPayload;
/**
* Dispatches to every `` (live-queried, like
* the toggle-layer action's own `` lookups — behaviors are
* static config, not something worth maintaining a registry for). Every
* attribute besides `on`/`layer`/`action` becomes an extra fixed payload
* key (kebab->camel), merged with the pick-derived payload — e.g.
* `target`/`anchor-offset` for show-overlay, `template` for show-tooltip.
*/
private dispatchToBehaviors;
/**
* Microtask-coalesced reconcile (spec: "MutationObserver burst batching").
* Any number of calls within one synchronous block collapse into a single
* reconcile() — an LLM streaming several attribute edits in one script
* shouldn't trigger a reconcile per edit.
*/
private scheduleReconcile;
private reconcile;
/**
* Re-runs on every reconcile when the `validate` attribute is present —
* live editing feedback, not just the `OmMap.validate(htmlString)`
* pre-flight (which calls `validateManifest` directly, standalone).
* Runtime errors (the error boundary) are folded in too, so the agent
* sees one consistent stream regardless of origin. `validate="silent"`
* still runs validation and fires the event, just skips the on-page panel
* (spec: "for teams building their own error UI").
*/
/**
* Reports a runtime-discovered validation entry — deck.gl crashes and
* license-limit violations already flow through this (via `onRuntimeError`
* below); this is also the entry point for facts a WIDGET discovers
* directly (no round trip through RuntimeCore) since it already holds an
* `` reference — see `ifc-loader`'s approximate-georeferencing
* warning. Same access-pattern precedent as `registerWidgetInternal`
* above: real and callable, not part of the documented public API.
*/
reportRuntimeErrorInternal(entry: ValidationEntry): void;
private publishValidation;
}
declare global {
interface HTMLElementTagNameMap {
"om-map": OmMapElement;
}
interface HTMLElementEventMap {
/** Camera settled (trailing-debounced) — detail is the full camera state. */
"om-view-changed": CustomEvent<{
longitude: number;
latitude: number;
zoom: number;
pitch: number;
bearing: number;
/** "user" if any change in the settled burst came from a canvas gesture; "programmatic" for pure API/action/story moves. */
origin: "user" | "programmatic";
}>;
/** Every click/hover's map coordinate (null when the pointer is off any
* geometry deck can unproject) — the consumer hook for custom capture
* tools beyond the built-in draw widget. */
"om-map-point": CustomEvent<{
coordinate: [number, number] | null;
kind: "click" | "hover";
}>;
/** A Tile3DLayer finished loading its root tileset — detail carries the
* authored layer id and the live deck `Tileset3D` for tools (e.g. region
* export) that need the real tileset, not the IR. */
"om-tileset-load": CustomEvent<{
layerId: string;
tileset: unknown;
}>;
/** A Route layer finished resolving its route (direct geometry or a
* RoutingProvider round-trip) — detail carries the authored layer id and
* the normalized route (geometry/distanceMeters/durationSec/legs/bounds),
* so a page can read a provider-resolved route without re-fetching it. */
"om-route-resolved": CustomEvent<{
layerId: string;
route: {
geometry?: {
type: "LineString";
coordinates: [number, number][];
};
distanceMeters?: number;
durationSec?: number;
legs?: {
distanceMeters: number;
durationSec: number;
}[];
bounds?: [[number, number], [number, number]];
};
}>;
}
}
export {};