@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.
Files changed (157) hide show
  1. package/dist/compass-widget.d.ts +11 -34
  2. package/dist/compass-widget.d.ts.map +1 -1
  3. package/dist/compass-widget.js +27 -62
  4. package/dist/compass-widget.js.map +1 -1
  5. package/dist/context-menu-widget.d.ts +37 -0
  6. package/dist/context-menu-widget.d.ts.map +1 -0
  7. package/dist/context-menu-widget.js +74 -0
  8. package/dist/context-menu-widget.js.map +1 -0
  9. package/dist/dist.dev.js +2271 -581
  10. package/dist/fps-widget.d.ts +29 -0
  11. package/dist/fps-widget.d.ts.map +1 -0
  12. package/dist/fps-widget.js +51 -0
  13. package/dist/fps-widget.js.map +1 -0
  14. package/dist/fullscreen-widget.d.ts +15 -33
  15. package/dist/fullscreen-widget.d.ts.map +1 -1
  16. package/dist/fullscreen-widget.js +20 -51
  17. package/dist/fullscreen-widget.js.map +1 -1
  18. package/dist/geocoder-widget.d.ts +51 -0
  19. package/dist/geocoder-widget.d.ts.map +1 -0
  20. package/dist/geocoder-widget.js +138 -0
  21. package/dist/geocoder-widget.js.map +1 -0
  22. package/dist/gimbal-widget.d.ts +33 -0
  23. package/dist/gimbal-widget.d.ts.map +1 -0
  24. package/dist/gimbal-widget.js +116 -0
  25. package/dist/gimbal-widget.js.map +1 -0
  26. package/dist/index.cjs +1587 -341
  27. package/dist/index.cjs.map +4 -4
  28. package/dist/index.d.ts +37 -8
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +27 -7
  31. package/dist/index.js.map +1 -1
  32. package/dist/info-widget.d.ts +37 -0
  33. package/dist/info-widget.d.ts.map +1 -0
  34. package/dist/info-widget.js +183 -0
  35. package/dist/info-widget.js.map +1 -0
  36. package/dist/lib/components/button-group.d.ts +8 -0
  37. package/dist/lib/components/button-group.d.ts.map +1 -0
  38. package/dist/lib/components/button-group.js +7 -0
  39. package/dist/lib/components/button-group.js.map +1 -0
  40. package/dist/lib/components/dropdown-menu.d.ts +8 -0
  41. package/dist/lib/components/dropdown-menu.d.ts.map +1 -0
  42. package/dist/lib/components/dropdown-menu.js +55 -0
  43. package/dist/lib/components/dropdown-menu.js.map +1 -0
  44. package/dist/lib/components/grouped-icon-button.d.ts +8 -0
  45. package/dist/lib/components/grouped-icon-button.d.ts.map +1 -0
  46. package/dist/lib/components/grouped-icon-button.js +7 -0
  47. package/dist/lib/components/grouped-icon-button.js.map +1 -0
  48. package/dist/lib/components/icon-button.d.ts +11 -0
  49. package/dist/lib/components/icon-button.d.ts.map +1 -0
  50. package/dist/lib/components/icon-button.js +7 -0
  51. package/dist/lib/components/icon-button.js.map +1 -0
  52. package/dist/lib/components/icon-menu.d.ts +16 -0
  53. package/dist/lib/components/icon-menu.d.ts.map +1 -0
  54. package/dist/lib/components/icon-menu.js +49 -0
  55. package/dist/lib/components/icon-menu.js.map +1 -0
  56. package/dist/lib/components/simple-menu.d.ts +16 -0
  57. package/dist/lib/components/simple-menu.d.ts.map +1 -0
  58. package/dist/lib/components/simple-menu.js +30 -0
  59. package/dist/lib/components/simple-menu.js.map +1 -0
  60. package/dist/lib/geocode/geocoder-history.d.ts +23 -0
  61. package/dist/lib/geocode/geocoder-history.d.ts.map +1 -0
  62. package/dist/lib/geocode/geocoder-history.js +64 -0
  63. package/dist/lib/geocode/geocoder-history.js.map +1 -0
  64. package/dist/lib/geocode/geocoder.d.ts +15 -0
  65. package/dist/lib/geocode/geocoder.d.ts.map +1 -0
  66. package/dist/lib/geocode/geocoder.js +5 -0
  67. package/dist/lib/geocode/geocoder.js.map +1 -0
  68. package/dist/lib/geocode/geocoders.d.ts +68 -0
  69. package/dist/lib/geocode/geocoders.d.ts.map +1 -0
  70. package/dist/lib/geocode/geocoders.js +182 -0
  71. package/dist/lib/geocode/geocoders.js.map +1 -0
  72. package/dist/loading-widget.d.ts +26 -0
  73. package/dist/loading-widget.d.ts.map +1 -0
  74. package/dist/loading-widget.js +41 -0
  75. package/dist/loading-widget.js.map +1 -0
  76. package/dist/reset-view-widget.d.ts +4 -4
  77. package/dist/reset-view-widget.d.ts.map +1 -1
  78. package/dist/reset-view-widget.js +7 -10
  79. package/dist/reset-view-widget.js.map +1 -1
  80. package/dist/scale-widget.d.ts +28 -0
  81. package/dist/scale-widget.d.ts.map +1 -0
  82. package/dist/scale-widget.js +111 -0
  83. package/dist/scale-widget.js.map +1 -0
  84. package/dist/screenshot-widget.d.ts +4 -4
  85. package/dist/screenshot-widget.d.ts.map +1 -1
  86. package/dist/screenshot-widget.js +7 -14
  87. package/dist/screenshot-widget.js.map +1 -1
  88. package/dist/splitter-widget.d.ts +32 -0
  89. package/dist/splitter-widget.d.ts.map +1 -0
  90. package/dist/splitter-widget.js +123 -0
  91. package/dist/splitter-widget.js.map +1 -0
  92. package/dist/stats-widget.d.ts +36 -0
  93. package/dist/stats-widget.d.ts.map +1 -0
  94. package/dist/stats-widget.js +115 -0
  95. package/dist/stats-widget.js.map +1 -0
  96. package/dist/stylesheet.css +121 -4
  97. package/dist/theme-widget.d.ts +31 -0
  98. package/dist/theme-widget.d.ts.map +1 -0
  99. package/dist/theme-widget.js +83 -0
  100. package/dist/theme-widget.js.map +1 -0
  101. package/dist/themes.d.ts +65 -36
  102. package/dist/themes.d.ts.map +1 -1
  103. package/dist/themes.js +20 -8
  104. package/dist/themes.js.map +1 -1
  105. package/dist/timeline-widget.d.ts +59 -0
  106. package/dist/timeline-widget.d.ts.map +1 -0
  107. package/dist/timeline-widget.js +83 -0
  108. package/dist/timeline-widget.js.map +1 -0
  109. package/dist/view-selector-widget.d.ts +33 -0
  110. package/dist/view-selector-widget.d.ts.map +1 -0
  111. package/dist/view-selector-widget.js +62 -0
  112. package/dist/view-selector-widget.js.map +1 -0
  113. package/dist/zoom-widget.d.ts +14 -43
  114. package/dist/zoom-widget.d.ts.map +1 -1
  115. package/dist/zoom-widget.js +14 -56
  116. package/dist/zoom-widget.js.map +1 -1
  117. package/dist.min.js +3 -1
  118. package/package.json +4 -3
  119. package/src/compass-widget.tsx +44 -106
  120. package/src/context-menu-widget.tsx +115 -0
  121. package/src/fps-widget.tsx +67 -0
  122. package/src/fullscreen-widget.tsx +33 -86
  123. package/src/geocoder-widget.tsx +209 -0
  124. package/src/gimbal-widget.tsx +188 -0
  125. package/src/index.ts +58 -8
  126. package/src/info-widget.tsx +225 -0
  127. package/src/lib/components/button-group.tsx +16 -0
  128. package/src/lib/components/dropdown-menu.tsx +98 -0
  129. package/src/lib/components/grouped-icon-button.tsx +24 -0
  130. package/src/lib/components/icon-button.tsx +30 -0
  131. package/src/lib/components/icon-menu.tsx +89 -0
  132. package/src/lib/components/simple-menu.tsx +52 -0
  133. package/src/lib/geocode/geocoder-history.ts +73 -0
  134. package/src/lib/geocode/geocoder.ts +15 -0
  135. package/src/lib/geocode/geocoders.ts +199 -0
  136. package/src/loading-widget.tsx +67 -0
  137. package/src/reset-view-widget.tsx +8 -11
  138. package/src/scale-widget.tsx +183 -0
  139. package/src/screenshot-widget.tsx +9 -11
  140. package/src/splitter-widget.tsx +179 -0
  141. package/src/stats-widget.tsx +166 -0
  142. package/src/stylesheet.css +121 -4
  143. package/src/theme-widget.tsx +120 -0
  144. package/src/themes.ts +45 -12
  145. package/src/timeline-widget.tsx +155 -0
  146. package/src/view-selector-widget.tsx +108 -0
  147. package/src/zoom-widget.tsx +39 -113
  148. package/dist/components.d.ts +0 -4
  149. package/dist/components.d.ts.map +0 -1
  150. package/dist/components.js +0 -17
  151. package/dist/components.js.map +0 -1
  152. package/dist/widget-impl.d.ts +0 -29
  153. package/dist/widget-impl.d.ts.map +0 -1
  154. package/dist/widget-impl.js +0 -60
  155. package/dist/widget-impl.js.map +0 -1
  156. package/src/components.tsx +0 -38
  157. 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 = WidgetImplProps & {
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 WidgetImpl<ScreenshotWidgetProps> {
29
+ export class ScreenshotWidget extends Widget<ScreenshotWidgetProps> {
30
30
  static defaultProps: Required<ScreenshotWidgetProps> = {
31
- ...WidgetImpl.defaultProps,
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({...ScreenshotWidget.defaultProps, ...props});
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
- element
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
+ }