File : core/lib/OverpassAPIDataLoader.js

1
/*
2
Copyright - 2017 2023 - wwwouaiebe - Contact: https://www.ouaie.be/
3
4
This  program is free software;
5
you can redistribute it and/or modify it under the terms of the
6
GNU General Public License as published by the Free Software Foundation;
7
either version 3 of the License, or any later version.
8
9
This program is distributed in the hope that it will be useful,
10
but WITHOUT ANY WARRANTY; without even the implied warranty of
11
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
GNU General Public License for more details.
13
14
You should have received a copy of the GNU General Public License
15
along with this program; if not, write to the Free Software
16
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
*/
18
/*
19
Changes:
20
    - v4.0.0:
21
        - created from v3.6.0
22
Doc reviewed 202208
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
This class is used to search osm data with the OverpassAPI
33
*/
34
/* ------------------------------------------------------------------------------------------------------------------------- */
35
36
class OverpassAPIDataLoader {
37
38
    /**
39
    The options for the OverpassAPIDataLoader
40
    @type {OverpassAPIDataLoaderOptions}
41
    */
42
43
    #options;
44
45
    /**
46
    A map with the osm nodes found by the API, ordered by id
47
    @type {Map.>OsmElement}
48
    */
49
50
    #nodes;
51
52
    /**
53
    A map with the osm ways found by the API, ordered by id
54
    @type {Map.>OsmElement}
55
    */
56
57
    #ways;
58
59
    /**
60
    A map with the osm relations found by the API, ordered by id
61
    @type {Map.>OsmElement}
62
    */
63
64
    #relations;
65
66
    /**
67
    A list with the administrative names found by the API
68
    @type {Array.<String>}
69
    */
70
71
    #adminNames;
72
73
    /**
74
    The Admin level at witch the cities are placed in OSM (Is country dependant...)
75
    @type {Number}
76
    */
77
78
    #osmCityAdminLevel;
79
80
    /**
81
    an Object with hamlet, village, city and town properties.
82
    Each properties are objects with name, distance and maxDistance properties.
83
    @type {Object}
84
    */
85
86
    #places;
87
88
    /**
89
    A reference to the latitude and longitude used in the queries
90
    @type {Array.<Number>}
91
    */
92
93
    #latLng;
94
95
    /**
96
    A hamlet, village, city or town name found in the OSM data
97
    @type {?String}
98
    */
99
100
    #place;
101
102
    /**
103
    The city name found in the OSM data
104
    @type {?String}
105
    */
106
107
    #city;
108
109
    /**
110
    A flag indicating the success or failure of the request
111
    @type {Boolean}
112
    */
113
114
    #statusOk;
115
116
    /**
117
    This method add the geometry to the osm elements
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
    this method parse the osm elements received from the OverpassAPI
169
    @param {Array.<Object>} osmElements The osm elements received in the request.
170
    */
171
172
    #parseData ( osmElements ) {
173
        osmElements.forEach (
174
            /* eslint-disable-next-line complexity */
175
            osmElement => {
176
                switch ( osmElement.type ) {
177
                case 'node' :
178
                    this.#nodes.set ( osmElement.id, osmElement );
179
                    if (
180
                        osmElement?.tags?.place &&
181
                        this.#options.searchPlaces &&
182
                        this.#places [ osmElement.tags.place ] &&
183
                        osmElement?.tags?.name
184
                    ) {
185
                        const nodeDistance = theSphericalTrigonometry.pointsDistance (
186
                            this.#latLng,
187
                            [ osmElement.lat, osmElement.lon ]
188
                        );
189
                        const place = this.#places [ osmElement.tags.place ];
190
                        if ( place.maxDistance > nodeDistance && place.distance > nodeDistance ) {
191
                            place.distance = nodeDistance;
192
                            place.name = osmElement.tags.name;
193
                        }
194
                    }
195
                    break;
196
                case 'way' :
197
                    if ( this.#options.searchWays ) {
198
                        this.#ways.set ( osmElement.id, osmElement );
199
                    }
200
                    break;
201
                case 'relation' :
202
                    if ( this.#options.searchRelations ) {
203
                        this.#relations.set ( osmElement.id, osmElement );
204
                    }
205
                    break;
206
                case 'area' :
207
                    if ( this.#options.searchPlaces ) {
208
                        let elementName = osmElement.tags.name;
209
                        if (
210
                            '*' !== theConfig.nominatim.language &&
211
                            osmElement.tags [ 'name:' + theConfig.nominatim.language ]
212
                        ) {
213
                            elementName = osmElement.tags [ 'name:' + theConfig.nominatim.language ];
214
                        }
215
                        this.#adminNames [ Number.parseInt ( osmElement.tags.admin_level ) ] = elementName;
216
                        if ( OSM_COUNTRY_ADMIN_LEVEL === osmElement.tags.admin_level ) {
217
                            this.#osmCityAdminLevel =
218
                                theConfig.geoCoder.osmCityAdminLevel [ osmElement.tags [ 'ISO3166-1' ] ]
219
                                ||
220
                                this.#osmCityAdminLevel;
221
                        }
222
                    }
223
                    break;
224
                default :
225
                    break;
226
                }
227
            }
228
        );
229
230
        if ( this.#options.setGeometry ) {
231
            this.#setGeometry ( );
232
        }
233
234
        if ( this.#options.searchPlaces ) {
235
            this.#setPlaceAndCity ( );
236
        }
237
238
    }
239
240
    /**
241
    this method search the city and place name from the osm elements
242
    */
243
244
    #setPlaceAndCity ( ) {
245
        let adminHamlet = null;
