forked from Project-OSRM/osrm-text-instructions
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
306 lines (274 loc) · 14.5 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
var languages = require('./languages');
var instructions = languages.instructions;
var grammars = languages.grammars;
var abbreviations = languages.abbreviations;
module.exports = function(version) {
Object.keys(instructions).forEach(function(code) {
if (!instructions[code][version]) { throw 'invalid version ' + version + ': ' + code + ' not supported'; }
});
return {
capitalizeFirstLetter: function(language, string) {
return string.charAt(0).toLocaleUpperCase(language) + string.slice(1);
},
ordinalize: function(language, number) {
// Transform numbers to their translated ordinalized value
if (!language) throw new Error('No language code provided');
return instructions[language][version].constants.ordinalize[number.toString()] || '';
},
directionFromDegree: function(language, degree) {
// Transform degrees to their translated compass direction
if (!language) throw new Error('No language code provided');
if (!degree && degree !== 0) {
// step had no bearing_after degree, ignoring
return '';
} else if (degree >= 0 && degree <= 20) {
return instructions[language][version].constants.direction.north;
} else if (degree > 20 && degree < 70) {
return instructions[language][version].constants.direction.northeast;
} else if (degree >= 70 && degree <= 110) {
return instructions[language][version].constants.direction.east;
} else if (degree > 110 && degree < 160) {
return instructions[language][version].constants.direction.southeast;
} else if (degree >= 160 && degree <= 200) {
return instructions[language][version].constants.direction.south;
} else if (degree > 200 && degree < 250) {
return instructions[language][version].constants.direction.southwest;
} else if (degree >= 250 && degree <= 290) {
return instructions[language][version].constants.direction.west;
} else if (degree > 290 && degree < 340) {
return instructions[language][version].constants.direction.northwest;
} else if (degree >= 340 && degree <= 360) {
return instructions[language][version].constants.direction.north;
} else {
throw new Error('Degree ' + degree + ' invalid');
}
},
laneConfig: function(step) {
// Reduce any lane combination down to a contracted lane diagram
if (!step.intersections || !step.intersections[0].lanes) throw new Error('No lanes object');
var config = [];
var currentLaneValidity = null;
step.intersections[0].lanes.forEach(function (lane) {
if (currentLaneValidity === null || currentLaneValidity !== lane.valid) {
if (lane.valid) {
config.push('o');
} else {
config.push('x');
}
currentLaneValidity = lane.valid;
}
});
return config.join('');
},
getWayName: function(language, step, options) {
var classes = options ? options.classes || [] : [];
if (typeof step !== 'object') throw new Error('step must be an Object');
if (!language) throw new Error('No language code provided');
if (!Array.isArray(classes)) throw new Error('classes must be an Array or undefined');
var wayName;
var name = step.name || '';
var ref = (step.ref || '').split(';')[0];
// Remove hacks from Mapbox Directions mixing ref into name
if (name === step.ref) {
// if both are the same we assume that there used to be an empty name, with the ref being filled in for it
// we only need to retain the ref then
name = '';
}
name = name.replace(' (' + step.ref + ')', '');
// In attempt to avoid using the highway name of a way,
// check and see if the step has a class which should signal
// the ref should be used instead of the name.
var wayMotorway = classes.indexOf('motorway') !== -1;
if (name && ref && name !== ref && !wayMotorway) {
var phrase = instructions[language][version].phrase['name and ref'] ||
instructions.en[version].phrase['name and ref'];
wayName = this.tokenize(language, phrase, {
name: name,
ref: ref
}, options);
} else if (name && ref && wayMotorway && (/\d/).test(ref)) {
wayName = options && options.formatToken ? options.formatToken('ref', ref) : ref;
} else if (!name && ref) {
wayName = options && options.formatToken ? options.formatToken('ref', ref) : ref;
} else {
wayName = options && options.formatToken ? options.formatToken('name', name) : name;
}
return wayName;
},
/**
* Formulate a localized text instruction from a step.
*
* @param {string} language Language code.
* @param {object} step Step including maneuver property.
* @param {object} opts Additional options.
* @param {string} opts.legIndex Index of leg in the route.
* @param {string} opts.legCount Total number of legs in the route.
* @param {array} opts.classes List of road classes.
* @param {string} opts.waypointName Name of waypoint for arrival instruction.
*
* @return {string} Localized text instruction.
*/
compile: function(language, step, opts) {
if (!language) throw new Error('No language code provided');
if (languages.supportedCodes.indexOf(language) === -1) throw new Error('language code ' + language + ' not loaded');
if (!step.maneuver) throw new Error('No step maneuver provided');
var options = opts || {};
var type = step.maneuver.type;
var modifier = step.maneuver.modifier;
var mode = step.mode;
// driving_side will only be defined in OSRM 5.14+
var side = step.driving_side;
if (!type) { throw new Error('Missing step maneuver type'); }
if (type !== 'depart' && type !== 'arrive' && !modifier) { throw new Error('Missing step maneuver modifier'); }
if (!instructions[language][version][type]) {
// Log for debugging
console.log('Encountered unknown instruction type: ' + type); // eslint-disable-line no-console
// OSRM specification assumes turn types can be added without
// major version changes. Unknown types are to be treated as
// type `turn` by clients
type = 'turn';
}
// Use special instructions if available, otherwise `defaultinstruction`
var instructionObject;
if (instructions[language][version].modes[mode]) {
instructionObject = instructions[language][version].modes[mode];
} else {
// omit side from off ramp if same as driving_side
// note: side will be undefined if the input is from OSRM <5.14
// but the condition should still evaluate properly regardless
var omitSide = type === 'off ramp' && modifier.indexOf(side) >= 0;
if (instructions[language][version][type][modifier] && !omitSide) {
instructionObject = instructions[language][version][type][modifier];
} else {
instructionObject = instructions[language][version][type].default;
}
}
// Special case handling
var laneInstruction;
switch (type) {
case 'use lane':
laneInstruction = instructions[language][version].constants.lanes[this.laneConfig(step)];
if (!laneInstruction) {
// If the lane combination is not found, default to continue straight
instructionObject = instructions[language][version]['use lane'].no_lanes;
}
break;
case 'rotary':
case 'roundabout':
if (step.rotary_name && step.maneuver.exit && instructionObject.name_exit) {
instructionObject = instructionObject.name_exit;
} else if (step.rotary_name && instructionObject.name) {
instructionObject = instructionObject.name;
} else if (step.maneuver.exit && instructionObject.exit) {
instructionObject = instructionObject.exit;
} else {
instructionObject = instructionObject.default;
}
break;
default:
// NOOP, since no special logic for that type
}
// Decide way_name with special handling for name and ref
var wayName = this.getWayName(language, step, options);
// Decide which instruction string to use
// In order of precedence:
// - exit + destination signage
// - destination signage
// - exit signage
// - junction name
// - road name
// - waypoint name (for arrive maneuver)
// - default
var instruction;
if (step.destinations && step.exits && instructionObject.exit_destination) {
instruction = instructionObject.exit_destination;
} else if (step.destinations && instructionObject.destination) {
instruction = instructionObject.destination;
} else if (step.exits && instructionObject.exit) {
instruction = instructionObject.exit;
} else if (step.junction_name && instructionObject.junction_name) {
instruction = instructionObject.junction_name;
} else if (wayName && instructionObject.name) {
instruction = instructionObject.name;
} else if (options.waypointName && instructionObject.named) {
instruction = instructionObject.named;
} else {
instruction = instructionObject.default;
}
var destinations = step.destinations && step.destinations.split(': ');
var destinationRef = destinations && destinations[0].split(',')[0];
var destination = destinations && destinations[1] && destinations[1].split(',')[0];
var firstDestination;
if (destination && destinationRef) {
firstDestination = destinationRef + ': ' + destination;
} else {
firstDestination = destinationRef || destination || '';
}
var nthWaypoint = options.legIndex >= 0 && options.legIndex !== options.legCount - 1 ? this.ordinalize(language, options.legIndex + 1) : '';
// Replace tokens
// NOOP if they don't exist
var replaceTokens = {
'way_name': wayName,
'destination': firstDestination,
'exit': (step.exits || '').split(';')[0],
'exit_number': this.ordinalize(language, step.maneuver.exit || 1),
'rotary_name': step.rotary_name,
'lane_instruction': laneInstruction,
'modifier': instructions[language][version].constants.modifier[modifier],
'direction': this.directionFromDegree(language, step.maneuver.bearing_after),
'nth': nthWaypoint,
'waypoint_name': options.waypointName,
'junction_name': (step.junction_name || '').split(';')[0]
};
return this.tokenize(language, instruction, replaceTokens, options);
},
grammarize: function(language, name, grammar) {
if (!language) throw new Error('No language code provided');
// Process way/rotary/any name with applying grammar rules if any
if (grammar && grammars && grammars[language] && grammars[language][version]) {
var rules = grammars[language][version][grammar];
if (rules) {
// Pass original name to rules' regular expressions enclosed with spaces for simplier parsing
var n = ' ' + name + ' ';
var flags = grammars[language].meta.regExpFlags || '';
rules.forEach(function(rule) {
var re = new RegExp(rule[0], flags);
n = n.replace(re, rule[1]);
});
return n.trim();
}
}
return name;
},
abbreviations: abbreviations,
tokenize: function(language, instruction, tokens, options) {
if (!language) throw new Error('No language code provided');
// Keep this function context to use in inline function below (no arrow functions in ES4)
var that = this;
var startedWithToken = false;
var output = instruction
.replace(/\{(\w+)(?::(\w+))?\}/g, function(token, tag, grammar, offset) {
var value = tokens[tag];
// Return unknown token unchanged
if (typeof value === 'undefined') {
return token;
}
value = that.grammarize(language, value, grammar);
// If this token appears at the beginning of the instruction, capitalize it.
if (offset === 0 && instructions[language].meta.capitalizeFirstLetter) {
startedWithToken = true;
value = that.capitalizeFirstLetter(language, value);
}
if (options && options.formatToken) {
value = options.formatToken(tag, value);
}
return value;
})
.replace(/ {2}/g, ' '); // remove excess spaces
if (!startedWithToken && instructions[language].meta.capitalizeFirstLetter) {
return this.capitalizeFirstLetter(language, output);
}
return output;
}
};
};