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
            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
    this method search the city and place name from the osm elements
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
    This method parse the responses from the OverpassAPI
275
    @param {Array.<Object>} results the results received from fetch
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
    The constructor
300
    @param {OverpassAPIDataLoaderOptions} options An object with the options to set
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
    This method launch the queries in the OverpassAPI and parse the received data
320
    @param {Array.<String>} queries An array of queries to be executed in the OverpassAPI
321
    @param {Array.<Number>} latLng The latitude and longitude used in the queries
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;// myOsmCityAdminLevel
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
    A map with the osm nodes
384
    @type {Map}
385
    */
386
387
    get nodes ( ) { return this.#nodes; }
388
389
    /**
390
    A map with the osm ways
391
    @type {Map}
392
    */
393
394
    get ways ( ) { return this.#ways; }
395
396
    /**
397
    A map with the osm relations
398
    @type {Map}
399
    */
400
401
    get relations ( ) { return this.#relations; }
402
403
    /**
404
    The osm place ( hamlet or village )
405
    @type {String}
406
    */
407
408
    get place ( ) { return this.#place; }
409
410
    /**
411
    The osm city
412
    @type {String}
413
    */
414
415
    get city ( ) { return this.#city; }
416
417
    /**
418
    The osm country
419
    @type {String}
420
    */
421
422
    get country ( ) { return this.#adminNames [ OSM_COUNTRY_ADMIN_LEVEL ]; }
423
424
    /**
425
    The final status
426
    @type {Boolean}
427
    */
428
429
    get statusOk ( ) { return this.#statusOk; }
430
}
431
432
export default OverpassAPIDataLoader;
433
434
/* --- End of file --------------------------------------------------------------------------------------------------------- */
435