@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,115 @@
1
+ /* global document */
2
+ import {Widget, WidgetProps} from '@deck.gl/core';
3
+ import type {Deck, PickingInfo} from '@deck.gl/core';
4
+ import {render} from 'preact';
5
+ import {SimpleMenu} from './lib/components/simple-menu';
6
+
7
+ /** The standard, modern way is to use event.button === 2, where button is the standardized property (0 = left, 1 = middle, 2 = right). */
8
+ const MOUSE_BUTTON_RIGHT = 2;
9
+ /** A name for the legacy MouseEvent.which value that corresponds to the right-mouse button. In older browsers, the check is: if (event.which === 3) */
10
+ const MOUSE_WHICH_RIGHT = 3;
11
+
12
+ export type ContextWidgetMenuItem = {
13
+ label: string;
14
+ key: string;
15
+ };
16
+
17
+ export type ContextMenuWidgetProps = WidgetProps & {
18
+ /** View to attach to and interact with. Required when using multiple views. */
19
+ viewId?: string | null;
20
+ /** Controls visibility of the context menu */
21
+ visible?: boolean;
22
+ /** Screen position at which to place the menu */
23
+ position: {x: number; y: number};
24
+ /** Items to render */
25
+ menuItems: ContextWidgetMenuItem[];
26
+ /** Provide menu items for the menu given the picked object */
27
+ getMenuItems: (info: PickingInfo, widget: ContextMenuWidget) => ContextWidgetMenuItem[] | null;
28
+ /** Callback with the selected item */
29
+ onMenuItemSelected?: (key: string, pickInfo: PickingInfo | null) => void;
30
+ };
31
+
32
+ export class ContextMenuWidget extends Widget<ContextMenuWidgetProps> {
33
+ static defaultProps: Required<ContextMenuWidgetProps> = {
34
+ ...Widget.defaultProps,
35
+ viewId: null,
36
+ visible: false,
37
+ position: {x: 0, y: 0},
38
+ getMenuItems: undefined!,
39
+ menuItems: [],
40
+ // eslint-disable-next-line no-console
41
+ onMenuItemSelected: (key, pickInfo) => console.log('Context menu item selected:', key, pickInfo)
42
+ };
43
+
44
+ className = 'deck-widget-context-menu';
45
+ placement = 'fill' as const;
46
+
47
+ pickInfo: PickingInfo | null = null;
48
+
49
+ constructor(props: ContextMenuWidgetProps) {
50
+ super(props, ContextMenuWidget.defaultProps);
51
+ this.pickInfo = null;
52
+ this.setProps(props);
53
+ }
54
+
55
+ onAdd({deck}: {deck: Deck<any>}): HTMLDivElement {
56
+ const element = document.createElement('div');
57
+ element.classList.add('deck-widget', 'deck-widget-context-menu');
58
+ const style = {
59
+ margin: '0px',
60
+ top: '0px',
61
+ left: '0px',
62
+ position: 'absolute',
63
+ pointerEvents: 'auto'
64
+ };
65
+ Object.entries(style).forEach(([key, value]) => element.style.setProperty(key, value));
66
+
67
+ deck.getCanvas()?.addEventListener('click', () => this.hide());
68
+ deck.getCanvas()?.addEventListener('contextmenu', event => this.handleContextMenu(event));
69
+ return element;
70
+ }
71
+
72
+ onRenderHTML(rootElement: HTMLElement): void {
73
+ const {visible, position, menuItems} = this.props;
74
+
75
+ const ui =
76
+ visible && menuItems.length ? (
77
+ <SimpleMenu
78
+ menuItems={menuItems}
79
+ onItemSelected={key => this.props.onMenuItemSelected(key, this.pickInfo)}
80
+ position={position}
81
+ style={{pointerEvents: 'auto'}}
82
+ />
83
+ ) : null;
84
+ render(ui, rootElement);
85
+ }
86
+
87
+ handleContextMenu(srcEvent: MouseEvent): boolean {
88
+ if (
89
+ srcEvent &&
90
+ (srcEvent.button === MOUSE_BUTTON_RIGHT || srcEvent.which === MOUSE_WHICH_RIGHT)
91
+ ) {
92
+ this.pickInfo =
93
+ this.deck?.pickObject({
94
+ x: srcEvent.clientX,
95
+ y: srcEvent.clientY
96
+ }) || null;
97
+ const menuItems = (this.pickInfo && this.props.getMenuItems?.(this.pickInfo, this)) || [];
98
+ const visible = menuItems.length > 0;
99
+ this.setProps({
100
+ visible,
101
+ position: {x: srcEvent.clientX, y: srcEvent.clientY},
102
+ menuItems
103
+ });
104
+ this.updateHTML();
105
+ srcEvent.preventDefault();
106
+ return visible;
107
+ }
108
+
109
+ return false;
110
+ }
111
+
112
+ hide(): void {
113
+ this.setProps({visible: false});
114
+ }
115
+ }
@@ -0,0 +1,67 @@
1
+ // deck.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {Widget, WidgetProps} from '@deck.gl/core';
6
+ import type {WidgetPlacement, Deck} from '@deck.gl/core';
7
+
8
+ /** Properties for the FpsWidget. */
9
+ export type FpsWidgetProps = WidgetProps & {
10
+ /** Widget positioning within the view. Default 'top-left'. */
11
+ placement?: WidgetPlacement;
12
+ };
13
+
14
+ /**
15
+ * Displays the average frames per second reported by the Deck instance.
16
+ */
17
+ export class FpsWidget extends Widget<FpsWidgetProps> {
18
+ static defaultProps: Required<FpsWidgetProps> = {
19
+ ...Widget.defaultProps,
20
+ id: 'fps',
21
+ placement: 'top-left'
22
+ };
23
+
24
+ className = 'deck-widget-fps';
25
+ placement: WidgetPlacement = 'top-left';
26
+
27
+ private _lastFps: number = -1;
28
+
29
+ constructor(props: FpsWidgetProps = {}) {
30
+ super(props, FpsWidget.defaultProps);
31
+ this.placement = props.placement ?? this.placement;
32
+ }
33
+
34
+ setProps(props: Partial<FpsWidgetProps>): void {
35
+ if (props.placement) {
36
+ this.placement = props.placement;
37
+ }
38
+ super.setProps(props);
39
+ }
40
+
41
+ onRenderHTML(rootElement: HTMLElement): void {
42
+ const fps = this._getFps();
43
+ rootElement.innerText = `FPS:\n${fps}`;
44
+ rootElement.style.backgroundColor = 'white';
45
+ rootElement.style.fontFamily = 'monospace';
46
+ rootElement.style.fontSize = '8px';
47
+ rootElement.style.fontWeight = '700'; // Make font bolder on click
48
+ }
49
+
50
+ onRedraw({}: {viewports: any[]; layers: any[]}): void {
51
+ const fps = this._getFps();
52
+ if (fps !== this._lastFps) {
53
+ this._lastFps = fps;
54
+ this.updateHTML();
55
+ }
56
+ }
57
+
58
+ onAdd({}: {deck: Deck<any>; viewId: string | null}): void {
59
+ this._lastFps = this._getFps();
60
+ this.updateHTML();
61
+ }
62
+
63
+ _getFps(): number {
64
+ // @ts-expect-error protected
65
+ return Math.round(this.deck.metrics.fps ?? 0);
66
+ }
67
+ }
@@ -3,121 +3,68 @@
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
5
  /* global document */
