@deck.gl/widgets 9.1.12 → 9.2.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compass-widget.d.ts +11 -34
- package/dist/compass-widget.d.ts.map +1 -1
- package/dist/compass-widget.js +27 -62
- package/dist/compass-widget.js.map +1 -1
- package/dist/context-menu-widget.d.ts +37 -0
- package/dist/context-menu-widget.d.ts.map +1 -0
- package/dist/context-menu-widget.js +74 -0
- package/dist/context-menu-widget.js.map +1 -0
- package/dist/dist.dev.js +2271 -581
- package/dist/fps-widget.d.ts +29 -0
- package/dist/fps-widget.d.ts.map +1 -0
- package/dist/fps-widget.js +51 -0
- package/dist/fps-widget.js.map +1 -0
- package/dist/fullscreen-widget.d.ts +15 -33
- package/dist/fullscreen-widget.d.ts.map +1 -1
- package/dist/fullscreen-widget.js +20 -51
- package/dist/fullscreen-widget.js.map +1 -1
- package/dist/geocoder-widget.d.ts +51 -0
- package/dist/geocoder-widget.d.ts.map +1 -0
- package/dist/geocoder-widget.js +138 -0
- package/dist/geocoder-widget.js.map +1 -0
- package/dist/gimbal-widget.d.ts +33 -0
- package/dist/gimbal-widget.d.ts.map +1 -0
- package/dist/gimbal-widget.js +116 -0
- package/dist/gimbal-widget.js.map +1 -0
- package/dist/index.cjs +1587 -341
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +37 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -7
- package/dist/index.js.map +1 -1
- package/dist/info-widget.d.ts +37 -0
- package/dist/info-widget.d.ts.map +1 -0
- package/dist/info-widget.js +183 -0
- package/dist/info-widget.js.map +1 -0
- package/dist/lib/components/button-group.d.ts +8 -0
- package/dist/lib/components/button-group.d.ts.map +1 -0
- package/dist/lib/components/button-group.js +7 -0
- package/dist/lib/components/button-group.js.map +1 -0
- package/dist/lib/components/dropdown-menu.d.ts +8 -0
- package/dist/lib/components/dropdown-menu.d.ts.map +1 -0
- package/dist/lib/components/dropdown-menu.js +55 -0
- package/dist/lib/components/dropdown-menu.js.map +1 -0
- package/dist/lib/components/grouped-icon-button.d.ts +8 -0
- package/dist/lib/components/grouped-icon-button.d.ts.map +1 -0
- package/dist/lib/components/grouped-icon-button.js +7 -0
- package/dist/lib/components/grouped-icon-button.js.map +1 -0
- package/dist/lib/components/icon-button.d.ts +11 -0
- package/dist/lib/components/icon-button.d.ts.map +1 -0
- package/dist/lib/components/icon-button.js +7 -0
- package/dist/lib/components/icon-button.js.map +1 -0
- package/dist/lib/components/icon-menu.d.ts +16 -0
- package/dist/lib/components/icon-menu.d.ts.map +1 -0
- package/dist/lib/components/icon-menu.js +49 -0
- package/dist/lib/components/icon-menu.js.map +1 -0
- package/dist/lib/components/simple-menu.d.ts +16 -0
- package/dist/lib/components/simple-menu.d.ts.map +1 -0
- package/dist/lib/components/simple-menu.js +30 -0
- package/dist/lib/components/simple-menu.js.map +1 -0
- package/dist/lib/geocode/geocoder-history.d.ts +23 -0
- package/dist/lib/geocode/geocoder-history.d.ts.map +1 -0
- package/dist/lib/geocode/geocoder-history.js +64 -0
- package/dist/lib/geocode/geocoder-history.js.map +1 -0
- package/dist/lib/geocode/geocoder.d.ts +15 -0
- package/dist/lib/geocode/geocoder.d.ts.map +1 -0
- package/dist/lib/geocode/geocoder.js +5 -0
- package/dist/lib/geocode/geocoder.js.map +1 -0
- package/dist/lib/geocode/geocoders.d.ts +68 -0
- package/dist/lib/geocode/geocoders.d.ts.map +1 -0
- package/dist/lib/geocode/geocoders.js +182 -0
- package/dist/lib/geocode/geocoders.js.map +1 -0
- package/dist/loading-widget.d.ts +26 -0
- package/dist/loading-widget.d.ts.map +1 -0
- package/dist/loading-widget.js +41 -0
- package/dist/loading-widget.js.map +1 -0
- package/dist/reset-view-widget.d.ts +4 -4
- package/dist/reset-view-widget.d.ts.map +1 -1
- package/dist/reset-view-widget.js +7 -10
- package/dist/reset-view-widget.js.map +1 -1
- package/dist/scale-widget.d.ts +28 -0
- package/dist/scale-widget.d.ts.map +1 -0
- package/dist/scale-widget.js +111 -0
- package/dist/scale-widget.js.map +1 -0
- package/dist/screenshot-widget.d.ts +4 -4
- package/dist/screenshot-widget.d.ts.map +1 -1
- package/dist/screenshot-widget.js +7 -14
- package/dist/screenshot-widget.js.map +1 -1
- package/dist/splitter-widget.d.ts +32 -0
- package/dist/splitter-widget.d.ts.map +1 -0
- package/dist/splitter-widget.js +123 -0
- package/dist/splitter-widget.js.map +1 -0
- package/dist/stats-widget.d.ts +36 -0
- package/dist/stats-widget.d.ts.map +1 -0
- package/dist/stats-widget.js +115 -0
- package/dist/stats-widget.js.map +1 -0
- package/dist/stylesheet.css +121 -4
- package/dist/theme-widget.d.ts +31 -0
- package/dist/theme-widget.d.ts.map +1 -0
- package/dist/theme-widget.js +83 -0
- package/dist/theme-widget.js.map +1 -0
- package/dist/themes.d.ts +65 -36
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +20 -8
- package/dist/themes.js.map +1 -1
- package/dist/timeline-widget.d.ts +59 -0
- package/dist/timeline-widget.d.ts.map +1 -0
- package/dist/timeline-widget.js +83 -0
- package/dist/timeline-widget.js.map +1 -0
- package/dist/view-selector-widget.d.ts +33 -0
- package/dist/view-selector-widget.d.ts.map +1 -0
- package/dist/view-selector-widget.js +62 -0
- package/dist/view-selector-widget.js.map +1 -0
- package/dist/zoom-widget.d.ts +14 -43
- package/dist/zoom-widget.d.ts.map +1 -1
- package/dist/zoom-widget.js +14 -56
- package/dist/zoom-widget.js.map +1 -1
- package/dist.min.js +3 -1
- package/package.json +4 -3
- package/src/compass-widget.tsx +44 -106
- package/src/context-menu-widget.tsx +115 -0
- package/src/fps-widget.tsx +67 -0
- package/src/fullscreen-widget.tsx +33 -86
- package/src/geocoder-widget.tsx +209 -0
- package/src/gimbal-widget.tsx +188 -0
- package/src/index.ts +58 -8
- package/src/info-widget.tsx +225 -0
- package/src/lib/components/button-group.tsx +16 -0
- package/src/lib/components/dropdown-menu.tsx +98 -0
- package/src/lib/components/grouped-icon-button.tsx +24 -0
- package/src/lib/components/icon-button.tsx +30 -0
- package/src/lib/components/icon-menu.tsx +89 -0
- package/src/lib/components/simple-menu.tsx +52 -0
- package/src/lib/geocode/geocoder-history.ts +73 -0
- package/src/lib/geocode/geocoder.ts +15 -0
- package/src/lib/geocode/geocoders.ts +199 -0
- package/src/loading-widget.tsx +67 -0
- package/src/reset-view-widget.tsx +8 -11
- package/src/scale-widget.tsx +183 -0
- package/src/screenshot-widget.tsx +9 -11
- package/src/splitter-widget.tsx +179 -0
- package/src/stats-widget.tsx +166 -0
- package/src/stylesheet.css +121 -4
- package/src/theme-widget.tsx +120 -0
- package/src/themes.ts +45 -12
- package/src/timeline-widget.tsx +155 -0
- package/src/view-selector-widget.tsx +108 -0
- package/src/zoom-widget.tsx +39 -113
- package/dist/components.d.ts +0 -4
- package/dist/components.d.ts.map +0 -1
- package/dist/components.js +0 -17
- package/dist/components.js.map +0 -1
- package/dist/widget-impl.d.ts +0 -29
- package/dist/widget-impl.d.ts.map +0 -1
- package/dist/widget-impl.js +0 -60
- package/dist/widget-impl.js.map +0 -1
- package/src/components.tsx +0 -38
- package/src/widget-impl.ts +0 -88
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// deck.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {Widget, LinearInterpolator} from '@deck.gl/core';
|
|
6
|
+
import type {Viewport, WidgetPlacement, WidgetProps} from '@deck.gl/core';
|
|
7
|
+
import {render} from 'preact';
|
|
8
|
+
|
|
9
|
+
export type GimbalWidgetProps = WidgetProps & {
|
|
10
|
+
placement?: WidgetPlacement;
|
|
11
|
+
/** View to attach to and interact with. Required when using multiple views. */
|
|
12
|
+
viewId?: string | null;
|
|
13
|
+
/** Tooltip message. */
|
|
14
|
+
label?: string;
|
|
15
|
+
/** Width of gimbal lines */
|
|
16
|
+
strokeWidth?: number;
|
|
17
|
+
/** Transition duration in ms when resetting rotation. */
|
|
18
|
+
transitionDuration?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class GimbalWidget extends Widget<GimbalWidgetProps> {
|
|
22
|
+
static defaultProps: Required<GimbalWidgetProps> = {
|
|
23
|
+
...Widget.defaultProps,
|
|
24
|
+
id: 'gimbal',
|
|
25
|
+
placement: 'top-left',
|
|
26
|
+
viewId: undefined!,
|
|
27
|
+
label: 'Gimbal',
|
|
28
|
+
strokeWidth: 1.5,
|
|
29
|
+
transitionDuration: 200
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
className = 'deck-widget-gimbal';
|
|
33
|
+
placement: WidgetPlacement = 'top-left';
|
|
34
|
+
viewId?: string | null = null;
|
|
35
|
+
|
|
36
|
+
constructor(props: GimbalWidgetProps = {}) {
|
|
37
|
+
super(props, GimbalWidget.defaultProps);
|
|
38
|
+
this.setProps(this.props);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setProps(props: Partial<GimbalWidgetProps>) {
|
|
42
|
+
this.placement = props.placement ?? this.placement;
|
|
43
|
+
this.viewId = props.viewId ?? this.viewId;
|
|
44
|
+
super.setProps(props);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
onRenderHTML(rootElement: HTMLElement): void {
|
|
48
|
+
const {rotationOrbit, rotationX} = this.getNormalizedRotation();
|
|
49
|
+
// Note - we use CSS 3D transforms instead of SVG 2D transforms
|
|
50
|
+
const ui = (
|
|
51
|
+
<div className="deck-widget-button" style={{perspective: 100, pointerEvents: 'auto'}}>
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
onClick={() => {
|
|
55
|
+
this.resetOrbitView();
|
|
56
|
+
}}
|
|
57
|
+
title={this.props.label}
|
|
58
|
+
style={{position: 'relative', width: 26, height: 26}}
|
|
59
|
+
>
|
|
60
|
+
{/* Outer ring */}
|
|
61
|
+
<svg
|
|
62
|
+
className="gimbal-outer-ring"
|
|
63
|
+
width="100%"
|
|
64
|
+
height="100%"
|
|
65
|
+
viewBox="0 0 26 26"
|
|
66
|
+
style={{
|
|
67
|
+
position: 'absolute',
|
|
68
|
+
top: 0,
|
|
69
|
+
left: 0,
|
|
70
|
+
transform: `rotateY(${rotationOrbit}deg)`
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
<circle
|
|
74
|
+
cx="13"
|
|
75
|
+
cy="13"
|
|
76
|
+
r="10"
|
|
77
|
+
stroke="var(--icon-gimbal-outer-color, rgb(68, 92, 204))"
|
|
78
|
+
strokeWidth={this.props.strokeWidth}
|
|
79
|
+
fill="none"
|
|
80
|
+
/>
|
|
81
|
+
</svg>
|
|
82
|
+
|
|
83
|
+
{/* Inner ring */}
|
|
84
|
+
<svg
|
|
85
|
+
className="gimbal-inner-ring"
|
|
86
|
+
width="100%"
|
|
87
|
+
height="100%"
|
|
88
|
+
viewBox="0 0 26 26"
|
|
89
|
+
style={{
|
|
90
|
+
position: 'absolute',
|
|
91
|
+
top: 0,
|
|
92
|
+
left: 0,
|
|
93
|
+
transform: `rotateX(${rotationX}deg)`
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<circle
|
|
97
|
+
cx="13"
|
|
98
|
+
cy="13"
|
|
99
|
+
r="7"
|
|
100
|
+
stroke="var(--icon-gimbal-inner-color, rgb(240, 92, 68))"
|
|
101
|
+
strokeWidth={this.props.strokeWidth}
|
|
102
|
+
fill="none"
|
|
103
|
+
/>
|
|
104
|
+
</svg>
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
render(ui, rootElement);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onViewportChange(viewport: Viewport) {
|
|
113
|
+
this.updateHTML();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
resetOrbitView() {
|
|
117
|
+
const viewId = this.getViewId();
|
|
118
|
+
const viewState = this.getViewState(viewId);
|
|
119
|
+
if ('rotationOrbit' in viewState || 'rotationX' in viewState) {
|
|
120
|
+
const nextViewState = {
|
|
121
|
+
...viewState,
|
|
122
|
+
rotationOrbit: 0,
|
|
123
|
+
rotationX: 0,
|
|
124
|
+
transitionDuration: this.props.transitionDuration,
|
|
125
|
+
transitionInterpolator: new LinearInterpolator({
|
|
126
|
+
transitionProps: ['rotationOrbit', 'rotationX']
|
|
127
|
+
})
|
|
128
|
+
};
|
|
129
|
+
// @ts-ignore Using private method temporary until there's a public one
|
|
130
|
+
this.deck._onViewStateChange({viewId, viewState: nextViewState, interactionState: {}});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getNormalizedRotation() {
|
|
135
|
+
const viewState = this.getViewState(this.getViewId());
|
|
136
|
+
const [rz, rx] = this.getRotation(viewState);
|
|
137
|
+
const rotationOrbit = normalizeAndClampAngle(rz);
|
|
138
|
+
const rotationX = normalizeAndClampAngle(rx);
|
|
139
|
+
return {rotationOrbit, rotationX};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getRotation(viewState?: any): [number, number] {
|
|
143
|
+
if (viewState && ('rotationOrbit' in viewState || 'rotationX' in viewState)) {
|
|
144
|
+
return [-(viewState.rotationOrbit || 0), viewState.rotationX || 0];
|
|
145
|
+
}
|
|
146
|
+
return [0, 0];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Move to Widget/WidgetManager?
|
|
150
|
+
|
|
151
|
+
getViewId() {
|
|
152
|
+
const viewId = this.viewId || 'OrbitView';
|
|
153
|
+
return viewId;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getViewState(viewId: string) {
|
|
157
|
+
const viewManager = this.getViewManager();
|
|
158
|
+
const viewState = (viewId && viewManager.getViewState(viewId)) || viewManager.viewState;
|
|
159
|
+
return viewState;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getViewManager() {
|
|
163
|
+
// @ts-expect-error protected
|
|
164
|
+
const viewManager = this.deck?.viewManager;
|
|
165
|
+
if (!viewManager) {
|
|
166
|
+
throw new Error('wigdet must be added to a deck instance');
|
|
167
|
+
}
|
|
168
|
+
return viewManager;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizeAndClampAngle(angle: number): number {
|
|
173
|
+
// Bring angle into [-180, 180]
|
|
174
|
+
let normalized = ((((angle + 180) % 360) + 360) % 360) - 180;
|
|
175
|
+
|
|
176
|
+
// Avoid rotating the gimbal rings to close to 90 degrees as they will visually disappear
|
|
177
|
+
const AVOID_ANGLE_DELTA = 10;
|
|
178
|
+
const distanceFrom90 = normalized - 90;
|
|
179
|
+
if (Math.abs(distanceFrom90) < AVOID_ANGLE_DELTA) {
|
|
180
|
+
if (distanceFrom90 < AVOID_ANGLE_DELTA) {
|
|
181
|
+
normalized = 90 + AVOID_ANGLE_DELTA;
|
|
182
|
+
} else if (distanceFrom90 > -AVOID_ANGLE_DELTA) {
|
|
183
|
+
normalized = 90 - AVOID_ANGLE_DELTA;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Clamp to [-80, 80]
|
|
187
|
+
return normalized; // Math.max(-80, Math.min(80, normalized));
|
|
188
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,21 +2,71 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
// Copyright (c) vis.gl contributors
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
export {CompassWidget} from './compass-widget';
|
|
5
|
+
// Navigation widgets
|
|
7
6
|
export {ZoomWidget} from './zoom-widget';
|
|
8
|
-
export {ScreenshotWidget} from './screenshot-widget';
|
|
9
7
|
export {ResetViewWidget} from './reset-view-widget';
|
|
10
8
|
|
|
9
|
+
// Geospatial widgets
|
|
10
|
+
export {CompassWidget} from './compass-widget';
|
|
11
|
+
export {GimbalWidget} from './gimbal-widget';
|
|
12
|
+
export {ScaleWidget as _ScaleWidget} from './scale-widget';
|
|
13
|
+
export {GeocoderWidget as _GeocoderWidget} from './geocoder-widget';
|
|
14
|
+
|
|
15
|
+
// Utility widgets
|
|
16
|
+
export {FullscreenWidget} from './fullscreen-widget';
|
|
17
|
+
export {ScreenshotWidget} from './screenshot-widget';
|
|
18
|
+
export {LoadingWidget as _LoadingWidget} from './loading-widget';
|
|
19
|
+
export {FpsWidget as _FpsWidget} from './fps-widget';
|
|
20
|
+
export {ThemeWidget as _ThemeWidget} from './theme-widget';
|
|
21
|
+
export {InfoWidget as _InfoWidget} from './info-widget';
|
|
22
|
+
export {StatsWidget as _StatsWidget} from './stats-widget';
|
|
23
|
+
export {ContextMenuWidget as _ContextMenuWidget} from './context-menu-widget';
|
|
24
|
+
export {SplitterWidget as _SplitterWidget} from './splitter-widget';
|
|
25
|
+
export {TimelineWidget as _TimelineWidget} from './timeline-widget';
|
|
26
|
+
export {ViewSelectorWidget as _ViewSelectorWidget} from './view-selector-widget';
|
|
27
|
+
|
|
11
28
|
export type {FullscreenWidgetProps} from './fullscreen-widget';
|
|
12
29
|
export type {CompassWidgetProps} from './compass-widget';
|
|
13
30
|
export type {ZoomWidgetProps} from './zoom-widget';
|
|
14
31
|
export type {ScreenshotWidgetProps} from './screenshot-widget';
|
|
15
32
|
export type {ResetViewWidgetProps} from './reset-view-widget';
|
|
33
|
+
export type {GeocoderWidgetProps} from './geocoder-widget';
|
|
34
|
+
export type {LoadingWidgetProps} from './loading-widget';
|
|
35
|
+
export type {FpsWidgetProps} from './fps-widget';
|
|
36
|
+
export type {ScaleWidgetProps} from './scale-widget';
|
|
37
|
+
export type {ThemeWidgetProps} from './theme-widget';
|
|
38
|
+
export type {InfoWidgetProps} from './info-widget';
|
|
39
|
+
export type {StatsWidgetProps} from './stats-widget';
|
|
40
|
+
export type {ContextMenuWidgetProps} from './context-menu-widget';
|
|
41
|
+
export type {SplitterWidgetProps} from './splitter-widget';
|
|
42
|
+
export type {TimelineWidgetProps} from './timeline-widget';
|
|
43
|
+
export type {ViewSelectorWidgetProps} from './view-selector-widget';
|
|
44
|
+
export type {GimbalWidgetProps} from './gimbal-widget';
|
|
45
|
+
|
|
46
|
+
export {LightTheme, DarkTheme, LightGlassTheme, DarkGlassTheme} from './themes';
|
|
47
|
+
export type {DeckWidgetTheme} from './themes';
|
|
48
|
+
|
|
49
|
+
// Experimental exports
|
|
16
50
|
|
|
17
|
-
export type
|
|
18
|
-
export {
|
|
19
|
-
|
|
20
|
-
|
|
51
|
+
export {ButtonGroup as _ButtonGroup, type ButtonGroupProps} from './lib/components/button-group';
|
|
52
|
+
export {IconButton as _IconButton, type IconButtonProps} from './lib/components/icon-button';
|
|
53
|
+
export {
|
|
54
|
+
GroupedIconButton as _GroupedIconButton,
|
|
55
|
+
type GroupedIconButtonProps
|
|
56
|
+
} from './lib/components/grouped-icon-button';
|
|
57
|
+
export {
|
|
58
|
+
DropdownMenu as _DropdownMenu,
|
|
59
|
+
type DropdownMenuProps
|
|
60
|
+
} from './lib/components/dropdown-menu';
|
|
61
|
+
export {SimpleMenu as _SimpleMenu, type SimpleMenuProps} from './lib/components/simple-menu';
|
|
62
|
+
export {IconMenu as _IconMenu, type IconMenuProps} from './lib/components/icon-menu';
|
|
21
63
|
|
|
22
|
-
|
|
64
|
+
// Experimental geocoders. May be removed, use at your own risk!
|
|
65
|
+
export {type Geocoder} from './lib/geocode/geocoder';
|
|
66
|
+
export {
|
|
67
|
+
GoogleGeocoder as _GoogleGeocoder,
|
|
68
|
+
MapboxGeocoder as _MapboxGeocoder,
|
|
69
|
+
OpenCageGeocoder as _OpenCageGeocoder,
|
|
70
|
+
CoordinatesGeocoder as _CoordinatesGeocoder,
|
|
71
|
+
CurrentLocationGeocoder as _CurrentLocationGeocoder
|
|
72
|
+
} from './lib/geocode/geocoders';
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/* global document */
|
|
2
|
+
import {Widget, WidgetProps} from '@deck.gl/core';
|
|
3
|
+
import type {Deck, PickingInfo, Viewport} from '@deck.gl/core';
|
|
4
|
+
import {render, JSX} from 'preact';
|
|
5
|
+
|
|
6
|
+
export type InfoWidgetProps = WidgetProps & {
|
|
7
|
+
/** View to attach to and interact with. Required when using multiple views. */
|
|
8
|
+
viewId?: string | null;
|
|
9
|
+
/** InfoWidget mode */
|
|
10
|
+
mode: 'click' | 'hover' | 'static';
|
|
11
|
+
/** Get the popup contents from the selected element */
|
|
12
|
+
getTooltip?: (info: PickingInfo, widget: InfoWidget) => any;
|
|
13
|
+
/** Position at which to place popup (clicked point: [longitude, latitude]). */
|
|
14
|
+
position: [number, number];
|
|
15
|
+
/** Text of popup */
|
|
16
|
+
text?: string;
|
|
17
|
+
/** Visibility of info widget */
|
|
18
|
+
visible?: boolean;
|
|
19
|
+
/** Minimum offset (in pixels) to keep the popup away from the canvas edges. */
|
|
20
|
+
minOffset?: number;
|
|
21
|
+
onClick?: (widget: InfoWidget, info: PickingInfo) => boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export class InfoWidget extends Widget<InfoWidgetProps> {
|
|
25
|
+
static defaultProps: Required<InfoWidgetProps> = {
|
|
26
|
+
...Widget.defaultProps,
|
|
27
|
+
position: [0, 0],
|
|
28
|
+
text: '',
|
|
29
|
+
visible: false,
|
|
30
|
+
// Set default minOffset if not provided
|
|
31
|
+
minOffset: 0,
|
|
32
|
+
viewId: null,
|
|
33
|
+
mode: 'hover',
|
|
34
|
+
getTooltip: undefined!,
|
|
35
|
+
onClick: undefined!
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
className = 'deck-widget-info';
|
|
39
|
+
placement = 'fill' as const;
|
|
40
|
+
viewId?: string | null = null;
|
|
41
|
+
viewport?: Viewport;
|
|
42
|
+
|
|
43
|
+
constructor(props: InfoWidgetProps) {
|
|
44
|
+
super(props, InfoWidget.defaultProps);
|
|
45
|
+
props.position = props.position || [0, 0];
|
|
46
|
+
props.text = props.text || '';
|
|
47
|
+
props.visible = props.visible || false;
|
|
48
|
+
// Set default minOffset if not provided
|
|
49
|
+
props.minOffset = props.minOffset ?? 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setProps(props: Partial<InfoWidgetProps>) {
|
|
53
|
+
super.setProps(props);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
onViewportChange(viewport) {
|
|
57
|
+
this.viewport = viewport;
|
|
58
|
+
this.updateHTML();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
onHover(info: PickingInfo): void {
|
|
62
|
+
if (this.props.mode === 'hover' && this.props.getTooltip) {
|
|
63
|
+
const tooltip = this.props.getTooltip(info, this);
|
|
64
|
+
// hover tooltips should show over static and click infos
|
|
65
|
+
this.setProps({
|
|
66
|
+
visible: tooltip !== null,
|
|
67
|
+
...tooltip,
|
|
68
|
+
style: {zIndex: 1, ...tooltip?.style}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
onClick(info: PickingInfo): boolean {
|
|
74
|
+
if (this.props.mode === 'click' && this.props.getTooltip) {
|
|
75
|
+
const tooltip = this.props.getTooltip(info, this);
|
|
76
|
+
this.setProps({
|
|
77
|
+
visible: tooltip !== null,
|
|
78
|
+
...tooltip
|
|
79
|
+
});
|
|
80
|
+
return tooltip !== null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Original behavior
|
|
84
|
+
return this.props.onClick?.(this, info) || false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// TODO - Align with onCreateRootElement
|
|
88
|
+
onAdd({deck, viewId}: {deck: Deck<any>; viewId: string | null}): HTMLDivElement {
|
|
89
|
+
const {className} = this.props;
|
|
90
|
+
const element = document.createElement('div');
|
|
91
|
+
element.classList.add('deck-widget', 'deck-widget-info');
|
|
92
|
+
if (className) element.classList.add(className);
|
|
93
|
+
// Ensure absolute positioning relative to the deck container
|
|
94
|
+
const style = {margin: '0px', top: '0px', left: '0px', position: 'absolute'};
|
|
95
|
+
Object.entries(style).forEach(([key, value]) => element.style.setProperty(key, value));
|
|
96
|
+
this.deck = deck;
|
|
97
|
+
if (!viewId) {
|
|
98
|
+
this.viewport = deck.getViewports()[0];
|
|
99
|
+
} else {
|
|
100
|
+
this.viewport = deck.getViewports().find(viewport => viewport.id === viewId);
|
|
101
|
+
}
|
|
102
|
+
this.rootElement = element;
|
|
103
|
+
this.updateHTML();
|
|
104
|
+
return element;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
onRenderHTML(rootElement: HTMLElement): void {
|
|
108
|
+
if (!this.viewport) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const [longitude, latitude] = this.props.position;
|
|
112
|
+
// Project the clicked geographic coordinate to canvas (x, y)
|
|
113
|
+
const [x, y] = this.viewport.project([longitude, latitude]);
|
|
114
|
+
|
|
115
|
+
const minOffset = this.props.minOffset || 0;
|
|
116
|
+
const gap = 10; // gap between clicked point and popup box
|
|
117
|
+
const arrowHeight = 8; // height of the triangle arrow
|
|
118
|
+
const arrowWidth = 16; // full width of the arrow
|
|
119
|
+
|
|
120
|
+
// Decide vertical orientation.
|
|
121
|
+
// If the clicked point is in the bottom half, place the popup above the point.
|
|
122
|
+
const isAbove = y > this.viewport.height / 2;
|
|
123
|
+
const background = (this.props.style && this.props.style.background) || 'rgba(255,255,255,0.9)';
|
|
124
|
+
|
|
125
|
+
// Render the popup container with a content box and a placeholder for the arrow.
|
|
126
|
+
// The container is positioned absolutely (initially at 0,0) and will be repositioned after measuring.
|
|
127
|
+
const ui = this.props.visible ? (
|
|
128
|
+
<div className="popup-container" style={{position: 'absolute', left: 0, top: 0}}>
|
|
129
|
+
<div
|
|
130
|
+
className="popup-content"
|
|
131
|
+
style={{
|
|
132
|
+
background,
|
|
133
|
+
padding: '10px',
|
|
134
|
+
position: 'relative',
|
|
135
|
+
// Include any additional styles
|
|
136
|
+
...(this.props.style as JSX.CSSProperties)
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
{this.props.text}
|
|
140
|
+
</div>
|
|
141
|
+
<div className="popup-arrow" style={{position: 'absolute', width: '0px', height: '0px'}} />
|
|
142
|
+
</div>
|
|
143
|
+
) : null;
|
|
144
|
+
render(ui, rootElement);
|
|
145
|
+
|
|
146
|
+
// After rendering, measure the content and adjust positioning so that:
|
|
147
|
+
// - The popup (with its arrow) does not cover the clicked point.
|
|
148
|
+
// - The arrow's tip points to the clicked point.
|
|
149
|
+
// - The popup remains within the canvas bounds, with minOffset.
|
|
150
|
+
// eslint-disable-next-line max-statements, complexity
|
|
151
|
+
requestAnimationFrame(() => {
|
|
152
|
+
if (!this.props.visible || !rootElement.firstChild || !this.viewport) return;
|
|
153
|
+
|
|
154
|
+
const container = rootElement.firstChild as HTMLElement;
|
|
155
|
+
const contentEl = container.querySelector('.popup-content') as HTMLElement;
|
|
156
|
+
const arrowEl = container.querySelector('.popup-arrow') as HTMLElement;
|
|
157
|
+
if (!contentEl || !arrowEl) return;
|
|
158
|
+
const contentRect = contentEl.getBoundingClientRect();
|
|
159
|
+
const popupWidth = contentRect.width;
|
|
160
|
+
const popupHeight = contentRect.height;
|
|
161
|
+
|
|
162
|
+
// Compute ideal horizontal position: center the popup on the clicked x
|
|
163
|
+
let computedLeft = x - popupWidth / 2;
|
|
164
|
+
// Compute vertical position based on orientation:
|
|
165
|
+
// If the popup is above, position its bottom edge (minus arrow & gap) at the clicked y.
|
|
166
|
+
// Otherwise, position its top edge (plus arrow & gap) at the clicked y.
|
|
167
|
+
let computedTop;
|
|
168
|
+
if (isAbove) {
|
|
169
|
+
computedTop = y - gap - arrowHeight - popupHeight;
|
|
170
|
+
} else {
|
|
171
|
+
computedTop = y + gap + arrowHeight;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Ensure the popup (content) stays within horizontal bounds of the canvas
|
|
175
|
+
if (computedLeft < minOffset) {
|
|
176
|
+
computedLeft = minOffset;
|
|
177
|
+
}
|
|
178
|
+
if (computedLeft + popupWidth > this.viewport.width - minOffset) {
|
|
179
|
+
computedLeft = this.viewport.width - minOffset - popupWidth;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Ensure the vertical position (including the arrow) stays within canvas bounds.
|
|
183
|
+
if (isAbove) {
|
|
184
|
+
if (computedTop < minOffset) {
|
|
185
|
+
computedTop = minOffset;
|
|
186
|
+
}
|
|
187
|
+
} else if (computedTop + popupHeight + arrowHeight > this.viewport.height - minOffset) {
|
|
188
|
+
computedTop = this.viewport.height - minOffset - popupHeight - arrowHeight;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Update container position (remove any transform and set left/top explicitly)
|
|
192
|
+
container.style.left = `${computedLeft}px`;
|
|
193
|
+
container.style.top = `${computedTop}px`;
|
|
194
|
+
container.style.transform = '';
|
|
195
|
+
|
|
196
|
+
// Compute arrow's horizontal offset relative to the container.
|
|
197
|
+
// We want the arrow's tip to align with the clicked point.
|
|
198
|
+
let arrowLeft = x - computedLeft - arrowWidth / 2;
|
|
199
|
+
// Optionally constrain arrowLeft so it stays within the popup's width.
|
|
200
|
+
arrowLeft = Math.max(arrowLeft, 0);
|
|
201
|
+
arrowLeft = Math.min(arrowLeft, popupWidth - arrowWidth);
|
|
202
|
+
|
|
203
|
+
// Update arrow element's style based on orientation.
|
|
204
|
+
if (isAbove) {
|
|
205
|
+
// Popup is above the clicked point so arrow appears at the bottom of the popup.
|
|
206
|
+
arrowEl.style.left = `${arrowLeft}px`;
|
|
207
|
+
arrowEl.style.bottom = `-${arrowHeight}px`;
|
|
208
|
+
arrowEl.style.top = '';
|
|
209
|
+
arrowEl.style.borderLeft = `${arrowWidth / 2}px solid transparent`;
|
|
210
|
+
arrowEl.style.borderRight = `${arrowWidth / 2}px solid transparent`;
|
|
211
|
+
arrowEl.style.borderTop = `${arrowHeight}px solid ${background}`;
|
|
212
|
+
arrowEl.style.borderBottom = '';
|
|
213
|
+
} else {
|
|
214
|
+
// Popup is below the clicked point so arrow appears at the top.
|
|
215
|
+
arrowEl.style.left = `${arrowLeft}px`;
|
|
216
|
+
arrowEl.style.top = `-${arrowHeight}px`;
|
|
217
|
+
arrowEl.style.bottom = '';
|
|
218
|
+
arrowEl.style.borderLeft = `${arrowWidth / 2}px solid transparent`;
|
|
219
|
+
arrowEl.style.borderRight = `${arrowWidth / 2}px solid transparent`;
|
|
220
|
+
arrowEl.style.borderBottom = `${arrowHeight}px solid ${background}`;
|
|
221
|
+
arrowEl.style.borderTop = '';
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// deck.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {ComponentChildren} from 'preact';
|
|
6
|
+
|
|
7
|
+
export type ButtonGroupProps = {
|
|
8
|
+
children: ComponentChildren;
|
|
9
|
+
orientation;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Renders a group of buttons with Widget CSS */
|
|
13
|
+
export const ButtonGroup = (props: ButtonGroupProps) => {
|
|
14
|
+
const {children, orientation} = props;
|
|
15
|
+
return <div className={`deck-widget-button-group ${orientation}`}>{children}</div>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// deck.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {type JSX} from 'preact';
|
|
6
|
+
import {useState, useRef, useEffect} from 'preact/hooks';
|
|
7
|
+
|
|
8
|
+
export type DropdownMenuProps = {
|
|
9
|
+
menuItems: string[];
|
|
10
|
+
onSelect: (value: string) => void;
|
|
11
|
+
style?: JSX.CSSProperties;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const DropdownMenu = (props: DropdownMenuProps) => {
|
|
15
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
16
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
17
|
+
|
|
18
|
+
const toggleDropdown = () => setIsOpen(!isOpen);
|
|
19
|
+
|
|
20
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
21
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
22
|
+
setIsOpen(false);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
28
|
+
return () => {
|
|
29
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
30
|
+
};
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const handleSelect = (value: string) => {
|
|
34
|
+
props.onSelect(value);
|
|
35
|
+
setIsOpen(false);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
className="dropdown-container"
|
|
41
|
+
ref={dropdownRef}
|
|
42
|
+
style={{
|
|
43
|
+
position: 'relative',
|
|
44
|
+
display: 'inline-block',
|
|
45
|
+
...props.style
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<button
|
|
49
|
+
onClick={toggleDropdown}
|
|
50
|
+
style={{
|
|
51
|
+
width: '30px',
|
|
52
|
+
height: '30px',
|
|
53
|
+
display: 'flex',
|
|
54
|
+
alignItems: 'center',
|
|
55
|
+
justifyContent: 'center',
|
|
56
|
+
border: '1px solid #ccc',
|
|
57
|
+
borderRadius: '4px',
|
|
58
|
+
background: '#fff',
|
|
59
|
+
cursor: 'pointer',
|
|
60
|
+
padding: 0
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
▼
|
|
64
|
+
</button>
|
|
65
|
+
{isOpen && (
|
|
66
|
+
<ul
|
|
67
|
+
style={{
|
|
68
|
+
position: 'absolute',
|
|
69
|
+
top: '100%',
|
|
70
|
+
right: '100%',
|
|
71
|
+
background: '#fff',
|
|
72
|
+
border: '1px solid #ccc',
|
|
73
|
+
borderRadius: '4px',
|
|
74
|
+
listStyle: 'none',
|
|
75
|
+
padding: '4px 0',
|
|
76
|
+
margin: 0,
|
|
77
|
+
zIndex: 1000,
|
|
78
|
+
minWidth: '200px'
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{props.menuItems.map(item => (
|
|
82
|
+
<li
|
|
83
|
+
key={item}
|
|
84
|
+
onClick={() => handleSelect(item)}
|
|
85
|
+
style={{
|
|
86
|
+
padding: '4px 8px',
|
|
87
|
+
cursor: 'pointer',
|
|
88
|
+
whiteSpace: 'nowrap'
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
{item}
|
|
92
|
+
</li>
|
|
93
|
+
))}
|
|
94
|
+
</ul>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// deck.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
export type GroupedIconButtonProps = {
|
|
6
|
+
className?: string;
|
|
7
|
+
label: string;
|
|
8
|
+
onClick: () => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Renders an icon button as part of a ButtonGroup */
|
|
12
|
+
export const GroupedIconButton = props => {
|
|
13
|
+
const {className, label, onClick} = props;
|
|
14
|
+
return (
|
|
15
|
+
<button
|
|
16
|
+
className={`deck-widget-icon-button ${className || ''}`}
|
|
17
|
+
type="button"
|
|
18
|
+
onClick={onClick}
|
|
19
|
+
title={label}
|
|
20
|
+
>
|
|
21
|
+
<div className="deck-widget-icon" />
|
|
22
|
+
</button>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// deck.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {ComponentChildren} from 'preact';
|
|
6
|
+
|
|
7
|
+
export type IconButtonProps = {
|
|
8
|
+
className: string;
|
|
9
|
+
label: string;
|
|
10
|
+
onClick: (event?) => unknown;
|
|
11
|
+
/** Optional icon or element to render inside the button */
|
|
12
|
+
children?: ComponentChildren;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Renders a button component with widget CSS */
|
|
16
|
+
export const IconButton = (props: IconButtonProps) => {
|
|
17
|
+
const {className, label, onClick, children} = props;
|
|
18
|
+
return (
|
|
19
|
+
<div className="deck-widget-button">
|
|
20
|
+
<button
|
|
21
|
+
className={`deck-widget-icon-button ${className}`}
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={onClick}
|
|
24
|
+
title={label}
|
|
25
|
+
>
|
|
26
|
+
{children ? children : <div className="deck-widget-icon" />}
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
};
|