Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
208 lines (187 sloc)
8.74 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use strict"; | |
| const walkTimeFromAptDoorToPathSec = 10 * 60; | |
| const walkTimeFromAptDoorToFerrySec = 9.5 * 60; | |
| const maxLeaveSecToShowOption = 90 * 60; | |
| const METHOD_SCHEDULE = 'SCHEDULE'; | |
| const METHOD_API = 'API'; | |
| // Given a Luxon DateTime and a string like '8:30 AM', return a Luxon DateTime with the same date as | |
| // the input DateTime but with the time modified to be the time expressed by the string. | |
| function dateTimeWithModifiedTime(dateTime, timeStr) { | |
| let [time, ampm] = timeStr.split(' '); | |
| let [hourStr, minute] = time.split(':'); | |
| let hour = parseInt(hourStr); | |
| if (ampm === 'AM' && hour == 12) { | |
| hour = 0; | |
| } | |
| if (ampm === 'PM' && hour != 12) { | |
| hour += 12; | |
| } | |
| var obj = dateTime.toObject(); | |
| obj.hour = hour; | |
| obj.minute = minute; | |
| obj.second = 0; | |
| obj.millisecond = 0; | |
| return luxon.DateTime.fromObject(obj); | |
| } | |
| // Take a list of departure times, expressed as strings like "8:30 AM", and get the number of seconds | |
| // from a given Date to those times. Times are interpreted as the next instance of that time, in the | |
| // local timezone, that is not before the given Date. | |
| function getSecUntilDepartures(date, departureTimes) { | |
| var DateTime = luxon.DateTime; | |
| var Duration = luxon.Duration; | |
| const datetime = DateTime.fromJSDate(date); | |
| return departureTimes.map(timeAmPm => { | |
| var departure = dateTimeWithModifiedTime(datetime, timeAmPm); | |
| if (departure < datetime) { | |
| departure += Duration.fromObject({days: 1}); | |
| } | |
| var ret = (departure - datetime) / 1000; | |
| return ret; | |
| }); | |
| } | |
| // Get number of seconds until next departure. Takes a list of departure strings, expressed as strings | |
| // "8:30 AM" and a Date. | |
| // | |
| // The return value is the number of seconds until the first departure time that is not before `date`. | |
| function getSecUntilNextDeparture(date, departureTimes) { | |
| return Math.min(...getSecUntilDepartures(date, departureTimes)); | |
| } | |
| // Get number of seconds until next departure. Takes a whole-week schedule and a Date. | |
| // | |
| // A whole week schedule is a list of 7 lists of strings. The first sublist represents Monday, the | |
| // second represents Tuesday, etc. Each sublist has a list of strings like "8:30 AM" (i.e. the same) | |
| // format that getSecUntilNextDeparture expects). | |
| // | |
| // The return value is the number of seconds until the first departure time that is not before `date`. | |
| function getSecUntilNextDepartureFromWeekSchedule(date, schedule) { | |
| var DateTime = luxon.DateTime; | |
| var Duration = luxon.Duration; | |
| const dateTime = DateTime.fromJSDate(date); | |
| for (let dayDelta = 0; dayDelta < 8; dayDelta += 1) { | |
| const futureDateTime = dateTime.plus(Duration.fromObject({days: dayDelta})); | |
| const daySchedule = schedule[futureDateTime.weekday - 1]; | |
| for (const timeAmPm of daySchedule) { | |
| const departure = dateTimeWithModifiedTime(futureDateTime, timeAmPm); | |
| if (departure >= dateTime) { | |
| return (departure - dateTime) / 1000; | |
| } | |
| } | |
| } | |
| console.log('There were no future departures in the entire week schedule'); | |
| return Number.NaN; | |
| } | |
| function getLeaveSecFromSchedule(date, departureTimes, walkTimeSec) { | |
| let queryDate = new Date(date * 1 + walkTimeSec * 1000); | |
| let leaveSec = getSecUntilNextDeparture(queryDate, departureTimes); | |
| return Promise.resolve({leaveSec: leaveSec, method: METHOD_SCHEDULE}); | |
| } | |
| function getLeaveSecFromWeekSchedule(date, weekSchedule, walkTimeSec) { | |
| let queryDate = new Date(date * 1 + walkTimeSec * 1000); | |
| let leaveSec = getSecUntilNextDepartureFromWeekSchedule(queryDate, weekSchedule); | |
| return Promise.resolve({leaveSec: leaveSec, method: METHOD_SCHEDULE}); | |
| } | |
| const pathApiUrl = 'https://path.api.razza.dev/v1/stations/hoboken/realtime'; | |
| // Get the number of seconds until all the upcoming PATH departures for a given list of routes and a direction. | |
| // | |
| // The valid values for routes and direction are defined at https://github.com/mrazza/path-data | |
| // | |
| // Returns a Promise holding an array of Numbers. | |
| function getSecsUntilNextPathDeparturesFromApi(routes, direction) { | |
| return fetch(pathApiUrl).then(resp => { | |
| if (!resp.ok) { | |
| throw new Error(`HTTP error fetching PATH API URL: ${response.status}`); | |
| } | |
| return resp.body.getReader().read(); | |
| }).then(dataArr => { | |
| var data = JSON.parse(new TextDecoder().decode(dataArr.value)); | |
| var trains = data.upcomingTrains.filter( | |
| train => routes.includes(train.route) && train.direction == direction | |
| ); | |
| return trains.map(train => (Date.parse(train.projectedArrival) - new Date()) / 1000); | |
| }); | |
| } | |
| // Returns a promise with the number of seconds until you must leave to catch the next PATH train, | |
| // using the API at https://path.api.razza.dev/ | |
| function getPathLeaveSecFromApi(routes, direction, walkTimeSec) { | |
| return getSecsUntilNextPathDeparturesFromApi(routes, direction).then(secs => { | |
| for (const sec of secs) { | |
| const leaveSec = sec - walkTimeSec; | |
| if (leaveSec >= 0) { | |
| return {leaveSec: leaveSec, method: METHOD_API}; | |
| } else { | |
| console.log('Skipping PATH API option because not enough time until departure: ' + sec + ' seconds'); | |
| } | |
| } | |
| return {leaveSec: null, method: METHOD_API}; | |
| }); | |
| } | |
| // This function uses the API and falls back to the schedule if the API is unavailable | |
| // (e.g. because there is no train far enough in the future, or the API is down). | |
| function getLeaveSecFromBothApiAndWeekSchedule(routes, direction, date, weekSchedule, walkTimeSec) { | |
| let apiPromise = getPathLeaveSecFromApi(routes, direction, walkTimeSec); | |
| let schedPromise = getLeaveSecFromWeekSchedule(date, weekSchedule, walkTimeSec); | |
| return Promise.allSettled([apiPromise, schedPromise]).then(([apiResult, schedResult]) => { | |
| if (apiResult.status == 'fulfilled' && apiResult.value.leaveSec != null) { | |
| return Promise.resolve(apiResult.value); | |
| } else if (apiResult.status != 'fulfilled') { | |
| console.log('PATH API request failed: ' + JSON.stringify(apiResult)); | |
| } | |
| return Promise.resolve(schedResult.value); | |
| }); | |
| } | |
| function getPathTo33rdLeaveSec() { | |
| return getLeaveSecFromBothApiAndWeekSchedule(['HOB_33', 'JSQ_33_HOB'], 'TO_NY', new Date(), pathHobokenTo33rdWeekSchedule, walkTimeFromAptDoorToPathSec); | |
| } | |
| function getWtcPathLeaveSec() { | |
| return getLeaveSecFromBothApiAndWeekSchedule(['HOB_WTC'], 'TO_NY', new Date(), pathHobokenToWtcWeekSchedule, walkTimeFromAptDoorToPathSec); | |
| } | |
| function getBrookfieldFerryLeaveSec() { | |
| return getLeaveSecFromWeekSchedule(new Date(), hobokenToBrookfieldFerryWeekSchedule, walkTimeFromAptDoorToFerrySec); | |
| } | |
| function methodAbbrev(method) { | |
| return method.substr(0, 3); | |
| } | |
| // Display remaining minutes before you need to leave in order to catch a given transit option. | |
| // | |
| // Runs the function to compute the remaining time (returned as a Promise), displays that value | |
| // in the given div, then calls setTimeout to run the update again when the value changes. | |
| function displayLeaveMinUpdateLoop(rowId, leaveSecFunc) { | |
| const secPerMin = 60; // Can set this to 1 to see the value update every second for testing. | |
| leaveSecFunc().then(({leaveSec, method}) => { | |
| let row = document.getElementById(rowId); | |
| if (leaveSec < maxLeaveSecToShowOption) { | |
| row.style.display = ''; | |
| } else { | |
| row.style.display = 'none'; | |
| } | |
| let leaveMin = Math.floor(leaveSec / secPerMin); | |
| // This is probably poor style. | |
| row.querySelector('.leave-in-min').innerHTML = leaveMin; | |
| let methodDiv = row.querySelector('.method'); | |
| if (methodDiv !== null) { | |
| methodDiv.innerHTML = methodAbbrev(method); | |
| } | |
| // + .1 is a little hack to make sure that the minute has definitely rolled over by the time we get there. | |
| setTimeout(() => displayLeaveMinUpdateLoop(rowId, leaveSecFunc), | |
| (((leaveSec % secPerMin) + secPerMin) % secPerMin + .1) * 1000); | |
| }); | |
| } | |
| function addFullscreenButton() { | |
| var button = document.createElement('button'); | |
| button.innerHTML = 'Click to go fullscreen'; | |
| document.body.appendChild(button); | |
| button.onclick = el => { | |
| document.querySelector('body').requestFullscreen().catch(err => { | |
| alert('Unable to fullscreen: ' + err.message); | |
| }); | |
| button.style['display'] = 'none'; | |
| }; | |
| } | |
| // Used for demos. | |
| function getRandomLeaveSec(maxMin) { | |
| return () => Promise.resolve(maxMin * 60 * Math.random()); | |
| } | |
| displayLeaveMinUpdateLoop('ferry-to-brookfield-row', getBrookfieldFerryLeaveSec); | |
| displayLeaveMinUpdateLoop('path-to-wtc-row', getWtcPathLeaveSec); | |
| displayLeaveMinUpdateLoop('path-to-33rd-row', getPathTo33rdLeaveSec); | |
| addFullscreenButton(); |