6
- import {
7
- _deepEqual as deepEqual,
8
- _applyStyles as applyStyles,
9
- _removeStyles as removeStyles
10
- } from '@deck.gl/core';
11
- import type {Deck, Widget, WidgetPlacement} from '@deck.gl/core';
6
+ import {Widget, type WidgetProps, type WidgetPlacement} from '@deck.gl/core';
12
7
  import {render} from 'preact';
13
- import {IconButton} from './components';
8
+ import {IconButton} from './lib/components/icon-button';
14
9
 
15
- export type FullscreenWidgetProps = {
10
+ /* eslint-enable max-len */
11
+
12
+ export type FullscreenWidgetProps = WidgetProps & {
16
13
  id?: string;
17
- /**
18
- * Widget positioning within the view. Default 'top-left'.
19
- */
14
+ /** Widget positioning within the view. Default 'top-left'. */
20
15
  placement?: WidgetPlacement;
21
- /**
22
- * A [compatible DOM element](https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullScreen#Compatible_elements) which should be made full screen.
23
- * By default, the map container element will be made full screen.
24
- */
25
- /* eslint-enable max-len */
26
- container?: HTMLElement;
27
- /**
28
- * Tooltip message when out of fullscreen.
29
- */
16
+ /** Tooltip message when out of fullscreen. */
30
17
  enterLabel?: string;
31
- /**
32
- * Tooltip message when fullscreen.
33
- */
18
+ /** Tooltip message when fullscreen. */
34
19
  exitLabel?: string;
35
20
  /**
36
- * CSS inline style overrides.
21
+ * A compatible DOM element which should be made full screen. By default, the map container element will be made full screen.
22
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullScreen#Compatible_elements
37
23
  */
38
- style?: Partial<CSSStyleDeclaration>;
39
- /**
40
- * Additional CSS class.
41
- */
42
- className?: string;
24
+ container?: HTMLElement;
43
25
  };
44
26
 
45
- export class FullscreenWidget implements Widget<FullscreenWidgetProps> {
46
- id = 'fullscreen';
47
- props: FullscreenWidgetProps;
27
+ export class FullscreenWidget extends Widget<FullscreenWidgetProps> {
28
+ static defaultProps: Required<FullscreenWidgetProps> = {
29
+ ...Widget.defaultProps,
30
+ id: 'fullscreen',
31
+ placement: 'top-left',
32
+ enterLabel: 'Enter Fullscreen',
33
+ exitLabel: 'Exit Fullscreen',
34
+ container: undefined!
35
+ };
36
+
37
+ className = 'deck-widget-fullscreen';
48
38
  placement: WidgetPlacement = 'top-left';
49
-
50
- deck?: Deck<any>;
51
- element?: HTMLDivElement;
52
-
53
39
  fullscreen: boolean = false;
54
40
 
55
- constructor(props: FullscreenWidgetProps) {
56
- this.id = props.id ?? this.id;
41
+ constructor(props: FullscreenWidgetProps = {}) {
42
+ super(props, FullscreenWidget.defaultProps);
57
43
  this.placement = props.placement ?? this.placement;
58
-
59
- this.props = {
60
- ...props,
61
- enterLabel: props.enterLabel ?? 'Enter Fullscreen',
62
- exitLabel: props.exitLabel ?? 'Exit Fullscreen',
63
- style: props.style ?? {}
64
- };
65
44
  }
66
45
 
67
- onAdd({deck}: {deck: Deck<any>}): HTMLDivElement {
68
- const {style, className} = this.props;
69
- const el = document.createElement('div');
70
- el.classList.add('deck-widget', 'deck-widget-fullscreen');
71
- if (className) el.classList.add(className);
72
- applyStyles(el, style);
73
- this.deck = deck;
74
- this.element = el;
75
- this.update();
46
+ onAdd(): void {
76
47
  document.addEventListener('fullscreenchange', this.onFullscreenChange.bind(this));
77
- return el;
78
48
  }
79
49
 
80
50
  onRemove() {
81
- this.deck = undefined;
82
- this.element = undefined;
83
51
  document.removeEventListener('fullscreenchange', this.onFullscreenChange.bind(this));
84
52
  }
85
53
 
86
- private update() {
87
- const {enterLabel, exitLabel} = this.props;
88
- const element = this.element;
89
- if (!element) {
90
- return;
91
- }
92
-
93
- const ui = (
54
+ onRenderHTML(rootElement: HTMLElement): void {
55
+ render(
94
56
  <IconButton
95
57
  onClick={this.handleClick.bind(this)}
96
- label={this.fullscreen ? exitLabel : enterLabel}
58
+ label={this.fullscreen ? this.props.exitLabel : this.props.enterLabel}
97
59
  className={this.fullscreen ? 'deck-widget-fullscreen-exit' : 'deck-widget-fullscreen-enter'}
98
- />
60
+ />,
61
+ rootElement
99
62
  );
100
- render(ui, element);
101
63
  }
102
64
 
103
65
  setProps(props: Partial<FullscreenWidgetProps>) {
104
66
  this.placement = props.placement ?? this.placement;
105
- const oldProps = this.props;
106
- const el = this.element;
107
- if (el) {
108
- if (oldProps.className !== props.className) {
109
- if (oldProps.className) el.classList.remove(oldProps.className);
110
- if (props.className) el.classList.add(props.className);
111
- }
112
-
113
- if (!deepEqual(oldProps.style, props.style, 1)) {
114
- removeStyles(el, oldProps.style);
115
- applyStyles(el, props.style);
116
- }
117
- }
118
-
119
- Object.assign(this.props, props);
120
- this.update();
67
+ super.setProps(props);
121
68
  }
122
69
 
123
70
  getContainer() {
@@ -130,7 +77,7 @@ export class FullscreenWidget implements Widget<FullscreenWidgetProps> {
130
77
  if (prevFullscreen !== fullscreen) {
131
78
  this.fullscreen = !this.fullscreen;
132
79
  }
133
- this.update();
80
+ this.updateHTML();
134
81
  }
135
82
 
136
83
  async handleClick() {
@@ -139,7 +86,7 @@ export class FullscreenWidget implements Widget<FullscreenWidgetProps> {
139
86
  } else {
140
87
  await this.requestFullscreen();
141
88
  }
142
- this.update();
89
+ this.updateHTML();
143
90
  }
144
91
 
145
92
  async requestFullscreen() {
@@ -0,0 +1,209 @@
1
+ // deck.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {Widget, WidgetProps} from '@deck.gl/core';
6
+ import type {WidgetPlacement, Viewport} from '@deck.gl/core';
7
+ import {FlyToInterpolator, LinearInterpolator} from '@deck.gl/core';
8
+ import {render} from 'preact';
9
+ import {DropdownMenu} from './lib/components/dropdown-menu';
10
+ import {type Geocoder} from './lib/geocode/geocoder';
11
+ import {GeocoderHistory} from './lib/geocode/geocoder-history';
12
+ import {
13
+ GoogleGeocoder,
14
+ MapboxGeocoder,
15
+ OpenCageGeocoder,
16
+ CoordinatesGeocoder,
17
+ CurrentLocationGeocoder
18
+ } from './lib/geocode/geocoders';
19
+
20
+ /** @todo - is the the best we can do? */
21
+ type ViewState = Record<string, unknown>;
22
+
23
+ const CURRENT_LOCATION = 'current';
24
+
25
+ /** Properties for the GeocoderWidget */
26
+ export type GeocoderWidgetProps = WidgetProps & {
27
+ viewId?: string;
28
+ /** Widget positioning within the view. Default 'top-left'. */
29
+ placement?: WidgetPlacement;
30
+ /** Tooltip message */
31
+ label?: string;
32
+ transitionDuration?: number;
33
+ /** Geocoding service selector, for declarative usage */
34
+ geocoder?: 'google' | 'mapbox' | 'opencage' | 'coordinates' | 'custom';
35
+ /** Custom geocoding service (Used when geocoder = 'custom') */
36
+ customGeocoder?: Geocoder;
37
+ /** API key used for geocoding services */
38
+ apiKey?: string;
39
+ /** Whether to use geolocation @note Experimental*/
40
+ _geolocation?: boolean;
41
+ };
42
+
43
+ /**
44
+ * A widget that display a text box that lets user type in a location
45
+ * and a button that moves the view to that location.
46
+ * @todo For now only supports coordinates, Could be extended with location service integrations.
47
+ */
48
+ export class GeocoderWidget extends Widget<GeocoderWidgetProps> {
49
+ static defaultProps: Required<GeocoderWidgetProps> = {
50
+ ...Widget.defaultProps,
51
+ id: 'geolocate',
52
+ viewId: undefined!,
53
+ placement: 'top-left',
54
+ label: 'Geolocate',
55
+ transitionDuration: 200,
56
+ geocoder: 'coordinates',
57
+ customGeocoder: CoordinatesGeocoder,
58
+ apiKey: '',
59
+ _geolocation: false
60
+ };
61
+
62
+ className = 'deck-widget-geocoder';
63
+ placement: WidgetPlacement = 'top-left';
64
+
65
+ geocodeHistory = new GeocoderHistory({});
66
+ addressText: string = '';
67
+ geocoder: Geocoder = CoordinatesGeocoder;
68
+
69
+ constructor(props: GeocoderWidgetProps = {}) {
70
+ super(props, GeocoderWidget.defaultProps);
71
+ this.placement = props.placement ?? this.placement;
72
+ this.setProps(props);
73
+ this.geocoder = getGeocoder(props);
74
+ }
75
+
76
+ setProps(props: Partial<GeocoderWidgetProps>): void {
77
+ super.setProps(props);
78
+ this.placement = props.placement ?? this.placement;
79
+ this.geocoder = getGeocoder(this.props);
80
+ if (this.geocoder.requiresApiKey && !this.props.apiKey) {
81
+ throw new Error(`API key is required for the ${this.geocoder.name} geocoder`);
82
+ }
83
+ }
84
+
85
+ onRenderHTML(rootElement: HTMLElement): void {
86
+ const menuItems = this.props._geolocation
87
+ ? [CURRENT_LOCATION, ...this.geocodeHistory.addressHistory]
88
+ : [...this.geocodeHistory.addressHistory];
89
+ render(
90
+ <div
91
+ className="deck-widget-geocoder"
92
+ style={{
93
+ pointerEvents: 'auto',
94
+ display: 'flex',
95
+ alignItems: 'center',
96
+ flexWrap: 'wrap' // Allows wrapping on smaller screens
97
+ }}
98
+ >
99
+ <input
100
+ type="text"
101
+ placeholder={this.geocoder.placeholderLocation ?? 'Enter address or location'}
102
+ value={this.geocodeHistory.addressText}
103
+ // @ts-expect-error event type
104
+ onInput={e => this.setInput(e.target?.value || '')}
105
+ onKeyPress={this.handleKeyPress}
106
+ style={{
107
+ flex: '1 1 auto',
108
+ minWidth: '200px',
109
+ margin: 0,
110
+ padding: '8px',
111
+ boxSizing: 'border-box'
112
+ }}
113
+ />
114
+ <DropdownMenu
115
+ menuItems={menuItems}
116
+ onSelect={this.handleSelect}
117
+ style={{
118
+ margin: 2,
119
+ padding: '4px 2px',
120
+ boxSizing: 'border-box'
121
+ }}
122
+ />
123
+ {this.geocodeHistory.errorText && (
124
+ <div className="error">{this.geocodeHistory.errorText}</div>
125
+ )}
126
+ </div>,
127
+ rootElement
128
+ );
129
+ }
130
+
131
+ setInput = (text: string) => {
132
+ this.addressText = text;
133
+ };
134
+
135
+ handleKeyPress = e => {
136
+ if (e.key === 'Enter') {
137
+ this.handleSubmit();
138
+ }
139
+ };
140
+
141
+ handleSelect = (address: string) => {
142
+ this.setInput(address);
143
+ this.handleSubmit();
144
+ };
145
+
146
+ /** Sync wrapper for async geocode() */
147
+ handleSubmit = () => {
148
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
149
+ this.geocode(this.addressText);
150
+ };
151
+
152
+ /** Perform geocoding */
153
+ geocode: (address: string) => Promise<void> = async address => {
154
+ const useGeolocation = this.props._geolocation && address === CURRENT_LOCATION;
155
+ const geocoder = useGeolocation ? CurrentLocationGeocoder : this.geocoder;
156
+ const coordinates = await this.geocodeHistory.geocode(
157
+ geocoder,
158
+ this.addressText,
159
+ this.props.apiKey
160
+ );
161
+ if (coordinates) {
162
+ this.setViewState(coordinates);
163
+ }
164
+ };
165
+
166
+ // TODO - MOVE TO WIDGETIMPL?
167
+ setViewState(viewState: ViewState) {
168
+ const viewId = this.props.viewId || (viewState?.id as string) || 'default-view';
169
+ const viewport = this.viewports[viewId] || {};
170
+ const nextViewState: ViewState = {
171
+ ...viewport,
172
+ ...viewState
173
+ };
174
+ if (this.props.transitionDuration > 0) {
175
+ nextViewState.transitionDuration = this.props.transitionDuration;
176
+ nextViewState.transitionInterpolator =
177
+ 'latitude' in nextViewState ? new FlyToInterpolator() : new LinearInterpolator();
178
+ }
179
+
180
+ // @ts-ignore Using private method temporary until there's a public one
181
+ this.deck._onViewStateChange({viewId, viewState: nextViewState, interactionState: {}});
182
+ }
183
+
184
+ onViewportChange(viewport: Viewport) {
185
+ this.viewports[viewport.id] = viewport;
186
+ }
187
+
188
+ viewports: Record<string, Viewport> = {};
189
+ }
190
+
191
+ function getGeocoder(props: {geocoder?: string; customGeocoder?: Geocoder}): Geocoder {
192
+ switch (props.geocoder) {
193
+ case 'google':
194
+ return GoogleGeocoder;
195
+ case 'mapbox':
196
+ return MapboxGeocoder;
197
+ case 'opencage':
198
+ return OpenCageGeocoder;
199
+ case 'coordinates':
200
+ return CoordinatesGeocoder;
201
+ case 'custom':
202
+ if (!props.customGeocoder) {
203
+ throw new Error('Custom geocoder is not defined');
204
+ }
205
+ return props.customGeocoder;
206
+ default:
207
+ throw new Error(`Unknown geocoder: ${props.geocoder}`);
208
+ }
209
+ }