File : routeProviders/OsrmTextInstructions.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
/*
26
See https://github.com/Project-OSRM/osrm-text-instructions
27
This file is an adaptation for ES6 of the osrm-text-instruction project
28
The osrmTextInstructions object code is the same than the code in the osrm-text-instruction/index.js.
29
If changes are done in osrm-text-instruction, they can be reported there without major problems.
30
Language json files are installed by the grunt.js file. Adapt this file if you will more languages.
31
Don't rename variables for compatibility with osrm-text-instructions
32
*/
33
/* ------------------------------------------------------------------------------------------------------------------------- */
34
35
/* eslint-disable max-lines */
36
37
import { ZERO, ONE, NOT_FOUND, HTTP_STATUS_OK } from '../main/Constants.js';
38
39
/**
40
@ignore
41
*/
42
43
const OUR_OSRM_LANGUAGES = [
44
    'ar',
45
    'da',
46
    'de',
47
    'en',
48
    'eo',
49
    'es',
50
    'es-ES',
51
    'fi',
52
    'fr',
53
    'he',
54
    'hu',
55
    'id',
56
    'it',
57
    'ja',
58
    'ko',
59
    'my',
60
    'nl',
61
    'no',
62
    'pl',
63
    'pt-BR',
64
    'pt-PT',
65
    'ro',
66
    'ru',
67
    'sl',
68
    'sv',
69
    'tr',
70
    'uk',
71
    'vi',
72
    'yo',
73
    'zh-Hans'
74
];
75
76
/**
77
@ignore
78
*/
79
80
const OUR_VERSION = 'v5';
81
82
/**
83
@ignore
84
*/
85
86
const languages =
87
{
88
    supportedCodes : [ ],
89
    instructions : {},
90
    grammars : {},
91
    abbreviations : {}
92
};
93
94
// references to avoid rewriting OsrmTextInstructions
95
96
/**
97
@ignore
98
*/
99
100
const instructions = languages.instructions;
101
102
/**
103
@ignore
104
*/
105
106
const grammars = languages.grammars;
107
108
/**
109
@ignore
110
*/
111
112
const abbreviations = languages.abbreviations;
113
114
/* ------------------------------------------------------------------------------------------------------------------------- */
115
/**
116
This class contains methods to write / translate moneuver instructions
117
in MapboxRouteProvider and OsrmRouteProvider
118
119
See theOsrmTextInstructions for the one and only one instance of this class
120
@ignore
121
*/
122
/* ------------------------------------------------------------------------------------------------------------------------- */
123
124
class OsrmTextInstructions     {
125
126
    /**
127
    new version of the OsrmTextInstructions.directionFromDegree ( ) method
128
    */
129
130
    #directionFromDegree ( language, degree ) {
131
        const NNE = 20;
132
        const ENE = 70;
133
        const ESE = 110;
134
        const SSE = 160;
135
        const SSW = 200;
136
        const WSW = 250;
137
        const WNW = 290;
138
        const NNW = 340;
139
        const NORTH = 360;
140
141
        if ( ! degree && ZERO !== degree ) {
142
            return '';
143
        }
144
        else if ( ZERO > degree && NORTH < degree ) {
145
            throw new Error ( 'Degree ' + degree + ' invalid' );
146
        }
147
        else if ( NNE >= degree ) {
148
            return instructions[ language ][ OUR_VERSION ].constants.direction.north;
149
        }
150
        else if ( ENE > degree ) {
151
            return instructions[ language ][ OUR_VERSION ].constants.direction.northeast;
152
        }
153
        else if ( ESE >= degree ) {
154
            return instructions[ language ][ OUR_VERSION ].constants.direction.east;
155
        }
156
        else if ( SSE > degree ) {
157
            return instructions[ language ][ OUR_VERSION ].constants.direction.southeast;
158
        }
159
        else if ( SSW >= degree ) {
160
            return instructions[ language ][ OUR_VERSION ].constants.direction.south;
161
        }
162
        else if ( WSW > degree ) {
163
            return instructions[ language ][ OUR_VERSION ].constants.direction.southwest;
164
        }
165
        else if ( WNW >= degree ) {
166
            return instructions[ language ][ OUR_VERSION ].constants.direction.west;
167
        }
168
        else if ( NNW > degree ) {
169
            return instructions[ language ][ OUR_VERSION ].constants.direction.northwest;
170
        }
171
        else {
172
            return instructions[ language ][ OUR_VERSION ].constants.direction.north;
173
        }
174
    }
