Add Navigation Arrow and simplify static page

This commit is contained in:
Florian Zumpe
2026-06-16 16:20:09 +02:00
parent 278813fba6
commit 4dfa7da08b
9 changed files with 667 additions and 194 deletions
+186 -35
View File
@@ -7,13 +7,31 @@
position: null,
triggered: new Set(),
tracking: false,
activePage: null
activePage: null,
routeProgressIndex: null,
deviceHeading: null
};
function escapeHtml(value) {
return $('<div>').text(value ?? '').html();
}
function activate(selector, handler) {
$(document)
.on('pointerup', selector, function (event) {
const pointer = event.originalEvent;
if (pointer && 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(`Unbekannte Ansicht: ${pageSelector}`);
@@ -49,19 +67,20 @@
function renderRouteItems(listSelector, routes, emptyText) {
const list = $(listSelector).empty();
if (!routes.length) {
list.append(`<li class="empty-list">${escapeHtml(emptyText)}</li>`);
list.append(`<li><p>${escapeHtml(emptyText)}</p></li>`);
return;
}
routes.forEach(route => {
const proximity = route.proximityM == null
? ''
: `<span class="route-distance">${escapeHtml(ns.Distance.format(route.proximityM))} entfernt</span>`;
: `<small>${escapeHtml(ns.Distance.format(route.proximityM))} entfernt</small>`;
list.append(`
<li>
<button class="card-link" type="button" data-route-id="${route.id}">
<span class="card-title">${escapeHtml(route.name)}</span>
<span class="card-description">${escapeHtml(route.schoolName || route.description || '')}</span>
<button type="button" data-route-id="${route.id}">
<strong>${escapeHtml(route.name)}</strong>
<span>${escapeHtml(route.schoolName || route.description || '')}</span>
${proximity}
</button>
</li>
@@ -88,8 +107,11 @@
: 'Für Empfehlungen in der Nähe ist ein Standort erforderlich. Alle Routen bleiben unten auswählbar.';
renderRouteItems('#nearby-route-list', nearbyRoutes, nearbyEmptyText);
renderRouteItems('#all-route-list', allRoutes,
searchTerm ? 'Keine Route entspricht dem Suchbegriff.' : 'Es sind noch keine Routen vorhanden.');
renderRouteItems(
'#all-route-list',
allRoutes,
searchTerm ? 'Keine Route entspricht dem Suchbegriff.' : 'Es sind noch keine Routen vorhanden.'
);
$('#all-routes-summary').text(
searchTerm
@@ -117,9 +139,43 @@
});
}
function hideNavigation() {
$('#navigation-footer').prop('hidden', true);
$('#app-shell').removeClass('has-navigation');
}
function showNavigation(message = 'Position wird ermittelt', distance = '–') {
$('#navigation-mode').text(message);
$('#navigation-distance').text(distance);
$('#navigation-footer').prop('hidden', false);
$('#app-shell').addClass('has-navigation');
}
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 stopTracking() {
state.tracking = false;
state.routeProgressIndex = null;
state.deviceHeading = null;
ns.Geo.stop();
ns.Orientation.stop();
$('#tracking-status').prop('hidden', true).text('');
hideNavigation();
updateTrackingButton();
}
function renderRoute(route) {
if (state.tracking) stopTracking();
state.route = route;
state.triggered.clear();
state.routeProgressIndex = null;
$('#route-title').text(route.name);
$('#route-description').text(route.description || '');
$('#route-distance').text(ns.Distance.format(route.distanceM));
@@ -128,16 +184,16 @@
const list = $('#poi-list').empty();
if (!route.pois.length) {
list.append('<li class="empty-list">Diese Route enthält noch keine Stationen.</li>');
list.append('<li><p>Diese Route enthält noch keine Stationen.</p></li>');
return;
}
route.pois.forEach(poi => {
list.append(`
<li>
<button class="card-link" type="button" data-poi-id="${poi.id}">
<span class="card-title">${escapeHtml(poi.title)}</span>
<span class="card-description">${escapeHtml(poi.description || '')}</span>
<button type="button" data-poi-id="${poi.id}">
<strong>${escapeHtml(poi.title)}</strong>
<span>${escapeHtml(poi.description || '')}</span>
</button>
</li>
`);
@@ -153,10 +209,99 @@
if (automatic && poi.audioUrl) ns.AudioPlayer.play().catch(() => {});
}
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 updateNavigation(position) {
const points = state.route?.points || [];
if (!state.tracking || !points.length) return;
const nearestIndex = nearestRoutePointIndex(position);
if (nearestIndex == null) return;
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;
$('#navigation-arrow').css('transform', 'rotate(0deg)');
showNavigation('Ziel der Route erreicht', '0 m');
$('#navigation-footer').attr('aria-label', 'Ziel der Route erreicht');
return;
}
const bearing = ns.Distance.bearing(position, target);
const movementHeading = Number.isFinite(position.heading) && Number(position.speed) > 0.5
? position.heading
: null;
const heading = state.deviceHeading ?? movementHeading;
const rotation = heading == null
? bearing
: ns.Distance.normalizeDegrees(bearing - heading);
$('#navigation-arrow').css('transform', `rotate(${rotation.toFixed(1)}deg)`);
showNavigation(
heading == null ? 'Norden oben · nächster Routenpunkt' : 'Nächster Routenpunkt',
`${Math.round(distance)} m`
);
$('#navigation-footer').attr(
'aria-label',
`Nächster Routenpunkt in ${Math.round(distance)} Metern`
);
}
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;
if (!state.route || !state.tracking) return;
updateNavigation(position);
let nearest = null;
state.route.pois.forEach(poi => {
if (state.triggered.has(poi.id)) return;
@@ -165,8 +310,8 @@
});
$('#tracking-status').text(
`GPS aktiv · Genauigkeit ${Math.round(position.accuracy)} m` +
(nearest ? ` · nächste Station ${Math.round(nearest.distance)} m` : '')
`GPS aktiv · Genauigkeit ${Math.round(position.accuracy)} m`
+ (nearest ? ` · nächste Station ${Math.round(nearest.distance)} m` : '')
);
if (nearest && nearest.distance <= nearest.poi.triggerRadiusM) {
@@ -176,22 +321,33 @@
}
}
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'
});
async function startTracking() {
state.tracking = true;
state.routeProgressIndex = null;
state.deviceHeading = null;
updateTrackingButton();
$('#tracking-status').prop('hidden', false).text('GPS wird gestartet.');
showNavigation();
ns.Geo.start(
evaluatePosition,
error => $('#tracking-status').text(`GPS-Fehler: ${error.message}`)
);
const orientationAvailable = await ns.Orientation.start(updateHeading);
if (!orientationAvailable && state.tracking) {
$('#navigation-mode').text('Norden oben · nächster Routenpunkt');
}
}
function bindEvents() {
$(document).on('input', '#route-search', renderRoutes);
$(document).on('click', '#clear-route-search', function () {
activate('#clear-route-search', function () {
$('#route-search').val('').trigger('input').trigger('focus');
});
$(document).on('click', '[data-route-id]', async function () {
activate('[data-route-id]', async function () {
const button = $(this);
button.prop('disabled', true);
try {
@@ -205,26 +361,20 @@
}
});
$(document).on('click', '[data-poi-id]', function () {
activate('[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 () {
activate('[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();
}
activate('#start-tracking', async function () {
if (state.tracking) stopTracking();
else await startTracking();
});
window.addEventListener('popstate', event => {
@@ -249,8 +399,8 @@
const position = await ns.Geo.current();
state.position = position;
$('#location-hint').text(
`Standort ermittelt (Genauigkeit ${Math.round(position.accuracy)} m). ` +
`Nahe Routen werden bis ${ns.Config.routeRadiusKm} km empfohlen.`
`Standort ermittelt (Genauigkeit ${Math.round(position.accuracy)} m). `
+ `Nahe Routen werden bis ${ns.Config.routeRadiusKm} km empfohlen.`
);
state.routes = state.routes.map(route => ({
...route,
@@ -266,6 +416,7 @@
initializeRouteSearch();
renderRoutes();
updateTrackingButton();
hideNavigation();
}
};
}(window.Wegwichtel, window.jQuery));