File : routeProviders/PolylineRouteProvider.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 BaseRouteProvider from '../routeProviders/BaseRouteProvider.js';
29
30
import { ZERO, ONE, TWO, LAT, LNG, DEGREES } from '../main/Constants.js';
31
32
/**
33
@ignore
34
*/
35
36
const OUR_HALF_PI = Math.PI / TWO;
37
38
/* ------------------------------------------------------------------------------------------------------------------------- */
39
/**
40
This class implements the BaseRouteProvider for a polyline or circle. It's not possible to instanciate
41
this class because the class is not exported from the module. Only one instance is created and added to the list
42
of Providers of TravelNotes
43
*/
44
/* ------------------------------------------------------------------------------------------------------------------------- */
45
46
class PolylineRouteProvider extends BaseRouteProvider {
47
48
    /**
49
    A reference to the edited route
50
    @type {Route}
51
    */
52
53
    #route;
54
55
    /**
56
    Translations for instructions
57
    @type {Object}
58
    */
59
60
    static get #INSTRUCTIONS_LIST ( ) {
61
        return Object.freeze (
62
            {
63
                en : Object.freeze ( { kStart : 'Start', kContinue : 'Continue', kEnd : 'Stop' } ),
64
                fr : Object.freeze ( { kStart : 'Départ', kContinue : 'Continuer', kEnd : 'Arrivée' } )
65
            }
66
        );
67
    }
68
69
    /**
70
    Enum for icons names
71
    @type {Object}
72
    */
73
74
    static get #ICON_NAMES ( ) {
75
        return Object.freeze (
76
            {
77
                kStart : 'kDepartDefault',
78
                kContinue : 'kContinueStraight',
79
                kEnd : 'kArriveDefault'
80
            }
81
        );
82
    }
83
84
    /**
85
    Add a maneuver to the itinerary
86
    @param {Number} itineraryPointObjId the objId of the itineraryPoint linked to the maneuver
87
    @param {String} position the position of the maneuver. Must be kStart or kEnd
88
    */
89
90
    #addManeuver ( itineraryPointObjId, position ) {
91
        const maneuver = new Maneuver ( );
92
93
        maneuver.iconName = PolylineRouteProvider.#ICON_NAMES [ position ];
94
        maneuver.instruction =
95
            PolylineRouteProvider.#INSTRUCTIONS_LIST [ this.userLanguage ]
96
                ?
97
                PolylineRouteProvider.#INSTRUCTIONS_LIST [ this.userLanguage ] [ position ]
98
                :
99
                PolylineRouteProvider.#INSTRUCTIONS_LIST.en [ position ];
100
        maneuver.duration = ZERO;
101
        maneuver.itineraryPointObjId = itineraryPointObjId;
102
103
        this.#route.itinerary.maneuvers.add ( maneuver );
104
    }
105
106
    /**
107
    Add a itineraryPoint to the itineraryPoints collection
108
    @param {Array.<Number>} latLng the position of the itineraryPoint
109
    @return {Number} the objId of the new itineraryPoint
110
    */
111
112
    #addItineraryPoint ( latLng ) {
113
        const itineraryPoint = new ItineraryPoint ( );
114
        itineraryPoint.latLng = latLng;
115
        this.#route.itinerary.itineraryPoints.add ( itineraryPoint );
116
        return itineraryPoint.objId;
117
    }
118
119
    /**
120
    This method add 64 intermediates points on a stuff of great circle
121
    @param {WayPoint} startWayPoint the starting wayPoint
122
    @param {WayPoint} endWaypoint the ending wayPoint
123
    */
124
125
    #addIntermediateItineraryPoints ( startWayPoint, endWaypoint ) {
126
127
        // first conversion to radian
128
        const latLngStartPoint = [
129
            startWayPoint.lat * DEGREES.toRadians,
130
            startWayPoint.lng * DEGREES.toRadians
131
        ];
