File : core/mapIcon/StreetFinder.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 '../../core/lib/SphericalTrigonometry.js';
27
import theTranslator from '../../core/uiLib/Translator.js';
28
import RoundaboutData from './RoundaboutData.js';
29
import { DISTANCE, ZERO, ONE, TWO, NOT_FOUND, ICON_POSITION } from '../../main/Constants.js';
30
31
/**
32
A simple container to store data on roundabouts
33
*/
34
35
/* ------------------------------------------------------------------------------------------------------------------------- */
36
/**
37
Search:
38
- the rcn ref number at the icon position
39
- roundabout info at the icon position
40
- street names at the icon position
41
*/
42
/* ------------------------------------------------------------------------------------------------------------------------- */
43
44
class StreetFinder {
45
46
    /**
47
    A reference to the computeData object of the MapIconFromOsmFactory
48
    @type {ComputeDataForMapIcon}
49
    */
50
51
    #computeData;
52
53
    /**
54
    A reference to the noteData Object of the MapIconFromOsmFactory
55
    @type {NoteDataForMapIcon}
56
    */
57
58
    #noteData;
59
60
    /**
61
    A reference to the noteData overpassAPIDataLoader of the MapIconFromOsmFactory
62
    @type {OverpassAPIDataLoader}
63
    */
64
65
    #overpassAPIDataLoader;
66
67
    /**
68
    The osm node of the rcnRef
69
    @type {Number}
70
    */
71
72
    #rcnRefOsmNode;
73
74
    /**
75
    The osm node of the rcnRef
76
    @type {Number}
77
    */
78
79
    #iconOsmNode;
80
81
    /**
82
    The osm node id of the incoming node
83
    @type {Number}
84
    */
85
86
    #iconOsmNodeId;
87
88
    /**
89
    The osm node id of the incoming node
90
    @type {Number}
91
    */
92
93
    #incomingOsmNodeId;
94
95
    /**
96
    The osm node id of the outgoing node
97
    @type {Number}
98
    */
99
100
    #outgoingOsmNodeId;
101
102
    /**
103
    The incoming street name
104
    @type {String}
105
    */
106
107
    #incomingStreetName;
108
109
    /**
110
    The outgoing street name
111
    @type {String}
112
    */
113
114
    #outgoingStreetName;
115
116
    /**
117
    Roundabout data
118
    @type {RoundaboutData}
119
    */
120
121
    #roundaboutData;
122
123
    /**
124
    Return the name of a way
125
    @param {OsmElement} way  A way found in the request result
126
    @return {String} the concatenation of the way.ref and way.name if any
127
    */
128
129
    #getWayName ( way ) {
130
        return ( way.tags.name ? way.tags.name : '' ) +
131
            ( way.tags.name && way.tags.ref ? ' ' : '' ) +
132
            ( way.tags.ref ? '[' + way.tags.ref + ']' : '' );
133
    }
134
135
    /**
136
    This method compare the lat and lng of the parameter with the lat and lng of the route waypoints
137
    @param {ItineraryPoint} itineraryPoint the itineraryPoint to test
138
    @return {Boolean} true when the itineraryPoint is not at the same position than a WayPoint and not at the
139
    same position than the icon point
140
    */
141
142
    #latLngCompare ( itineraryPoint ) {
143
144
        const COMPARE_PRECISION = 0.000005;
145
146
        let isWayPoint = false;
147
        this.#computeData.route.wayPoints.forEach (
148
            wayPoint => {
149
                if (
150
                    ( Math.abs ( itineraryPoint.lat - wayPoint.lat ) < COMPARE_PRECISION )
151
                    &&
152
                    ( Math.abs ( itineraryPoint.lng - wayPoint.lng ) < COMPARE_PRECISION )
153
                ) {
154
                    isWayPoint = true;
155
                }
156
            }
157
        );
158
        return (
159
            ! isWayPoint
160
            &&
161
            (
162
                this.#noteData.latLng [ ZERO ] !== itineraryPoint.lat
163
                ||
164
                this.#noteData.latLng [ ONE ] !== itineraryPoint.lng
165
            )
166
        );
167
    }
168
169
    /**
170
    Searching incoming node and outgoing node ( nodes before and after the icon node on the route )
171
    and the rcnRef node ( bike only )
172
    */
173
174
    #findOsmNodes ( ) {
175
176
        // searching the previous and next point on the itinerary
177
        const incomingItineraryPoint = this.#computeData.route.itinerary.itineraryPoints.previous (
178
            this.#computeData.nearestItineraryPointObjId,
179
            itineraryPoint => this.#latLngCompare ( itineraryPoint )
180
        );
181
        const outgoingItineraryPoint = this.#computeData.route.itinerary.itineraryPoints.next (
182
            this.#computeData.nearestItineraryPointObjId,
183
            itineraryPoint => this.#latLngCompare ( itineraryPoint )
184
        );
185
186
        // creating some var for finding the nearest points. We don't use lat lng to avoid problems with precision
187
        let iconNodeDistance = Number.MAX_VALUE;
188
        let rcnRefDistance = Number.MAX_VALUE;
