File : core/osmSearch/OsmSearchEngine.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 theEventDispatcher from '../lib/EventDispatcher.js';
26
import theTravelNotesData from '../../data/TravelNotesData.js';
27
import theConfig from '../../data/Config.js';
28
import OverpassAPIDataLoader from '../lib/OverpassAPIDataLoader.js';
29
import theOsmSearchDictionary from './OsmSearchDictionary.js';
30
import theGeometry from '../lib/Geometry.js';
31
import theErrorUI from '../../uis/errorsUI/ErrorsUI.js';
32
33
import { ZERO, ONE, LAT_LNG } from '../../main/Constants.js';
34
import theTranslator from '../uiLib/Translator.js';
35
36
/* ------------------------------------------------------------------------------------------------------------------------- */
37
/**
38
This class search the osm data
39
40
See theOsmSearchEngine for the one and only one instance of this class
41
*/
42
/* ------------------------------------------------------------------------------------------------------------------------- */
43
44
class OsmSearchEngine    {
45
46
    /**
47
    A flag to avoid to start a new search when a search is already ongoing
48
    @type {Boolean}
49
    */
50
51
    #searchStarted;
52
53
    /**
54
    A list of DictionaryItem objects used to filter the results received from Osm
55
    @type {Array.<DictionaryItem>}
56
    */
57
58
    #filterItems;
59
60
    /**
61
    A leaflet LatLngBounds object with the previous search limits
62
    @type {LeafletObject}
63
    */
64
65
    #previousSearchBounds;
66
67
    /**
68
    A constant with the half dimension in meter of the search area
69
    @type {Number}
70
    */
71
72
    // eslint-disable-next-line no-magic-numbers
73
    static get #SEARCH_DIMENSION ( ) { return 5000; }
74
75
    /**
76
    Compare the tags of the osmElement with the tags of the filterTags
77
    @param {OsmElement} osmElement the osmElement to compare
78
    @param {Array.<Object>} filterTags The filter tags to use Seee DictionaryItem.filterTagsArray
79
    @return {Boolean} true when all the tags present in the filterTags are present in the osmElement with the same value
80
    */
81
82
    #filterOsmElement ( osmElement, filterTags ) {
83
        let isValidOsmElement = true;
84
        filterTags.forEach (
85
            filterTag => {
86
                const [ key, value ] = Object.entries ( filterTag ) [ ZERO ];
87
                isValidOsmElement =
88
                    isValidOsmElement &&
89
                    osmElement.tags [ key ] &&
90
                    ( ! value || osmElement.tags [ key ] === value );
91
92
            }
93
        );
94
95
        return isValidOsmElement;
96
    }
97
98
    /**
99
    Filter the osmElement with the list of selected DictionaryItems and add the osmElement to the map of pointsOfInterest
100
    if the osmElement pass the filter. Add also a description, a latitude and longitude to the osmElement
101
    @param {OsmElement} osmElement the object to analyse
102
    @param {Map} pointsOfInterest A map with all the retained osmElements
103
    */
104
105
    #addPointOfInterest ( osmElement, pointsOfInterest ) {
106
        this.#filterItems.forEach (
107
            filterItem => {
108
                filterItem.filterTagsArray.forEach (
109
                    filterTags => {
110
                        if ( this.#filterOsmElement ( osmElement, filterTags ) ) {
111
                            osmElement.description = filterItem.name;
112
                            pointsOfInterest.set ( osmElement.id, osmElement );
113
                        }
114
                    }
115
                );
116
            }
117
        );
118
    }
119
120
    /**
121
    Build an array of queries for calls to OSM.
122
    @return {Array.<String>} An array of string to use with OverpassAPIDataLoader
123
    */
124
125
    #getSearchQueries ( ) {
126
        const searchQueries = [];
127
        const keysMap = new Map ( );
128
129
        this.#filterItems.forEach (
