New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@furkot/directions

Package Overview
Dependencies
Maintainers
2
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@furkot/directions - npm Package Compare versions

Comparing version

to
2.0.0

166

lib/directions.js

@@ -1,11 +0,9 @@

const strategy = require('run-waterfall-until');
const travelMode = require('./model').travelMode;
const util = require('./service/util');
const { defaults: defaults, withTimeout } = require('./service/util');
module.exports = furkotDirections;
function skip(options, query, result) {
// some other service already calculated directions
// or service is disabled
return result || !options.enable(query, result);
function skip(options, query) {
// if service is disabled
return !options.enable(query);
}

@@ -35,5 +33,5 @@

service: require('./service/osrm'),
skip(options, query, result) {
skip(options, query) {
// or asking for walking or biking directions (OSRM doesn't do it well)
return skip(options, query, result) || (query.mode !== travelMode.car && query.mode !== travelMode.motorcycle);
return skip(options, query) || (query.mode !== travelMode.car && query.mode !== travelMode.motorcycle);
}

@@ -46,87 +44,89 @@ }

let id = 0;
function furkotDirections(options) {
options = {
timeout: defaultTimeout,
order: ['osrm', 'mapquest', 'valhalla', 'graphhopper', 'openroute'],
...options
};
if (!options.services) {
options.services = options.order.map(name => {
const service = services[options[name] || name];
if (!service) {
return;
}
const enable = options[`${name}_enable`];
if (!enable) {
return;
}
// object representing actual parameters for a service
const serviceOptions = {
name,
limiter: options[`${name}_limiter`],
enable,
skip: service.skip
};
if (options[name]) {
Object.keys(options).reduce(mapOptions, {
options,
name,
optName: options[name],
serviceOptions
});
}
// we are adding options that has not been copied to serviceOptions yet
return service.service(defaults(serviceOptions, options));
}).filter(Boolean);
}
directions.options = options;
return directions;
/**
* Asynchronous directions service
* @param query directions query object
* @param fn function called with directions
*/
function directions(query, fn) {
if (!query) {
return fn();
function directions(query, { signal } = {}) {
if (query?.points?.length > 1) {
return requestDirections(query, options.timeout);
}
id += 1;
const result = new Array(query.length);
if (!query.length) {
return fn(query, result);
}
const queryId = id;
let timeoutId = setTimeout(function () {
timeoutId = undefined;
// cancel outstanding requests
options.services.forEach(function (service) {
service.abort(queryId);
});
}, options.timeout);
strategy(options.services, queryId, query, result, function (err, queryId, query, result) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = undefined;
}
if (err) {
return fn();
}
// if no results, mark first as empty
if (result.length > 0 && !result.some(function (r) {
return r;
})) {
result[0] = {
query: query[0],
routes: [{
distance: 0,
duration: 0
}]
};
}
fn(query, result);
});
}
options = util.defaults(options, {
timeout: defaultTimeout,
order: ['osrm', 'mapquest', 'valhalla', 'graphhopper', 'openroute']
});
if (!options.services) {
options.services = options.order.reduce(function (result, name) {
const service = services[options[name] || name];
let defaults;
if (service && options[(name + '_enable')]) {
defaults = {
name,
limiter: options[(name + '_limiter')],
enable: options[(name + '_enable')],
skip: service.skip
};
if (options[name]) {
Object.keys(options).reduce(mapOptions, {
options,
name,
optName: options[name],
defaults
});
async function requestDirections(query, timeout) {
const stats = [];
for (const service of options.services) {
if (service.skip(service, query)) {
continue;
}
result.push(service.service(util.defaults(defaults, options)));
stats.push(service.name);
const startTime = Date.now();
const result = await withTimeout(service.operation(query), Math.floor(timeout / 2), signal);
if (signal?.aborted) {
break;
}
if (result?.query) {
result.stats = stats;
result.provider = service.name;
return result;
}
timeout -= Date.now() - startTime;
if (timeout <= 0) {
break;
}
if (query.points.length > 2) {
return requestDirections({
...query,
points: query.points.slice(0, 2)
}, timeout);
}
}
return result;
}, []);
return {
query: {
...query,
points: query.points.slice(0, 2)
},
stats,
routes: [{}]
};
}
}
directions.options = options;
return directions;
}

@@ -136,5 +136,5 @@

if (opt.startsWith(result.name)) {
result.defaults[opt.replace(result.name, result.optName)] = result.options[opt];
result.serviceOptions[opt.replace(result.name, result.optName)] = result.options[opt];
}
return result;
}

@@ -20,3 +20,3 @@ // path simplification constants

// template for directions query object
const directionsQuery = [{ // array of legs each for consecutive series of points
const directionsQuery = {
mode: travelMode.car, // numeric value of travel mode

@@ -35,8 +35,7 @@ avoidHighways: false, // true to avoid highways

span: 0, // distance in meters for more detailed path simplification
alternate: false, // return alternatives to the default route
stats: [] // set on output - list of providers that requests have been sent to to obtain directions
}];
alternate: false // return alternatives to the default route
};
// template for directions results object
const directionsResult = [{ // array of directions legs, one for each consecutive series of points
const directionsResult = {
query: directionsQuery, // query parameters

@@ -58,4 +57,5 @@ places: [], // addresses or place names corresponding to points (if directions service performs reverse geocoding)

}],
stats: [], // list of providers that requests have been sent to to obtain directions
provider: '' // identifies service providing the directions
}];
};

@@ -62,0 +62,0 @@ module.exports = {

const fetchagent = require('fetchagent');
const pathType = require("../model").pathType;
const series = require('run-series');
const { pathType } = require("../model");
const makeLimiter = require('limiter-component');
const status = require('./status');
const util = require('./util');
const makeSimplify = require('./simplify');
const debug = require('debug')('furkot:directions:service');

@@ -14,216 +14,105 @@

function eachOfSeries(items, task, fn) {
const tasks = items.map(function (item, i) {
return task.bind(null, item, i);
});
return series(tasks, fn);
}
function init(options) {
options = {
interval: 340,
penaltyInterval: 2000,
limiter: limiters[options.name],
request,
operation,
...options
};
options.url = initUrl(options.url);
limiters[options.name] = options.limiter || makeLimiter(options.interval, options.penaltyInterval);
const limiter = limiters[options.name];
const simplify = makeSimplify(options);
return options;
function request(url, req, fn) {
const options = this;
let fa = fetchagent;
if (options.post) {
fa = fa.post(url).send(req);
} else {
fa = fa.get(url).query(req);
async function operation(query) {
const { maxPoints } = options;
const { points } = query;
if (points.length > maxPoints) {
debug('Can only query %d points', maxPoints);
query = {
...query,
points: points.slice(0, maxPoints)
};
}
return queryDirections(query);
}
if (options.authorization) {
fa.set('authorization', options.authorization);
}
return fa
.set('accept', 'application/json')
.end(fn);
}
function initUrl(url) {
if (typeof url === 'function') {
return url;
}
return function () {
return url;
};
}
async function queryDirections(query) {
if (!query) {
throw ERROR;
}
function init(options) {
let limiter;
let holdRequests;
let simplify;
const outstanding = {};
query.path = query.path || pathType.none;
let req = options.prepareRequest(query);
if (!req) {
return;
}
if (req === true) {
req = undefined;
}
function abort(queryId) {
debug('abort', queryId);
if (!outstanding[queryId]) {
await limiter.trigger();
const { status: err, response } = await options.request(options.url(query), req);
let st = options.status(err, response);
if (st === undefined) {
// shouldn't happen (bug or unexpected response format)
// treat it as no route
st = status.empty;
}
if (st === status.failure) {
// don't ever ask again
options.skip = () => true;
return;
}
// cancel later request if scheduled
if (outstanding[queryId].laterTimeoutId) {
clearTimeout(outstanding[queryId].laterTimeoutId);
if (st === status.error) {
// try again later
limiter.penalty();
return queryDirections(query);
}
// cancel request in progress
if (outstanding[queryId].reqInProgress) {
outstanding[queryId].reqInProgress.abort();
if (st === status.empty) {
return;
}
outstanding[queryId].callback(ERROR);
}
function directions(queryId, queryArray, result, fn) {
function spliceResults(idx, segments, segResult) {
Array.prototype.splice.apply(queryArray, [idx + queryArray.delta, 1].concat(segments));
Array.prototype.splice.apply(result, [idx + queryArray.delta, 1].concat(segResult));
queryArray.delta += segments.length - 1;
}
function queryDirections(query, idx, callback) {
let req;
let segments;
function requestLater() {
outstanding[queryId].laterTimeoutId = setTimeout(function () {
if (outstanding[queryId]) {
delete outstanding[queryId].laterTimeoutId;
}
queryDirections(query, idx, callback);
}, options.penaltyTimeout);
const res = options.processResponse(response, query);
if (res) {
if (!res.pathReady && res.routes && res.segments) {
simplify(query.path, query.span, res.routes, res.segments);
}
if (!outstanding[queryId]) {
// query has been aborted
return;
if (!query.turnbyturn) {
delete res.segments;
}
outstanding[queryId].callback = callback;
if (options.skip(options, query, result[idx + queryArray.delta])) {
return callback();
}
if (holdRequests) {
return callback();
}
segments = util.splitPoints(query, queryArray.maxPoints || options.maxPoints);
if (!segments) {
return callback(ERROR);
}
if (segments !== query) {
segments[0].stats = query.stats;
delete query.stats;
return directions(queryId, segments,
new Array(segments.length),
function (err, stop, id, query, result) {
if (query && result) {
spliceResults(idx, query, result);
}
callback(err);
});
}
query.path = query.path || pathType.none;
req = options.prepareRequest(query);
if (!req) {
return callback();
}
if (req === true) {
req = undefined;
}
limiter.trigger(function () {
if (!outstanding[queryId]) {
// query has been aborted
limiter.skip(); // immediately process the next request in the queue
return;
}
query.stats = query.stats || [];
query.stats.push(options.name);
outstanding[queryId].reqInProgress = options.request(options.url(query), req, function (err, response) {
let st;
let res;
if (!outstanding[queryId]) {
// query has been aborted
return;
}
delete outstanding[queryId].reqInProgress;
st = options.status(err, response);
if (st === undefined) {
// shouldn't happen (bug or unexpected response format)
// treat it as no route
st = status.empty;
}
if (st === status.failure) {
// don't ever ask again
holdRequests = true;
return callback();
}
if (st === status.error) {
// try again later
limiter.penalty();
return requestLater();
}
if (st === status.empty && query.points.length > 2) {
query = [query];
query.maxPoints = 2;
return directions(queryId, query,
new Array(1),
function (err, stop, id, query, result) {
if (query && result) {
spliceResults(idx, query, result);
}
callback(err);
});
}
res = options.processResponse(response, query);
if (res) {
if (!res.pathReady && res.routes && res.segments) {
simplify(query.path, query.span, res.routes, res.segments);
}
if (!query.turnbyturn) {
delete res.segments;
}
result[idx + queryArray.delta] = res;
}
callback();
});
});
}
return res;
}
}
outstanding[queryId] = outstanding[queryId] || {
stack: 0,
hits: 0
async function request(url, req) {
const options = this;
let fa = fetchagent;
if (options.post) {
fa = fa.post(url).send(req);
} else {
fa = fa.get(url).query(req);
}
if (options.authorization) {
fa.set('authorization', options.authorization);
}
const res = await fa.set('accept', 'application/json').end();
let status;
if (!res.ok) {
status = {
status: res.status
};
outstanding[queryId].stack += 1;
outstanding[queryId].callback = function (err) {
fn(err, true, queryId, queryArray, result);
};
queryArray.delta = 0;
eachOfSeries(queryArray, queryDirections, function (err) {
if (outstanding[queryId]) {
outstanding[queryId].stack -= 1;
if (!outstanding[queryId].stack) {
delete outstanding[queryId];
}
if (err === ERROR) {
return fn(outstanding[queryId] ? err : undefined, true, queryId, queryArray, result);
}
fn(err, false, queryId, queryArray, result);
}
});
}
return {
status,
response: await res.json()
};
}
options = util.defaults(options, {
interval: 340,
penaltyInterval: 2000,
limiter: limiters[options.name],
request,
abort
});
options.url = initUrl(options.url);
limiters[options.name] = options.limiter || require('limiter-component')(options.interval, options.penaltyInterval);
limiter = limiters[options.name];
simplify = require('./simplify')(options);
directions.abort = options.abort;
return directions;
function initUrl(url) {
return typeof url === 'function' ? url : () => url;
}

@@ -16,3 +16,5 @@ const LatLon = require('geodesy/latlon-spherical');

split2object,
splitPoints
collateResults,
withTimeout,
timeout
};

@@ -47,5 +49,4 @@

let d = 0;
let p2;
while (index < path.length) {
p2 = toLatLon(path[index]);
const p2 = toLatLon(path[index]);
d += p1.distanceTo(p2);

@@ -83,22 +84,54 @@ if (d > distance) {

function splitPoints(query, maxPoints) {
let i;
let segments;
if (!(query.points && query.points.length > 1)) {
return;
function collateResults(results, query) {
return results.reduce((result, r) => {
concatArrayProp(result, r, 'segments');
concatArrayProp(result, r, 'places');
concatArrayProp(result, r, 'routes');
if (!result.name && r?.name) {
result.name = r.name;
}
return result;
}, {
query
});
function concatArrayProp(to, from, prop) {
if (!from[prop]) {
return;
}
if (!to[prop]) {
to[prop] = from[prop];
} else {
to[prop].push(...from[prop]);
}
}
if (query.points.length <= maxPoints) {
return query;
}
function withTimeout(promise, millis, signal) {
let id;
let reject;
signal?.addEventListener('abort', onabort);
return Promise
.race([promise, new Promise(timeoutPromise)])
.finally(() => {
signal?.removeEventListener('abort', onabort);
clearTimeout(id);
});
function onabort() {
reject(signal.reason);
}
segments = [];
for (i = 0; i < query.points.length - 1; i += maxPoints - 1) {
segments.push(defaults({
points: query.points.slice(i, i + maxPoints),
stats: []
}, query));
function timeoutPromise(_, _reject) {
reject = _reject;
id = setTimeout(
() => reject(Error('timeout', { cause: Symbol.for('timeout') })),
millis
);
}
if (last(segments).points.length === 1) {
last(segments).points.unshift(segments[segments.length - 2].points.pop());
}
return segments;
}
function timeout(millis = 0) {
return new Promise(resolve => setTimeout(resolve, millis));
}
{
"name": "@furkot/directions",
"version": "1.5.2",
"version": "2.0.0",
"description": "Directions service for Furkot",

@@ -20,7 +20,5 @@ "author": {

"debug": "~2 || ~3 || ~4",
"fetchagent": "~2",
"fetchagent": "~2.1.0",
"geodesy": "^1.1.1",
"limiter-component": "^1.0.0",
"run-series": "^1.1.4",
"run-waterfall-until": "~1",
"limiter-component": "^1.2.0",
"vis-why": "^1.2.2"

@@ -30,4 +28,2 @@ },

"jshint": "~2",
"lodash.clonedeep": "^4.5.0",
"lodash.clonedeepwith": "^4.5.0",
"mocha": "~10",

@@ -34,0 +30,0 @@ "should": "~13",