348 lines
9.9 KiB
TypeScript
348 lines
9.9 KiB
TypeScript
import {div} from "@bokehjs/core/dom"
|
|
import type * as p from "@bokehjs/core/properties"
|
|
import {isNumber} from "@bokehjs/core/util/types"
|
|
import {LayoutDOM, LayoutDOMView} from "@bokehjs/models/layouts/layout_dom"
|
|
import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source"
|
|
|
|
import {debounce} from "debounce"
|
|
|
|
import {transform_cds_to_records} from "./data"
|
|
import {set_size} from "./layout"
|
|
import {GL} from "./lumagl"
|
|
import {makeTooltip} from "./tooltips"
|
|
|
|
function extractClasses() {
|
|
// Get classes for registration from standalone deck.gl
|
|
const classesDict: any = {}
|
|
const deck = (window as any).deck
|
|
const classes = Object.keys(deck).filter(x => x.charAt(0) === x.charAt(0).toUpperCase())
|
|
for (const cls of classes) {
|
|
classesDict[cls] = deck[cls]
|
|
}
|
|
const carto = (window as any).CartoLibrary
|
|
const layers = Object.keys(carto.CARTO_LAYERS).filter(x => x.endsWith("Layer"))
|
|
for (const layer of layers) {
|
|
classesDict[layer] = carto.CARTO_LAYERS[layer]
|
|
}
|
|
return classesDict
|
|
}
|
|
|
|
function extractFunctions() {
|
|
const carto = (window as any).CartoLibrary
|
|
const sources = Object.keys(carto.CARTO_SOURCES).filter(x => x.toLowerCase().endsWith("source"))
|
|
const functionDict: any = {}
|
|
for (const src of sources) {
|
|
functionDict[src] = carto.CARTO_SOURCES[src]
|
|
}
|
|
return functionDict
|
|
}
|
|
|
|
export class DeckGLPlotView extends LayoutDOMView {
|
|
declare model: DeckGLPlot
|
|
|
|
jsonConverter: any
|
|
deckGL: any
|
|
_connected: any[]
|
|
_map: any
|
|
_layer_map: any
|
|
_view_cb: any
|
|
_initialized: boolean = false
|
|
|
|
override connect_signals(): void {
|
|
super.connect_signals()
|
|
const {data, mapbox_api_key, tooltip, configuration, layers, initialViewState, data_sources} = this.model.properties
|
|
this.on_change([mapbox_api_key, tooltip, configuration], () => this.render())
|
|
this.on_change([data, initialViewState], () => this.updateDeck())
|
|
this.on_change([layers], () => this._update_layers())
|
|
this.on_change([data_sources], () => this._connect_sources(true))
|
|
this._layer_map = {}
|
|
this._connected = []
|
|
this._connect_sources()
|
|
}
|
|
|
|
override remove(): void {
|
|
this.deckGL.finalize()
|
|
super.remove()
|
|
}
|
|
|
|
_update_layers(): void {
|
|
this._layer_map = {}
|
|
this._update_data(true)
|
|
}
|
|
|
|
_connect_sources(render: boolean = false): void {
|
|
for (const cds of this.model.data_sources) {
|
|
if (this._connected.indexOf(cds) < 0) {
|
|
this.on_change(cds.properties.data, () => this._update_data(true))
|
|
this._connected.push(cds)
|
|
}
|
|
}
|
|
this._update_data(render)
|
|
}
|
|
|
|
override initialize(): void {
|
|
super.initialize()
|
|
const view_timeout = this.model.throttle.view || 200
|
|
this._view_cb = debounce((event: any) => this._on_viewState_event(event), view_timeout, false)
|
|
if ((window as any).deck.JSONConverter) {
|
|
const {CSVLoader, Tiles3DLoader} = (window as any).loaders;
|
|
(window as any).loaders.registerLoaders([Tiles3DLoader, CSVLoader])
|
|
const jsonConverterConfiguration: any = {
|
|
classes: extractClasses(),
|
|
functions: extractFunctions(),
|
|
// Will be resolved as `<enum-name>.<enum-value>`
|
|
enumerations: {
|
|
COORDINATE_SYSTEM: (window as any).deck.COORDINATE_SYSTEM,
|
|
GL,
|
|
},
|
|
// Constants that should be resolved with the provided values by JSON converter
|
|
constants: {
|
|
Tiles3DLoader,
|
|
},
|
|
}
|
|
this.jsonConverter = new (window as any).deck.JSONConverter({
|
|
configuration: jsonConverterConfiguration,
|
|
})
|
|
}
|
|
}
|
|
|
|
_update_data(render: boolean = true): void {
|
|
let n = 0
|
|
for (const layer of this.model.layers) {
|
|
let cds
|
|
n += 1
|
|
if ((n-1) in this._layer_map) {
|
|
cds = this.model.data_sources[this._layer_map[n-1]]
|
|
} else if (!isNumber(layer.data)) {
|
|
continue
|
|
} else {
|
|
this._layer_map[n-1] = layer.data
|
|
cds = this.model.data_sources[layer.data]
|
|
}
|
|
layer.data = transform_cds_to_records(cds)
|
|
}
|
|
if (render) {
|
|
this.updateDeck()
|
|
}
|
|
}
|
|
|
|
_on_click_event(event: any): void {
|
|
const click_state: any = {
|
|
coordinate: event.coordinate,
|
|
lngLat: event.coordinate,
|
|
index: event.index,
|
|
}
|
|
if (event.layer) {
|
|
click_state.layer = event.layer.id
|
|
}
|
|
this.model.clickState = click_state
|
|
}
|
|
|
|
_on_hover_event(event: any): void {
|
|
if (event.coordinate == null) {
|
|
return
|
|
}
|
|
const hover_state: any = {
|
|
coordinate: event.coordinate,
|
|
lngLat: event.coordinate,
|
|
index: event.index,
|
|
}
|
|
if (event.layer) {
|
|
hover_state.layer = event.layer.id
|
|
}
|
|
this.model.hoverState = hover_state
|
|
}
|
|
|
|
_on_viewState_event(event: any): void {
|
|
const view_state = {...event.viewState}
|
|
delete view_state.normalize
|
|
for (const p in view_state) {
|
|
if (p.startsWith("transition")) {
|
|
delete view_state[p]
|
|
}
|
|
}
|
|
const viewport = new (window as any).deck.WebMercatorViewport(view_state)
|
|
view_state.nw = viewport.unproject([0, 0])
|
|
view_state.se = viewport.unproject([viewport.width, viewport.height])
|
|
this.model.viewState = view_state
|
|
}
|
|
|
|
get child_models(): LayoutDOM[] {
|
|
return []
|
|
}
|
|
|
|
getData(): any {
|
|
const hover_timeout = this.model.throttle.hover || 100
|
|
const hover_cb = debounce((event: any) => this._on_hover_event(event), hover_timeout, false)
|
|
const data = {
|
|
...this.model.data,
|
|
layers: this.model.layers,
|
|
initialViewState: this.model.initialViewState,
|
|
onViewStateChange: (event: any) => this._sync_viewstate(event),
|
|
onClick: (event: any) => this._on_click_event(event),
|
|
onHover: hover_cb,
|
|
}
|
|
return data
|
|
}
|
|
|
|
_sync_viewstate(event: any): void {
|
|
if (this._map) {
|
|
const {longitude, latitude, ...rest} = event.viewState
|
|
this._map.jumpTo({center: [longitude, latitude], ...rest})
|
|
}
|
|
this._view_cb(event)
|
|
}
|
|
|
|
updateDeck(): void {
|
|
if (!this.deckGL) {
|
|
this.render()
|
|
return
|
|
}
|
|
const data = this.getData()
|
|
if ((window as any).deck.updateDeck) {
|
|
(window as any).deck.updateDeck(data, this.deckGL)
|
|
} else {
|
|
const results = this.jsonConverter.convert(data)
|
|
this.deckGL.setProps(results)
|
|
}
|
|
}
|
|
|
|
createDeck({mapboxApiKey, container, jsonInput, tooltip}: any): void {
|
|
let deckgl
|
|
try {
|
|
let configuration
|
|
if (this.model.configuration) {
|
|
configuration = eval(`(${ this.model.configuration })`)
|
|
} else {
|
|
configuration = null
|
|
}
|
|
this.jsonConverter.mergeConfiguration(configuration)
|
|
const props = this.jsonConverter.convert(jsonInput)
|
|
const getTooltip = makeTooltip(tooltip, props.layers)
|
|
if (props.mapStyle === null) {
|
|
props.map = null
|
|
} else if (props.mapStyle.includes("carto")) {
|
|
this._map = new (window as any).maplibregl.Map({
|
|
container,
|
|
style: props.mapStyle,
|
|
interactive: false,
|
|
})
|
|
props.controller = true
|
|
props.map = null
|
|
delete props.mapStyle
|
|
} else {
|
|
props.map = (window as any).mapboxgl
|
|
props.mapboxApiAccessToken = mapboxApiKey
|
|
}
|
|
deckgl = new (window as any).deck.DeckGL({
|
|
...props,
|
|
container,
|
|
getTooltip,
|
|
height: "100%",
|
|
width: "100%",
|
|
})
|
|
} catch (err) {
|
|
console.error(err)
|
|
}
|
|
return deckgl
|
|
}
|
|
|
|
override render(): void {
|
|
super.render()
|
|
const container = div({class: "deckgl"})
|
|
set_size(container, this.model, false)
|
|
const MAPBOX_API_KEY = this.model.mapbox_api_key
|
|
const tooltip = this.model.tooltip
|
|
const data = this.getData()
|
|
if ((window as any).deck.createDeck) {
|
|
this.deckGL = (window as any).deck.createDeck({
|
|
mapboxApiKey: MAPBOX_API_KEY,
|
|
container,
|
|
jsonInput: data,
|
|
tooltip,
|
|
})
|
|
} else {
|
|
this.deckGL = this.createDeck({
|
|
mapboxApiKey: MAPBOX_API_KEY,
|
|
container,
|
|
jsonInput: data,
|
|
tooltip,
|
|
})
|
|
}
|
|
this.shadow_el.appendChild(container)
|
|
this._initialized = false
|
|
}
|
|
|
|
resize(): void {
|
|
this.deckGL.redraw(true)
|
|
if (this._map) {
|
|
this._map.resize()
|
|
}
|
|
}
|
|
|
|
override after_layout(): void {
|
|
super.after_layout()
|
|
if (!this._initialized) {
|
|
this.resize()
|
|
}
|
|
this._initialized = true
|
|
}
|
|
|
|
override after_resize(): void {
|
|
super.after_resize()
|
|
this.resize()
|
|
}
|
|
}
|
|
|
|
export namespace DeckGLPlot {
|
|
export type Attrs = p.AttrsOf<Props>
|
|
export type Props = LayoutDOM.Props & {
|
|
data: p.Property<any>
|
|
data_sources: p.Property<ColumnDataSource[]>
|
|
initialViewState: p.Property<any>
|
|
layers: p.Property<any[]>
|
|
mapbox_api_key: p.Property<string>
|
|
tooltip: p.Property<any>
|
|
configuration: p.Property<string>
|
|
clickState: p.Property<any>
|
|
hoverState: p.Property<any>
|
|
throttle: p.Property<any>
|
|
viewState: p.Property<any>
|
|
}
|
|
}
|
|
|
|
export interface DeckGLPlot extends DeckGLPlot.Attrs { }
|
|
|
|
export class DeckGLPlot extends LayoutDOM {
|
|
declare properties: DeckGLPlot.Props
|
|
|
|
constructor(attrs?: Partial<DeckGLPlot.Attrs>) {
|
|
super(attrs)
|
|
}
|
|
|
|
static override __module__ = "panel.models.deckgl"
|
|
|
|
static {
|
|
this.prototype.default_view = DeckGLPlotView
|
|
|
|
this.define<DeckGLPlot.Props>(({Any, List, Str, Ref}) => ({
|
|
data: [ Any ],
|
|
data_sources: [ List(Ref(ColumnDataSource)), [] ],
|
|
clickState: [ Any, {} ],
|
|
hoverState: [ Any, {} ],
|
|
initialViewState: [ Any, {} ],
|
|
layers: [ List(Any), [] ],
|
|
mapbox_api_key: [ Str, "" ],
|
|
throttle: [ Any, {} ],
|
|
tooltip: [ Any, true ],
|
|
configuration: [ Str, "" ],
|
|
viewState: [ Any, {} ],
|
|
}))
|
|
|
|
this.override<DeckGLPlot.Props>({
|
|
height: 400,
|
|
width: 600,
|
|
})
|
|
}
|
|
}
|