189
        let incomingNodeDistance = Number.MAX_VALUE;
190
        let outgoingNodeDistance = Number.MAX_VALUE;
191
192
        let nodeDistance = DISTANCE.defaultValue;
193
194
        // searching in the nodes map of the overpassAPIDataLoader ...
195
        this.#overpassAPIDataLoader.nodes.forEach (
196
            node => {
197
                nodeDistance = theSphericalTrigonometry.pointsDistance (
198
                    [ node.lat, node.lon ],
199
                    this.#noteData.latLng
200
                );
201
202
                // ... a rcnRef...
203
                if (
204
                    'bike' === this.#computeData.route.itinerary.transitMode
205
                    &&
206
                    node?.tags?.rcn_ref
207
                    &&
208
                    node.tags [ 'network:type' ]
209
                    &&
210
                    'node_network' === node.tags [ 'network:type' ]
211
                    &&
212
                    nodeDistance < theConfig.note.svgIcon.rcnRefDistance
213
                    &&
214
                    nodeDistance < rcnRefDistance
215
                ) {
216
                    this.#rcnRefOsmNode = node;
217
                    rcnRefDistance = nodeDistance;
218
                }
219
220
                // ... the note node
221
                if ( nodeDistance < iconNodeDistance ) {
222
                    this.#iconOsmNodeId = node.id;
223
                    iconNodeDistance = nodeDistance;
224
                }
225
226
                // ... the incoming node...
227
                if ( incomingItineraryPoint ) {
228
                    nodeDistance =
229
                        theSphericalTrigonometry.pointsDistance ( [ node.lat, node.lon ], incomingItineraryPoint.latLng );
230
                    if ( nodeDistance < incomingNodeDistance ) {
231
                        this.#incomingOsmNodeId = node.id;
232
                        incomingNodeDistance = nodeDistance;
233
                    }
234
                }
235
236
                // ... and the outgoing node
237
                if ( outgoingItineraryPoint ) {
238
                    nodeDistance =
239
                        theSphericalTrigonometry.pointsDistance ( [ node.lat, node.lon ], outgoingItineraryPoint.latLng );
240
                    if ( nodeDistance < outgoingNodeDistance ) {
241
                        this.#outgoingOsmNodeId = node.id;
242
                        outgoingNodeDistance = nodeDistance;
243
                    }
244
                }
245
            }
246
        );
247
248
        this.#iconOsmNode = this.#overpassAPIDataLoader.nodes.get ( this.#iconOsmNodeId );
249
    }
250
251
    /**
252
    Searching a mini roundabout at the icon node
253
    */
254
255
    #findMiniRoundabout ( ) {
256
        this.#roundaboutData.isMini = 'mini_roundabout' === this?.#iconOsmNode?.tags?.highway;
257
    }
258
259
    /**
260
    Adding the rcnRef number to the tooltip and the computeData
261
    */
262
263
    #addRcnRefNumber ( ) {