132
        const latLngEndPoint = [
133
            endWaypoint.lat * DEGREES.toRadians,
134
            endWaypoint.lng * DEGREES.toRadians
135
        ];
136
137
        // searching the direction: from west to east or east to west...
138
        const WestEast =
139
            ( endWaypoint.lng - startWayPoint.lng + DEGREES.d360 ) % DEGREES.d360 > DEGREES.d180
140
                ?
141
                -ONE
142
                :
143
                ONE;
144
145
        // computing the distance
146
        const angularDistance = theSphericalTrigonometry.arcFromSummitArcArc (
147
            latLngEndPoint [ LNG ] - latLngStartPoint [ LNG ],
148
            OUR_HALF_PI - latLngStartPoint [ LAT ],
149
            OUR_HALF_PI - latLngEndPoint [ LAT ]
150
        );
151
152
        /* for short distances a single line is ok */
153
        // eslint-disable-next-line no-magic-numbers
154
        if ( 0.1 > angularDistance * DEGREES.fromRadians ) {
155
            return;
156
        }
157
158
        // and the direction at the start point
159
        const direction = theSphericalTrigonometry.summitFromArcArcArc (
160
            OUR_HALF_PI - latLngStartPoint [ LAT ],
161
            angularDistance,
162
            OUR_HALF_PI - latLngEndPoint [ LAT ]
163
        );
164
165
        const addedSegments = 64;
166
        const itineraryPoints = [];
167
168
        // loop to compute the added segments
169
        for ( let counter = 1; counter <= addedSegments; counter ++ ) {
170
            const partialDistance = angularDistance * counter / addedSegments;
171
172
            // computing the opposite arc to the start point
173
            const tmpArc = theSphericalTrigonometry.arcFromSummitArcArc (
174
                direction,
175
                OUR_HALF_PI - latLngStartPoint [ LAT ],
176
                partialDistance
177
            );
178
179
            // computing the lng
180
            const deltaLng = theSphericalTrigonometry.summitFromArcArcArc (
181
                OUR_HALF_PI - latLngStartPoint [ LAT ],
182
                tmpArc,
183
                partialDistance
184
            );
185
186
            // adding the itinerary point to a tmp array
187
            const itineraryPoint = new ItineraryPoint ( );
188
            itineraryPoint.latLng = [
189
                ( OUR_HALF_PI - tmpArc ) * DEGREES.fromRadians,
190
                ( latLngStartPoint [ LNG ] + ( WestEast * deltaLng ) ) * DEGREES.fromRadians
191
            ];
192
            itineraryPoints.push ( itineraryPoint );
193
        }
194
195
        // last added itinerary point  is the same than the end waypoint, so we remove and we adapt the lng
196
        // of the end waypoint ( we can have a difference of 360 degree due to computing east or west
197
        endWaypoint.lng = itineraryPoints.pop ( ).lng;
198
199
        // adding itinerary points to the route
200
        itineraryPoints.forEach ( itineraryPoint => this.#route.itinerary.itineraryPoints.add ( itineraryPoint ) );
201
    }
202
203
    /**
204
    Set a stuff of great circle as itinerary
205
    */
206
207
    #parseGreatCircle ( ) {
208
        let wayPointsIterator = this.#route.wayPoints.iterator;
209
        let previousWayPoint = null;
210
        while ( ! wayPointsIterator.done ) {
211
            if ( wayPointsIterator.first ) {
212
213
                // first point... adding an itinerary point and the start maneuver
214
                previousWayPoint = wayPointsIterator.value;
215
                this.#addManeuver (
216
                    this.#addItineraryPoint ( wayPointsIterator.value.latLng ),
217
                    'kStart'
218
                );
219
            }
220
            else {
221
222
                // next points.... adding intermediate points, itinerary point and maneuver
223
                this.#addIntermediateItineraryPoints (
224
                    previousWayPoint,
225
                    wayPointsIterator.value
226
                );
