routexl-client
Version:
this is a request client for routexl.com api
385 lines (372 loc) • 12.4 kB
JavaScript
var request = require("request");
var async = require("async");
var permutate = require("enum-permutate");
var rxltouraccounts = [],
rxldistanceacount;
var tourqueue,
tourwdqueue,
proxy = [],
proxycounter = 0;
(rxoptions = {}), (counter = 1);
/**
*
* @param {{address:string,lng:number,lat:number}[]} locations
* @param {number} tries number of try in total if attemp failes
* @returns {promise} (josn body|false)
*/
var rxldistances = async function rxldistances(locations, tries, temptoken) {
let trycount = 1;
let options = {
...(proxy[0] || {}),
method: "POST",
headers: { Authorization: rxldistanceacount || temptoken },
url: "https://api.routexl.com/distances",
form: { locations }
};
return new Promise(async resolve => {
if (!rxldistanceacount && !temptoken) {
return resolve(
await new Promise(reso => {
tourqueue.push({ locations, tries, type: "distance" }, result =>
reso(result)
);
})
);
}
if (tries === 0) resolve({ error: null, body: {} });
request(options, async function(err, response, body) {
// logging error requests
if (err)
return resolve({
error: { title: "request error", err, request: locations },
body: {}
});
if (response && response.statusCode != 200)
return resolve({
error: {
title: "not 200",
err: { statusCode: response.statusCode, body },
request: locations
},
body: {}
});
let result;
try {
result = { error: null, body: JSON.parse(body) };
} catch (err) {
result = {
error: { title: "error parsing body", err, request: locations, body },
body: {}
};
}
while (!result && tries && trycount < tries) {
trycount++;
result = await rxldistances(locations);
}
return resolve(result);
});
});
};
/**
*
* @param {{address:string,lng:number,lat:number}[]} locations less then 25 locations is supported at the time
* @param {number|{tourtry:number,tourwdtry:number}} tries number of try in total if attemp failes
* @returns {promise} (josn body|false)
*/
var rxltour = async function(locations, tries) {
return await new Promise(reso => {
let notbusycount = rxltouraccounts.filter(ac => !ac.inuse).length;
if (locations.length < 8 && rxoptions.tourwithdistance && !notbusycount) {
let tourwdtry = typeof tries == "object" ? tries.tourwdtry : tries;
tourwdqueue.push({ locations, tries: tourwdtry }, result => reso(result));
} else {
let tourtry = typeof tries == "object" ? tries.tourtry : tries;
tourqueue.push({ locations, tries: tourtry }, result => reso(result));
}
});
};
/**
*
* @param {{token:string,username:string,password:string}[]} accounts routexl acounts
*/
var settourqueue = accounts => {
rxltouraccounts = accounts;
tourqueue = async.queue(function(input, callback) {
if (input.type == "distance") {
let currtoken = rxltouraccounts.find(adri => !adri.inuse);
if (!currtoken)
callback({ error: { title: "no token available", body: {} } });
currtoken.inuse = true;
rxldistances(input.locations, input.tries, currtoken.token).then(
result => {
currtoken.inuse = false;
callback(result);
}
);
} else {
rxltourrequest(input.locations, input.tries).then(result => {
callback(result);
});
}
}, accounts.length);
};
/**
*
* @param {number} threadsqty number of concurent workers
*/
var settourwdequeue = (threadsqty = 20) => {
tourwdqueue = async.queue(function(input, callback) {
routexltourwithdistance(input.locations, input.tries).then(result => {
callback(result);
});
}, threadsqty);
};
var rxltourrequest = function(locations, tries) {
let trycount = 1;
return new Promise(resolve => {
if (tries === 0)
return resolve({ error: { title: "you set tries to zero" }, body: {} });
var currtoken = rxltouraccounts.find(adri => !adri.inuse);
// if no free acount is available
if (!currtoken) {
return resolve({
error: {
title: "threads are more then acount length",
err: "",
request: locations
},
body: {}
});
}
currtoken.inuse = true;
Authorization = currtoken.token;
if (proxy.length > 1) {
proxycounter++;
proxycounter = proxycounter > proxy.length - 1 ? 1 : proxycounter;
}
request(
{
...(proxy[proxycounter] || {}),
method: "POST",
url: "https://api.routexl.com/tour",
headers: { Authorization },
form: { locations }
},
async (err, response, body) => {
//free current account
currtoken.inuse = false;
let result;
if (err)
result = {
error: { title: "request error", err, request: locations },
body: {}
};
if (response && response.statusCode != 200)
result = {
error: {
title: "not 200",
err: { statusCode: response.statusCode, body },
request: locations
},
body: {}
};
try {
if (!result) result = { error: null, body: JSON.parse(body) };
} catch (err) {
result = {
error: {
title: "error parsing body",
err,
request: locations,
body
},
body: {}
};
}
while (result.error && tries && trycount < tries) {
trycount++;
result = await rxltour(locations);
}
return resolve(result);
}
);
});
};
// calc possible routes(array wise);
//max acceptable is 7 waypoints (3 orders)
var possibleroutes = function(waypoints) {
return new Promise(async resolve => {
//create array of numbers
let wparray = [...Array(waypoints.length).keys()];
// all possible orders
let possiblearray = permutate(wparray);
waypoints.forEach((wp, i) => {
if (wp.restrictions && wp.restrictions.after !== undefined) {
let after = parseInt(wp.restrictions.after);
possiblearray = possiblearray.filter(
wpa => wpa.indexOf(i) > wpa.indexOf(after)
);
}
if (wp.restrictions && wp.restrictions.before !== undefined) {
let before = parseInt(wp.restrictions.before);
possiblearray = possiblearray.filter(
wpa => wpa.indexOf(i) < wpa.indexOf(before)
);
}
});
resolve(possiblearray);
});
};
// simulates routexl tour with its distance api
var routexltourwithdistance = async function(waypoints, tries) {
return new Promise(async resolve => {
let output;
let result = await Promise.all([
rxldistances(waypoints, tries),
possibleroutes(waypoints)
]);
if (result[0].error) resolve(result[0]);
try {
let routexlresult = Object.values(result[0].body.distances);
let proutes = result[1];
proutes.forEach(pr => {
let route, thisrout, due, ready, finalresult;
let distance = 0;
ready = (waypoints[pr[0]].restrictions || {}).ready || 0;
servicetime = waypoints[pr[0]].servicetime || 0;
let duration = ready + servicetime;
due = (waypoints[pr[0]].restrictions || {}).due;
route = [
{
waypoint: waypoints[pr[0]],
name: waypoints[pr[0]].address,
arrival: duration,
distance: 0,
relativeArrival: duration * 60,
relativeDistance: 0,
passeddue: due ? due < duration : false
}
];
for (let i = 1; i < pr.length; i++) {
thisrout = routexlresult.find(
rr => rr.from == parseInt(pr[i - 1]) && rr.to == parseInt(pr[i])
);
thisrout =
thisrout ||
routexlresult.find(
rr => rr.to == parseInt(pr[i - 1]) && rr.from == parseInt(pr[i])
);
ready = (waypoints[pr[0]].restrictions || {}).ready || 0;
due = (waypoints[pr[i]].restrictions || {}).due;
if (duration < ready) duration = ready;
duration +=
Math.round(thisrout.duration / 60) +
(waypoints[pr[i]].servicetime || 0);
distance += Math.round(thisrout.distance / 100) / 10;
distance = Math.round(distance * 10) / 10;
route.push({
waypoint: waypoints[pr[i]],
name: waypoints[pr[i]].address,
arrival: duration,
distance: distance,
relativeArrival: thisrout.duration,
relativeDistance: thisrout.distance,
passeddue: due ? due < duration : false
});
}
finalresult = {
route,
distance,
duration,
feasible: route.find(r => r.passeddue) ? false : true
};
output = output || finalresult;
if (output.feasible) {
output =
finalresult.feasible & (output.duration > finalresult.duration)
? finalresult
: output;
} else {
output = finalresult.feasible
? finalresult
: output.duration > finalresult.duration
? finalresult
: output;
}
});
resolve({ error: null, body: output });
} catch (err) {
resolve({
error: {
title: "routewithdistance proccess error",
err,
request: waypoints
},
body: {}
});
}
});
};
/**
*
* @param {string[]|{username:string,password:string}[]} toureacounts string or object of acounts
* @param {{tourwithdistance:boolean,tourwithdistancethreads:number}} options extra options for connectivity
* @param {string|{username:string,password:string}} distanceacount token string or object of acount
* only use one instance of me for routexl tour is single threaded
*/
var initalize = function(toureacounts, options = {}, distanceacount) {
if (!toureacounts) throw new Error("you should provide at lease one acount");
if (!Array.isArray(toureacounts)) toureacounts = [toureacounts];
rxltouraccounts = toureacounts.map(createacounttoken);
if (distanceacount)
rxldistanceacount = createacounttoken(distanceacount).token;
if (!rxldistanceacount && options.tourwithdistance)
throw new Error("input distance acount when useing route with distance");
rxoptions.tourwithdistance = options.tourwithdistance ? true : false;
rxoptions.tourwithdistancethreads =
options.tourwithdistancethreads && options.tourwithdistancethreads < 20
? options.tourwithdistancethreads
: 20;
if (options.proxy) {
if (!Array.isArray(options.proxy)) options.proxy = [options.proxy];
options.proxy.forEach(opro => {
if (opro.type == "socks5")
proxy.push({
strictSSL: true,
agentClass: require("socks5-https-client/lib/Agent"),
agentOptions: {
socksHost: opro.host,
socksPort: opro.port,
socksUsername: opro.username,
socksPassword: opro.password
}
});
});
}
settourqueue(rxltouraccounts);
settourwdequeue(rxoptions.tourwithdistancethreads);
return {
distances: rxldistances,
tour: rxltour,
settourqueue,
settourwdequeue
};
};
// create basic auth token form user and path
var createacounttoken = acount => {
if (typeof acount == "string") return { token: acount };
if (typeof acount == "object") {
if (!acount.username && !acount.password && acount.token) {
return { token: acount.token };
} else {
return {
token:
"Basic " +
Buffer.from(acount.username + ":" + acount.password).toString(
"base64"
)
};
}
}
};
module.exports = initalize;