/** * @type {string | null} */ var fromStation = null /** * @type {string | null} */ var toStation = null /** * @type {Date | null} */ var departureDate = null var arrivalInsteadOfDeparture = false 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 = [] /** * @type {'unavailable' | 'notRequested' | 'waiting' | 'gotData'} */ var nearbyStatus = 'notRequested' var nearbyStations = [] 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 createSuggestion(suggestion, index) { delete suggestion['products'] var suggestionDiv = document.createElement('div') suggestionDiv.classList.add('suggestion') var suggestionLi = document.createElement('li') suggestionDiv.appendChild(suggestionLi) suggestionLi.classList.add('items') if (index) { 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) var stationNameSpan = document.createElement('span') stationNameP.append(stationNameSpan) stationNameSpan.textContent = suggestion.name || suggestion.address stationNameSpan.classList.add('pri', 'stationName') if (suggestion.distance) { stationNameP.append(' ') var stationDistanceSpan = document.createElement('span') stationNameP.append(stationDistanceSpan) stationDistanceSpan.append('(', suggestion.distance.toString(), ' m)') stationDistanceSpan.classList.add('stationDistance') } if (window.localStorage) { var suggestionDistance = suggestion['distance'] delete suggestion['distance'] var suggestionLink = document.createElement('a') 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() suggestionDiv.appendChild(suggestionLink) suggestion['distance'] = suggestionDistance } // var trainCompanyP = document.createElement('p') // suggestionLi.appendChild(trainCompanyP) // trainCompanyP.textContent = suggestion.company // trainCompanyP.classList.add('thi') return suggestionDiv } 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) { suggestionsArea.appendChild(createSuggestion(suggestion, index)) }) if (nearbyStatus !== 'unavailable') { suggestionsArea.appendChild(h4('Nearby stations')) if (nearbyStatus === 'notRequested') { suggestionsArea.appendChild(a('', 'Load nearby stations').event$('click', function (event) { event.preventDefault() var latitude = 0 var longitude = 0 var watchId = navigator.geolocation.watchPosition( function (data) { if (data.coords.latitude !== latitude || data.coords.longitude !== longitude) { latitude = data.coords.latitude longitude = data.coords.longitude var geoUrl = new URL('https://v6.db.transport.rest/locations/nearby') geoUrl.searchParams.append('latitude', data.coords.latitude.toString()) geoUrl.searchParams.append('longitude', data.coords.longitude.toString()) geoUrl.searchParams.append('results', '10') fetch(geoUrl) .then(function (response) { return response.json() }) .then(function (data) { nearbyStatus = 'gotData' nearbyStations = data rebuildSuggestions() }) .catch(function () { nearbyStatus = 'unavailable' rebuildSuggestions() }) } }, function (error) { if (nearbyStations.length === 0) { nearbyStatus = 'unavailable' rebuildSuggestions() } navigator.geolocation.clearWatch(watchId) }, { enableHighAccuracy: true, }, ) nearbyStatus = 'waiting' rebuildSuggestions() })) } else if (nearbyStatus === 'waiting') { var waitingP = document.createElement('p') suggestionsArea.append(waitingP) waitingP.append('Loading...') waitingP.classList.add('pri') } else if (nearbyStatus === 'gotData') { nearbyStations.forEach(function (suggestion, index) { suggestionsArea.appendChild(createSuggestion(suggestion, suggestions.length + index)) }) } } 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() } }) } function lsk() { document.getElementById('stationName').focus() } function csk() { if (focusedElement == null) { return } focusedElement.click() } window.addEventListener('load', function (e) { if ('geolocation' in navigator) {} else { nearbyStatus = 'unavailable' } 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 - 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' if (arrivalInsteadOfDeparture) { url.searchParams.delete('departureDate') url.searchParams.set('arrivalDate', departureDate.toISOString()) } else { url.searchParams.delete('arrivalDate') 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() } function timeKindRadioChanged(event) { var kind = event.target.id.split('-')[2] if (kind === 'arrival') { arrivalInsteadOfDeparture = true } else if (kind === 'departure') { arrivalInsteadOfDeparture = false } 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'), div( p(label('Departure time').att$('for', 'time-kind-departure')), input('radio') .checked$(!arrivalInsteadOfDeparture) .id$('time-kind-departure') .att$('name', 'time-kind') .class$('time-kind') .class$('items') .event$('change', timeKindRadioChanged), ).class$('checkbox'), div( p(label('Arrival time').att$('for', 'time-kind-arrival')), input('radio') .checked$(arrivalInsteadOfDeparture) .id$('time-kind-arrival') .att$('name', 'time-kind') .class$('time-kind') .class$('items') .event$('change', timeKindRadioChanged), ).class$('checkbox'), 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')) .class$('items') .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('Transport 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') .class$('items') .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') .class$('items') .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') .class$('items') .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') .class$('items') .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') .class$('items') .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') .class$('items') .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') .class$('items') .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') .class$('items') .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') .class$('items') .event$('change', transitKindCheckChanged), ).class$('checkbox'), h4('Start search'), a('', 'Search').id$('search-link').class$('items'), ).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() } }) })