Last active
January 25, 2025 00:44
-
-
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
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
<!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> |
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
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(); |
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
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