import "maplibre-gl/dist/maplibre-gl.css"; import type { DeckProps, Viewport } from "@deck.gl/core"; import type { ResolvedBasemap } from "./basemap-registry"; import type { ViewOrigin } from "./external-store"; export interface ViewportLike { project(xyz: number[]): number[]; getBounds(): [number, number, number, number]; width: number; height: number; } /** What `RuntimeCore.getViewport()` returns in either mode — deck.gl's own `Viewport` (standalone) or this adapter's narrower shape (with a basemap). */ export type MapViewport = Viewport | ViewportLike; export interface BasemapViewState { longitude: number; latitude: number; zoom: number; pitch: number; bearing: number; } export declare class MapLibreBasemapAdapter { private map; private overlay; /** Our own compact attribution control (spec: "attribution becomes correct, not optional") — undefined when the author opted out. */ private attribution?; private attributionElement?; private attributionText?; private showAttribution; private attributionHost?; /** * One-time "has the map initialized at least once" flag, set by the first * `load` event. Deliberately NOT `this.map.loaded()` — that method means * something different (and narrower): "is everything currently settled — * no in-flight camera transition, no tiles still loading for the current * viewport." It goes false for the ENTIRE DURATION of a pan/zoom (while * new tiles fetch), which made `getViewport()` return `undefined` for the * whole gesture — the CPU-side viewport-filter fallback ("no viewport * resolved yet — include everything") kicked in for every intermediate * frame, and since MapLibre's `move` event stops firing once the camera * actually settles, the LAST widget render before settling was often * still mid-gesture and never got superseded — visibly "stuck" showing * unfiltered data after a pan, exactly the bug this fixes. */ private initialized; private loadCallbacks; constructor(container: HTMLElement, resolved: ResolvedBasemap, initialView: { longitude: number; latitude: number; zoom: number; pitch?: number; bearing?: number; }, deckProps: DeckProps, showAttribution?: boolean, attributionHost?: HTMLElement, deterministic?: boolean); /** * whenSettled's basemap gate (issue #37 follow-up), as an EVENT LATCH * rather than a polling predicate: MapLibre's `idle` event is its own * definition of done — camera transitions finished, all requested tiles * loaded, all fades/transitions complete — which is strictly stronger * than `loaded() && areTilesLoaded()` (those say nothing about label * fade/collision animation, the measured source of warm-page frame * nondeterminism). The latch clears on anything that restarts work * (camera writes, a source starting to load, a style change) and sets * again only on the next `idle`, so `isIdle()` stays a synchronous read — * settleOnce's `loaded()` shape — and both lanes plus `onlymapjs record` * inherit it unchanged. */ private idleLatch; private wireIdleLatch; isIdle(): boolean; /** Camera writes from OUR side must clear the latch immediately — the map's own movestart fires a tick later, and a settle read in that gap would see stale idle. */ private markBusy; /** The overlay deck's live layers — whenSettled's readiness gate (issue #37). */ getDeckLayers(): { isLoaded: boolean; }[] | undefined; /** One forced draw of both canvases (no capture) — whenSettled's "at least one completed render" guarantee (issue #37). */ forceRepaint(): void; /** Snapshot capture queue for the overlay's deck canvas — see captureComposite. */ private deckCaptures; /** In-flight captureComposite rejects — destroy() settles them so a caller never hangs on a torn-down renderer. */ private pendingCaptureRejects; private drainDeckCaptures; /** * Scene snapshot (spec: "Snapshot API") — one composite canvas of the * basemap + deck canvases at device pixels. Neither context keeps its * drawing buffer, so each canvas is copied synchronously inside its own * post-render callback after a forced repaint: the basemap via * `map.once("render")` + triggerRepaint, the deck canvas via the * overlay's onAfterRender (forced through the overlay's internal Deck — * private-by-underscore, but it's OUR bundled deck, `redraw(reason)` is * public Deck API, and the access is optional-chained). */ captureComposite(): Promise; private markInitialized; onLoad(cb: () => void): void; /** Fires continuously during pan/zoom/rotate — MapLibre's own camera-change event, standing in for deck's onViewStateChange (which doesn't apply — deck isn't driving the camera). */ /** * Viewport-change stream with origin tagging (spec: "External-Store * Contract") — MapLibre attaches `originalEvent` (the DOM pointer/wheel * event) to gesture-driven move events; our own easeTo/jumpTo/fitBounds * fire without one. The ecosystem's standard user-vs-programmatic signal. */ onMove(cb: (origin: ViewOrigin) => void): void; setLayers(layers: DeckProps["layers"]): void; /** * Scene lighting (spec: "Scene Lighting") — MapboxOverlay accepts full * DeckProps, effects included. An effects-only setProps doesn't mark the * basemap frame dirty, so nudge a repaint; the overlay redraws with the * map's render pass. */ setEffects(effects: DeckProps["effects"]): void; /** XY snapping (spec: issue #34 Part A) — deck's own pick tolerance, 0 by default; MapboxOverlay forwards the same DeckProps a standalone Deck instance takes. */ setPickingRadius(pickingRadius: number): void; /** * Live basemap switch (spec: "Basemap presets & switching"). Deck layers * survive BY CONSTRUCTION: the MapboxOverlay is a map control, not style * layers, and controls persist across `setStyle` — as does the camera. * MapLibre diffs same-source styles and falls back to a full style reload * internally when the diff fails (the cross-provider case). */ setStyle(resolved: ResolvedBasemap): void; /** * (Re)mounts the compact attribution control. Source-declared attributions * (what OpenFreeMap/CARTO/OSM styles carry) are read live by the control on * every style change; only the PRESET-level `attribution` override needs a * remount, since `customAttribution` is fixed at construction. */ private mountAttribution; /** * Toggle map interactions (spec: "Manual Drawing", D4 — doubleClickZoom; * issue #34's clip-box gizmo — dragPan) — used to suspend a MapLibre * gesture while a tool needs sole ownership of it (double-click-zoom * while a draw tool is active; drag-pan while a clip-box face handle is * grabbed, or it fights the map's own camera drag for every mousemove). */ setInteractive(opts: { doubleClickZoom?: boolean; dragPan?: boolean; }): void; isLoaded(): boolean; getViewport(): ViewportLike; getViewState(): BasemapViewState; /** Instant recenter/rezoom — the basemap's own camera jump, no deck viewState involved. */ jumpTo(center: [number, number], zoom?: number): void; /** * Animated camera (spec: "Map Stories / Animation primitives") — MapLibre's * own easing. `curve` uses `flyTo` (the arcing zoom-out-and-in) instead of * the direct `easeTo` interpolation. duration 0 degrades to an instant * move, so this is also the one method that reaches pitch/bearing. */ easeTo(view: { center?: [number, number]; zoom?: number; pitch?: number; bearing?: number; }, durationMs: number, curve?: boolean): void; fitBounds(bounds: [[number, number], [number, number]], padding: number, durationMs?: number): void; destroy(): void; }