(function (ns, $) { 'use strict'; const state = { routes: [], route: null, position: null, triggered: new Set(), tracking: false, activePage: null }; function escapeHtml(value) { return $('
').text(value ?? '').html(); } function navigate(pageSelector, options = {}) { const target = $(pageSelector); if (!target.length) throw new Error(`Unbekannte Ansicht: ${pageSelector}`); const previous = state.activePage; $('.app-page').attr('hidden', true).removeClass('is-active'); target.removeAttr('hidden').addClass('is-active'); state.activePage = pageSelector; window.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 renderRoutes() { const list = $('#route-list').empty(); if (!state.routes.length) { list.append('
  • Keine Route in der Nähe gefunden.
  • '); return; } state.routes.forEach(route => { const proximity = route.proximityM == null ? '' : `${escapeHtml(ns.Distance.format(route.proximityM))} entfernt`; list.append(`
  • `); }); } function renderRoute(route) { state.route = route; state.triggered.clear(); $('#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)} m`); $('#route-poi-count').text(route.pois.length); const list = $('#poi-list').empty(); if (!route.pois.length) { list.append('
  • Diese Route enthält noch keine Stationen.
  • '); return; } route.pois.forEach(poi => { list.append(`
  • `); }); } function showPoi(poi, automatic) { $('#poi-title').text(poi.title); $('#poi-description').text(poi.description || ''); ns.Slideshow.show(poi.images); ns.AudioPlayer.load(poi.audioUrl); navigate('#poi-page'); if (automatic && poi.audioUrl) ns.AudioPlayer.play().catch(() => {}); } function evaluatePosition(position) { state.position = position; if (!state.route || !state.tracking) return; let nearest = null; state.route.pois.forEach(poi => { if (state.triggered.has(poi.id)) return; const distance = ns.Distance.meters(position, poi); if (!nearest || distance < nearest.distance) nearest = { poi, distance }; }); $('#tracking-status').text( `GPS aktiv · Genauigkeit ${Math.round(position.accuracy)} m` + (nearest ? ` · nächste Station ${Math.round(nearest.distance)} m` : '') ); if (nearest && nearest.distance <= nearest.poi.triggerRadiusM) { state.triggered.add(nearest.poi.id); if (navigator.vibrate) navigator.vibrate([120, 80, 120]); showPoi(nearest.poi, true); } } function updateTrackingButton() { const button = $('#start-tracking'); button.button('option', { label: state.tracking ? 'GPS-Begleitung stoppen' : 'GPS-Begleitung starten', icon: state.tracking ? 'ui-icon-stop' : 'ui-icon-pin-s' }); } function bindEvents() { $(document).on('click', '[data-route-id]', async function () { const button = $(this); button.prop('disabled', true); try { const route = await ns.Api.route(button.data('route-id')); renderRoute(route); navigate('#route-page'); } catch (error) { alert(error.responseJSON?.message || error.statusText || error.message); } finally { button.prop('disabled', false); } }); $(document).on('click', '[data-poi-id]', function () { const poi = state.route?.pois.find(item => item.id === Number($(this).data('poi-id'))); if (poi) showPoi(poi, false); }); $(document).on('click', '[data-page-target]', function () { const target = $(this).data('page-target'); if (history.state?.previous === target) history.back(); else navigate(target, { replace: true }); }); $(document).on('click', '#start-tracking', function () { state.tracking = !state.tracking; updateTrackingButton(); $('#tracking-status').toggle(state.tracking); if (state.tracking) { ns.Geo.start(evaluatePosition, error => $('#tracking-status').text(`GPS-Fehler: ${error.message}`)); } else { ns.Geo.stop(); } }); window.addEventListener('popstate', event => { const page = event.state?.page || '#routes-page'; navigate(page, { updateHistory: false, instant: true }); }); } ns.App = { navigate, async initialize(context) { bindEvents(); await ns.Api.health(); context.markStep('server', 'done'); const response = await ns.Api.routes(null); state.routes = response.routes; context.markStep('routes', 'done'); try { const position = await ns.Geo.current(); state.position = position; $('#location-hint').text(`Standort ermittelt (Genauigkeit ${Math.round(position.accuracy)} m).`); state.routes = state.routes .map(route => ({ ...route, proximityM: route.start ? ns.Distance.meters(position, route.start) : null })) .filter(route => route.proximityM == null || route.proximityM <= ns.Config.routeRadiusKm * 1000) .sort((a, b) => (a.proximityM ?? Number.MAX_VALUE) - (b.proximityM ?? Number.MAX_VALUE)); } catch (error) { $('#location-hint').text(`Standort nicht verfügbar: ${error.message}. Es werden alle Routen angezeigt.`); } context.markStep('location', 'done'); renderRoutes(); updateTrackingButton(); } }; }(window.Wegwichtel, window.jQuery));