264
        if ( this.#rcnRefOsmNode ) {
265
            this.#computeData.rcnRef = this.#rcnRefOsmNode.tags.rcn_ref;
266
            this.#noteData.tooltipContent +=
267
                theTranslator.getText ( 'StreetFinder - rcnRef', { rcnRef : this.#computeData.rcnRef } );
268
        }
269
    }
270
271
    /**
272
    Searching  passing streets names, incoming and outgoing streets names, roundabout entry and exit
273
    */
274
275
    #findStreets ( ) {
276
        this.#overpassAPIDataLoader.ways.forEach (
277
            way => {
278
                if ( ! way.nodes.includes ( this.#iconOsmNodeId ) ) {
279
                    return;
280
                }
281
282
                const wayName = this.#getWayName ( way );
283
                const haveName = '' !== wayName;
284
285
                const isIncomingStreet = way.nodes.includes ( this.#incomingOsmNodeId );
286
                const isOutgoingStreet = way.nodes.includes ( this.#outgoingOsmNodeId );
287
288
                // the same way can enter multiple times in the intersection!
289
                let streetOcurrences = way.nodes.filter ( nodeId => nodeId === this.#iconOsmNodeId ).length * TWO;
290
291
                // the icon is at the begining of the street
292
                if ( way.nodes [ ZERO ] === this.#iconOsmNodeId ) {
293
                    streetOcurrences --;
294
                }
295
296
                // the icon is at end of the street
297
                if ( way.nodes [ way.nodes.length - ONE ] === this.#iconOsmNodeId ) {
298
                    streetOcurrences --;
299
                }
300
301
                // it's the incoming street ...saving name  and eventually the roundabout exit
302
                if ( isIncomingStreet ) {
303
                    this.#incomingStreetName = haveName ? wayName : '???';
304
                    streetOcurrences --;
305
                    if ( 'roundabout' === way?.tags?.junction ) {
306
                        this.#roundaboutData.isExit = true;
307
                    }
308
                }
309
                if ( ZERO === streetOcurrences ) {
310
                    return;
311
                }
312
313
                // it's the outgoing street ...saving name  and eventually the roundabout exit
314
                if ( isOutgoingStreet ) {
315
                    this.#outgoingStreetName = haveName ? wayName : '???';
316
                    streetOcurrences --;
317
                    if ( 'roundabout' === way?.tags?.junction ) {
318
                        this.#roundaboutData.isEntry = true;
319
                    }
320
                }
321
                if ( ZERO === streetOcurrences || ! haveName ) {
322
                    return;
323
                }
324
325
                // It's a passing street ... saving name...
326
                while ( ZERO !== streetOcurrences ) {
327
                    this.#noteData.address =
328
                        '' === this.#noteData.address ? wayName : this.#noteData.address + ' ⪥  ' + wayName; // ⪥  = ><
329
                    streetOcurrences --;
330
                }
331
            }
332
        );
333
    }
334
335
    /**
336
    Adding city and hamlet to the address
337
    */
338
339
    #addCity ( ) {
340
        if ( '' !== this.#overpassAPIDataLoader.city ) {
341
            this.#noteData.address +=
342
                ' <span class="TravelNotes-NoteHtml-Address-City">' + this.#overpassAPIDataLoader.city + '</span>';
343
        }
344
        if ( this.#overpassAPIDataLoader.place && this.#overpassAPIDataLoader.place !== this.#overpassAPIDataLoader.city ) {
345
            this.#noteData.address += ' (' + this.#overpassAPIDataLoader.place + ')';
346
        }
347
    }
348
349
    /**
350
    Adding street name
351
    */
352
353
    #addAddress ( ) {
354
355
        if ( ICON_POSITION.atStart === this.#computeData.positionOnRoute ) {
356
357
            // It's the start point adding a green circle to the outgoing street
358
            this.#noteData.address = '🟢 ' + this.#outgoingStreetName;
359
            this.#addCity ( );
360
        }
361
        else if ( ICON_POSITION.atEnd === this.#computeData.positionOnRoute ) {
362
363
            this.#addCity ( );
364
365
            // It's the end point adding a red circle to the incoming street
366
            this.#noteData.address = this.#incomingStreetName;
367
            this.#addCity ( );
368
            this.#noteData.address += ' 🔴 ';
369
        }
370
        else {
371
372
            // Adiing the incoming and outgoing streets and direction arrow
373
            this.#noteData.address =
374
                this.#incomingStreetName +
375
                ( '' === this.#noteData.address ? '' : ' ⪥  ' + this.#noteData.address ) + // ⪥ = ><
376
                ' ' + this.#computeData.directionArrow + ' ' +
377
                this.#outgoingStreetName;
378
            this.#addCity ( );
379
        }
380
    }
381
382
    /**
383
    Adding roundabout info
384
    */
385
386
    #addRoundaboutInfo ( ) {
387
        if ( this.#roundaboutData.isEntry && ! this.#roundaboutData.isExit ) {
388
            this.#noteData.tooltipContent += theTranslator.getText ( 'StreetFinder - entry roundabout' );
389
        }
390
        else if ( ! this.#roundaboutData.isEntry && this.#roundaboutData.isExit ) {
391
            this.#noteData.tooltipContent += theTranslator.getText ( 'StreetFinder - exit roundabout' );
392
        }
393
        else if ( this.#roundaboutData.isEntry && this.#roundaboutData.isExit ) {
394
            this.#noteData.tooltipContent +=
395
                theTranslator.getText ( 'StreetFinder - continue roundabout' ); // strange but correct
396
        }
397
        if ( this.#roundaboutData.isMini ) {
398
            this.#noteData.tooltipContent +=
399
                theTranslator.getText ( 'StreetFinder - at the small roundabout on the ground' );
400
        }
401
    }
402
403
    /**
404
    The constructor
405
    */
406
407
    constructor ( ) {
408
        Object.freeze ( this );
409
    }
410
411
    /**
412
    Find street info: street names, city, roundabout info, rcnRef info ...
413
    @param {ComputeDataForMapIcon} computeData The object with the data needed for the computations
414
    @param {NoteDataForMapIcon} noteData The object with the nota data
415
    @param {OverpassAPIDataLoader} overpassAPIDataLoader The OverpassAPIDataLoader object containing the data found in OSM
416
    */
417
418
    findData ( computeData, noteData, overpassAPIDataLoader ) {
419
420
        this.#computeData = computeData;
421
        this.#noteData = noteData;
422
        this.#overpassAPIDataLoader = overpassAPIDataLoader;
423
424
        this.#iconOsmNodeId = NOT_FOUND;
425
        this.#incomingOsmNodeId = NOT_FOUND;
426
        this.#outgoingOsmNodeId = NOT_FOUND;
427
        this.#incomingStreetName = '';
428
        this.#outgoingStreetName = '';
429
        this.#roundaboutData = new RoundaboutData ( );
430
431
        this.#findOsmNodes ( );
432
        this.#findMiniRoundabout ( );
433
        this.#addRcnRefNumber ( );
434
        this.#findStreets ( );
435
        this.#addAddress ( );
436
        this.#addRoundaboutInfo ( );
437
    }
438
}
439
440
export default StreetFinder;
441
442
/* --- End of file --------------------------------------------------------------------------------------------------------- */
443