@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,89 @@
1
+ import type {JSX} from 'preact';
2
+ import {useState, useRef, useEffect} from 'preact/hooks';
3
+ import {IconButton} from './icon-button';
4
+
5
+ const MENU_STYLE: JSX.CSSProperties = {
6
+ position: 'absolute',
7
+ top: '100%',
8
+ left: 0,
9
+ background: 'white',
10
+ border: '1px solid #ccc',
11
+ borderRadius: '4px',
12
+ marginTop: '4px',
13
+ zIndex: 100
14
+ };
15
+
16
+ const MENU_ITEM_STYLE: JSX.CSSProperties = {
17
+ background: 'white',
18
+ border: 'none',
19
+ padding: '4px',
20
+ cursor: 'pointer',
21
+ pointerEvents: 'auto'
22
+ };
23
+
24
+ export type IconMenuProps<KeyType = string> = {
25
+ className: string;
26
+ icon?: () => JSX.Element;
27
+ label?: string;
28
+ menuItems: {value: KeyType; icon: () => JSX.Element; label: string}[];
29
+ initialItem: KeyType;
30
+ onItemSelected: (item: KeyType) => void;
31
+ };
32
+
33
+ /** A component that renders an icon popup menu */
34
+ export function IconMenu<KeyType extends string>(props: IconMenuProps<KeyType>) {
35
+ const [menuOpen, setMenuOpen] = useState(false);
36
+ const containerRef = useRef<HTMLDivElement>(null);
37
+
38
+ // Close the menu when clicking outside.
39
+ const handleClickOutside = (event: MouseEvent) => {
40
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
41
+ setMenuOpen(false);
42
+ }
43
+ };
44
+
45
+ useEffect(() => {
46
+ document.addEventListener('mousedown', handleClickOutside);
47
+ return () => {
48
+ document.removeEventListener('mousedown', handleClickOutside);
49
+ };
50
+ }, [containerRef]);
51
+
52
+ const [selectedItem, setSelectedItem] = useState<KeyType>(props.initialItem);
53
+
54
+ const handleSelectItem = (item: KeyType) => {
55
+ setSelectedItem(item);
56
+ setMenuOpen(false);
57
+ props.onItemSelected(item);
58
+ };
59
+
60
+ const handleButtonClick = () => setMenuOpen(!menuOpen);
61
+
62
+ const selectedMenuItem = props.menuItems.find(item => item.value === selectedItem);
63
+ const label = props.label || selectedMenuItem?.label || '';
64
+ const Icon = (props.icon || selectedMenuItem?.icon)!;
65
+
66
+ return (
67
+ <div style={{position: 'relative', display: 'inline-block'}} ref={containerRef}>
68
+ <IconButton className={props.className} label={label} onClick={handleButtonClick}>
69
+ <Icon />
70
+ </IconButton>
71
+ {menuOpen && (
72
+ <div style={MENU_STYLE}>
73
+ <div style={{display: 'flex', flexDirection: 'column'}}>
74
+ {props.menuItems.map(item => (
75
+ <button
76
+ key={item.value}
77
+ style={MENU_ITEM_STYLE}
78
+ title={item.label}
79
+ onClick={() => handleSelectItem(item.value)}
80
+ >
81
+ {item.icon}
82
+ </button>
83
+ ))}
84
+ </div>
85
+ </div>
86
+ )}
87
+ </div>
88
+ );
89
+ }
@@ -0,0 +1,52 @@
1
+ import type {JSX} from 'preact';
2
+
3
+ const MENU_STYLE: JSX.CSSProperties = {
4
+ position: 'absolute',
5
+ top: '100%',
6
+ left: 0,
7
+ background: 'white',
8
+ border: '1px solid #ccc',
9
+ borderRadius: '4px',
10
+ marginTop: '4px',
11
+ zIndex: 100
12
+ };
13
+
14
+ const MENU_ITEM_STYLE: JSX.CSSProperties = {
15
+ background: 'white',
16
+ border: 'none',
17
+ padding: '4px',
18
+ cursor: 'pointer',
19
+ pointerEvents: 'auto'
20
+ };
21
+
22
+ export type SimpleMenuProps = {
23
+ menuItems: {key: string; label: string}[];
24
+ onItemSelected: (key: string) => void;
25
+ position: {x: number; y: number};
26
+ style?: JSX.CSSProperties;
27
+ };
28
+
29
+ /** Renders a simple dropdown menu at an arbitrary position */
30
+ export const SimpleMenu = (props: SimpleMenuProps) => {
31
+ const {menuItems, onItemSelected, position, style} = props;
32
+ const styleOverride: JSX.CSSProperties = {
33
+ ...MENU_STYLE,
34
+ ...style,
35
+ left: `${position.x}px`,
36
+ top: `${position.y}px`
37
+ };
38
+
39
+ return (
40
+ <div style={styleOverride}>
41
+ {menuItems.map(({key, label}) => (
42
+ <button
43
+ key={key}
44
+ style={{...MENU_ITEM_STYLE, display: 'block'}}
45
+ onClick={_ => onItemSelected(key)}
46
+ >
47
+ {label}
48
+ </button>
49
+ ))}
50
+ </div>
51
+ );
52
+ };
@@ -0,0 +1,73 @@
1
+ // deck.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {type Geocoder} from './geocoder';
6
+
7
+ const CURRENT_LOCATION = 'current';
8
+ const LOCAL_STORAGE_KEY = 'deck-geocoder-history';
9
+
10
+ export type GeocoderHistoryProps = {
11
+ maxEntries?: number;
12
+ };
13
+
14
+ /**
15
+ * An internal, experimental helper class for storing a list of locations in local storage.
16
+ * @todo Remove the UI related state.
17
+ */
18
+ export class GeocoderHistory {
19
+ props: Required<GeocoderHistoryProps>;
20
+ addressText = '';
21
+ errorText = '';
22
+ addressHistory: string[] = [];
23
+
24
+ constructor(props: GeocoderHistoryProps) {
25
+ this.props = {maxEntries: 5, ...props};
26
+ this.addressHistory = this.loadPreviousAddresses();
27
+ }
28
+
29
+ /** PErform geocoding */
30
+ async geocode(geocoder: Geocoder, address: string, apiKey: string) {
31
+ this.errorText = '';
32
+ this.addressText = address;
33
+ try {
34
+ const coordinates = await geocoder.geocode(address, apiKey);
35
+ if (coordinates) {
36
+ this.storeAddress(this.addressText);
37
+ return coordinates;
38
+ }
39
+ this.errorText = 'Invalid address';
40
+ } catch (error) {
41
+ this.errorText = `${(error as Error).message}`;
42
+ }
43
+ return null;
44
+ }
45
+
46
+ loadPreviousAddresses(): string[] {
47
+ try {
48
+ const stored = window.localStorage.getItem(LOCAL_STORAGE_KEY);
49
+ const list = stored && JSON.parse(stored);
50
+ const addresses = Array.isArray(list)
51
+ ? list.filter((v): v is string => typeof v === 'string')
52
+ : [];
53
+ return addresses;
54
+ } catch {
55
+ // ignore
56
+ }
57
+ return [];
58
+ }
59
+
60
+ storeAddress(address: string) {
61
+ const cleaned = address.trim();
62
+ if (!cleaned || cleaned === CURRENT_LOCATION) {
63
+ return;
64
+ }
65
+ const deduped = [cleaned, ...this.addressHistory.filter(a => a !== cleaned)];
66
+ this.addressHistory = deduped.slice(0, this.props.maxEntries);
67
+ try {
68
+ window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.addressHistory));
69
+ } catch {
70
+ // ignore
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,15 @@
1
+ // deck.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ /** A geocoder that translates an address string into a location */
6
+ export type Geocoder = {
7
+ /** The name of the geocoder, e.g. google, mapbox, ... */
8
+ name: string;
9
+ /** Does the geocoder require an api key? */
10
+ requiresApiKey?: boolean;
11
+ /** A string that can be used to indicate the accepted format */
12
+ placeholderLocation?: string;
13
+ /** Calls the geocoder service, translating an address string into a location */
14
+ geocode(address: string, apiKey: string): Promise<{longitude: number; latitude: number} | null>;
15
+ };
@@ -0,0 +1,199 @@
1
+ // deck.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {type Geocoder} from './geocoder';
6
+
7
+ const GOOGLE_URL = 'https://maps.googleapis.com/maps/api/geocode/json';
8
+ const MAPBOX_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places';
9
+ const OPENCAGE_API_URL = 'https://api.opencagedata.com/geocode/v1/json';
10
+
11
+ /**
12
+ * A geocoder that uses the google geocoding service
13
+ * @note Requires an API key from Google
14
+ * @see https://developers.google.com/maps/documentation/geocoding/get-api-key
15
+ */
16
+ export const GoogleGeocoder = {
17
+ name: 'google',
18
+ requiresApiKey: true,
19
+ async geocode(
20
+ address: string,
21
+ apiKey: string
22
+ ): Promise<{longitude: number; latitude: number} | null> {
23
+ const encodedAddress = encodeURIComponent(address);
24
+ const json = await fetchJson(`${GOOGLE_URL}?address=${encodedAddress}&key=${apiKey}`);
25
+
26
+ switch (json.status) {
27
+ case 'OK':
28
+ const loc = json.results.length > 0 && json.results[0].geometry.location;
29
+ return loc ? {longitude: loc.lng, latitude: loc.lat} : null;
30
+ default:
31
+ throw new Error(`Google Geocoder failed: ${json.status}`);
32
+ }
33
+ }
34
+ } as const satisfies Geocoder;
35
+
36
+ /**
37
+ * A geocoder that uses the google geocoding service
38
+ * @note Requires an API key from Mapbox
39
+ * @see https://docs.mapbox.com/api/search/geocoding/
40
+ */
41
+ export const MapboxGeocoder = {
42
+ name: 'google',
43
+ requiresApiKey: true,
44
+ async geocode(
45
+ address: string,
46
+ apiKey: string
47
+ ): Promise<{longitude: number; latitude: number} | null> {
48
+ const encodedAddress = encodeURIComponent(address);
49
+ const json = await fetchJson(`${MAPBOX_URL}/${encodedAddress}.json?access_token=${apiKey}`);
50
+
51
+ if (Array.isArray(json.features) && json.features.length > 0) {
52
+ const center = json.features[0].center;
53
+ if (Array.isArray(center) && center.length >= 2) {
54
+ return {longitude: center[0], latitude: center[1]};
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+ } as const satisfies Geocoder;
60
+
61
+ /**
62
+ * A geocoder that uses the google geocoding service
63
+ * @note Requires an API key from OpenCageData
64
+ * @see https://opencagedata.com/api
65
+ */
66
+ export const OpenCageGeocoder = {
67
+ name: 'opencage',
68
+ requiresApiKey: true,
69
+ async geocode(
70
+ address: string,
71
+ key: string
72
+ ): Promise<{longitude: number; latitude: number} | null> {
73
+ const encodedAddress = encodeURIComponent(address);
74
+ const data = await fetchJson(`${OPENCAGE_API_URL}?q=${encodedAddress}&key=${key}`);
75
+ if (Array.isArray(data.results) && data.results.length > 0) {
76
+ const geometry = data.results[0].geometry;
77
+ return {longitude: geometry.lng, latitude: geometry.lat};
78
+ }
79
+ return null;
80
+ }
81
+ } as const satisfies Geocoder;
82
+
83
+ /**
84
+ * A geocoder adapter that wraps the browser's geolocation API. Always returns the user's current location.
85
+ * @note Not technically a geocoder, but a geolocation service that provides a source of locations.
86
+ * @note The user must allow location access for this to work.
87
+ */
88
+ export const CurrentLocationGeocoder = {
89
+ name: 'current',
90
+ requiresApiKey: false,
91
+ /** Attempt to call browsers geolocation API */
92
+ async geocode(): Promise<{longitude: number; latitude: number} | null> {
93
+ if (!navigator.geolocation) {
94
+ throw new Error('Geolocation not supported');
95
+ }
96
+ return new Promise((resolve, reject) => {
97
+ navigator.geolocation.getCurrentPosition(
98
+ /** @see https://developer.mozilla.org/docs/Web/API/GeolocationPosition */
99
+ (position: GeolocationPosition) => {
100
+ const {longitude, latitude} = position.coords;
101
+ resolve({longitude, latitude});
102
+ },
103
+ /** @see https://developer.mozilla.org/docs/Web/API/GeolocationPositionError */
104
+ (error: GeolocationPositionError) => reject(new Error(error.message))
105
+ );
106
+ });
107
+ }
108
+ } as const satisfies Geocoder;
109
+
110
+ /** Fetch JSON, catching HTTP errors */
111
+ async function fetchJson(url: string): Promise<any> {
112
+ let response: Response;
113
+ try {
114
+ response = await fetch(url);
115
+ } catch (error) {
116
+ // Annoyingly, fetch reports some errors (e.g. CORS) using excpetions, not response.ok
117
+ throw new Error(`CORS error? ${error}. ${url}: `);
118
+ }
119
+ if (!response.ok) {
120
+ throw new Error(`${response.statusText}. ${url}: `);
121
+ }
122
+ const data = await response.json();
123
+ if (!data) {
124
+ throw new Error(`No data returned. ${url}`);
125
+ }
126
+ return data;
127
+ }
128
+
129
+ /**
130
+ * Parse a coordinate string.
131
+ * Supports comma- or semicolon-separated values.
132
+ * Heuristically determines which value is longitude and which is latitude.
133
+ */
134
+ export const CoordinatesGeocoder = {
135
+ name: 'coordinates',
136
+ requiresApiKey: false,
137
+ placeholderLocation: `-122.45, 37.8 or 37°48'N, 122°27'W`,
138
+ async geocode(address: string): Promise<{longitude: number; latitude: number} | null> {
139
+ return parseCoordinates(address) || null;
140
+ }
141
+ } as const satisfies Geocoder;
142
+
143
+ /**
144
+ * Parse an input string for coordinates.
145
+ * Supports comma- or semicolon-separated values.
146
+ * Heuristically determines which value is longitude and which is latitude.
147
+ */
148
+ function parseCoordinates(input) {
149
+ input = input.trim();
150
+ const parts = input.split(/[,;]/).map(p => p.trim());
151
+ if (parts.length < 2) return null;
152
+ const first = parseCoordinatePart(parts[0]);
153
+ const second = parseCoordinatePart(parts[1]);
154
+ if (first === null || second === null) return null;
155
+ // Use a heuristic:
156
+ // If one number exceeds 90 in absolute value, it's likely a longitude.
157
+ if (Math.abs(first) > 90 && Math.abs(second) <= 90) {
158
+ return {longitude: first, latitude: second};
159
+ } else if (Math.abs(second) > 90 && Math.abs(first) <= 90) {
160
+ return {longitude: second, latitude: first};
161
+ }
162
+ // If both are <= 90, assume order: latitude, longitude.
163
+ return {latitude: first, longitude: second};
164
+ }
165
+
166
+ /**
167
+ * Parse a single coordinate part (which may be in decimal or DMS format).
168
+ */
169
+ function parseCoordinatePart(s: string): number | null {
170
+ s = s.trim();
171
+ // If the string contains a degree symbol or similar markers, use DMS parsing.
172
+ if (s.includes('°') || s.includes("'") || s.includes('"')) {
173
+ const value = dmsToDecimal(s);
174
+ return isNaN(value) ? null : value;
175
+ }
176
+ // Otherwise, check for a cardinal letter and remove it.
177
+ let sign = 1;
178
+ if (/[SW]/i.test(s)) sign = -1;
179
+ s = s.replace(/[NSEW]/gi, '');
180
+ const value = parseFloat(s);
181
+ return isNaN(value) ? null : sign * value;
182
+ }
183
+
184
+ /** Convert a DMS string (e.g. "37°48'00\"N") to decimal degrees. */
185
+ function dmsToDecimal(s: string): number {
186
+ // A simple regex to extract degrees, minutes, seconds and direction.
187
+ const regex = /(\d+)[°d]\s*(\d+)?['′m]?\s*(\d+(?:\.\d+)?)?[\"″s]?\s*([NSEW])?/i;
188
+ const match = s.match(regex);
189
+ if (!match) return NaN;
190
+ const degrees = parseFloat(match[1]) || 0;
191
+ const minutes = parseFloat(match[2]) || 0;
192
+ const seconds = parseFloat(match[3]) || 0;
193
+ const direction = match[4] || '';
194
+ let dec = degrees + minutes / 60 + seconds / 3600;
195
+ if (/[SW]/i.test(direction)) {
196
+ dec = -dec;
197
+ }
198
+ return dec;
199
+ }
@@ -0,0 +1,67 @@
1
+ // deck.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {WidgetPlacement, Layer} from '@deck.gl/core';
6
+ import {render} from 'preact';
7
+ import {Widget, WidgetProps} from '@deck.gl/core';
8
+ import {IconButton} from './lib/components/icon-button';
9
+
10
+ /** Properties for the LoadingWidget */
11
+ export type LoadingWidgetProps = WidgetProps & {
12
+ /** Widget positioning within the view. Default 'top-left'. */
13
+ placement?: WidgetPlacement;
14
+ /** Tooltip message when loading */
15
+ label?: string;
16
+ };
17
+
18
+ /**
19
+ * A non-interactive widget that shows a loading spinner if any layers are loading data
20
+ */
21
+ export class LoadingWidget extends Widget<LoadingWidgetProps> {
22
+ static defaultProps: Required<LoadingWidgetProps> = {
23
+ ...Widget.defaultProps,
24
+ id: 'loading',
25
+ placement: 'top-left',
26
+ label: 'Loading layer data'
27
+ };
28
+
29
+ className = 'deck-widget-loading';
30
+ placement: WidgetPlacement = 'top-left';
31
+ loading = true;
32
+
33
+ constructor(props: LoadingWidgetProps = {}) {
34
+ super(props, LoadingWidget.defaultProps);
35
+ this.placement = props.placement ?? this.placement;
36
+ }
37
+
38
+ setProps(props: Partial<LoadingWidgetProps>) {
39
+ this.placement = props.placement ?? this.placement;
40
+ super.setProps(props);
41
+ }
42
+
43
+ onRenderHTML(rootElement: HTMLElement): void {
44
+ render(
45
+ // TODO(ibgreen) - this should not be a button, but styling is so nested that it is easier to reuse this component.
46
+ this.loading && (
47
+ <IconButton
48
+ className="deck-widget-spinner-icon"
49
+ label={this.props.label}
50
+ onClick={this.handleClick.bind(this)}
51
+ />
52
+ ),
53
+ rootElement
54
+ );
55
+ }
56
+
57
+ onRedraw({layers}: {layers: Layer[]}): void {
58
+ const loading = !layers.some(layer => !layer.isLoaded);
59
+ if (loading !== this.loading) {
60
+ this.loading = loading;
61
+ this.updateHTML();
62
+ }
63
+ }
64
+
65
+ // TODO(ibgreen) - this should not be a button, see above.
66
+ handleClick() {}
67
+ }
@@ -2,17 +2,16 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- /* global document */
6
5
  import type {WidgetPlacement} from '@deck.gl/core';
7
6
  import {render} from 'preact';
8
- import {WidgetImpl, WidgetImplProps} from './widget-impl';
9
- import {IconButton} from './components';
7
+ import {Widget, WidgetProps} from '@deck.gl/core';
8
+ import {IconButton} from './lib/components/icon-button';
10
9
 
11
10
  /** @todo - is the the best we can do? */
12
11
  type ViewState = Record<string, unknown>;
13
12
 
14
13
  /** Properties for the ResetViewWidget */
15
- export type ResetViewWidgetProps = WidgetImplProps & {
14
+ export type ResetViewWidgetProps = WidgetProps & {
16
15
  /** Widget positioning within the view. Default 'top-left'. */
17
16
  placement?: WidgetPlacement;
18
17
  /** Tooltip message */
@@ -26,9 +25,9 @@ export type ResetViewWidgetProps = WidgetImplProps & {
26
25
  /**
27
26
  * A button widget that resets the view state of deck to an initial state.
28
27
  */
29
- export class ResetViewWidget extends WidgetImpl<ResetViewWidgetProps> {
28
+ export class ResetViewWidget extends Widget<ResetViewWidgetProps> {
30
29
  static defaultProps: Required<ResetViewWidgetProps> = {
31
- ...WidgetImpl.defaultProps,
30
+ ...Widget.defaultProps,
32
31
  id: 'reset-view',
33
32
  placement: 'top-left',
34
33
  label: 'Reset View',
@@ -40,7 +39,7 @@ export class ResetViewWidget extends WidgetImpl<ResetViewWidgetProps> {
40
39
  placement: WidgetPlacement = 'top-left';
41
40
 
42
41
  constructor(props: ResetViewWidgetProps = {}) {
43
- super({...ResetViewWidget.defaultProps, ...props});
42
+ super(props, ResetViewWidget.defaultProps);
44
43
  this.placement = props.placement ?? this.placement;
45
44
  }
46
45
 
@@ -49,16 +48,14 @@ export class ResetViewWidget extends WidgetImpl<ResetViewWidgetProps> {
49
48
  super.setProps(props);
50
49
  }
51
50
 
52
- onRenderHTML() {
53
- const element = this.element;
54
- if (!element) return;
51
+ onRenderHTML(rootElement: HTMLElement): void {
55
52
  render(
56
53
  <IconButton
57
54
  className="deck-widget-reset-focus"
58
55
  label={this.props.label}
59
56
  onClick={this.handleClick.bind(this)}
60
57
  />,
61
- element
58
+ rootElement
62
59
  );
63
60
  }
64
61