You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

672 lines
26 KiB

/**
* @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')
suggestionDiv.classList.add('suggestion')
var suggestionLi = document.createElement('li')
suggestionDiv.appendChild(suggestionLi)
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')
suggestionsArea.appendChild(suggestionDiv)
})
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()
}
})
})