UNPKG

@deck.gl/widgets

Version:

UI widgets for deck.gl

4 lines 141 kB
{ "version": 3, "sources": ["../src/index.ts", "../src/zoom-widget.tsx", "../src/lib/components/button-group.tsx", "../src/lib/components/grouped-icon-button.tsx", "../src/reset-view-widget.tsx", "../src/lib/components/icon-button.tsx", "../src/gimbal-widget.tsx", "../src/compass-widget.tsx", "../src/scale-widget.tsx", "../src/geocoder-widget.tsx", "../src/lib/components/dropdown-menu.tsx", "../src/lib/geocode/geocoder-history.ts", "../src/lib/geocode/geocoders.ts", "../src/fullscreen-widget.tsx", "../src/splitter-widget.tsx", "../src/view-selector-widget.tsx", "../src/lib/components/icon-menu.tsx", "../src/info-widget.tsx", "../src/context-menu-widget.tsx", "../src/lib/components/simple-menu.tsx", "../src/timeline-widget.tsx", "../src/screenshot-widget.tsx", "../src/theme-widget.tsx", "../src/themes.ts", "../src/loading-widget.tsx", "../src/fps-widget.tsx", "../src/stats-widget.tsx"], "sourcesContent": ["// deck.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\n// Navigation widgets\nexport {ZoomWidget} from './zoom-widget';\nexport {ResetViewWidget} from './reset-view-widget';\nexport {GimbalWidget} from './gimbal-widget';\n\n// Geospatial widgets\nexport {CompassWidget} from './compass-widget';\nexport {ScaleWidget as _ScaleWidget} from './scale-widget';\nexport {GeocoderWidget as _GeocoderWidget} from './geocoder-widget';\n\n// View widgets\nexport {FullscreenWidget} from './fullscreen-widget';\nexport {SplitterWidget as _SplitterWidget} from './splitter-widget';\nexport {ViewSelectorWidget as _ViewSelectorWidget} from './view-selector-widget';\n\n// Information widgets\nexport {InfoWidget as _InfoWidget} from './info-widget';\nexport {ContextMenuWidget as _ContextMenuWidget} from './context-menu-widget';\n\n// Control widgets\nexport {TimelineWidget as _TimelineWidget} from './timeline-widget';\n\n// Utility widgets\nexport {ScreenshotWidget} from './screenshot-widget';\nexport {ThemeWidget as _ThemeWidget} from './theme-widget';\nexport {LoadingWidget as _LoadingWidget} from './loading-widget';\nexport {FpsWidget as _FpsWidget} from './fps-widget';\nexport {StatsWidget as _StatsWidget} from './stats-widget';\n\nexport type {FullscreenWidgetProps} from './fullscreen-widget';\nexport type {CompassWidgetProps} from './compass-widget';\nexport type {ZoomWidgetProps} from './zoom-widget';\nexport type {ScreenshotWidgetProps} from './screenshot-widget';\nexport type {ResetViewWidgetProps} from './reset-view-widget';\nexport type {GeocoderWidgetProps} from './geocoder-widget';\nexport type {LoadingWidgetProps} from './loading-widget';\nexport type {FpsWidgetProps} from './fps-widget';\nexport type {ScaleWidgetProps} from './scale-widget';\nexport type {ThemeWidgetProps} from './theme-widget';\nexport type {InfoWidgetProps} from './info-widget';\nexport type {StatsWidgetProps} from './stats-widget';\nexport type {ContextMenuWidgetProps} from './context-menu-widget';\nexport type {SplitterWidgetProps} from './splitter-widget';\nexport type {TimelineWidgetProps} from './timeline-widget';\nexport type {ViewSelectorWidgetProps} from './view-selector-widget';\nexport type {GimbalWidgetProps} from './gimbal-widget';\n\nexport {LightTheme, DarkTheme, LightGlassTheme, DarkGlassTheme} from './themes';\nexport type {DeckWidgetTheme} from './themes';\n\n// Experimental preact components\nexport {ButtonGroup as _ButtonGroup, type ButtonGroupProps} from './lib/components/button-group';\nexport {IconButton as _IconButton, type IconButtonProps} from './lib/components/icon-button';\nexport {\n GroupedIconButton as _GroupedIconButton,\n type GroupedIconButtonProps\n} from './lib/components/grouped-icon-button';\nexport {\n DropdownMenu as _DropdownMenu,\n type DropdownMenuProps\n} from './lib/components/dropdown-menu';\nexport {SimpleMenu as _SimpleMenu, type SimpleMenuProps} from './lib/components/simple-menu';\nexport {IconMenu as _IconMenu, type IconMenuProps} from './lib/components/icon-menu';\n\n// Experimental geocoders. May be removed, use at your own risk!\nexport {type Geocoder} from './lib/geocode/geocoder';\nexport {\n GoogleGeocoder as _GoogleGeocoder,\n MapboxGeocoder as _MapboxGeocoder,\n OpenCageGeocoder as _OpenCageGeocoder,\n CoordinatesGeocoder as _CoordinatesGeocoder,\n CurrentLocationGeocoder as _CurrentLocationGeocoder\n} from './lib/geocode/geocoders';\n", "// deck.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport {Widget, FlyToInterpolator, LinearInterpolator} from '@deck.gl/core';\nimport type {Viewport, WidgetProps, WidgetPlacement} from '@deck.gl/core';\nimport {render} from 'preact';\nimport {ButtonGroup} from './lib/components/button-group';\nimport {GroupedIconButton} from './lib/components/grouped-icon-button';\n\nexport type ZoomWidgetProps = WidgetProps & {\n /** Widget positioning within the view. Default 'top-left'. */\n placement?: WidgetPlacement;\n /** View to attach to and interact with. Required when using multiple views. */\n viewId?: string | null;\n /** Button orientation. */\n orientation?: 'vertical' | 'horizontal';\n /** Tooltip message on zoom in button. */\n zoomInLabel?: string;\n /** Tooltip message on zoom out button. */\n zoomOutLabel?: string;\n /** Zoom transition duration in ms. 0 disables the transition */\n transitionDuration?: number;\n};\n\nexport class ZoomWidget extends Widget<ZoomWidgetProps> {\n static defaultProps: Required<ZoomWidgetProps> = {\n ...Widget.defaultProps,\n id: 'zoom',\n placement: 'top-left',\n orientation: 'vertical',\n transitionDuration: 200,\n zoomInLabel: 'Zoom In',\n zoomOutLabel: 'Zoom Out',\n viewId: null\n };\n\n className = 'deck-widget-zoom';\n placement: WidgetPlacement = 'top-left';\n viewports: {[id: string]: Viewport} = {};\n\n constructor(props: ZoomWidgetProps = {}) {\n super(props);\n this.setProps(this.props);\n }\n\n setProps(props: Partial<ZoomWidgetProps>) {\n this.placement = props.placement ?? this.placement;\n this.viewId = props.viewId ?? this.viewId;\n super.setProps(props);\n }\n\n onRenderHTML(rootElement: HTMLElement): void {\n const ui = (\n <ButtonGroup orientation={this.props.orientation}>\n <GroupedIconButton\n onClick={() => this.handleZoomIn()}\n label={this.props.zoomInLabel}\n className=\"deck-widget-zoom-in\"\n />\n <GroupedIconButton\n onClick={() => this.handleZoomOut()}\n label={this.props.zoomOutLabel}\n className=\"deck-widget-zoom-out\"\n />\n </ButtonGroup>\n );\n render(ui, rootElement);\n }\n\n onViewportChange(viewport: Viewport) {\n this.viewports[viewport.id] = viewport;\n }\n\n handleZoom(viewport: Viewport, nextZoom: number) {\n const viewId = this.viewId || viewport?.id || 'default-view';\n const nextViewState: Record<string, unknown> = {\n ...viewport,\n zoom: nextZoom\n };\n if (this.props.transitionDuration > 0) {\n nextViewState.transitionDuration = this.props.transitionDuration;\n nextViewState.transitionInterpolator =\n 'latitude' in nextViewState\n ? new FlyToInterpolator()\n : new LinearInterpolator({\n transitionProps: ['zoom']\n });\n }\n this.setViewState(viewId, nextViewState);\n }\n\n handleZoomIn() {\n for (const viewport of Object.values(this.viewports)) {\n this.handleZoom(viewport, viewport.zoom + 1);\n }\n }\n\n handleZoomOut() {\n for (const viewport of Object.values(this.viewports)) {\n this.handleZoom(viewport, viewport.zoom - 1);\n }\n }\n\n /** @todo - move to deck or widget manager */\n private setViewState(viewId: string, viewState: Record<string, unknown>): void {\n // @ts-ignore Using private method temporary until there's a public one\n this.deck._onViewStateChange({viewId, viewState, interactionState: {}});\n }\n}\n", "// deck.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport type {ComponentChildren} from 'preact';\n\nexport type ButtonGroupProps = {\n children: ComponentChildren;\n orientation: 'vertical' | 'horizontal';\n};\n\n/** Renders a group of buttons with Widget CSS */\nexport const ButtonGroup = (props: ButtonGroupProps) => {\n const {children, orientation = 'horizontal'} = props;\n return <div className={`deck-widget-button-group ${orientation}`}>{children}</div>;\n};\n", "// deck.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport type {ComponentChildren, JSX} from 'preact';\n\nexport type GroupedIconButtonProps = {\n className?: string;\n label?: string;\n onClick?: JSX.MouseEventHandler<HTMLButtonElement>;\n children?: ComponentChildren;\n};\n\n/** Renders an icon button as part of a ButtonGroup */\nexport const GroupedIconButton = (props: GroupedIconButtonProps) => {\n const {className = '', label, onClick, children} = props;\n return (\n <button\n className={`deck-widget-icon-button ${className}`}\n type=\"button\"\n onClick={onClick}\n title={label}\n >\n {children ? children : <div className=\"deck-widget-icon\" />}\n </button>\n );\n};\n", "// deck.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport type {WidgetPlacement, WidgetProps} from '@deck.gl/core';\nimport type {ViewStateMap, ViewOrViews} from '@deck.gl/core/src/lib/view-manager';\nimport {render} from 'preact';\nimport {Widget} from '@deck.gl/core';\nimport {IconButton} from './lib/components/icon-button';\n\n/** Properties for the ResetViewWidget */\nexport type ResetViewWidgetProps<ViewsT extends ViewOrViews = null> = WidgetProps & {\n /** Widget positioning within the view. Default 'top-left'. */\n placement?: WidgetPlacement;\n /** Tooltip message */\n label?: string;\n /** The initial view state to reset the view to. Defaults to deck.props.initialViewState */\n initialViewState?: ViewStateMap<ViewsT>;\n /** View to interact with. Required when using multiple views. */\n viewId?: string | null;\n};\n\n/**\n * A button widget that resets the view state of deck to an initial state.\n */\nexport class ResetViewWidget<ViewsT extends ViewOrViews = null> extends Widget<\n ResetViewWidgetProps<ViewsT>,\n ViewsT\n> {\n static defaultProps: Required<ResetViewWidgetProps> = {\n ...Widget.defaultProps,\n id: 'reset-view',\n placement: 'top-left',\n label: 'Reset View',\n initialViewState: undefined!,\n viewId: null\n };\n\n className = 'deck-widget-reset-view';\n placement: WidgetPlacement = 'top-left';\n\n constructor(props: ResetViewWidgetProps<ViewsT> = {}) {\n super(props);\n this.setProps(this.props);\n }\n\n setProps(props: Partial<ResetViewWidgetProps<ViewsT>>) {\n this.placement = props.placement ?? this.placement;\n this.viewId = props.viewId ?? this.viewId;\n super.setProps(props);\n }\n\n onRenderHTML(rootElement: HTMLElement): void {\n render(\n <IconButton\n className=\"deck-widget-reset-focus\"\n label={this.props.label}\n onClick={this.handleClick.bind(this)}\n />,\n rootElement\n );\n }\n\n handleClick() {\n const initialViewState = this.props.initialViewState || this.deck?.props.initialViewState;\n this.setViewState(initialViewState);\n }\n\n setViewState(viewState?: ViewStateMap<ViewsT>) {\n const viewId = (this.props.viewId || 'default-view') as unknown as string;\n const nextViewState = {\n ...(viewId !== 'default-view' ? viewState?.[viewId] : viewState)\n // only works for geospatial?\n // transitionDuration: this.props.transitionDuration,\n // transitionInterpolator: new FlyToInterpolator()\n };\n // @ts-ignore Using private method temporary until there's a public one\n this.deck._onViewStateChange({viewId, viewState: nextViewState, interactionState: {}});\n }\n}\n", "// deck.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport type {ComponentChildren, JSX} from 'preact';\n\nexport type IconButtonProps = {\n className?: string;\n label?: string;\n onClick?: JSX.MouseEventHandler<HTMLButtonElement>;\n children?: ComponentChildren;\n};\n\n/** Renders a button component with widget CSS */\nexport const IconButton = (props: IconButtonProps) => {\n const {className = '', label, onClick, children} = props;\n return (\n <div className=\"deck-widget-button\">\n <button\n className={`deck-widget-icon-button ${className}`}\n type=\"button\"\n onClick={onClick}\n title={label}\n >\n {children ? children : <div className=\"deck-widget-icon\" />}\n </button>\n </div>\n );\n};\n", "// deck.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport {Widget, LinearInterpolator} from '@deck.gl/core';\nimport type {Viewport, WidgetPlacement, WidgetProps} from '@deck.gl/core';\nimport {render} from 'preact';\n\nexport type GimbalWidgetProps = WidgetProps & {\n placement?: WidgetPlacement;\n /** View to attach to and interact with. Required when using multiple views. */\n viewId?: string | null;\n /** Tooltip message. */\n label?: string;\n /** Width of gimbal lines. */\n strokeWidth?: number;\n /** Transition duration in ms when resetting rotation. */\n transitionDuration?: number;\n};\n\nexport class GimbalWidget extends Widget<GimbalWidgetProps> {\n static defaultProps: Required<GimbalWidgetProps> = {\n ...Widget.defaultProps,\n id: 'gimbal',\n placement: 'top-left',\n viewId: null,\n label: 'Gimbal',\n strokeWidth: 1.5,\n transitionDuration: 200\n };\n\n className = 'deck-widget-gimbal';\n placement: WidgetPlacement = 'top-left';\n viewports: {[id: string]: Viewport} = {};\n\n constructor(props: GimbalWidgetProps = {}) {\n super(props);\n this.setProps(this.props);\n }\n\n setProps(props: Partial<GimbalWidgetProps>) {\n this.placement = props.placement ?? this.placement;\n this.viewId = props.viewId ?? this.viewId;\n super.setProps(props);\n }\n\n onRenderHTML(rootElement: HTMLElement): void {\n const viewId = this.viewId || Object.values(this.viewports)[0]?.id || 'default-view';\n const widgetViewport = this.viewports[viewId];\n const {rotationOrbit, rotationX} = this.getNormalizedRotation(widgetViewport);\n // Note - we use CSS 3D transforms instead of SVG 2D transforms\n const ui = (\n <div className=\"deck-widget-button\" style={{perspective: 100, pointerEvents: 'auto'}}>\n <button\n type=\"button\"\n onClick={() => {\n for (const viewport of Object.values(this.viewports)) {\n this.resetOrbitView(viewport);\n }\n }}\n title={this.props.label}\n style={{position: 'relative', width: 26, height: 26}}\n >\n {/* Outer ring */}\n <svg\n className=\"gimbal-outer-ring\"\n width=\"100%\"\n height=\"100%\"\n viewBox=\"0 0 26 26\"\n style={{\n position: 'absolute',\n top: 0,\n left: 0,\n transform: `rotateY(${rotationOrbit}deg)`\n }}\n >\n <circle\n cx=\"13\"\n cy=\"13\"\n r=\"10\"\n stroke=\"var(--icon-gimbal-outer-color, rgb(68, 92, 204))\"\n strokeWidth={this.props.strokeWidth}\n fill=\"none\"\n />\n </svg>\n\n {/* Inner ring */}\n <svg\n className=\"gimbal-inner-ring\"\n width=\"100%\"\n height=\"100%\"\n viewBox=\"0 0 26 26\"\n style={{\n position: 'absolute',\n top: 0,\n left: 0,\n transform: `rotateX(${rotationX}deg)`\n }}\n >\n <circle\n cx=\"13\"\n cy=\"13\"\n r=\"7\"\n stroke=\"var(--icon-gimbal-inner-color, rgb(240, 92, 68))\"\n strokeWidth={this.props.strokeWidth}\n fill=\"none\"\n />\n </svg>\n </button>\n </div>\n );\n\n render(ui, rootElement);\n }\n\n onViewportChange(viewport: Viewport) {\n this.viewports[viewport.id] = viewport;\n this.updateHTML();\n }\n\n resetOrbitView(viewport?: Viewport) {\n const viewId = this.getViewId(viewport);\n const viewState = this.getViewState(viewId);\n if ('rotationOrbit' in viewState || 'rotationX' in viewState) {\n const nextViewState = {\n ...viewState,\n rotationOrbit: 0,\n rotationX: 0,\n transitionDuration: this.props.transitionDuration,\n transitionInterpolator: new LinearInterpolator({\n transitionProps: ['rotationOrbit', 'rotationX']\n })\n };\n // @ts-ignore Using private method temporary until there's a public one\n this.deck._onViewStateChange({viewId, viewState: nextViewState, interactionState: {}});\n }\n }\n\n getNormalizedRotation(viewport?: Viewport): {rotationOrbit: number; rotationX: number} {\n const viewState = this.getViewState(this.getViewId(viewport));\n const [rz, rx] = this.getRotation(viewState);\n const rotationOrbit = normalizeAndClampAngle(rz);\n const rotationX = normalizeAndClampAngle(rx);\n return {rotationOrbit, rotationX};\n }\n\n getRotation(viewState?: any): [number, number] {\n if (viewState && ('rotationOrbit' in viewState || 'rotationX' in viewState)) {\n return [-(viewState.rotationOrbit || 0), viewState.rotationX || 0];\n }\n return [0, 0];\n }\n\n // Move to Widget/WidgetManager?\n\n getViewId(viewport?: Viewport) {\n const viewId = this.viewId || viewport?.id || 'OrbitView';\n return viewId;\n }\n\n getViewState(viewId: string) {\n const viewManager = this.getViewManager();\n const viewState = (viewId && viewManager.getViewState(viewId)) || viewManager.viewState;\n return viewState;\n }\n\n getViewManager() {\n // @ts-expect-error protected\n const viewManager = this.deck?.viewManager;\n if (!viewManager) {\n throw new Error('wigdet must be added to a deck instance');\n }\n return viewManager;\n }\n}\n\nfunction normalizeAndClampAngle(angle: number): number {\n // Bring angle into [-180, 180]\n let normalized = ((((angle + 180) % 360) + 360) % 360) - 180;\n\n // Avoid rotating the gimbal rings to close to 90 degrees as they will visually disappear\n const AVOID_ANGLE_DELTA = 10;\n const distanceFrom90 = normalized - 90;\n if (Math.abs(distanceFrom90) < AVOID_ANGLE_DELTA) {\n if (distanceFrom90 < AVOID_ANGLE_DELTA) {\n normalized = 90 + AVOID_ANGLE_DELTA;\n } else if (distanceFrom90 > -AVOID_ANGLE_DELTA) {\n normalized = 90 - AVOID_ANGLE_DELTA;\n }\n }\n // Clamp to [-80, 80]\n return normalized; // Math.max(-80, Math.min(80, normalized));\n}\n", "// deck.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport {Widget, FlyToInterpolator, WebMercatorViewport, _GlobeViewport} from '@deck.gl/core';\nimport type {Viewport, WidgetPlacement, WidgetProps} from '@deck.gl/core';\nimport {render} from 'preact';\n\nexport type CompassWidgetProps = WidgetProps & {\n /** Widget positioning within the view. Default 'top-left'. */\n placement?: WidgetPlacement;\n /** View to attach to and interact with. Required when using multiple views. */\n viewId?: string | null;\n /** Tooltip message. */\n label?: string;\n /** Bearing and pitch reset transition duration in ms. */\n transitionDuration?: number;\n};\n\nexport class CompassWidget extends Widget<CompassWidgetProps> {\n static defaultProps: Required<CompassWidgetProps> = {\n ...Widget.defaultProps,\n id: 'compass',\n placement: 'top-left',\n viewId: null,\n label: 'Reset Compass',\n transitionDuration: 200\n };\n\n className = 'deck-widget-compass';\n placement: WidgetPlacement = 'top-left';\n viewports: {[id: string]: Viewport} = {};\n\n constructor(props: CompassWidgetProps = {}) {\n super(props);\n this.setProps(this.props);\n }\n\n setProps(props: Partial<CompassWidgetProps>) {\n this.placement = props.placement ?? this.placement;\n this.viewId = props.viewId ?? this.viewId;\n super.setProps(props);\n }\n\n onRenderHTML(rootElement: HTMLElement): void {\n const viewId = this.viewId || Object.values(this.viewports)[0]?.id || 'default-view';\n const widgetViewport = this.viewports[viewId];\n const [rz, rx] = this.getRotation(widgetViewport);\n\n const ui = (\n <div className=\"deck-widget-button\" style={{perspective: 100}}>\n <button\n type=\"button\"\n onClick={() => {\n for (const viewport of Object.values(this.viewports)) {\n this.handleCompassReset(viewport);\n }\n }}\n title={this.props.label}\n style={{transform: `rotateX(${rx}deg)`}}\n >\n <svg fill=\"none\" width=\"100%\" height=\"100%\" viewBox=\"0 0 26 26\">\n <g transform={`rotate(${rz},13,13)`}>\n <path\n d=\"M10 13.0001L12.9999 5L15.9997 13.0001H10Z\"\n fill=\"var(--icon-compass-north-color, rgb(240, 92, 68))\"\n />\n <path\n d=\"M16.0002 12.9999L13.0004 21L10.0005 12.9999H16.0002Z\"\n fill=\"var(--icon-compass-south-color, rgb(204, 204, 204))\"\n />\n </g>\n </svg>\n </button>\n </div>\n );\n\n render(ui, rootElement);\n }\n\n onViewportChange(viewport: Viewport) {\n // no need to update if viewport is the same\n if (!viewport.equals(this.viewports[viewport.id])) {\n this.viewports[viewport.id] = viewport;\n this.updateHTML();\n }\n }\n\n getRotation(viewport?: Viewport) {\n if (viewport instanceof WebMercatorViewport) {\n return [-viewport.bearing, viewport.pitch];\n } else if (viewport instanceof _GlobeViewport) {\n return [0, Math.max(-80, Math.min(80, viewport.latitude))];\n }\n return [0, 0];\n }\n\n handleCompassReset(viewport: Viewport) {\n const viewId = this.viewId || viewport.id || 'default-view';\n if (viewport instanceof WebMercatorViewport) {\n const nextViewState = {\n ...viewport,\n bearing: 0,\n ...(this.getRotation(viewport)[0] === 0 ? {pitch: 0} : {}),\n transitionDuration: this.props.transitionDuration,\n transitionInterpolator: new FlyToInterpolator()\n };\n // @ts-ignore Using private method temporary until there's a public one\n this.deck._onViewStateChange({viewId, viewState: nextViewState, interactionState: {}});\n }\n }\n}\n", "// deck.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport type {WidgetPlacement, Viewport, WidgetProps} from '@deck.gl/core';\nimport {render} from 'preact';\nimport {Widget} from '@deck.gl/core';\n\nexport type ScaleWidgetProps = WidgetProps & {\n /** Widget positioning within the view. Default 'bottom-left'. */\n placement?: WidgetPlacement;\n /** Label for the scale widget */\n label?: string;\n /** View to attach to and interact with. Required when using multiple views */\n viewId?: string | null;\n};\n\n/**\n * A scale widget that displays a Google Maps\u2013like scale indicator.\n * Instead of text inside a div, this widget renders an SVG that contains a horizontal line\n * with two vertical tick marks (extending upward from the line only) and a pretty distance label\n * positioned to the left of the line. The horizontal line\u2019s length is computed from a \u201Cnice\u201D\n * candidate distance (e.g. 200, 500, 1000 m, etc.) so that its pixel width is between 100 and 200.\n */\nexport class ScaleWidget extends Widget<ScaleWidgetProps> {\n static defaultProps: Required<ScaleWidgetProps> = {\n ...Widget.defaultProps,\n id: 'scale',\n placement: 'bottom-left',\n label: 'Scale',\n viewId: null\n };\n\n className = 'deck-widget-scale';\n placement: WidgetPlacement = 'bottom-left';\n\n // The pixel width of the scale line (computed from a candidate distance)\n scaleWidth: number = 10;\n // The candidate distance (in meters) corresponding to the scale line length.\n scaleValue: number = 0;\n // The formatted distance label (e.g. \"200 m\" or \"1.0 km\")\n scaleText: string = '';\n\n constructor(props: ScaleWidgetProps = {}) {\n super(props);\n this.setProps(this.props);\n }\n\n setProps(props: Partial<ScaleWidgetProps>): void {\n this.placement = props.placement ?? this.placement;\n this.viewId = props.viewId ?? this.viewId;\n super.setProps(props);\n }\n\n onRenderHTML(rootElement: HTMLElement): void {\n // Reserve space for the text label (to the left of the horizontal line)\n const lineOffsetX = 50;\n // Overall SVG width includes the left offset plus the computed scale line width.\n const svgWidth = lineOffsetX + this.scaleWidth;\n const tickHeight = 10; // vertical tick extends upward by 10 pixels from the horizontal line\n render(\n <svg\n className=\"deck-widget-scale\"\n width={svgWidth}\n height={30}\n style={{overflow: 'visible', background: 'transparent'}}\n onClick={this.handleClick.bind(this)}\n >\n {/* Pretty distance label positioned to the left of the horizontal line */}\n <text\n x={lineOffsetX + 5}\n y=\"10\"\n textAnchor=\"end\"\n alignmentBaseline=\"middle\"\n style={{fontSize: '16px', fill: 'black', fontWeight: 'bold', fontFamily: 'sans-serif'}}\n >\n {this.scaleText}\n </text>\n {/* Horizontal line */}\n <line\n x1={lineOffsetX}\n y1=\"15\"\n x2={lineOffsetX + this.scaleWidth}\n y2=\"15\"\n stroke=\"black\"\n strokeWidth=\"6\"\n />\n {/* Left vertical tick (extending upward from the horizontal line) */}\n <line\n x1={lineOffsetX}\n y1=\"15\"\n x2={lineOffsetX}\n y2={15 - tickHeight}\n stroke=\"black\"\n strokeWidth=\"6\"\n />\n {/* Right vertical tick (extending upward from the horizontal line) */}\n <line\n x1={lineOffsetX + this.scaleWidth}\n y1=\"15\"\n x2={lineOffsetX + this.scaleWidth}\n y2={15 - tickHeight}\n stroke=\"black\"\n strokeWidth=\"6\"\n />\n </svg>,\n rootElement\n );\n }\n\n onViewportChange(viewport: Viewport): void {\n // Only handle geospatial viewports (which contain latitude)\n if (!('latitude' in viewport)) return;\n\n const {latitude, zoom} = viewport as {latitude: number; zoom: number};\n const metersPerPixel = getMetersPerPixel(latitude, zoom);\n const {candidate, candidatePixels} = computeScaleCandidate(metersPerPixel);\n\n this.scaleValue = candidate;\n this.scaleWidth = candidatePixels;\n // Format the candidate distance for display (using km if >= 1000 m)\n if (candidate >= 1000) {\n this.scaleText = `${(candidate / 1000).toFixed(1)} km`;\n } else {\n this.scaleText = `${candidate} m`;\n }\n this.updateHTML();\n }\n\n handleClick(): void {}\n}\n\n/**\n * Compute the meters per pixel at a given latitude and zoom level.\n *\n * @param latitude - The current latitude.\n * @param zoom - The current zoom level.\n * @returns The number of meters per pixel.\n */\nfunction getMetersPerPixel(latitude: number, zoom: number): number {\n const earthCircumference = 40075016.686;\n return (earthCircumference * Math.cos((latitude * Math.PI) / 180)) / Math.pow(2, zoom + 8);\n}\n\n/**\n * Compute a \"nice\" scale candidate such that the scale bar width in pixels is between 100 and 200.\n * The candidate distance (in meters) will be one of a set of round numbers (100, 200, 500, 1000, 2000, 5000, etc.).\n *\n * @param metersPerPixel - The number of meters per pixel at the current zoom/latitude.\n * @returns An object containing the candidate distance and its width in pixels.\n */\nfunction computeScaleCandidate(metersPerPixel: number): {\n candidate: number;\n candidatePixels: number;\n} {\n const minPixels = 100;\n const maxPixels = 200;\n const targetPixels = (minPixels + maxPixels) / 2;\n const targetDistance = targetPixels * metersPerPixel;\n\n const exponent = Math.floor(Math.log10(targetDistance));\n const base = Math.pow(10, exponent);\n const multipliers = [1, 2, 5];\n\n let candidate = multipliers[0] * base;\n let candidatePixels = candidate / metersPerPixel;\n\n for (let i = 0; i < multipliers.length; i++) {\n const currentCandidate = multipliers[i] * base;\n const currentPixels = currentCandidate / metersPerPixel;\n if (currentPixels >= minPixels && currentPixels <= maxPixels) {\n candidate = currentCandidate;\n candidatePixels = currentPixels;\n break;\n }\n if (currentPixels > maxPixels) {\n candidate = i > 0 ? multipliers[i - 1] * base : currentCandidate;\n candidatePixels = candidate / metersPerPixel;\n break;\n }\n if (i === multipliers.length - 1 && currentPixels < minPixels) {\n candidate = multipliers[0] * base * 10;\n candidatePixels = candidate / metersPerPixel;\n }\n }\n return {candidate, candidatePixels};\n}\n", "// deck.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport {Widget} from '@deck.gl/core';\nimport type {WidgetPlacement, Viewport, WidgetProps} from '@deck.gl/core';\nimport {FlyToInterpolator, LinearInterpolator} from '@deck.gl/core';\nimport {render} from 'preact';\nimport {DropdownMenu} from './lib/components/dropdown-menu';\nimport {type Geocoder} from './lib/geocode/geocoder';\nimport {GeocoderHistory} from './lib/geocode/geocoder-history';\nimport {\n GoogleGeocoder,\n MapboxGeocoder,\n OpenCageGeocoder,\n CoordinatesGeocoder,\n CurrentLocationGeocoder\n} from './lib/geocode/geocoders';\n\n/** @todo - is the the best we can do? */\ntype ViewState = Record<string, unknown>;\n\nconst CURRENT_LOCATION = 'current';\n\n/** Properties for the GeocoderWidget */\nexport type GeocoderWidgetProps = WidgetProps & {\n viewId?: string | null;\n /** Widget positioning within the view. Default 'top-left'. */\n placement?: WidgetPlacement;\n /** Tooltip message */\n label?: string;\n /** View state reset transition duration in ms. 0 disables the transition */\n transitionDuration?: number;\n /** Geocoding service selector, for declarative usage */\n geocoder?: 'google' | 'mapbox' | 'opencage' | 'coordinates' | 'custom';\n /** Custom geocoding service (Used when geocoder = 'custom') */\n customGeocoder?: Geocoder;\n /** API key used for geocoding services */\n apiKey?: string;\n /** Whether to use geolocation @note Experimental*/\n _geolocation?: boolean;\n};\n\n/**\n * A widget that display a text box that lets user type in a location\n * and a button that moves the view to that location.\n * @todo For now only supports coordinates, Could be extended with location service integrations.\n */\nexport class GeocoderWidget extends Widget<GeocoderWidgetProps> {\n static defaultProps: Required<GeocoderWidgetProps> = {\n ...Widget.defaultProps,\n id: 'geocoder',\n viewId: null,\n placement: 'top-left',\n label: 'Geocoder',\n transitionDuration: 200,\n geocoder: 'coordinates',\n customGeocoder: CoordinatesGeocoder,\n apiKey: '',\n _geolocation: false\n };\n\n className = 'deck-widget-geocoder';\n placement: WidgetPlacement = 'top-left';\n\n geocodeHistory = new GeocoderHistory({});\n addressText: string = '';\n geocoder: Geocoder = CoordinatesGeocoder;\n\n constructor(props: GeocoderWidgetProps = {}) {\n super(props);\n this.setProps(this.props);\n }\n\n setProps(props: Partial<GeocoderWidgetProps>): void {\n this.placement = props.placement ?? this.placement;\n this.viewId = props.viewId ?? this.viewId;\n this.geocoder = getGeocoder(this.props);\n if (this.geocoder.requiresApiKey && !this.props.apiKey) {\n throw new Error(`API key is required for the ${this.geocoder.name} geocoder`);\n }\n super.setProps(props);\n }\n\n onRenderHTML(rootElement: HTMLElement): void {\n const menuItems = this.props._geolocation\n ? [CURRENT_LOCATION, ...this.geocodeHistory.addressHistory]\n : [...this.geocodeHistory.addressHistory];\n render(\n <div\n className=\"deck-widget-geocoder\"\n style={{\n pointerEvents: 'auto',\n display: 'flex',\n alignItems: 'center',\n flexWrap: 'wrap' // Allows wrapping on smaller screens\n }}\n >\n <input\n type=\"text\"\n placeholder={this.geocoder.placeholderLocation ?? 'Enter address or location'}\n value={this.geocodeHistory.addressText}\n // @ts-expect-error event type\n onInput={e => this.setInput(e.target?.value || '')}\n onKeyPress={this.handleKeyPress}\n style={{\n flex: '1 1 auto',\n minWidth: '200px',\n margin: 0,\n padding: '8px',\n boxSizing: 'border-box'\n }}\n />\n <DropdownMenu\n menuItems={menuItems}\n onSelect={this.handleSelect}\n style={{\n margin: 2,\n padding: '4px 2px',\n boxSizing: 'border-box'\n }}\n />\n {this.geocodeHistory.errorText && (\n <div className=\"error\">{this.geocodeHistory.errorText}</div>\n )}\n </div>,\n rootElement\n );\n }\n\n setInput = (text: string) => {\n this.addressText = text;\n };\n\n handleKeyPress = e => {\n if (e.key === 'Enter') {\n this.handleSubmit();\n }\n };\n\n handleSelect = (address: string) => {\n this.setInput(address);\n this.handleSubmit();\n };\n\n /** Sync wrapper for async geocode() */\n handleSubmit = () => {\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.geocode(this.addressText);\n };\n\n /** Perform geocoding */\n geocode: (address: string) => Promise<void> = async address => {\n const useGeolocation = this.props._geolocation && address === CURRENT_LOCATION;\n const geocoder = useGeolocation ? CurrentLocationGeocoder : this.geocoder;\n const coordinates = await this.geocodeHistory.geocode(\n geocoder,\n this.addressText,\n this.props.apiKey\n );\n if (coordinates) {\n this.setViewState(coordinates);\n }\n };\n\n // TODO - MOVE TO WIDGETIMPL?\n setViewState(viewState: ViewState) {\n const viewId = this.props.viewId || (viewState?.id as string) || 'default-view';\n const viewport = this.viewports[viewId] || {};\n const nextViewState: ViewState = {\n ...viewport,\n ...viewState\n };\n if (this.props.transitionDuration > 0) {\n nextViewState.transitionDuration = this.props.transitionDuration;\n nextViewState.transitionInterpolator =\n 'latitude' in nextViewState ? new FlyToInterpolator() : new LinearInterpolator();\n }\n\n // @ts-ignore Using private method temporary until there's a public one\n this.deck._onViewStateChange({viewId, viewState: nextViewState, interactionState: {}});\n }\n\n onViewportChange(viewport: Viewport) {\n this.viewports[viewport.id] = viewport;\n }\n\n viewports: Record<string, Viewport> = {};\n}\n\nfunction getGeocoder(props: {geocoder?: string; customGeocoder?: Geocoder}): Geocoder {\n switch (props.geocoder) {\n case 'google':\n return GoogleGeocoder;\n case 'mapbox':\n return MapboxGeocoder;\n case 'opencage':\n return OpenCageGeocoder;\n case 'coordinates':\n return CoordinatesGeocoder;\n case 'custom':\n if (!props.customGeocoder) {\n throw new Error('Custom geocoder is not defined');\n }\n return props.customGeocoder;\n default:\n throw new Error(`Unknown geocoder: ${props.geocoder}`);\n }\n}\n", "// deck.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport {type JSX} from 'preact';\nimport {useState, useRef, useEffect} from 'preact/hooks';\n\nexport type DropdownMenuProps = {\n menuItems: string[];\n onSelect: (value: string) => void;\n style?: JSX.CSSProperties;\n};\n\nexport const DropdownMenu = (props: DropdownMenuProps) => {\n const [isOpen, setIsOpen] = useState(false);\n const dropdownRef = useRef<HTMLDivElement>(null);\n\n const toggleDropdown = () => setIsOpen(!isOpen);\n\n const handleClickOutside = (event: MouseEvent) => {\n if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {\n setIsOpen(false);\n }\n };\n\n useEffect(() => {\n document.addEventListener('mousedown', handleClickOutside);\n return () => {\n document.removeEventListener('mousedown', handleClickOutside);\n };\n }, []);\n\n const handleSelect = (value: string) => {\n props.onSelect(value);\n setIsOpen(false);\n };\n\n return (\n <div\n className=\"dropdown-container\"\n ref={dropdownRef}\n style={{\n position: 'relative',\n display: 'inline-block',\n ...props.style\n }}\n >\n <button\n onClick={toggleDropdown}\n style={{\n width: '30px',\n height: '30px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n border: '1px solid #ccc',\n borderRadius: '4px',\n background: '#fff',\n cursor: 'pointer',\n padding: 0\n }}\n >\n \u25BC\n </button>\n {isOpen && (\n <ul\n style={{\n position: 'absolute',\n top: '100%',\n right: '100%',\n background: '#fff',\n border: '1px solid #ccc',\n borderRadius: '4px',\n listStyle: 'none',\n padding: '4px 0',\n margin: 0,\n zIndex: 1000,\n minWidth: '200px'\n }}\n >\n {props.menuItems.map(item => (\n <li\n key={item}\n onClick={() => handleSelect(item)}\n style={{\n padding: '4px 8px',\n cursor: 'pointer',\n whiteSpace: 'nowrap'\n }}\n >\n {item}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n};\n", "// deck.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport {type Geocoder} from './geocoder';\n\nconst CURRENT_LOCATION = 'current';\nconst LOCAL_STORAGE_KEY = 'deck-geocoder-history';\n\nexport type GeocoderHistoryProps = {\n maxEntries?: number;\n};\n\n/**\n * An internal, experimental helper class for storing a list of locations in local storage.\n * @todo Remove the UI related state.\n */\nexport class GeocoderHistory {\n props: Required<GeocoderHistoryProps>;\n addressText = '';\n errorText = '';\n addressHistory: string[] = [];\n\n constructor(props: GeocoderHistoryProps) {\n this.props = {maxEntries: 5, ...props};\n this.addressHistory = this.loadPreviousAddresses();\n }\n\n /** PErform geocoding */\n async geocode(geocoder: Geocoder, address: string, apiKey: string) {\n this.errorText = '';\n this.addressText = address;\n try {\n const coordinates = await geocoder.geocode(address, apiKey);\n if (coordinates) {\n this.storeAddress(this.addressText);\n return coordinates;\n }\n this.errorText = 'Invalid address';\n } catch (error) {\n this.errorText = `${(error as Error).message}`;\n }\n return null;\n }\n\n loadPreviousAddresses(): string[] {\n try {\n const stored = window.localStorage.getItem(LOCAL_STORAGE_KEY);\n const list = stored && JSON.parse(stored);\n const addresses = Array.isArray(list)\n ? list.filter((v): v is string => typeof v === 'string')\n : [];\n return addresses;\n } catch {\n // ignore\n }\n return [];\n }\n\n storeAddress(address: string) {\n const cleaned = address.trim();\n if (!cleaned || cleaned === CURRENT_LOCATION) {\n return;\n }\n const deduped = [cleaned, ...this.addressHistory.filter(a => a !== cleaned)];\n this.addressHistory = deduped.slice(0, this.props.maxEntries);\n try {\n window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.addressHistory));\n } catch {\n // ignore\n }\n }\n}\n", "// deck.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport {type Geocoder} from './geocoder';\n\nconst GOOGLE_URL = 'https://maps.googleapis.com/maps/api/geocode/json';\nconst MAPBOX_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places';\nconst OPENCAGE_API_URL = 'https://api.opencagedata.com/geocode/v1/json';\n\n/**\n * A geocoder that uses the google geocoding service\n * @note Requires an API key from Google\n * @see https://developers.google.com/maps/documentation/geocoding/get-api-key\n */\nexport const GoogleGeocoder = {\n name: 'google',\n requiresApiKey: true,\n async geocode(\n address: string,\n apiKey: string\n ): Promise<{longitude: number; latitude: number} | null> {\n const encodedAddress = encodeURIComponent(address);\n const json = await fetchJson(`${GOOGLE_URL}?address=${encodedAddress}&key=${apiKey}`);\n\n switch (json.status) {\n case 'OK':\n const loc = json.results.length > 0 && json.results[0].geometry.location;\n return loc ? {longitude: loc.lng, latitude: loc.lat} : null;\n default:\n throw new Error(`Google Geocoder failed: ${json.status}`);\n }\n }\n} as const satisfies Geocoder;\n\n/**\n * A geocoder that uses the google geocoding service\n * @note Requires an API key from Mapbox\n * @see https://docs.mapbox.com/api/search/geocoding/\n */\nexport const MapboxGeocoder = {\n name: 'google',\n requiresApiKey: true,\n async geocode(\n address: string,\n apiKey: string\n ): Promise<{longitude: number; latitude: number} | null> {\n const encodedAddress = encodeURIComponent(address);\n const json = await fetchJson(`${MAPBOX_URL}/${encodedAddress}.json?access_token=${apiKey}`);\n\n if (Array.isArray(json.features) && json.features.length > 0) {\n const center = json.features[0].center;\n if (Array.isArray(center) && center.length >= 2) {\n return {longitude: center[0], latitude: center[1]};\n }\n }\n return null;\n }\n} as const satisfies Geocoder;\n\n/**\n * A geocoder that uses the google geocoding service\n * @note Requires an API key from OpenCageData\n * @see https://opencagedata.com/api\n */\nexport const OpenCageGeocoder = {\n name: 'opencage',\n requiresApiKey: true,\n async geocode(\n address: string,\n key: string\n ): Promise<{longitude: number; latitude: number} | null> {\n const encodedAddress = encodeURIComponent(address);\n const data = await fetchJson(`${OPENCAGE_API_URL}?q=${encodedAddress}&key=${key}`);\n if (Array.isArray(data.results) && data.results.length > 0) {\n const geometry = data.results[0].geometry;\n return {longitude: geometry.lng, latitude: geometry.lat};\n }\n return null;\n }\n} as const satisfies Geocoder;\n\n/**\n * A geocoder adapter that wraps the browser's geolocation API. Always returns the user's current location.\n * @note Not technically a geocoder, but a geolocation service that provides a source of locations.\n * @note The user must allow location access for this to work.\n */\nexport const CurrentLocationGeocoder = {\n name: 'current',\n requiresApiKey: false,\n /** Attempt to call browsers geolocation API */\n async geocode(): Promise<{longitude: number; latitude: number} | null> {\n if (!navigator.geolocation) {\n throw new Error('Geolocation not supported');\n }\n return new Promise((resolve, reject) => {\n navigator.geolocation.getCurrentPosition(\n /** @see https://developer.mozilla.org/docs/Web/API/GeolocationPosition */\n (position: GeolocationPosition) => {\n const {longitude, latitude} = position.coords;\n resolve({longitude, latitude});\n },\n /** @see https://developer.mozilla.org/docs/Web/API/GeolocationPositionError */\n (error: GeolocationPositionError) => reject(new Error(error.message))\n );\n });\n }\n} as const satisfies Geocoder;\n\n/** Fetch JSON, catching HTTP errors */\nasync function fetchJson(url: string): Promise<any> {\n let response: Response;\n try {\n response = await fetch(url);\n } catch (error) {\n // Annoyingly, fetch reports some errors (e.g. CORS) using excpetions, not response.ok\n throw new Error(`CORS error? ${error}. ${url}: `);\n }\n if (!response.ok) {\n throw new Error(`${response.statusText}. ${url}: `);\n }\n const data = await response.json();\n if (!data) {\n throw new Error(`No data returned. ${url}`);\n }\n return data;\n}\n\n/**\n * Parse a coordinate string.\n * Supports comma- or semicolon-separated values.\n * Heuristically determines which value is longitude and which is latitude.\n */\nexport const CoordinatesGeocoder = {\n name: 'coordinates',\n requiresApiKey: false,\n placeholderLocation: `-122.45, 37.8 or 37\u00B048'N, 122\u00B027'W`,\n async geocode(address: string): Promise<{longitude: number; latitude: number} | null> {\n return parseCoordinates(address) || null;\n }\n} as const satisfies Geocoder;\n\n/**\n * Parse an input string for coordinates.\n * Supports comma- or semicolon-separated values.\n * Heuristically determines which value is longitude and which is latitude.\n */\nfunction parseCoordinates(input) {\n input = input.trim();\n const parts = input.split(/[,;]/).map(p => p.trim());\n if (parts.length < 2) return null;\n const first = parseCoordinatePart(parts[0]);\n const second = parseCoordinatePart(parts[1]);\n if (first === null || second === null) return null;\n // Use a heuristic:\n // If one number exceeds 90 in absolute value, it's likely a longitude.\n if (Math.abs(first) > 90 && Math.abs(second) <= 90) {\n return {longitude: first, latitude: second};\n } else if (Math.abs(second) > 90 && Math.abs(first) <= 90) {\n return {longitude: second, latitude: first};\n }\n // If both are <= 90, assume order: latitude, longitude.\n return {latitude: first, longitude: second};\n}\n\n/**\n * Parse a single coordinate part (which may be in decimal or DMS format).\n */\nfunction parseCoordinatePart(s: string): number | null {\n s = s.trim();\n // If the string contains a degree symbol or similar markers, use DMS parsing.\n if (s.includes('\u00B0') || s.includes(\"'\") || s.includes('\"')) {\n const value = dmsToDecimal(s);\n return isNaN(value) ? null : value;\n }\n // Otherwise, check for a cardinal letter and remove it.\n let sign = 1;\n if (/[SW]/i.test(s)) sign = -1;\n s = s.replace(/[NSEW]/gi, '');\n const value = parseFloat(s);\n return isNaN(value) ? null : sign * value;\n}\n\n/** Convert a DMS string (e.g. \"37\u00B048'00\\\"N\") to decimal degrees. */\nfunction dmsToDecimal(s: string): number {\n // A simple regex to extract degrees, minutes, seconds and direction.\n const regex = /(\\d+)[\u00B0d]\\s*(\\d+)?['\u2032m]?\\s*(\\d+(?:\\.\\d+)?)?[\\\"\u2033s]?\\s*([NSEW])?/i;\n const match = s.match(regex);\n if (!match) return NaN;\n const degrees = parseFloat(match[1]) || 0;\n const minutes = parseFloat(match[2]) || 0;\n const seconds = parseFloat(match[3]) || 0;\n const direction = match[4] || '';\n let dec = degrees + minutes / 60 + seconds / 3600;\n if (/[SW]/i.test(direction)) {\n dec = -dec;\n }\n return dec;\n}\n", "// deck.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\n/* global document */\nimport {log, Widget, type WidgetProps, type WidgetPlacement} from '@deck.gl/core';\nimport {render} from 'preact';\nimport {IconButton} from './lib/components/icon-button';\n\n/* eslint-enable max-len */\n\nexport type FullscreenWidgetProps = WidgetProps & {\n id?: string;\n /** Widget positioning within the view. Default 'top-left'. */\n placement?: WidgetPlacement;\n /** View to attach to and interact with. Required when using multiple views. */\n viewId?: string | null;\n /** Tooltip message when out of fullscreen. */\n enterLabel?: string;\n /** Tooltip message when fullscreen. */\n exitLabel?: string;\n /**\n * A compatible DOM element which should be made full screen. By default, the map container element will be made full screen.\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullScreen#Compatible_elements\n */\n container?: HTMLElement;\n};\n\nexport class FullscreenWidget extends Widget<FullscreenWidgetProps> {\n static defaultProps: Required<FullscreenWidgetProps> = {\n ...Widget.defaultProps,\n id: 'fullscreen',\n placement: 'top-left',\n viewId: null,\n enterLabel: 'Enter Fullscreen',\n exitLabel: 'Exit Fullscreen',\n container: undefined!\n };\n\n className = 'deck-widget-fullscreen';\n placement: WidgetPlacement = 'top-left';\n fullscreen: boolean = false;\n\n constructor(props: FullscreenWidgetProps = {}) {\n super(props);\n this.setProps(this.props);\n }\n\n onAdd(): void {\n document.addEventListener('fullscreenchange', this.onFullscreenChange.bind(this));\n }\n\n onRemove() {\n document.removeEventListener('fullscreenchange', this.onFullscreenChange.bind(this));\n }\n\n onRenderHTML(rootElement: HTMLElement): void {\n render(\n <IconButton\n onClick={() => {\n this.handl