227
                this.#addManeuver (
228
                    this.#addItineraryPoint ( wayPointsIterator.value.latLng ),
229
                    wayPointsIterator.last ? 'kEnd' : 'kContinue'
230
                );
231
                previousWayPoint = wayPointsIterator.value;
232
            }
233
        }
234
235
        // moving complete travel if needed, so we are always near the origine
236
        let maxLng = -Number.MAX_VALUE;
237
        let itineraryPointsIterator = this.#route.itinerary.itineraryPoints.iterator;
238
        while ( ! itineraryPointsIterator.done ) {
239
            maxLng = Math.max ( maxLng, itineraryPointsIterator.value.lng );
240
        }
241
        const deltaLng = ( maxLng % DEGREES.d360 ) - maxLng;
242
243
        itineraryPointsIterator = this.#route.itinerary.itineraryPoints.iterator;
244
        while ( ! itineraryPointsIterator.done ) {
245
            itineraryPointsIterator.value.lng += deltaLng;
246
        }
247
        wayPointsIterator = this.#route.wayPoints.iterator;
248
        while ( ! wayPointsIterator.done ) {
249
            wayPointsIterator.value.lng += deltaLng;
250
        }
251
    }
252
253
    /**
254
    this function set a circle as itinerary
255
    */
256
257
    #parseCircle ( ) {
258
259
        const centerPoint = [
260
            this.#route.wayPoints.first.lat * DEGREES.toRadians,
261
            this.#route.wayPoints.first.lng * DEGREES.toRadians
262
        ];
263
264
        const distancePoint = [
265
            this.#route.wayPoints.last.lat * DEGREES.toRadians,
266
            this.#route.wayPoints.last.lng * DEGREES.toRadians
267
        ];
268
269
        const angularDistance = theSphericalTrigonometry.arcFromSummitArcArc (
270
            centerPoint [ LNG ] - distancePoint [ LNG ],
271
            OUR_HALF_PI - centerPoint [ LAT ],
272
            OUR_HALF_PI - distancePoint [ LAT ]
273
        );
274
275
        const addedSegments = 360;
276
        const itineraryPoints = [];
277
278
        // loop to compute the added segments
279
        for ( let counter = 0; counter <= addedSegments; counter ++ ) {
280
281
            const direction = ( Math.PI / ( TWO * addedSegments ) ) + ( ( Math.PI * counter ) / addedSegments );
282
283
            const tmpArc = theSphericalTrigonometry.arcFromSummitArcArc (
284
                direction,
285
                angularDistance,
286
                OUR_HALF_PI - centerPoint [ LAT ]
287
            );
288
289
            const deltaLng = theSphericalTrigonometry.summitFromArcArcArc (
290
                OUR_HALF_PI - centerPoint [ LAT ],
291
                tmpArc,
292
                angularDistance
293
            );
294
            let itineraryPoint = new ItineraryPoint ( );
295
            itineraryPoint.latLng = [
296
                ( OUR_HALF_PI - tmpArc ) * DEGREES.fromRadians,
297
                ( centerPoint [ LNG ] + deltaLng ) * DEGREES.fromRadians
298
            ];
299
            itineraryPoints.push ( itineraryPoint );
300
301
            itineraryPoint = new ItineraryPoint ( );
302
            itineraryPoint.latLng = [
303
                ( OUR_HALF_PI - tmpArc ) * DEGREES.fromRadians,
304
                ( centerPoint [ LNG ] - deltaLng ) * DEGREES.fromRadians
305
            ];
306
            itineraryPoints.unshift ( itineraryPoint );
307
            if ( counter === addedSegments ) {
308
                this.#addManeuver ( itineraryPoint.objId, 'kStart' );
309
                itineraryPoint = new ItineraryPoint ( );
310
                itineraryPoint.latLng = [
311
                    ( OUR_HALF_PI - tmpArc ) * DEGREES.fromRadians,
312
                    ( centerPoint [ LNG ] - deltaLng ) * DEGREES.fromRadians
313
                ];
314
                this.#addManeuver ( itineraryPoint.objId, 'kEnd' );
315
                itineraryPoints.push ( itineraryPoint );
316
            }
317
        }