130
            filterItem => {
131
                filterItem.filterTagsArray.forEach (
132
                    filterTags => {
133
134
                        const [ key, value ] = Object.entries ( filterTags [ ZERO ] ) [ ZERO ];
135
                        let valuesElements = keysMap.get ( key );
136
                        if ( ! valuesElements ) {
137
                            valuesElements = { values : new Map ( ), elements : new Map ( ) };
138
                            keysMap.set ( key, valuesElements );
139
                        }
140
                        valuesElements.values.set ( value, value );
141
                        filterItem.elementTypes.forEach (
142
                            elementType => {
143
                                valuesElements.elements.set ( elementType, elementType );
144
                            }
145
                        );
146
                    }
147
                );
148
            }
149
        );
150
151
        const searchBounds = this.#computeSearchBounds ( );
152
        this.#previousSearchBounds = searchBounds;
153
        const searchBoundingBoxString = '(' +
154
            searchBounds.getSouthWest ( ).lat.toFixed ( LAT_LNG.fixed ) +
155
            ',' +
156
            searchBounds.getSouthWest ( ).lng.toFixed ( LAT_LNG.fixed ) +
157
            ',' +
158
            searchBounds.getNorthEast ( ).lat.toFixed ( LAT_LNG.fixed ) +
159
            ',' +
160
            searchBounds.getNorthEast ( ).lng.toFixed ( LAT_LNG.fixed ) +
161
            ')';
162
163
        keysMap.forEach (
164
            ( valuesElements, key ) => {
165
                let queryTag = '"' + key + '"';
166
                if ( ONE === valuesElements.values.size ) {
167
                    const value = valuesElements.values.values ( ).next ( ).value;
168
                    if ( value ) {
169
                        queryTag += '="' + value + '"';
170
                    }
171
                }
172
                else if ( ONE < valuesElements.values.size ) {
173
                    queryTag += '~"';
174
                    valuesElements.values.forEach (
175
                        value => {
176
                            queryTag += value + '|';
177
                        }
178
                    );
179
                    queryTag = queryTag.substring ( ZERO, queryTag.length - ONE ) + '"';
180
                }
181
182
                // This modification due to slow response from https://lz4.overpass-api.de/api/interpreter
183
                // Some overpass API servers don't know nwr...
184
185
                if ( theConfig.overpassApi.useNwr ) {
186
                    const queryElement =
187
                        ONE === valuesElements.elements.size ? valuesElements.elements.values ( ).next ( ).value : 'nwr';
188
189
                    searchQueries.push (
190
                        queryElement + '[' + queryTag + ']' + searchBoundingBoxString + ';' +
191
                        ( 'node' === queryElement ? '' : '(._;>;);' ) + 'out;'
192
                    );
193
                }
194
                else {
195
                    let queryElements = [];
196
                    if ( ONE === valuesElements.elements.size ) {
197
                        queryElements .push ( valuesElements.elements.values ( ).next ( ).value );
198
                    }
199
                    else {
200
                        queryElements = [ 'node', 'way', 'rel' ];
201
                    }
202
                    queryElements.forEach (
203
                        queryElement => {
204
                            searchQueries.push (
205
                                queryElement + '[' + queryTag + ']' + searchBoundingBoxString + ';' +
206
                                ( 'node' === queryElement ? '' : '(._;>;);' ) + 'out;'
207
                            );
208
                        }
209
                    );
210
                }
211
            }
212
        );
213
214
        return searchQueries;
215
    }
216
217
    /**
218
    Search all selected items on the tree dictionary and for each selected item, add it to a list of selected items
219
    and add the first tag to the root tags map.
220
    @param {DictionaryItem} item The item from witch the search start. Recursive function. The first
221
    call start with this.#dictionary
222
    */
223
224
    #searchFilterItems ( item ) {
225
        if ( item.isSelected && ( ZERO < item.filterTagsArray.length ) ) {
226
            this.#filterItems.push ( item );
227
        }
228
        item.items.forEach ( nextItem => this.#searchFilterItems ( nextItem ) );
229
    }
