Files
Wegwichtel/public/js/app.js
T
Florian Zumpe 0db91660b3
Sonarqube Scanner / Prepare environment (push) Successful in 8s
Sonarqube Scanner / Build and test (push) Successful in 21s
Sonarqube Scanner / SonarQube analysis (push) Failing after 1m3s
Sonarqube Scanner / Deploy to Server (push) Successful in 6s
allow update for uploaded routes and implement creator id for routes
2026-10-03 16:43:08 +02:00

822 lines
28 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 DROPDOWN_ANIMATION_MS = 180;
let routeControlOpen = false;
let routeControlCloseTimer = null;
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.resume-hint"));
} 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.draft")) + "</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 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.draft") : ns.I18n.t("app.published")}</small>
</button>
</li>
`);
});
}
function renderRouteControl() {
const routeControlLists = $('.route-control-list');
routeControlLists.attr('aria-busy', 'true');
$('#route-control-server-title').text(ns.I18n.t("app.public-route-count", { value1: state.routes.length }));
$('#route-control-local-title').text(ns.I18n.t("app.draft-count", { value1: state.localRoutes.length }));
$('#local-route-title').text(ns.I18n.t("app.draft-count", { value1: state.localRoutes.length }));
renderRouteControlList(
'#route-control-server-list',
state.routes,
'server',
ns.I18n.t("app.no-public-route")
);
renderRouteControlList(
'#route-control-local-list',
state.localRoutes,
'local',
state.localRoutesError || ns.I18n.t("app.no-route")
);
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.no-nearby-route", { value1: ns.Config.routeRadiusKm })
: ns.I18n.t("app.location-permission-hint");
renderRouteItems('#nearby-route-list', nearbyRoutes, nearbyEmptyText);
renderRouteItems(
'#all-route-list',
allRoutes,
searchTerm ? ns.I18n.t("app.no-search-result") : ns.I18n.t("app.no-public-route")
);
renderRouteItems(
'#local-route-list',
localRoutes,
state.localRoutesError || ns.I18n.t("app.no-route"),
'local'
);
$('#all-routes-summary').text(
searchTerm
? ns.I18n.t("app.search-result-count", { value1: allRoutes.length, value2: routes.length })
: ns.I18n.t("app.route-count", { 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.resume-route") : ns.I18n.t("app.start-route");
$('#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.route-finished"));
} else {
$('#tracking-status').prop('hidden', true).text('');
}
updateRouteControls();
if (returnToRoutes) {
state.route = null;
renderRouteControl();
navigate('#routes-page', { replace: true });
}
}
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.pause-hint"));
showNavigation(ns.I18n.t("app.route-paused"), $('#navigation-distance').text() || '–');
$('#navigation-arrow').prop('hidden', true);
$('#route-navigation-arrow').prop('hidden', true);
$('#route-navigation-mode').text(ns.I18n.t('app.route-paused'));
updateRouteControls();
}
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.empty-route")) + "</p></li>"));
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);
}
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.filter(poi => Number.isFinite(poi.lat) && Number.isFinite(poi.lon)).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.next-station-north", { value1: metric.poi.title })
: ns.I18n.t("app.next-station", { value1: metric.poi.title }),
`${distance} m`
);
$('#station-navigation').attr(
'aria-label',
ns.I18n.t("app.next-station-distance", { value1: metric.poi.title, value2: distance })
);
}
function showRoutePointNavigation(position, progress) {
const arrow = $('#route-navigation-arrow');
const mode = $('#route-navigation-mode');
if (!progress) {
arrow.css('transform', 'rotate(0deg)').prop('hidden', true);
mode.text(ns.I18n.t('app.path.missing'));
return;
}
if (progress.reached) {
arrow.css('transform', 'rotate(0deg)').prop('hidden', true);
mode.text(ns.I18n.t('app.destination-reached'));
return;
}
const bearing = ns.Distance.bearing(position, progress.target);
const heading = effectiveHeading(position);
const rotation = heading == null ? bearing : ns.Distance.normalizeDegrees(bearing - heading);
arrow.prop('hidden', false).css('transform', `rotate(${rotation.toFixed(1)}deg)`);
mode.text(ns.I18n.t(heading == null ? 'app.next-path-point-north' : 'app.next-path-point'));
}
function updateNavigation(position) {
if (state.routeState !== 'running') return;
const progress = updateRouteProgress(position);
showRoutePointNavigation(position, progress);
const metrics = updatePoiIndicators(position);
const nextPoi = nearestPendingPoi(metrics);
$('#navigation-arrow').prop('hidden', !nextPoi);
if (nextPoi) showPoiNavigation(position, nextPoi);
else showNavigation(ns.I18n.t('recorder.no-station'), '–');
}
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);
if (state.routeState !== 'running') return;
const nearest = nearestPendingPoi(metrics);
updateNavigation(position);
$('#tracking-status').text(
ns.I18n.t("app.location-accuracy", { value1: Math.round(position.accuracy) })
+ (nearest ? ns.I18n.t("app.nearest-station-distance", { value1: Math.round(nearest.distance) }) : ns.I18n.t("app.stations-complete-suffix"))
);
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);
}
}
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();
$('#tracking-status')
.prop('hidden', false)
.text(resumed ? ns.I18n.t("app.resuming") : ns.I18n.t("app.location.start"));
showNavigation(resumed ? ns.I18n.t("app.resuming-title") : ns.I18n.t("app.location.searching"));
ns.Geo.start(
evaluatePosition,
() => $('#tracking-status').text(ns.I18n.t("app.error.location"))
);
const orientationAvailable = await ns.Orientation.start(updateHeading);
if (state.routeState !== 'running') {
ns.Orientation.stop();
return;
}
if (!orientationAvailable) $('#navigation-mode').text(ns.I18n.t("app.navigation-north"));
}
function routesFromResponse(response) {
if (!Array.isArray(response?.routes)) {
throw new TypeError(ns.I18n.t("app.error.load-routes"));
}
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.error.load-drafts")) || ns.I18n.t("app.error.load-drafts");
}
}
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.location-found", { value1: Math.round(position.accuracy) })
+ ns.I18n.t("app.search-radius", { value1: ns.Config.routeRadiusKm })
);
} catch {
$('#location-hint').text(
ns.I18n.t("app.location-optional")
);
} finally {
context.markStep('location', 'done');
renderRoutes();
}
}
function restoreRouteLists() {
loadLocalRoutes();
if (state.position) updateRouteProximity(state.position);
renderRoutes();
}
function setRouteControlOpen(open) {
routeControlOpen = Boolean(open);
globalThis.clearTimeout(routeControlCloseTimer);
routeControlCloseTimer = null;
const dropdown = $('#route-control-dropdown');
$('#route-control-button').attr('aria-expanded', String(routeControlOpen));
if (routeControlOpen) {
dropdown.prop('hidden', false);
const nextFrame = globalThis.requestAnimationFrame ?? (callback => callback());
nextFrame(() => nextFrame(() => {
if (routeControlOpen) dropdown.addClass('is-open');
}));
} else {
dropdown.removeClass('is-open');
routeControlCloseTimer = globalThis.setTimeout(() => {
if (!routeControlOpen) dropdown.prop('hidden', true);
}, DROPDOWN_ANIMATION_MS);
}
}
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.error.open-route")) || ns.I18n.t("app.error.open-draft"));
} 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(!routeControlOpen);
});
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 (!routeControlOpen) 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) {
ns.LocalRoutes?.creatorId?.();
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));