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.
645 lines
23 KiB
645 lines
23 KiB
/** |
|
* @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() |
|
} |
|
}) |
|
})
|
|
|