const internalData = { lastUpdate: 0, stations: [], }; let geolocationPermission = false; let geolocation = [null, null]; const openStations = new Set(); Object.defineProperty(String.prototype, 'capitalize', { value: function () { return this.charAt(0).toUpperCase() + this.toLowerCase().slice(1); }, enumerable: false }); async function loadElevators() { const res = await fetch('https://www.hvv.de/elevators', {referrer: ""}); const data = await res.json(); const stations = data['stations']; stations.sort(sortStations) internalData.lastUpdate = new Date(data['lastUpdate']); let stationIndex = 0; for (const station of stations) { const stationName = station['mainSubStation']['stationName']; const lines = new Set(); const elevators = []; for (const elevatorKey of Object.keys(station.elevators)) { const elevatorApi = station.elevators[elevatorKey]; const elevatorLines = []; for (let line of elevatorApi.lines) { line = line.replace(/[()]/g, ""); lines.add(line); elevatorLines.push({ line: line, type: getType(line), }); } // try to detect levels from description let levels = []; if (elevatorApi.description.search('<->') >= 0) { levels = elevatorApi.description.split('<->').map(level => level.trim()); } else if (elevatorApi.description.search('<>') >= 0) { levels = elevatorApi.description.split('<>').map(level => level.trim()); } else if (elevatorApi.description.search('/ ') >= 0) { levels = elevatorApi.description.split('/ ').map(level => level.trim()); } const elevator = { working: elevatorApi['state'] === 1, stateUnavailable: elevatorApi['state'] === -1, dimensions: { width: elevatorApi['cabinWidth'], length: elevatorApi['cabinLength'], door: elevatorApi['doorWidth'], }, description: elevatorApi['description'], label: elevatorApi['label'], type: elevatorApi['type'].replace('_', ' ').capitalize(), braille: elevatorApi['tasterType'] === 'UNBEKANNT' ? -1 : elevatorApi['tasterType'] === 'KOMBI' || elevatorApi['tasterType'] === 'BRAILLE', speaker: elevatorApi['tasterType'] === 'UNBEKANNT' ? -1 : elevatorApi['tasterType'] === 'KOMBI', lines: elevatorLines, levels: levels, instCause: elevatorApi['instCause'], osmNodeId: elevatorApi['osmId'], } elevators.push(elevator); } let stationState = { unavailable: 0, working: 0, outOfOrder: 0, } for (const elevator of elevators) { if (elevator.stateUnavailable) { stationState.unavailable++; } else if (elevator.working) { stationState.working++; } else { stationState.outOfOrder++; } } const stationLines = Array.from(lines); const stationTypes = Array.from(getTypes(stationLines)); internalData.stations[stationIndex++] = { name: stationName, comment: station['comment'], state: stationState, lines: stationLines, types: stationTypes, elevators: elevators, } } localStorage.setItem("internal_data", JSON.stringify(internalData)); } const dateTimeStyle = new Intl.DateTimeFormat('de-DE', { dateStyle: 'medium', timeStyle: 'medium', timeZone: 'Europe/Berlin', }) async function loadOsmData() { const nodeIdList = []; for (const station of internalData.stations) { for (const elevator of station.elevators) { nodeIdList.push(elevator.osmNodeId) } } const osmData = await fetch(`https://overpass-api.de/api/interpreter?data=[out:json];node(id:${nodeIdList.join(',')});out%20body;`, {}); const osmJson = await osmData.json(); if (!osmJson.hasOwnProperty('elements')) return; localStorage.setItem("osm_data", JSON.stringify(osmJson)); } function distance([lat1, lon1], [lat2, lon2]) { if (!lat1 || !lat2 || !lon1 || !lon2) return null const R = 6371e3; // metres const phi1 = lat1 * Math.PI / 180; // φ, λ in radians const phi2 = lat2 * Math.PI / 180; const dPhi = (lat2 - lat1) * Math.PI / 180; const dLambda = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dPhi / 2) * Math.sin(dPhi / 2) + Math.cos(phi1) * Math.cos(phi2) * Math.sin(dLambda / 2) * Math.sin(dLambda / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; // in metres } function registerGeolocationWatcher() { navigator.geolocation.watchPosition((pos) => { geolocation = [pos.coords.latitude, pos.coords.longitude] }, (e) => { console.log(e) }, { enableHighAccuracy: true, timeout: 5000, maximumAge: 0, }) } function checkGeolocationPermission() { navigator.permissions.query({name: "geolocation"}).then((result) => { geolocationPermission = result.state if (result.state === 'granted') { registerGeolocationWatcher() } }); } function allowGeolocation() { navigator.geolocation.getCurrentPosition(() => { console.log('success') }, () => { console.log('error') }); checkGeolocationPermission() registerGeolocationWatcher() } checkGeolocationPermission() document.querySelector('#stationsNearMe').addEventListener('click', allowGeolocation) function sortStations(stationA, stationB) { const nameA = stationA.mainSubStation.stationName.toUpperCase(); // ignore upper and lowercase const nameB = stationB.mainSubStation.stationName.toUpperCase(); // ignore upper and lowercase if (nameA < nameB) { return -1; } if (nameA > nameB) { return 1; } // names must be equal return 0; } function getType(line) { const type = line.replace(/[^A-z]/g, ""); switch (type) { case 'RE': case 'RB': case 'R': case 'DB': return 'R'; case 'S': return 'S'; case 'U': return 'U'; case 'A': return 'A'; } } function getTypes(lines) { const types = new Set(); for (const line of lines) { types.add(getType(line)) } return types; } function renderData() { const ls = JSON.parse(localStorage.getItem("internal_data")); if (!ls) return; internalData.lastUpdate = ls['lastUpdate']; internalData.stations = ls['stations']; const osmData = JSON.parse(localStorage.getItem("osm_data")); if (!internalData || !internalData.stations.length) { console.error('No Data available!') return; } document.querySelector('#updateInfo').classList.remove('hidden'); document.querySelector('#loadElevators').classList.remove('hidden'); document.querySelector('#filters').classList.remove('hidden'); document.querySelector('#initialLoad').classList.add('hidden'); const dateContainer = document.querySelector('#lastUpdated'); dateContainer.innerHTML = dateTimeStyle.format(new Date(internalData.lastUpdate)); const listContainer = document.querySelector('#stationList'); //clear list before update listContainer.innerHTML = ''; const stations = internalData['stations']; for (const stationIndex in stations) { const station = stations[stationIndex]; let elevatorsTemplate = ''; let previewTemplate = ''; let templateHasOsmButton = false; for (const elevator of station.elevators) { let linesTemplate = '
Linien: '; for (const line of elevator.lines) { linesTemplate += `${line.line}`; } linesTemplate += '
'; let levelsTemplate = '
    '; for (const levelDescription of elevator.levels) { levelsTemplate += `
  1. ${levelDescription}
  2. `; } levelsTemplate += '
'; let osmTemplate = ''; if (osmData) { const nodes = osmData.elements.filter(node => node.id === elevator.osmNodeId) if (nodes.length) { const nodeInfo = nodes[0]; if (nodeInfo.hasOwnProperty('tags')) { const tags = nodeInfo['tags']; if (tags['highway'] === 'elevator') { osmTemplate = '
'; osmTemplate += `
Link zur Karte
Auf Karte anzeigen
`; if (tags.hasOwnProperty('description')) { osmTemplate += `
Beschreibung
${tags['description']}
`; } if (tags.hasOwnProperty('level')) { osmTemplate += `
Ebenen
${tags['level'].split(';').sort().join(', ')}
`; } if (tags.hasOwnProperty('wheelchair')) { osmTemplate += `
Rollstühle
${tags['wheelchair'] === 'yes' ? 'Ja' : 'Nein'}
`; } if (tags.hasOwnProperty('bicycle')) { osmTemplate += `
Fahrräder
${tags['bicycle'] === 'yes' ? 'Ja' : 'Nein'}
`; } osmTemplate += '
'; } else { console.warn(`OSM Node is not an elevator. At:\t${station.name}\t${elevator.label} (NodeID: ${elevator.osmNodeId})`); } } else { console.warn(`OSM Node has no Tags. At:\t${station.name}\t${elevator.label} (NodeID: ${elevator.osmNodeId})`); } } else { console.warn(`OSM Node not found (deleted). At:\t${station.name}\t${elevator.label} (NodeID: ${elevator.osmNodeId})`); } } else { console.warn(`Elevator has no OSM Node id:\t${station.name}\t${elevator}`); } previewTemplate += `` elevatorsTemplate += `
  • ${elevator.lines.length ? `${linesTemplate}` : ''} ${elevator.instCause !== '' ? `
    ${elevator.instCause}
    ` : ''} ${elevator.levels.length ? levelsTemplate : elevator.description}
    Durchgang
    ${elevator.dimensions.door} cm
    Breite
    ${elevator.dimensions.width} cm
    Länge
    ${elevator.dimensions.length} cm
    Braille
    ${elevator.braille === -1 ? 'unbekannt' : elevator.braille ? `verfügbar` : 'nicht verfügbar'}
    Ansage
    ${elevator.speaker === -1 ? 'unbekannt' : elevator.speaker ? `verfügbar` : 'nicht verfügbar'}
    ${osmTemplate ? `

    Daten von OpenStreetMap

    ${osmTemplate}
    ` : ``}
  • `; templateHasOsmButton = templateHasOsmButton || !osmTemplate; } const template = `
  • ${station.types.sort().map(t => `${t}`).join('')}

    ${station.name}

    Aufzüge anzeigen
  • `; listContainer.insertAdjacentHTML('beforeend', template); if (templateHasOsmButton) { //immediate invocation (function () { listContainer.querySelectorAll(`#station_${stationIndex} .loadOSM`).forEach(e => { e.addEventListener('click', (ev) => { ev.target.querySelector('.spinner').classList.remove('hidden'); loadOsmData().then(() => { ev.target.classList.add('hidden'); renderData(); }); }) }) }()); } } listContainer.querySelectorAll(`details`).forEach(e => { e.addEventListener("toggle", (event) => { const stationId = event.target.dataset['stationid']; if (event.target.open) { openStations.add(stationId); } else { openStations.delete(stationId); } }); }) } document.querySelector('#loadElevators') .addEventListener('click', (e) => { e.target.querySelector('.spinner').classList.remove('hidden'); loadElevators().then(() => { e.target.querySelector('.spinner').classList.add('hidden'); renderData(); filterData(); }); }) document.querySelector('#initialLoad') .addEventListener('click', (e) => { e.target.querySelector('.spinner').classList.remove('hidden'); loadElevators().then(() => { e.target.classList.add('hidden'); renderData(); filterData(); }); }) renderData(); function filterData() { const searchString = document.querySelector('#searchStation').value; if (internalData) { for (const stationIndex in internalData.stations) { const matches = internalData.stations[stationIndex].name.toLowerCase().search(searchString.toLowerCase()) >= 0; document.querySelector(`#station_${stationIndex}`).classList.toggle('hidden', !matches); } } } document.querySelector('#searchStation').addEventListener('input', (e) => { filterData(); }) filterData()