Files
Wegwichtel/public/js/app.js
T
Florian Zumpe aff88eba1b
Sonarqube Scanner / Prepare environment (push) Successful in 20s
Sonarqube Scanner / Build and test (push) Failing after 36s
Sonarqube Scanner / SonarQube analysis (push) Skipped
Sonarqube Scanner / Deploy to Server (push) Successful in 9s
Internationalization
2026-10-01 09:41:01 +02:00

853 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function (ns, $) {
'use strict';
const state = {
routes: [],
localRoutes: [],
localRoutesError: null,
route: null,
position: null,
triggered: new Set(),
routeState: 'idle',
activePage: null,
routeProgressIndex: null,
deviceHeading: null,
viewed: new Set(),
playedAudio: new Set(),
activePoiId: null,
persistTimer: null
};
function escapeHtml(value) {
return $('<div>').text(value ?? '').html();
}
function persistRouteProgress() {
if (!state.route) return;
if (state.persistTimer != null) {
globalThis.clearTimeout(state.persistTimer);
state.persistTimer = null;
}
ns.PrivacyStorage.saveRouteState(
state.route,
state.routeState,
state.routeProgressIndex,
[...state.triggered],
[...state.viewed],
[...state.playedAudio]
);
}
function scheduleRouteProgressPersist() {
if (!state.route || state.persistTimer != null) return;
state.persistTimer = globalThis.setTimeout(persistRouteProgress, 1200);
}
function restoreRouteProgress(route) {
const saved = ns.PrivacyStorage.activateRoute(route);
state.triggered = new Set(saved?.visitedPoiIds ?? []);
state.viewed = new Set(saved?.viewedPoiIds ?? []);
state.playedAudio = new Set(saved?.playedAudioPoiIds ?? []);
state.routeProgressIndex = Number.isInteger(saved?.routeProgressIndex)
? saved.routeProgressIndex
: null;
state.routeState = saved?.routeState === 'running' || saved?.routeState === 'paused'
? 'paused'
: 'idle';
if (state.routeState === 'paused') {
$('#tracking-status')
.prop('hidden', false)
.text(ns.I18n.t("app.du.kannst.dort.weitermachen.wo.du.aufgehort"));
} else {
$('#tracking-status').prop('hidden', true).text('');
}
}
function activate(selector, handler) {
$(document)
.on('pointerup', selector, function (event) {
const pointer = event.originalEvent;
if (pointer?.isPrimary === false) return;
if (pointer?.pointerType === 'mouse' && pointer.button !== 0) return;
event.preventDefault();
handler.call(this, event);
})
.on('keydown', selector, function (event) {
if (event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
handler.call(this, event);
});
}
function navigate(pageSelector, options = {}) {
const target = $(pageSelector);
if (!target.length) throw new Error(ns.I18n.t("app.page.missing"));
const previous = state.activePage;
$('.app-page').attr('hidden', true);
target.removeAttr('hidden');
state.activePage = pageSelector;
globalThis.scrollTo({ top: 0, behavior: options.instant ? 'auto' : 'smooth' });
if (options.updateHistory === false) return;
const historyState = { page: pageSelector, previous };
if (options.replace) history.replaceState(historyState, '', pageSelector);
else if (previous !== pageSelector) history.pushState(historyState, '', pageSelector);
}
function normalizeSearch(value) {
return String(value ?? '')
.normalize('NFKD')
.replaceAll(/[\u0300-\u036f]/g, '')
.toLocaleLowerCase('de');
}
function routeMatchesSearch(route, searchTerm) {
if (!searchTerm) return true;
return normalizeSearch([
route.name,
route.schoolName,
route.description
].filter(Boolean).join(' ')).includes(searchTerm);
}
function routeButton(route, source) {
const proximity = route.proximityM == null
? ''
: `<small>${escapeHtml(ns.Distance.format(route.proximityM))} entfernt</small>`;
const sourceLabel = source === 'local' ? ("<small>" + escapeHtml(ns.I18n.t("app.eigener.entwurf")) + "</small>") : '';
return `
<li>
<button type="button" data-route-select data-route-id="${escapeHtml(route.id)}" data-route-source="${source}">
<strong>${escapeHtml(route.name)}</strong>
<span>${escapeHtml(route.schoolName || route.description || '')}</span>
${sourceLabel}
${proximity}
</button>
</li>
`;
}
function renderRouteItems(listSelector, routes, emptyText, source = 'server') {
const list = $(listSelector).empty();
const items = Array.isArray(routes) ? routes.filter(Boolean) : [];
if (!items.length) {
list.append(`<li><p>${escapeHtml(emptyText)}</p></li>`);
return;
}
items.forEach(route => list.append(routeButton(route, source)));
}
function nextPoiMetric() {
const pois = state.route?.pois || [];
const pending = pois
.filter(poi => !state.triggered.has(poi.id))
.slice()
.sort((left, right) => Number(left.sequence ?? 0) - Number(right.sequence ?? 0));
if (!pending.length) return null;
if (!state.position) return { poi: pending[0], distance: null };
return nearestPendingPoi(poiMetrics(state.position));
}
function setRouteControlSummary(titleText, detailText) {
$('#route-control-title').text(titleText);
$('#route-control-next').text(detailText);
$('.route-control-summary').attr('aria-label', `${titleText}. ${detailText}`);
}
function updateRouteControlSummary() {
if (!state.route) {
setRouteControlSummary(ns.I18n.t("app.strecke.wahlen"), ns.I18n.t("app.strecken.fur.alle.und.meine.entwurfe"));
return;
}
const titleText = state.route.name || ns.I18n.t("app.strecke");
const metric = nextPoiMetric();
if (!metric) {
const detailText = (state.route.pois || []).length
? ns.I18n.t("app.alle.stationen.erreicht")
: ns.I18n.t("app.keine.stationen.vorhanden");
setRouteControlSummary(titleText, detailText);
return;
}
const distance = metric.distance == null ? '' : ` · ${Math.round(metric.distance)} m`;
setRouteControlSummary(titleText, ns.I18n.t("app.nachste.station", { value1: metric.poi.title, value2: distance }));
}
function renderRouteControlList(selector, routes, source, emptyText) {
const list = $(selector).empty();
if (!routes.length) {
list.append(`<li><span>${escapeHtml(emptyText)}</span></li>`);
return;
}
routes.forEach(route => {
const current = state.route && String(state.route.id) === String(route.id)
&& (state.route.source || 'server') === source;
list.append(`
<li>
<button type="button" data-route-select data-route-id="${escapeHtml(route.id)}" data-route-source="${source}" ${current ? 'aria-current="true"' : ''}>
<strong>${escapeHtml(route.name)}</strong>
<small>${source === 'local' ? ns.I18n.t("app.eigener.entwurf") : ns.I18n.t("app.veroffentlicht")}</small>
</button>
</li>
`);
});
}
function renderRouteControl() {
const routeControlLists = $('.route-control-list');
routeControlLists.attr('aria-busy', 'true');
$('#route-control-server-title').text(ns.I18n.t("app.strecken.fur.alle", { value1: state.routes.length }));
$('#route-control-local-title').text(ns.I18n.t("app.meine.entwurfe", { value1: state.localRoutes.length }));
$('#local-route-title').text(ns.I18n.t("app.meine.entwurfe", { value1: state.localRoutes.length }));
updateRouteControlSummary();
renderRouteControlList(
'#route-control-server-list',
state.routes,
'server',
ns.I18n.t("app.hier.gibt.es.noch.keine.strecken.fur")
);
renderRouteControlList(
'#route-control-local-list',
state.localRoutes,
'local',
state.localRoutesError || ns.I18n.t("app.du.hast.noch.keine.eigene.strecke.angelegt")
);
routeControlLists.attr('aria-busy', 'false');
}
function renderRoutes() {
const routes = Array.isArray(state.routes) ? state.routes.filter(Boolean) : [];
const radiusM = ns.Config.routeRadiusKm * 1000;
const nearbyRoutes = state.position
? routes
.filter(route => route.proximityM != null && route.proximityM <= radiusM)
.slice()
.sort((a, b) => a.proximityM - b.proximityM)
: [];
const searchTerm = normalizeSearch($('#route-search').val());
const allRoutes = routes
.filter(route => routeMatchesSearch(route, searchTerm))
.slice()
.sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''), 'de'));
const localRoutes = state.localRoutes
.slice()
.sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''), 'de'));
const nearbyEmptyText = state.position
? ns.I18n.t("app.keine.strecke.im.umkreis.von.km.gefunden", { value1: ns.Config.routeRadiusKm })
: ns.I18n.t("app.erlaube.deinen.standort.um.strecken.in.deiner");
renderRouteItems('#nearby-route-list', nearbyRoutes, nearbyEmptyText);
renderRouteItems(
'#all-route-list',
allRoutes,
searchTerm ? ns.I18n.t("app.keine.passende.strecke.gefunden.versuche.ein.anderes") : ns.I18n.t("app.hier.gibt.es.noch.keine.strecken.fur")
);
renderRouteItems(
'#local-route-list',
localRoutes,
state.localRoutesError || ns.I18n.t("app.du.hast.noch.keine.eigene.strecke.angelegt"),
'local'
);
$('#all-routes-summary').text(
searchTerm
? ns.I18n.t("app.von.strecken.gefunden", { value1: allRoutes.length, value2: routes.length })
: ns.I18n.t("app.strecken.zur.auswahl", { value1: routes.length })
);
renderRouteControl();
}
function initializeRouteSearch() {
const input = $('#route-search');
if (!input.length || typeof input.autocomplete !== 'function') return;
input.autocomplete({
minLength: 0,
source(request, response) {
const term = normalizeSearch(request.term);
response(state.routes
.filter(route => routeMatchesSearch(route, term))
.slice(0, 12)
.map(route => ({ label: route.name, value: route.name })));
},
select(event, ui) {
input.val(ui.item.value);
renderRoutes();
return false;
}
});
}
function hideNavigation() {
$('#navigation-footer').prop('hidden', true);
$('#app-shell').removeClass('has-navigation');
}
function showNavigation(message = ns.I18n.t("app.location.searching"), distance = '–') {
$('#navigation-mode').text(message);
$('#navigation-distance').text(distance);
$('#navigation-footer').prop('hidden', false);
$('#app-shell').addClass('has-navigation');
}
function stopRouteSensors() {
ns.Geo.stop();
ns.Orientation.stop();
ns.Vibration.stop();
state.deviceHeading = null;
}
function updateRouteControls() {
const running = state.routeState === 'running';
const paused = state.routeState === 'paused';
const startLabel = paused ? ns.I18n.t("app.strecke.fortsetzen") : ns.I18n.t("app.strecke.starten");
$('#start-route')
.prop('disabled', running)
.attr({ 'aria-label': startLabel, title: startLabel });
$('#pause-route').prop('disabled', !running);
$('#stop-route').prop('disabled', state.routeState === 'idle');
}
function setActivePoi(poiId) {
$('#poi-list [data-poi-id]').removeAttr('aria-current');
if (poiId == null) return;
$(`#poi-list [data-poi-id="${Number(poiId)}"]`).attr('aria-current', 'step');
}
function endRoute({ announce = true, returnToRoutes = false } = {}) {
const wasActive = state.routeState !== 'idle';
state.routeState = 'idle';
persistRouteProgress();
state.routeProgressIndex = null;
state.triggered.clear();
state.viewed.clear();
state.playedAudio.clear();
state.activePoiId = null;
stopRouteSensors();
ns.AudioPlayer.unload();
setActivePoi(null);
hideNavigation();
if (announce && wasActive && !returnToRoutes) {
$('#tracking-status').prop('hidden', false).text(ns.I18n.t("app.strecke.beendet"));
} else {
$('#tracking-status').prop('hidden', true).text('');
}
updateRouteControls();
if (returnToRoutes) {
state.route = null;
updateRouteControlSummary();
renderRouteControl();
navigate('#routes-page', { replace: true });
} else {
updateRouteControlSummary();
}
}
function pauseRoute() {
if (state.routeState !== 'running') return;
state.routeState = 'paused';
persistRouteProgress();
stopRouteSensors();
ns.AudioPlayer.pause();
$('#tracking-status')
.prop('hidden', false)
.text(ns.I18n.t("app.du.machst.eine.pause.die.app.sucht"));
showNavigation(ns.I18n.t("app.strecke.pausiert"), $('#navigation-distance').text() || '–');
updateRouteControls();
updateRouteControlSummary();
}
function suspendCurrentRoute() {
if (!state.route) return;
if (state.routeState === 'running') state.routeState = 'paused';
persistRouteProgress();
stopRouteSensors();
ns.AudioPlayer.unload();
state.activePoiId = null;
setActivePoi(null);
hideNavigation();
}
function renderRouteView(route) {
ns.OfflineCache?.renderRoute?.(route)?.catch?.(() => {});
$('#route-title').text(route.name);
$('#route-description').text(route.description || '');
$('#route-distance').text(ns.Distance.format(route.distanceM));
$('#route-elevation').text(`${Math.round(route.elevationGainM || 0)} m`);
$('#route-poi-count').text(route.pois.length);
updateRouteControls();
const list = $('#poi-list').empty();
if (!route.pois.length) {
list.append(("<li><p>" + escapeHtml(ns.I18n.t("app.diese.strecke.hat.noch.keine.stationen")) + "</p></li>"));
updateRouteControlSummary();
return;
}
route.pois.forEach(poi => {
list.append(`
<li>
<button type="button" data-poi-select data-poi-id="${poi.id}">
<strong>${escapeHtml(poi.title)}</strong>
<span>${escapeHtml(poi.description || '')}</span>
<span class="poi-proximity" aria-label="Entfernung wird ermittelt">
<svg viewBox="0 0 32 32" aria-hidden="true" focusable="false">
<path d="M16 2 27 28 16 23 5 28Z"/>
</svg>
<small>–</small>
</span>
</button>
</li>
`);
});
updatePoiIndicators(state.position);
updateRouteControlSummary();
}
function renderRoute(route) {
if (state.route) suspendCurrentRoute();
state.route = route;
state.activePoiId = null;
restoreRouteProgress(route);
setActivePoi(null);
renderRouteView(route);
renderRouteControl();
}
function showPoi(poi) {
state.activePoiId = Number(poi.id);
state.viewed.add(state.activePoiId);
persistRouteProgress();
setActivePoi(poi.id);
$('#poi-title').text(poi.title);
$('#poi-description').text(poi.description || '');
ns.Slideshow.show(poi.images);
ns.AudioPlayer.load(poi.audioUrl);
navigate('#poi-page');
}
function nearestRoutePointIndex(position) {
const points = state.route?.points || [];
if (!points.length) return null;
let start = 0;
let end = points.length;
if (state.routeProgressIndex != null) {
start = Math.max(0, state.routeProgressIndex - 3);
end = Math.min(points.length, state.routeProgressIndex + 201);
}
let nearestIndex = start;
let nearestDistance = Number.POSITIVE_INFINITY;
for (let index = start; index < end; index += 1) {
const distance = ns.Distance.meters(position, points[index]);
if (distance < nearestDistance) {
nearestDistance = distance;
nearestIndex = index;
}
}
return nearestIndex;
}
function effectiveHeading(position) {
const movementHeading = position
&& Number.isFinite(position.heading)
&& Number(position.speed) > 0.5
? position.heading
: null;
return state.deviceHeading ?? movementHeading;
}
function poiMetrics(position) {
if (!position || !state.route?.pois?.length) return [];
const heading = effectiveHeading(position);
return state.route.pois.map(poi => {
const distance = ns.Distance.meters(position, poi);
const bearing = ns.Distance.bearing(position, poi);
return {
poi,
distance,
bearing,
rotation: heading == null
? bearing
: ns.Distance.normalizeDegrees(bearing - heading)
};
});
}
function updatePoiIndicators(position) {
const metrics = poiMetrics(position);
metrics.forEach(metric => {
const indicator = $(`#poi-list [data-poi-id="${metric.poi.id}"] .poi-proximity`);
const distance = Math.round(metric.distance);
indicator
.attr('aria-label', ns.I18n.t("app.distance.spoken", { value1: distance }))
.attr('title', ns.I18n.t("app.distance.short", { value1: distance }));
indicator.find('svg').css('transform', `rotate(${metric.rotation.toFixed(1)}deg)`);
indicator.find('small').text(`${distance} m`);
});
return metrics;
}
function nearestPendingPoi(metrics) {
return metrics
.filter(metric => !state.triggered.has(metric.poi.id))
.reduce((nearest, metric) => (
!nearest || metric.distance < nearest.distance ? metric : nearest
), null);
}
function updateRouteProgress(position) {
const previousProgressIndex = state.routeProgressIndex;
const points = state.route?.points || [];
if (!points.length) return null;
const nearestIndex = nearestRoutePointIndex(position);
if (nearestIndex == null) return null;
state.routeProgressIndex = state.routeProgressIndex == null
? nearestIndex
: Math.max(state.routeProgressIndex, nearestIndex);
const lastIndex = points.length - 1;
let targetIndex = Math.min(lastIndex, state.routeProgressIndex + 1);
let target = points[targetIndex];
let distance = ns.Distance.meters(position, target);
const reachedRadius = Math.max(4, Math.min(12, Math.round(position.accuracy || 4)));
while (targetIndex < lastIndex && distance <= reachedRadius) {
state.routeProgressIndex = targetIndex;
targetIndex += 1;
target = points[targetIndex];
distance = ns.Distance.meters(position, target);
}
if (targetIndex === lastIndex && distance <= reachedRadius) {
state.routeProgressIndex = lastIndex;
if (state.routeProgressIndex !== previousProgressIndex) scheduleRouteProgressPersist();
return { reached: true, target, distance: 0 };
}
if (state.routeProgressIndex !== previousProgressIndex) scheduleRouteProgressPersist();
return { reached: false, target, distance };
}
function showPoiNavigation(position, metric) {
const heading = effectiveHeading(position);
const distance = Math.round(metric.distance);
$('#navigation-arrow').css('transform', `rotate(${metric.rotation.toFixed(1)}deg)`);
showNavigation(
heading == null
? ns.I18n.t("app.norden.oben.nachste.station", { value1: metric.poi.title })
: ns.I18n.t("app.nachste.station.2", { value1: metric.poi.title }),
`${distance} m`
);
$('#navigation-footer').attr(
'aria-label',
ns.I18n.t("app.nachste.station.in.metern", { value1: metric.poi.title, value2: distance })
);
}
function showRoutePointNavigation(position, progress) {
if (!progress) {
$('#navigation-arrow').css('transform', 'rotate(0deg)');
showNavigation(ns.I18n.t("app.path.missing"), '–');
return;
}
if (progress.reached) {
$('#navigation-arrow').css('transform', 'rotate(0deg)');
showNavigation(ns.I18n.t("app.alle.stationen.und.das.ziel.erreicht"), '0 m');
$('#navigation-footer').attr('aria-label', ns.I18n.t("app.alle.stationen.und.das.ziel.erreicht"));
return;
}
const bearing = ns.Distance.bearing(position, progress.target);
const heading = effectiveHeading(position);
const rotation = heading == null
? bearing
: ns.Distance.normalizeDegrees(bearing - heading);
const distance = Math.round(progress.distance);
$('#navigation-arrow').css('transform', `rotate(${rotation.toFixed(1)}deg)`);
showNavigation(
heading == null ? ns.I18n.t("app.norden.oben.nachster.punkt.auf.dem.weg") : ns.I18n.t("app.nachster.punkt.auf.dem.weg"),
`${distance} m`
);
$('#navigation-footer').attr(
'aria-label',
ns.I18n.t("app.nachster.punkt.auf.dem.weg.in.metern", { value1: distance })
);
}
function updateNavigation(position) {
if (state.routeState !== 'running') return;
const progress = updateRouteProgress(position);
const metrics = updatePoiIndicators(position);
const nextPoi = nearestPendingPoi(metrics);
if (nextPoi) showPoiNavigation(position, nextPoi);
else showRoutePointNavigation(position, progress);
}
function updateHeading(heading) {
if (state.deviceHeading == null) {
state.deviceHeading = heading;
} else {
const delta = ((heading - state.deviceHeading + 540) % 360) - 180;
state.deviceHeading = ns.Distance.normalizeDegrees(state.deviceHeading + delta * 0.25);
}
if (state.position) updateNavigation(state.position);
}
function evaluatePosition(position) {
state.position = position;
updateRouteProximity(position);
renderRouteControl();
if (!state.route) return;
const metrics = updatePoiIndicators(position);
updateRouteControlSummary();
if (state.routeState !== 'running') return;
const nearest = nearestPendingPoi(metrics);
updateNavigation(position);
$('#tracking-status').text(
ns.I18n.t("app.dein.standort.wird.gesucht.auf.etwa.m", { value1: Math.round(position.accuracy) })
+ (nearest ? ns.I18n.t("app.nachste.station.m", { value1: Math.round(nearest.distance) }) : ns.I18n.t("app.alle.stationen.erreicht.2"))
);
if (nearest && nearest.distance <= nearest.poi.triggerRadiusM) {
state.triggered.add(nearest.poi.id);
persistRouteProgress();
ns.Vibration.start(ns.Vibration.Patterns.ACTIVE_POI);
showPoi(nearest.poi);
updateNavigation(position);
updateRouteControlSummary();
}
}
async function startRoute() {
if (!state.route || state.routeState === 'running') return;
const resumed = state.routeState === 'paused';
if (!resumed) {
state.routeProgressIndex = null;
state.triggered.clear();
}
state.routeState = 'running';
state.deviceHeading = null;
persistRouteProgress();
updateRouteControls();
updateRouteControlSummary();
$('#tracking-status')
.prop('hidden', false)
.text(resumed ? ns.I18n.t("app.strecke.wird.fortgesetzt") : ns.I18n.t("app.location.start"));
showNavigation(resumed ? ns.I18n.t("app.strecke.wird.fortgesetzt.2") : ns.I18n.t("app.location.searching"));
ns.Geo.start(
evaluatePosition,
() => $('#tracking-status').text(ns.I18n.t("app.dein.standort.konnte.nicht.gefunden.werden.prufe"))
);
const orientationAvailable = await ns.Orientation.start(updateHeading);
if (state.routeState !== 'running') {
ns.Orientation.stop();
return;
}
if (!orientationAvailable) $('#navigation-mode').text(ns.I18n.t("app.norden.oben.navigation"));
}
function routesFromResponse(response) {
if (!Array.isArray(response?.routes)) {
throw new TypeError(ns.I18n.t("app.die.strecken.konnten.nicht.geladen.werden.bitte"));
}
return response.routes
.filter(route => route?.id != null && route?.name != null)
.map(route => ({ ...route, source: 'server', local: false }));
}
function loadLocalRoutes() {
try {
state.localRoutes = ns.LocalRoutes?.listAppRoutes?.() || [];
state.localRoutesError = null;
} catch (error) {
state.localRoutes = [];
state.localRoutesError = ns.UserMessages?.error(error, ns.I18n.t("app.deine.entwurfe.konnten.nicht.geoffnet.werden.bitte")) || ns.I18n.t("app.deine.entwurfe.konnten.nicht.geoffnet.werden.bitte");
}
}
function updateRouteProximity(position) {
const withProximity = route => ({
...route,
proximityM: route.start ? ns.Distance.meters(position, route.start) : null
});
state.routes = state.routes.map(withProximity);
state.localRoutes = state.localRoutes.map(withProximity);
}
async function initializeLocation(context) {
try {
const position = await ns.Geo.current();
state.position = position;
updateRouteProximity(position);
$('#location-hint').text(
ns.I18n.t("app.dein.standort.wurde.auf.etwa.m.genau", { value1: Math.round(position.accuracy) })
+ ns.I18n.t("app.wir.zeigen.dir.strecken.bis.km.entfernung", { value1: ns.Config.routeRadiusKm })
);
} catch {
$('#location-hint').text(
ns.I18n.t("app.dein.standort.konnte.nicht.gefunden.werden.du")
);
} finally {
context.markStep('location', 'done');
renderRoutes();
}
}
function restoreRouteLists() {
loadLocalRoutes();
if (state.position) updateRouteProximity(state.position);
renderRoutes();
}
function setRouteControlOpen(open) {
$('#route-control-dropdown').prop('hidden', !open);
$('#route-control-button').attr('aria-expanded', String(open));
}
async function selectRoute(source, id) {
setRouteControlOpen(false);
const route = source === 'local'
? await ns.LocalRoutes.getAppRoute(id, { requestPermission: true })
: { ...(await ns.Api.route(id)), source: 'server', local: false };
renderRoute(route);
navigate('#route-page');
}
async function refreshCurrentLocalRoute(id) {
if (state.route?.source !== 'local' || String(state.route.id) !== String(id)) return;
persistRouteProgress();
try {
state.route = await ns.LocalRoutes.getAppRoute(id);
renderRouteView(state.route);
renderRouteControl();
} catch {
state.route = null;
state.routeState = 'idle';
hideNavigation();
navigate('#routes-page', { replace: true });
}
}
function bindEvents() {
$(document).on('input', '#route-search', renderRoutes);
activate('#clear-route-search', function () {
$('#route-search').val('').trigger('input').trigger('focus');
});
activate('[data-route-select]', async function () {
const button = $(this);
button.prop('disabled', true);
try {
await selectRoute(String(button.data('route-source') || 'server'), String(button.data('route-id')));
} catch (error) {
alert(ns.UserMessages?.error(error, ns.I18n.t("app.die.strecke.konnte.nicht.geoffnet.werden.prufe")) || ns.I18n.t("app.die.strecke.konnte.nicht.geoffnet.werden.bitte"));
} finally {
button.prop('disabled', false);
}
});
activate('[data-poi-select]', function () {
const poi = state.route?.pois.find(item => item.id === Number($(this).data('poi-id')));
if (poi) showPoi(poi);
});
activate('[data-page-target]', function () {
const target = $(this).data('page-target');
if (history.state?.previous === target) history.back();
else navigate(target, { replace: true });
});
activate('#route-control-button', function () {
setRouteControlOpen($('#route-control-dropdown').prop('hidden'));
});
activate('.route-control-actions #route-control-new', function () {
setRouteControlOpen(false);
ns.EditorUi?.openNewLocalRoute?.();
});
activate('.route-control-actions #editor-button', function () {
setRouteControlOpen(false);
});
$(document).on('pointerup', event => {
if ($('#route-control-dropdown').prop('hidden')) return;
if ($(event.target).closest('.route-control').length) return;
setRouteControlOpen(false);
});
$(document).on('keydown', event => {
if (event.key === 'Escape') setRouteControlOpen(false);
});
activate('#start-route', async function () {
await startRoute();
});
activate('#pause-route', pauseRoute);
activate('#stop-route', function () {
endRoute({ returnToRoutes: true });
});
$('#poi-audio').on('play', function () {
if (!state.route || state.activePoiId == null) return;
state.playedAudio.add(state.activePoiId);
persistRouteProgress();
});
document.addEventListener('wegwichtel:progress-storage-changed', event => {
if (event.detail?.enabled) persistRouteProgress();
});
document.addEventListener('wegwichtel:local-routes-changed', event => {
loadLocalRoutes();
if (state.position) updateRouteProximity(state.position);
void refreshCurrentLocalRoute(event.detail?.id);
renderRoutes();
ns.EditorUi?.refresh?.();
});
globalThis.addEventListener('pagehide', persistRouteProgress);
globalThis.addEventListener('popstate', event => {
const page = event.state?.page || '#routes-page';
navigate(page, { updateHistory: false, instant: true });
});
globalThis.addEventListener('pageshow', restoreRouteLists);
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') restoreRouteLists();
else persistRouteProgress();
});
}
ns.App = {
navigate,
currentPosition() {
return state.position ? { ...state.position } : null;
},
async initialize(context) {
bindEvents();
await ns.Api.health();
context.markStep('server', 'done');
const response = await ns.Api.routes(null);
state.routes = routesFromResponse(response);
loadLocalRoutes();
context.markStep('routes', 'done');
renderRoutes();
initializeRouteSearch();
updateRouteControls();
hideNavigation();
setRouteControlOpen(false);
await initializeLocation(context);
}
};
}(globalThis.Wegwichtel, globalThis.jQuery));