Skip to content

Instantly share code, notes, and snippets.

@primaryobjects
Last active January 25, 2025 00:44
Show Gist options
  • Save primaryobjects/b7cb14a0ca304237c999a9834d35f86f to your computer and use it in GitHub Desktop.
Save primaryobjects/b7cb14a0ca304237c999a9834d35f86f to your computer and use it in GitHub Desktop.
Parse NJ/Philadelphia PATCO train timetable schedule and highlight next upcoming stop https://jsbin.com/wukokay/edit?js,output https://output.jsbin.com/wukokay
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Train Schedule</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.10.377/pdf.min.js"></script>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="train-stops"></div>
</body>
</html>
async function getTrainSchedule() {
const today = new Date().toLocaleDateString('en-CA'); // Format: YYYY-MM-DD
const schedulePageUrl = 'https://www.ridepatco.org/schedules/schedules.asp';
const timetablePageUrl = 'https://www.ridepatco.org/schedules/default_schedule.pdf';
const corsProxy = 'https://cors-proxy.fringe.zone/';
try {
// Fetch the schedule page HTML using a CORS proxy
const response = await fetch(corsProxy + schedulePageUrl);
const html = await response.text();
const { defaultScheduleUrl, specialScheduleUrl } = parseHTML(html, today);
// Determine which schedule to use
const scheduleUrl = (specialScheduleUrl || defaultScheduleUrl).replace(/https:\/\/(null|output)\.jsbin\.com/, 'https://www.ridepatco.org');
if (scheduleUrl) {
const scheduleResponse = await fetch(corsProxy + scheduleUrl);
const arrayBuffer = await scheduleResponse.arrayBuffer();
const pdfData = new Uint8Array(arrayBuffer);
const scheduleData = await extractTextFromPDF(pdfData);
// Process the schedule data
const { westboundTimes, eastboundTimes, locustTimes, eightTimes } = parseSchedule(scheduleData);
displayTrainStops(westboundTimes, eastboundTimes, locustTimes, eightTimes, schedulePageUrl, scheduleUrl);
} else {
console.error('No schedule found for today.');
}
} catch (error) {
console.error(`Error fetching train schedule ${error}`);
}
}
function parseHTML(html, today) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const timetableLink = doc.querySelector('a[href*="PATCO_Timetable"]');
const defaultScheduleUrl = timetableLink ? timetableLink.href.replace(/https:\/\/(null|output)\.jsbin\.com/, 'https://www.ridepatco.org') : null;
const specialScheduleLinks = Array.from(doc.querySelectorAll('a[href*="TW_"]'));
const links = specialScheduleLinks.find(link => link.href.includes(today));
const specialScheduleUrl = links.href.replace(/https:\/\/(null|output)\.jsbin\.com/, 'https://www.ridepatco.org');
return { defaultScheduleUrl, specialScheduleUrl };
}
async function extractTextFromPDF(pdfData) {
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
let text = '';
for (let i = 0; i < pdf.numPages; i++) {
const page = await pdf.getPage(i + 1);
const textContent = await page.getTextContent().then(function (textContent) {
var textItems = textContent.items;
var finalString = "";
var line = 0;
// Concatenate the string of the item to the final string
for (var i = 0; i < textItems.length; i++) {
if (line != textItems[i].transform[5]) {
if (line != 0) {
finalString += '\r\n';
}
line = textItems[i].transform[5];
}
var item = textItems[i];
finalString += item.str;
}
text += finalString;
});
}
return text;
}
function parseSchedule(data) {
const westboundTimes = [];
const eastboundTimes = [];
const locustTimes = [];
const eightTimes = [];
const lines = data.split('\n');
let isWestbound = false;
let isEastbound = false;
lines.forEach(line => {
if (line.includes('EASTBOUND')) {
isWestbound = true;
isEastbound = false;
} else if (line.startsWith('LINDENWOLD')) {
isEastbound = true;
isWestbound = false;
}
if (isWestbound) {
const times = line.match(/\d{1,2}:\d{2} [AP]/g);
if (times && times.length >= 6) {
westboundTimes.push(times[5]);
}
}
if (isEastbound) {
const times = line.match(/\d{1,2}:\d{2} [AP]/g);
if (times && times.length >= 4) {
eightTimes.push(times[3]);
}
if (times && times.length >= 2) {
eastboundTimes.push(times[1]);
}
if (times && times.length >= 1) {
locustTimes.push(times[0]);
}
}
});
return { westboundTimes, eastboundTimes, locustTimes, eightTimes };
}
function displayTrainStops(westboundTimes, eastboundTimes, locustTimes, eightTimes, schedulePageUrl, timetablePageUrl) {
const container = document.getElementById('train-stops');
container.innerHTML = '';
const navBar = document.createElement('div');
navBar.className = 'nav-bar';
navBar.style.display = 'flex';
navBar.style.justifyContent = 'space-between';
navBar.style.backgroundColor = '#333';
navBar.style.padding = '10px';
const scheduleLink = document.createElement('a');
scheduleLink.href = schedulePageUrl;
scheduleLink.textContent = 'Source Schedule';
scheduleLink.target = '_blank';
scheduleLink.style.color = 'white';
scheduleLink.style.textDecoration = 'none';
navBar.appendChild(scheduleLink);
const timetableLink = document.createElement('a');
timetableLink.href = timetablePageUrl;
timetableLink.textContent = 'PATCO Timetable';
timetableLink.target = '_blank';
timetableLink.style.color = 'white';
timetableLink.style.textDecoration = 'none';
navBar.appendChild(timetableLink);
container.appendChild(navBar);
const currentTime = new Date();
const westboundSection = createTrainSection('COLLINGSWOOD - Westbound to Philadelphia', westboundTimes, currentTime);
container.appendChild(westboundSection);
const eightSection = createTrainSection('8TH & MARKET - Eastbound to Lindenwold', eightTimes, currentTime);
container.appendChild(eightSection);
const eastboundSection = createTrainSection('12/13TH & LOCUST - Eastbound to Lindenwold', eastboundTimes, currentTime);
container.appendChild(eastboundSection);
const locustSection = createTrainSection('15/16TH & LOCUST - Eastbound to Lindenwold', locustTimes, currentTime);
container.appendChild(locustSection);
}
function createTrainSection(headerText, times, currentTime) {
const section = document.createElement('div');
const header = document.createElement('h3');
header.className = 'section-header';
header.textContent = headerText;
section.appendChild(header);
let highlighted = false;
let visibleTimes = [];
let hiddenTimes = [];
times.forEach(time => {
const timeElement = document.createElement('div');
timeElement.textContent = time;
timeElement.classList.add('time');
const timeDate = convertToTimeDate(time);
if (timeDate > currentTime && !highlighted) {
timeElement.classList.add('highlight');
highlighted = true;
}
if (timeDate > currentTime) {
if (visibleTimes.length < 5) {
visibleTimes.push(timeElement);
} else {
hiddenTimes.push(timeElement);
}
}
});
visibleTimes.forEach(timeElement => section.appendChild(timeElement));
if (hiddenTimes.length > 0) {
const hiddenTimesDiv = document.createElement('div');
hiddenTimesDiv.style.display = 'none';
hiddenTimes.forEach(timeElement => hiddenTimesDiv.appendChild(timeElement));
section.appendChild(hiddenTimesDiv);
const showAllLink = document.createElement('a');
showAllLink.href = '#';
showAllLink.textContent = 'Show All';
showAllLink.style.color = 'blue';
showAllLink.style.cursor = 'pointer';
showAllLink.addEventListener('click', () => {
event.preventDefault();
hiddenTimesDiv.style.display = 'block';
showAllLink.style.display = 'none';
showLessLink.style.display = 'block';
});
section.appendChild(showAllLink);
const showLessLink = document.createElement('a');
showLessLink.href = '#';
showLessLink.textContent = 'Show Less';
showLessLink.style.color = 'blue';
showLessLink.style.cursor = 'pointer';
showLessLink.style.display = 'none';
showLessLink.addEventListener('click', () => {
event.preventDefault();
hiddenTimesDiv.style.display = 'none';
showAllLink.style.display = 'block';
showLessLink.style.display = 'none';
});
section.appendChild(showLessLink);
}
return section;
}
function convertToTimeDate(time) {
const [hours, minutes, period] = time.split(/[: ]/);
const timeDate = new Date();
timeDate.setHours(period === 'P' && hours !== '12' ? parseInt(hours) + 12 : (period === 'A' && hours === '12' ? 0 : parseInt(hours)));
timeDate.setMinutes(parseInt(minutes));
timeDate.setSeconds(0);
timeDate.setMilliseconds(0);
return timeDate;
}
// Call the function to get the train schedule
getTrainSchedule();
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
}
#train-stops {
max-width: 800px;
margin: 20px auto;
padding: 10px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h3 {
margin-top: 0;
}
.nav-bar {
display: flex;
justify-content: space-between;
background-color: #333;
padding: 10px;
}
.nav-bar a {
color: white;
text-decoration: none;
}
.section-header {
background-color: #007bff;
color: white;
padding: 10px;
margin: 10px 0;
border-radius: 5px;
}
.highlight {
background-color: green;
color: white;
}
@media (max-width: 600px) {
.nav-bar {
flex-direction: column;
align-items: center;
}
.nav-bar a {
margin-bottom: 10px;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment