const Client = require('node-rest-client').Client;
const logger = require("./logger");
class VaoClient {
constructor() {
this.startPoint = {};
this.endPoint= {};
this.client = new Client();
// this.url = "https://projektplattform.verkehrsauskunft.at/bin/extxml.exe";
// this.accessKey = "LWZreWtrHuo/7868(T&gf)=(80(=hH&6fg)80h=0h=H90HhhohHHhg7$38068f6";
this.url = "https://routenplaner.verkehrsauskunft.at/bin/extxml.exe";
this.accessKey = "LWZogu)/r68FIouvPh9ßHPIipHIP9ßIPHipnipbougg87vbkiO0099ipni0jhpPO";
}
/**
* The fucntion below takes an an XML-elements as a string. It wrapps this string with the outer XML markup, required by the VAO api.
* Also it assignes the access key to the outer XML
* @param {string} requestBody
*/
generateRequestBlock(requestBody) {
return `${requestBody}`;
}
/**
* The function below takes in a name (string or substring of name) of a place and looks it up using the VAO api
* It returns a prmoise, which
* - resolves to an array of VAO-places or
* - rejects with an error message.
* @param {string} name
*/
getLocationByName(name) {
logger.info({
message: "getLocationByName called for: " + name,
method: 'getLocationByName',
class: "VaoClient"
});
return new Promise((resolve, reject) => {
let args = {
data: this.generateRequestBlock(' ')
};
var req = this.client.post(this.url, args, (data, response) => {
let foundLocations = [];
/**
* data: parsed response body as js object
* response: raw response
*/
/**
* Throw error if undefined is returned
*/
if (!data.ResC.LocValRes) {
logger.error({
message: "Error in getLocationByName(), data.ResC.Err",
err: data.ResC.Err,
method: 'getLocationByName',
class: "VaoClient"
});
reject("No Location found. Error: " + data.ResC.Err);
return;
}
/**
* Define location types of interest as array of objects
*/
let locationTypes = [
{
dataset: data.ResC.LocValRes.Address,
type: "address"
},
{
dataset: data.ResC.LocValRes.Station,
type: "station"
},
{
dataset: data.ResC.LocValRes.Poi,
type: "poi"
},
]
/**
* Filter for Locations in Niederösterreich
*/
let lowerAutriaCoords = {
northEast: {
lat: 49.136993,
lng: 17.274755
},
southWest: {
lat: 47.298545,
lng: 14.263646
}
}
/**
* Loop through locationTypes array to get array of locations
*/
for (let i = 0; i < locationTypes.length; i++) {
let locations = this.processLocations(locationTypes[i].dataset, locationTypes[i].type);
if (locations) {
let filteredLocations = [];
for (let j = 0; j < locations.length; j++) {
// Check if Found Place in Bounds of Niederösterreich
if (locations[j].geolocation.lat < lowerAutriaCoords.northEast.lat &&
locations[j].geolocation.lat > lowerAutriaCoords.southWest.lat &&
locations[j].geolocation.lng < lowerAutriaCoords.northEast.lng &&
locations[j].geolocation.lng > lowerAutriaCoords.southWest.lng) {
filteredLocations.push(locations[j])
}
}
foundLocations = foundLocations.concat(filteredLocations)
}
}
resolve(foundLocations);
});
/**
* Error Handling
*/
req.on('requestTimeout', function (req) {
logger.error({
message: "Request has expired in getLocationByName()",
method: 'getLocationByName',
class: "VaoClient"
});
req.abort();
resolve([]);
});
req.on('responseTimeout', function (res) {
logger.error({
message: "Response has expired in getLocationByName()",
method: 'getLocationByName',
class: "VaoClient"
});
resolve([]);
});
req.on('error', function (err) {
logger.error({
message: "Error in getLocationByName()",
error: err,
method: 'getLocationByName',
class: "VaoClient"
});
resolve([]);
});
})
}
/**
* Function to return a dummy route from Bieberbach to Eisentsadt
*/
dummyRoute() {
let locations = [
this.getLocationByName("Bieberbach"),
this.getLocationByName("Eisenstadt")
]
Promise.all(locations)
.then(locations => {
let fromLocations = locations[0];
let toLocations = locations[1];
if (fromLocations && toLocations) {
let fromLocation = fromLocations[0];
let toLocation = toLocations[0];
this.getRoute(fromLocation, toLocation)
}
})
}
getRoute(from, to, vehicle = "public") {
return new Promise((resolve, reject) => {
let payload = [];
if (from && to) {
logger.info({
message: "From Place",
from: from,
method: 'getRoute',
class: "VaoClient"
});
this.startPoint = {
x: from.x,
y: from.y
}
logger.info({
message: "To Place",
to: to,
method: 'getRoute',
class: "VaoClient"
});
this.endPoint = {
x: to.x,
y: to.y
}
let startBlock = '';
switch (from.type) {
case "station":
startBlock = "" + this.generateStationBlock(from) + '';
break;
case "address":
startBlock = "" + this.generateAddressBlock(from) + '';
break;
case "poi":
startBlock = "" + this.generateAddressBlock(from) + '';
break;
default:
break;
}
let endBlock = '';
switch (to.type) {
case "station":
endBlock = "" + this.generateStationBlock(to) + '';
break;
case "address":
endBlock = "" + this.generateAddressBlock(to) + '';
break;
case "poi":
endBlock = "" + this.generateAddressBlock(to) + '';
break;
default:
break;
}
let args = {
data: this.generateRequestBlock('' + startBlock + endBlock + this.generateGisParameters(vehicle) + '')
}
var req = this.client.post(this.url, args, (data, response) => {
if(typeof data.ResC === "undefined") {
logger.error({
message: "Error returned from VAO API",
error: "Response undefined!",
method: 'getRoute',
class: "VaoClient"
});
resolve([]);
return;
}
if (data.ResC.Err) {
logger.error({
message: "Error returned from VAO API",
error: data.ResC.Err,
method: 'getRoute',
class: "VaoClient"
});
resolve([]);
return;
}
if (data.ResC.ConRes.Err) {
logger.error({
message: "Error returned from VAO API",
error: data.ResC.ConRes.Err,
method: 'getRoute',
class: "VaoClient"
});
resolve([]);
return;
}
/**
* Process found Connections
*/
if (!data.ResC.ConRes.ConnectionList) {
logger.error({
message: "No Connection Found!",
error: err,
method: 'getRoute',
class: "VaoClient"
});
resolve([]);
return;
}
logger.info({
message: "Connection List from VAO API",
method: 'getRoute',
class: "VaoClient"
});
let connection = data.ResC.ConRes.ConnectionList.Connection;
if (Array.isArray(data.ResC.ConRes.ConnectionList.Connection)) {
for (let i = 0; i < data.ResC.ConRes.ConnectionList.Connection.length; i++) {
connection = data.ResC.ConRes.ConnectionList.Connection[i];
}
}
let processedConnections = this.processConnection(connection, vehicle);
if (processedConnections) {
resolve(processedConnections);
} else {
// Send empty Array if no connection found
resolve([]);
}
})
/**
* Error Handling
*/
req.on('requestTimeout', function (req) {
logger.error({
message: "Request has expired in getRoute()",
method: 'getRoute',
class: "VaoClient"
});
req.abort();
resolve([]);
});
req.on('responseTimeout', function (res) {
logger.error({
message: "Response has expired in getRoute()",
method: 'getRoute',
class: "VaoClient"
});
resolve([]);
});
req.on('error', function (err) {
logger.error({
message: "Error in getRoute()",
error: err,
method: 'getRoute',
class: "VaoClient"
});
resolve([]);
});
}
})
}
generateStationBlock(station) {
return ``;
}
generateAddressBlock(address) {
return `
`;
}
/**
* The function below returns the corresponding gis parameters for a request in XML format.
* Here the different parameters for Public only, Park & Rida and Bike and Ride are specified.
* @param {string} vehicle
*/
generateGisParameters(vehicle) {
let gisParameter = ``;
if (vehicle === "public") {
gisParameter = ``;
} else if (vehicle === "parkAndRide") {
gisParameter = `
`;
} else if (vehicle === "bikeAndRide") {
gisParameter = `
`;
} else {
gisParameter = ``;
}
return gisParameter;
}
processLocations(locations, type) {
if (locations) {
let outputArray = [];
// Check if multiple items in category
if (locations.length > 1) {
for (let i = 0; i < locations.length; i++) {
locations[i].$.type = type;
locations[i].$.geolocation = this.parseGeoCoordinates({ x: locations[i].$.x, y: locations[i].$.y });
outputArray.push(locations[i].$)
}
} else {
locations.$.type = type;
locations.$.geolocation = this.parseGeoCoordinates({ x: locations.$.x, y: locations.$.y });
outputArray.push(locations.$)
}
return outputArray;
}
}
parseGeoCoordinates(point) {
let lat = point.y;
lat = parseFloat(lat.slice(0, 2) + "." + lat.slice(2));
let lng = point.x;
lng = parseFloat(lng.slice(0, 2) + "." + lng.slice(2));
let coordinate = {
lat: lat,
lng: lng
}
return coordinate;
}
parseGeoCoordinatesAsArray(point) {
let lat = point.y;
lat = parseFloat(lat.slice(0, 2) + "." + lat.slice(2));
let lng = point.x;
lng = parseFloat(lng.slice(0, 2) + "." + lng.slice(2));
return [lng, lat];
}
getCoordinatedFromPolyline(Polyline) {
if(typeof Polyline === "undefined") {
logger.error({
message: "Polyline undefined",
method: 'getCoordinatedFromPolyline',
class: "VaoClient"
});
return [];
}
let points = Polyline.Point;
let outputArray = [];
for (let i = 0; i < points.length; i++) {
let coordinate = this.parseGeoCoordinatesAsArray(points[i].$)
outputArray.push(coordinate);
}
return outputArray;
}
getNameofBasicStop(BasicStop) {
let name = ""
if (BasicStop.Station && BasicStop.Station.$.name) {
name = BasicStop.Station.$.name;
} else if (BasicStop.Address && BasicStop.Address.$.name) {
name = BasicStop.Address.$.name;
}
return name;
}
getPointNames(connection) {
let pointNames = {
start: "",
end: ""
}
// Departure
if (connection.Overview.Departure.BasicStop) {
pointNames.start = this.getNameofBasicStop(connection.Overview.Departure.BasicStop);
}
// Arrival
if (connection.Overview.Arrival.BasicStop) {
pointNames.end = this.getNameofBasicStop(connection.Overview.Arrival.BasicStop);
}
return pointNames;
}
processConnection(connection, vehicle) {
if (typeof connection === "undefined") {
logger.error({
message: "No Connection Found!",
method: 'processConnection',
class: "VaoClient"
});
return;
}
let conSection = [];
let lineOutput = [];
let totalDistance = 0;
let totalDuration = 0;
let changeOvers = []
if (!connection.ConSectionList.ConSection.length) {
logger.info({
message: "Not an array!",
method: 'processConnection',
class: "VaoClient"
});
conSection.push(connection.ConSectionList.ConSection);
} else {
conSection = connection.ConSectionList.ConSection
}
for (let j = 0; j < conSection.length; j++) {
logger.info({
message: "In conn section: " + j,
method: 'processConnection',
class: "VaoClient"
});
let section = conSection[j];
if (section.$ && section.$.gisHash) {
logger.info({
message: "============ GIS ROUTE ============",
method: 'processConnection',
class: "VaoClient"
});
let sectionMeta = this.getSectionMeta(section.GisRoute.$.type, section.Departure.BasicStop, section.Arrival.BasicStop, section.GisRoute.Duration.Time, section.GisRoute.Distance);
totalDuration += sectionMeta.duration;
totalDistance += sectionMeta.distance;
this.printSection(sectionMeta);
} else if (section.Journey) {
logger.info({
message: "============ PUBLIC ROUTE ============",
//journey: section,
method: 'processConnection',
class: "VaoClient"
});
lineOutput = lineOutput.concat(this.getCoordinatedFromPolyline(section.Polyline));
// Add Changeover Points
changeOvers.push(lineOutput[lineOutput.length - 1]);
let sectionMeta = this.getSectionMeta("PUBLIC", section.Departure.BasicStop, section.Arrival.BasicStop, this.getDurationFromPassList(section.Journey.PassList.BasicStop, true), section.Journey.$.length);
totalDuration += sectionMeta.duration;
totalDistance += sectionMeta.distance;
this.printSection(sectionMeta);
} else if (conSection[j].Walk) {
logger.info({
message: "============ WALK ROUTE ============",
method: 'processConnection',
class: "VaoClient"
});
let sectionMeta = this.getSectionMeta("WALK", section.Departure.BasicStop, section.Arrival.BasicStop, conSection[j].Walk.Duration.Time, conSection[j].Walk.$.length);
totalDuration += sectionMeta.duration;
totalDistance += sectionMeta.distance;
this.printSection(sectionMeta);
} else {
logger.info({
message: "============ UNKNOWN ROUTE ============",
section: section,
method: 'processConnection',
class: "VaoClient"
});
}
}
let startEndNames = this.getPointNames(connection);
logger.info({
message: startEndNames.start + " / " + startEndNames.end,
method: 'processConnection',
class: "VaoClient"
});
if(lineOutput.length === 0) {
logger.info({
message: "Polyline has no Points",
startPoint: this.startPoint,
endPoint: this.endPoint,
method: 'processConnection',
class: "VaoClient"
});
lineOutput.push(this.parseGeoCoordinatesAsArray(this.startPoint));
lineOutput.push(this.parseGeoCoordinatesAsArray(this.endPoint));
}
let payload = {
vehicle: vehicle,
coordinates: lineOutput,
distance: totalDistance,
duration: totalDuration,
changeOvers: changeOvers
}
return payload;
}
generatePointArray(points) {
let outputArray = [];
for (let i = 0; i < points.length; i++) {
let coordinate = this.parseGeoCoordinates(points[i].$)
outputArray.push(coordinate);
}
return outputArray;
}
secondsToTime(seconds) {
let durationInMinutes = seconds / 60;
let minutes = (durationInMinutes % 60);
let hours = Math.floor(durationInMinutes / 60);
return hours + "h " + minutes + "min";
}
parseTime(timeInput) {
let hms = timeInput.substr(3);
var d = timeInput.substr(0, 2);
var a = hms.split(':');
var seconds = (+a[0]) * 60 * 60 + (+a[1]) * 60 + (+a[2]);
// Add one Day in Seconds if arrival one day later
if (d > 0) {
seconds = seconds + 86400;
}
return seconds;
}
getDurationFromPassList(passList, inSeconds) {
let start = passList[0].Dep.Time;
logger.info({
message: "start: " + start,
method: 'getDurationFromPassList',
class: "VaoClient"
});
let end = passList[passList.length - 1].Arr.Time;
logger.info({
message: "end: " + end,
method: 'getDurationFromPassList',
class: "VaoClient"
});
let parsedStart = this.parseTime(start);
let parsedEnd = this.parseTime(end);
let durationInSeconds = parsedEnd - parsedStart;
if (inSeconds) {
return durationInSeconds;
} else {
return this.secondsToTime(durationInSeconds);
}
}
/**
* This function returns the meta data of a routes section
* @param {string} type
* @param {*} from
* @param {*} to
* @param {*} duration
* @param {*} distance
*/
getSectionMeta(type, from, to, duration, distance) {
let metadata = {
type: type,
from: this.getNameofBasicStop(from),
to: this.getNameofBasicStop(to),
distance: parseFloat(distance)
}
logger.info({
message: "Metadata",
metadata: metadata,
method: 'getSectionMeta',
class: "VaoClient"
});
if (type === "PUBLIC") {
metadata.duration = duration
} else {
metadata.duration = this.parseTime(duration)
}
return metadata;
}
printSection(sectionMeta) {
logger.info({
message: "Print Section",
metadata: "From " + sectionMeta.from + " to " + sectionMeta.to + " by " + sectionMeta.type + " (" + sectionMeta.distance + "m / " + this.secondsToTime(sectionMeta.duration) + ")",
method: 'printSection',
class: "VaoClient"
});
}
}
module.exports = VaoClient;