458 lines
No EOL
18 KiB
JavaScript
458 lines
No EOL
18 KiB
JavaScript
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 = '';
|
|
|
|
for (const elevator of station.elevators) {
|
|
let linesTemplate = '<div class="lineList">Linien: ';
|
|
for (const line of elevator.lines) {
|
|
linesTemplate += `<span data-type="${line.type}" data-line="${line.line}" class="lineChip">${line.line}</span>`;
|
|
}
|
|
linesTemplate += '</div>';
|
|
|
|
let levelsTemplate = '<ol class="levelList">';
|
|
for (const levelDescription of elevator.levels) {
|
|
levelsTemplate += `<li>${levelDescription}</li>`;
|
|
}
|
|
levelsTemplate += '</ol>';
|
|
|
|
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 = '<dl>';
|
|
osmTemplate += `<div>
|
|
<dt><span data-icon="location" class="size-l"></span>Link zur Karte</dt>
|
|
<dd>
|
|
<a href="https://www.openstreetmap.org/node/${elevator.osmNodeId}" target="_blank">
|
|
Auf Karte anzeigen
|
|
</a>
|
|
</dd>
|
|
</div>`;
|
|
if (tags.hasOwnProperty('description')) {
|
|
osmTemplate += `<div><dt><span data-icon="info" class="size-l"></span>Beschreibung</dt><dd>${tags['description']}</dd></div>`;
|
|
}
|
|
if (tags.hasOwnProperty('level')) {
|
|
osmTemplate += `<div><dt><span data-icon="up-down" class="size-l"></span>Ebenen</dt><dd>${tags['level'].split(';').sort().join(', ')}</dd></div>`;
|
|
}
|
|
if (tags.hasOwnProperty('wheelchair')) {
|
|
osmTemplate += `<div><dt><span data-icon="wheelchair" class="size-l"></span>Rollstühle</dt><dd>${tags['wheelchair'] === 'yes' ? 'Ja' : 'Nein'}</dd></div>`;
|
|
}
|
|
if (tags.hasOwnProperty('bicycle')) {
|
|
osmTemplate += `<div><dt><span data-icon="bicycle" class="size-l"></span>Fahrräder</dt><dd>${tags['bicycle'] === 'yes' ? 'Ja' : 'Nein'}</dd></div>`;
|
|
}
|
|
|
|
osmTemplate += '</dl>';
|
|
} 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 += `<span data-icon="${elevator.working || elevator.stateUnavailable ? 'elevator' : 'elevator-slash'}"
|
|
class="size-l ${elevator.working ? 'text-green' : elevator.stateUnavailable ? 'text-orange' : 'text-red'}"></span>`
|
|
|
|
|
|
elevatorsTemplate += `<li class="elevator">
|
|
<span data-icon="${elevator.working || elevator.stateUnavailable ? 'elevator' : 'elevator-slash'}"
|
|
class="size-xl ${elevator.working ? 'text-green' : elevator.stateUnavailable ? 'text-orange' : 'text-red'}"
|
|
role="img"
|
|
aria-label="${elevator.stateUnavailable
|
|
? 'Status nicht verfügbar.'
|
|
: elevator.working
|
|
? 'Funktionsfähig'
|
|
: 'Außer Betrieb'}">
|
|
</span>
|
|
<div class="elevatorData">
|
|
${elevator.lines.length ? `${linesTemplate}` : ''}
|
|
${elevator.instCause !== '' ? `<div class="elevatorInfo">${elevator.instCause}</div>` : ''}
|
|
${elevator.levels.length ? levelsTemplate : elevator.description}
|
|
<dl>
|
|
<div>
|
|
<dt><span data-icon="fit-width" class="size-l"></span>Durchgang</dt>
|
|
<dd>${elevator.dimensions.door} cm</dd>
|
|
</div>
|
|
<div>
|
|
<dt><span data-icon="width" class="size-l"></span>Breite</dt>
|
|
<dd>${elevator.dimensions.width} cm</dd>
|
|
</div>
|
|
<div>
|
|
<dt><span data-icon="length" class="size-l"></span>Länge</dt>
|
|
<dd>${elevator.dimensions.length} cm</dd>
|
|
</div>
|
|
<div>
|
|
<dt><span data-icon="braille" class="size-l"></span>Braille</dt>
|
|
<dd>${elevator.braille === -1 ? 'unbekannt' : elevator.braille ? `verfügbar` : 'nicht verfügbar'}</dd>
|
|
</div>
|
|
<div>
|
|
<dt><span data-icon="speaker" class="size-l"></span>Ansage</dt>
|
|
<dd>${elevator.speaker === -1 ? 'unbekannt' : elevator.speaker ? `verfügbar` : 'nicht verfügbar'}</dd>
|
|
</div>
|
|
</dl>
|
|
<!--<hr>-->
|
|
${osmTemplate ? `<div class="osm" data-nodeid="${elevator.osmNodeId}">
|
|
<div class="osmHeading">
|
|
<h4>Daten von OpenStreetMap</h4>
|
|
<button class="loadOSM size-s">
|
|
OSM Daten aktualisieren
|
|
<span data-icon="load" class="size-s spinner hidden"></span>
|
|
</button>
|
|
</div>
|
|
${osmTemplate}
|
|
</div>` : `<button class="loadOSM">
|
|
Zusätzliche Daten von OpenStreetMap abrufen
|
|
<span data-icon="load" class="size-s spinner hidden"></span>
|
|
</button>`}
|
|
</div>
|
|
</li>`;
|
|
}
|
|
|
|
const template = `<li class="station" id="station_${stationIndex}">
|
|
<div class="stationSummary">
|
|
<div class="typeList">
|
|
${station.types.sort().map(t => `<span class="typeChip" data-type="${t}">${t}</span>`).join('')}
|
|
</div>
|
|
<div class="stationTitle">
|
|
<h3>${station.name}</h3>
|
|
<div class="elevator-preview" role="img"
|
|
aria-label="${station.state.working ? `${station.state.working} ${station.state.working > 1 ? 'Aufzüge sind' : 'Aufzug ist'} funktionstüchtig.` : ''}
|
|
${station.state.outOfOrder ? `${station.state.outOfOrder} ${station.state.outOfOrder > 1 ? 'Aufzüge sind' : 'Aufzug ist'} außer Betrieb.` : ''}
|
|
${station.state.unavailable ? `Bei ${station.state.unavailable} ${station.state.unavailable > 1 ? 'Aufzügen' : 'Aufzug'} ist der Funktionsstatus unbekannt.` : ''}">
|
|
${previewTemplate}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<details data-stationid="${stationIndex}" ${openStations.has(stationIndex) ? 'open' : ''}>
|
|
<summary>
|
|
Aufzüge anzeigen
|
|
</summary>
|
|
<ul class="elevatorList collapsed" aria-expanded="false" id="station_${stationIndex}_elevatorList">
|
|
${elevatorsTemplate}
|
|
</ul>
|
|
</details>
|
|
</li>`;
|
|
listContainer.insertAdjacentHTML('beforeend', template);
|
|
|
|
//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() |