@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,183 @@
|
|
|
1
|
+
// deck.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {WidgetPlacement, Viewport} from '@deck.gl/core';
|
|
6
|
+
import {render} from 'preact';
|
|
7
|
+
import {Widget, WidgetProps} from '@deck.gl/core';
|
|
8
|
+
|
|
9
|
+
export type ScaleWidgetProps = WidgetProps & {
|
|
10
|
+
placement?: WidgetPlacement;
|
|
11
|
+
label?: string;
|
|
12
|
+
onCapture?: (widget: ScaleWidget) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A scale widget that displays a Google Maps–like scale indicator.
|
|
17
|
+
* Instead of text inside a div, this widget renders an SVG that contains a horizontal line
|
|
18
|
+
* with two vertical tick marks (extending upward from the line only) and a pretty distance label
|
|
19
|
+
* positioned to the left of the line. The horizontal line’s length is computed from a “nice”
|
|
20
|
+
* candidate distance (e.g. 200, 500, 1000 m, etc.) so that its pixel width is between 100 and 200.
|
|
21
|
+
*/
|
|
22
|
+
export class ScaleWidget extends Widget<ScaleWidgetProps> {
|
|
23
|
+
static defaultProps: Required<ScaleWidgetProps> = {
|
|
24
|
+
...Widget.defaultProps,
|
|
25
|
+
id: 'scale',
|
|
26
|
+
placement: 'bottom-left',
|
|
27
|
+
label: 'Scale',
|
|
28
|
+
onCapture: undefined!
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
className = 'deck-widget-scale';
|
|
32
|
+
placement: WidgetPlacement = 'bottom-left';
|
|
33
|
+
|
|
34
|
+
// The pixel width of the scale line (computed from a candidate distance)
|
|
35
|
+
scaleWidth: number = 10;
|
|
36
|
+
// The candidate distance (in meters) corresponding to the scale line length.
|
|
37
|
+
scaleValue: number = 0;
|
|
38
|
+
// The formatted distance label (e.g. "200 m" or "1.0 km")
|
|
39
|
+
scaleText: string = '';
|
|
40
|
+
|
|
41
|
+
constructor(props: ScaleWidgetProps = {}) {
|
|
42
|
+
super(props, ScaleWidget.defaultProps);
|
|
43
|
+
this.placement = props.placement ?? this.placement;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setProps(props: Partial<ScaleWidgetProps>): void {
|
|
47
|
+
this.placement = props.placement ?? this.placement;
|
|
48
|
+
super.setProps(props);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
onRenderHTML(rootElement: HTMLElement): void {
|
|
52
|
+
// Reserve space for the text label (to the left of the horizontal line)
|
|
53
|
+
const lineOffsetX = 50;
|
|
54
|
+
// Overall SVG width includes the left offset plus the computed scale line width.
|
|
55
|
+
const svgWidth = lineOffsetX + this.scaleWidth;
|
|
56
|
+
const tickHeight = 10; // vertical tick extends upward by 10 pixels from the horizontal line
|
|
57
|
+
render(
|
|
58
|
+
<svg
|
|
59
|
+
className="deck-widget-scale"
|
|
60
|
+
width={svgWidth}
|
|
61
|
+
height={30}
|
|
62
|
+
style={{overflow: 'visible', background: 'transparent'}}
|
|
63
|
+
onClick={this.handleClick.bind(this)}
|
|
64
|
+
>
|
|
65
|
+
{/* Pretty distance label positioned to the left of the horizontal line */}
|
|
66
|
+
<text
|
|
67
|
+
x={lineOffsetX + 5}
|
|
68
|
+
y="10"
|
|
69
|
+
textAnchor="end"
|
|
70
|
+
alignmentBaseline="middle"
|
|
71
|
+
style={{fontSize: '16px', fill: 'black', fontWeight: 'bold', fontFamily: 'sans-serif'}}
|
|
72
|
+
>
|
|
73
|
+
{this.scaleText}
|
|
74
|
+
</text>
|
|
75
|
+
{/* Horizontal line */}
|
|
76
|
+
<line
|
|
77
|
+
x1={lineOffsetX}
|
|
78
|
+
y1="15"
|
|
79
|
+
x2={lineOffsetX + this.scaleWidth}
|
|
80
|
+
y2="15"
|
|
81
|
+
stroke="black"
|
|
82
|
+
strokeWidth="6"
|
|
83
|
+
/>
|
|
84
|
+
{/* Left vertical tick (extending upward from the horizontal line) */}
|
|
85
|
+
<line
|
|
86
|
+
x1={lineOffsetX}
|
|
87
|
+
y1="15"
|
|
88
|
+
x2={lineOffsetX}
|
|
89
|
+
y2={15 - tickHeight}
|
|
90
|
+
stroke="black"
|
|
91
|
+
strokeWidth="6"
|
|
92
|
+
/>
|
|
93
|
+
{/* Right vertical tick (extending upward from the horizontal line) */}
|
|
94
|
+
<line
|
|
95
|
+
x1={lineOffsetX + this.scaleWidth}
|
|
96
|
+
y1="15"
|
|
97
|
+
x2={lineOffsetX + this.scaleWidth}
|
|
98
|
+
y2={15 - tickHeight}
|
|
99
|
+
stroke="black"
|
|
100
|
+
strokeWidth="6"
|
|
101
|
+
/>
|
|
102
|
+
</svg>,
|
|
103
|
+
rootElement
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
onViewportChange(viewport: Viewport): void {
|
|
108
|
+
// Only handle geospatial viewports (which contain latitude)
|
|
109
|
+
if (!('latitude' in viewport)) return;
|
|
110
|
+
|
|
111
|
+
const {latitude, zoom} = viewport as {latitude: number; zoom: number};
|
|
112
|
+
const metersPerPixel = getMetersPerPixel(latitude, zoom);
|
|
113
|
+
const {candidate, candidatePixels} = computeScaleCandidate(metersPerPixel);
|
|
114
|
+
|
|
115
|
+
this.scaleValue = candidate;
|
|
116
|
+
this.scaleWidth = candidatePixels;
|
|
117
|
+
// Format the candidate distance for display (using km if >= 1000 m)
|
|
118
|
+
if (candidate >= 1000) {
|
|
119
|
+
this.scaleText = `${(candidate / 1000).toFixed(1)} km`;
|
|
120
|
+
} else {
|
|
121
|
+
this.scaleText = `${candidate} m`;
|
|
122
|
+
}
|
|
123
|
+
this.updateHTML();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
handleClick(): void {}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Compute the meters per pixel at a given latitude and zoom level.
|
|
131
|
+
*
|
|
132
|
+
* @param latitude - The current latitude.
|
|
133
|
+
* @param zoom - The current zoom level.
|
|
134
|
+
* @returns The number of meters per pixel.
|
|
135
|
+
*/
|
|
136
|
+
function getMetersPerPixel(latitude: number, zoom: number): number {
|
|
137
|
+
const earthCircumference = 40075016.686;
|
|
138
|
+
return (earthCircumference * Math.cos((latitude * Math.PI) / 180)) / Math.pow(2, zoom + 8);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Compute a "nice" scale candidate such that the scale bar width in pixels is between 100 and 200.
|
|
143
|
+
* The candidate distance (in meters) will be one of a set of round numbers (100, 200, 500, 1000, 2000, 5000, etc.).
|
|
144
|
+
*
|
|
145
|
+
* @param metersPerPixel - The number of meters per pixel at the current zoom/latitude.
|
|
146
|
+
* @returns An object containing the candidate distance and its width in pixels.
|
|
147
|
+
*/
|
|
148
|
+
function computeScaleCandidate(metersPerPixel: number): {
|
|
149
|
+
candidate: number;
|
|
150
|
+
candidatePixels: number;
|
|
151
|
+
} {
|
|
152
|
+
const minPixels = 100;
|
|
153
|
+
const maxPixels = 200;
|
|
154
|
+
const targetPixels = (minPixels + maxPixels) / 2;
|
|
155
|
+
const targetDistance = targetPixels * metersPerPixel;
|
|
156
|
+
|
|
157
|
+
const exponent = Math.floor(Math.log10(targetDistance));
|
|
158
|
+
const base = Math.pow(10, exponent);
|
|
159
|
+
const multipliers = [1, 2, 5];
|
|
160
|
+
|
|
161
|
+
let candidate = multipliers[0] * base;
|
|
162
|
+
let candidatePixels = candidate / metersPerPixel;
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < multipliers.length; i++) {
|
|
165
|
+
const currentCandidate = multipliers[i] * base;
|
|
166
|
+
const currentPixels = currentCandidate / metersPerPixel;
|
|
167
|
+
if (currentPixels >= minPixels && currentPixels <= maxPixels) {
|
|
168
|
+
candidate = currentCandidate;
|
|
169
|
+
candidatePixels = currentPixels;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
if (currentPixels > maxPixels) {
|
|
173
|
+
candidate = i > 0 ? multipliers[i - 1] * base : currentCandidate;
|
|
174
|
+
candidatePixels = candidate / metersPerPixel;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
if (i === multipliers.length - 1 && currentPixels < minPixels) {
|
|
178
|
+
candidate = multipliers[0] * base * 10;
|
|
179
|
+
candidatePixels = candidate / metersPerPixel;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return {candidate, candidatePixels};
|
|
183
|
+
}
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
// Copyright (c) vis.gl contributors
|
|
4
4
|
|
|
5
5
|
/* global document */
|
|
6
|
-
import {WidgetImpl, WidgetImplProps} from './widget-impl';
|
|
7
|
-
import {h, render} from 'preact';
|
|
8
|
-
import {IconButton} from './components';
|
|
9
6
|
import type {WidgetPlacement} from '@deck.gl/core';
|
|
7
|
+
import {render} from 'preact';
|
|
8
|
+
import {Widget, WidgetProps} from '@deck.gl/core';
|
|
9
|
+
import {IconButton} from './lib/components/icon-button';
|
|
10
10
|
|
|
11
11
|
/** Properties for the ScreenshotWidget */
|
|
12
|
-
export type ScreenshotWidgetProps =
|
|
12
|
+
export type ScreenshotWidgetProps = WidgetProps & {
|
|
13
13
|
/** Widget positioning within the view. Default 'top-left'. */
|
|
14
14
|
placement?: WidgetPlacement;
|
|
15
15
|
/** Tooltip message */
|
|
@@ -26,9 +26,9 @@ export type ScreenshotWidgetProps = WidgetImplProps & {
|
|
|
26
26
|
* A button widget that captures a screenshot of the current canvas and downloads it as a (png) file.
|
|
27
27
|
* @note only captures canvas contents, not HTML DOM or CSS styles
|
|
28
28
|
*/
|
|
29
|
-
export class ScreenshotWidget extends
|
|
29
|
+
export class ScreenshotWidget extends Widget<ScreenshotWidgetProps> {
|
|
30
30
|
static defaultProps: Required<ScreenshotWidgetProps> = {
|
|
31
|
-
...
|
|
31
|
+
...Widget.defaultProps,
|
|
32
32
|
id: 'screenshot',
|
|
33
33
|
placement: 'top-left',
|
|
34
34
|
label: 'Screenshot',
|
|
@@ -41,7 +41,7 @@ export class ScreenshotWidget extends WidgetImpl<ScreenshotWidgetProps> {
|
|
|
41
41
|
placement: WidgetPlacement = 'top-left';
|
|
42
42
|
|
|
43
43
|
constructor(props: ScreenshotWidgetProps = {}) {
|
|
44
|
-
super(
|
|
44
|
+
super(props, ScreenshotWidget.defaultProps);
|
|
45
45
|
this.placement = props.placement ?? this.placement;
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -50,16 +50,14 @@ export class ScreenshotWidget extends WidgetImpl<ScreenshotWidgetProps> {
|
|
|
50
50
|
super.setProps(props);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
onRenderHTML() {
|
|
54
|
-
const element = this.element;
|
|
55
|
-
if (!element) return;
|
|
53
|
+
onRenderHTML(rootElement: HTMLElement): void {
|
|
56
54
|
render(
|
|
57
55
|
<IconButton
|
|
58
56
|
className="deck-widget-camera"
|
|
59
57
|
label={this.props.label}
|
|
60
58
|
onClick={this.handleClick.bind(this)}
|
|
61
59
|
/>,
|
|
62
|
-
|
|
60
|
+
rootElement
|
|
63
61
|
);
|
|
64
62
|
}
|
|
65
63
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// SplitterWidget.tsx
|
|
2
|
+
// deck.gl
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
// Copyright (c) vis.gl contributors
|
|
5
|
+
|
|
6
|
+
import {h, render} from 'preact';
|
|
7
|
+
import {useState, useRef} from 'preact/hooks';
|
|
8
|
+
import {Widget, WidgetProps} from '@deck.gl/core';
|
|
9
|
+
|
|
10
|
+
/** Properties for the SplitterWidget */
|
|
11
|
+
export type SplitterWidgetProps = WidgetProps & {
|
|
12
|
+
/** The view id for the first (resizable) view */
|
|
13
|
+
viewId1: string;
|
|
14
|
+
/** The view id for the second view */
|
|
15
|
+
viewId2: string;
|
|
16
|
+
/** Orientation of the splitter: vertical (default) or horizontal */
|
|
17
|
+
orientation?: 'vertical' | 'horizontal';
|
|
18
|
+
/** The initial split percentage (0 to 1) for the first view, default 0.5 */
|
|
19
|
+
initialSplit?: number;
|
|
20
|
+
/** Callback invoked when the splitter is dragged with the new split value */
|
|
21
|
+
onChange?: (newSplit: number) => void;
|
|
22
|
+
/** Callback invoked when dragging starts */
|
|
23
|
+
onDragStart?: () => void;
|
|
24
|
+
/** Callback invoked when dragging ends */
|
|
25
|
+
onDragEnd?: () => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A draggable splitter widget that appears as a vertical or horizontal line
|
|
30
|
+
* across the deck.gl canvas. It positions itself based on the split percentage
|
|
31
|
+
* of the first view and provides callbacks when dragged.
|
|
32
|
+
*/
|
|
33
|
+
export class SplitterWidget extends Widget<SplitterWidgetProps> {
|
|
34
|
+
static defaultProps: Required<SplitterWidgetProps> = {
|
|
35
|
+
...Widget.defaultProps,
|
|
36
|
+
id: 'splitter-widget',
|
|
37
|
+
viewId1: '',
|
|
38
|
+
viewId2: '',
|
|
39
|
+
orientation: 'vertical',
|
|
40
|
+
initialSplit: 0.5,
|
|
41
|
+
onChange: () => {},
|
|
42
|
+
onDragStart: () => {},
|
|
43
|
+
onDragEnd: () => {}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
className = 'deck-widget-splitter';
|
|
47
|
+
placement = 'fill' as const;
|
|
48
|
+
|
|
49
|
+
constructor(props: SplitterWidgetProps) {
|
|
50
|
+
// No placement prop is used.
|
|
51
|
+
super(props, SplitterWidget.defaultProps);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setProps(props: Partial<SplitterWidgetProps>) {
|
|
55
|
+
super.setProps(props);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
onRenderHTML(rootElement: HTMLElement): void {
|
|
59
|
+
// Ensure the widget container fills the deck.gl canvas.
|
|
60
|
+
// TODO - Move styling to CSS
|
|
61
|
+
rootElement.style.position = 'absolute';
|
|
62
|
+
rootElement.style.top = '0';
|
|
63
|
+
rootElement.style.left = '0';
|
|
64
|
+
rootElement.style.width = '100%';
|
|
65
|
+
rootElement.style.height = '100%';
|
|
66
|
+
rootElement.style.margin = '0px';
|
|
67
|
+
|
|
68
|
+
render(
|
|
69
|
+
<Splitter
|
|
70
|
+
orientation={this.props.orientation}
|
|
71
|
+
initialSplit={this.props.initialSplit}
|
|
72
|
+
onChange={this.props.onChange}
|
|
73
|
+
onDragStart={this.props.onDragStart}
|
|
74
|
+
onDragEnd={this.props.onDragEnd}
|
|
75
|
+
/>,
|
|
76
|
+
rootElement
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A functional component that renders a draggable splitter line.
|
|
83
|
+
* It computes its position based on the provided split percentage and
|
|
84
|
+
* updates it during mouse drag events.
|
|
85
|
+
*/
|
|
86
|
+
function Splitter({
|
|
87
|
+
orientation,
|
|
88
|
+
initialSplit,
|
|
89
|
+
onChange,
|
|
90
|
+
onDragStart,
|
|
91
|
+
onDragEnd
|
|
92
|
+
}: {
|
|
93
|
+
orientation: 'vertical' | 'horizontal';
|
|
94
|
+
initialSplit: number;
|
|
95
|
+
onChange?: (newSplit: number) => void;
|
|
96
|
+
onDragStart?: () => void;
|
|
97
|
+
onDragEnd?: () => void;
|
|
98
|
+
}) {
|
|
99
|
+
const [split, setSplit] = useState(initialSplit);
|
|
100
|
+
const dragging = useRef(false);
|
|
101
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
102
|
+
|
|
103
|
+
const handleDragStart = (event: MouseEvent) => {
|
|
104
|
+
dragging.current = true;
|
|
105
|
+
onDragStart?.();
|
|
106
|
+
document.addEventListener('mousemove', handleDragging);
|
|
107
|
+
document.addEventListener('mouseup', handleDragEnd);
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handleDragging = (event: MouseEvent) => {
|
|
112
|
+
if (!dragging.current || !containerRef.current) return;
|
|
113
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
114
|
+
let newSplit: number;
|
|
115
|
+
if (orientation === 'vertical') {
|
|
116
|
+
newSplit = (event.clientX - rect.left) / rect.width;
|
|
117
|
+
} else {
|
|
118
|
+
newSplit = (event.clientY - rect.top) / rect.height;
|
|
119
|
+
}
|
|
120
|
+
// Clamp newSplit between 5% and 95%
|
|
121
|
+
newSplit = Math.min(Math.max(newSplit, 0.05), 0.95);
|
|
122
|
+
setSplit(newSplit);
|
|
123
|
+
onChange?.(newSplit);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleDragEnd = (event: MouseEvent) => {
|
|
127
|
+
if (!dragging.current) return;
|
|
128
|
+
dragging.current = false;
|
|
129
|
+
onDragEnd?.();
|
|
130
|
+
document.removeEventListener('mousemove', handleDragging);
|
|
131
|
+
document.removeEventListener('mouseup', handleDragEnd);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// The splitter line style based on orientation and the current split percentage.
|
|
135
|
+
const splitterStyle: h.JSX.CSSProperties =
|
|
136
|
+
orientation === 'vertical'
|
|
137
|
+
? {
|
|
138
|
+
position: 'absolute',
|
|
139
|
+
top: 0,
|
|
140
|
+
bottom: 0,
|
|
141
|
+
left: `${split * 100}%`,
|
|
142
|
+
width: '4px',
|
|
143
|
+
cursor: 'col-resize',
|
|
144
|
+
background: '#ccc',
|
|
145
|
+
zIndex: 10,
|
|
146
|
+
pointerEvents: 'auto',
|
|
147
|
+
boxShadow: 'inset -1px 0 0 white, inset 1px 0 0 white'
|
|
148
|
+
}
|
|
149
|
+
: {
|
|
150
|
+
position: 'absolute',
|
|
151
|
+
left: 0,
|
|
152
|
+
right: 0,
|
|
153
|
+
top: `${split * 100}%`,
|
|
154
|
+
height: '4px',
|
|
155
|
+
cursor: 'row-resize',
|
|
156
|
+
background: '#ccc',
|
|
157
|
+
zIndex: 10,
|
|
158
|
+
pointerEvents: 'auto',
|
|
159
|
+
boxShadow: 'inset -1px 0 0 white, inset 1px 0 0 white'
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Container style to fill the entire deck.gl canvas.
|
|
163
|
+
const containerStyle: h.JSX.CSSProperties = {
|
|
164
|
+
position: 'absolute',
|
|
165
|
+
top: 0,
|
|
166
|
+
left: 0,
|
|
167
|
+
right: 0,
|
|
168
|
+
bottom: 0
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div ref={containerRef} style={containerStyle}>
|
|
173
|
+
<div
|
|
174
|
+
style={splitterStyle}
|
|
175
|
+
onMouseDown={handleDragStart as h.JSX.MouseEventHandler<HTMLElement>} // Use the appropriate Preact event type.
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {Widget, WidgetPlacement, WidgetProps} from '@deck.gl/core';
|
|
2
|
+
import {luma} from '@luma.gl/core';
|
|
3
|
+
import {render} from 'preact';
|
|
4
|
+
import type {Stats, Stat} from '@probe.gl/stats';
|
|
5
|
+
|
|
6
|
+
const RIGHT_ARROW = '\u25b6';
|
|
7
|
+
const DOWN_ARROW = '\u2b07';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_COUNT_FORMATTER = (stat: Stat): string => `${stat.name}: ${stat.count}`;
|
|
10
|
+
|
|
11
|
+
function formatTime(time: number): string {
|
|
12
|
+
return time < 1000 ? `${time.toFixed(2)}ms` : `${(time / 1000).toFixed(2)}s`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatMemory(bytes: number): string {
|
|
16
|
+
const mb = bytes / 1e6;
|
|
17
|
+
return `${mb.toFixed(1)} MB`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_FORMATTERS: Record<string, (stat: Stat) => string> = {
|
|
21
|
+
count: DEFAULT_COUNT_FORMATTER,
|
|
22
|
+
averageTime: (stat: Stat) => `${stat.name}: ${formatTime(stat.getAverageTime())}`,
|
|
23
|
+
totalTime: (stat: Stat) => `${stat.name}: ${formatTime(stat.time)}`,
|
|
24
|
+
fps: (stat: Stat) => `${stat.name}: ${Math.round(stat.getHz())}fps`,
|
|
25
|
+
memory: (stat: Stat) => `${stat.name}: ${formatMemory(stat.count)}`
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type StatsWidgetProps = WidgetProps & {
|
|
29
|
+
type?: 'deck' | 'luma' | 'device' | 'custom';
|
|
30
|
+
/** Stats object to visualize. */
|
|
31
|
+
stats?: Stats;
|
|
32
|
+
/** Title shown in the header of the pop-up. Defaults to stats.id. */
|
|
33
|
+
title?: string;
|
|
34
|
+
/** How many redraws to wait between updates. */
|
|
35
|
+
framesPerUpdate?: number;
|
|
36
|
+
/** Custom formatters for stat values. */
|
|
37
|
+
formatters?: Record<string, string | ((stat: Stat) => string)>;
|
|
38
|
+
/** Whether to reset particular stats after each update. */
|
|
39
|
+
resetOnUpdate?: Record<string, boolean>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Displays probe.gl stats in a floating pop-up. */
|
|
43
|
+
export class StatsWidget extends Widget<StatsWidgetProps> {
|
|
44
|
+
static defaultProps: Required<StatsWidgetProps> = {
|
|
45
|
+
...Widget.defaultProps,
|
|
46
|
+
type: 'deck',
|
|
47
|
+
stats: undefined!,
|
|
48
|
+
title: 'Stats',
|
|
49
|
+
framesPerUpdate: 1,
|
|
50
|
+
formatters: {},
|
|
51
|
+
resetOnUpdate: {},
|
|
52
|
+
id: 'stats'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
className = 'deck-widget-stats';
|
|
56
|
+
placement = 'top-left' as WidgetPlacement;
|
|
57
|
+
|
|
58
|
+
private _counter = 0;
|
|
59
|
+
private _formatters: Record<string, (stat: Stat) => string>;
|
|
60
|
+
private _resetOnUpdate: Record<string, boolean>;
|
|
61
|
+
collapsed: boolean = true;
|
|
62
|
+
_stats: Stats;
|
|
63
|
+
|
|
64
|
+
constructor(props: StatsWidgetProps = {}) {
|
|
65
|
+
super(props, StatsWidget.defaultProps);
|
|
66
|
+
this._formatters = {...DEFAULT_FORMATTERS};
|
|
67
|
+
this.setProps(props);
|
|
68
|
+
this._resetOnUpdate = {...this.props.resetOnUpdate};
|
|
69
|
+
this._stats = this.props.stats;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setProps(props: Partial<StatsWidgetProps>): void {
|
|
73
|
+
super.setProps(props);
|
|
74
|
+
this._stats = this._getStats();
|
|
75
|
+
if (props.formatters) {
|
|
76
|
+
for (const name in props.formatters) {
|
|
77
|
+
const f = props.formatters[name];
|
|
78
|
+
this._formatters[name] =
|
|
79
|
+
typeof f === 'string' ? DEFAULT_FORMATTERS[f] || DEFAULT_COUNT_FORMATTER : f;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (props.resetOnUpdate) {
|
|
83
|
+
this._resetOnUpdate = {...props.resetOnUpdate};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
onAdd(): void {
|
|
88
|
+
this._stats = this._getStats();
|
|
89
|
+
this.updateHTML();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
onRenderHTML(rootElement: HTMLElement): void {
|
|
93
|
+
const stats = this._stats;
|
|
94
|
+
const collapsed = this.collapsed;
|
|
95
|
+
const title = this.props.title || stats?.id || 'Stats';
|
|
96
|
+
const items: JSX.Element[] = [];
|
|
97
|
+
|
|
98
|
+
if (!collapsed && stats) {
|
|
99
|
+
stats.forEach(stat => {
|
|
100
|
+
const lines = this._getLines(stat);
|
|
101
|
+
if (this._resetOnUpdate && this._resetOnUpdate[stat.name]) {
|
|
102
|
+
stat.reset();
|
|
103
|
+
}
|
|
104
|
+
lines.forEach((line, i) => {
|
|
105
|
+
items.push(
|
|
106
|
+
<div key={`${stat.name}-${i}`} style={{whiteSpace: 'pre'}}>
|
|
107
|
+
{line}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
render(
|
|
115
|
+
<div className="deck-widget-stats-container" style={{cursor: 'default'}}>
|
|
116
|
+
<div
|
|
117
|
+
className="deck-widget-stats-header"
|
|
118
|
+
style={{cursor: 'pointer', pointerEvents: 'auto'}}
|
|
119
|
+
onClick={this._toggleCollapsed}
|
|
120
|
+
>
|
|
121
|
+
{collapsed ? RIGHT_ARROW : DOWN_ARROW} {title}
|
|
122
|
+
</div>
|
|
123
|
+
{!collapsed && <div className="deck-widget-stats-content deck-widget-common">{items}</div>}
|
|
124
|
+
</div>,
|
|
125
|
+
rootElement
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onRedraw(): void {
|
|
130
|
+
const framesPerUpdate = Math.max(1, this.props.framesPerUpdate || 1);
|
|
131
|
+
if (this._counter++ % framesPerUpdate === 0) {
|
|
132
|
+
console.log('Redrawing stats widget');
|
|
133
|
+
this.updateHTML();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
protected _getStats(): Stats {
|
|
138
|
+
switch (this.props.type) {
|
|
139
|
+
case 'deck':
|
|
140
|
+
// @ts-expect-error stats is protected
|
|
141
|
+
return this.deck?.stats;
|
|
142
|
+
case 'luma':
|
|
143
|
+
return Array.from(luma.stats.stats.values())[0];
|
|
144
|
+
case 'device':
|
|
145
|
+
// @ts-expect-error is protected
|
|
146
|
+
const device = this.deck?.device;
|
|
147
|
+
const stats = device?.statsManager.stats.values();
|
|
148
|
+
return stats ? Array.from(stats)[0] : undefined;
|
|
149
|
+
case 'custom':
|
|
150
|
+
return this.props.stats;
|
|
151
|
+
default:
|
|
152
|
+
throw new Error(`Unknown stats type: ${this.props.type}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
protected _toggleCollapsed = (): void => {
|
|
157
|
+
this.collapsed = !this.collapsed;
|
|
158
|
+
this.updateHTML();
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
protected _getLines(stat: Stat): string[] {
|
|
162
|
+
const formatter =
|
|
163
|
+
this._formatters[stat.name] || this._formatters[stat.type || ''] || DEFAULT_COUNT_FORMATTER;
|
|
164
|
+
return formatter(stat).split('\n');
|
|
165
|
+
}
|
|
166
|
+
}
|