318
319
        itineraryPoints.forEach ( itineraryPoint => this.#route.itinerary.itineraryPoints.add ( itineraryPoint ) );
320
321
    }
322
323
    /**
324
    Build a polyline (as stuff of a great circle) or a circle from the start and end wayPoints
325
    @param {function} onOk a function to call when the response is parsed correctly
326
    @param {function} onError a function to call when an error occurs
327
    */
328
329
    #parseResponse ( onOk, onError ) {
330
        try {
331
            this.#route.itinerary.itineraryPoints.removeAll ( );
332
            this.#route.itinerary.maneuvers.removeAll ( );
333
            this.#route.itinerary.hasProfile = false;
334
            this.#route.itinerary.ascent = ZERO;
335
            this.#route.itinerary.descent = ZERO;
336
337
            switch ( this.#route.itinerary.transitMode ) {
338
            case 'line' :
339
                this.#parseGreatCircle ( );
340
                break;
341
            case 'circle' :
342
                this.#parseCircle ( );
343
                break;
344
            default :
345
                break;
346
            }
347
            onOk ( this.#route );
348
        }
349
        catch ( err ) { onError ( err ); }
350
    }
351
352
    /**
353
    The constructor
354
    */
355
356
    constructor ( ) {
357
        super ( );
358
    }
359
360
    /**
361
    Call the provider, using the waypoints defined in the route and, on success,
362
    complete the route with the data from the provider
363
    @param {Route} route The route to witch the data will be added
364
    @return {Promise} A Promise. On success, the Route is completed with the data given by the provider.
365
    */
366
367
    getPromiseRoute ( route ) {
368
        this.#route = route;
369
        return new Promise ( ( onOk, onError ) => this.#parseResponse ( onOk, onError ) );
370
    }
371
372
    /**
373
    The icon used in the ProviderToolbarUI.
374
    Overload of the base class icon property
375
    @type {String}
376
    */
377
378
    get icon ( ) {
379
        return 'data:image/svg+xml;utf8,<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" > <circle cx="12" c' +
380
            'y="12" r="3" stroke="rgb(0,0,0)" fill="transparent" /> <line x1="5" y1="17" x2="11" y2="2" stroke="rgb(0,0,0)" ' +
381
            '/> <line x1="3" y1="6" x2="17" y2="9" stroke="rgb(191,0,0)" /> <line x1="3" y1="16" x2="17" y2="5" stroke="rgb(' +
382
            '255,204,0)" /> </svg>';
383
    }
384
385
    /**
386
    The provider name.
387
    Overload of the base class name property
388
    @type {String}
389
    */
390
391
    get name ( ) { return 'Polyline'; }
392
393
    /**
394
    The title to display in the ProviderToolbarUI button.
395
    Overload of the base class title property
396
    @type {String}
397
    */
398
399
    get title ( ) { return 'Polyline & Circle'; }
400
401
    /**
402
    The possible transit modes for the provider.
403
    Overload of the base class transitModes property
404
    Must be a subarray of [ 'bike', 'pedestrian', 'car', 'train', 'line', 'circle' ]
405
    @type {Array.<String>}
406
    */
407
408
    get transitModes ( ) { return [ 'line', 'circle' ]; }
409
410
    /**
411
    A boolean indicating when a provider key is needed for the provider.
412
    Overload of the base class providerKeyNeeded property
413
    @type {Boolean}
414
    */
415
416
    get providerKeyNeeded ( ) { return false; }
417
}
418
419
window.TaN.addProvider ( PolylineRouteProvider );
420
421
/* --- End of file --------------------------------------------------------------------------------------------------------- */
422