diff --git a/index.html b/index.html
index c6b0134..a9f8e6a 100644
--- a/index.html
+++ b/index.html
@@ -20,7 +20,7 @@
- - Train routes
+ - Train routes
- My train
- Station departures/arrivals
- About
diff --git a/route.css b/route.css
new file mode 100644
index 0000000..5fa6c3e
--- /dev/null
+++ b/route.css
@@ -0,0 +1,60 @@
+.itinerary-train {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-template-rows: repeat(auto-fit, auto);
+}
+
+.itinerary-train:not(:last-child) {
+ grid-template-areas:
+ "dep-time dep-station"
+ "train train"
+ "arr-time arrdep-station"
+ "dep2-time arrdep-station";
+}
+
+.itinerary-train:last-child {
+ grid-template-areas:
+ "train train"
+ "arr-time arr-station";
+}
+
+.itinerary-train:only-child {
+ grid-template-areas:
+ "dep-time dep-station"
+ "train train"
+ "arr-time arr-station";
+}
+
+.itinerary-train .departure.time {
+ grid-area: dep-time;
+}
+
+.itinerary-train .next-departure.time {
+ grid-area: dep2-time;
+}
+
+.itinerary-train .departure.station {
+ grid-area: dep-station;
+}
+
+.itinerary-train .train {
+ grid-area: train;
+}
+
+.itinerary-train .arrival.time {
+ grid-area: arr-time;
+}
+
+.itinerary-train:not(:last-child) .arrival.station {
+ grid-area: arrdep-station;
+ align-self: center;
+}
+
+.itinerary-train .arrival.station {
+ grid-area: arr-station;
+}
+
+.itinerary-train .time {
+ margin-left: 2px;
+ margin-right: 2px;
+}
diff --git a/route.html b/route.html
new file mode 100644
index 0000000..f2e4a8b
--- /dev/null
+++ b/route.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+ Route - InfoTren
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Find Route
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/route.js b/route.js
new file mode 100644
index 0000000..0069b43
--- /dev/null
+++ b/route.js
@@ -0,0 +1,466 @@
+/**
+ * @type {string | null}
+ */
+var fromStation = null
+/**
+ * @type {string | null}
+ */
+var toStation = null
+/**
+ * @type {Date | null}
+ */
+var departureDate = null
+
+/**
+ * @type {{ name: string, stoppedAtBy: string[] }[]}
+ */
+var knownStations = []
+
+function goToStation(station) {
+ var url = new URL(window.location.href)
+ if (!fromStation) {
+ url.searchParams.set('from', station)
+ }
+ else if (!toStation) {
+ url.searchParams.set('to', station)
+ }
+ // url.searchParams.set('date', new Date().toISOString())
+ window.location.href = url.toString()
+}
+
+function setDepartureDate(departureDate) {
+ var url = new URL(window.location.href)
+ url.searchParams.set('departureDate', departureDate.toISOString())
+ window.location.href = url.toString()
+}
+
+function searchNormalize(str) {
+ return str
+ .toLowerCase()
+ .replaceAll('ă', 'a')
+ .replaceAll('â', 'a')
+ .replaceAll('î', 'i')
+ .replaceAll('ș', 's')
+ .replaceAll('ț', '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)
+}
+
+/**
+ * @typedef ItineraryTrain
+ * @property {string} from
+ * @property {string} to
+ * @property {string[]} intermediateStops
+ * @property {string} departureDate
+ * @property {string} arrivalDate
+ * @property {number} km
+ * @property {string} operator
+ * @property {string} trainRank
+ * @property {string} trainNumber
+ */
+/**
+ * @typedef Itinerary
+ * @property {ItineraryTrain[]} trains
+ */
+/**
+ * @param {Itinerary[]} data
+ */
+function onItineraries(data) {
+ var contentDiv = document.createElement('div')
+ document.body.insertBefore(contentDiv, document.querySelector('footer'))
+ contentDiv.classList.add('content')
+
+ for (var i = 0; i < data.length; i++) {
+ var itineraryDiv = document.createElement('div')
+ contentDiv.appendChild(itineraryDiv)
+
+ var heading = document.createElement('h4')
+ itineraryDiv.appendChild(heading)
+ heading.textContent = `Itinerary ${i + 1}`
+
+ var trainsDiv = document.createElement('div')
+ itineraryDiv.appendChild(trainsDiv)
+ trainsDiv.classList.add('itinerary-trains')
+
+ data[i].trains.forEach(function (train, idx) {
+ var last = idx === data[i].trains.length - 1
+
+ var trainDiv = document.createElement('div')
+ trainsDiv.appendChild(trainDiv)
+ trainDiv.classList.add('itinerary-train')
+
+ if (idx === 0) {
+ var departureTimeP = document.createElement('p')
+ trainDiv.appendChild(departureTimeP)
+ departureTimeP.classList.add('sec', 'departure', 'time')
+ var departureTimePre = document.createElement('pre')
+ departureTimeP.appendChild(departureTimePre)
+ var departure = new Date(train.departureDate)
+ departureTimePre.textContent = departure.toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' })
+
+ var departureHeading = document.createElement('h3')
+ trainDiv.appendChild(departureHeading)
+ departureHeading.classList.add('departure', 'station')
+ departureHeading.textContent = train.from
+ }
+
+ var trainP = document.createElement('p')
+ trainDiv.appendChild(trainP)
+ trainP.classList.add('pri', 'train')
+ trainIdSpan(train.trainRank, train.trainNumber, trainP)
+
+ var arrivalTimeP = document.createElement('p')
+ trainDiv.appendChild(arrivalTimeP)
+ arrivalTimeP.classList.add('sec', 'arrival', 'time')
+ var arrivalTimePre = document.createElement('pre')
+ arrivalTimeP.appendChild(arrivalTimePre)
+ var arrival = new Date(train.arrivalDate)
+ arrivalTimePre.textContent = arrival.toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' })
+
+ var arrivalHeading = document.createElement('h3')
+ trainDiv.appendChild(arrivalHeading)
+ arrivalHeading.classList.add('arrival', 'station')
+ arrivalHeading.textContent = train.to
+
+ if (!last) {
+ var nextDepartureTimeP = document.createElement('p')
+ trainDiv.appendChild(nextDepartureTimeP)
+ nextDepartureTimeP.classList.add('sec', 'next-departure', 'time')
+ var departureTimePre = document.createElement('pre')
+ nextDepartureTimeP.appendChild(departureTimePre)
+ var departure = new Date(data[i].trains[idx + 1].departureDate)
+ departureTimePre.textContent = departure.toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' })
+ }
+ })
+ }
+}
+
+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 sp = new URL(window.location.href).searchParams
+ fromStation = sp.get('from')
+ toStation = sp.get('to')
+ var departureDateStr = sp.get('departureDate')
+ if (departureDateStr) {
+ departureDate = new Date(departureDateStr)
+ }
+
+ var titleH1 = document.querySelector("header > h1")
+ if (!fromStation) {
+ titleH1.textContent = 'Find Route - From'
+ }
+ else if (!toStation) {
+ titleH1.textContent = 'Find Route - To'
+ }
+ else if (!departureDate) {
+ titleH1.textContent = 'Find Route - Departure Date'
+ }
+ else {
+ titleH1.textContent = `${fromStation} - ${toStation}`
+ }
+
+ var footer = document.querySelector('footer')
+
+ if (!fromStation || !toStation) {
+ // Build station selection UI
+ var stationNameH4 = document.createElement('h4')
+ document.body.insertBefore(stationNameH4, footer)
+ var stationNameLabel = document.createElement('label')
+ stationNameH4.appendChild(stationNameLabel)
+ stationNameLabel.htmlFor = 'stationName'
+ stationNameLabel.textContent = 'Station Name'
+
+ var stationNameInput = document.createElement('input')
+ document.body.insertBefore(stationNameInput, footer)
+ stationNameInput.type = 'search'
+ stationNameInput.classList.add('items')
+ stationNameInput.name = 'stationName'
+ stationNameInput.id = 'stationName'
+
+ var suggestionsH4 = document.createElement('h4')
+ document.body.insertBefore(suggestionsH4, footer)
+ suggestionsH4.textContent = 'Suggestions'
+
+ var contentDiv = document.createElement('div')
+ document.body.insertBefore(contentDiv, footer)
+ contentDiv.classList.add('content')
+ var suggestionsUl = document.createElement('ul')
+ contentDiv.appendChild(suggestionsUl)
+ suggestionsUl.id = 'suggestionsArea'
+
+ document.querySelector('.csk').textContent = 'Search'
+
+ 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())
+ }
+ })
+
+ fetch('https://scraper.infotren.dcdev.ro/v3/stations')
+ .then(function (response) {
+ return response.json()
+ })
+ .then(function (response) {
+ knownStations = response
+ knownStations = knownStations.filter((s) => ![fromStation, toStation].includes(s.name))
+ knownStations.sort(function(a, b) { return b.stoppedAtBy.length - a.stoppedAtBy.length })
+ })
+ .then(function () {
+ rebuildSuggestions()
+ })
+ }
+ else if (!departureDate) {
+ var departureDateH4 = document.createElement('h4')
+ document.body.insertBefore(departureDateH4, footer)
+ departureDateH4.textContent = 'Departure Date'
+
+ var contentDiv = document.createElement('div')
+ document.body.insertBefore(contentDiv, footer)
+ contentDiv.classList.add('content')
+ var departureDateUl = document.createElement('ul')
+ contentDiv.appendChild(departureDateUl)
+ departureDateUl.id = 'suggestionsArea'
+
+ for (var i = 0, departureOption = new Date(); i < 30; i++, departureOption.setDate(departureOption.getDate() + 1)) {
+ var suggestionLi = document.createElement('li')
+ departureDateUl.appendChild(suggestionLi)
+ suggestionLi.classList.add('items')
+ suggestionLi.tabIndex = i + 10
+ // Capture
+ ;(function () {
+ var d = new Date(departureOption.getTime())
+ function onAction() {
+ setDepartureDate(d)
+ }
+ suggestionLi.addEventListener('click', onAction)
+ suggestionLi.addEventListener('keypress', function (e) {
+ if (e.key == 'Enter') {
+ onAction(e)
+ }
+ })
+ suggestionLi.addEventListener('focus', function (e) {
+ focusedElement = suggestionLi
+ })
+ })()
+ var innerP = document.createElement('p')
+ suggestionLi.appendChild(innerP)
+ innerP.classList.add('pri')
+ var innerPre = document.createElement('pre')
+ innerP.appendChild(innerPre)
+ innerPre.textContent = `${departureOption.getDate().toString().padStart(2, '0')}.${(departureOption.getMonth() + 1).toString().padStart(2, '0')}.${departureOption.getFullYear().toString().padStart(4, '0')}`
+ }
+
+ document.querySelector('.csk').textContent = 'Select'
+ }
+ else {
+ var contentDiv = document.createElement('div')
+ document.body.insertBefore(contentDiv, footer)
+ contentDiv.classList.add('content')
+ contentDiv.style.display = 'flex'
+ contentDiv.style.flexDirection = 'column'
+ contentDiv.style.alignItems = 'center'
+ contentDiv.style.justifyContent = 'center'
+
+ var loadingP = document.createElement('p')
+ contentDiv.appendChild(loadingP)
+ loadingP.classList.add('pri')
+ loadingP.textContent = 'Loading data...'
+
+
+ var url = new URL('https://scraper.infotren.dcdev.ro/v3/itineraries')
+ url.searchParams.set('from', fromStation)
+ url.searchParams.set('to', toStation)
+ url.searchParams.set('date', departureDate.toISOString())
+ fetch(url.toString())
+ .then(function (response) {
+ return response.json()
+ })
+ .then(function (data) {
+ contentDiv.remove()
+ onItineraries(data)
+ })
+ .catch(function (e) {
+ loadingP.textContent = 'An error has occured'
+
+ var errorP = document.createElement('p')
+ contentDiv.appendChild(errorP)
+ errorP.classList.add('sec')
+ errorP.textContent = e.toString()
+
+ var retryLink = document.createElement('a')
+ contentDiv.appendChild(retryLink)
+ retryLink.classList.add('items')
+ retryLink.textContent = 'Retry'
+ retryLink.href = ''
+ })
+ }
+
+ 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()
+ }
+ })
+})
diff --git a/sw.js b/sw.js
index d4993e1..85417eb 100755
--- a/sw.js
+++ b/sw.js
@@ -1,7 +1,8 @@
-const VERSION = 'v24'
+const VERSION = 'v25'
const API_ORIGIN = 'https://scraper.infotren.dcdev.ro/'
const API_TRAINS = `${API_ORIGIN}v3/trains`
const API_STATIONS = `${API_ORIGIN}v3/stations`
+const API_ITINERARIES = `${API_ORIGIN}v3/itineraries`
const CACHE_FIRST = [
// Root
@@ -41,9 +42,14 @@ const CACHE_FIRST = [
'/view-station.js',
'/view-station.css',
+ '/route.html',
+ '/route.js',
+ '/route.css',
+
// API
API_TRAINS,
API_STATIONS,
+ API_ITINERARIES,
];
/**