175
176
    async #fetchJson ( data, lngCode ) {
177
        const response = await fetch ( 'TravelNotesProviders/languages/' + data + '/' + lngCode + '.json' );
178
        if ( HTTP_STATUS_OK === response.status && response.ok ) {
179
            const result = await response.json ( );
180
            languages [ data ] [ lngCode ] = result;
181
        }
182
    }
183
184
    /*
185
    constructor
186
    */
187
188
    constructor ( ) {
189
        this.abbreviations = abbreviations;
190
        Object.freeze ( this );
191
    }
192
193
    loadLanguage ( lng ) {
194
        const language = NOT_FOUND === OUR_OSRM_LANGUAGES.indexOf ( lng ) ? 'en' : lng;
195
        [ 'instructions', 'grammars', 'abbreviations' ].forEach (
196
            data => this.#fetchJson ( data, language )
197
        );
198
        return language;
199
    }
200
201
    capitalizeFirstLetter ( language, string ) {
202
        return string.charAt ( ZERO ).toLocaleUpperCase ( language ) + string.slice ( ONE );
203
    }
204
205
    ordinalize ( language, number ) {
206
        return instructions[ language ][ OUR_VERSION ].constants.ordinalize[ number.toString () ] || '';
207
    }
208
209
    directionFromDegree ( language, degree ) {
210
        return this.#directionFromDegree ( language, degree );
211
    }
212
213
    laneConfig ( step ) {
214
        if ( ! step.intersections || ! step.intersections[ ZERO ].lanes ) {
215
            throw new Error ( 'No lanes object' );
216
        }
217
        const config = [];
218
        let currentLaneValidity = null;
219
        step.intersections[ ZERO ].lanes.forEach ( function ( lane ) {
220
            if ( null === currentLaneValidity || currentLaneValidity !== lane.valid ) {
221
                if ( lane.valid ) {
222
                    config.push ( 'o' );
223
                }
224
                else {
225
                    config.push ( 'x' );
226
                }
227
                currentLaneValidity = lane.valid;
228
            }
229
        } );
230
        return config.join ( '' );
231
    }
232
233
    // eslint-disable-next-line complexity
234
    getWayName ( language, step, options ) {
235
        const classes = options ? options.classes || [] : [];
236
        if ( 'object' !== typeof step ) {
237
            throw new Error ( 'step must be an Object' );
238
        }
239
        if ( ! Array.isArray ( classes ) ) {
240
            throw new Error ( 'classes must be an Array or undefined' );
241
        }
242
        let wayName = '';
243
        let stepName = step.name || '';
244
        const ref = ( step.ref || '' ).split ( ';' )[ ZERO ];
245
        if ( stepName === step.ref ) {
246
            stepName = '';
247
        }
248
        stepName = stepName.replace ( ' (' + step.ref + ')', '' );
249
        const wayMotorway = NOT_FOUND !== classes.indexOf ( 'motorway' );
250
        if ( stepName && ref && stepName !== ref && ! wayMotorway ) {
251
            const phrase = instructions[ language ][ OUR_VERSION ].phrase[ 'name and ref' ] ||
252
                instructions.en[ OUR_VERSION ].phrase[ 'name and ref' ];
253
            wayName = this.tokenize ( language, phrase, {
254
                name : stepName,
255
                ref : ref
256
            }, options );
257
        }
258
        else if ( stepName && ref && wayMotorway && ( /\d/ ).test ( ref ) ) {
259
            wayName = options && options.formatToken ? options.formatToken ( 'ref', ref ) : ref;
260
        }
261
        else if ( ! stepName && ref ) {
262
            wayName = options && options.formatToken ? options.formatToken ( 'ref', ref ) : ref;
263
        }
264
        else {
265
            wayName = options && options.formatToken ? options.formatToken ( 'name', stepName ) : stepName;
266
        }
267
        return wayName;
268
    }
