@furkot/directions 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md ADDED
@@ -0,0 +1,34 @@
1
+ [![NPM version][npm-image]][npm-url]
2
+ [![Build Status][build-image]][build-url]
3
+ [![Dependency Status][deps-image]][deps-url]
4
+
5
+ # furkot-directions
6
+
7
+ Directions service for Furkot
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ $ npm install --save furkot-directions
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```js
18
+ var furkotDirections = require('furkot-directions');
19
+
20
+ furkotDirections('Rainbow');
21
+ ```
22
+
23
+ ## License
24
+
25
+ MIT © [Damian Krzeminski](https://pirxpilot.me)
26
+
27
+ [npm-image]: https://img.shields.io/npm/v/@furkot/directions
28
+ [npm-url]: https://npmjs.org/package/@furkot/directions
29
+
30
+ [build-url]: https://github.com/furkot/directions/actions/workflows/check.yaml
31
+ [build-image]: https://img.shields.io/github/workflow/status/furkot/directions/check
32
+
33
+ [deps-image]: https://img.shields.io/librariesio/release/npm/@furkot/directions
34
+ [deps-url]: https://libraries.io/npm/@furkot%2Fdirections
package/index.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./lib/directions');
@@ -0,0 +1,137 @@
1
+ const strategy = require('run-waterfall-until');
2
+ const travelMode = require('./model').travelMode;
3
+ const util = require('./service/util');
4
+
5
+ module.exports = furkotDirections;
6
+
7
+ function skip(options, query, result) {
8
+ // some other service already calculated directions
9
+ // or service is disabled
10
+ return result || !options.enable(query, result);
11
+ }
12
+
13
+ // query cascades through services until one produces a result
14
+ // 'skip' function for a service is used to determine whether
15
+ // it should be skipped or applied to a given request
16
+ const services = {
17
+ graphhopper: {
18
+ service: require('./service/graphhopper'),
19
+ skip
20
+ },
21
+ mapquest: {
22
+ service: require('./service/mapquest'),
23
+ skip
24
+ },
25
+ openroute: {
26
+ service: require('./service/openroute'),
27
+ skip
28
+ },
29
+ valhalla: {
30
+ service: require('./service/valhalla'),
31
+ skip
32
+ },
33
+ osrm: {
34
+ service: require('./service/osrm'),
35
+ skip(options, query, result) {
36
+ // or asking for walking or biking directions (OSRM doesn't do it well)
37
+ return skip(options, query, result) || (query.mode !== travelMode.car && query.mode !== travelMode.motorcycle);
38
+ }
39
+ }
40
+ };
41
+
42
+ // default timeout to complete operation
43
+ const defaultTimeout = 20 * 1000;
44
+
45
+ let id = 0;
46
+
47
+ function furkotDirections(options) {
48
+
49
+ /**
50
+ * Asynchronous directions service
51
+ * @param query directions query object
52
+ * @param fn function called with directions
53
+ */
54
+ function directions(query, fn) {
55
+ if (!query) {
56
+ return fn();
57
+ }
58
+
59
+ id += 1;
60
+
61
+ const result = new Array(query.length);
62
+ if (!query.length) {
63
+ return fn(query, result);
64
+ }
65
+
66
+ const queryId = id;
67
+ let timeoutId = setTimeout(function () {
68
+ timeoutId = undefined;
69
+ // cancel outstanding requests
70
+ options.services.forEach(function (service) {
71
+ service.abort(queryId);
72
+ });
73
+ }, options.timeout);
74
+
75
+ strategy(options.services, queryId, query, result, function (err, queryId, query, result) {
76
+ if (timeoutId) {
77
+ clearTimeout(timeoutId);
78
+ timeoutId = undefined;
79
+ }
80
+ if (err) {
81
+ return fn();
82
+ }
83
+ // if no results, mark first as empty
84
+ if (result.length > 0 && !result.some(function (r) {
85
+ return r;
86
+ })) {
87
+ result[0] = {
88
+ query: query[0],
89
+ routes: [{
90
+ distance: 0,
91
+ duration: 0
92
+ }]
93
+ };
94
+ }
95
+ fn(query, result);
96
+ });
97
+ }
98
+
99
+ options = util.defaults(options, {
100
+ timeout: defaultTimeout,
101
+ order: ['osrm', 'mapquest', 'valhalla', 'graphhopper', 'openroute']
102
+ });
103
+ if (!options.services) {
104
+ options.services = options.order.reduce(function (result, name) {
105
+ const service = services[options[name] || name];
106
+ let defaults;
107
+ if (service && options[(name + '_enable')]) {
108
+ defaults = {
109
+ name,
110
+ limiter: options[(name + '_limiter')],
111
+ enable: options[(name + '_enable')],
112
+ skip: service.skip
113
+ };
114
+ if (options[name]) {
115
+ Object.keys(options).reduce(mapOptions, {
116
+ options,
117
+ name,
118
+ optName: options[name],
119
+ defaults
120
+ });
121
+ }
122
+ result.push(service.service(util.defaults(defaults, options)));
123
+ }
124
+ return result;
125
+ }, []);
126
+ }
127
+
128
+ directions.options = options;
129
+ return directions;
130
+ }
131
+
132
+ function mapOptions(result, opt) {
133
+ if (opt.startsWith(result.name)) {
134
+ result.defaults[opt.replace(result.name, result.optName)] = result.options[opt];
135
+ }
136
+ return result;
137
+ }
package/lib/model.js ADDED
@@ -0,0 +1,64 @@
1
+ // path simplification constants
2
+ const pathType = {
3
+ none: 'none', // don't include the path in route (default)
4
+ coarse: 'coarse', // include heavily simplified path
5
+ smooth: 'smooth', // include path that is somewhat simplified
6
+ full: 'full' // don't simplify the route path at all
7
+ };
8
+
9
+ // travel mode constants
10
+ const travelMode = {
11
+ motorcycle: -1,
12
+ car: 0,
13
+ bicycle: 1,
14
+ walk: 2,
15
+ other: 3,
16
+ ferry: 6
17
+ };
18
+
19
+ // template for directions query object
20
+ const directionsQuery = [{ // array of legs each for consecutive series of points
21
+ mode: travelMode.car, // numeric value of travel mode
22
+ avoidHighways: false, // true to avoid highways
23
+ avoidTolls: false, // true to avoid toll roads
24
+ units: 'm', // m - miles, km - kilometers
25
+ points: [
26
+ [0, 0]
27
+ ], // array of consecutive series of points; each point is [lon, lat]
28
+ isInChina: false, // points are in China (some services need this information)
29
+ begin: '', // date/time for the begin of route as 'YYYY-MM-DDThh:mm'
30
+ turnbyturn: false, // provide detailed turn-by-turn instructions (segments in directionsResult)
31
+ seasonal: false, // include roads that are seasonally closed
32
+ path: pathType.none, // the degree of route path simplification
33
+ span: 0, // distance in meters for more detailed path simplification
34
+ alternate: false, // return alternatives to the default route
35
+ stats: [] // set on output - list of providers that requests have been sent to to obtain directions
36
+ }];
37
+
38
+ // template for directions results object
39
+ const directionsResult = [{ // array of directions legs, one for each consecutive series of points
40
+ query: directionsQuery, // query parameters
41
+ places: [], // addresses or place names corresponding to points (if directions service performs reverse geocoding)
42
+ name: '', // human-readable name of directions (if available)
43
+ routes: [{ // routes; one for each point with a successor in the query.points
44
+ duration: 0, // route duration in seconds
45
+ distance: 0, // route distance in meters
46
+ path: [], // simplified series of interim points; each point is [lon, lat]
47
+ seasonal: false, // indicates a road that is seasonally closed
48
+ segmentIndex: 0 // index of the turn-by-turn directions segment
49
+ }],
50
+ segments: [{ // turn-by-turn directions
51
+ duration: 0, // segment duration in seconds
52
+ distance: 0, // segment distance in meters
53
+ path: [], // series of interim points; each point is [lon, lat]
54
+ instructions: '' // textual instructions for this segment
55
+ }],
56
+ provider: '' // identifies service providing the directions
57
+ }];
58
+
59
+ module.exports = {
60
+ directionsQuery,
61
+ directionsResult,
62
+ pathType,
63
+ travelMode
64
+ };
@@ -0,0 +1,164 @@
1
+ const { pathType } = require("../../model");
2
+ const status = require('../status');
3
+ const util = require('../util');
4
+
5
+ module.exports = init;
6
+
7
+ const vehicle = {
8
+ '-1': 'car',
9
+ 0: 'car',
10
+ 1: 'bike',
11
+ 2: 'foot',
12
+ 5: 'truck'
13
+ };
14
+
15
+ const weighting = {
16
+ true: 'curvature',
17
+ 1: 'curvature',
18
+ 2: 'curvaturefastest'
19
+ };
20
+
21
+ function prepareWaypoint(qs, p) {
22
+ // waypoint format is lat,lon
23
+ qs.push('point=' + encodeURIComponent(p[1] + ',' + p[0]));
24
+ return qs;
25
+ }
26
+
27
+ function extractSegment(result, { distance, interval, text, time }) {
28
+ const { directions: { segments }, path } = result;
29
+ segments.push({
30
+ duration: Math.round((time || 0) / 1000),
31
+ distance: Math.round(distance || 0),
32
+ path: path && path.slice(interval[0], interval[1]),
33
+ instructions: text
34
+ });
35
+ return result;
36
+ }
37
+
38
+ function extractDirections(result, { distance, instructions, points, time }) {
39
+ const { directions: { routes, segments }, fullPath } = result;
40
+ result.path = util.decode(points);
41
+ const route = {
42
+ duration: Math.round((time || 0) / 1000),
43
+ distance: Math.round(distance || 0)
44
+ };
45
+ if (fullPath) {
46
+ route.path = result.path;
47
+ }
48
+ if (segments) {
49
+ route.segmentIndex = segments.length;
50
+ }
51
+ routes.push(route);
52
+ if (segments && instructions) {
53
+ instructions.reduce(extractSegment, result);
54
+ if (segments.length) {
55
+ util.last(segments).path.push(util.last(result.path));
56
+ }
57
+ }
58
+ return result;
59
+ }
60
+
61
+ function getStatus(st, response) {
62
+ st = st && st.status;
63
+ if (!(st || response)) {
64
+ return;
65
+ }
66
+ response = response || {};
67
+ response.status = response.status || st;
68
+ // 401 Unauthorized, 429 Too Many Requests
69
+ if (st === 401 || st === 429) {
70
+ // we exceeded the limit
71
+ return status.failure;
72
+ }
73
+ if (st === 400 && response.message) {
74
+ return status.empty;
75
+ }
76
+ if (!st) {
77
+ return status.success;
78
+ }
79
+ }
80
+
81
+ function init(options) {
82
+
83
+ function prepareUrl(url, { avoidHighways, avoidTolls, avoidUnpaved, curvy, mode, path, points, turnbyturn }) {
84
+ let req = {
85
+ vehicle: vehicle[mode] || vehicle[0],
86
+ key: options.graphhopper_key
87
+ };
88
+ if (curvy && mode === -1) {
89
+ req.vehicle = 'motorcycle.kurviger.de';
90
+ req.weighting = weighting[curvy];
91
+ if (options.parameters.app_type) {
92
+ req['app.type'] = options.parameters.app_type;
93
+ }
94
+ if (avoidUnpaved) {
95
+ req.avoid_unpaved_roads = true;
96
+ }
97
+ }
98
+ if (!turnbyturn && path !== pathType.smooth && path !== pathType.coarse) {
99
+ req.instructions = false;
100
+ }
101
+ if (options.parameters.flexible) {
102
+ if (avoidTolls) {
103
+ req['ch.disable'] = true;
104
+ req.avoid = ['toll'];
105
+ }
106
+ if (avoidHighways) {
107
+ req['ch.disable'] = true;
108
+ req.avoid = req.avoid || [];
109
+ req.avoid.push('motorway');
110
+ }
111
+ }
112
+
113
+ req = points.reduce(prepareWaypoint, Object.keys(req).map(function (name) {
114
+ return name + '=' + encodeURIComponent(req[name]);
115
+ }));
116
+
117
+ return url + '?' + req.join('&');
118
+ }
119
+
120
+ function prepareRequest({ mode, points, curvy }) {
121
+ return !(options.parameters.max_curvy_distance && curvy && mode === -1 && points.length === 2 &&
122
+ util.distance(points[0], points[1]) > options.parameters.max_curvy_distance);
123
+ }
124
+
125
+ function processResponse(response, query) {
126
+
127
+ if (response && response.status >= 400) {
128
+ // let it cascade to the next service
129
+ return;
130
+ }
131
+
132
+ const directions = {
133
+ query,
134
+ provider: options.name
135
+ };
136
+ const paths = response && response.paths;
137
+ if (paths) {
138
+ directions.routes = [];
139
+ if (query.turnbyturn || query.path === pathType.smooth || query.path === pathType.coarse) {
140
+ directions.segments = [];
141
+ }
142
+ const fullPath = query.path === pathType.full;
143
+ paths.reduce(extractDirections, {
144
+ directions,
145
+ fullPath
146
+ });
147
+ if (fullPath) {
148
+ // path is already prepared - no need to do it again from segments
149
+ directions.pathReady = true;
150
+ }
151
+ }
152
+ return directions;
153
+ }
154
+
155
+ options = util.defaults(options, {
156
+ maxPoints: options.graphhopper_max_points || 5, // max 5 points for free and 30-150 for paid plan
157
+ url: prepareUrl.bind(undefined, options.graphhopper_url),
158
+ status: getStatus,
159
+ prepareRequest,
160
+ processResponse
161
+ });
162
+ options.parameters = options.graphhopper_parameters || {};
163
+ return require('..')(options);
164
+ }
@@ -0,0 +1,228 @@
1
+ const fetchagent = require('fetchagent');
2
+ const pathType = require("../model").pathType;
3
+ const series = require('run-series');
4
+ const status = require('./status');
5
+ const util = require('./util');
6
+ const debug = require('debug')('furkot:directions:service');
7
+
8
+ module.exports = init;
9
+
10
+ const limiters = {};
11
+
12
+ const ERROR = 'input error';
13
+
14
+ function eachOfSeries(items, task, fn) {
15
+ const tasks = items.map(function (item, i) {
16
+ return task.bind(null, item, i);
17
+ });
18
+ return series(tasks, fn);
19
+ }
20
+
21
+ function request(url, req, fn) {
22
+ const options = this;
23
+ let fa = fetchagent;
24
+ if (options.post) {
25
+ fa = fa.post(url).send(req);
26
+ } else {
27
+ fa = fa.get(url).query(req);
28
+ }
29
+ if (options.authorization) {
30
+ fa.set('authorization', options.authorization);
31
+ }
32
+ return fa
33
+ .set('accept', 'application/json')
34
+ .end(fn);
35
+ }
36
+
37
+ function initUrl(url) {
38
+ if (typeof url === 'function') {
39
+ return url;
40
+ }
41
+ return function () {
42
+ return url;
43
+ };
44
+ }
45
+
46
+ function init(options) {
47
+ let limiter;
48
+ let holdRequests;
49
+ let simplify;
50
+ const outstanding = {};
51
+
52
+ function abort(queryId) {
53
+ debug('abort', queryId);
54
+ if (!outstanding[queryId]) {
55
+ return;
56
+ }
57
+ // cancel later request if scheduled
58
+ if (outstanding[queryId].laterTimeoutId) {
59
+ clearTimeout(outstanding[queryId].laterTimeoutId);
60
+ }
61
+ // cancel request in progress
62
+ if (outstanding[queryId].reqInProgress) {
63
+ outstanding[queryId].reqInProgress.abort();
64
+ }
65
+ outstanding[queryId].callback(ERROR);
66
+ }
67
+
68
+ function directions(queryId, queryArray, result, fn) {
69
+
70
+ function spliceResults(idx, segments, segResult) {
71
+ Array.prototype.splice.apply(queryArray, [idx + queryArray.delta, 1].concat(segments));
72
+ Array.prototype.splice.apply(result, [idx + queryArray.delta, 1].concat(segResult));
73
+ queryArray.delta += segments.length - 1;
74
+ }
75
+
76
+ function queryDirections(query, idx, callback) {
77
+ let req;
78
+ let segments;
79
+
80
+ function requestLater() {
81
+ outstanding[queryId].laterTimeoutId = setTimeout(function () {
82
+ if (outstanding[queryId]) {
83
+ delete outstanding[queryId].laterTimeoutId;
84
+ }
85
+ queryDirections(query, idx, callback);
86
+ }, options.penaltyTimeout);
87
+ }
88
+
89
+ if (!outstanding[queryId]) {
90
+ // query has been aborted
91
+ return;
92
+ }
93
+ outstanding[queryId].callback = callback;
94
+
95
+ if (options.skip(options, query, result[idx + queryArray.delta])) {
96
+ return callback();
97
+ }
98
+
99
+ if (holdRequests) {
100
+ return callback();
101
+ }
102
+
103
+ segments = util.splitPoints(query, queryArray.maxPoints || options.maxPoints);
104
+ if (!segments) {
105
+ return callback(ERROR);
106
+ }
107
+
108
+ if (segments !== query) {
109
+ segments[0].stats = query.stats;
110
+ delete query.stats;
111
+ return directions(queryId, segments,
112
+ new Array(segments.length),
113
+ function (err, stop, id, query, result) {
114
+ if (query && result) {
115
+ spliceResults(idx, query, result);
116
+ }
117
+ callback(err);
118
+ });
119
+ }
120
+
121
+ query.path = query.path || pathType.none;
122
+ req = options.prepareRequest(query);
123
+ if (!req) {
124
+ return callback();
125
+ }
126
+ if (req === true) {
127
+ req = undefined;
128
+ }
129
+
130
+ limiter.trigger(function () {
131
+ if (!outstanding[queryId]) {
132
+ // query has been aborted
133
+ limiter.skip(); // immediately process the next request in the queue
134
+ return;
135
+ }
136
+ query.stats = query.stats || [];
137
+ query.stats.push(options.name);
138
+ outstanding[queryId].reqInProgress = options.request(options.url(query), req, function (err, response) {
139
+ let st;
140
+ let res;
141
+ if (!outstanding[queryId]) {
142
+ // query has been aborted
143
+ return;
144
+ }
145
+ delete outstanding[queryId].reqInProgress;
146
+ st = options.status(err, response);
147
+ if (st === undefined) {
148
+ // shouldn't happen (bug or unexpected response format)
149
+ // treat it as no route
150
+ st = status.empty;
151
+ }
152
+ if (st === status.failure) {
153
+ // don't ever ask again
154
+ holdRequests = true;
155
+ return callback();
156
+ }
157
+ if (st === status.error) {
158
+ // try again later
159
+ limiter.penalty();
160
+ return requestLater();
161
+ }
162
+ if (st === status.empty && query.points.length > 2) {
163
+ query = [query];
164
+ query.maxPoints = 2;
165
+
166
+ return directions(queryId, query,
167
+ new Array(1),
168
+ function (err, stop, id, query, result) {
169
+ if (query && result) {
170
+ spliceResults(idx, query, result);
171
+ }
172
+ callback(err);
173
+ });
174
+ }
175
+
176
+ res = options.processResponse(response, query);
177
+ if (res) {
178
+ if (!res.pathReady && res.routes && res.segments) {
179
+ simplify(query.path, query.span, res.routes, res.segments);
180
+ }
181
+ if (!query.turnbyturn) {
182
+ delete res.segments;
183
+ }
184
+ result[idx + queryArray.delta] = res;
185
+ }
186
+ callback();
187
+ });
188
+ });
189
+ }
190
+
191
+ outstanding[queryId] = outstanding[queryId] || {
192
+ stack: 0,
193
+ hits: 0
194
+ };
195
+ outstanding[queryId].stack += 1;
196
+ outstanding[queryId].callback = function (err) {
197
+ fn(err, true, queryId, queryArray, result);
198
+ };
199
+ queryArray.delta = 0;
200
+
201
+ eachOfSeries(queryArray, queryDirections, function (err) {
202
+ if (outstanding[queryId]) {
203
+ outstanding[queryId].stack -= 1;
204
+ if (!outstanding[queryId].stack) {
205
+ delete outstanding[queryId];
206
+ }
207
+ if (err === ERROR) {
208
+ return fn(outstanding[queryId] ? err : undefined, true, queryId, queryArray, result);
209
+ }
210
+ fn(err, false, queryId, queryArray, result);
211
+ }
212
+ });
213
+ }
214
+
215
+ options = util.defaults(options, {
216
+ interval: 340,
217
+ penaltyInterval: 2000,
218
+ limiter: limiters[options.name],
219
+ request,
220
+ abort
221
+ });
222
+ options.url = initUrl(options.url);
223
+ limiters[options.name] = options.limiter || require('limiter-component')(options.interval, options.penaltyInterval);
224
+ limiter = limiters[options.name];
225
+ simplify = require('./simplify')(options);
226
+ directions.abort = options.abort;
227
+ return directions;
228
+ }