// deck.gl-community // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors /** @jsxImportSource preact */ import {render} from 'preact'; import type {JSX} from 'preact'; import {LongPressButton} from './long-press-button'; import {cloneViewState, hasViewManager} from '../widgets/view-manager-utils'; import { Widget, type Deck, type Viewport, type WidgetPlacement, type WidgetProps } from '@deck.gl/core'; const WRAPPER_STYLE: Partial = { position: 'absolute', display: 'flex', flexDirection: 'column', alignItems: 'center', background: '#f7f7f7', border: '0.5px solid #eaeaea', marginTop: '6px', padding: '2px 0', width: '18px', userSelect: 'none', pointerEvents: 'auto' }; const ZOOM_BUTTON_STYLE: JSX.CSSProperties = { cursor: 'pointer', fontSize: '14px', fontWeight: '500', margin: '-4px' }; const SLIDER_CONTAINER_STYLE: JSX.CSSProperties = { display: 'inline-block', height: '100px', padding: '0', width: '10px' }; const VERTICAL_SLIDER_STYLE: JSX.CSSProperties = { writingMode: 'vertical-lr', height: '100px', padding: '0', margin: '0', width: '10px' }; export type ZoomRangeWidgetProps = WidgetProps & { viewId?: string | null; placement?: WidgetPlacement; minZoom?: number; maxZoom?: number; step?: number; }; export class ZoomRangeWidget extends Widget { static override defaultProps = { id: 'zoom-range', viewId: null, _container: null, placement: 'top-left', minZoom: undefined, maxZoom: undefined, step: 0.1, style: {}, className: '' } satisfies Required & Required> & ZoomRangeWidgetProps; placement: WidgetPlacement = 'top-left'; className = 'deck-widget-zoom-range'; step: number; currentZoom = 0; inferredMinZoom: number | null = null; inferredMaxZoom: number | null = null; constructor(props: ZoomRangeWidgetProps = {}) { super({...ZoomRangeWidget.defaultProps, ...props}); this.viewId = props.viewId ?? null; this.placement = props.placement ?? 'top-left'; this.step = props.step ?? ZoomRangeWidget.defaultProps.step; } override setProps(props: Partial): void { if (props.viewId !== undefined) { this.viewId = props.viewId; } if (props.placement !== undefined) { this.placement = props.placement; } if (props.step !== undefined) { this.step = props.step; } super.setProps(props); } override onAdd({deck, viewId}: {deck: Deck; viewId: string | null}): void { this.deck = deck; if (this.viewId === undefined) { this.viewId = viewId; } } override onRemove(): void { this.deck = undefined; } override onRenderHTML(rootElement: HTMLElement): void { const style = {...WRAPPER_STYLE, ...this.props.style}; Object.assign(rootElement.style, style); const {minZoom, maxZoom} = this.getZoomBounds(); const clampedZoom = Math.max(minZoom, Math.min(maxZoom, this.currentZoom)); const stopEventPropagation = (event: Event) => { event.stopPropagation(); if (typeof (event as any).stopImmediatePropagation === 'function') { (event as any).stopImmediatePropagation(); } }; const ui = ( <>
this.handleZoomDelta(this.step)}>{'+'}
this.handleZoomTo(Number((event.target as HTMLInputElement).value))} onChange={(event) => this.handleZoomTo(Number((event.target as HTMLInputElement).value)) } onPointerDown={stopEventPropagation} onPointerMove={stopEventPropagation} onPointerUp={stopEventPropagation} onMouseDown={stopEventPropagation} onMouseMove={stopEventPropagation} onMouseUp={stopEventPropagation} onClick={stopEventPropagation} onWheel={stopEventPropagation} onTouchStart={stopEventPropagation} onTouchMove={stopEventPropagation} onTouchEnd={stopEventPropagation} /* @ts-expect-error - non-standard attribute for vertical sliders */ orient="vertical" style={VERTICAL_SLIDER_STYLE} />
this.handleZoomDelta(-this.step)}>{'-'}
); render(ui, rootElement); } override onViewportChange(viewport: Viewport): void { const viewState = this.getViewportViewState(viewport); const zoom = Number(viewState?.zoom); if (Number.isFinite(zoom)) { this.currentZoom = zoom; } if (this.props.minZoom === undefined) { const minZoom = Number(viewState?.minZoom); if (Number.isFinite(minZoom)) { this.inferredMinZoom = minZoom; } } if (this.props.maxZoom === undefined) { const maxZoom = Number(viewState?.maxZoom); if (Number.isFinite(maxZoom)) { this.inferredMaxZoom = maxZoom; } } this.updateHTML(); } private getZoomBounds(): {minZoom: number; maxZoom: number} { const minZoom = this.props.minZoom ?? this.inferredMinZoom ?? Number.NEGATIVE_INFINITY; const maxZoom = this.props.maxZoom ?? this.inferredMaxZoom ?? Number.POSITIVE_INFINITY; if (minZoom > maxZoom) { return {minZoom: maxZoom, maxZoom: minZoom}; } return { minZoom: Number.isFinite(minZoom) ? minZoom : -20, maxZoom: Number.isFinite(maxZoom) ? maxZoom : 20 }; } private getTargetViewports(): Viewport[] { const deck = this.deck; if (!deck) { return []; } if (this.viewId) { if (hasViewManager(deck)) { const viewport = deck.viewManager?.getViewport(this.viewId); return viewport ? [viewport] : []; } return []; } return deck.getViewports(); } private getViewportViewState(viewport: Viewport): Record { const deck = this.deck; if (deck && hasViewManager(deck)) { const viewId = this.viewId || viewport.id; try { return cloneViewState(deck.viewManager?.getViewState(viewId)); } catch { return cloneViewState(deck.viewManager?.viewState); } } return cloneViewState(viewport); } private updateViewState(viewport: Viewport, nextViewState: Record): void { if (!this.deck) { return; } const viewId = this.viewId || viewport.id || 'default-view'; // @ts-expect-error Using private method until a public alternative is available this.deck._onViewStateChange({viewId, viewState: nextViewState, interactionState: {}}); } private handleZoomDelta(delta: number) { const {minZoom, maxZoom} = this.getZoomBounds(); for (const viewport of this.getTargetViewports()) { const viewState = this.getViewportViewState(viewport); const baseZoom = Number(viewState.zoom); const current = Number.isFinite(baseZoom) ? baseZoom : this.currentZoom; const nextZoom = Math.max(minZoom, Math.min(maxZoom, current + delta)); this.currentZoom = nextZoom; this.updateHTML(); this.updateViewState(viewport, {...viewState, zoom: nextZoom}); } } private handleZoomTo(zoom: number) { const {minZoom, maxZoom} = this.getZoomBounds(); const nextZoom = Math.max(minZoom, Math.min(maxZoom, zoom)); for (const viewport of this.getTargetViewports()) { const viewState = this.getViewportViewState(viewport); this.currentZoom = nextZoom; this.updateHTML(); this.updateViewState(viewport, {...viewState, zoom: nextZoom}); } } }