269
270
    // eslint-disable-next-line complexity, max-statements
271
    compile ( language, step, opts ) {
272
        if ( ! step.maneuver ) {
273
            throw new Error ( 'No step maneuver provided' );
274
        }
275
        const options = opts || {};
276
        let type = step.maneuver.type;
277
        const modifier = step.maneuver.modifier;
278
        const mode = step.mode;
279
        const side = step.driving_side;
280
        if ( ! type ) {
281
            throw new Error ( 'Missing step maneuver type' );
282
        }
283
        if ( 'depart' !== type && 'arrive' !== type && ! modifier ) {
284
            throw new Error ( 'Missing step maneuver modifier' );
285
        }
286
        if ( ! instructions[ language ][ OUR_VERSION ][ type ] ) {
287
            // eslint-disable-next-line no-console
288
            console.log ( 'Encountered unknown instruction type: ' + type );
289
            type = 'turn';
290
        }
291
        let instructionObject = null;
292
        if ( instructions[ language ][ OUR_VERSION ].modes[ mode ] ) {
293
            instructionObject = instructions[ language ][ OUR_VERSION ].modes[ mode ];
294
        }
295
        else {
296
            const omitSide = 'off ramp' === type && ZERO <= modifier.indexOf ( side );
297
            if ( instructions[ language ][ OUR_VERSION ][ type ][ modifier ] && ! omitSide ) {
298
                instructionObject = instructions[ language ][ OUR_VERSION ][ type ][ modifier ];
299
            }
300
            else {
301
                instructionObject = instructions[ language ][ OUR_VERSION ][ type ].default;
302
            }
303
        }
304
        let laneInstruction = null;
305
        switch ( type ) {
306
        case 'use lane' :
307
            laneInstruction = instructions[ language ][ OUR_VERSION ].constants.lanes[ this.laneConfig ( step ) ];
308
            if ( ! laneInstruction ) {
309
                instructionObject = instructions[ language ][ OUR_VERSION ][ 'use lane' ].no_lanes;
310
            }
311
            break;
312
        case 'rotary' :
313
        case 'roundabout' :
314
            if ( step.rotary_name && step.maneuver.exit && instructionObject.name_exit ) {
315
                instructionObject = instructionObject.name_exit;
316
            }
317
            else if ( step.rotary_name && instructionObject.name ) {
318
                instructionObject = instructionObject.name;
319
            }
320
            else if ( step.maneuver.exit && instructionObject.exit ) {
321
                instructionObject = instructionObject.exit;
322
            }
323
            else {
324
                instructionObject = instructionObject.default;
325
            }
326
            break;
327
        default :
328
        }
329
        const wayName = this.getWayName ( language, step, options );
330
        let instruction = instructionObject.default;
331
        if ( step.destinations && step.exits && instructionObject.exit_destination ) {
332
            instruction = instructionObject.exit_destination;
333
        }
334
        else if ( step.destinations && instructionObject.destination ) {
335
            instruction = instructionObject.destination;
336
        }
337
        else if ( step.exits && instructionObject.exit ) {
338
            instruction = instructionObject.exit;
339
        }
340
        else if ( wayName && instructionObject.name ) {
341
            instruction = instructionObject.name;
342
        }
343
        else if ( options.waypointName && instructionObject.named ) {
344
            instruction = instructionObject.named;
345
        }
346
        const destinations = step.destinations && step.destinations.split ( ': ' );
347
        const destinationRef = destinations && destinations[ ZERO ].split ( ',' )[ ZERO ];
348
        const destination = destinations && destinations[ ONE ] && destinations[ ONE ].split ( ',' )[ ZERO ];
349
        let firstDestination = '';
350
        if ( destination && destinationRef ) {
351
            firstDestination = destinationRef + ': ' + destination;
352
        }
353
        else {
354
            firstDestination = destinationRef || destination || '';
355
        }
356
357
        const nthWaypoint =
358
            ZERO <= options.legIndex && options.legIndex !== options.legCount - ONE
359
                ?
360
                this.ordinalize ( language, options.legIndex + ONE )
361
                :
362
                '';
363
        const replaceTokens = {
364
            // eslint-disable-next-line camelcase
365
            way_name : wayName,
366
            destination : firstDestination,
367
            exit : ( step.exits || '' ).split ( ';' )[ ZERO ],
368
            // eslint-disable-next-line camelcase
369
            exit_number : this.ordinalize ( language, step.maneuver.exit || ONE ),
370
            // eslint-disable-next-line camelcase
371
            rotary_name : step.rotary_name,
372
            // eslint-disable-next-line camelcase
373
            lane_instruction : laneInstruction,
374
            modifier : instructions[ language ][ OUR_VERSION ].constants.modifier[ modifier ],
375
            direction : this.directionFromDegree ( language, step.maneuver.bearing_after ),
376
            nth : nthWaypoint,
377
            // eslint-disable-next-line camelcase
378
            waypoint_name : options.waypointName
379
        };
380
        return this.tokenize ( language, instruction, replaceTokens, options );
381
    }