246
247
        for ( let namesCounter = TWO; namesCounter < this.#adminNames.length; namesCounter ++ ) {
248
            if ( 'undefined' !== typeof ( this.#adminNames [ namesCounter ] ) ) {
249
                if ( this.#osmCityAdminLevel >= namesCounter ) {
250
                    this.#city = this.#adminNames [ namesCounter ];
251
                }
252
                else {
253
                    adminHamlet = this.#adminNames [ namesCounter ];
254
                }
255
            }
256
        }
257
        let placeDistance = Number.MAX_VALUE;
258
259
        Object.values ( this.#places ).forEach (
260
            place => {
261
                if ( place.distance < placeDistance ) {
262
                    placeDistance = place.distance;
263
                    this.#place = place.name;
264
                }
265
            }
266
        );
267
268
        this.#place = adminHamlet || this.#place;
269
        if ( this.#place === this.#city ) {
270
            this.#place = null;
271
        }
272
    }
273
274
    /**
275
    This method parse the responses from the OverpassAPI
276
    @param {Array.<Object>} results the results received from fetch
277
    */
278
279
    async #parseSearchResults ( results ) {
280
        for ( let counter = ZERO; counter < results.length; counter ++ ) {
281
            if (
282
                'fulfilled' === results[ counter ].status
283
                &&
284
                HTTP_STATUS_OK === results[ counter ].value.status
285
                &&
286
                results[ counter ].value.ok
287
            ) {
288
                const response = await results[ counter ].value.json ( );
289
                this.#parseData ( response.elements );
290
            }
291
            else {
292
                this.#statusOk = false;
293
                console.error ( 'An error occurs when calling theOverpassAPI: ' );
294
                console.error ( results[ counter ] );
295
            }
296
        }
297
    }
298
299
    /**
300
    The constructor
301
    @param {OverpassAPIDataLoaderOptions} options An object with the options to set
302
    */
303
304
    constructor ( options ) {
305
        Object.freeze ( this );
306
        this.#options = new OverpassAPIDataLoaderOptions ( );
307
        if ( options ) {
308
            for ( const [ key, value ] of Object.entries ( options ) ) {
309
                if ( this.#options [ key ] ) {
310
                    this.#options [ key ] = value;
311
                }
312
            }
313
        }
314
        this.#nodes = new Map ( );
315
        this.#ways = new Map ( );
316
        this.#relations = new Map ( );
317
    }
318
319
    /**
320
    This method launch the queries in the OverpassAPI and parse the received data
321
    @param {Array.<String>} queries An array of queries to be executed in the OverpassAPI
322
    @param {Array.<Number>} latLng The latitude and longitude used in the queries
323
    */
324
325
    async loadData ( queries, latLng ) {
326
        this.#latLng = latLng;
327
        this.#statusOk = true;
328
        this.#adminNames = [];
329
        this.#osmCityAdminLevel = theConfig.geoCoder.osmCityAdminLevel.DEFAULT;// myOsmCityAdminLevel
330
        this.#places = Object.freeze (
331
            {
332
                hamlet : Object.seal (
333
                    {
334
                        name : null,
335
                        distance : Number.MAX_VALUE,
336
                        maxDistance : theConfig.geoCoder.distances.hamlet
337
                    }
338
                ),
339
                village : Object.seal (
340
                    {
341
                        name : null,
342
                        distance : Number.MAX_VALUE,
343
                        maxDistance : theConfig.geoCoder.distances.village
344
                    }
345
                ),
346
                city : Object.seal (
347
                    {
348
                        name : null,
349
                        distance : Number.MAX_VALUE,
350
                        maxDistance : theConfig.geoCoder.distances.city
351
                    }
352
                ),
353
                town : Object.seal (
354
                    {
355
                        name : null,
356
                        distance : Number.MAX_VALUE,
357
                        maxDistance : theConfig.geoCoder.distances.town
358
                    }
359
                )
360
            }
361
        );
362
363
        this.#place = null;
364
        this.#city = null;
365
366
        this.#nodes.clear ( );
367
        this.#ways.clear ( );
368
        this.#relations.clear ( );
369
370
        const promises = [];
371
        queries.forEach ( query => {
372
            promises.push (
373
                fetch ( theConfig.overpassApi.url +
374
                        '?data=[out:json][timeout:' + theConfig.overpassApi.timeOut + '];' +
375
                        query )
376
            );
377
        }
378
        );
379
380
        await Promise.allSettled ( promises ).then ( results => this.#parseSearchResults ( results ) );
381
    }
382
383
    /**
384
    A map with the osm nodes
385
    @type {Map}
386
    */
387
388
    get nodes ( ) { return this.#nodes; }
389
390
    /**
391
    A map with the osm ways
392
    @type {Map}
393
    */
394
395
    get ways ( ) { return this.#ways; }
396
397
    /**
398
    A map with the osm relations
399
    @type {Map}
400
    */
401
402
    get relations ( ) { return this.#relations; }
403
404
    /**
405
    The osm place ( hamlet or village )
406
    @type {String}
407
    */
408
409
    get place ( ) { return this.#place; }
410
411
    /**
412
    The osm city
413
    @type {String}
414
    */
415
416
    get city ( ) { return this.#city; }
417
418
    /**
419
    The osm country
420
    @type {String}
421
    */
422
423
    get country ( ) { return this.#adminNames [ OSM_COUNTRY_ADMIN_LEVEL ]; }
424
425
    /**
426
    The final status
427
    @type {Boolean}
428
    */
429
430
    get statusOk ( ) { return this.#statusOk; }
431
}
432
433
export default OverpassAPIDataLoader;
434
435
/* --- End of file --------------------------------------------------------------------------------------------------------- */
436