File : routeProviders/PublicTransportRouteBuilder.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 theSphericalTrigonometry from '../core/lib/SphericalTrigonometry.js';
26
import ItineraryPoint from '../data/ItineraryPoint.js';
27
import Maneuver from '../data/Maneuver.js';
28
import publicTransportData from '../routeProviders/PublicTransportData.js';
29
import PublicTransportHolesRemover from '../routeProviders/PublicTransportHolesRemover.js';
30
31
import { ZERO, ONE, TWO, THREE } from '../main/Constants.js';
32
33
/*
34
35
Building a line that follow a public transport relation version 2 is in theory very simple.
36
It's only line segments that we have to join in a single polyline. That's the theory.
37
38
In practice OpenStreetMap data are plenty of mistakes:
39
40
- line segments are not ordered, so we have to reorder and join
41
42
- some line segments are missing. In this case we try to join the segments
43
  with a new one, but we don't follow anymore the track.
44
45
- we can find nodes with 3 segments arriving or starting from the node.
46
  Yes, that can be. A train comes in a station then restart for a short distance
47
  on the same track that he was coming . In this case, the shortest path is arbitrary duplicated
48
  and joined to the two others segments so we have only one polyline with
49
  some duplicate nodes
50
51
  What's we have in th OSM data (the train go from A to C then from C to B, using the same track):
52
  A----------------------------------+----------------------------------B
53
                                     |
54
                                     |
55
                                     C
56
57
  And what we need to draw only one polyline:
58
  A---------------------------------+ +---------------------------------B
59
                                    | |
60
                                    | |
61
                                    +-+
62
                                     C
63
64
- we can find nodes with more than 3 segments arriving or starting from the node.
65
  This case is the hell and currently I don't have a solution.
66
67
- stop position are not always on the track.
68
69
- stop position are missing
70
71
- stop position are present, but the station is missing
72
73
- ....
74
75
And also we have to look at this:
76
77
- we can have more than one relation between the stations given by the user
78
79
- and finally we have to cut the polyline, because the departure or the final destination of
80
  the train is in another station than the station given by the user
81
82
*/
83
84
/* ------------------------------------------------------------------------------------------------------------------------- */
85
/**
86
coming soon...
87
@ignore
88
*/
89
/* ------------------------------------------------------------------------------------------------------------------------- */
90
91
class PublicTransportRouteBuilder {
92
93
    #selectedRelationId;
94
    #nodes3Ways;
95
    #route;
96
    #publicTransportData;
97
98
    /**
99
    */
100
101
    #merge3WaysNodes ( ) {
102
103
        this.#nodes3Ways.forEach (
104
            node => {
105
106
                // searching the shortest way starting or ending in the node
107
                let shortestWaydistance = Number.MAX_VALUE;
108
                let shortestWay = null;
109
                const linkedWaysId = node.startingWaysIds.concat ( node.endingWaysIds );
110
                linkedWaysId.forEach (
111
                    wayId => {
112
                        const way = this.#publicTransportData.waysMap.get ( wayId );
113
                        if ( way.distance < shortestWaydistance ) {
114
                            shortestWaydistance = way.distance;
115
                            shortestWay = way;
116
                        }
117
                    }
118
                );
119
120
                // the shortest way is removed of the list of linked ways
121
                this.#publicTransportData.removeFrom ( linkedWaysId, shortestWay.id );
122
123
                // cloning the shortest way
124
                const clonedWay = this.#publicTransportData.waysMap.get (
125
                    this.#publicTransportData.cloneWay ( shortestWay.id )
126
                );
127
128
                // and adapting the nodes in the cloned way...
129
                let tmpNodeId = null;
130
                if ( this.#publicTransportData.firstOf ( shortestWay.nodesIds ) === node.id ) {
131
                    clonedWay.nodesIds.pop ( );
132
                    clonedWay.nodesIds.push ( this.#publicTransportData.lastOf ( shortestWay.nodesIds ) );
133
                    tmpNodeId = this.#publicTransportData.firstOf ( clonedWay.nodesIds );
134
                }
135
                else {
136
                    clonedWay.nodesIds.shift ( );
137
                    clonedWay.nodesIds.unshift ( this.#publicTransportData.firstOf ( shortestWay.nodesIds ) );
138
                    tmpNodeId = this.#publicTransportData.lastOf ( clonedWay.nodesIds );
139
                }
140
141
                // and in the last linked way
142
                const lastWay = this.#publicTransportData.waysMap.get ( linkedWaysId [ ONE ] );
143
                lastWay.nodesIds [ lastWay.nodesIds.indexOf ( node.id ) ] = tmpNodeId;
144
145
                // merging the 4 ways
146
                this.#publicTransportData.mergeWays (
147
                    this.#publicTransportData.mergeWays (
148
                        this.#publicTransportData.mergeWays (
149
                            shortestWay.id,
150
                            clonedWay.id
151
                        ),
152
                        linkedWaysId [ ZERO ]
153
                    ),
154
                    lastWay.id );