230
231
    /**
232
    Compute the search bounds
233
    */
234
235
    #computeSearchBounds ( ) {
236
        const mapCenter = theTravelNotesData.map.getCenter ( );
237
        const searchBounds = theTravelNotesData.map.getBounds ( );
238
        const maxBounds = theGeometry.getSquareBoundingBox (
239
            [ mapCenter.lat, mapCenter.lng ],
240
            OsmSearchEngine.#SEARCH_DIMENSION
241
        );
242
        searchBounds.getSouthWest ( ).lat =
243
            Math.max ( searchBounds.getSouthWest ( ).lat, maxBounds.getSouthWest ( ).lat );
244
        searchBounds.getSouthWest ( ).lng =
245
            Math.max ( searchBounds.getSouthWest ( ).lng, maxBounds.getSouthWest ( ).lng );
246
        searchBounds.getNorthEast ( ).lat =
247
            Math.min ( searchBounds.getNorthEast ( ).lat, maxBounds.getNorthEast ( ).lat );
248
        searchBounds.getNorthEast ( ).lng =
249
            Math.min ( searchBounds.getNorthEast ( ).lng, maxBounds.getNorthEast ( ).lng );
250
251
        return searchBounds;
252
    }
253
254
    /**
255
    The constructor
256
    */
257
258
    constructor ( ) {
259
        Object.freeze ( this );
260
        this.#searchStarted = false;
261
        this.#filterItems = [];
262
        this.#previousSearchBounds = null;
263
    }
264
265
    /**
266
    Start a search into osm for the items selected in the dictionary
267
    */
268
269
    async search ( ) {
270
        if ( this.#searchStarted ) {
271
            return;
272
        }
273
        this.#searchStarted = true;
274
        this.#filterItems = [];
275
        this.#searchFilterItems ( theOsmSearchDictionary.dictionary );
276
        const dataLoader = new OverpassAPIDataLoader ( { searchPlaces : false } );
277
        await dataLoader.loadData ( this.#getSearchQueries ( ) );
278
        const pointsOfInterest = new Map ( );
279
280
        if ( ! dataLoader.statusOk ) {
281
            theErrorUI.showError ( theTranslator.getText ( 'OsmSearchEngine - incomplete results' ) );
282
        }
283
284
        [ dataLoader.nodes, dataLoader.ways, dataLoader.relations ]. forEach (
285
            elementsMap => {
286
                elementsMap.forEach (
287
                    osmElement => {
288
                        if ( osmElement.tags ) {
289
                            this.#addPointOfInterest ( osmElement, pointsOfInterest );
290
                        }
291
                    }
292
                );
293
            }
294
        );
295
        theTravelNotesData.searchData.length = ZERO;
296
        Array.from ( pointsOfInterest.values ( ) ).sort (
297
            ( obj1, obj2 ) => obj1.description > obj2.description
298
        )
299
            .forEach ( poi => theTravelNotesData.searchData.push ( poi ) );
300
        this.#searchStarted = false;
301
        theEventDispatcher.dispatch ( 'updateosmsearch' );
302
    }
303
304
    /**
305
    A leaflet LatLngBounds object with the current search limits
306
    @type {LeafletObject}
307
    */
308
309
    get searchBounds ( ) { return this.#computeSearchBounds ( ); }
310
311
    /**
312
    A leaflet LatLngBounds object with the previous search limits
313
    @type {LeafletObject}
314
    */
315
316
    get previousSearchBounds ( ) { return this.#previousSearchBounds; }
317
318
}
319
320
/* ------------------------------------------------------------------------------------------------------------------------- */
321
/**
322
The one and only one instance of OsmSearchEngine class
323
@type {OsmSearchEngine}
324
*/
325
/* ------------------------------------------------------------------------------------------------------------------------- */
326
327
const theOsmSearchEngine = new OsmSearchEngine ( );
328
329
export default theOsmSearchEngine;
330
331
/* --- End of file --------------------------------------------------------------------------------------------------------- */
332