'use strict';
const https = require('https');
const parseXML = require('xml2js').parseString;
const TF = require('right-track-core/modules/classes/RightTrackTransitAgency/TransitFeed');
const TransitFeed = TF.TransitFeed;
const TransitDivision = TF.TransitDivision;
const TransitEvent = TF.TransitEvent;
// Agency Config
let CONFIG = {};
// Feed Cache
let CACHE = undefined;
let CACHE_UPDATED = new Date(0);
/**
* Load the NYSTA Transit Feed
* @param {Object} config Transit Agency config
* @param {function} callback Callback function
* @param {Error} [callback.error] Transit Feed Error. The Error's message will be a pipe (|) separated
* string in the format of: Error Code|Error Type|Error Message that will be parsed out by the Right
* Track API Server into a more specific error Response.
* @param {TransitFeed} [callback.feed] The built Transit Feed for NYSTA
*/
function loadFeed(config, callback) {
// Set Config
CONFIG = config;
// Return Cached Feed
if ( CACHE !== undefined &&
CACHE_UPDATED.getTime() >= (new Date().getTime() - (CONFIG.maxCache*1000)) ) {
return callback(null, CACHE);
}
// Get Fresh Feed
else {
// Download the Feed
_download(function(xml) {
// Process the Feed
if ( xml ) {
_parse(xml, function(feed) {
// Return the Feed
if ( feed ) {
CACHE = feed;
CACHE_UPDATED = feed.updated;
return callback(null, feed);
}
// No Feed Returned
else {
_parseError();
}
});
}
// No Feed Returned
else {
_parseError();
}
});
}
/**
* Return a Parse Error Response
* @private
*/
function _parseError() {
return callback(
new Error("5004|Could Not Parse Transit Data|The NYSTA Status Feed did not return a valid response. This may be temporary so try again later.")
);
}
}
/**
* Parse the NYSTA Service Feed XML into a Transit Feed
* @param {string} xml NYSTA Service Feed XML
* @param {function} callback Callback function(feed)
* @private
*/
function _parse(xml, callback) {
parseXML(xml, function(err, result) {
if ( err || !result || !result.events ) {
return callback();
}
// Get the events
let events = result.events.event ? result.events.event : [];
// Parse the Timestamp
let timestamp = result.events.lastupdatetime[0];
let updated = new Date(Date.parse(timestamp));
// Create the Feed
let feed = new TransitFeed(updated);
// Create the Divisions (with lines set)
feed.divisions = _createDivisions();
// Add Events to Feed
feed = _parseEvents(feed, events);
// Return the Feed
return callback(feed);
});
}
/**
* Create the Divisions (with Lines set)
* @returns {TransitDivision[]} List of Transit Divisions
* @private
*/
function _createDivisions() {
// Divisions to Return
let regions = [];
// Parse the regions from the config
for ( let i = 0; i < CONFIG.regions.length; i++ ) {
// CREATE TOP-LEVEL DIVISION FROM REGION
let region = new TransitDivision(
CONFIG.regions[i].code,
CONFIG.regions[i].name,
CONFIG.regions[i].icon
);
// Get the region's highways
let highways = [];
for ( let j = 0; j < CONFIG.regions[i].highways.length; j++ ) {
let highwayCode = CONFIG.regions[i].highways[j];
// Find Highway Definition
for ( let k = 0; k < CONFIG.highways.length; k++ ) {
if ( highwayCode === CONFIG.highways[k].code ) {
// CREATE 2ND-LEVEL DIVISION FROM HIGHWAY
let highway = new TransitDivision(
CONFIG.highways[k].code,
CONFIG.highways[k].name,
CONFIG.highways[k].backgroundColor,
CONFIG.highways[k].textColor
);
// Set default status
highway.status = "No Alerts";
// Add highway to list of region highways
highways.push(highway);
}
}
}
// Add the highways to the region's divisions
region.divisions = highways;
// Add the region to the List
regions.push(region);
}
// Return the regions
return regions;
}
/**
* Parse NYSTA Events and add to Transit Feed
* @param {TransitFeed} feed Transit Feed
* @param {Object[]} events List of NYSTA Events
* @returns {TransitFeed}
* @private
*/
function _parseEvents(feed, events) {
// Parse Each Event
if ( events ) {
for ( let i = 0; i < events.length; i++ ) {
let event = events[i]['$'];
// Format Title
let title = _title(event['eventtype']);
// Format Description
let description = event['eventdesc'].replace(event['eventtype'] + ", ", "");
// Set HTML Style
let style = "<style>";
style += "@font-face {font-family: 'Material Icons'; font-style: normal; font-weight: 400; src: url(https://fonts.gstatic.com/s/materialicons/v36/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2) format('woff2');} .material-icons {font-family: 'Material Icons'; font-weight: normal; font-style: normal;}";
style += "</style>";
// Set Details HTML
let details = style;
details += "<div class='event-details-description'>" + description + "</div>";
details += "<div class='event-details-info'>"
details += "<p><strong>Milepost:</strong> " + event['milepost'] + "</p>";
details += "<p><strong>Posted:</strong> " + event['updatetime'] + "</p>";
details += "<p><strong>Until:</strong> " + event['expirationdatetime'] + "</p>";
details += "<p><strong>Location:</strong> ";
details += "<a href='https://www.google.com/maps/search/?api=1&query=" + event['latitude'] + "," + event['longitude'] + "&zoom=15&layer=traffic' target='_blank'>";
details += event['latitude'] + ", " + event['longitude'];
details += "</a></p>";
details += "</div>";
// Create Event
let te = new TransitEvent(title, details);
// Add Event to Feed
feed = _addEventToFeed(feed, te, event);
}
}
// Return the Feed
return feed;
}
/**
* Add the Transit Event to the Transit Feed
* @param {TransitFeed} feed Transit Feed
* @param {TransitEvent} transitEvent Transit Event
* @param {Object} eventProps NYSTA Event Properties
* @returns {TransitFeed}
* @private
*/
function _addEventToFeed(feed, transitEvent, eventProps) {
// Find matching region
let regionCode = eventProps['region'];
for ( let j = 0; j < feed.divisions.length; j++ ) {
// Region Matches....
if ( feed.divisions[j].code === regionCode ) {
// Find matching highway
let route = eventProps['route'];
let routeCode = route.indexOf('-') > -1 ? route.substr(route.lastIndexOf('-')) : route;
let direction = eventProps['direction'];
let directionCode = "";
if ( direction.toLowerCase().indexOf("north") > -1 ) {
directionCode = "N";
}
else if ( direction.toLowerCase().indexOf("east") > -1 ) {
directionCode = "E";
}
else if ( direction.toLowerCase().indexOf("south") > -1 ) {
directionCode = "S";
}
else if ( direction.toLowerCase().indexOf("west") > -1 ) {
directionCode = "W";
}
// Parse the region's highways
for ( let k = 0; k < feed.divisions[j].divisions.length; k++ ) {
let name = feed.divisions[j].divisions[k].name;
let nameCode = name.indexOf('-') > -1 ? name.substr(name.lastIndexOf("-")) : name;
let dir = name.substr(name.lastIndexOf('('));
// Highway Division Matches...
if ( nameCode.toLowerCase().indexOf(routeCode.toLowerCase()) > -1 ) {
if ( dir.indexOf(directionCode) !== -1 ) {
// Set Status
let current = feed.divisions[j].divisions[k].status;
let status = eventProps['category'].toUpperCase();
if ( !current || current === "No Alerts" || current === "ROADWORK" ) {
feed.divisions[j].divisions[k].status = status;
}
// Add Event to Line
feed.divisions[j].divisions[k].events.push(transitEvent);
}
}
}
}
}
// Return the Feed
return feed;
}
/**
* Download the MTA Status Feed
* @param callback Callback function(body)
* @private
*/
function _download(callback) {
// Make the get request
https.get(CONFIG.url, function(response) {
let body = "";
response.on("data", function(data) {
body += data;
});
response.on("end", function() {
body = body.toString();
return callback(body);
});
}).on('error', function(err) {
console.error(err);
return callback();
});
}
/**
* Convert a string to Title Case
* @param {String} str String to convert
* @returns {String}
*/
function _title(str) {
return str.toLowerCase().split(' ').map(function(word) {
return word.replace(word[0], word[0].toUpperCase());
}).join(' ');
}
module.exports = loadFeed;