Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 226 additions & 1 deletion src/essence/Basics/MapEngines/Adapters/DeckGLAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ import type {
MapInitOptions,
BasemapOptions,
} from '../types/view'
import type { LayerOptions } from '../types/layers'
import type { LayerOptions, OverlayOptions } from '../types/layers'
import type {
MapEventHandler,
MapEventOptions,
FeatureInteractionHandler,
FeaturePickResult,
QueryFeaturesOptions,
DrawShape,
DrawingOptions,
} from '../types/events'

import {
Expand All @@ -60,6 +62,8 @@ import {
resolveLayerId,
pickInfoToResult,
buildDeckLayer,
buildDrawnFeature,
buildPreviewFeature,
} from './DeckGLHelpers'

/**
Expand Down Expand Up @@ -194,6 +198,17 @@ export class DeckGLAdapter implements IMapEngine<Deck, Layer, PickingInfo> {
private _featureClickHandler: FeatureInteractionHandler | null = null
private _featureHoverHandler: FeatureInteractionHandler | null = null

private _drawingShape: DrawShape | null = null
private _drawingVertices: { lat: number; lng: number }[] = []
private _drawingCursor: { lat: number; lng: number } | null = null
private _drawingStyle: Record<string, unknown> = {}
private _drawingPreviewLayerId: string | null = null
private _drawingEscapeHandler: ((e: KeyboardEvent) => void) | null = null
private _drawingFinishing = false

/** Registry of anchored HTML overlays (id -> teardown function). */
private _overlays = new Map<string, () => void>()

/**
* Bound handler kept as a class field so it can be removed cleanly in {@link destroy}.
* Syncs `_viewState` from the basemap and emits the engine-level `'moveend'` event.
Expand Down Expand Up @@ -292,6 +307,15 @@ export class DeckGLAdapter implements IMapEngine<Deck, Layer, PickingInfo> {
this._deck = null
}

this._overlays.forEach((teardown) => {
try {
teardown()
} catch {
// ignore — destroy must remain idempotent
}
})
this._overlays.clear()

this._layers.clear()
this._layerZIndices.clear()
this._eventListeners.clear()
Expand Down Expand Up @@ -328,6 +352,69 @@ export class DeckGLAdapter implements IMapEngine<Deck, Layer, PickingInfo> {
return this._container
}

/**
* Anchored HTML overlay. deck.gl renders to canvas and has no native
* overlay system, so we own the DOM node directly: append to the
* container, project lat/lng -> pixel on every view change, reposition.
*/
addOverlay(options: OverlayOptions): void {
if (!options?.id) {
throw new Error('addOverlay: options.id is required')
}
if (this._overlays.has(options.id)) {
this.removeOverlay(options.id)
}

const container = this._container
if (!container) return

const node = document.createElement('div')
node.style.position = 'absolute'
node.style.zIndex = '500'
container.appendChild(node)

let userCleanup: (() => void) | void
try {
userCleanup = options.mount(node)
} catch (err) {
console.warn('[DeckGLAdapter] addOverlay mount threw:', err)
}

const reposition = (): void => {
try {
const pt = this.latLngToContainerPoint(options.latlng) as {
x: number
y: number
}
node.style.left = pt.x - node.offsetWidth / 2 + 'px'
node.style.top = pt.y - node.offsetHeight / 2 + 'px'
} catch {
// projection not ready yet — try again on next view change
}
}
reposition()
this.on('move', reposition)
this.on('moveend', reposition)

this._overlays.set(options.id, () => {
this.off('move', reposition)
this.off('moveend', reposition)
try {
if (typeof userCleanup === 'function') userCleanup()
} catch (err) {
console.warn('[DeckGLAdapter] addOverlay cleanup threw:', err)
}
if (node.parentNode) node.parentNode.removeChild(node)
})
}

removeOverlay(id: string): void {
const teardown = this._overlays.get(id)
if (!teardown) return
teardown()
this._overlays.delete(id)
}

setView(center: LatLngLike, zoom?: number, options?: ViewOptions): void {
const { lat, lng } = resolveLatLng(center)
if (this._isOverlayMode) {
Expand Down Expand Up @@ -724,6 +811,130 @@ export class DeckGLAdapter implements IMapEngine<Deck, Layer, PickingInfo> {
this._emitEvent(eventName, data)
}

enableDrawing(shape: DrawShape, options: DrawingOptions = {}): void {
if (this._drawingShape) {
this.disableDrawing()
}

this._drawingShape = shape
this._drawingVertices = []
this._drawingStyle = (options.style as Record<string, unknown>) ?? {}
this._drawingPreviewLayerId = `__aoi-draw-preview-${Math.random()
.toString(36)
.slice(2, 8)}`

if (options.cancelOnEscape !== false) {
this._drawingEscapeHandler = (e: KeyboardEvent) => {
if (e.key === 'Escape') this.disableDrawing()
}
document.addEventListener('keydown', this._drawingEscapeHandler)
}

this._emitEvent('drawstart', { shape })
}

disableDrawing(): void {
if (!this._drawingShape) return

const shape = this._drawingShape

this._removeDrawingPreview()
this._drawingShape = null
this._drawingVertices = []
this._drawingCursor = null
this._drawingPreviewLayerId = null
this._drawingStyle = {}

if (this._drawingEscapeHandler) {
document.removeEventListener('keydown', this._drawingEscapeHandler)
this._drawingEscapeHandler = null
}

if (!this._drawingFinishing) {
this._emitEvent('drawcancel', { shape })
}
}

finishDrawing(): void {
if (!this._drawingShape) return
const shape = this._drawingShape
const vertices = this._drawingVertices.slice()

const feature = buildDrawnFeature(shape, vertices)
if (!feature) {
this.disableDrawing()
return
}

this._drawingFinishing = true
this.disableDrawing()
this._drawingFinishing = false
this._emitEvent('drawcomplete', { feature })
}

isDrawing(): boolean {
return this._drawingShape !== null
}

private _handleDrawClick(info: PickingInfo): void {
const c = info.coordinate
if (!c || !this._drawingShape) return
const vertex = { lat: c[1], lng: c[0] }
this._drawingVertices.push(vertex)

this._renderDrawingPreview()
this._emitEvent('drawvertex', {
shape: this._drawingShape,
vertices: this._drawingVertices.slice(),
})

if (
(this._drawingShape === 'rectangle' || this._drawingShape === 'circle') &&
this._drawingVertices.length >= 2
) {
this.finishDrawing()
}
}

private _handleDrawHover(info: PickingInfo): void {
const c = info.coordinate
if (!c || !this._drawingShape) return
if (this._drawingVertices.length === 0) return
this._drawingCursor = { lat: c[1], lng: c[0] }
this._renderDrawingPreview()
}

private _renderDrawingPreview(): void {
if (!this._drawingShape || !this._drawingPreviewLayerId) return
const previewFeature = buildPreviewFeature(
this._drawingShape,
this._drawingVertices,
this._drawingCursor ?? undefined
)
if (!previewFeature) return
const id = this._drawingPreviewLayerId
const opts: LayerOptions = {
id,
type: 'vector',
style: this._drawingStyle,
interactive: false,
geojson: { type: 'FeatureCollection', features: [previewFeature] },
} as LayerOptions
const next = buildDeckLayer(id, opts)
this._layers.set(id, next)
this._syncLayers()
}

private _removeDrawingPreview(): void {
const id = this._drawingPreviewLayerId
if (!id) return
if (this._layers.has(id)) {
this._layers.delete(id)
this._layerZIndices.delete(id)
this._syncLayers()
}
}

onFeatureClick(handler: FeatureInteractionHandler): void {
this._featureClickHandler = handler
}
Expand Down Expand Up @@ -822,9 +1033,16 @@ export class DeckGLAdapter implements IMapEngine<Deck, Layer, PickingInfo> {
this._emitEvent('moveend', clamped)
},
onClick: (info: PickingInfo) => {
if (this._drawingShape) {
this._handleDrawClick(info)
return
}
this._featureClickHandler?.(pickInfoToResult(info))
},
onHover: (info: PickingInfo) => {
if (this._drawingShape) {
this._handleDrawHover(info)
}
this._featureHoverHandler?.(pickInfoToResult(info))
},
} as any)
Expand Down Expand Up @@ -889,9 +1107,16 @@ export class DeckGLAdapter implements IMapEngine<Deck, Layer, PickingInfo> {
interleaved: false,
layers: [],
onClick: (info: PickingInfo) => {
if (this._drawingShape) {
this._handleDrawClick(info)
return
}
this._featureClickHandler?.(pickInfoToResult(info))
},
onHover: (info: PickingInfo) => {
if (this._drawingShape) {
this._handleDrawHover(info)
}
this._featureHoverHandler?.(pickInfoToResult(info))
},
})
Expand Down
Loading