UNPKG

@deck.gl/widgets

Version:

UI widgets for deck.gl

1,307 lines (1,282 loc) 68.4 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // dist/index.js var dist_exports = {}; __export(dist_exports, { CompassWidget: () => CompassWidget, DarkGlassTheme: () => DarkGlassTheme, DarkTheme: () => DarkTheme, FullscreenWidget: () => FullscreenWidget, GimbalWidget: () => GimbalWidget, LightGlassTheme: () => LightGlassTheme, LightTheme: () => LightTheme, ResetViewWidget: () => ResetViewWidget, ScreenshotWidget: () => ScreenshotWidget, ZoomWidget: () => ZoomWidget, _ButtonGroup: () => ButtonGroup, _ContextMenuWidget: () => ContextMenuWidget, _CoordinatesGeocoder: () => CoordinatesGeocoder, _CurrentLocationGeocoder: () => CurrentLocationGeocoder, _DropdownMenu: () => DropdownMenu, _FpsWidget: () => FpsWidget, _GeocoderWidget: () => GeocoderWidget, _GoogleGeocoder: () => GoogleGeocoder, _GroupedIconButton: () => GroupedIconButton, _IconButton: () => IconButton, _IconMenu: () => IconMenu, _InfoWidget: () => InfoWidget, _LoadingWidget: () => LoadingWidget, _MapboxGeocoder: () => MapboxGeocoder, _OpenCageGeocoder: () => OpenCageGeocoder, _ScaleWidget: () => ScaleWidget, _SimpleMenu: () => SimpleMenu, _SplitterWidget: () => SplitterWidget, _StatsWidget: () => StatsWidget, _ThemeWidget: () => ThemeWidget, _TimelineWidget: () => TimelineWidget, _ViewSelectorWidget: () => ViewSelectorWidget }); module.exports = __toCommonJS(dist_exports); // dist/zoom-widget.js var import_jsx_runtime3 = require("preact/jsx-runtime"); var import_core = require("@deck.gl/core"); var import_preact = require("preact"); // dist/lib/components/button-group.js var import_jsx_runtime = require("preact/jsx-runtime"); var ButtonGroup = (props) => { const { children, orientation = "horizontal" } = props; return (0, import_jsx_runtime.jsx)("div", { className: `deck-widget-button-group ${orientation}`, children }); }; // dist/lib/components/grouped-icon-button.js var import_jsx_runtime2 = require("preact/jsx-runtime"); var GroupedIconButton = (props) => { const { className = "", label, onClick, children } = props; return (0, import_jsx_runtime2.jsx)("button", { className: `deck-widget-icon-button ${className}`, type: "button", onClick, title: label, children: children ? children : (0, import_jsx_runtime2.jsx)("div", { className: "deck-widget-icon" }) }); }; // dist/zoom-widget.js var ZoomWidget = class extends import_core.Widget { constructor(props = {}) { super(props); this.className = "deck-widget-zoom"; this.placement = "top-left"; this.viewports = {}; this.setProps(this.props); } setProps(props) { this.placement = props.placement ?? this.placement; this.viewId = props.viewId ?? this.viewId; super.setProps(props); } onRenderHTML(rootElement) { const ui = (0, import_jsx_runtime3.jsxs)(ButtonGroup, { orientation: this.props.orientation, children: [(0, import_jsx_runtime3.jsx)(GroupedIconButton, { onClick: () => this.handleZoomIn(), label: this.props.zoomInLabel, className: "deck-widget-zoom-in" }), (0, import_jsx_runtime3.jsx)(GroupedIconButton, { onClick: () => this.handleZoomOut(), label: this.props.zoomOutLabel, className: "deck-widget-zoom-out" })] }); (0, import_preact.render)(ui, rootElement); } onViewportChange(viewport) { this.viewports[viewport.id] = viewport; } handleZoom(viewport, nextZoom) { const viewId = this.viewId || (viewport == null ? void 0 : viewport.id) || "default-view"; const nextViewState = { ...viewport, zoom: nextZoom }; if (this.props.transitionDuration > 0) { nextViewState.transitionDuration = this.props.transitionDuration; nextViewState.transitionInterpolator = "latitude" in nextViewState ? new import_core.FlyToInterpolator() : new import_core.LinearInterpolator({ transitionProps: ["zoom"] }); } this.setViewState(viewId, nextViewState); } handleZoomIn() { for (const viewport of Object.values(this.viewports)) { this.handleZoom(viewport, viewport.zoom + 1); } } handleZoomOut() { for (const viewport of Object.values(this.viewports)) { this.handleZoom(viewport, viewport.zoom - 1); } } /** @todo - move to deck or widget manager */ setViewState(viewId, viewState) { this.deck._onViewStateChange({ viewId, viewState, interactionState: {} }); } }; ZoomWidget.defaultProps = { ...import_core.Widget.defaultProps, id: "zoom", placement: "top-left", orientation: "vertical", transitionDuration: 200, zoomInLabel: "Zoom In", zoomOutLabel: "Zoom Out", viewId: null }; // dist/reset-view-widget.js var import_jsx_runtime5 = require("preact/jsx-runtime"); var import_preact2 = require("preact"); var import_core2 = require("@deck.gl/core"); // dist/lib/components/icon-button.js var import_jsx_runtime4 = require("preact/jsx-runtime"); var IconButton = (props) => { const { className = "", label, onClick, children } = props; return (0, import_jsx_runtime4.jsx)("div", { className: "deck-widget-button", children: (0, import_jsx_runtime4.jsx)("button", { className: `deck-widget-icon-button ${className}`, type: "button", onClick, title: label, children: children ? children : (0, import_jsx_runtime4.jsx)("div", { className: "deck-widget-icon" }) }) }); }; // dist/reset-view-widget.js var ResetViewWidget = class extends import_core2.Widget { constructor(props = {}) { super(props); this.className = "deck-widget-reset-view"; this.placement = "top-left"; this.setProps(this.props); } setProps(props) { this.placement = props.placement ?? this.placement; this.viewId = props.viewId ?? this.viewId; super.setProps(props); } onRenderHTML(rootElement) { (0, import_preact2.render)((0, import_jsx_runtime5.jsx)(IconButton, { className: "deck-widget-reset-focus", label: this.props.label, onClick: this.handleClick.bind(this) }), rootElement); } handleClick() { var _a; const initialViewState = this.props.initialViewState || ((_a = this.deck) == null ? void 0 : _a.props.initialViewState); this.setViewState(initialViewState); } setViewState(viewState) { const viewId = this.props.viewId || "default-view"; const nextViewState = { ...viewId !== "default-view" ? viewState == null ? void 0 : viewState[viewId] : viewState // only works for geospatial? // transitionDuration: this.props.transitionDuration, // transitionInterpolator: new FlyToInterpolator() }; this.deck._onViewStateChange({ viewId, viewState: nextViewState, interactionState: {} }); } }; ResetViewWidget.defaultProps = { ...import_core2.Widget.defaultProps, id: "reset-view", placement: "top-left", label: "Reset View", initialViewState: void 0, viewId: null }; // dist/gimbal-widget.js var import_jsx_runtime6 = require("preact/jsx-runtime"); var import_core3 = require("@deck.gl/core"); var import_preact3 = require("preact"); var GimbalWidget = class extends import_core3.Widget { constructor(props = {}) { super(props); this.className = "deck-widget-gimbal"; this.placement = "top-left"; this.viewports = {}; this.setProps(this.props); } setProps(props) { this.placement = props.placement ?? this.placement; this.viewId = props.viewId ?? this.viewId; super.setProps(props); } onRenderHTML(rootElement) { var _a; const viewId = this.viewId || ((_a = Object.values(this.viewports)[0]) == null ? void 0 : _a.id) || "default-view"; const widgetViewport = this.viewports[viewId]; const { rotationOrbit, rotationX } = this.getNormalizedRotation(widgetViewport); const ui = (0, import_jsx_runtime6.jsx)("div", { className: "deck-widget-button", style: { perspective: 100, pointerEvents: "auto" }, children: (0, import_jsx_runtime6.jsxs)("button", { type: "button", onClick: () => { for (const viewport of Object.values(this.viewports)) { this.resetOrbitView(viewport); } }, title: this.props.label, style: { position: "relative", width: 26, height: 26 }, children: [(0, import_jsx_runtime6.jsx)("svg", { className: "gimbal-outer-ring", width: "100%", height: "100%", viewBox: "0 0 26 26", style: { position: "absolute", top: 0, left: 0, transform: `rotateY(${rotationOrbit}deg)` }, children: (0, import_jsx_runtime6.jsx)("circle", { cx: "13", cy: "13", r: "10", stroke: "var(--icon-gimbal-outer-color, rgb(68, 92, 204))", strokeWidth: this.props.strokeWidth, fill: "none" }) }), (0, import_jsx_runtime6.jsx)("svg", { className: "gimbal-inner-ring", width: "100%", height: "100%", viewBox: "0 0 26 26", style: { position: "absolute", top: 0, left: 0, transform: `rotateX(${rotationX}deg)` }, children: (0, import_jsx_runtime6.jsx)("circle", { cx: "13", cy: "13", r: "7", stroke: "var(--icon-gimbal-inner-color, rgb(240, 92, 68))", strokeWidth: this.props.strokeWidth, fill: "none" }) })] }) }); (0, import_preact3.render)(ui, rootElement); } onViewportChange(viewport) { this.viewports[viewport.id] = viewport; this.updateHTML(); } resetOrbitView(viewport) { const viewId = this.getViewId(viewport); const viewState = this.getViewState(viewId); if ("rotationOrbit" in viewState || "rotationX" in viewState) { const nextViewState = { ...viewState, rotationOrbit: 0, rotationX: 0, transitionDuration: this.props.transitionDuration, transitionInterpolator: new import_core3.LinearInterpolator({ transitionProps: ["rotationOrbit", "rotationX"] }) }; this.deck._onViewStateChange({ viewId, viewState: nextViewState, interactionState: {} }); } } getNormalizedRotation(viewport) { const viewState = this.getViewState(this.getViewId(viewport)); const [rz, rx] = this.getRotation(viewState); const rotationOrbit = normalizeAndClampAngle(rz); const rotationX = normalizeAndClampAngle(rx); return { rotationOrbit, rotationX }; } getRotation(viewState) { if (viewState && ("rotationOrbit" in viewState || "rotationX" in viewState)) { return [-(viewState.rotationOrbit || 0), viewState.rotationX || 0]; } return [0, 0]; } // Move to Widget/WidgetManager? getViewId(viewport) { const viewId = this.viewId || (viewport == null ? void 0 : viewport.id) || "OrbitView"; return viewId; } getViewState(viewId) { const viewManager = this.getViewManager(); const viewState = viewId && viewManager.getViewState(viewId) || viewManager.viewState; return viewState; } getViewManager() { var _a; const viewManager = (_a = this.deck) == null ? void 0 : _a.viewManager; if (!viewManager) { throw new Error("wigdet must be added to a deck instance"); } return viewManager; } }; GimbalWidget.defaultProps = { ...import_core3.Widget.defaultProps, id: "gimbal", placement: "top-left", viewId: null, label: "Gimbal", strokeWidth: 1.5, transitionDuration: 200 }; function normalizeAndClampAngle(angle) { let normalized = ((angle + 180) % 360 + 360) % 360 - 180; const AVOID_ANGLE_DELTA = 10; const distanceFrom90 = normalized - 90; if (Math.abs(distanceFrom90) < AVOID_ANGLE_DELTA) { if (distanceFrom90 < AVOID_ANGLE_DELTA) { normalized = 90 + AVOID_ANGLE_DELTA; } else if (distanceFrom90 > -AVOID_ANGLE_DELTA) { normalized = 90 - AVOID_ANGLE_DELTA; } } return normalized; } // dist/compass-widget.js var import_jsx_runtime7 = require("preact/jsx-runtime"); var import_core4 = require("@deck.gl/core"); var import_preact4 = require("preact"); var CompassWidget = class extends import_core4.Widget { constructor(props = {}) { super(props); this.className = "deck-widget-compass"; this.placement = "top-left"; this.viewports = {}; this.setProps(this.props); } setProps(props) { this.placement = props.placement ?? this.placement; this.viewId = props.viewId ?? this.viewId; super.setProps(props); } onRenderHTML(rootElement) { var _a; const viewId = this.viewId || ((_a = Object.values(this.viewports)[0]) == null ? void 0 : _a.id) || "default-view"; const widgetViewport = this.viewports[viewId]; const [rz, rx] = this.getRotation(widgetViewport); const ui = (0, import_jsx_runtime7.jsx)("div", { className: "deck-widget-button", style: { perspective: 100 }, children: (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: () => { for (const viewport of Object.values(this.viewports)) { this.handleCompassReset(viewport); } }, title: this.props.label, style: { transform: `rotateX(${rx}deg)` }, children: (0, import_jsx_runtime7.jsx)("svg", { fill: "none", width: "100%", height: "100%", viewBox: "0 0 26 26", children: (0, import_jsx_runtime7.jsxs)("g", { transform: `rotate(${rz},13,13)`, children: [(0, import_jsx_runtime7.jsx)("path", { d: "M10 13.0001L12.9999 5L15.9997 13.0001H10Z", fill: "var(--icon-compass-north-color, rgb(240, 92, 68))" }), (0, import_jsx_runtime7.jsx)("path", { d: "M16.0002 12.9999L13.0004 21L10.0005 12.9999H16.0002Z", fill: "var(--icon-compass-south-color, rgb(204, 204, 204))" })] }) }) }) }); (0, import_preact4.render)(ui, rootElement); } onViewportChange(viewport) { if (!viewport.equals(this.viewports[viewport.id])) { this.viewports[viewport.id] = viewport; this.updateHTML(); } } getRotation(viewport) { if (viewport instanceof import_core4.WebMercatorViewport) { return [-viewport.bearing, viewport.pitch]; } else if (viewport instanceof import_core4._GlobeViewport) { return [0, Math.max(-80, Math.min(80, viewport.latitude))]; } return [0, 0]; } handleCompassReset(viewport) { const viewId = this.viewId || viewport.id || "default-view"; if (viewport instanceof import_core4.WebMercatorViewport) { const nextViewState = { ...viewport, bearing: 0, ...this.getRotation(viewport)[0] === 0 ? { pitch: 0 } : {}, transitionDuration: this.props.transitionDuration, transitionInterpolator: new import_core4.FlyToInterpolator() }; this.deck._onViewStateChange({ viewId, viewState: nextViewState, interactionState: {} }); } } }; CompassWidget.defaultProps = { ...import_core4.Widget.defaultProps, id: "compass", placement: "top-left", viewId: null, label: "Reset Compass", transitionDuration: 200 }; // dist/scale-widget.js var import_jsx_runtime8 = require("preact/jsx-runtime"); var import_preact5 = require("preact"); var import_core5 = require("@deck.gl/core"); var ScaleWidget = class extends import_core5.Widget { constructor(props = {}) { super(props); this.className = "deck-widget-scale"; this.placement = "bottom-left"; this.scaleWidth = 10; this.scaleValue = 0; this.scaleText = ""; this.setProps(this.props); } setProps(props) { this.placement = props.placement ?? this.placement; this.viewId = props.viewId ?? this.viewId; super.setProps(props); } onRenderHTML(rootElement) { const lineOffsetX = 50; const svgWidth = lineOffsetX + this.scaleWidth; const tickHeight = 10; (0, import_preact5.render)((0, import_jsx_runtime8.jsxs)("svg", { className: "deck-widget-scale", width: svgWidth, height: 30, style: { overflow: "visible", background: "transparent" }, onClick: this.handleClick.bind(this), children: [(0, import_jsx_runtime8.jsx)("text", { x: lineOffsetX + 5, y: "10", textAnchor: "end", alignmentBaseline: "middle", style: { fontSize: "16px", fill: "black", fontWeight: "bold", fontFamily: "sans-serif" }, children: this.scaleText }), (0, import_jsx_runtime8.jsx)("line", { x1: lineOffsetX, y1: "15", x2: lineOffsetX + this.scaleWidth, y2: "15", stroke: "black", strokeWidth: "6" }), (0, import_jsx_runtime8.jsx)("line", { x1: lineOffsetX, y1: "15", x2: lineOffsetX, y2: 15 - tickHeight, stroke: "black", strokeWidth: "6" }), (0, import_jsx_runtime8.jsx)("line", { x1: lineOffsetX + this.scaleWidth, y1: "15", x2: lineOffsetX + this.scaleWidth, y2: 15 - tickHeight, stroke: "black", strokeWidth: "6" })] }), rootElement); } onViewportChange(viewport) { if (!("latitude" in viewport)) return; const { latitude, zoom } = viewport; const metersPerPixel = getMetersPerPixel(latitude, zoom); const { candidate, candidatePixels } = computeScaleCandidate(metersPerPixel); this.scaleValue = candidate; this.scaleWidth = candidatePixels; if (candidate >= 1e3) { this.scaleText = `${(candidate / 1e3).toFixed(1)} km`; } else { this.scaleText = `${candidate} m`; } this.updateHTML(); } handleClick() { } }; ScaleWidget.defaultProps = { ...import_core5.Widget.defaultProps, id: "scale", placement: "bottom-left", label: "Scale", viewId: null }; function getMetersPerPixel(latitude, zoom) { const earthCircumference = 40075016686e-3; return earthCircumference * Math.cos(latitude * Math.PI / 180) / Math.pow(2, zoom + 8); } function computeScaleCandidate(metersPerPixel) { const minPixels = 100; const maxPixels = 200; const targetPixels = (minPixels + maxPixels) / 2; const targetDistance = targetPixels * metersPerPixel; const exponent = Math.floor(Math.log10(targetDistance)); const base = Math.pow(10, exponent); const multipliers = [1, 2, 5]; let candidate = multipliers[0] * base; let candidatePixels = candidate / metersPerPixel; for (let i = 0; i < multipliers.length; i++) { const currentCandidate = multipliers[i] * base; const currentPixels = currentCandidate / metersPerPixel; if (currentPixels >= minPixels && currentPixels <= maxPixels) { candidate = currentCandidate; candidatePixels = currentPixels; break; } if (currentPixels > maxPixels) { candidate = i > 0 ? multipliers[i - 1] * base : currentCandidate; candidatePixels = candidate / metersPerPixel; break; } if (i === multipliers.length - 1 && currentPixels < minPixels) { candidate = multipliers[0] * base * 10; candidatePixels = candidate / metersPerPixel; } } return { candidate, candidatePixels }; } // dist/geocoder-widget.js var import_jsx_runtime10 = require("preact/jsx-runtime"); var import_core6 = require("@deck.gl/core"); var import_core7 = require("@deck.gl/core"); var import_preact6 = require("preact"); // dist/lib/components/dropdown-menu.js var import_jsx_runtime9 = require("preact/jsx-runtime"); var import_hooks = require("preact/hooks"); var DropdownMenu = (props) => { const [isOpen, setIsOpen] = (0, import_hooks.useState)(false); const dropdownRef = (0, import_hooks.useRef)(null); const toggleDropdown = () => setIsOpen(!isOpen); const handleClickOutside = (event) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { setIsOpen(false); } }; (0, import_hooks.useEffect)(() => { document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, []); const handleSelect = (value) => { props.onSelect(value); setIsOpen(false); }; return (0, import_jsx_runtime9.jsxs)("div", { className: "dropdown-container", ref: dropdownRef, style: { position: "relative", display: "inline-block", ...props.style }, children: [(0, import_jsx_runtime9.jsx)("button", { onClick: toggleDropdown, style: { width: "30px", height: "30px", display: "flex", alignItems: "center", justifyContent: "center", border: "1px solid #ccc", borderRadius: "4px", background: "#fff", cursor: "pointer", padding: 0 }, children: "\u25BC" }), isOpen && (0, import_jsx_runtime9.jsx)("ul", { style: { position: "absolute", top: "100%", right: "100%", background: "#fff", border: "1px solid #ccc", borderRadius: "4px", listStyle: "none", padding: "4px 0", margin: 0, zIndex: 1e3, minWidth: "200px" }, children: props.menuItems.map((item) => (0, import_jsx_runtime9.jsx)("li", { onClick: () => handleSelect(item), style: { padding: "4px 8px", cursor: "pointer", whiteSpace: "nowrap" }, children: item }, item)) })] }); }; // dist/lib/geocode/geocoder-history.js var CURRENT_LOCATION = "current"; var LOCAL_STORAGE_KEY = "deck-geocoder-history"; var GeocoderHistory = class { constructor(props) { this.addressText = ""; this.errorText = ""; this.addressHistory = []; this.props = { maxEntries: 5, ...props }; this.addressHistory = this.loadPreviousAddresses(); } /** PErform geocoding */ async geocode(geocoder, address, apiKey) { this.errorText = ""; this.addressText = address; try { const coordinates = await geocoder.geocode(address, apiKey); if (coordinates) { this.storeAddress(this.addressText); return coordinates; } this.errorText = "Invalid address"; } catch (error) { this.errorText = `${error.message}`; } return null; } loadPreviousAddresses() { try { const stored = window.localStorage.getItem(LOCAL_STORAGE_KEY); const list = stored && JSON.parse(stored); const addresses = Array.isArray(list) ? list.filter((v) => typeof v === "string") : []; return addresses; } catch { } return []; } storeAddress(address) { const cleaned = address.trim(); if (!cleaned || cleaned === CURRENT_LOCATION) { return; } const deduped = [cleaned, ...this.addressHistory.filter((a) => a !== cleaned)]; this.addressHistory = deduped.slice(0, this.props.maxEntries); try { window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.addressHistory)); } catch { } } }; // dist/lib/geocode/geocoders.js var GOOGLE_URL = "https://maps.googleapis.com/maps/api/geocode/json"; var MAPBOX_URL = "https://api.mapbox.com/geocoding/v5/mapbox.places"; var OPENCAGE_API_URL = "https://api.opencagedata.com/geocode/v1/json"; var GoogleGeocoder = { name: "google", requiresApiKey: true, async geocode(address, apiKey) { const encodedAddress = encodeURIComponent(address); const json = await fetchJson(`${GOOGLE_URL}?address=${encodedAddress}&key=${apiKey}`); switch (json.status) { case "OK": const loc = json.results.length > 0 && json.results[0].geometry.location; return loc ? { longitude: loc.lng, latitude: loc.lat } : null; default: throw new Error(`Google Geocoder failed: ${json.status}`); } } }; var MapboxGeocoder = { name: "google", requiresApiKey: true, async geocode(address, apiKey) { const encodedAddress = encodeURIComponent(address); const json = await fetchJson(`${MAPBOX_URL}/${encodedAddress}.json?access_token=${apiKey}`); if (Array.isArray(json.features) && json.features.length > 0) { const center = json.features[0].center; if (Array.isArray(center) && center.length >= 2) { return { longitude: center[0], latitude: center[1] }; } } return null; } }; var OpenCageGeocoder = { name: "opencage", requiresApiKey: true, async geocode(address, key) { const encodedAddress = encodeURIComponent(address); const data = await fetchJson(`${OPENCAGE_API_URL}?q=${encodedAddress}&key=${key}`); if (Array.isArray(data.results) && data.results.length > 0) { const geometry = data.results[0].geometry; return { longitude: geometry.lng, latitude: geometry.lat }; } return null; } }; var CurrentLocationGeocoder = { name: "current", requiresApiKey: false, /** Attempt to call browsers geolocation API */ async geocode() { if (!navigator.geolocation) { throw new Error("Geolocation not supported"); } return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( /** @see https://developer.mozilla.org/docs/Web/API/GeolocationPosition */ (position) => { const { longitude, latitude } = position.coords; resolve({ longitude, latitude }); }, /** @see https://developer.mozilla.org/docs/Web/API/GeolocationPositionError */ (error) => reject(new Error(error.message)) ); }); } }; async function fetchJson(url) { let response; try { response = await fetch(url); } catch (error) { throw new Error(`CORS error? ${error}. ${url}: `); } if (!response.ok) { throw new Error(`${response.statusText}. ${url}: `); } const data = await response.json(); if (!data) { throw new Error(`No data returned. ${url}`); } return data; } var CoordinatesGeocoder = { name: "coordinates", requiresApiKey: false, placeholderLocation: `-122.45, 37.8 or 37\xB048'N, 122\xB027'W`, async geocode(address) { return parseCoordinates(address) || null; } }; function parseCoordinates(input) { input = input.trim(); const parts = input.split(/[,;]/).map((p) => p.trim()); if (parts.length < 2) return null; const first = parseCoordinatePart(parts[0]); const second = parseCoordinatePart(parts[1]); if (first === null || second === null) return null; if (Math.abs(first) > 90 && Math.abs(second) <= 90) { return { longitude: first, latitude: second }; } else if (Math.abs(second) > 90 && Math.abs(first) <= 90) { return { longitude: second, latitude: first }; } return { latitude: first, longitude: second }; } function parseCoordinatePart(s) { s = s.trim(); if (s.includes("\xB0") || s.includes("'") || s.includes('"')) { const value2 = dmsToDecimal(s); return isNaN(value2) ? null : value2; } let sign = 1; if (/[SW]/i.test(s)) sign = -1; s = s.replace(/[NSEW]/gi, ""); const value = parseFloat(s); return isNaN(value) ? null : sign * value; } function dmsToDecimal(s) { const regex = /(\d+)[°d]\s*(\d+)?['′m]?\s*(\d+(?:\.\d+)?)?[\"″s]?\s*([NSEW])?/i; const match = s.match(regex); if (!match) return NaN; const degrees = parseFloat(match[1]) || 0; const minutes = parseFloat(match[2]) || 0; const seconds = parseFloat(match[3]) || 0; const direction = match[4] || ""; let dec = degrees + minutes / 60 + seconds / 3600; if (/[SW]/i.test(direction)) { dec = -dec; } return dec; } // dist/geocoder-widget.js var CURRENT_LOCATION2 = "current"; var GeocoderWidget = class extends import_core6.Widget { constructor(props = {}) { super(props); this.className = "deck-widget-geocoder"; this.placement = "top-left"; this.geocodeHistory = new GeocoderHistory({}); this.addressText = ""; this.geocoder = CoordinatesGeocoder; this.setInput = (text) => { this.addressText = text; }; this.handleKeyPress = (e) => { if (e.key === "Enter") { this.handleSubmit(); } }; this.handleSelect = (address) => { this.setInput(address); this.handleSubmit(); }; this.handleSubmit = () => { this.geocode(this.addressText); }; this.geocode = async (address) => { const useGeolocation = this.props._geolocation && address === CURRENT_LOCATION2; const geocoder = useGeolocation ? CurrentLocationGeocoder : this.geocoder; const coordinates = await this.geocodeHistory.geocode(geocoder, this.addressText, this.props.apiKey); if (coordinates) { this.setViewState(coordinates); } }; this.viewports = {}; this.setProps(this.props); } setProps(props) { this.placement = props.placement ?? this.placement; this.viewId = props.viewId ?? this.viewId; this.geocoder = getGeocoder(this.props); if (this.geocoder.requiresApiKey && !this.props.apiKey) { throw new Error(`API key is required for the ${this.geocoder.name} geocoder`); } super.setProps(props); } onRenderHTML(rootElement) { const menuItems = this.props._geolocation ? [CURRENT_LOCATION2, ...this.geocodeHistory.addressHistory] : [...this.geocodeHistory.addressHistory]; (0, import_preact6.render)((0, import_jsx_runtime10.jsxs)("div", { className: "deck-widget-geocoder", style: { pointerEvents: "auto", display: "flex", alignItems: "center", flexWrap: "wrap" // Allows wrapping on smaller screens }, children: [(0, import_jsx_runtime10.jsx)("input", { type: "text", placeholder: this.geocoder.placeholderLocation ?? "Enter address or location", value: this.geocodeHistory.addressText, // @ts-expect-error event type onInput: (e) => { var _a; return this.setInput(((_a = e.target) == null ? void 0 : _a.value) || ""); }, onKeyPress: this.handleKeyPress, style: { flex: "1 1 auto", minWidth: "200px", margin: 0, padding: "8px", boxSizing: "border-box" } }), (0, import_jsx_runtime10.jsx)(DropdownMenu, { menuItems, onSelect: this.handleSelect, style: { margin: 2, padding: "4px 2px", boxSizing: "border-box" } }), this.geocodeHistory.errorText && (0, import_jsx_runtime10.jsx)("div", { className: "error", children: this.geocodeHistory.errorText })] }), rootElement); } // TODO - MOVE TO WIDGETIMPL? setViewState(viewState) { const viewId = this.props.viewId || (viewState == null ? void 0 : viewState.id) || "default-view"; const viewport = this.viewports[viewId] || {}; const nextViewState = { ...viewport, ...viewState }; if (this.props.transitionDuration > 0) { nextViewState.transitionDuration = this.props.transitionDuration; nextViewState.transitionInterpolator = "latitude" in nextViewState ? new import_core7.FlyToInterpolator() : new import_core7.LinearInterpolator(); } this.deck._onViewStateChange({ viewId, viewState: nextViewState, interactionState: {} }); } onViewportChange(viewport) { this.viewports[viewport.id] = viewport; } }; GeocoderWidget.defaultProps = { ...import_core6.Widget.defaultProps, id: "geocoder", viewId: null, placement: "top-left", label: "Geocoder", transitionDuration: 200, geocoder: "coordinates", customGeocoder: CoordinatesGeocoder, apiKey: "", _geolocation: false }; function getGeocoder(props) { switch (props.geocoder) { case "google": return GoogleGeocoder; case "mapbox": return MapboxGeocoder; case "opencage": return OpenCageGeocoder; case "coordinates": return CoordinatesGeocoder; case "custom": if (!props.customGeocoder) { throw new Error("Custom geocoder is not defined"); } return props.customGeocoder; default: throw new Error(`Unknown geocoder: ${props.geocoder}`); } } // dist/fullscreen-widget.js var import_jsx_runtime11 = require("preact/jsx-runtime"); var import_core8 = require("@deck.gl/core"); var import_preact7 = require("preact"); var FullscreenWidget = class extends import_core8.Widget { constructor(props = {}) { super(props); this.className = "deck-widget-fullscreen"; this.placement = "top-left"; this.fullscreen = false; this.setProps(this.props); } onAdd() { document.addEventListener("fullscreenchange", this.onFullscreenChange.bind(this)); } onRemove() { document.removeEventListener("fullscreenchange", this.onFullscreenChange.bind(this)); } onRenderHTML(rootElement) { (0, import_preact7.render)((0, import_jsx_runtime11.jsx)(IconButton, { onClick: () => { this.handleClick().catch((err) => import_core8.log.error(err)()); }, label: this.fullscreen ? this.props.exitLabel : this.props.enterLabel, className: this.fullscreen ? "deck-widget-fullscreen-exit" : "deck-widget-fullscreen-enter" }), rootElement); } setProps(props) { this.placement = props.placement ?? this.placement; this.viewId = props.viewId ?? this.viewId; super.setProps(props); } getContainer() { var _a, _b; return this.props.container || ((_b = (_a = this.deck) == null ? void 0 : _a.getCanvas()) == null ? void 0 : _b.parentElement); } onFullscreenChange() { const prevFullscreen = this.fullscreen; const fullscreen = document.fullscreenElement === this.getContainer(); if (prevFullscreen !== fullscreen) { this.fullscreen = !this.fullscreen; } this.updateHTML(); } async handleClick() { if (this.fullscreen) { await this.exitFullscreen(); } else { await this.requestFullscreen(); } this.updateHTML(); } async requestFullscreen() { const container = this.getContainer(); if (container == null ? void 0 : container.requestFullscreen) { await container.requestFullscreen({ navigationUI: "hide" }); } else { this.togglePseudoFullscreen(); } } async exitFullscreen() { if (document.exitFullscreen) { await document.exitFullscreen(); } else { this.togglePseudoFullscreen(); } } togglePseudoFullscreen() { var _a; (_a = this.getContainer()) == null ? void 0 : _a.classList.toggle("deck-pseudo-fullscreen"); } }; FullscreenWidget.defaultProps = { ...import_core8.Widget.defaultProps, id: "fullscreen", placement: "top-left", viewId: null, enterLabel: "Enter Fullscreen", exitLabel: "Exit Fullscreen", container: void 0 }; // dist/splitter-widget.js var import_jsx_runtime12 = require("preact/jsx-runtime"); var import_preact8 = require("preact"); var import_hooks2 = require("preact/hooks"); var import_core9 = require("@deck.gl/core"); var SplitterWidget = class extends import_core9.Widget { constructor(props) { super(props); this.className = "deck-widget-splitter"; this.placement = "fill"; } setProps(props) { super.setProps(props); } onRenderHTML(rootElement) { rootElement.style.position = "absolute"; rootElement.style.top = "0"; rootElement.style.left = "0"; rootElement.style.width = "100%"; rootElement.style.height = "100%"; rootElement.style.margin = "0px"; (0, import_preact8.render)((0, import_jsx_runtime12.jsx)(Splitter, { orientation: this.props.orientation, initialSplit: this.props.initialSplit, onChange: this.props.onChange, onDragStart: this.props.onDragStart, onDragEnd: this.props.onDragEnd }), rootElement); } }; SplitterWidget.defaultProps = { ...import_core9.Widget.defaultProps, id: "splitter-widget", viewId1: "", viewId2: "", orientation: "vertical", initialSplit: 0.5, onChange: () => { }, onDragStart: () => { }, onDragEnd: () => { } }; function Splitter({ orientation, initialSplit, onChange, onDragStart, onDragEnd }) { const [split, setSplit] = (0, import_hooks2.useState)(initialSplit); const dragging = (0, import_hooks2.useRef)(false); const containerRef = (0, import_hooks2.useRef)(null); const handleDragStart = (event) => { dragging.current = true; onDragStart == null ? void 0 : onDragStart(); document.addEventListener("mousemove", handleDragging); document.addEventListener("mouseup", handleDragEnd); event.preventDefault(); }; const handleDragging = (event) => { if (!dragging.current || !containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); let newSplit; if (orientation === "vertical") { newSplit = (event.clientX - rect.left) / rect.width; } else { newSplit = (event.clientY - rect.top) / rect.height; } newSplit = Math.min(Math.max(newSplit, 0.05), 0.95); setSplit(newSplit); onChange == null ? void 0 : onChange(newSplit); }; const handleDragEnd = (event) => { if (!dragging.current) return; dragging.current = false; onDragEnd == null ? void 0 : onDragEnd(); document.removeEventListener("mousemove", handleDragging); document.removeEventListener("mouseup", handleDragEnd); }; const splitterStyle = orientation === "vertical" ? { position: "absolute", top: 0, bottom: 0, left: `${split * 100}%`, width: "4px", cursor: "col-resize", background: "#ccc", zIndex: 10, pointerEvents: "auto", boxShadow: "inset -1px 0 0 white, inset 1px 0 0 white" } : { position: "absolute", left: 0, right: 0, top: `${split * 100}%`, height: "4px", cursor: "row-resize", background: "#ccc", zIndex: 10, pointerEvents: "auto", boxShadow: "inset -1px 0 0 white, inset 1px 0 0 white" }; const containerStyle = { position: "absolute", top: 0, left: 0, right: 0, bottom: 0 }; return (0, import_jsx_runtime12.jsx)("div", { ref: containerRef, style: containerStyle, children: (0, import_jsx_runtime12.jsx)("div", { style: splitterStyle, onMouseDown: handleDragStart }) }); } // dist/view-selector-widget.js var import_jsx_runtime14 = require("preact/jsx-runtime"); var import_preact9 = require("preact"); var import_core10 = require("@deck.gl/core"); // dist/lib/components/icon-menu.js var import_jsx_runtime13 = require("preact/jsx-runtime"); var import_hooks3 = require("preact/hooks"); function IconMenu(props) { const [menuOpen, setMenuOpen] = (0, import_hooks3.useState)(false); const containerRef = (0, import_hooks3.useRef)(null); const handleClickOutside = (event) => { if (containerRef.current && !containerRef.current.contains(event.target)) { setMenuOpen(false); } }; (0, import_hooks3.useEffect)(() => { document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [containerRef]); const [selectedItem, setSelectedItem] = (0, import_hooks3.useState)(props.initialItem); const handleSelectItem = (item) => { setSelectedItem(item); setMenuOpen(false); props.onItemSelected(item); }; const handleButtonClick = () => setMenuOpen(!menuOpen); const selectedMenuItem = props.menuItems.find((item) => item.value === selectedItem); const label = props.label || (selectedMenuItem == null ? void 0 : selectedMenuItem.label) || ""; const icon = props.icon || (selectedMenuItem == null ? void 0 : selectedMenuItem.icon); return (0, import_jsx_runtime13.jsxs)("div", { style: { position: "relative", display: "inline-block" }, ref: containerRef, children: [(0, import_jsx_runtime13.jsx)(IconButton, { className: props.className, label, onClick: handleButtonClick, children: icon }), menuOpen && (0, import_jsx_runtime13.jsx)("div", { className: "deck-widget-icon-menu", children: (0, import_jsx_runtime13.jsx)(ButtonGroup, { orientation: "vertical", children: props.menuItems.map((item) => (0, import_jsx_runtime13.jsx)(GroupedIconButton, { label: item.label, onClick: () => handleSelectItem(item.value), children: item.icon }, item.value)) }) })] }); } // dist/view-selector-widget.js var ViewSelectorWidget = class extends import_core10.Widget { constructor(props = {}) { super(props); this.className = "deck-widget-view-selector"; this.placement = "top-left"; this.handleSelectMode = (viewMode) => { this.viewMode = viewMode; this.updateHTML(); this.props.onViewModeChange(viewMode); }; this.viewMode = this.props.initialViewMode; this.setProps(this.props); } setProps(props) { this.placement = props.placement ?? this.placement; this.viewId = props.viewId ?? this.viewId; super.setProps(props); } onRenderHTML(rootElement) { (0, import_preact9.render)((0, import_jsx_runtime14.jsx)(IconMenu, { className: "deck-widget-view-selector", menuItems: MENU_ITEMS.map((item) => ({ ...item, icon: item.icon() })), initialItem: this.props.initialViewMode, onItemSelected: this.handleSelectMode }), rootElement); } }; ViewSelectorWidget.defaultProps = { ...import_core10.Widget.defaultProps, id: "view-selector", placement: "top-left", viewId: null, label: "Split View", initialViewMode: "single", onViewModeChange: () => { } }; var ICON_STYLE = { width: "24px", height: "24px" }; var ICONS = { single: () => (0, import_jsx_runtime14.jsx)("svg", { width: "24", height: "24", style: ICON_STYLE, children: (0, import_jsx_runtime14.jsx)("rect", { x: "4", y: "4", width: "16", height: "16", stroke: "var(--button-icon-hover, rgb(24, 24, 26))", fill: "none", strokeWidth: "2" }) }), "split-horizontal": () => (0, import_jsx_runtime14.jsxs)("svg", { width: "24", height: "24", style: ICON_STYLE, children: [(0, import_jsx_runtime14.jsx)("rect", { x: "4", y: "4", width: "16", height: "7", stroke: "var(--button-icon-hover, rgb(24, 24, 26))", fill: "none", strokeWidth: "2" }), (0, import_jsx_runtime14.jsx)("rect", { x: "4", y: "13", width: "16", height: "7", stroke: "var(--button-icon-hover, rgb(24, 24, 26))", fill: "none", strokeWidth: "2" })] }), "split-vertical": () => (0, import_jsx_runtime14.jsxs)("svg", { width: "24", height: "24", style: ICON_STYLE, children: [(0, import_jsx_runtime14.jsx)("rect", { x: "4", y: "4", width: "7", height: "16", stroke: "var(--button-icon-hover, rgb(24, 24, 26))", fill: "none", strokeWidth: "2" }), (0, import_jsx_runtime14.jsx)("rect", { x: "13", y: "4", width: "7", height: "16", stroke: "var(--button-icon-hover, rgb(24, 24, 26))", fill: "none", strokeWidth: "2" })] }) }; var MENU_ITEMS = [ { value: "single", icon: ICONS.single, label: "Single View" }, { value: "split-horizontal", icon: ICONS["split-horizontal"], label: "Split Horizontal" }, { value: "split-vertical", icon: ICONS["split-vertical"], label: "Split Vertical" } ]; // dist/info-widget.js var import_jsx_runtime15 = require("preact/jsx-runtime"); var import_core11 = require("@deck.gl/core"); var import_preact10 = require("preact"); var InfoWidget = class extends import_core11.Widget { constructor(props) { super(props); this.className = "deck-widget-info"; this.placement = "fill"; this.setProps(this.props); } setProps(props) { this.viewId = props.viewId ?? this.viewId; super.setProps(props); } onCreateRootElement() { const element = super.onCreateRootElement(); const style = { margin: "0px", top: "0px", left: "0px", position: "absolute" }; Object.entries(style).forEach(([key, value]) => element.style.setProperty(key, value)); return element; } onViewportChange(viewport) { this.viewport = viewport; this.updateHTML(); } onHover(info) { if (this.props.mode === "hover" && this.props.getTooltip) { const tooltip = this.props.getTooltip(info, this); this.setProps({ visible: tooltip !== null, ...tooltip, style: { zIndex: "1", ...tooltip == null ? void 0 : tooltip.style } }); } } onClick(info) { var _a, _b; if (this.props.mode === "click" && this.props.getTooltip) { const tooltip = this.props.getTooltip(info, this); this.setProps({ visible: tooltip !== null, ...tooltip }); return tooltip !== null; } return ((_b = (_a = this.props).onClick) == null ? void 0 : _b.call(_a, this, info)) || false; } onAdd({ deck, viewId }) { this.deck = deck; if (!viewId) { this.viewport = deck.getViewports()[0]; } else { this.viewport = deck.getViewports().find((viewport) => viewport.id === viewId); } } onRenderHTML(rootElement) { if (!this.viewport) { return; } const [longitude, latitude] = this.props.position; const [x, y] = this.viewport.project([longitude, latitude]); const minOffset = this.props.minOffset || 0; const gap = 10; const arrowHeight = 8; const arrowWidth = 16; const isAbove = y > this.viewport.height / 2; const background = this.props.style && this.props.style.background || "rgba(255,255,255,0.9)"; const ui = this.props.visible ? (0, import_jsx_runtime15.jsxs)("div", { className: "popup-container", style: { position: "absolute", left: 0, top: 0 }, children: [(0, import_jsx_runtime15.jsx)("div", { className: "popup-content", style: { background, padding: "10px", position: "relative", // Include any additional styles ...this.props.style }, children: this.props.text }), (0, import_jsx_runtime15.jsx)("div", { className: "popup-arrow", style: { position: "absolute", width: "0px", height: "0px" } })] }) : null; (0, import_preact10.render)(ui, rootElement); requestAnimationFrame(() => { if (!this.props.visible || !rootElement.firstChild || !this.viewport) return; const container = rootElement.firstChild; const contentEl = container.querySelector(".popup-content"); const arrowEl = container.querySelector(".popup-arrow"); if (!contentEl || !arrowEl) return; const contentRect = contentEl.getBoundingClientRect(); const popupWidth = contentRect.width; const popupHeight = contentRect.height; let computedLeft = x - popupWidth / 2; let computedTop; if (isAbove) { computedTop = y - gap - arrowHeight - popupHeight; } else { computedTop = y + gap + arrowHeight; } if (computedLeft < minOffset) { computedLeft = minOffset; } if (computedLeft + popupWidth > this.viewport.width - minOffset) { computedLeft = this.viewport.width - minOffset - popupWidth; } if (isAbove) { if (computedTop < minOffset) { computedTop = minOffset; } } else if (computedTop + popupHeight + arrowHeight > this.viewport.height - minOffset) { computedTop = this.viewport.height - minOffset - popupHeight - arrowHeight; } container.style.left = `${computedLeft}px`; container.style.top = `${computedTop}px`; container.style.transform = ""; let arrowLeft = x - computedLeft - arrowWidth / 2; arrowLeft = Math.max(arrowLeft, 0); arrowLeft = Math.min(arrowLeft, popupWidth - arrowWidth); if (isAbove) { arrowEl.style.left = `${arrowLeft}px`; arrowEl.style.bottom = `-${arrowHeight}px`; arrowEl.style.top = ""; arrowEl.style.borderLeft = `${arrowWidth / 2}px solid transparent`; arrowEl.style.borderRight = `${arrowWidth / 2}px solid transparent`; arrowEl.style.borderTop = `${arrowHeight}px solid ${background}`; arrowEl.style.borderBottom = ""; } else { arrowEl.style.left = `${arrowLeft}px`; arrowEl.style.top = `-${arrowHeight}px`; arrowEl.style.bottom = ""; arrowEl.style.borderLeft = `${arrowWidth / 2}px solid transparent`; arrowEl.style.borderRight = `${arrowWidth / 2}px solid transparent`; arrowEl.style.borderBottom = `${arrowHeight}px solid ${background}`; arrowEl.style.borderTop = ""; } }); } }; InfoWidget.defaultProps = { ...import_core11.Widget.defaultProps, id: "info", position: [0, 0], text: "", visible: false, minOffset: 0, viewId: null, mode: "hover", getTooltip: void 0, onClick: void 0 }; // dist/context-menu-widget.js var import_jsx_runtime17 = require("preact/jsx-runtime"); var import_core12 = require("@deck.gl/core"); var import_preact11 = require("preact"); // dist/lib/components/simple-menu.js var import_jsx_runtime16 = require("preact/jsx-runtime"); var MENU_STYLE = { position: "absolute", top: "100%", left: 0, background: "white", border: "1px solid #ccc", borderRadius: "4px", marginTop: "var(--menu-gap, 4px)", zIndex: 100 }; var MENU_ITEM_STYLE = { background: "white", border: "none", padding: "4px", cursor: "pointer", pointerEvents: "auto" }; var SimpleMenu = (props) => { const { menuItems, onItemSelected, position, style } = props; const styleOverride = { ...MENU_STYLE, ...style, left: `${position.x}px`, top: `${position.y}px` }; return (0, import_jsx_runtime16.jsx)("div", { style: styleOverride, children: menuItems.map(({ key, label }) => (0, import_jsx_runtime16.jsx)("button", { style: { ...MENU_ITEM_STYLE, display: "block" }, onClick: (_) => onItemSelected(key), children: label }, key)) }); }; // dist/context-menu-widget.js var MOUSE_BUTTON_RIGHT = 2; var MOUSE_WHICH_RIGHT = 3; var ContextMenuWidget = class extends import_core12.Widget { constructor(props) { super(props); this.className = "deck-widget-context-menu"; this.placement = "fill"; this.pickInfo = null; this.pickInfo = null; this.setProps(this.props); } onAdd({ deck }) { var _a, _b; const element = document.createElement("div"); element.classList.add("deck-widget", "deck-widget-context-menu"); const style = { margin: "0px", top: "0px", left: "0px", position: "absolute", pointerEvents: "auto" }; Object.entries(style).forEach(([key, value]) => element.style.setProperty(key, value)); (_a = deck.getCanvas()) == null ? void 0 : _a.addEventListener("click", () => this.hide()); (_b = deck.getCanvas()) == null ? void 0 : _b.addEventListener("contextmenu", (event) => this.handleContextMenu(event)); return element; } onRenderHTML(rootElement) { const { visible, position, menuItems } = this.props; const ui = visible && menuItems.length ? (0, import_jsx_runtime17.jsx)(SimpleMenu, { menuItems, onItemSelected: (key) => this.props.onMenuItemSelected(key, this.