/** * @type {string} */ var trainNumber /** * @type {string | null} */ var tripId = null /** * @type {Date} */ var date var groupIndex = null /** * @type {string | null} */ var startId = null /** * @type {string | null} */ var stopId = null var yesterday = false var showKm = false var trainData = null /** * @type {?Date} */ var lastSuccessfulFetch = null var loadMap = false /** * @type {any} */ var leafletMap = null /** * @type {any[]} */ var mapLayers = [] /** * @param {DbTrip & DbArrDep} data * @param {?Date} fetchDate */ function onTrainData(data, fetchDate) { if (loadMap) { document.getElementById('load-map-button').remove() } if (window.localStorage) { localStorage.setItem('recent/view-train', JSON.stringify({ trainNumber: data.line.fahrtNr, name: data.line.name, tripId: data.id, date: date ? date.toISOString() : undefined, $addDate: new Date().toISOString(), })) } var title = document.getElementById('title') title.textContent = '' var titleCategory = '' switch (data.line.product) { case 'bus': titleCategory = '' break case 'ferry': titleCategory = 'Ferry' break default: titleCategory = 'Train' } title.appendChild(document.createTextNode(titleCategory + ' ')) var lineNameSpan = document.createElement('span') title.appendChild(lineNameSpan) lineNameSpan.textContent = data.line.name if (data.line.product) { if (data.line.productName === 'STB' && data.line.name.startsWith('STB U')) { data.line.product = 'subway' } if (data.line.adminCode === 'vvs020') { // Stuttgart Stadtbahn lineNameSpan.innerText = data.line.name.slice(4) } else if (data.line.adminCode === '800643') { // Stuttgart S-Bahn lineNameSpan.innerText = 'S' + data.line.name.slice(2) } else if (data.line.adminCode === 'kvv021') { // Karlsruhe Tram lineNameSpan.innerText = data.line.name.slice(4) } lineNameSpan.classList.add('product-' + data.line.product) lineNameSpan.classList.add('product-id-' + data.line.id) lineNameSpan.classList.add('product-adminCode-' + data.line.adminCode) if (data.line.operator) { lineNameSpan.classList.add('product-operator-' + data.line.operator.id) } } // trainIdSpan(data.line.productName, data.line.fahrtNr, title) // title.append(' ') title.append(` (${data.line.fahrtNr})`) document.getElementsByTagName('title')[0].textContent = `Train ${data.line.productName} ${data.line.fahrtNr} (${data.line.name})` if (data.line.operator) { document.getElementById('company').textContent = data.line.operator.name } var dateHref = document.createElement('a') var dateP = document.getElementById('date') while (dateP.childNodes.length > 0) { dateP.childNodes[0].remove() } dateP.appendChild(dateHref) dateHref.textContent = new Date(data.departure || data.plannedDeparture).toDateString() dateHref.href = '#' dateHref.classList.add('no-a-custom') dateHref.classList.add('items', 'no-a-custom') dateHref.addEventListener('click', function (e) { e.preventDefault() // Implement date switcher yesterday = !yesterday if (!yesterday) { history.back() } else { const yesterdayUrl = new URL(location.href) const newDate = new Date(date.getTime()) newDate.setDate(newDate.getDate() - 1) yesterdayUrl.searchParams.set('date', newDate.toISOString()) history.pushState('', '', yesterdayUrl) } refresh() }) document.getElementById('loading').classList.add('hidden') /** * @type {DbTrip & DbArrDep} */ var group = data document.getElementById('group-choice').classList.add('hidden') document.getElementById('train-info').classList.remove('hidden') document.getElementById('train-info').focus() document.getElementsByClassName('rsk')[0].textContent = 'Refresh' document.getElementsByClassName('csk')[0].textContent = '' document.getElementById('route-from').textContent = group.origin.name document.getElementById('route-to').textContent = group.destination.name if (group.status) { document.getElementById('status').classList.remove('hidden') var statusDelay = document.getElementById('status-delay') while (statusDelay.childNodes.length > 0) { statusDelay.childNodes[0].remove() } var delayString = '' var delayMinutes = group.status.delay if (delayMinutes === 0) { delayString = 'On time' statusDelay.appendChild(document.createTextNode(delayString)) } else { var early = false if (delayMinutes < 0) { early = true delayMinutes = -delayMinutes } if (delayMinutes >= 60) { var hours = Math.floor(delayMinutes / 60) delayMinutes = delayMinutes % 60 delayString += hours.toString() delayString += ' hour' if (hours > 1) { delayString += 's' } } if (delayMinutes > 0) { if (delayString.length > 0) { delayString += ' and ' } delayString += delayMinutes.toString() delayString += ' minute' if (delayMinutes > 1) { delayString += 's' } } delayString += ' ' statusDelay.appendChild(document.createTextNode(delayString)) var kindSpan = document.createElement('span') statusDelay.appendChild(kindSpan) if (early) { kindSpan.textContent = 'early' kindSpan.classList.add('early') } else { kindSpan.textContent = 'late' kindSpan.classList.add('late') } } var statusLocation = document.getElementById('status-location') while (statusLocation.childNodes.length > 0) { statusLocation.childNodes[0].remove() } var stateString = '' if (group.status.state === 'arrival') { stateString += 'when arriving at ' } else if (group.status.state === 'departure') { stateString += 'when departing from ' } else if (group.status.state === 'passing') { stateString += 'while passing through ' } statusLocation.appendChild(document.createTextNode(stateString)) var stationSpan = document.createElement('span') statusLocation.appendChild(stationSpan) stationSpan.textContent = group.status.station stationSpan.classList.add('station') } else { document.getElementById('status').classList.add('hidden') } var stationsDiv = document.getElementById('stations') while (stationsDiv.childNodes.length > 0) { stationsDiv.childNodes[0].remove() } var separator = document.createElement('h4') stationsDiv.appendChild(separator) separator.textContent = 'Stations' var stationsList = document.createElement('ul') stationsDiv.appendChild(stationsList) /** * @type {string[] | null} */ var journeyStations = null if (startId && stopId) { journeyStations = [] var include = false for (var si = 0; si < group.stopovers.length; si++) { if (group.stopovers[si].stop.id === startId) { include = true } if (include) { journeyStations.push(group.stopovers[si].stop.id) } if (group.stopovers[si].stop.id === stopId) { break } } } group.stopovers.forEach(function (station) { var stationItem = document.createElement('li') stationsList.appendChild(stationItem) stationItem.classList.add('stationItem') if (station.cancelled && !station.arrival && !station.departure) { stationItem.classList.add('cancelled') } if (journeyStations && !journeyStations.includes(station.stop.id)) { stationItem.classList.add('not-in-journey') } var stationName = document.createElement('p') stationItem.appendChild(stationName) stationName.classList.add('pri', 'name') var stationNameHref = document.createElement('a') stationName.appendChild(stationNameHref) stationNameHref.textContent = station.stop.name stationNameHref.classList.add('items', 'no-a-custom') var stationUrl = new URL('/view-station.html', window.location.origin) stationUrl.searchParams.append('stationId', station.stop.id) stationUrl.searchParams.append('date', (station.arrival || station.departure)) stationNameHref.href = stationUrl.toString() if (station.arrival) { var stationArrival = document.createElement('div') stationItem.appendChild(stationArrival) stationArrival.classList.add('arrival') var originalArr = document.createElement('p') stationArrival.appendChild(originalArr) var originalArrSpan = document.createElement('pre') originalArr.appendChild(originalArrSpan) var arrDate = new Date(station.plannedArrival) originalArrSpan.textContent = arrDate.toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' }) originalArr.classList.add('pri') if (station.arrivalDelay) { originalArr.classList.remove('pri') originalArr.classList.add('thi') originalArrSpan.classList.add('original') var delaySpanArr = document.createElement('span') originalArr.appendChild(delaySpanArr) delaySpanArr.textContent = `${station.arrivalDelay > 0 ? '+' : ''}${station.arrivalDelay / 60}`; delaySpanArr.classList.add(station.arrivalDelay > 0 ? 'late' : 'early') delaySpanArr.style.marginLeft = '4px' var actualArr = document.createElement('p') stationArrival.appendChild(actualArr) arrDate.setSeconds(arrDate.getSeconds() + station.arrivalDelay) actualArr.classList.add('pri', station.arrivalDelay > 0 ? 'late' : 'early') // if (!station.arrival.status.real) { // actualArr.classList.add('not-real') // } var actualArrPre = document.createElement('pre') actualArr.appendChild(actualArrPre) actualArrPre.textContent = arrDate.toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' }) } } if (station.departure) { var stationDeparture = document.createElement('div') stationItem.appendChild(stationDeparture) stationDeparture.classList.add('departure') var originalDep = document.createElement('p') stationDeparture.appendChild(originalDep) var depDate = new Date(station.plannedDeparture) var originalDepSpan = document.createElement('pre') originalDep.appendChild(originalDepSpan) originalDepSpan.textContent = depDate.toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' }) originalDep.classList.add('pri') if (station.departureDelay) { originalDep.classList.remove('pri') originalDep.classList.add('thi') originalDepSpan.classList.add('original') var delaySpanDep = document.createElement('span') originalDep.appendChild(delaySpanDep) delaySpanDep.textContent = `${station.departureDelay > 0 ? '+' : ''}${station.departureDelay / 60}`; delaySpanDep.classList.add(station.departureDelay > 0 ? 'late' : 'early') delaySpanDep.style.marginLeft = '4px' var actualDep = document.createElement('p') stationDeparture.appendChild(actualDep) depDate.setSeconds(depDate.getSeconds() + station.departureDelay) actualDep.classList.add('pri', station.departureDelay > 0 ? 'late' : 'early') // if (!station.departure.status.real) { // actualDep.classList.add('not-real') // } var actualDepPre = document.createElement('pre') actualDep.appendChild(actualDepPre) actualDepPre.textContent = depDate.toLocaleTimeString([], { 'hour': '2-digit', 'minute': '2-digit' }) } } // var stationKm = document.createElement('p') // stationItem.appendChild(stationKm) // stationKm.textContent = `${station.km} km` // stationKm.classList.add('thi', 'km') // if (!showKm) { // stationKm.classList.add('hidden') // } if (station.arrivalPlatform || station.departurePlatform) { var stationPlatform = document.createElement('p') stationItem.appendChild(stationPlatform) stationPlatform.textContent = `platform ${station.departurePlatform || station.arrivalPlatform}` stationPlatform.classList.add('thi', 'platform') if (station.departurePlatform && station.departurePlatform !== station.plannedDeparturePlatform) { stationPlatform.classList.add('changed') } } var stationNotes = document.createElement('div') stationItem.appendChild(stationNotes) stationNotes.classList.add('notes') if (station.remarks) { station.remarks.forEach(function (remark) { var noteP = document.createElement('p') stationNotes.appendChild(noteP) noteP.classList.add('note', 'thi', `remark-${remark.type}`) noteP.textContent = remark.text }) } if (journeyStations && station.stop.id === journeyStations[0]) { var boardNoteP = document.createElement('p') stationNotes.appendChild(boardNoteP) boardNoteP.classList.add('note', 'thi', 'remark-board') boardNoteP.textContent = 'Board here' } if (journeyStations && station.stop.id === journeyStations[journeyStations.length - 1]) { var boardNoteP = document.createElement('p') stationNotes.appendChild(boardNoteP) boardNoteP.classList.add('note', 'thi', 'remark-exit') boardNoteP.textContent = 'Exit here' } // if (station.notes && station.notes.length > 0) { // station.notes.forEach(function (note) { // var noteP = document.createElement('p') // stationNotes.appendChild(noteP) // noteP.classList.add('note', 'thi') // switch (note.kind) { // case 'departsAs': { // noteP.textContent = 'Train departs as ' // trainIdSpan(note.rank, note.number, noteP) // break // } // case 'detachingWagons': { // noteP.textContent = `Detaching wagons to ${note.station}` // break // } // case 'receivingWagons': { // noteP.textContent = `Receiving wagons from ${note.station}` // break // } // case 'trainNumberChange': { // noteP.textContent = 'Train changes number to ' // trainIdSpan(note.rank, note.number, noteP) // break // } // } // }) // } if (station.cancelled && !station.arrival && !station.departure) { var noteP = document.createElement('p') stationNotes.appendChild(noteP) noteP.classList.add('note', 'thi') noteP.textContent = 'Stop is cancelled' } }) if (group.remarks && group.remarks.length > 0) { var remarksSeparator = document.createElement('h4') stationsDiv.appendChild(remarksSeparator) remarksSeparator.textContent = 'Remarks' var remarksList = document.createElement('ul') stationsDiv.appendChild(remarksList) remarksList.classList.add('remarks') group.remarks.forEach(function (remark) { var remarkItem = document.createElement('li') remarksList.append(remarkItem) remarkItem.classList.add('remarkItem', `remark-${remark.type}`) var remarkSummaryP = document.createElement('p') remarkItem.appendChild(remarkSummaryP) remarkSummaryP.classList.add('remark', 'pri') remarkSummaryP.textContent = remark.summary var remarkTextP = document.createElement('p') remarkItem.appendChild(remarkTextP) remarkTextP.classList.add('remark', 'sec') remarkTextP.textContent = remark.text }) } var mapDiv = document.getElementById('map') if (group.polyline) { console.log(group.polyline) mapDiv.classList.remove('hidden') var actualMap = document.getElementById('actual-map') if (!actualMap) { actualMap = document.createElement('div') actualMap.id = 'actual-map' mapDiv.insertBefore(actualMap, null) } if (!leafletMap) { leafletMap = L.map('actual-map', { zoomSnap: 0.5, // zoomDelta: 0.5, }).setView([51.1657, 10.4515], 6) L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(leafletMap); L.tileLayer('https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', { attribution: ' Style: CC-BY-SA 2.0 OpenRailwayMap and OpenStreetMap', minZoom: 2, maxZoom: 19, tileSize: 256 }).addTo(leafletMap) } if (mapLayers) { mapLayers.forEach(function (layer) { layer.remove() }) mapLayers = [] } /** * @type {[number, number][]} */ var lines = [] /** * @type {[number, number][] | null} */ var journeyLines = null include = false var stops = [] var minLat = group.polyline.features[0].geometry.coordinates[1] var maxLat = minLat var minLng = group.polyline.features[0].geometry.coordinates[0] var maxLng = minLng for (var pi = 0; pi < group.polyline.features.length; pi++) { var feature = group.polyline.features[pi] var coord = feature.geometry.coordinates lines.push([coord[1], coord[0]]) if (journeyStations && include !== -1) { if (feature.properties && feature.properties.id === journeyStations[0]) { minLat = maxLat = coord[1] minLng = maxLng = coord[0] include = true } if (include) { if (!journeyLines) journeyLines = [] journeyLines.push([coord[1], coord[0]]) } if (feature.properties && feature.properties.id === journeyStations[journeyStations.length - 1]) { include = -1 } } if (feature.properties && feature.properties.type == 'stop') { stops.push({ coord: [coord[1], coord[0]], id: feature.properties.id, }) } if (!journeyStations || include === true) { if (minLat > coord[1]) { minLat = coord[1] } if (minLng > coord[0]) { minLng = coord[0] } if (maxLat < coord[1]) { maxLat = coord[1] } if (maxLng < coord[0]) { maxLng = coord[0] } } } mapLayers.push(L.polyline(lines, { color: !journeyLines ? '#6666ff' : '#9999dd', opacity: !journeyLines ? 1.0 : 0.8, weight: 4, }).addTo(leafletMap)); if (journeyLines) { mapLayers.push(L.polyline(journeyLines, { color: '#6666ff', weight: 2, }).addTo(leafletMap)); } for (var si = 0; si < stops.length; si++) { var stop = stops[si] mapLayers.push(L.circleMarker(stop.coord, { color: !journeyStations || journeyStations.includes(stop.id) ? '#ffff66' : '#dfdf66', radius: 5, }).addTo(leafletMap)) } leafletMap.flyToBounds([[minLat, minLng], [maxLat, maxLng]], { duration: 0.2, }) actualMap.scrollIntoView() } else if (loadMap) { mapDiv.classList.add('hidden') } lastSuccessfulFetch = fetchDate || new Date() } var refreshStopToken = null /** * @typedef DbProducts * @property {boolean} nationalExpress * @property {boolean} national * @property {boolean} regionalExpress * @property {boolean} regional * @property {boolean} suburban * @property {boolean} bus * @property {boolean} ferry * @property {boolean} subway * @property {boolean} tram * @property {boolean} taxi */ /** * @typedef DbLocation * @property {'location'} type * @property {?string} id * @property {number} latitude * @property {number} longitude */ /** * @typedef DbLine * @property {'line'} type * @property {string} id * @property {string} fahrtNr * @property {string} name * @property {boolean} public * @property {string} adminCode * @property {string} productName * @property {string} mode * @property {string} product * @property {{type: 'operator', id: string, name: string}} operator * @property {string} additionalName */ /** * @typedef DbStop * @property {'stop'} type * @property {?string} id * @property {string} name * @property {DbLocation} location * @property {DbProducts} products */ /** * @typedef DbRemark * @property {string} type * @property {string} text * @property {string} code * @property {string} summary */ /** * @typedef DbArrDep * @property {string | null} arrival * @property {string | null} plannedArrival * @property {number | null} arrivalDelay * @property {string | null} arrivalPlatform * @property {string | null} plannedArrivalPlatform * @property {string | null} arrivalPrognosisType * @property {string | null} departure * @property {string | null} plannedDeparture * @property {number | null} departureDelay * @property {string | null} departurePlatform * @property {string | null} plannedDeparturePlatform * @property {string | null} departurePrognosisType */ /** * @typedef DbTrip * @property {DbStop} origin * @property {DbStop} destination * @property {DbLine} line * @property {string} direction * @property {DbLocation} location * @property {({stop: DbStop, cancelled?: boolean, remarks?: DbRemark[]} & DbArrDep)[]} stopovers * @property {DbRemark[]} remarks * @property {string} id */ /** * @param {string | null} tripId * @returns {Promise} */ function onTripId(tripId) { if (tripId) { var reqUrl = new URL(`https://v6.db.transport.rest/trips/${encodeURIComponent(tripId)}`) reqUrl.searchParams.append('stopovers', 'true') reqUrl.searchParams.append('remarks', 'true') if (loadMap) { reqUrl.searchParams.append('polyline', 'true') } return fetch( reqUrl.toString(), { cache: 'no-store', }, ).then(function (response) { if (!response.ok) { // Check in 10 seconds if server returned error return Promise.reject('Response not okay') } var cacheDate = response.headers.get('SW-Cached-At') return response.json().then(function (data) { data['$cacheDate'] = cacheDate return data }) }).then(function (response) { if (!response) { return Promise.reject('No JSON response') } var cacheDate = response['$cacheDate'] if (cacheDate) { cacheDate = new Date(cacheDate) } var success = !cacheDate trainData = response.trip onTrainData(response.trip, cacheDate) return success }) } else { return Promise.reject('No tripId found') } } /** * Callback for when trips are found * @param {string} wantedTrainNumber * @param {{trips: (DbTrip & DbArrDep)[]}} data * @param {?Date} fetchDate */ function onTripsData(wantedTrainNumber, data, fetchDate) { data.trips.forEach(function (trip) { if (trip.line.fahrtNr == wantedTrainNumber || trip.line.name == wantedTrainNumber) { tripId = trip.id console.group('Found tripId by train number') console.log(`ID: ${trip.id}`) console.log(`Fahrt Nr: ${trip.line.fahrtNr}`) console.log(`Name: ${trip.line.name}`) console.log(`+Name: ${trip.line.additionalName}`) console.groupEnd() } }) return onTripId(tripId) } /** * @returns {Promise} */ function refresh() { function reschedule(timeout) { if (refreshStopToken != null) { clearTimeout(refreshStopToken) } refreshStopToken = setTimeout(function () { refresh() }, timeout || 60000) } if (tripId) { return onTripId(tripId) } /** * @type {Date} */ var reqDate = new Date(date.valueOf()) if (yesterday) { reqDate.setDate(reqDate.getDate() - 1) } reqDate.setMinutes(0, 0, 0) var reqUrl = new URL(`https://v6.db.transport.rest/trips`) reqUrl.searchParams.append('query', trainNumber) if (loadMap) { reqUrl.searchParams.append('polyline', 'true') } reqUrl.searchParams.append('when', reqDate.toISOString()) return fetch( reqUrl.toString(), { cache: 'no-store', }, ).then(function (response) { if (!response.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) { data['$cacheDate'] = cacheDate return data }) }).then(function (response) { if (!response) { return } var cacheDate = response['$cacheDate'] if (cacheDate) { cacheDate = new Date(cacheDate) } var success = !cacheDate return onTripsData(trainNumber, response, cacheDate) .then(function () { // Check in 1 seconds if network error reschedule(success ? undefined : 1000) return success }) }).catch(function (e) { // Check in 1 second if network error reschedule(1000) throw e }) } window.addEventListener('unload', function (e) { if (refreshStopToken != null) { clearTimeout(refreshStopToken) } }) function rsk() { function changeRskText(newText) { document.querySelectorAll('.rsk').forEach(function (elem) { elem.textContent = newText }) } changeRskText('Refreshing...') refresh().catch(function () { return false}).then(function (success) { if (!success) { changeRskText('Refreshing failed') setTimeout(function (_) { changeRskText('Refresh') }, 3000) } else { changeRskText('Refresh') } }) } window.addEventListener('popstate', function (e) { if (yesterday) { yesterday = false trainData = null } else { groupIndex = null } if (trainData) { onTrainData(trainData) } else { refresh() } }) window.addEventListener('load', function (e) { var sp = new URL(window.location.href).searchParams if (!sp.has('train') && !sp.has('tripId')) { window.history.back() this.setTimeout(function () { var url = new URL(window.location.href) url.pathname = 'train.html' window.location.href = url.toString() }, 100) } trainNumber = sp.get('train') date = sp.has('date') ? new Date(sp.get('date')) : new Date() groupIndex = sp.has('groupIndex') ? parseInt(sp.get('groupIndex')) : null tripId = sp.get('tripId') startId = sp.get('startId') stopId = sp.get('stopId') if (window.localStorage) { var oldRecent = localStorage.getItem('recent/view-train') if (oldRecent) { oldRecent = JSON.parse(oldRecent) } localStorage.setItem('recent/view-train', JSON.stringify({ trainNumber: trainNumber, tripId: tripId, date: date.toISOString(), $addDate: new Date().toISOString(), })) } document.querySelectorAll('.rsk').forEach(function (rskElem) { rskElem.addEventListener('click', function (e) { rsk() }) }) if (navigator.canShare && navigator.canShare({ url: '' })) { document.getElementById('title').addEventListener('click', function () { navigator.share({ url: '' }); }) } var content = document.getElementsByClassName('content')[0] content.focus() content.addEventListener('keydown', function (e) { switch (e.key) { // case 'ArrowUp': // content.scrollBy(0, -50) // break // case 'ArrowDown': // content.scrollBy(0, 50) // break case 'SoftRight': rsk() break case '1': date.setDate(date.getDate() - 1) refresh() break case '3': date.setDate(date.getDate() + 1) refresh() break case '7': showKm = !showKm document.querySelectorAll('.km').forEach(function (kmItem) { if (showKm) { kmItem.classList.remove('hidden') } else { kmItem.classList.add('hidden') } }) break default: console.log(e.key) } }) document.getElementById('load-map-button').addEventListener('click', function (event) { event.preventDefault() event.target.textContent = 'Loading...' loadMap = true; refresh() }) refresh() setInterval(function () { if (!lastSuccessfulFetch) { return } var millis = new Date() - lastSuccessfulFetch var secs = Math.floor(millis / 1000) var timeStr = '' if (secs / 3600 >= 1) { timeStr += `${Math.floor(secs / 3600)}h` secs = secs % 3600 } if (secs / 60 >= 1) { timeStr += `${Math.floor(secs / 60)}m` secs = secs % 60 } if (secs >= 1) { timeStr += `${Math.floor(secs)}s` } if (!timeStr) { document.querySelectorAll('.lsk').forEach(function (elem) { elem.textContent = 'Last refreshed now' elem.classList.add('last-refreshed') }) } else { document.querySelectorAll('.lsk').forEach(function (elem) { elem.textContent = `Last refreshed ${timeStr} ago` elem.classList.add('last-refreshed') }) } }, 500) // if (this.localStorage && !this.localStorage.getItem('info-yesterday')) { // this.alert("New feature: You can now view yesterday's train by selecting the date!") // this.localStorage.setItem('info-yesterday', 'true') // } })