@@ -10,6 +10,10 @@ import {render} from 'preact';
10
10
/** @todo - is the the best we can do? */
11
11
type ViewState = Record < string , unknown > ;
12
12
13
+ const GOOGLE_URL = 'https://maps.googleapis.com/maps/api/geocode/json' ;
14
+ const MAPBOX_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places' ;
15
+ const OPENCAGE_API_URL = 'https://api.opencagedata.com/geocode/v1/json' ;
16
+
13
17
/** Properties for the GeolocateWidget */
14
18
export type GeolocateWidgetProps = WidgetProps & {
15
19
viewId ?: string ;
@@ -18,6 +22,15 @@ export type GeolocateWidgetProps = WidgetProps & {
18
22
/** Tooltip message */
19
23
label ?: string ;
20
24
transitionDuration ?: number ;
25
+ /** Geocoding service */
26
+ geocoder ?: 'google' | 'mapbox' | 'opencage' | 'custom' | 'coordinates' ;
27
+ /** API key used for geocoding services */
28
+ apiKey ?: string ;
29
+ /** Callback when using a custom geocoder */
30
+ onGeocode ?: (
31
+ address : string ,
32
+ apiKey : string
33
+ ) => Promise < { longitude : number ; latitude : number } | null > ;
21
34
} ;
22
35
23
36
/**
@@ -32,7 +45,10 @@ export class GeolocateWidget extends Widget<GeolocateWidgetProps> {
32
45
viewId : undefined ! ,
33
46
placement : 'top-left' ,
34
47
label : 'Geolocate' ,
35
- transitionDuration : 200
48
+ transitionDuration : 200 ,
49
+ geocoder : 'coordinates' ,
50
+ apiKey : '' ,
51
+ onGeocode : undefined !
36
52
} ;
37
53
38
54
className = 'deck-widget-geolocate' ;
@@ -50,6 +66,9 @@ export class GeolocateWidget extends Widget<GeolocateWidgetProps> {
50
66
setProps ( props : Partial < GeolocateWidgetProps > ) : void {
51
67
this . placement = props . placement ?? this . placement ;
52
68
super . setProps ( props ) ;
69
+ if ( ! this . props . apiKey && [ 'google' , 'mapbox' , 'opencage' ] . includes ( this . props . geocoder ) ) {
70
+ throw new Error ( 'API key is required' ) ;
71
+ }
53
72
}
54
73
55
74
onRenderHTML ( rootElement : HTMLElement ) : void {
@@ -59,10 +78,8 @@ export class GeolocateWidget extends Widget<GeolocateWidgetProps> {
59
78
type = "text"
60
79
placeholder = "-122.45, 37.8 or 37°48'N, 122°27'W"
61
80
value = { this . geolocateText }
62
- onInput = {
63
- // @ts -expect-error event type
64
- e => this . setInput ( e . target ?. value || '' )
65
- }
81
+ // @ts -expect-error event type
82
+ onInput = { e => this . setInput ( e . target ?. value || '' ) }
66
83
onKeyPress = { this . handleKeyPress }
67
84
/>
68
85
< button onClick = { this . handleSubmit } > Go</ button >
@@ -76,26 +93,123 @@ export class GeolocateWidget extends Widget<GeolocateWidgetProps> {
76
93
this . geolocateText = text ;
77
94
} ;
78
95
79
- handleSubmit = ( ) => {
80
- const coords = parseCoordinates ( this . geolocateText ) ;
81
- if ( coords ) {
82
- this . errorText = '' ;
83
- this . handleCoordinates ( coords ) ;
84
- } else {
85
- this . errorText = 'Invalid coordinate format.' ;
86
- }
87
- } ;
88
-
89
96
handleKeyPress = e => {
90
97
if ( e . key === 'Enter' ) {
91
98
this . handleSubmit ( ) ;
92
99
}
93
100
} ;
94
101
95
- handleCoordinates = coordinates => {
96
- this . setViewState ( coordinates ) ;
102
+ /** Sync wrapper for async geocode() */
103
+ handleSubmit = ( ) => {
104
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
105
+ this . geocode ( ) ;
97
106
} ;
98
107
108
+ geocode : ( ) => Promise < void > = async ( ) => {
109
+ this . errorText = '' ;
110
+ try {
111
+ const coordinates = await this . callGeocoderService ( this . geolocateText ) ;
112
+ if ( coordinates ) {
113
+ this . setViewState ( coordinates ) ;
114
+ } else {
115
+ this . errorText = 'Invalid address' ;
116
+ }
117
+ } catch ( error ) {
118
+ this . errorText = `Error: ${ ( error as Error ) . message } ` ;
119
+ }
120
+ } ;
121
+
122
+ callGeocoderService = async (
123
+ address : string
124
+ ) : Promise < { longitude : number ; latitude : number } | null > => {
125
+ const { geocoder, apiKey, onGeocode} = this . props ;
126
+
127
+ switch ( geocoder ) {
128
+ case 'google' :
129
+ return this . geocodeGoogle ( address , apiKey ) ;
130
+ case 'mapbox' :
131
+ return this . geocodeMapbox ( address , apiKey ) ;
132
+ case 'opencage' :
133
+ return this . geocodeOpenCage ( address , apiKey ) ;
134
+ case 'custom' :
135
+ return await onGeocode ( address , apiKey ) ;
136
+ case 'coordinates' :
137
+ return await this . geocodeCoordinates ( address ) ;
138
+ default :
139
+ throw new Error ( `Unsupported geocoder: ${ geocoder } ` ) ;
140
+ }
141
+ } ;
142
+
143
+ async geocodeGoogle (
144
+ address : string ,
145
+ apiKey : string
146
+ ) : Promise < { longitude : number ; latitude : number } | null > {
147
+ const encodedAddress = encodeURIComponent ( address ) ;
148
+ const json = await this . _fetchJson ( `${ GOOGLE_URL } ?address=${ encodedAddress } &key=${ apiKey } ` ) ;
149
+
150
+ switch ( json . status ) {
151
+ case 'OK' :
152
+ const loc = json . results . length > 0 && json . results [ 0 ] . geometry . location ;
153
+ return loc ? { longitude : loc . lng , latitude : loc . lat } : null ;
154
+ default :
155
+ throw new Error ( `Google Geocoder failed: ${ json . status } ` ) ;
156
+ }
157
+ }
158
+
159
+ async geocodeMapbox (
160
+ address : string ,
161
+ apiKey : string
162
+ ) : Promise < { longitude : number ; latitude : number } | null > {
163
+ const encodedAddress = encodeURIComponent ( address ) ;
164
+ const json = await this . _fetchJson (
165
+ `${ MAPBOX_URL } /${ encodedAddress } .json?access_token=${ apiKey } `
166
+ ) ;
167
+
168
+ if ( Array . isArray ( json . features ) && json . features . length > 0 ) {
169
+ const center = json . features [ 0 ] . center ;
170
+ if ( Array . isArray ( center ) && center . length >= 2 ) {
171
+ return { longitude : center [ 0 ] , latitude : center [ 1 ] } ;
172
+ }
173
+ }
174
+ return null ;
175
+ }
176
+
177
+ async geocodeOpenCage (
178
+ address : string ,
179
+ key : string
180
+ ) : Promise < { longitude : number ; latitude : number } | null > {
181
+ const encodedAddress = encodeURIComponent ( address ) ;
182
+ const data = await this . _fetchJson ( `${ OPENCAGE_API_URL } ?q=${ encodedAddress } &key=${ key } ` ) ;
183
+ if ( Array . isArray ( data . results ) && data . results . length > 0 ) {
184
+ const geometry = data . results [ 0 ] . geometry ;
185
+ return { longitude : geometry . lng , latitude : geometry . lat } ;
186
+ }
187
+ return null ;
188
+ }
189
+
190
+ async geocodeCoordinates ( address : string ) : Promise < { longitude : number ; latitude : number } | null > {
191
+ return parseCoordinates ( address ) || null ;
192
+ }
193
+
194
+ /** Fetch JSON, catching HTTP errors */
195
+ async _fetchJson ( url : string ) : Promise < any > {
196
+ let response : Response ;
197
+ try {
198
+ response = await fetch ( url ) ;
199
+ } catch ( error ) {
200
+ // Annoyingly, fetch reports some errors (e.g. CORS) using excpetions, not response.ok
201
+ throw new Error ( `Failed to fetch ${ url } : ${ error } ` ) ;
202
+ }
203
+ if ( ! response . ok ) {
204
+ throw new Error ( `Failed to fetch ${ url } : ${ response . statusText } ` ) ;
205
+ }
206
+ const data = await response . json ( ) ;
207
+ if ( ! data ) {
208
+ throw new Error ( `No data returned from ${ url } ` ) ;
209
+ }
210
+ return data ;
211
+ }
212
+
99
213
// TODO - MOVE TO WIDGETIMPL?
100
214
101
215
setViewState ( viewState : ViewState ) {
0 commit comments