// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import {Widget} from '@deck.gl/core'; import type {Deck, PickingInfo, Viewport, WidgetProps} from '@deck.gl/core'; import {render, JSX} from 'preact'; import {Popover, type PopoverProps} from './lib/components/popover'; import {UserContent} from './lib/components/user-content'; export type TooltipContent = { /** Anchor of the popup in world coordinates, e.g. [longitude, latitude]. * If not supplied, default to the mouse position where the popup was triggered. */ position?: number[]; /** Text content to display in the popup */ text?: string; /** HTML content to display in the popup. If supplied, `text` is ignored. */ html?: string; /** HTML element to attach inside the popup. */ element?: HTMLElement | null; /** Custom class name to add to the popup */ className?: string; /** CSS style overrides of the popup */ style?: Partial; }; export type InfoWidgetProps = WidgetProps & { /** View to attach to and interact with. Required when using multiple views */ viewId?: string | null; /** Determines the interaction mode of the widget */ mode: 'click' | 'hover'; /** Function to generate the popup contents from the selected element */ getTooltip?: ( info: PickingInfo, widget: InfoWidget ) => TooltipContent | string | null | undefined; /** Position popup relative to the anchor. * @default 'right' */ placement?: PopoverProps['placement']; /** Pixel offset * @default 10 */ offset?: PopoverProps['offset']; /** * Show an arrow pointing at the anchor. Optionally accepts a pixel size. * @default 10 */ arrow?: PopoverProps['arrow']; }; export class InfoWidget extends Widget { static defaultProps: Required = { ...Widget.defaultProps, id: 'info', viewId: null, mode: 'hover', getTooltip: undefined!, placement: 'right', offset: 10, arrow: 10 }; className = 'deck-widget-info'; placement = 'fill' as const; viewport?: Viewport; tooltip: Required | null = null; constructor(props: InfoWidgetProps) { super(props); this.setProps(this.props); } setProps(props: Partial) { this.viewId = props.viewId ?? this.viewId; super.setProps(props); } onAdd({deck}: {deck: Deck; viewId: string | null}) { this.deck = deck; } onRemove() { // Invoke clean up of preact hooks if (this.rootElement) { render(null, this.rootElement); } } onViewportChange(viewport) { this.viewport = viewport; this.updateHTML(); } onHover(info: PickingInfo): void { if (this.props.mode === 'hover') { this.tooltip = this._getTooltip(info); } } onClick(info: PickingInfo): void { if (this.props.mode === 'click') { this.tooltip = this._getTooltip(info); } } protected _getTooltip(info: PickingInfo): Required | null { if (!this.props.getTooltip) return null; const content = this.props.getTooltip(info, this) ?? null; if (content === null) return null; const normalizedTooltip: TooltipContent = typeof content === 'string' ? {text: content} : content; const position = normalizedTooltip.position || info.coordinate; if (!position) return null; return { position, text: '', html: '', element: null, className: '', style: {}, ...normalizedTooltip }; } onRenderHTML(rootElement: HTMLElement): void { if (!this.viewport || this.tooltip === null) { render(null, rootElement); return; } const style: Partial = { ...this.props.style, ...this.tooltip.style }; // Project the clicked geographic coordinate to canvas (x, y) const [x, y] = this.viewport.project(this.tooltip.position); // Render the popup container with a content box and a placeholder for the arrow. // The container is positioned absolutely (initially at 0,0) and will be repositioned after measuring. const ui = ( ); render(ui, rootElement); } }