diff --git a/index.html b/index.html index d95fb2e..04b658c 100644 --- a/index.html +++ b/index.html @@ -20,7 +20,7 @@ diff --git a/station.html b/station.html new file mode 100644 index 0000000..9c2b499 --- /dev/null +++ b/station.html @@ -0,0 +1,35 @@ + + + + + + + Station - InfoTren + + + + + + + + + + + +

Station Information

+ +

Station Name

+ + +

Suggestions

+
+ +
+ + + + diff --git a/station.js b/station.js new file mode 100755 index 0000000..9ae78f2 --- /dev/null +++ b/station.js @@ -0,0 +1,211 @@ +var knownStations = [] + +function goToStation(station) { + var url = new URL(window.location.href) + url.pathname = 'view-station.html' + url.searchParams.set('station', station) + url.searchParams.set('date', new Date().toISOString()) + window.location.href = url.toString() +} + +function searchNormalize(str) { + return str + .toLowerCase() + .replace('ă', 'a') + .replace('â', 'a') + .replace('î', 'i') + .replace('ș', 's') + .replace('ț', 't') +} + +var focusedElement = null + +var _rebuildDebounce = null +var _rebuildRequested = false +function rebuildSuggestions() { + if (_rebuildDebounce !== null) { + _rebuildRequested = true + return + } + + _rebuildRequested = false + _rebuildDebounce = 123 + + var suggestionsArea = document.getElementById('suggestionsArea') + while (suggestionsArea.childNodes.length > 0) { + suggestionsArea.childNodes[0].remove() + } + + var stationNameInput = document.getElementById('stationName') + var stationName = searchNormalize(stationNameInput.value.trim()) + + var suggestions = [] + if (!stationName) { + suggestions = knownStations.slice() + } + else { + for (var i = 0; i < knownStations.length; i++) { + if (!searchNormalize(knownStations[i].name).includes(stationName)) { + continue + } + suggestions.push(knownStations[i]) + } + suggestions.sort((s1, s2) => { + var s1n = searchNormalize(s1.name); + var s2n = searchNormalize(s2.name); + + if (s1n.indexOf(stationName) != s2n.indexOf(stationName)) { + return s1n.indexOf(stationName) - s2n.indexOf(stationName); + } + + if (s1.stoppedAtBy.length != s2.stoppedAtBy.length) { + return s2.stoppedAtBy.length - s1.stoppedAtBy.length; + } + + return s1.name.localeCompare(s2.name); + }) + } + + var foundInput = false + suggestions.forEach(function (suggestion, index) { + if (stationName == searchNormalize(suggestion.name)) { + foundInput = true + } + var suggestionLi = document.createElement('li') + suggestionsArea.appendChild(suggestionLi) + + setTimeout(function () { + suggestionLi.classList.add('items') + suggestionLi.tabIndex = index + 1 + suggestionLi.style.padding = '2px 0' + + function onAction(e) { + goToStation(suggestion.name) + } + suggestionLi.addEventListener('click', onAction) + suggestionLi.addEventListener('keypress', function (e) { + if (e.key == 'Enter') { + onAction(e) + } + }) + suggestionLi.addEventListener('focus', function (e) { + focusedElement = suggestionLi + }) + + var stationNameP = document.createElement('p') + suggestionLi.appendChild(stationNameP) + + stationNameP.textContent = suggestion.name + stationNameP.classList.add('pri', 'stationName') + + // var trainCompanyP = document.createElement('p') + // suggestionLi.appendChild(trainCompanyP) + + // trainCompanyP.textContent = suggestion.company + // trainCompanyP.classList.add('thi') + }, 0) + }) + if (!foundInput && stationName) { + var suggestionLi = document.createElement('li') + suggestionsArea.appendChild(suggestionLi) + + suggestionLi.classList.add('items') + suggestionLi.tabIndex = suggestions.length + 2 + suggestionLi.style.padding = '2px 0' + + function onAction(e) { + goToStation(stationNameInput.value.trim()) + } + suggestionLi.addEventListener('click', onAction) + suggestionLi.addEventListener('keypress', function (e) { + if (e.key == 'Enter') { + onAction(e) + } + }) + suggestionLi.addEventListener('focus', function (e) { + focusedElement = suggestionLi + }) + + var stationNameP = document.createElement('p') + suggestionLi.appendChild(stationNameP) + + stationNameP.textContent = stationNameInput.value.trim() + stationNameP.classList.add('pri', 'stationName') + } + + setTimeout(function () { + _rebuildDebounce = null + if (_rebuildRequested) { + rebuildSuggestions() + } + }, 500) +} + +function lsk() { + document.getElementById('stationName').focus() +} + +function csk() { + if (focusedElement == null) { + return + } + + if (focusedElement.id === 'stationName') { + goToTrain(document.activeElement.value.trim()) + } + else { + focusedElement.click() + } +} + +window.addEventListener('load', function (e) { + var stationName = document.getElementById('stationName') + stationName.addEventListener('input', function (e) { + rebuildSuggestions() + }) + stationName.addEventListener('focus', function (e) { + focusedElement = stationName + document.getElementsByClassName('lsk')[0].textContent = '' + document.getElementsByClassName('csk')[0].textContent = 'Search' + }) + stationName.addEventListener('blur', function (e) { + document.getElementsByClassName('lsk')[0].textContent = 'Search' + document.getElementsByClassName('csk')[0].textContent = 'Select' + }) + stationName.addEventListener('keypress', function (e) { + if (e.key == 'Enter') { + goToStation(stationName.value.trim()) + } + }) + + document.querySelectorAll('.lsk').forEach(function (lskElem) { + lskElem.addEventListener('click', function (e) { + lsk() + }) + }) + document.querySelectorAll('.csk').forEach(function (cskElem) { + cskElem.addEventListener('click', function (e) { + csk() + }) + }) + document.body.addEventListener('keydown', function (e) { + if (e.key == 'SoftLeft') { + lsk() + } + else if (e.key == 'Enter') { + csk() + } + }) + + fetch('https://scraper.infotren.dcdev.ro/v3/stations') + .then(function (response) { + return response.json() + }) + .then(function (response) { + knownStations = response + knownStations.sort(function(a, b) { return b.stoppedAtBy.length - a.stoppedAtBy.length }) + }) + .then(function () { + rebuildSuggestions() + }) +}) diff --git a/sw.js b/sw.js index e64df91..9a75023 100755 --- a/sw.js +++ b/sw.js @@ -16,6 +16,7 @@ self.addEventListener('install', (event) => { '/common/worker.js', '/common/items.js', '/common/back.js', + '/common/tabs.js', // Base '/base.css', @@ -32,6 +33,13 @@ self.addEventListener('install', (event) => { '/view-train.js', '/view-train.css', + '/station.html', + '/station.js', + + '/view-station.html', + '/view-station.js', + '/view-station.css', + // API API_TRAINS, API_STATIONS, diff --git a/view-station.css b/view-station.css new file mode 100644 index 0000000..0bc7d83 --- /dev/null +++ b/view-station.css @@ -0,0 +1,115 @@ +.IR, .IRN { + color: red !important; +} + +.early { + color: green !important; +} + +.late { + color: red !important; +} + +#-date { + display: flex; + justify-content: space-between; +} + +#date { + text-align: end; +} + +#tabs-arr { + border-bottom-color: #55ff55; +} + +#tabs-dep { + border-bottom-color: #5555ff; +} + +#arrivals .train-item { + background-color: #fafffa; +} + +#arrivals .train-item:nth-of-type(even) { + background-color: #eaffea; +} + +#departures .train-item { + background-color: #fafaff; +} + +#departures .train-item:nth-of-type(even) { + background-color: #eaeaff; +} + +.train-item.cancelled { + background-color: #ffeaea !important; +} + +.train-item.cancelled + .train-item.cancelled { + border-top: 1px solid black; +} + +.train-item { + display: grid; + grid-template-columns: 30px 60px auto 1fr auto; + grid-template-rows: auto; + grid-template-areas: + "rank train time terminus platform" + "rank train delay terminus platform" + "status status status status status"; + align-items: center; + padding: 4px 0; + + page-break-inside: avoid; + break-inside: avoid; +} + +.train-item > * { + margin: 2px; +} + +.train-item .time { + grid-area: time; + min-width: 60px; + text-align: center; +} + +.train-item .train { + grid-area: train; +} + +.train-item .rank { + grid-area: rank; + white-space: nowrap; +} + +.train-item .delay { + grid-area: delay; + text-align: center; +} + +.train-item .terminus { + grid-area: terminus; +} + +.train-item .status { + grid-area: status; + text-align: center; + margin: 0; +} + +.train-item .platform { + grid-area: platform; + border: 1px solid black; + padding: 1px; + margin: 1px; + border-radius: 5px; + aspect-ratio: 1 / 1; + min-width: 22px; + + display: flex; + justify-content: center; + align-items: center; +} \ No newline at end of file diff --git a/view-station.html b/view-station.html new file mode 100644 index 0000000..7355415 --- /dev/null +++ b/view-station.html @@ -0,0 +1,44 @@ + + + + + + + View Station - InfoTren + + + + + + + + + + + + + +

View Station

+ + +
+

Arrivals

+

Departures

+
+ +
+
+ +
+
+ + + + diff --git a/view-station.js b/view-station.js new file mode 100644 index 0000000..c387956 --- /dev/null +++ b/view-station.js @@ -0,0 +1,199 @@ +var station +var date + +var stationData = null +var lastSuccessfulFetch = null + +function onStationData(data) { + if (!data) { + return + } + + var title = document.getElementById('title') + title.textContent = data.stationName + + document.getElementById('date').textContent = data.date + + /** + * @param {HTMLElement} elem + * @param {any[]} trains + */ + function addTrains(elem, trains) { + while (elem.childNodes.length > 0) { + elem.childNodes[0].remove() + } + + var trainsList = document.createElement('ul') + elem.appendChild(trainsList) + + trains.forEach(function (train, tIdx) { + var trainItem = document.createElement('li') + trainsList.appendChild(trainItem) + trainItem.classList.add('train-item') + if (train.status && train.status.cancelled) { + trainItem.classList.add('cancelled') + } + + var timeDiv = document.createElement('p') + trainItem.appendChild(timeDiv) + timeDiv.classList.add('pri', 'time') + timeDiv.textContent = new Date(train.time).toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' }) + + if (train.status && train.status.delay != 0) { + var delayDiv = document.createElement('p') + trainItem.appendChild(delayDiv) + delayDiv.classList.add('thi', 'delay') + delayDiv.textContent = `${train.status.delay} min ` + // delayDiv.appendChild(document.createElement('br')) + var descSpan = document.createElement('span') + delayDiv.appendChild(descSpan) + if (train.status.delay > 0) { + descSpan.classList.add('late') + descSpan.textContent = 'late' + } + else { + descSpan.classList.add('early') + descSpan.textContent = 'early' + } + } + + var rankDiv = document.createElement('p') + trainItem.appendChild(rankDiv) + rankDiv.textContent = train.train.rank + rankDiv.classList.add('sec', 'rank', train.train.rank) + + var trainDiv = document.createElement('p') + trainItem.appendChild(trainDiv) + trainDiv.classList.add('pri', 'train') + trainDiv.appendChild(document.createTextNode(`${train.train.number}`)) + + var terminusDiv = document.createElement('p') + trainItem.appendChild(terminusDiv) + terminusDiv.classList.add('pri', 'terminus') + terminusDiv.textContent = train.train.terminus + + if (train.status && train.status.platform) { + var platformDiv = document.createElement('div') + trainItem.appendChild(platformDiv) + platformDiv.classList.add('thi', 'platform') + platformDiv.textContent = train.status.platform + } + + if (train.status && train.status.cancelled) { + var statusDiv = document.createElement('p') + trainItem.appendChild(statusDiv) + statusDiv.classList.add('sec', 'status') + statusDiv.textContent = 'This train is cancelled' + } + }) + } + addTrains(document.getElementById('arrivals'), data.arrivals) + addTrains(document.getElementById('departures'), data.departures) +} + +var refreshStopToken = null +function refresh() { + function reschedule(timeout) { + if (refreshStopToken != null) { + clearTimeout(refreshStopToken) + } + refreshStopToken = setTimeout(function () { + refresh() + }, timeout || 90000) + } + return fetch( + `https://scraper.infotren.dcdev.ro/v3/stations/${station}?date=${date.getFullYear().toString()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`, + { + cache: 'no-store', + }, + ).then(function (response) { + if (!response.ok) { + // Check in 10 seconds if server returned error + reschedule(10000) + return + } + return response.json() + }).then(function (response) { + if (!response) { + return + } + stationData = response + onStationData(response) + reschedule() + }).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() { + refresh() +} + +window.addEventListener('load', function (e) { + if (!new URL(window.location.href).searchParams.has('station')) { + window.history.back() + this.setTimeout(function () { + var url = new URL(window.location.href) + url.pathname = 'station.html' + window.location.href = url.toString() + }, 100) + } + + var sp = new URL(window.location.href).searchParams + + station = sp.get('station') + date = sp.has('date') ? new Date(sp.get('date')) : new Date() + + // View departures first + selectedTab = 1 + selectTab(selectedTab) + + document.querySelectorAll('.rsk').forEach(function (rskElem) { + rskElem.addEventListener('click', function (e) { + rsk() + }) + }) + + 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) +})