382
383
    grammarize ( language, nameToProceed, grammar ) {
384
        if ( grammar && grammars && grammars[ language ] && grammars[ language ][ OUR_VERSION ] ) {
385
            const rules = grammars[ language ][ OUR_VERSION ][ grammar ];
386
            if ( rules ) {
387
                let nameWithSpace = ' ' + nameToProceed + ' ';
388
                const flags = grammars[ language ].meta.regExpFlags || '';
389
                rules.forEach ( function ( rule ) {
390
                    const re = new RegExp ( rule[ ZERO ], flags );
391
                    nameWithSpace = nameWithSpace.replace ( re, rule[ ONE ] );
392
                } );
393
394
                return nameWithSpace.trim ();
395
            }
396
        }
397
        return nameToProceed;
398
    }
399
400
    // eslint-disable-next-line max-params
401
    tokenize ( language, instruction, tokens, options ) {
402
        const that = this;
403
        let startedWithToken = false;
404
        const output = instruction.replace (
405
            // eslint-disable-next-line max-params
406
            /\{(\w+)(?::(\w+))?\}/g, function ( token, tag, grammar, offset ) {
407
                let value = tokens[ tag ];
408
                if ( 'undefined' === typeof value ) {
409
                    return token;
410
                }
411
                value = that.grammarize ( language, value, grammar );
412
                if ( ZERO === offset && instructions[ language ].meta.capitalizeFirstLetter ) {
413
                    startedWithToken = true;
414
                    value = that.capitalizeFirstLetter ( language, value );
415
                }
416
                if ( options && options.formatToken ) {
417
                    value = options.formatToken ( tag, value );
418
                }
419
                return value;
420
            }
421
        )
422
            .replace ( / {2}/g, ' ' );
423
        if ( ! startedWithToken && instructions[ language ].meta.capitalizeFirstLetter ) {
424
            return this.capitalizeFirstLetter ( language, output );
425
        }
426
        return output;
427
    }
428
}
429
430
/* ------------------------------------------------------------------------------------------------------------------------- */
431
/**
432
The one and only one instance of OsrmTextInstructions class
433
@type {OsrmTextInstructions}
434
@ignore
435
*/
436
/* ------------------------------------------------------------------------------------------------------------------------- */
437
438
const theOsrmTextInstructions = new OsrmTextInstructions ( );
439
440
export default theOsrmTextInstructions;
441
442
/* eslint-enable max-lines */
443
444
/* --- End of file --------------------------------------------------------------------------------------------------------- */
445