const internalData = { lastUpdate: 0, stations: [], }; let geolocationPermission = false; let geolocation = null; const openStations = new Set(); let sortByDistance = false; const substituteData = [ { name: 'Borgweg (Stadtpark)', coordinates: [53.5907696, 10.0147719], }, { name: 'Emilienstraße', coordinates: [53.5716862, 9.9525424], }, { name: 'Garstedt', coordinates: [53.6844739, 9.9860415], }, { name: 'Hagenbecks Tierpark', coordinates: [53.5925874, 9.9440359], }, { name: 'Hamburg Hbf', searchTarget: "Hauptbahnhof", }, { name: 'Jungfernstieg', searchTarget: "Rathaus", }, { name: 'Rathaus', searchTarget: "Jungfernstieg", }, { name: 'Stephansplatz (Oper/CCH)', searchTarget: "Dammtor (Messe/CCH)", }, { name: 'Dammtor (Messe/CCH)', searchTarget: "Stephansplatz (Oper/CCH)", }, { name: 'Hauptbahnhof Nord', coordinates: [53.5541197, 10.0061270], }, { name: 'Hoheneichen', coordinates: [53.6355141, 10.0677176], }, { name: 'Kornweg (Klein Borstel)', coordinates: [53.6324430, 10.0541722], }, { name: 'Lauenbrück', coordinates: [53.1971209, 9.5640765], }, { name: 'Lutterothstraße', coordinates: [53.5819938, 9.9476215], }, { name: 'Meckelfeld', coordinates: [53.4248897, 10.0291223], }, { name: 'Sengelmannstraße (City Nord)', coordinates: [53.6093953, 10.0220004], }, { name: 'St.Pauli', coordinates: [53.5507957, 9.9700752], }, { name: 'Winsen(Luhe)', coordinates: [53.3534304, 10.2086841], }, ] async function loadElevators() { document.querySelector('#errorMessage').classList.add('hidden'); const res = await fetch('https://www.hvv.de/elevators', { referrer: "", }).catch(e => { document.querySelector('#errorMessage').classList.remove('hidden'); }); 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 stationComment = station['mainSubStation']['comment']; let searchTarget = undefined; const substitute = substituteData.filter(subs => subs.name === stationName) if (substitute.length && substitute[0].hasOwnProperty('searchTarget')) { searchTarget = substitute[0].searchTarget; } 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: stationComment, searchTarget: searchTarget, state: stationState, lines: stationLines, types: stationTypes, elevators: elevators, } } localStorage.setItem("internal_data", JSON.stringify({ api: minorVersion, ...internalData })); } async function loadOsmData() { const nodeIdList = []; for (const station of internalData.stations) { for (const elevator of station.elevators) { nodeIdList.push(elevator.osmNodeId) } } const osmResponse = await fetch(`https://overpass-api.de/api/interpreter?data=[out:json];node(id:${nodeIdList.join(',')});out%20body;`, {}); const osmJson = await osmResponse.json(); if (!osmJson.hasOwnProperty('elements')) return; const osmNodes = {}; for await (const node of osmJson.elements) { if (node.hasOwnProperty('tags')) { const tags = node['tags']; if (tags['highway'] === 'elevator') { osmNodes[node['id']] = node; } else { console.warn(`OSM Node is not an elevator. (NodeID: ${node['id']})`); } } else { console.warn(`OSM Node has no Tags. (NodeID: ${node['id']})`); } } //update coordinates in stations for (const stationIndex in internalData.stations) { const station = internalData.stations[stationIndex]; for (const elevator of station.elevators) { const node = osmNodes[elevator.osmNodeId] if (node) { internalData.stations[stationIndex]['coordinates'] = [ node['lat'], node['lon'], ] } } if (!internalData.stations[stationIndex].hasOwnProperty('coordinates')) { const substitute = substituteData.filter(subs => subs.name === internalData.stations[stationIndex].name) if (substitute.length && substitute.hasOwnProperty('coordinates')) { internalData.stations[stationIndex]['coordinates'] = substitute[0].coordinates; } } } localStorage.setItem("osm_data", JSON.stringify({ api: minorVersion, lastUpdate: new Date(), nodes: osmNodes })); localStorage.setItem("internal_data", JSON.stringify({ api: minorVersion, ...internalData })); } 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) => { if (geolocation === null) { geolocation = [pos.coords.latitude, pos.coords.longitude]; renderData(geolocation); } }, (e) => { console.warn(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() 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 sortStationsByDistance(stationA, stationB) { const distanceA = stationA.distance; // ignore upper and lowercase const distanceB = stationB.distance; // ignore upper and lowercase if (distanceA < distanceB) { return -1; } if (distanceA > distanceB) { 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(location = null) { 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; } if ( location !== null && ( location.length !== 2 || typeof location[0] !== 'number' || typeof location[1] !== 'number' ) ) { console.error('No valid location provided') 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'); const oldDataWarning = document.querySelector('#oldDataWarning'); const lastUpdate = new Date(internalData.lastUpdate); const now = new Date(); dateContainer.innerHTML = dateTimeStyle.format(lastUpdate); oldDataWarning.classList.add('hidden'); if (now - lastUpdate > 86400 * 1000) { const days = numberFormat.format((now - lastUpdate) / (86400 * 1000)); oldDataWarning.classList.remove('hidden'); oldDataWarning.innerHTML = `Daten ${days} Tag${days !== '1' ? 'e' : ''} alt!`; } const listContainer = document.querySelector('#stationList'); //clear list before update listContainer.innerHTML = ''; let stations = [...internalData['stations']]; for (const stationIndex in stations) { const station = stations[stationIndex]; station.id = stationIndex; if (location !== null) { if (station.hasOwnProperty('coordinates')) { station.distance = distance(location, station.coordinates); } else { console.log('station has no position:', station.name); } } } stations = stations.sort(sortStationsByDistance); for (const stationIndex in stations) { const station = stations[stationIndex]; if (location !== null) { if (station.hasOwnProperty('coordinates')) { station.distance = distance(location, station.coordinates); } else { console.log('station has no position:', station.name); } } let elevatorsTemplate = ''; let previewTemplate = ''; for (const elevator of station.elevators) { const stateTemplate = ` `; let linesTemplate = ''; linesTemplate = `
${station.comment}