155
            }
156
        );
157
    }
158
159
    /**
160
    */
161
162
    #createRoute ( route ) {
163
164
        // Searching the nearest stops from the start and end WayPoints given by user.
165
        let startStop = null;
166
        let endStop = null;
167
        let startStopDistance = Number.MAX_VALUE;
168
        let endStopDistance = Number.MAX_VALUE;
169
170
        this.#publicTransportData.stopsMap.forEach (
171
            stopPoint => {
172
                let distance = theSphericalTrigonometry.pointsDistance (
173
                    [ stopPoint.lat, stopPoint.lon ],
174
                    this.#route.wayPoints.first.latLng
175
                );
176
                if ( distance < startStopDistance ) {
177
                    startStopDistance = distance;
178
                    startStop = stopPoint;
179
                }
180
                distance = theSphericalTrigonometry.pointsDistance (
181
                    [ stopPoint.lat, stopPoint.lon ],
182
                    this.#route.wayPoints.last.latLng
183
                );
184
                if ( distance < endStopDistance ) {
185
                    endStopDistance = distance;
186
                    endStop = stopPoint;
187
                }
188
            }
189
        );
190
191
        // the route is created. All existing itineraryPoints and maneuvers are removed
192
        route.itinerary.itineraryPoints.removeAll ( );
193
        route.itinerary.maneuvers.removeAll ( );
194
        route.itinerary.hasProfile = false;
195
        route.itinerary.ascent = ZERO;
196
        route.itinerary.descent = ZERO;
197
198
        // adding the new itinerary points. We use the nodes linked to the first way
199
        // ( and normally it's the only way !)
200
        // only nodes from the start stop to the end stop are added
201
202
        const NO_POINT_ADDED = 0;
203
        const FIRST_POINT_REACHED = 1;
204
        const OTHERS_POINTS_REACHED = 2;
205
        const LAST_POINT_REACHED = 3;
206
        const ALL_POINTS_ADDED = 4;
207
208
        let addPoint = NO_POINT_ADDED;
209
        let reversePoints = false; // the relation is not ordered, so it's possible we have to reverse
210
        Array.from ( this.#publicTransportData.waysMap.values ( ) )[ ZERO ].nodesIds.forEach (
211
            nodeId => {
212
                if ( NO_POINT_ADDED === addPoint && ( nodeId === startStop.id || nodeId === endStop.id ) ) {
213
214
                    // start stop or end stop is reached
215
                    addPoint = FIRST_POINT_REACHED;
216
                    reversePoints = ( nodeId === endStop.id );
217
                }
218
                else if ( OTHERS_POINTS_REACHED === addPoint && ( nodeId === startStop.id || nodeId === endStop.id ) ) {
219
220
                    // the second stop is reached
221
                    addPoint = LAST_POINT_REACHED;
222
                }
223
                if ( NO_POINT_ADDED < addPoint && ALL_POINTS_ADDED > addPoint ) {
224
225
                    // an itinerary point is created from the node and is added to the itinerary
226
                    const itineraryPoint = new ItineraryPoint ( );
227
                    const node = this.#publicTransportData.nodesMap.get ( nodeId );
228
                    itineraryPoint.latLng = [ node.lat, node.lon ];
229
                    route.itinerary.itineraryPoints.add ( itineraryPoint );
230
231
                    // we verify that the node is not a stop, otherwise we add a maneuver.
232
                    const stopNode = this.#publicTransportData.stopsMap.get ( nodeId );
233
                    if ( stopNode ) {
234
235
                        const maneuver = new Maneuver ( );
236
                        let stopName = null;
237
                        if ( stopNode.tags && stopNode.tags.name ) {
238
                            stopName = stopNode.tags.name;
239
                            maneuver.instruction = stopName + ' : ';
240
                        }
241
                        if ( stopNode.id === startStop.id ) {
242
                            if ( stopName ) {
243
                                route.wayPoints.first.name = stopName;
244
                            }
245
                            maneuver.iconName = 'kTrainStart';
246
                            maneuver.instruction += 'Monter dans le train';
247
                        }
248
                        else if ( stopNode.id === endStop.id ) {
249
                            if ( stopName ) {
250
                                route.wayPoints.last.name = stopName;
251
                            }
252
                            maneuver.iconName = 'kTrainEnd';
253
                            maneuver.instruction += 'Descendre du train';
254
                        }
255
                        else {
256
                            maneuver.iconName = 'kTrainContinue';
257
                            maneuver.instruction += 'Rester dans le train';
258
                        }
259
                        maneuver.distance = ZERO;
260
                        maneuver.duration = ZERO;
261
                        maneuver.itineraryPointObjId = itineraryPoint.objId;
262
263
                        route.itinerary.maneuvers.add ( maneuver );
264
                    }
265
                }
266
                if ( FIRST_POINT_REACHED === addPoint ) {
267
268
                    // start stop or end stop was reached at the beginning of the loop
269
                    addPoint = OTHERS_POINTS_REACHED;
270
                }
271
                if ( LAST_POINT_REACHED === addPoint ) {
272
273
                    // the second stop was reached at the beginning of the loop
274
                    addPoint = ALL_POINTS_ADDED;
275
                }
276
            }
277
        );
