diff --git a/about.html b/about.html index 4d3d7dc..62c2ae6 100644 --- a/about.html +++ b/about.html @@ -4,9 +4,10 @@ - About - InfoTren + About - InfoDTrain + @@ -24,8 +25,8 @@
-

InfoTren

-

KaiOS webapp for Informatica Feroviară scraper

+

InfoDTrain

+

Webapp for Deutsche Bahn API

Acknowledgements

diff --git a/base.css b/base.css index 67286f3..50275ca 100644 --- a/base.css +++ b/base.css @@ -73,6 +73,13 @@ footer .rsk { } header { + background-color: white; + + display: flex; + align-items: center; +} + +header.embedded { position: fixed; left: env(titlebar-area-x, 0); top: env(titlebar-area-y, 0); @@ -81,19 +88,6 @@ header { -webkit-app-region: drag; app-region: drag; - background-color: white; - - display: flex; - align-items: center; -} - -.header-placeholder { - box-sizing: border-box; - height: max(env(titlebar-area-height, 36px), 42px); - margin-bottom: 2px; -} - -header.embedded { background-color: #0000ff; color: white; } @@ -108,6 +102,14 @@ header.embedded { @media (display-mode: window-controls-overlay) { header { + position: fixed; + left: env(titlebar-area-x, 0); + top: env(titlebar-area-y, 0); + width: env(titlebar-area-width, 100%); + min-height: env(titlebar-area-height, 36px); + -webkit-app-region: drag; + app-region: drag; + background-color: #0000ff; color: white; } @@ -128,8 +130,13 @@ header.embedded { } .header-placeholder { + box-sizing: border-box; + height: max(env(titlebar-area-height, 36px), 42px); + margin-bottom: 2px; + background-color: #0000ff; } + } header .left, header .right { @@ -206,7 +213,7 @@ p.sec { margin: 0 8px; - color: gray; + color: grey; } p.thi { @@ -344,14 +351,26 @@ pre { border-bottom-right-radius: 5%; } -.IR, .IRN { - color: #ff0000 !important; +.product-suburban { + color: green !important; +} + +.product-national, .product-nationalExpress { + font-style: italic; + font-weight: 500; } -.IC { - color: #00aa00 !important; +.product-bus { + color: purple !important; } +.product-subway { + color: blue !important; +} + +.product-tram { + color: lightcoral !important; +} @media print { footer, .no-print { diff --git a/base.dark.css b/base.dark.css new file mode 100644 index 0000000..d9aab4e --- /dev/null +++ b/base.dark.css @@ -0,0 +1,49 @@ +@media (prefers-color-scheme: dark) { + + body { + color: white; + background-color: black; + } + + header:not(.embedded) { + background-color: black; + } + + footer { + background-color: #303030; + } + + h4 { + color: #a0a0a0; + background-color: #0f0f0f; + } + + p.sec, p.thi { + color: lightgrey; + } + + li.items:not(.disabled):hover:not(:focus), a:not(.disabled):hover:not(:focus) { + background-color: #0000bb; + color: white; + } + + a:not(.no-a-custom):not(.no-custom-a):not(:focus):not(:hover) { + color: white; + } + + .back { + filter: invert(1); + } + + .product-suburban { + color: #33ff33 !important; + } + + .product-bus { + color: #dd33dd !important; + } + + .product-subway { + color: #33aaff !important; + } +} diff --git a/common/components.js b/common/components.js new file mode 100644 index 0000000..5ba3360 --- /dev/null +++ b/common/components.js @@ -0,0 +1,88 @@ +// Adapted from: https://github.com/tsoding/grecha.js/blob/master/grecha.js + +function tag(name) { + var result = document.createElement(name); + for (var i = 1; i < arguments.length; i++) { + var child = arguments[i]; + if (child instanceof Node) { + result.appendChild(child) + } else { + result.appendChild(document.createTextNode(child ? child.toString() : '')) + } + } + + result.att$ = function(name, value) { + this.setAttribute(name, value); + return this; + }; + + result.value$ = function(value) { + this.value = value; + return this; + } + + result.checked$ = function(value) { + this.checked = value; + return this; + } + + result.id$ = function(name) { + this.id = name; + return this; + }; + + result.class$ = function(name) { + this.classList.add(name); + return this; + }; + + result.event$ = function(eventName, handler) { + this.addEventListener(eventName, handler); + return this; + }; + + result.onclick$ = function(callback) { + this.onclick = callback; + return this; + }; + + result.also$ = function(callback) { + callback(this); + return this; + }; + + result.let$ = function(callback) { + return callback(this); + }; + + return result; +} + +var MUNDANE_TAGS = ["canvas", "h1", "h2", "h3", "h4", "p", "a", "div", "span", "select", "label", "hr"]; +for (var i = 0; i < MUNDANE_TAGS.length; i++) { + (function (tagName) { + window[tagName] = function() { + var args = [tagName]; + for (var j = 0; j < arguments.length; j++) { + args.push(arguments[j]); + } + return tag.apply(null, args); + } + })(MUNDANE_TAGS[i]); +} + +function a(href) { + var args = ["a"]; + for (var i = 1; i < arguments.length; i++) { + args.push(arguments[i]); + } + return tag.apply(null, args).att$("href", href); +} + +function img(src) { + return tag("img").att$("src", src); +} + +function input(type) { + return tag("input").att$("type", type); +} diff --git a/config-route.css b/config-route.css new file mode 100644 index 0000000..b12e9eb --- /dev/null +++ b/config-route.css @@ -0,0 +1,146 @@ +.itinerary-train { + display: grid; + grid-template-columns: auto 1fr auto; + grid-template-rows: repeat(auto-fit, auto); + align-items: center; +} + +.itinerary-train:not(:last-child) { + grid-template-areas: + "dep-time dep-station dep-platform" + "train train train" + "arr-time arrdep-station arr-platform" + "dep2-time arrdep-station dep2-platform"; +} + +.itinerary-train:last-child { + grid-template-areas: + "train train train" + "arr-time arr-station arr-platform"; +} + +.itinerary-train:only-child { + grid-template-areas: + "dep-time dep-station dep-platform" + "train train train" + "arr-time arr-station arr-platform"; +} + +.itinerary-train .departure.time { + grid-area: dep-time; +} + +.itinerary-train .departure.platform { + grid-area: dep-platform; +} + +.itinerary-train .next-departure.time { + grid-area: dep2-time; +} + +.itinerary-train .next-departure.platform { + grid-area: dep2-platform; +} + +.itinerary-train .departure.station { + grid-area: dep-station; +} + +.itinerary-train .train { + grid-area: train; +} + +.itinerary-train .arrival.time { + grid-area: arr-time; +} + +.itinerary-train .arrival.platform { + grid-area: arr-platform; +} + +.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; + align-self: center; +} + +.itinerary-train .platform { + margin: 2px; + padding: 2px; + border: 1px solid black; + border-radius: 4px; + justify-self: end; + min-width: 16px; + text-align: center; +} + +.itinerary-train .platform.changed { + color: red; + border-color: red; +} + +.train .company { + font-size: 0.8em; + font-style: italic; +} + +.walking { + font-size: 0.95em; + font-style: italic; +} + +input#time { + margin: 1px; +} + +div.checkbox { + display: flex; + flex-direction: row; + align-items: center; +} + +div.checkbox p { + flex: 1; +} + +div.checkbox input { + flex: 0; +} + +.suggestion { + display: flex; + flex-direction: row; + align-items: center; +} + +.suggestion :first-child { + flex: 1; +} + +.suggestion .star { + flex: 0; + pointer-events: none; +} + +.suggestion .star.checked { + filter: invert(90%) sepia(49%) saturate(704%) hue-rotate(359deg) brightness(94%) contrast(99%); +} + +@media (prefers-color-scheme: dark) { + .suggestion .star { + filter: invert(100%); + } + + .suggestion .star.checked { + filter: invert(86%) sepia(79%) saturate(2126%) hue-rotate(357deg) brightness(108%) contrast(104%); + } +} diff --git a/config-route.html b/config-route.html new file mode 100644 index 0000000..a3fdfe1 --- /dev/null +++ b/config-route.html @@ -0,0 +1,49 @@ + + + + + + + + Route - InfoDTrain + + + + + + + + + + + + + + + + +
+
+ Back +
+

Find Route

+
+
+
+ + + + + + + + + +
+
+ +
+
+
+ + \ No newline at end of file diff --git a/config-route.js b/config-route.js new file mode 100644 index 0000000..3b7c12a --- /dev/null +++ b/config-route.js @@ -0,0 +1,674 @@ +/** + * @type {string | null} + */ +var fromStation = null +/** + * @type {string | null} + */ +var toStation = null +/** + * @type {Date | null} + */ +var departureDate = null +var transitKind = { + ice: true, + ic: true, + re: true, + rb: true, + s: true, + bus: true, + ferry: true, + u: true, + tram: true, +} +var starred = [] + +/** + * @type {{id: string, name: string}[]} + */ +var knownStations = [] + +var itineraries = null + +function goToStation(stationId) { + var url = new URL(window.location.href) + if (!fromStation) { + url.searchParams.set('from', stationId) + } + else if (!toStation) { + url.searchParams.set('to', stationId) + } + // 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 suggestions = knownStations.slice() + if (suggestions.length === 0) { + suggestions = starred.map(function (s) { return JSON.parse(s) }) + } + + suggestions.forEach(function (suggestion, index) { + var suggestionDiv = document.createElement('div') + suggestionsArea.appendChild(suggestionDiv) + suggestionDiv.classList.add('suggestion') + + var suggestionLi = document.createElement('li') + suggestionDiv.appendChild(suggestionLi) + + setTimeout(function () { + suggestionLi.classList.add('items') + suggestionLi.tabIndex = index + 1 + suggestionLi.style.padding = '2px 0' + + function onAction(e) { + goToStation(JSON.stringify(suggestion)) + } + 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 || suggestion.address + stationNameP.classList.add('pri', 'stationName') + + if (window.localStorage) { + var suggestionLink = document.createElement('a') + suggestionDiv.appendChild(suggestionLink) + suggestionLink.classList.add('no-custom-a') + + var suggestionStar = document.createElement('object') + suggestionLink.appendChild(suggestionStar) + suggestionStar.classList.add('star') + suggestionStar.type = 'image/svg+xml' + function setStar() { + if (starred.includes(JSON.stringify(suggestion))) { + suggestionStar.data = '/icons/star_full.svg' + suggestionStar.classList.add('checked') + } + else { + suggestionStar.data = '/icons/star_empty.svg' + suggestionStar.classList.remove('checked') + } + } + suggestionLink.addEventListener('click', function (event) { + event.preventDefault() + if (starred.includes(JSON.stringify(suggestion))) { + starred = starred.filter(function (s) { + return s !== suggestion + }) + } else { + starred.push(JSON.stringify(suggestion)) + } + setStar() + localStorage.setItem('stations/starred', JSON.stringify(starred)) + }) + setStar() + } + + // var trainCompanyP = document.createElement('p') + // suggestionLi.appendChild(trainCompanyP) + + // trainCompanyP.textContent = suggestion.company + // trainCompanyP.classList.add('thi') + }, 0) + }) + + setTimeout(function () { + _rebuildDebounce = null + if (_rebuildRequested) { + rebuildSuggestions() + } + }, 500) +} + +var fetchAbortController = new AbortController() +function reloadSuggestions() { + var stationNameInput = document.getElementById('stationName') + var stationName = searchNormalize(stationNameInput.value.trim()) + + var locationsUrl = new URL('https://v6.db.transport.rest/locations') + locationsUrl.searchParams.set('query', stationName) + locationsUrl.searchParams.set('limit', '25') + locationsUrl.searchParams.set('fuzzy', 'true') + locationsUrl.searchParams.set('stops', 'true') + locationsUrl.searchParams.set('addresses', 'true') + locationsUrl.searchParams.set('poi', 'true') + + fetchAbortController.abort() + fetchAbortController = new AbortController() + fetch(locationsUrl.toString(), { signal: fetchAbortController.signal }) + .then(function (response) { + if (response.ok) { + return response.json() + } + else { + return {} + } + }) + .then(function (data) { + if (data) { + knownStations = Object.values(data) + rebuildSuggestions() + } + }) +} + +/** + * @typedef DbJourney + * @prop {'journey'} type + * @prop {(DbTrip & DbArrDep & {tripId: string})[]} legs + * @prop {string} refreshToken + * @prop {DbRemark[]} remarks + */ + +/** + * @param {{journeys: DbJourney[]}} 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.journeys.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.journeys[i].legs.forEach(function (train, idx) { + var last = idx === data.journeys[i].legs.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.plannedDeparture) + departureTimePre.textContent = departure.toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' }) + + var departureHeading = document.createElement('h3') + trainDiv.appendChild(departureHeading) + departureHeading.classList.add('departure', 'station') + if (train.origin.type === 'stop' || train.origin.type === 'station') { + var departureLink = document.createElement('a') + departureHeading.appendChild(departureLink) + departureLink.textContent = train.origin.name + departureLink.classList.add('no-custom-a', 'items') + var departureUrl = new URL('/view-station.html', window.location.origin) + departureUrl.searchParams.set('stationId', train.origin.id) + departureLink.href = departureUrl.toString() + } + else { + var departureSpan = document.createElement('span') + departureHeading.append(departureSpan) + departureSpan.innerText = train.origin.name || train.origin.address + } + + if (train.departurePlatform || train.plannedDeparturePlatform) { + var departurePlatformP = document.createElement('p') + trainDiv.append(departurePlatformP) + departurePlatformP.classList.add('sec', 'departure', 'platform') + if (train.departurePlatform && train.departurePlatform != train.plannedDeparturePlatform) { + departurePlatformP.classList.add('changed') + } + departurePlatformP.textContent = `${train.departurePlatform || train.plannedDeparturePlatform}` + } + } + + var trainP = document.createElement('p') + trainDiv.appendChild(trainP) + trainP.classList.add('pri', 'train') + if (!train.walking) { + var trainLink = document.createElement('a') + trainP.appendChild(trainLink) + trainLink.innerText = train.line.name + trainLink.classList.add('no-custom-a', 'items') + var trainUrl = new URL('/view-train.html', window.location.origin) + trainUrl.searchParams.set('tripId', train.tripId) + trainLink.href = trainUrl.toString() + trainP.appendChild(document.createTextNode(' ')) + if (train.line.operator) { + var trainCompany = document.createElement('span') + trainP.appendChild(trainCompany) + trainCompany.textContent = '(' + train.line.operator.name + ')' + trainCompany.classList.add('company') + } + } + else { + var walkingSpan = document.createElement('span') + trainP.append(walkingSpan) + walkingSpan.classList.add('walking') + walkingSpan.innerText = `Walking (${train.distance} m)` + } + + 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.plannedArrival) + arrivalTimePre.textContent = arrival.toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' }) + + var arrivalHeading = document.createElement('h3') + trainDiv.appendChild(arrivalHeading) + arrivalHeading.classList.add('arrival', 'station') + if (train.destination.type === 'stop' || train.destination.type === 'station') { + var arrivalLink = document.createElement('a') + arrivalHeading.appendChild(arrivalLink) + arrivalLink.textContent = train.destination.name + arrivalLink.classList.add('no-custom-a', 'items') + var arrivalUrl = new URL('/view-station.html', window.location.origin) + arrivalUrl.searchParams.set('stationId', train.destination.id) + arrivalLink.href = arrivalUrl.toString() + } + else { + var arrivalSpan = document.createElement('span') + arrivalHeading.append(arrivalSpan) + arrivalSpan.innerText = train.destination.name || train.origin.address + } + + if (train.arrivalPlatform || train.plannedArrivalPlatform) { + var arrivalPlatformP = document.createElement('p') + trainDiv.append(arrivalPlatformP) + arrivalPlatformP.classList.add('sec', 'arrival', 'platform') + if (train.arrivalPlatform && train.arrivalPlatform != train.plannedArrivalPlatform) { + arrivalPlatformP.classList.add('changed') + } + arrivalPlatformP.textContent = `${train.arrivalPlatform || train.plannedArrivalPlatform}` + } + + if (!last) { + var nextTrain = data.journeys[i].legs[idx + 1] + 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(nextTrain.plannedDeparture) + departureTimePre.textContent = departure.toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' }) + + if (nextTrain.departurePlatform || nextTrain.plannedDeparturePlatform) { + var departurePlatformP = document.createElement('p') + trainDiv.append(departurePlatformP) + departurePlatformP.classList.add('sec', 'next-departure', 'platform') + if (nextTrain.departurePlatform && nextTrain.departurePlatform != nextTrain.plannedDeparturePlatform) { + departurePlatformP.classList.add('changed') + } + departurePlatformP.textContent = `${nextTrain.departurePlatform || nextTrain.plannedDeparturePlatform}` + } + } + }) + } +} + +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') + var fromJson = JSON.parse(fromStation || 'null') + toStation = sp.get('to') + var toJson = JSON.parse(toStation || 'null') + 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 = `${fromJson.name || fromJson.address} - ${toJson.name || toJson.address}` + titleH1.textContent = 'Find Route - Configure' + } + + if (window.localStorage) { + var maybeTransitKind = JSON.parse(localStorage.getItem('config-route/transitKind')) + if (maybeTransitKind) { + transitKind = maybeTransitKind + } + + var maybeStarred = JSON.parse(localStorage.getItem('stations/starred')) + if (maybeStarred) { + starred = maybeStarred + } + } + + var footer = document.querySelector('footer') + + if (!fromStation || !toStation) { + // Build station selection UI + document.body.insertBefore( + h4( + label('Station Name').att$('for', 'stationName'), + ), + footer, + ) + // 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' + + document.body.insertBefore( + input('search').id$('stationName').att$('name', 'stationName').class$('items'), + footer, + ) + // var stationNameInput = document.createElement('input') + // document.body.insertBefore(stationNameInput, footer) + // stationNameInput.type = 'search' + // stationNameInput.classList.add('items') + // stationNameInput.name = 'stationName' + // stationNameInput.id = 'stationName' + + document.body.insertBefore(h4('Suggestions'), footer) + // 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' + rebuildSuggestions() + + document.querySelector('.csk').textContent = 'Search' + + var stationName = document.getElementById('stationName') + stationName.addEventListener('input', function (e) { + reloadSuggestions() + }) + 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()) + } + }) + } + 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 = -1, departureOption = (function () { var d = new Date(); d.setDate(d.getDate() - 1); return d })(); 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 + }) + if (i === 0) { + suggestionLi.focus() + } + })() + 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')}` + if (i === 0) { + innerPre.textContent += ' (today)' + } + } + + 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' + + function updateSearchLink() { + var a = document.getElementById('search-link') + var url = new URL(window.location.href) + url.pathname = 'route.html' + url.searchParams.set('departureDate', departureDate.toISOString()) + url.searchParams.set('from', JSON.stringify(fromJson)) + url.searchParams.set('to', JSON.stringify(toJson)) + url.searchParams.set('transitKind', JSON.stringify(transitKind)) + a.href = url.toString() + } + + function transitKindCheckChanged(event) { + transitKind[event.target.id.slice(13)] = event.target.checked + if (window.localStorage) { + localStorage.setItem('config-route/transitKind', JSON.stringify(transitKind)) + } + updateSearchLink() + } + + var contentDiv = div( + h4('Route'), + p('From').class$('thi'), + p(fromJson.name || fromJson.address).class$('pri'), + p('To').class$('thi'), + p(toJson.name || toJson.address).class$('pri'), + // a('', 'Configure via...'), + h4('Date and time'), + p('Date').class$('thi'), + p(departureDate.toDateString()).class$('pri'), + p(label('Time').att$('for', 'time')).class$('thi'), + p( + input('time') + .id$('time') + .att$('value', departureDate.getHours().toString().padStart(2, '0') + ':' + departureDate.getMinutes().toString().padStart(2, '0')) + .event$('input', function(event) { + var text = event.target.value + var splitted = text.toString().split(':') + var h = parseInt(splitted[0]) + var m = parseInt(splitted[1]) + departureDate.setHours(h, m) + updateSearchLink() + }), + ), + h4('Train categories'), + div( + p(label('ICE, RJX, High speed').class$('product-nationalExpress').att$('for', 'transit-kind-ice')), + input('checkbox') + .checked$(transitKind.ice) + .id$('transit-kind-ice') + .class$('transit-kind') + .event$('change', transitKindCheckChanged), + ).class$('checkbox'), + div( + p(label('IC, EC, RJ').class$('product-national').att$('for', 'transit-kind-ic')), + input('checkbox') + .checked$(transitKind.ic) + .id$('transit-kind-ic') + .class$('transit-kind') + .event$('change', transitKindCheckChanged), + ).class$('checkbox'), + div( + p(label('RE, IRE').class$('product-regionalExpress').att$('for', 'transit-kind-re')), + input('checkbox') + .checked$(transitKind.re) + .id$('transit-kind-re') + .class$('transit-kind') + .event$('change', transitKindCheckChanged), + ).class$('checkbox'), + div( + p(label('RB').class$('product-regional').att$('for', 'transit-kind-rb')), + input('checkbox') + .checked$(transitKind.rb) + .id$('transit-kind-rb') + .class$('transit-kind') + .event$('change', transitKindCheckChanged), + ).class$('checkbox'), + div( + p(label('S-Bahn').class$('product-suburban').att$('for', 'transit-kind-s')), + input('checkbox') + .checked$(transitKind.s) + .id$('transit-kind-s') + .class$('transit-kind') + .event$('change', transitKindCheckChanged), + ).class$('checkbox'), + div( + p(label('U-Bahn').class$('product-subway').att$('for', 'transit-kind-u')), + input('checkbox') + .checked$(transitKind.u) + .id$('transit-kind-u') + .class$('transit-kind') + .event$('change', transitKindCheckChanged), + ).class$('checkbox'), + div( + p(label('Tram, Stadtbahn').class$('product-tram').att$('for', 'transit-kind-tram')), + input('checkbox') + .checked$(transitKind.tram) + .id$('transit-kind-tram') + .class$('transit-kind') + .event$('change', transitKindCheckChanged), + ).class$('checkbox'), + div( + p(label('Bus').class$('product-bus').att$('for', 'transit-kind-bus')), + input('checkbox') + .checked$(transitKind.bus) + .id$('transit-kind-bus') + .class$('transit-kind') + .event$('change', transitKindCheckChanged), + ).class$('checkbox'), + div( + p(label('Ferry').class$('product-ferry').att$('for', 'transit-kind-ferry')), + input('checkbox') + .checked$(transitKind.ferry) + .id$('transit-kind-ferry') + .class$('transit-kind') + .event$('change', transitKindCheckChanged), + ).class$('checkbox'), + h4('Start search'), + a('', 'Search').id$('search-link'), + ).class$('content') + document.body.insertBefore(contentDiv, footer) + contentDiv.style.display = 'flex' + contentDiv.style.flexDirection = 'column' + + updateSearchLink() + } + + 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/icons/star_empty.svg b/icons/star_empty.svg new file mode 100644 index 0000000..1736e08 --- /dev/null +++ b/icons/star_empty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/star_full.svg b/icons/star_full.svg new file mode 100644 index 0000000..cb2231e --- /dev/null +++ b/icons/star_full.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/index.html b/index.html index 3cbe8db..e99d883 100644 --- a/index.html +++ b/index.html @@ -4,31 +4,36 @@ - InfoTren + InfoDTrain + + +
-

InfoTren

+

InfoDTrain

diff --git a/index.js b/index.js index 9559327..c966449 100644 --- a/index.js +++ b/index.js @@ -1,59 +1,76 @@ window.addEventListener('load', function (e) { if (window.localStorage) { - var recentViewTrain = localStorage.getItem('recent/view-train') - if (recentViewTrain) { + var recentViewTrainStr = localStorage.getItem('recent/view-train') + if (recentViewTrainStr) { /** - * @property {string} trainNumber + * @type {object} + * @property {?string} trainNumber + * @property {?string} name + * @property {string} tripId * @property {string} date * @property {string} $addDate - * @property {string | undefined} groupIndex */ - recentViewTrain = JSON.parse(recentViewTrain) + var recentViewTrain = JSON.parse(recentViewTrainStr) var addDate = new Date(recentViewTrain.$addDate) - addDate.setHours(addDate.getHours() + 2) // store recents for 2 hours + addDate.setHours(addDate.getHours() + 6) // store recents for 6 hours if (addDate.getTime() > Date.now()) { var recentViewTrainLi = document.createElement('li') var recentViewTrainLink = document.createElement('a') recentViewTrainLi.appendChild(recentViewTrainLink) var recentViewTrainLinkUrl = new URL('/view-train.html', window.location.origin) - recentViewTrainLinkUrl.searchParams.append('train', recentViewTrain.trainNumber) + recentViewTrainLinkUrl.searchParams.append('tripId', recentViewTrain.tripId) recentViewTrainLinkUrl.searchParams.append('date', recentViewTrain.date) - if (recentViewTrain.groupIndex) { - recentViewTrainLinkUrl.searchParams.append('groupIndex', recentViewTrain.groupIndex) - } recentViewTrainLink.href = recentViewTrainLinkUrl.toString() recentViewTrainLink.classList.add('items') - recentViewTrainLink.innerText = `Recent train: ${recentViewTrain.trainNumber}` + recentViewTrainLink.innerText = `Recent train: ${recentViewTrain.name || "..."}` + if (recentViewTrain.trainNumber) { + recentViewTrainLink.innerText = `Recent train: ${recentViewTrain.name || "..."} (${recentViewTrain.trainNumber})` + } - fetch(`https://scraper.infotren.dcdev.ro/v3/trains/${recentViewTrain.trainNumber}?date=${recentViewTrain.date}`) + fetch(`https://v6.db.transport.rest/trips/${encodeURI(recentViewTrain.tripId)}`) .then(function (result) { if (result.ok) { return result.json() } + else { + return Promise.reject('Response not okay') + } }) .then(function (result) { - recentViewTrainLink.innerText = 'Recent train: ' - trainIdSpan(result.rank, result.number, recentViewTrainLink) - if (recentViewTrain.groupIndex !== undefined || result.groups.length === 1) { - var group = result.groups[recentViewTrain.groupIndex || 0] - if (group.status) { - if (group.status.delay === 0) { - recentViewTrainLink.appendChild(document.createTextNode(" (on time)")) - } - else if (group.status.delay > 0) { - recentViewTrainLink.appendChild(document.createTextNode(` (${group.status.delay} min late)`)) - } - else { - recentViewTrainLink.appendChild(document.createTextNode(` (${-group.status.delay} min early)`)) - } - } - } + recentViewTrainLink.innerText = `Recent train: ${result.trip.line.name} (${result.trip.line.fahrtNr})` }) var myTrainLi = document.getElementById("my-train-li") myTrainLi.parentNode.insertBefore(recentViewTrainLi, myTrainLi.nextSibling) } } + + var recentRouteStr = localStorage.getItem('recent/route') + if (recentRouteStr) { + /** + * @type {object} + * @property {string} queryParams + * @property {string} from + * @property {string} to + * @property {string} $addDate + */ + var recentRoute = JSON.parse(recentRouteStr) + var addDate = new Date(recentRoute.$addDate) + addDate.setHours(addDate.getHours() + 12) // store recents for 6 hours + if (addDate.getTime() > Date.now()) { + var recentRouteLi = document.createElement('li') + var recentRouteLink = document.createElement('a') + recentRouteLi.appendChild(recentRouteLink) + var recentRouteLinkUrl = new URL('/config-route.html', window.location.origin) + recentRouteLinkUrl.search = recentRoute.queryParams + recentRouteLink.href = recentRouteLinkUrl.toString() + recentRouteLink.classList.add('items') + recentRouteLink.innerText = `Recent route: ${recentRoute.from} → ${recentRoute.to}` + + var routesLi = document.getElementById("routes-li") + routesLi.parentNode.insertBefore(recentRouteLi, routesLi.nextSibling) + } + } } }) diff --git a/manifest.json b/manifest.json index eb0eea4..44986ca 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,7 @@ { - "id": "ro.dcdev.infotren.kai", - "name": "Info Tren: Romanian Railways", - "short_name": "Info Tren", - "description": "Web application for Informatica Feroviară scraper, showing data about Romanian Railways", + "name": "InfoDTrain", + "short_name": "InfoDTrain", + "description": "Web application for Deutsche Bahn API", "theme_color": "#0000ff", "background_color": "#ffffff", "display": "standalone", @@ -27,7 +26,7 @@ }, { "name": "Train Routes", - "url": "/route.html", + "url": "/config-route.html", "description": "Plan an itinerary" } ], diff --git a/manifest.webapp b/manifest.webapp index a954a30..48239eb 100644 --- a/manifest.webapp +++ b/manifest.webapp @@ -1,8 +1,8 @@ { "version": "1", - "name": "InfoTren", + "name": "InfoDTrain", "launch_path": "/index.html", - "description": "Frontend for InfoFer scraper", + "description": "Frontend for Deutsche Bahn API", "developer": { "name": "Dan Cojocaru", "url": "https://dcdev.ro" diff --git a/route.css b/route.css index 2e5dcf3..c54a383 100644 --- a/route.css +++ b/route.css @@ -1,38 +1,47 @@ .itinerary-train { display: grid; - grid-template-columns: auto 1fr; + grid-template-columns: auto 1fr auto; grid-template-rows: repeat(auto-fit, auto); + align-items: center; } .itinerary-train:not(:last-child) { grid-template-areas: - "dep-time dep-station" - "train train" - "arr-time arrdep-station" - "dep2-time arrdep-station"; + "dep-time dep-station dep-platform" + "train train train" + "arr-time arrdep-station arr-platform" + "dep2-time arrdep-station dep2-platform"; } .itinerary-train:last-child { grid-template-areas: - "train train" - "arr-time arr-station"; + "train train train" + "arr-time arr-station arr-platform"; } .itinerary-train:only-child { grid-template-areas: - "dep-time dep-station" - "train train" - "arr-time arr-station"; + "dep-time dep-station dep-platform" + "train train train" + "arr-time arr-station arr-platform"; } .itinerary-train .departure.time { grid-area: dep-time; } +.itinerary-train .departure.platform { + grid-area: dep-platform; +} + .itinerary-train .next-departure.time { grid-area: dep2-time; } +.itinerary-train .next-departure.platform { + grid-area: dep2-platform; +} + .itinerary-train .departure.station { grid-area: dep-station; } @@ -45,6 +54,10 @@ grid-area: arr-time; } +.itinerary-train .arrival.platform { + grid-area: arr-platform; +} + .itinerary-train:not(:last-child) .arrival.station { grid-area: arrdep-station; align-self: center; @@ -57,9 +70,34 @@ .itinerary-train .time { margin-left: 2px; margin-right: 2px; + align-self: center; +} + +.itinerary-train .platform { + margin: 2px; + padding: 2px; + border: 1px solid black; + border-radius: 4px; + justify-self: end; + min-width: 16px; + text-align: center; +} + +.itinerary-train .platform.changed { + color: red; + border-color: red; } .train .company { font-size: 0.8em; font-style: italic; } + +.walking { + font-size: 0.95em; + font-style: italic; +} + +input#time { + margin: 1px; +} diff --git a/route.dark.css b/route.dark.css new file mode 100644 index 0000000..b47aeff --- /dev/null +++ b/route.dark.css @@ -0,0 +1,11 @@ +@media (prefers-color-scheme: dark) { + .itinerary-train .platform { + border-color: white; + } + + .itinerary-train .platform.changed { + color: #ff3333; + border-color: #ff3333; + } + +} diff --git a/route.html b/route.html index 9801a28..8499421 100644 --- a/route.html +++ b/route.html @@ -4,15 +4,18 @@ - Route - InfoTren + Route - InfoDTrain + + + diff --git a/route.js b/route.js index 07dcc2c..76d3ef6 100644 --- a/route.js +++ b/route.js @@ -10,19 +10,32 @@ var toStation = null * @type {Date | null} */ var departureDate = null +var transitKind = { + ice: true, + ic: true, + re: true, + rb: true, + s: true, + bus: true, + ferry: true, + u: true, + tram: true, +} /** - * @type {{ name: string, stoppedAtBy: string[] }[]} + * @type {{id: string, name: string}[]} */ var knownStations = [] -function goToStation(station) { +var itineraries = null + +function goToStation(stationId) { var url = new URL(window.location.href) if (!fromStation) { - url.searchParams.set('from', station) + url.searchParams.set('from', stationId) } else if (!toStation) { - url.searchParams.set('to', station) + url.searchParams.set('to', stationId) } // url.searchParams.set('date', new Date().toISOString()) window.location.href = url.toString() @@ -62,41 +75,9 @@ function rebuildSuggestions() { 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); - } + var suggestions = knownStations.slice() - 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) @@ -106,7 +87,7 @@ function rebuildSuggestions() { suggestionLi.style.padding = '2px 0' function onAction(e) { - goToStation(suggestion.name) + goToStation(JSON.stringify(suggestion)) } suggestionLi.addEventListener('click', onAction) suggestionLi.addEventListener('keypress', function (e) { @@ -121,7 +102,7 @@ function rebuildSuggestions() { var stationNameP = document.createElement('p') suggestionLi.appendChild(stationNameP) - stationNameP.textContent = suggestion.name + stationNameP.textContent = suggestion.name || suggestion.address stationNameP.classList.add('pri', 'stationName') // var trainCompanyP = document.createElement('p') @@ -131,33 +112,6 @@ function rebuildSuggestions() { // 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 @@ -167,31 +121,58 @@ function rebuildSuggestions() { }, 500) } +var fetchAbortController = new AbortController() +function reloadSuggestions() { + var stationNameInput = document.getElementById('stationName') + var stationName = searchNormalize(stationNameInput.value.trim()) + + var locationsUrl = new URL('https://v6.db.transport.rest/locations') + locationsUrl.searchParams.set('query', stationName) + locationsUrl.searchParams.set('limit', '25') + locationsUrl.searchParams.set('fuzzy', 'true') + locationsUrl.searchParams.set('stops', 'true') + locationsUrl.searchParams.set('addresses', 'true') + locationsUrl.searchParams.set('poi', 'true') + + fetchAbortController.abort() + fetchAbortController = new AbortController() + fetch(locationsUrl.toString(), { signal: fetchAbortController.signal }) + .then(function (response) { + return response.json() + }) + .then(function (data) { + if (data) { + knownStations = Object.values(data) + rebuildSuggestions() + } + }) +} + /** - * @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 + * @typedef DbJourney + * @prop {'journey'} type + * @prop {(DbTrip & DbArrDep & {tripId: string})[]} legs + * @prop {string} refreshToken + * @prop {DbRemark[]} remarks */ + /** - * @param {Itinerary[]} data + * @param {{journeys: DbJourney[]}} data */ function onItineraries(data) { - var contentDiv = document.createElement('div') - document.body.insertBefore(contentDiv, document.querySelector('footer')) - contentDiv.classList.add('content') + var contentDiv = document.getElementById('content-div') + if (!contentDiv) { + contentDiv = document.createElement('div') + document.body.insertBefore(contentDiv, document.querySelector('footer')) + contentDiv.classList.add('content') + contentDiv.id = 'content-div' + } + + while (contentDiv.childNodes.length > 0) { + contentDiv.childNodes[contentDiv.childNodes.length - 1].remove() + } - for (var i = 0; i < data.length; i++) { + for (var i = 0; i < data.journeys.length; i++) { var itineraryDiv = document.createElement('div') contentDiv.appendChild(itineraryDiv) @@ -203,8 +184,8 @@ function onItineraries(data) { itineraryDiv.appendChild(trainsDiv) trainsDiv.classList.add('itinerary-trains') - data[i].trains.forEach(function (train, idx) { - var last = idx === data[i].trains.length - 1 + data.journeys[i].legs.forEach(function (train, idx) { + var last = idx === data.journeys[i].legs.length - 1 var trainDiv = document.createElement('div') trainsDiv.appendChild(trainDiv) @@ -216,64 +197,127 @@ function onItineraries(data) { departureTimeP.classList.add('sec', 'departure', 'time') var departureTimePre = document.createElement('pre') departureTimeP.appendChild(departureTimePre) - var departure = new Date(train.departureDate) + var departure = new Date(train.plannedDeparture) departureTimePre.textContent = departure.toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' }) var departureHeading = document.createElement('h3') trainDiv.appendChild(departureHeading) departureHeading.classList.add('departure', 'station') - var departureLink = document.createElement('a') - departureHeading.appendChild(departureLink) - departureLink.textContent = train.from - departureLink.classList.add('no-custom-a', 'items') - var departureUrl = new URL('/view-station.html', window.location.origin) - departureUrl.searchParams.set('station', train.from) - departureLink.href = departureUrl.toString() + if (train.origin.type === 'stop' || train.origin.type === 'station') { + var departureLink = document.createElement('a') + departureHeading.appendChild(departureLink) + departureLink.textContent = train.origin.name + departureLink.classList.add('no-custom-a', 'items') + var departureUrl = new URL('/view-station.html', window.location.origin) + departureUrl.searchParams.set('stationId', train.origin.id) + departureLink.href = departureUrl.toString() + } + else { + var departureSpan = document.createElement('span') + departureHeading.append(departureSpan) + departureSpan.innerText = train.origin.name || train.origin.address + } + + if (train.departurePlatform || train.plannedDeparturePlatform) { + var departurePlatformP = document.createElement('p') + trainDiv.append(departurePlatformP) + departurePlatformP.classList.add('sec', 'departure', 'platform') + if (train.departurePlatform && train.departurePlatform != train.plannedDeparturePlatform) { + departurePlatformP.classList.add('changed') + } + departurePlatformP.textContent = `${train.departurePlatform || train.plannedDeparturePlatform}` + } } var trainP = document.createElement('p') trainDiv.appendChild(trainP) trainP.classList.add('pri', 'train') - var trainLink = document.createElement('a') - trainP.appendChild(trainLink) - trainIdSpan(train.trainRank, train.trainNumber, trainLink) - trainLink.classList.add('no-custom-a', 'items') - var trainUrl = new URL('/view-train.html', window.location.origin) - trainUrl.searchParams.set('train', train.trainNumber) - trainLink.href = trainUrl.toString() - trainP.appendChild(document.createTextNode(' ')) - var trainCompany = document.createElement('span') - trainP.appendChild(trainCompany) - trainCompany.textContent = '(' + train.operator + ')' - trainCompany.classList.add('company') + if (!train.walking) { + var trainLink = document.createElement('a') + trainP.appendChild(trainLink) + trainLink.innerText = train.line.name + trainLink.classList.add('no-custom-a', 'items') + if (train.line.product) { + if (train.line.productName === 'STB' && train.line.name.startsWith('STB U')) { + train.line.product = 'subway' + } + trainLink.classList.add('product-' + train.line.product) + } + var trainUrl = new URL('/view-train.html', window.location.origin) + trainUrl.searchParams.set('tripId', train.tripId) + trainUrl.searchParams.set('startId', train.origin.id) + trainUrl.searchParams.set('stopId', train.destination.id) + trainLink.href = trainUrl.toString() + trainP.appendChild(document.createTextNode(' ')) + if (train.line.operator) { + var trainCompany = document.createElement('span') + trainP.appendChild(trainCompany) + trainCompany.textContent = '(' + train.line.operator.name + ')' + trainCompany.classList.add('company') + } + } + else { + var walkingSpan = document.createElement('span') + trainP.append(walkingSpan) + walkingSpan.classList.add('walking') + walkingSpan.innerText = `Walking (${train.distance} m)` + } 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) + var arrival = new Date(train.plannedArrival) arrivalTimePre.textContent = arrival.toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' }) var arrivalHeading = document.createElement('h3') trainDiv.appendChild(arrivalHeading) arrivalHeading.classList.add('arrival', 'station') - var arrivalLink = document.createElement('a') - arrivalHeading.appendChild(arrivalLink) - arrivalLink.textContent = train.to - arrivalLink.classList.add('no-custom-a', 'items') - var arrivalUrl = new URL('/view-station.html', window.location.origin) - arrivalUrl.searchParams.set('station', train.from) - arrivalLink.href = arrivalUrl.toString() + if (train.destination.type === 'stop' || train.destination.type === 'station') { + var arrivalLink = document.createElement('a') + arrivalHeading.appendChild(arrivalLink) + arrivalLink.textContent = train.destination.name + arrivalLink.classList.add('no-custom-a', 'items') + var arrivalUrl = new URL('/view-station.html', window.location.origin) + arrivalUrl.searchParams.set('stationId', train.destination.id) + arrivalLink.href = arrivalUrl.toString() + } + else { + var arrivalSpan = document.createElement('span') + arrivalHeading.append(arrivalSpan) + arrivalSpan.innerText = train.destination.name || train.destination.address + } + + if (train.arrivalPlatform || train.plannedArrivalPlatform) { + var arrivalPlatformP = document.createElement('p') + trainDiv.append(arrivalPlatformP) + arrivalPlatformP.classList.add('sec', 'arrival', 'platform') + if (train.arrivalPlatform && train.arrivalPlatform != train.plannedArrivalPlatform) { + arrivalPlatformP.classList.add('changed') + } + arrivalPlatformP.textContent = `${train.arrivalPlatform || train.plannedArrivalPlatform}` + } if (!last) { + var nextTrain = data.journeys[i].legs[idx + 1] 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) + var departure = new Date(nextTrain.plannedDeparture) departureTimePre.textContent = departure.toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' }) + + if (nextTrain.departurePlatform || nextTrain.plannedDeparturePlatform) { + var departurePlatformP = document.createElement('p') + trainDiv.append(departurePlatformP) + departurePlatformP.classList.add('sec', 'next-departure', 'platform') + if (nextTrain.departurePlatform && nextTrain.departurePlatform != nextTrain.plannedDeparturePlatform) { + departurePlatformP.classList.add('changed') + } + departurePlatformP.textContent = `${nextTrain.departurePlatform || nextTrain.plannedDeparturePlatform}` + } } }) } @@ -299,7 +343,9 @@ function csk() { window.addEventListener('load', function (e) { var sp = new URL(window.location.href).searchParams fromStation = sp.get('from') + var fromJson = JSON.parse(fromStation || 'null') toStation = sp.get('to') + var toJson = JSON.parse(toStation || 'null') var departureDateStr = sp.get('departureDate') if (departureDateStr) { departureDate = new Date(departureDateStr) @@ -316,30 +362,46 @@ window.addEventListener('load', function (e) { titleH1.textContent = 'Find Route - Departure Date' } else { - titleH1.textContent = `${fromStation} - ${toStation}` + titleH1.textContent = `${fromJson.name || fromJson.address} - ${toJson.name || toJson.address}` + } + + var transitKindJson = JSON.parse(sp.get('transitKind')) + if (transitKindJson) { + transitKind = transitKindJson } 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' + document.body.insertBefore( + h4( + label('Station Name').att$('for', 'stationName'), + ), + footer, + ) + // 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' + + document.body.insertBefore( + input('search').id$('stationName').att$('name', 'stationName').class$('items'), + footer, + ) + // var stationNameInput = document.createElement('input') + // document.body.insertBefore(stationNameInput, footer) + // stationNameInput.type = 'search' + // stationNameInput.classList.add('items') + // stationNameInput.name = 'stationName' + // stationNameInput.id = 'stationName' + + document.body.insertBefore(h4('Suggestions'), footer) + // var suggestionsH4 = document.createElement('h4') + // document.body.insertBefore(suggestionsH4, footer) + // suggestionsH4.textContent = 'Suggestions' var contentDiv = document.createElement('div') document.body.insertBefore(contentDiv, footer) @@ -352,7 +414,7 @@ window.addEventListener('load', function (e) { var stationName = document.getElementById('stationName') stationName.addEventListener('input', function (e) { - rebuildSuggestions() + reloadSuggestions() }) stationName.addEventListener('focus', function (e) { focusedElement = stationName @@ -368,19 +430,6 @@ window.addEventListener('load', function (e) { 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') @@ -394,7 +443,7 @@ window.addEventListener('load', function (e) { contentDiv.appendChild(departureDateUl) departureDateUl.id = 'suggestionsArea' - for (var i = 0, departureOption = new Date(); i < 30; i++, departureOption.setDate(departureOption.getDate() + 1)) { + for (var i = -1, departureOption = (function () { var d = new Date(); d.setDate(d.getDate() - 1); return d })(); i < 30; i++, departureOption.setDate(departureOption.getDate() + 1)) { var suggestionLi = document.createElement('li') departureDateUl.appendChild(suggestionLi) suggestionLi.classList.add('items') @@ -414,6 +463,9 @@ window.addEventListener('load', function (e) { suggestionLi.addEventListener('focus', function (e) { focusedElement = suggestionLi }) + if (i === 0) { + suggestionLi.focus() + } })() var innerP = document.createElement('p') suggestionLi.appendChild(innerP) @@ -421,6 +473,9 @@ window.addEventListener('load', function (e) { 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')}` + if (i === 0) { + innerPre.textContent += ' (today)' + } } document.querySelector('.csk').textContent = 'Select' @@ -439,11 +494,51 @@ window.addEventListener('load', function (e) { loadingP.classList.add('pri') loadingP.textContent = 'Loading data...' + var url = new URL('https://v6.db.transport.rest/journeys') + if (fromJson.type === 'stop' || fromJson.type === 'station') { + url.searchParams.set('from', fromJson.id) + } + else { + if (fromJson.type === 'location') { + delete fromJson.id + } + Object.keys(fromJson).forEach(function (key) { + url.searchParams.set(`from.${key}`, fromJson[key]) + }) + } + if (toJson.type === 'stop' || toJson.type === 'station') { + url.searchParams.set('to', toJson.id) + } + else { + if (toJson.type === 'location') { + delete toJson.id + } + Object.keys(toJson).forEach(function (key) { + url.searchParams.set(`to.${key}`, toJson[key]) + }) + } + url.searchParams.set('departure', departureDate.toISOString()) + url.searchParams.set('results', '20') + url.searchParams.set('stopovers', 'true') + url.searchParams.set('nationalExpress', transitKind.ice) + url.searchParams.set('national', transitKind.ic) + url.searchParams.set('regionalExpress', transitKind.re) + url.searchParams.set('regional', transitKind.rb) + url.searchParams.set('suburban', transitKind.s) + url.searchParams.set('bus', transitKind.bus) + url.searchParams.set('ferry', transitKind.ferry) + url.searchParams.set('subway', transitKind.u) + url.searchParams.set('tram', transitKind.tram) + + if (window.localStorage) { + this.localStorage.setItem('recent/route', JSON.stringify({ + $addDate: new Date().toISOString(), + from: fromJson.name || fromJson.address, + to: toJson.name || toJson.address, + queryParams: new URL(window.location.href).search, + })) + } - 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() @@ -451,6 +546,34 @@ window.addEventListener('load', function (e) { .then(function (data) { contentDiv.remove() onItineraries(data) + itineraries = data + function fetchMore() { + console.debug(`Got ${itineraries.journeys.length} journeys, fetching more`) + var moreUrl = new URL(url.toString()) + moreUrl.searchParams.delete('departure') + moreUrl.searchParams.set('laterThan', itineraries.laterRef) + fetch(moreUrl.toString()) + .then(function (result) { + return result.json() + }) + .then(function (data) { + if (data.journeys) { + itineraries.journeys = itineraries.journeys.concat(data.journeys) + itineraries.laterRef = data.laterRef + onItineraries(itineraries) + + if (itineraries.laterRef) { + var lastJourney = itineraries.journeys[itineraries.journeys.length - 1] + var lastLeg = lastJourney.legs[lastJourney.legs.length - 1] + var departureDate = new Date(lastLeg.plannedDeparture) + if (departureDate.getTime() - Date.now() < 86400000) { + setTimeout(fetchMore, 500) + } + } + } + }) + } + fetchMore() }) .catch(function (e) { loadingP.textContent = 'An error has occured' diff --git a/showcase.html b/showcase.html index 0954ffc..b13ca82 100644 --- a/showcase.html +++ b/showcase.html @@ -7,6 +7,7 @@ Showcase + diff --git a/station.html b/station.html index 7c2b858..c5d2580 100644 --- a/station.html +++ b/station.html @@ -3,12 +3,14 @@ + - Station - InfoTren + Station - InfoDTrain + diff --git a/station.js b/station.js index 7d65211..240e4a8 100755 --- a/station.js +++ b/station.js @@ -1,9 +1,9 @@ var knownStations = [] -function goToStation(station) { +function goToStation(stationId) { var url = new URL(window.location.href) url.pathname = 'view-station.html' - url.searchParams.set('station', station) + url.searchParams.set('stationId', stationId) url.searchParams.set('date', new Date().toISOString()) window.location.href = url.toString() } @@ -36,41 +36,9 @@ function rebuildSuggestions() { 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; - } + var suggestions = knownStations.slice() - 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) @@ -80,7 +48,7 @@ function rebuildSuggestions() { suggestionLi.style.padding = '2px 0' function onAction(e) { - goToStation(suggestion.name) + goToStation(suggestion.id) } suggestionLi.addEventListener('click', onAction) suggestionLi.addEventListener('keypress', function (e) { @@ -105,33 +73,6 @@ function rebuildSuggestions() { // 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 @@ -141,6 +82,31 @@ function rebuildSuggestions() { }, 500) } +var fetchAbortController = new AbortController() +function reloadSuggestions() { + var stationNameInput = document.getElementById('stationName') + var stationName = searchNormalize(stationNameInput.value.trim()) + + var locationsUrl = new URL('https://v6.db.transport.rest/locations') + locationsUrl.searchParams.set('query', stationName) + locationsUrl.searchParams.set('limit', '25') + locationsUrl.searchParams.set('fuzzy', 'true') + locationsUrl.searchParams.set('stops', 'true') + + fetchAbortController.abort() + fetchAbortController = new AbortController() + fetch(locationsUrl.toString(), { signal: fetchAbortController.signal }) + .then(function (response) { + return response.json() + }) + .then(function (data) { + if (data) { + knownStations = Object.values(data) + rebuildSuggestions() + } + }) +} + function lsk() { document.getElementById('stationName').focus() } @@ -161,7 +127,7 @@ function csk() { window.addEventListener('load', function (e) { var stationName = document.getElementById('stationName') stationName.addEventListener('input', function (e) { - rebuildSuggestions() + reloadSuggestions() }) stationName.addEventListener('focus', function (e) { focusedElement = stationName @@ -197,15 +163,5 @@ window.addEventListener('load', function (e) { } }) - 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() - }) + reloadSuggestions() }) diff --git a/sw.js b/sw.js index a0f4573..2c32cf5 100755 --- a/sw.js +++ b/sw.js @@ -1,4 +1,4 @@ -const VERSION = 'v34' +const VERSION = 'v14' const API_ORIGIN = 'https://scraper.infotren.dcdev.ro/' const API_TRAINS = `${API_ORIGIN}v3/trains` const API_STATIONS = `${API_ORIGIN}v3/stations` @@ -20,6 +20,7 @@ const CACHE_FIRST = [ // Base '/base.css', + '/base.dark.css', // Pages '/index.html', @@ -34,6 +35,7 @@ const CACHE_FIRST = [ '/view-train.html', '/view-train.js', '/view-train.css', + '/view-train.dark.css', '/station.html', '/station.js', @@ -41,10 +43,16 @@ const CACHE_FIRST = [ '/view-station.html', '/view-station.js', '/view-station.css', + '/view-station.dark.css', + + '/config-route.html', + '/config-route.js', + '/config-route.css', '/route.html', '/route.js', '/route.css', + '/route.dark.css', // API API_TRAINS, diff --git a/train.html b/train.html index cd4cca5..e1ad8fd 100644 --- a/train.html +++ b/train.html @@ -3,12 +3,14 @@ + - Train - InfoTren + Train - InfoDTrain + diff --git a/train.js b/train.js index 2de4c5c..6538841 100755 --- a/train.js +++ b/train.js @@ -1,9 +1,9 @@ var knownTrains = [] -function goToTrain(number) { +function goToTrip(tripId) { var url = new URL(window.location.href) url.pathname = 'view-train.html' - url.searchParams.set('train', number) + url.searchParams.set('tripId', tripId) url.searchParams.set('date', new Date().toISOString()) window.location.href = url.toString() } @@ -26,43 +26,16 @@ function rebuildSuggestions() { suggestionsArea.childNodes[0].remove() } - var trainNumberInput = document.getElementById('trainNumber') - var trainNumber = trainNumberInput.value.trim() - - var suggestions = [] - if (!trainNumber) { - suggestions = knownTrains.slice() - } - else { - for (var i = 0; i < knownTrains.length; i++) { - if (!knownTrains[i].number.includes(trainNumber)) { - continue - } - suggestions.push(knownTrains[i]) - } - suggestions.sort((s1, s2) => { - if (s1.number.indexOf(trainNumber) != s2.number.indexOf(trainNumber)) { - return s1.number.indexOf(trainNumber) - s2.number.indexOf(trainNumber); - } - - if (s1.number.length != s2.number.length) { - return s1.number.length - s2.number.length; - } - - return s1.number.localeCompare(s2.number); - }) - } + var suggestions = knownTrains.filter(function (suggestion) { + return suggestion.line.name + }) // Trim the amount of results displayed if (suggestions.length > 100) { suggestions.splice(100) } - var foundInput = false suggestions.forEach(function (suggestion, index) { - if (trainNumber == suggestion.number) { - foundInput = true - } var suggestionLi = document.createElement('li') suggestionsArea.appendChild(suggestionLi) @@ -72,7 +45,7 @@ function rebuildSuggestions() { suggestionLi.style.padding = '2px 0' function onAction(e) { - goToTrain(suggestion.number) + goToTrip(suggestion.id) } suggestionLi.addEventListener('click', onAction) suggestionLi.addEventListener('keypress', function (e) { @@ -86,44 +59,22 @@ function rebuildSuggestions() { var trainNameP = document.createElement('p') suggestionLi.appendChild(trainNameP) - - trainIdSpan(suggestion.rank, suggestion.number, trainNameP) + trainNameP.textContent = `${suggestion.line.name} (${suggestion.line.fahrtNr})` trainNameP.classList.add('pri', 'trainName') + var trainRouteP = document.createElement('p') + suggestionLi.appendChild(trainRouteP) + + trainRouteP.textContent = `${suggestion.origin.name} → ${suggestion.destination.name}` + trainRouteP.classList.add('thi') + var trainCompanyP = document.createElement('p') suggestionLi.appendChild(trainCompanyP) - trainCompanyP.textContent = suggestion.company + trainCompanyP.textContent = suggestion.line.operator.name trainCompanyP.classList.add('thi') }, 0) }) - if (!foundInput && trainNumber) { - 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) { - goToTrain(trainNumber) - } - suggestionLi.addEventListener('click', onAction) - suggestionLi.addEventListener('keypress', function (e) { - if (e.key == 'Enter') { - onAction(e) - } - }) - suggestionLi.addEventListener('focus', function (e) { - focusedElement = suggestionLi - }) - - var trainNameP = document.createElement('p') - suggestionLi.appendChild(trainNameP) - - trainNameP.textContent = `Train ${trainNumber}` - trainNameP.classList.add('pri', 'trainName') - } setTimeout(function () { _rebuildDebounce = null @@ -133,6 +84,34 @@ function rebuildSuggestions() { }, 500) } +var fetchAbortController = new AbortController() +function reloadSuggestions() { + var trainNumberInput = document.getElementById('trainNumber') + var trainNumber = trainNumberInput.value.trim() + + var tripsUrl = new URL('https://v6.db.transport.rest/trips') + tripsUrl.searchParams.set('query', trainNumber) + tripsUrl.searchParams.set('onlyCurrentlyRunning', 'true') + + fetchAbortController.abort() + fetchAbortController = new AbortController() + fetch(tripsUrl.toString(), { signal: fetchAbortController.signal }) + .then(function (response) { + return response.json() + }) + .then(function (data) { + if (data.trips) { + knownTrains = data.trips + knownTrains.sort(function (s1, s2) { + var diff1 = Math.abs(Date.now() - new Date(s1.plannedDeparture).getTime()) + var diff2 = Math.abs(Date.now() - new Date(s2.plannedDeparture).getTime()) + return diff1 - diff2 + }) + rebuildSuggestions() + } + }) +} + function lsk() { document.getElementById('trainNumber').focus() } @@ -143,7 +122,7 @@ function csk() { } if (focusedElement.id === 'trainNumber') { - goToTrain(document.activeElement.value.trim()) + goToTrip(document.activeElement.value.trim()) } else { focusedElement.click() @@ -153,7 +132,7 @@ function csk() { window.addEventListener('load', function (e) { var trainNumber = document.getElementById('trainNumber') trainNumber.addEventListener('input', function (e) { - rebuildSuggestions() + reloadSuggestions() }) trainNumber.addEventListener('focus', function (e) { focusedElement = trainNumber @@ -166,7 +145,7 @@ window.addEventListener('load', function (e) { }) trainNumber.addEventListener('keypress', function (e) { if (e.key == 'Enter') { - goToTrain(trainNumber.value.trim()) + goToTrip(trainNumber.value.trim()) } }) @@ -189,15 +168,5 @@ window.addEventListener('load', function (e) { } }) - fetch('https://scraper.infotren.dcdev.ro/v2/trains') - .then(function (response) { - return response.json() - }) - .then(function (response) { - knownTrains = response - knownTrains.sort(function(a, b) { return a.number - b.number }) - }) - .then(function () { - rebuildSuggestions() - }) + reloadSuggestions() }) diff --git a/view-station.css b/view-station.css index ce0b8be..0d7a9c2 100644 --- a/view-station.css +++ b/view-station.css @@ -53,12 +53,12 @@ .train-item { display: grid; - grid-template-columns: 30px 60px auto 1fr auto; + grid-template-columns: 100px 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"; + "train time terminus platform" + "train delay terminus platform" + "status status status status"; align-items: center; padding: 4px 0; @@ -106,10 +106,19 @@ padding: 1px; margin: 1px; border-radius: 5px; - aspect-ratio: 1 / 1; + /* aspect-ratio: 1 / 1; */ min-width: 22px; display: flex; justify-content: center; align-items: center; -} \ No newline at end of file +} + +.train-item .platform.changed { + color: red; + border-color: red; +} + +.status-cancel { + text-align: start; +} diff --git a/view-station.dark.css b/view-station.dark.css new file mode 100644 index 0000000..e7fb48f --- /dev/null +++ b/view-station.dark.css @@ -0,0 +1,43 @@ +@media(prefers-color-scheme: dark) { + + .early { + color: lightgreen; + } + + .late { + color: #ff3333; + } + + #tabs-arr { + border-bottom-color: #bbffbb; + } + + #tabs-dep { + border-bottom-color: #bbbbff; + } + + #arrivals .train-item { + background-color: #001a00; + } + + #arrivals .train-item:nth-of-type(even) { + background-color: #004400; + } + + #departures .train-item { + background-color: #00002a; + } + + #departures .train-item:nth-of-type(even) { + background-color: #000066; + } + + .train-item.cancelled { + background-color: #550000 !important; + } + + .train-item .platform { + border-color: white; + } + +} diff --git a/view-station.html b/view-station.html index a3f180b..baace16 100644 --- a/view-station.html +++ b/view-station.html @@ -4,12 +4,14 @@ - View Station - InfoTren + View Station - InfoDTrain + + diff --git a/view-station.js b/view-station.js index 153780a..3105ce9 100644 --- a/view-station.js +++ b/view-station.js @@ -1,24 +1,54 @@ -var station +var stationId var date -var stationData = null +var stationData = { + departures: [], + arrivals: [], +} var lastSuccessfulFetch = null +/** + * @typedef StationArrDep + * @property {string} tripId + * @property {DbStop} stop + * @property {string | null} when + * @property {string} plannedWhen + * @property {number | null} delay + * @property {string | null} platform + * @property {string | null} plannedPlatform + * @property {string | null} prognosisType + * @property {string | null} direction + * @property {string | null} provenance + * @property {DbLine} line + * @property {DbRemark[]} remarks + * @property {DbStop | null} origin + * @property {DbStop | null} destionation + */ + +/** + * @typedef StationData + * @prop {StationArrDep[]} departures + * @prop {StationArrDep[]} arrivals + */ + +/** + * @param {?StationData} data + */ function onStationData(data) { - if (!data) { + if (!data || !data.arrivals && !data?.departures) { return } var title = document.getElementById('title') - title.textContent = data.stationName + title.textContent = (data.departures[0] || data.arrivals[0]).stop.name - document.getElementById('date').textContent = data.date + // document.getElementById('date').textContent = data.date document.getElementById('loading').classList.add('hidden') /** * @param {HTMLElement} elem - * @param {any[]} trains + * @param {StationArrDep[]} trains */ function addTrains(elem, trains) { while (elem.childNodes.length > 0) { @@ -32,26 +62,26 @@ function onStationData(data) { var trainItem = document.createElement('li') trainsList.appendChild(trainItem) trainItem.classList.add('train-item') - if (train.status && train.status.cancelled) { - trainItem.classList.add('cancelled') - } + // if (train.status && train.status.cancelled) { + // trainItem.classList.add('cancelled') + // } var timeDiv = document.createElement('p') trainItem.appendChild(timeDiv) timeDiv.classList.add('pri', 'time') var timeDivPre = document.createElement('pre') timeDiv.appendChild(timeDivPre) - timeDivPre.textContent = new Date(train.time).toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' }) + timeDivPre.textContent = new Date(train.plannedWhen).toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' }) - if (train.status && train.status.delay != 0) { + if (train.delay && train.delay != 0) { var delayDiv = document.createElement('p') trainItem.appendChild(delayDiv) delayDiv.classList.add('thi', 'delay') - delayDiv.textContent = `${train.status.delay} min ` + delayDiv.textContent = `${train.delay / 60} min ` // delayDiv.appendChild(document.createElement('br')) var descSpan = document.createElement('span') delayDiv.appendChild(descSpan) - if (train.status.delay > 0) { + if (train.delay > 0) { descSpan.classList.add('late') descSpan.textContent = 'late' } @@ -61,12 +91,12 @@ function onStationData(data) { } } - var rankDiv = document.createElement('p') - trainItem.appendChild(rankDiv) - rankDiv.classList.add('sec', 'rank', train.train.rank) - var rankDivPre = document.createElement('pre') - rankDiv.appendChild(rankDivPre) - rankDivPre.textContent = train.train.rank + // var rankDiv = document.createElement('p') + // trainItem.appendChild(rankDiv) + // rankDiv.classList.add('sec', 'rank', train.train.rank) + // var rankDivPre = document.createElement('pre') + // rankDiv.appendChild(rankDivPre) + // rankDivPre.textContent = train.train.rank var trainDiv = document.createElement('p') trainItem.appendChild(trainDiv) @@ -75,32 +105,35 @@ function onStationData(data) { trainDiv.appendChild(trainDivHref) trainDivHref.classList.add('no-a-custom') var trainUrl = new URL('/view-train.html', window.location.origin) - trainUrl.searchParams.append('train', train.train.number) - trainUrl.searchParams.append('date', train.train.departureDate) + trainUrl.searchParams.append('tripId', train.tripId) trainDivHref.href = trainUrl.toString() var trainDivHrefPre = document.createElement('pre') trainDivHref.appendChild(trainDivHrefPre) - trainDivHrefPre.textContent = train.train.number + trainDivHrefPre.textContent = train.line.name var terminusDiv = document.createElement('p') trainItem.appendChild(terminusDiv) terminusDiv.classList.add('pri', 'terminus') - terminusDiv.textContent = train.train.terminus + terminusDiv.textContent = train.direction - if (train.status && train.status.platform) { + if (train.platform) { var platformDiv = document.createElement('div') trainItem.appendChild(platformDiv) platformDiv.classList.add('thi', 'platform') + if (train.platform && train.platform !== train.plannedPlatform) { + platformDiv.classList.add('changed') + } var platformDivPre = document.createElement('pre') platformDiv.appendChild(platformDivPre) - platformDivPre.textContent = train.status.platform + platformDivPre.textContent = train.platform || train.plannedPlatform } - if (train.status && train.status.cancelled) { + if (train.cancelled) { + trainItem.classList.add('cancelled') var statusDiv = document.createElement('p') trainItem.appendChild(statusDiv) - statusDiv.classList.add('sec', 'status') - statusDiv.textContent = 'This train is cancelled' + statusDiv.classList.add('sec', 'status', 'status-cancel') + statusDiv.textContent = 'This journey is cancelled' } }) } @@ -119,34 +152,44 @@ function refresh() { }, timeout || 90000) } var reqDate = new Date(date.valueOf()) - reqDate.setMinutes(0, 0, 0) - return fetch( - `https://scraper.infotren.dcdev.ro/v3/stations/${station}?date=${reqDate.toISOString()}`, - { - cache: 'no-store', - }, - ).then(function (response) { - if (!response.ok) { + // reqDate.setHours(0, 0, 0, 0) + reqDate.setMinutes(reqDate.getMinutes() - 15) + return Promise.all([ + fetch( + `https://v6.db.transport.rest/stops/${stationId}/arrivals?when=${reqDate.toISOString()}&duration=1440`, + { + cache: 'no-store', + }, + ), + fetch( + `https://v6.db.transport.rest/stops/${stationId}/departures?when=${reqDate.toISOString()}&duration=1440`, + { + cache: 'no-store', + }, + ), + ]).then(function (responses) { + if (!responses[0].ok || !responses[1].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) { + var cacheDate = responses[0].headers.get('SW-Cached-At') + return Promise.all(responses.map(function (r) {return r.json() })).then(function (data) { data['$cacheDate'] = cacheDate return data }) - }).then(function (response) { - if (!response) { + }).then(function (responses) { + if (!responses) { return } - var cacheDate = response['$cacheDate'] + var cacheDate = responses['$cacheDate'] if (cacheDate) { cacheDate = new Date(cacheDate) } var success = !cacheDate - stationData = response - onStationData(response) + stationData.arrivals = responses[0].arrivals + stationData.departures = responses[1].departures + onStationData(stationData) // Check in 1 seconds if network error reschedule(success ? undefined : 1000) return success @@ -185,7 +228,7 @@ function rsk() { } window.addEventListener('load', function (e) { - if (!new URL(window.location.href).searchParams.has('station')) { + if (!new URL(window.location.href).searchParams.has('stationId')) { window.history.back() this.setTimeout(function () { var url = new URL(window.location.href) @@ -196,7 +239,7 @@ window.addEventListener('load', function (e) { var sp = new URL(window.location.href).searchParams - station = sp.get('station') + stationId = sp.get('stationId') date = sp.has('date') ? new Date(sp.get('date')) : new Date() // View departures first diff --git a/view-train.css b/view-train.css index 6fc7fe8..6f35243 100644 --- a/view-train.css +++ b/view-train.css @@ -45,11 +45,29 @@ background-color: #fafafa; } +.stationItem.cancelled:nth-of-type(odd) { + background-color: #fffafa; +} + +.stationItem.cancelled:nth-of-type(even) { + background-color: #ffeaea; +} + .stationItem .name { text-align: center; grid-area: name; } +.stationItem.not-in-journey .name, .stationItem.not-in-journey .arrival, .stationItem.not-in-journey .departure { + color: grey; +} + +.stationItem.cancelled .name { + text-decoration: line-through; + text-decoration-color: red; + color: red; +} + .stationItem .arrival { text-align: start; grid-area: arr; @@ -90,6 +108,10 @@ grid-area: platform; } +.stationItem .platform.changed { + color: red; +} + .stationItem .notes { grid-area: notes; } @@ -98,8 +120,36 @@ text-align: center; } +.remarkItem { + margin-top: 4px; + margin-bottom: 4px; +} + +.remarkItem:nth-of-type(even) { + background-color: #fafafa; +} + .last-refreshed { font-size: 12px; text-transform: none; } +.remark-status, .remark-status * { + color: red !important; +} + +.remark-board, .remark-exit { + color: blue !important; +} + +#actual-map { + height: 500px; +} + +#actual-map a { + display: initial; + padding: initial; + color: inherit; + font-size: inherit; + font-weight: inherit; +} diff --git a/view-train.dark.css b/view-train.dark.css new file mode 100644 index 0000000..e14466c --- /dev/null +++ b/view-train.dark.css @@ -0,0 +1,49 @@ +@media(prefers-color-scheme: dark) { + + .early { + color: lightgreen; + } + + .late { + color: #ff3333; + } + + .station { + color: white; + } + + .stationItem:nth-of-type(even) { + background-color: #0f0f0f; + } + + .stationItem .arrival .original, .stationItem .departure .original { + color: #afafaf; + } + + .remark-status, .remark-status * { + color: #ff3333 !important; + } + + .remark-board, .remark-exit { + color: #8888ff !important; + } + + .stationItem.not-in-journey .name, .stationItem.not-in-journey .arrival, .stationItem.not-in-journey .departure { + color: #aaaaaa; + } + + .stationItem.cancelled .name { + text-decoration: line-through; + text-decoration-color: #ff3333; + color: #ff3333; + } + + .stationItem .platform.changed { + color: #ff3333; + } + + .remarkItem:nth-of-type(even) { + background-color: #202020; + } + +} diff --git a/view-train.html b/view-train.html index 7311146..70986d6 100644 --- a/view-train.html +++ b/view-train.html @@ -4,13 +4,21 @@ - View Train - InfoTren + View Train - InfoDTrain + + + + @@ -56,6 +64,10 @@

Stations

+
+

Map

+ Load map +