/** * @type {string} */ var trainNumber /** * @type {Date} */ var date var groupIndex = null var yesterday = false var showKm = false var trainData = null /** * @type {?Date} */ var lastSuccessfulFetch = null /** * @typedef ArrDep * @property {string} scheduleTime * @property {?{delay: number, real: boolean}} status * * @typedef Note * @property {string} kind * * @typedef DepartsAsNote * @type {Note} * @property {"departsAs"} kind * @property {string} rank * @property {string} number * * @typedef TrainNumberChangeNote * @type {Note} * @property {"trainNumberChange"} kind * @property {string} rank * @property {string} number * * @typedef DetachingWagonsNote * @type {Note} * @property {"detachingWagons"} kind * @property {string} station * * @typedef ReceivingWagonsNote * @type {Note} * @property {"receivingWagons"} kind * @property {string} station * * @typedef TrainStop * @property {string} name * @property {number} km * @property {?number} stoppingTime * @property {?string} platform * @property {ArrDep} arrival * @property {ArrDep} departure * @property {Note[]} notes * * @typedef Group * @property {{from: string; to: string}} route * @property {{delay: number; station: string; state: "passing" | "arrival" | "departure"} | undefined} status * @property {TrainStop[]} stations */ /** * @param {{ rank: string; number: string; operator: string; date: string; groups: Group[]; }} data * @param {?Date} fetchDate */ function onTrainData(data, fetchDate) { var title = document.getElementById('title') title.textContent = '' title.appendChild(document.createTextNode('Train ')) trainIdSpan(data.rank, data.number, title) document.getElementsByTagName('title')[0].textContent = `Train ${data.rank} ${data.number}` document.getElementById('company').textContent = data.operator var dateHref = document.createElement('a') var dateP = document.getElementById('date') while (dateP.childNodes.length > 0) { dateP.childNodes[0].remove() } dateP.appendChild(dateHref) dateHref.textContent = data.date dateHref.href = '#' dateHref.classList.add('no-a-custom') dateHref.classList.add('items', 'no-a-custom') dateHref.addEventListener('click', function (e) { e.preventDefault() // Implement date switcher yesterday = !yesterday refresh() }) document.getElementById('loading').classList.add('hidden') /** * @type {Group | null} */ var group = null; if (data.groups.length > 1 && groupIndex == null) { document.getElementById('group-choice').classList.remove('hidden') document.getElementById('group-choice').focus() document.getElementById('train-info').classList.add('hidden') document.getElementsByClassName('rsk')[0].textContent = '' document.getElementsByClassName('csk')[0].textContent = 'Select' title.textContent = 'Select Group for ' trainIdSpan(data.rank, data.number, title) var gc = document.getElementById('group-choice') while (gc.childNodes.length > 0) { gc.childNodes[0].remove() } for (var i = 0; i < data.groups.length; i++) { var g = data.groups[i] var groupLi = document.createElement('li') gc.append(groupLi) groupLi.tabIndex = i groupLi.classList.add('items') if (i === currentIndex) { groupLi.focus() } (function (i) { function onAction(e) { var url = new URL(window.location.toString()) groupIndex = i url.searchParams.append('groupIndex', groupIndex) window.history.pushState({'groupIndex': groupIndex}, '', url.toString( )) onTrainData(data) } groupLi.addEventListener('click', onAction) groupLi.addEventListener('keypress', function (e) { if (e.key == 'Enter') { onAction(e) } }) })(i) var routeP = document.createElement('p') groupLi.append(routeP) routeP.classList.add('pri') routeP.textContent = `${g.route.from} ➔ ${g.route.to}` var groupP = document.createElement('p') groupLi.append(groupP) groupP.classList.add('thi') groupP.textContent = i === 0 ? 'Main train' : `Group ${i}` } return } else if (data.groups.length === 1) { group = data.groups[0] } else { group = data.groups[groupIndex] } document.getElementById('group-choice').classList.add('hidden') document.getElementById('train-info').classList.remove('hidden') document.getElementById('train-info').focus() document.getElementsByClassName('rsk')[0].textContent = 'Refresh' document.getElementsByClassName('csk')[0].textContent = '' document.getElementById('route-from').textContent = group.route.from document.getElementById('route-to').textContent = group.route.to if (group.status) { document.getElementById('status').classList.remove('hidden') var statusDelay = document.getElementById('status-delay') while (statusDelay.childNodes.length > 0) { statusDelay.childNodes[0].remove() } var delayString = '' var delayMinutes = group.status.delay if (delayMinutes === 0) { delayString = 'On time' statusDelay.appendChild(document.createTextNode(delayString)) } else { var early = false if (delayMinutes < 0) { early = true delayMinutes = -delayMinutes } if (delayMinutes >= 60) { var hours = Math.floor(delayMinutes / 60) delayMinutes = delayMinutes % 60 delayString += hours.toString() delayString += ' hour' if (hours > 1) { delayString += 's' } } if (delayMinutes > 0) { if (delayString.length > 0) { delayString += ' and ' } delayString += delayMinutes.toString() delayString += ' minute' if (delayMinutes > 1) { delayString += 's' } } delayString += ' ' statusDelay.appendChild(document.createTextNode(delayString)) var kindSpan = document.createElement('span') statusDelay.appendChild(kindSpan) if (early) { kindSpan.textContent = 'early' kindSpan.classList.add('early') } else { kindSpan.textContent = 'late' kindSpan.classList.add('late') } } var statusLocation = document.getElementById('status-location') while (statusLocation.childNodes.length > 0) { statusLocation.childNodes[0].remove() } var stateString = '' if (group.status.state === 'arrival') { stateString += 'when arriving at ' } else if (group.status.state === 'departure') { stateString += 'when departing from ' } else if (group.status.state === 'passing') { stateString += 'while passing through ' } statusLocation.appendChild(document.createTextNode(stateString)) var stationSpan = document.createElement('span') statusLocation.appendChild(stationSpan) stationSpan.textContent = group.status.station stationSpan.classList.add('station') } else { document.getElementById('status').classList.add('hidden') } var stationsDiv = document.getElementById('stations') while (stationsDiv.childNodes.length > 0) { stationsDiv.childNodes[0].remove() } var separator = document.createElement('h4') stationsDiv.appendChild(separator) separator.textContent = 'Stations' var stationsList = document.createElement('ul') stationsDiv.appendChild(stationsList) group.stations.forEach(function (station) { var stationItem = document.createElement('li') stationsList.appendChild(stationItem) stationItem.classList.add('stationItem') var stationName = document.createElement('p') stationItem.appendChild(stationName) stationName.classList.add('pri', 'name') var stationNameHref = document.createElement('a') stationName.appendChild(stationNameHref) stationNameHref.textContent = station.name stationNameHref.classList.add('items', 'no-a-custom') var stationUrl = new URL('/view-station.html', window.location.origin) stationUrl.searchParams.append('station', station.name) stationUrl.searchParams.append('date', (station.arrival || station.departure).scheduleTime) stationNameHref.href = stationUrl.toString() if (station.arrival) { var stationArrival = document.createElement('div') stationItem.appendChild(stationArrival) stationArrival.classList.add('arrival') var originalArr = document.createElement('p') stationArrival.appendChild(originalArr) var originalArrSpan = document.createElement('pre') originalArr.appendChild(originalArrSpan) var arrDate = new Date(station.arrival.scheduleTime) originalArrSpan.textContent = arrDate.toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' }) originalArr.classList.add('pri') if (station.arrival.status && station.arrival.status.delay != 0) { originalArr.classList.remove('pri') originalArr.classList.add('thi') originalArrSpan.classList.add('original') var delaySpanArr = document.createElement('span') originalArr.appendChild(delaySpanArr) delaySpanArr.textContent = `${station.arrival.status.delay > 0 ? '+' : ''}${station.arrival.status.delay}`; delaySpanArr.classList.add(station.arrival.status.delay > 0 ? 'late' : 'early') delaySpanArr.style.marginLeft = '4px' var actualArr = document.createElement('p') stationArrival.appendChild(actualArr) arrDate.setMinutes(arrDate.getMinutes() + station.arrival.status.delay) actualArr.classList.add('pri', station.arrival.status.delay > 0 ? 'late' : 'early') if (!station.arrival.status.real) { actualArr.classList.add('not-real') } var actualArrPre = document.createElement('pre') actualArr.appendChild(actualArrPre) actualArrPre.textContent = arrDate.toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' }) } } if (station.departure) { var stationDeparture = document.createElement('div') stationItem.appendChild(stationDeparture) stationDeparture.classList.add('departure') var originalDep = document.createElement('p') stationDeparture.appendChild(originalDep) var depDate = new Date(station.departure.scheduleTime) var originalDepSpan = document.createElement('pre') originalDep.appendChild(originalDepSpan) originalDepSpan.textContent = depDate.toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' }) originalDep.classList.add('pri') if (station.departure.status && station.departure.status.delay != 0) { originalDep.classList.remove('pri') originalDep.classList.add('thi') originalDepSpan.classList.add('original') var delaySpanDep = document.createElement('span') originalDep.appendChild(delaySpanDep) delaySpanDep.textContent = `${station.departure.status.delay > 0 ? '+' : ''}${station.departure.status.delay}`; delaySpanDep.classList.add(station.departure.status.delay > 0 ? 'late' : 'early') delaySpanDep.style.marginLeft = '4px' var actualDep = document.createElement('p') stationDeparture.appendChild(actualDep) depDate.setMinutes(depDate.getMinutes() + station.departure.status.delay) actualDep.classList.add('pri', station.departure.status.delay > 0 ? 'late' : 'early') if (!station.departure.status.real) { actualDep.classList.add('not-real') } var actualDepPre = document.createElement('pre') actualDep.appendChild(actualDepPre) actualDepPre.textContent = depDate.toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' }) } } var stationKm = document.createElement('p') stationItem.appendChild(stationKm) stationKm.textContent = `${station.km} km` stationKm.classList.add('thi', 'km') if (!showKm) { stationKm.classList.add('hidden') } if (station.platform) { var stationPlatform = document.createElement('p') stationItem.appendChild(stationPlatform) stationPlatform.textContent = `platform ${station.platform}` stationPlatform.classList.add('thi', 'platform') } if (station.notes && station.notes.length > 0) { var stationNotes = document.createElement('div') stationItem.appendChild(stationNotes) stationNotes.classList.add('notes') station.notes.forEach(function (note) { var noteP = document.createElement('p') stationNotes.appendChild(noteP) noteP.classList.add('note', 'thi') switch (note.kind) { case 'departsAs': { noteP.textContent = 'Train departs as ' trainIdSpan(note.rank, note.number, noteP) break } case 'detachingWagons': { noteP.textContent = `Detaching wagons to ${note.station}` break } case 'receivingWagons': { noteP.textContent = `Receiving wagons from ${note.station}` break } case 'trainNumberChange': { noteP.textContent = 'Train changes number to ' trainIdSpan(note.rank, note.number, noteP) break } } }) } }) lastSuccessfulFetch = fetchDate || new Date() } var refreshStopToken = null /** * @returns {Promise} */ function refresh() { function reschedule(timeout) { if (refreshStopToken != null) { clearTimeout(refreshStopToken) } refreshStopToken = setTimeout(function () { refresh() }, timeout || 60000) } /** * @type {Date} */ var reqDate = new Date(date.valueOf()) if (yesterday) { reqDate.setDate(reqDate.getDate() - 1) } reqDate.setMinutes(0, 0, 0) return fetch( `https://scraper.infotren.dcdev.ro/v3/trains/${trainNumber}?date=${reqDate.toISOString()}`, { cache: 'no-store', }, ).then(function (response) { if (!response.ok) { // Check in 10 seconds if server returned error reschedule(10000) return } var cacheDate = response.headers.get('SW-Cached-At') return response.json().then(function (data) { data['$cacheDate'] = cacheDate return data }) }).then(function (response) { if (!response) { return } var cacheDate = response['$cacheDate'] if (cacheDate) { cacheDate = new Date(cacheDate) } var success = !cacheDate trainData = response onTrainData(response, cacheDate) // Check in 1 seconds if network error reschedule(success ? undefined : 1000) return success }).catch(function (e) { // Check in 1 second if network error reschedule(1000) throw e }) } window.addEventListener('unload', function (e) { if (refreshStopToken != null) { clearTimeout(refreshStopToken) } }) function rsk() { function changeRskText(newText) { document.querySelectorAll('.rsk').forEach(function (elem) { elem.textContent = newText }) } changeRskText('Refreshing...') refresh().catch(function () { return false}).then(function (success) { if (!success) { changeRskText('Refreshing failed') setTimeout(function (_) { changeRskText('Refresh') }, 3000) } else { changeRskText('Refresh') } }) } window.addEventListener('popstate', function (e) { groupIndex = null if (trainData) { onTrainData(trainData) } else { refresh() } }) window.addEventListener('load', function (e) { if (!new URL(window.location.href).searchParams.has('train')) { window.history.back() this.setTimeout(function () { var url = new URL(window.location.href) url.pathname = 'train.html' window.location.href = url.toString() }, 100) } var sp = new URL(window.location.href).searchParams trainNumber = sp.get('train') date = sp.has('date') ? new Date(sp.get('date')) : new Date() groupIndex = sp.has('groupIndex') ? parseInt(sp.get('groupIndex')) : null document.querySelectorAll('.rsk').forEach(function (rskElem) { rskElem.addEventListener('click', function (e) { rsk() }) }) if (navigator.canShare && navigator.canShare({ url: '' })) { document.getElementById('title').addEventListener('click', function () { navigator.share({ url: '' }); }) } var content = document.getElementsByClassName('content')[0] content.focus() content.addEventListener('keydown', function (e) { switch (e.key) { // case 'ArrowUp': // content.scrollBy(0, -50) // break // case 'ArrowDown': // content.scrollBy(0, 50) // break case 'SoftRight': rsk() break case '1': date.setDate(date.getDate() - 1) refresh() break case '3': date.setDate(date.getDate() + 1) refresh() break case '7': showKm = !showKm document.querySelectorAll('.km').forEach(function (kmItem) { if (showKm) { kmItem.classList.remove('hidden') } else { kmItem.classList.add('hidden') } }) break default: console.log(e.key) } }) refresh() setInterval(function () { if (!lastSuccessfulFetch) { return } var millis = new Date() - lastSuccessfulFetch var secs = Math.floor(millis / 1000) var timeStr = '' if (secs / 3600 >= 1) { timeStr += `${Math.floor(secs / 3600)}h` secs = secs % 3600 } if (secs / 60 >= 1) { timeStr += `${Math.floor(secs / 60)}m` secs = secs % 60 } if (secs >= 1) { timeStr += `${Math.floor(secs)}s` } if (!timeStr) { document.querySelectorAll('.lsk').forEach(function (elem) { elem.textContent = 'Last refreshed now' elem.classList.add('last-refreshed') }) } else { document.querySelectorAll('.lsk').forEach(function (elem) { elem.textContent = `Last refreshed ${timeStr} ago` elem.classList.add('last-refreshed') }) } }, 500) if (this.localStorage && !this.localStorage.getItem('info-yesterday')) { this.alert("New feature: You can now view yesterday's train by selecting the date!") this.localStorage.setItem('info-yesterday', 'true') } })