278
279
        // reversing points if needed
280
        if ( reversePoints ) {
281
            route.itinerary.itineraryPoints.reverse ( );
282
            route.itinerary.maneuvers.reverse ( );
283
        }
284
285
        // computing distances
286
        route.distance = ZERO;
287
288
        const maneuversIterator = route.itinerary.maneuvers.iterator;
289
        maneuversIterator.done;
290
        let previousManeuver = maneuversIterator.value;
291
        maneuversIterator.done;
292
293
        const itineraryPointsIterator = route.itinerary.itineraryPoints.iterator;
294
        itineraryPointsIterator.done;
295
        let previousPoint = itineraryPointsIterator.value;
296
297
        while ( ! itineraryPointsIterator.done ) {
298
            itineraryPointsIterator.value.distance = ZERO;
299
            previousPoint.distance = theSphericalTrigonometry.pointsDistance (
300
                previousPoint.latLng,
301
                itineraryPointsIterator.value.latLng
302
            );
303
            route.distance += previousPoint.distance;
304
            previousManeuver.distance += previousPoint.distance;
305
            if ( maneuversIterator.value.itineraryPointObjId === itineraryPointsIterator.value.objId ) {
306
307
                previousManeuver = maneuversIterator.value;
308
                previousManeuver.distance = ZERO;
309
310
                maneuversIterator.done;
311
            }
312
            previousPoint = itineraryPointsIterator.value;
313
        }
314
315
    }
316
317
    /**
318
    The constructor
319
    */
320
321
    constructor ( route, selectedRelationId ) {
322
        Object.freeze ( this );
323
        this.#route = route;
324
        this.#selectedRelationId = selectedRelationId;
325
        this.#publicTransportData = new publicTransportData ( selectedRelationId );
326
        this.#nodes3Ways = [];
327
    }
328
329
    buildRoute ( response, onOk, onError ) {
330
331
        // resetting variables
332
        this.#nodes3Ways = [];
333
        this.#publicTransportData.nodes3WaysCounter = ZERO;
334
335
        // maps creation
336
        this.#publicTransportData.createMaps ( response.elements );
337
338
        // Searching all nodes where a way can start or end
339
340
        // analysing the ways at each node
341
        let nodeWithMoreThan3WaysFound = false;
342
        this.#publicTransportData.nodesMap.forEach (
343
            node => {
344
                const waysIds = node.startingWaysIds.concat ( node.endingWaysIds );
345
                switch ( waysIds.length ) {
346
                case ZERO :
347
348
                    // it's a 'transit node'
349
                    break;
350
                case ONE :
351
352
                    // it's a start or end node
353
                    break;
354
                case TWO :
355
356
                    // ways are merged
357
                    this.#publicTransportData.mergeWays ( waysIds [ ZERO ], waysIds [ ONE ] );
358
                    break;
359
                case THREE :
360
                    node.isNode3Ways = true;
361
                    this.#nodes3Ways.push ( node );
362
                    this.#publicTransportData.nodes3WaysCounter ++;
363
                    break;
364
                default :
365
                    nodeWithMoreThan3WaysFound = true;
366
                    window.TaN.showInfo (
367
                        'A node with more than 3 ways is found : ' +
368
                        node.id +
369
                        ' - the relation ' +
370
                        this.#selectedRelationId +
371
                        ' - ways '
372
                        + node.startingWaysIds.concat ( node.endingWaysIds )
373
                    );
374
                    break;
375
                }
376
            }
377
        );
378
379
        if ( nodeWithMoreThan3WaysFound ) {
380
381
            onError ( new Error ( 'A node with more than 3 ways was found in the relation.See the console for more infos' ) );
382
            return;
383
        }
384
385
        // removing holes
386
        if ( this.#publicTransportData.waysMap.size > ( ( this.#publicTransportData.nodes3WaysCounter * TWO ) + ONE ) ) {
387
            new PublicTransportHolesRemover ( this.#publicTransportData ). removeHoles ( );
388
            window.TaN.showInfo (
389
                'Holes found in the OSM relation number ' + this.#selectedRelationId + '. Try to correct OSM data.'
390
            );
391
        }
392
393
        // merging paths at nodes with 3 ways
394
        if ( ZERO < this.#publicTransportData.nodes3WaysCounter ) {
395
            this.#merge3WaysNodes ( );
396
        }
397
398
        // route creation
399
        this.#createRoute ( this.#route );
400
401
        onOk ( this.#route );
402
403
    }
404
405
}
406
407
export default PublicTransportRouteBuilder;
408
409
/* --- End of file --------------------------------------------------------------------------------------------------------- */
410