1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
16 | |
17 | |
18 | |
19 | |
20 | |
21 | |
22 | |
23 | |
24 | |
25 | import theConfig from '../../data/Config.js'; |
26 | import theSphericalTrigonometry from './SphericalTrigonometry.js'; |
27 | import OverpassAPIDataLoaderOptions from './OverpassAPIDataLoaderOptions.js'; |
28 | import { ZERO, TWO, LAT_LNG, HTTP_STATUS_OK, OSM_COUNTRY_ADMIN_LEVEL } from '../../main/Constants.js'; |
29 | |
30 | |
31 | |
32 | |
33 | |
34 | |
35 | |
36 | class OverpassAPIDataLoader { |
37 | |
38 | |
39 | |
40 | |
41 | |
42 | |
43 | #options; |
44 | |
45 | |
46 | |
47 | |
48 | |
49 | |
50 | #nodes; |
51 | |
52 | |
53 | |
54 | |
55 | |
56 | |
57 | #ways; |
58 | |
59 | |
60 | |
61 | |
62 | |
63 | |
64 | #relations; |
65 | |
66 | |
67 | |
68 | |
69 | |
70 | |
71 | #adminNames; |
72 | |
73 | |
74 | |
75 | |
76 | |
77 | |
78 | #osmCityAdminLevel; |
79 | |
80 | |
81 | |
82 | |
83 | |
84 | |
85 | |
86 | #places; |
87 | |
88 | |
89 | |
90 | |
91 | |
92 | |
93 | #latLng; |
94 | |
95 | |
96 | |
97 | |
98 | |
99 | |
100 | #place; |
101 | |
102 | |
103 | |
104 | |
105 | |
106 | |
107 | #city; |
108 | |
109 | |
110 | |
111 | |
112 | |
113 | |
114 | #statusOk; |
115 | |
116 | |
117 | |
118 | |
119 | |
120 | #setGeometry ( ) { |
121 | this.#ways.forEach ( |
122 | way => { |
123 | way.geometry = [ [ ] ]; |
124 | way.lat = LAT_LNG.defaultValue; |
125 | way.lon = LAT_LNG.defaultValue; |
126 | let nodesCounter = ZERO; |
127 | way.nodes.forEach ( |
128 | nodeId => { |
129 | const node = this.#nodes.get ( nodeId ); |
130 | way.geometry [ ZERO ].push ( [ node.lat, node.lon ] ); |
131 | way.lat += node.lat; |
132 | way.lon += node.lon; |
133 | nodesCounter ++; |
134 | } |
135 | ); |
136 | if ( ZERO !== nodesCounter ) { |
137 | way.lat /= nodesCounter; |
138 | way.lon /= nodesCounter; |
139 | } |
140 | } |
141 | ); |
142 | this.#relations.forEach ( |
143 | relation => { |
144 | relation.geometry = [ [ ] ]; |
145 | relation.lat = LAT_LNG.defaultValue; |
146 | relation.lon = LAT_LNG.defaultValue; |
147 | let membersCounter = ZERO; |
148 | relation.members.forEach ( |
149 | member => { |
150 | if ( 'way' === member.type ) { |
151 | const way = this.#ways.get ( member.ref ); |
152 | relation.geometry.push ( way.geometry [ ZERO ] ); |
153 | relation.lat += way.lat; |
154 | relation.lon += way.lon; |
155 | membersCounter ++; |
156 | } |
157 | } |
158 | ); |
159 | if ( ZERO !== membersCounter ) { |
160 | relation.lat /= membersCounter; |
161 | relation.lon /= membersCounter; |
162 | } |
163 | } |
164 | ); |
165 | } |
166 | |
167 | |
168 | |
169 | |
170 | |
171 | |
172 | #parseData ( osmElements ) { |
173 | osmElements.forEach ( |
174 | osmElement => { |
175 | switch ( osmElement.type ) { |
176 | case 'node' : |
177 | this.#nodes.set ( osmElement.id, osmElement ); |
178 | if ( |
179 | osmElement?.tags?.place && |
180 | this.#options.searchPlaces && |
181 | this.#places [ osmElement.tags.place ] && |
182 | osmElement?.tags?.name |
183 | ) { |
184 | const nodeDistance = theSphericalTrigonometry.pointsDistance ( |
185 | this.#latLng, |
186 | [ osmElement.lat, osmElement.lon ] |
187 | ); |
188 | const place = this.#places [ osmElement.tags.place ]; |
189 | if ( place.maxDistance > nodeDistance && place.distance > nodeDistance ) { |
190 | place.distance = nodeDistance; |
191 | place.name = osmElement.tags.name; |
192 | } |
193 | } |
194 | break; |
195 | case 'way' : |
196 | if ( this.#options.searchWays ) { |
197 | this.#ways.set ( osmElement.id, osmElement ); |
198 | } |
199 | break; |
200 | case 'relation' : |
201 | if ( this.#options.searchRelations ) { |
202 | this.#relations.set ( osmElement.id, osmElement ); |
203 | } |
204 | break; |
205 | case 'area' : |
206 | if ( this.#options.searchPlaces ) { |
207 | let elementName = osmElement.tags.name; |
208 | if ( |
209 | '*' !== theConfig.nominatim.language && |
210 | osmElement.tags [ 'name:' + theConfig.nominatim.language ] |
211 | ) { |
212 | elementName = osmElement.tags [ 'name:' + theConfig.nominatim.language ]; |
213 | } |
214 | this.#adminNames [ Number.parseInt ( osmElement.tags.admin_level ) ] = elementName; |
215 | if ( OSM_COUNTRY_ADMIN_LEVEL === osmElement.tags.admin_level ) { |
216 | this.#osmCityAdminLevel = |
217 | theConfig.geoCoder.osmCityAdminLevel [ osmElement.tags [ 'ISO3166-1' ] ] |
218 | || |
219 | this.#osmCityAdminLevel; |
220 | } |
221 | } |
222 | break; |
223 | default : |
224 | break; |
225 | } |
226 | } |
227 | ); |
228 | |
229 | if ( this.#options.setGeometry ) { |
230 | this.#setGeometry ( ); |
231 | } |
232 | |
233 | if ( this.#options.searchPlaces ) { |
234 | this.#setPlaceAndCity ( ); |
235 | } |
236 | |
237 | } |
238 | |
239 | |
240 | |
241 | |
242 | |
243 | #setPlaceAndCity ( ) { |
244 | let adminHamlet = null; |
245 | |
246 | for ( let namesCounter = TWO; namesCounter < this.#adminNames.length; namesCounter ++ ) { |
247 | if ( 'undefined' !== typeof ( this.#adminNames [ namesCounter ] ) ) { |
248 | if ( this.#osmCityAdminLevel >= namesCounter ) { |
249 | this.#city = this.#adminNames [ namesCounter ]; |
250 | } |
251 | else { |
252 | adminHamlet = this.#adminNames [ namesCounter ]; |
253 | } |
254 | } |
255 | } |
256 | let placeDistance = Number.MAX_VALUE; |
257 | |
258 | Object.values ( this.#places ).forEach ( |
259 | place => { |
260 | if ( place.distance < placeDistance ) { |
261 | placeDistance = place.distance; |
262 | this.#place = place.name; |
263 | } |
264 | } |
265 | ); |
266 | |
267 | this.#place = adminHamlet || this.#place; |
268 | if ( this.#place === this.#city ) { |
269 | this.#place = null; |
270 | } |
271 | } |
272 | |
273 | |
274 | |
275 | |
276 | |
277 | |
278 | async #parseSearchResults ( results ) { |
279 | for ( let counter = ZERO; counter < results.length; counter ++ ) { |
280 | if ( |
281 | 'fulfilled' === results[ counter ].status |
282 | && |
283 | HTTP_STATUS_OK === results[ counter ].value.status |
284 | && |
285 | results[ counter ].value.ok |
286 | ) { |
287 | const response = await results[ counter ].value.json ( ); |
288 | this.#parseData ( response.elements ); |
289 | } |
290 | else { |
291 | this.#statusOk = false; |
292 | console.error ( 'An error occurs when calling theOverpassAPI: ' ); |
293 | console.error ( results[ counter ] ); |
294 | } |
295 | } |
296 | } |
297 | |
298 | |
299 | |
300 | |
301 | |
302 | |
303 | constructor ( options ) { |
304 | Object.freeze ( this ); |
305 | this.#options = new OverpassAPIDataLoaderOptions ( ); |
306 | if ( options ) { |
307 | for ( const [ key, value ] of Object.entries ( options ) ) { |
308 | if ( this.#options [ key ] ) { |
309 | this.#options [ key ] = value; |
310 | } |
311 | } |
312 | } |
313 | this.#nodes = new Map ( ); |
314 | this.#ways = new Map ( ); |
315 | this.#relations = new Map ( ); |
316 | } |
317 | |
318 | |
319 | |
320 | |
321 | |
322 | |
323 | |
324 | async loadData ( queries, latLng ) { |
325 | this.#latLng = latLng; |
326 | this.#statusOk = true; |
327 | this.#adminNames = []; |
328 | this.#osmCityAdminLevel = theConfig.geoCoder.osmCityAdminLevel.DEFAULT; |
329 | this.#places = Object.freeze ( |
330 | { |
331 | hamlet : Object.seal ( |
332 | { |
333 | name : null, |
334 | distance : Number.MAX_VALUE, |
335 | maxDistance : theConfig.geoCoder.distances.hamlet |
336 | } |
337 | ), |
338 | village : Object.seal ( |
339 | { |
340 | name : null, |
341 | distance : Number.MAX_VALUE, |
342 | maxDistance : theConfig.geoCoder.distances.village |
343 | } |
344 | ), |
345 | city : Object.seal ( |
346 | { |
347 | name : null, |
348 | distance : Number.MAX_VALUE, |
349 | maxDistance : theConfig.geoCoder.distances.city |
350 | } |
351 | ), |
352 | town : Object.seal ( |
353 | { |
354 | name : null, |
355 | distance : Number.MAX_VALUE, |
356 | maxDistance : theConfig.geoCoder.distances.town |
357 | } |
358 | ) |
359 | } |
360 | ); |
361 | |
362 | this.#place = null; |
363 | this.#city = null; |
364 | |
365 | this.#nodes.clear ( ); |
366 | this.#ways.clear ( ); |
367 | this.#relations.clear ( ); |
368 | |
369 | const promises = []; |
370 | queries.forEach ( query => { |
371 | promises.push ( |
372 | fetch ( theConfig.overpassApi.url + |
373 | '?data=[out:json][timeout:' + theConfig.overpassApi.timeOut + '];' + |
374 | query ) |
375 | ); |
376 | } |
377 | ); |
378 | |
379 | await Promise.allSettled ( promises ).then ( results => this.#parseSearchResults ( results ) ); |
380 | } |
381 | |
382 | |
383 | |
384 | |
385 | |
386 | |
387 | get nodes ( ) { return this.#nodes; } |
388 | |
389 | |
390 | |
391 | |
392 | |
393 | |
394 | get ways ( ) { return this.#ways; } |
395 | |
396 | |
397 | |
398 | |
399 | |
400 | |
401 | get relations ( ) { return this.#relations; } |
402 | |
403 | |
404 | |
405 | |
406 | |
407 | |
408 | get place ( ) { return this.#place; } |
409 | |
410 | |
411 | |
412 | |
413 | |
414 | |
415 | get city ( ) { return this.#city; } |
416 | |
417 | |
418 | |
419 | |
420 | |
421 | |
422 | get country ( ) { return this.#adminNames [ OSM_COUNTRY_ADMIN_LEVEL ]; } |
423 | |
424 | |
425 | |
426 | |
427 | |
428 | |
429 | get statusOk ( ) { return this.#statusOk; } |
430 | } |
431 | |
432 | export default OverpassAPIDataLoader; |
433 | |
434 | |
435 | |