overhaul gpx creation and usage
Sonarqube Scanner / Prepare environment (push) Successful in 8s
Sonarqube Scanner / Build and test (push) Failing after 22s
Sonarqube Scanner / SonarQube analysis (push) Skipped
Sonarqube Scanner / Deploy to Server (push) Successful in 6s

This commit is contained in:
Florian Zumpe
2026-10-03 12:40:26 +02:00
parent 1cb3977269
commit 171ea88d55
29 changed files with 969 additions and 344 deletions
+40 -26
View File
@@ -1,6 +1,10 @@
(function (ns, $) {
'use strict';
const DROPDOWN_ANIMATION_MS = 180;
let routeControlOpen = false;
let routeControlCloseTimer = null;
const state = {
routes: [],
localRoutes: [],
@@ -319,6 +323,9 @@
.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();
}
@@ -421,7 +428,7 @@
function poiMetrics(position) {
if (!position || !state.route?.pois?.length) return [];
const heading = effectiveHeading(position);
return state.route.pois.map(poi => {
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 {
@@ -498,49 +505,41 @@
: ns.I18n.t("app.next-station", { value1: metric.poi.title }),
`${distance} m`
);
$('#navigation-footer').attr(
$('#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) {
$('#navigation-arrow').css('transform', 'rotate(0deg)');
showNavigation(ns.I18n.t("app.path.missing"), '–');
arrow.css('transform', 'rotate(0deg)').prop('hidden', true);
mode.text(ns.I18n.t('app.path.missing'));
return;
}
if (progress.reached) {
$('#navigation-arrow').css('transform', 'rotate(0deg)');
showNavigation(ns.I18n.t("app.destination-reached"), '0 m');
$('#navigation-footer').attr('aria-label', ns.I18n.t("app.destination-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);
const distance = Math.round(progress.distance);
$('#navigation-arrow').css('transform', `rotate(${rotation.toFixed(1)}deg)`);
showNavigation(
heading == null ? ns.I18n.t("app.next-path-point-north") : ns.I18n.t("app.next-path-point"),
`${distance} m`
);
$('#navigation-footer').attr(
'aria-label',
ns.I18n.t("app.next-path-point-distance", { value1: distance })
);
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 showRoutePointNavigation(position, progress);
else showNavigation(ns.I18n.t('recorder.no-station'), '–');
}
function updateHeading(heading) {
@@ -659,8 +658,23 @@
}
function setRouteControlOpen(open) {
$('#route-control-dropdown').prop('hidden', !open);
$('#route-control-button').attr('aria-expanded', String(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) {
@@ -718,7 +732,7 @@
});
activate('#route-control-button', function () {
setRouteControlOpen($('#route-control-dropdown').prop('hidden'));
setRouteControlOpen(!routeControlOpen);
});
activate('.route-control-actions #route-control-new', function () {
@@ -731,7 +745,7 @@
});
$(document).on('pointerup', event => {
if ($('#route-control-dropdown').prop('hidden')) return;
if (!routeControlOpen) return;
if ($(event.target).closest('.route-control').length) return;
setRouteControlOpen(false);
});
+11 -11
View File
@@ -495,6 +495,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
function fillRouteForm(route) {
setHidden(byId('editor-path-positions'), state.currentSource !== 'local');
if (state.currentSource === 'local') ns.RouteRecorder?.renderPoints(route.gpx || ns.LocalRoutes.emptyGpx(route.name));
byId(ids.routeId).value = routeId(route);
byId(ids.routeName).value = routeName(route) === ns.I18n.t("editor.untitled-route") ? '' : routeName(route);
byId(ids.routeDescription).value = text(route?.description);
@@ -814,13 +816,15 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function saveLocalRoute(values) {
const file = byId(ids.routeGpx)?.files?.[0];
let gpx = state.currentRoute?.gpx || ns.LocalRoutes.emptyGpx(values.name);
let gpx = ns.LocalRoutes.get?.(routeId(state.currentRoute))?.gpx || state.currentRoute?.gpx || ns.LocalRoutes.emptyGpx(values.name);
if (file) {
gpx = await ns.LocalRoutes.readFileAsText(file);
const importedPois = ns.LocalRoutes.parseWaypoints(gpx);
if (!state.pois.length && importedPois.length) state.pois = importedPois;
}
gpx = ns.RouteRecorder?.editGpx(gpx, values.name) ?? gpx;
const isNew = !routeId(state.currentRoute);
const id = routeId(state.currentRoute) || ns.LocalRoutes.createRouteId();
const saved = ns.LocalRoutes.save({
id,
@@ -835,6 +839,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
});
state.currentRoute = { ...saved.route, id: saved.id, gpx: saved.gpx, _source: 'local' };
state.currentSource = 'local';
if (isNew) ns.RouteRecorder?.start(saved.id);
state.pois = saved.pois;
await loadRoutes();
await loadRoute({ id: saved.id, _source: 'local' });
@@ -989,17 +994,14 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
function currentPosition() {
if (ns.RouteRecorder?.routeId === routeId(state.currentRoute)) return ns.RouteRecorder.position;
return typeof ns.App?.currentPosition === 'function' ? ns.App.currentPosition() : null;
}
function newPoi() {
const position = currentPosition();
const lat = Number(position?.lat);
const lon = Number(position?.lon);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
setStatus(ns.I18n.t("editor.error.location-required"), true);
return;
}
const lat = position?.lat ?? null;
const lon = position?.lon ?? null;
const accuracy = Number(position?.accuracy);
state.currentPoi = null;
@@ -1067,7 +1069,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const currentPoiId = poiId(state.currentPoi);
const saved = ns.LocalRoutes.save({
id,
gpx: state.currentRoute.gpx,
gpx: ns.LocalRoutes.get?.(routeId(state.currentRoute))?.gpx || state.currentRoute.gpx,
route: state.currentRoute,
pois: state.pois
});
@@ -1081,9 +1083,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
async function saveLocalPoi(values) {
if (!Number.isFinite(values.lat) || !Number.isFinite(values.lon)) {
throw new TypeError(ns.I18n.t("editor.error.station-location"));
}
const currentId = numericPoiId(byId(ids.poiId).value);
const identifier = currentId ?? nextLocalPoiId();
byId(ids.poiId).value = String(identifier);
+3 -2
View File
@@ -29,7 +29,7 @@
}
ns.Geo = {
current() {
current(options = {}) {
return new Promise((resolve, reject) => {
const service = geolocation();
if (!service) {
@@ -43,7 +43,8 @@
{
...WATCH_OPTIONS,
timeout: ns.Config.geolocationTimeoutMs,
maximumAge: 30000
maximumAge: 30000,
...options
}
);
});
+8 -3
View File
@@ -5,8 +5,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const DROPDOWN_ANIMATION_MS = 180;
const pageTitles = Object.freeze({
get privacy() { return ns.I18n.t("info.privacy"); },
get imprint() { return ns.I18n.t("info.imprint"); },
privacy: 'Datenschutzerklärung',
imprint: 'Impressum',
get technology() { return ns.I18n.t("info.storage"); }
});
function menuEntries() { return [
@@ -438,7 +438,12 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
function updateDialogTitle(pageName) {
if (elements?.title) elements.title.textContent = pageTitles[pageName] ?? ns.I18n.t("info.title");
if (!elements?.title) return;
const isLegalPage = pageName === 'privacy' || pageName === 'imprint';
if (isLegalPage) delete elements.title.dataset.i18n;
else elements.title.dataset.i18n = 'info.storage';
setAttribute(elements.title, 'lang', isLegalPage ? 'de' : ns.I18n.language);
elements.title.textContent = pageTitles[pageName] ?? ns.I18n.t("info.title");
}
function focusSafely(element) {
+83 -20
View File
@@ -298,7 +298,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function parseWaypoints(gpx) {
const document = parseXml(gpx);
return Array.from(document.getElementsByTagNameNS('*', 'wpt')).map((node, index) => ({
return [...Array.from(document.getElementsByTagNameNS('*', 'wpt')), ...Array.from(document.getElementsByTagNameNS('*', 'pendingWaypoint'))].map((node, index) => ({
id: waypointId(node),
title: textOfChild(node, 'name') || ns.I18n.t("editor.station-number", { value1: index + 1 }),
description: textOfChild(node, 'desc'),
@@ -308,7 +308,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
sequence: index,
audio: null,
images: []
})).filter(poi => poi.lat != null && poi.lon != null);
}));
}
function createElement(document, localName, text = null) {
@@ -337,7 +337,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
let track = directChildrenByLocalName(root, 'trk')[0];
if (!track) {
track = createElement(document, 'trk');
root.appendChild(track);
root.insertBefore(track, directChildrenByLocalName(root, 'extensions')[0] || null);
}
let trackName = directChildrenByLocalName(track, 'name')[0];
@@ -364,24 +364,26 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const document = parseXml(gpx || emptyGpx(routeName));
updateMetadataName(document, routeName);
// Wegwichtel verwaltet neue Stationen direkt als Punkte im GPX-Track.
// Stationen sind Wegpunkte; aufgezeichnete Trackpunkte bleiben getrennt.
// Fremde/importierte GPX-Punkte ohne Wegwichtel-ID bleiben erhalten.
Array.from(document.getElementsByTagNameNS('*', 'wpt')).forEach(node => node.remove());
Array.from(document.getElementsByTagNameNS('*', 'pendingWaypoint')).forEach(node => node.remove());
Array.from(document.getElementsByTagNameNS('*', 'trkpt'))
.filter(isManagedGpxPoint)
.forEach(node => node.remove());
const segment = ensureTrackSegment(document, routeName);
const root = document.documentElement;
const following = Array.from(root.children).find(node => ['rte', 'trk', 'extensions'].includes(node.localName));
const orderedPois = [...(pois || [])].sort((left, right) => Number(left.sequence) - Number(right.sequence));
orderedPois.forEach((poi, index) => {
const title = poi.title || ns.I18n.t("editor.station-number", { value1: index + 1 });
if (!Number.isFinite(Number(poi.lat)) || !Number.isFinite(Number(poi.lon))) {
throw new TypeError(ns.I18n.t("local.error.station-location", { value1: title }));
const located = poi.lat != null && poi.lon != null && Number.isFinite(Number(poi.lat)) && Number.isFinite(Number(poi.lon));
const gpxPoint = located ? createElement(document, 'wpt')
: document.createElementNS(POI_NAMESPACE, 'wegwichtel:pendingWaypoint');
if (located) {
gpxPoint.setAttribute('lat', String(Number(poi.lat)));
gpxPoint.setAttribute('lon', String(Number(poi.lon)));
}
const gpxPoint = createElement(document, 'trkpt');
gpxPoint.setAttribute('lat', String(Number(poi.lat)));
gpxPoint.setAttribute('lon', String(Number(poi.lon)));
gpxPoint.appendChild(createElement(document, 'name', title));
if (poi.description) gpxPoint.appendChild(createElement(document, 'desc', poi.description));
const extensions = createElement(document, 'extensions');
@@ -389,12 +391,71 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
poiId.textContent = String(poi.id);
extensions.appendChild(poiId);
gpxPoint.appendChild(extensions);
segment.appendChild(gpxPoint);
if (located) root.insertBefore(gpxPoint, following || null);
else {
let pending = directChildrenByLocalName(root, 'extensions')[0];
if (!pending) { pending = createElement(document, 'extensions'); root.appendChild(pending); }
pending.appendChild(gpxPoint);
}
});
return new XMLSerializer().serializeToString(document);
}
function appendTrackPoint(gpx, position, routeName) {
const document = parseXml(gpx);
appendPosition(document, position, routeName);
return new XMLSerializer().serializeToString(document);
}
function appendPosition(document, position, routeName) {
const lat = position.lat == null ? NaN : Number(position.lat);
const lon = position.lon == null ? NaN : Number(position.lon);
const timestamp = new Date(position.timestamp ?? Date.now()).toISOString();
if (!Number.isFinite(lat) || !Number.isFinite(lon) || Math.abs(lat) > 90 || Math.abs(lon) > 180) {
const root = document.documentElement;
let extensions = directChildrenByLocalName(root, 'extensions')[0];
if (!extensions) { extensions = createElement(document, 'extensions'); root.appendChild(extensions); }
const gap = document.createElementNS(POI_NAMESPACE, 'wegwichtel:gpsGap');
gap.setAttribute('time', timestamp);
extensions.appendChild(gap);
return;
}
const point = createElement(document, 'trkpt');
point.setAttribute('lat', String(lat));
point.setAttribute('lon', String(lon));
if (Number.isFinite(position.altitude)) point.appendChild(createElement(document, 'ele', position.altitude));
point.appendChild(createElement(document, 'time', timestamp));
ensureTrackSegment(document, routeName).appendChild(point);
}
function recordedPositions(gpx) {
const document = parseXml(gpx);
const routes = Array.from(document.getElementsByTagNameNS('*', 'rtept'));
const tracks = Array.from(document.getElementsByTagNameNS('*', 'trkpt')).filter(node => !isManagedGpxPoint(node));
const nodes = routes.length ? routes : tracks;
const positions = nodes.map(node => ({
lat: numberAttribute(node, 'lat'), lon: numberAttribute(node, 'lon'),
altitude: Number.parseFloat(textOfChild(node, 'ele')),
timestamp: textOfChild(node, 'time') || null
}));
for (const node of Array.from(document.getElementsByTagNameNS('*', 'gpsGap'))) {
positions.push({ lat: null, lon: null, altitude: null, timestamp: node.getAttribute('time') });
}
return positions.sort((left, right) => left.timestamp && right.timestamp ? Date.parse(left.timestamp) - Date.parse(right.timestamp) : 0);
}
function replaceRecordedPositions(gpx, positions, routeName) {
const document = parseXml(gpx);
for (const name of ['trkpt', 'rtept', 'gpsGap']) {
Array.from(document.getElementsByTagNameNS('*', name)).forEach(node => node.remove());
}
for (const position of positions) appendPosition(document, {
...position, timestamp: position.timestamp ? Date.parse(position.timestamp) : Date.now()
}, routeName);
return new XMLSerializer().serializeToString(document);
}
function readPackage(id) {
const target = storage();
const gpx = target.getItem(routeKey(id, 'gpx'));
@@ -501,7 +562,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const document = parseXml(gpx);
const gpxPoints = Array.from(document.getElementsByTagNameNS('*', 'trkpt'));
const routePoints = Array.from(document.getElementsByTagNameNS('*', 'rtept'));
const selected = gpxPoints.length ? gpxPoints : routePoints;
const selected = routePoints.length ? routePoints : gpxPoints.filter(node => !isManagedGpxPoint(node));
return selected.map(node => {
const elevation = Number.parseFloat(textOfChild(node, 'ele'));
return {
@@ -515,7 +576,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function gpxForUpload(routePackage) {
if (!routePackage?.gpx) throw new Error(ns.I18n.t("local.error.path-missing"));
if (!routePoints(routePackage.gpx).length) {
if (!routePoints(routePackage.gpx).length && !parseWaypoints(routePackage.gpx).length && !recordedPositions(routePackage.gpx).length) {
throw new Error(ns.I18n.t("local.error.path-empty"));
}
return routePackage.gpx;
@@ -579,8 +640,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
id: Number(poi.id),
title: poi.title || ns.I18n.t("editor.untitled-station"),
description: poi.description || '',
lat: Number(poi.lat),
lon: Number(poi.lon),
lat: poi.lat == null ? null : Number(poi.lat),
lon: poi.lon == null ? null : Number(poi.lon),
triggerRadiusM: Number(poi.triggerRadiusM) || 25,
sequence: Number(poi.sequence) || 0,
audioUrl: poi.audio?.url || poi.audio?.dataUrl || null,
@@ -596,10 +657,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function toAppRoute(routePackage) {
const pois = (routePackage.pois || []).map(appPoi).sort((left, right) => left.sequence - right.sequence);
const parsedPoints = routePoints(routePackage.gpx);
const points = parsedPoints.length
? parsedPoints
: pois.map(poi => ({ lat: poi.lat, lon: poi.lon, elevation: null, recordedAt: null }));
const routeMetrics = metrics(points);
const points = parsedPoints;
const routeMetrics = metrics(points.length ? points : pois.filter(poi => poi.lat != null && poi.lon != null));
if (!points.length) { routeMetrics.distanceM = 0; routeMetrics.elevationGainM = 0; }
return {
id: String(routePackage.id),
source: 'local',
@@ -664,6 +724,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
ns.LocalRoutes = Object.freeze({
STORAGE_PREFIX,
appendTrackPoint,
recordedPositions,
replaceRecordedPositions,
createPoiId,
createRouteId,
emptyGpx,
+159
View File
@@ -0,0 +1,159 @@
globalThis.Wegwichtel ??= {};
(function (ns) {
'use strict';
const INTERVAL_MS = 30000;
let id = null;
let status = 'idle';
let timer = null;
let generation = 0;
let pending = false;
let message = '';
let manualFields = [];
let lastPosition = null;
function render() {
const control = document.getElementById('route-recording-control');
if (!control) return;
control.hidden = status === 'idle';
document.getElementById('route-recording-status').textContent = status === 'idle' ? '' : message || ns.I18n.t(`recorder.${status}`);
document.getElementById('route-recording-pause').textContent = ns.I18n.t(status === 'paused' ? 'recorder.resume' : 'recorder.pause');
}
async function sample() {
if (pending || status !== 'recording') return;
pending = true;
const currentGeneration = generation;
try {
let position;
try { position = await ns.Geo.current({ maximumAge: 0 }); }
catch { position = { timestamp: Date.now() }; }
if (currentGeneration !== generation || status !== 'recording') return;
lastPosition = Number.isFinite(position.lat) && Number.isFinite(position.lon) ? position : null;
const route = ns.LocalRoutes.get(id);
if (!route) { stop(); return; }
const gpx = ns.LocalRoutes.appendTrackPoint(route.gpx, position, route.route.name);
ns.LocalRoutes.save({ ...route, gpx });
message = ns.I18n.t(Number.isFinite(position.lat) && Number.isFinite(position.lon) ? 'recorder.recording' : 'recorder.no-location');
} catch (error) {
pause();
ns.StorageUsage?.reportError(error);
message = ns.I18n.t('recorder.save-error');
} finally {
pending = false;
render();
}
}
function clearTimer() {
if (timer !== null) globalThis.clearInterval(timer);
timer = null;
generation += 1;
}
function resume() {
if (!id) return;
clearTimer();
status = 'recording';
message = '';
timer = globalThis.setInterval(sample, INTERVAL_MS);
sample();
render();
}
function pause() {
clearTimer();
status = id ? 'paused' : 'idle';
message = '';
render();
}
function stop() {
clearTimer();
id = null;
lastPosition = null;
status = 'idle';
message = '';
render();
}
function start(routeId) {
lastPosition = null;
id = String(routeId);
resume();
}
function addPositionField(position = {}, index = null) {
const container = document.getElementById('editor-recorded-positions');
if (!container) return;
const row = document.createElement('fieldset');
const legend = document.createElement('legend');
legend.textContent = ns.I18n.t('recorder.position-number', { number: manualFields.length + 1 });
row.appendChild(legend);
const fields = {};
for (const [key, textKey, minimum, maximum] of [['lat', 'latitude', -90, 90], ['lon', 'longitude', -180, 180]]) {
const label = document.createElement('label');
const input = document.createElement('input');
input.id = `recorded-position-${manualFields.length}-${key}`;
label.setAttribute('for', input.id);
label.textContent = ns.I18n.t(`recorder.${textKey}`);
input.type = 'number'; input.step = 'any'; input.min = minimum; input.max = maximum;
input.value = position[key] == null ? '' : String(position[key]);
row.appendChild(label); row.appendChild(input);
fields[key] = input;
}
manualFields.push({ index, original: position, ...fields });
container.appendChild(row);
}
function renderPoints(gpx) {
const container = document.getElementById('editor-recorded-positions');
if (!container) return;
container.replaceChildren();
manualFields = [];
ns.LocalRoutes.recordedPositions(gpx).forEach(addPositionField);
}
function editGpx(gpx, routeName) {
let changed = false;
const positions = ns.LocalRoutes.recordedPositions(gpx);
for (const field of manualFields) {
const values = ['lat', 'lon'].map(key => field[key].value.trim() === '' ? null : Number(field[key].value));
const [lat, lon] = values;
if ((lat === null) !== (lon === null) || (lat !== null && (!Number.isFinite(lat) || !Number.isFinite(lon) || Math.abs(lat) > 90 || Math.abs(lon) > 180))) {
throw new Error(ns.I18n.t('editor.error.station-location'));
}
if (field.index !== null && lat === field.original.lat && lon === field.original.lon) continue;
const point = { ...field.original, lat, lon };
if (field.index === null) positions.push(point);
else positions[field.index] = point;
changed = true;
}
return changed ? ns.LocalRoutes.replaceRecordedPositions(gpx, positions, routeName) : gpx;
}
function bind() {
const activate = (elementId, action) => {
const element = document.getElementById(elementId);
element?.addEventListener('pointerup', event => {
if (event.isPrimary === false || (event.pointerType === 'mouse' && event.button !== 0)) return;
event.preventDefault();
action();
});
element?.addEventListener('keydown', event => {
if (event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
action();
});
};
activate('route-recording-pause', () => status === 'recording' ? pause() : resume());
activate('route-recording-stop', stop);
activate('editor-add-position', () => addPositionField({ timestamp: new Date().toISOString() }));
document.addEventListener('wegwichtel:local-routes-changed', () => {
if (id && !ns.LocalRoutes.get(id)) stop();
});
}
ns.RouteRecorder = Object.freeze({ start, pause, resume, stop, renderPoints, editGpx, get status() { return status; }, get routeId() { return id; }, get position() { return lastPosition; } });
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bind, { once: true });
else bind();
}(globalThis.Wegwichtel));
+20 -77
View File
@@ -7,6 +7,7 @@ globalThis.Wegwichtel ??= {};
static messages = Object.freeze(
{
"app": {
"no-tracking": "Hier gibt es keine Werbung. Wir verfolgen nicht, welche anderen Seiten du besuchst.",
"resume-hint": "Du kannst dort weitermachen, wo du aufgehört hast.",
"draft": "Eigener Entwurf",
"choose-route": "Strecke wählen",
@@ -367,29 +368,6 @@ globalThis.Wegwichtel ??= {};
"experiences": "Erlebnisse"
}
},
"privacy": {
"no-tracking": "Hier gibt es keine Werbung. Wir verfolgen nicht, welche anderen Seiten du besuchst.",
"incomplete-notice": "Diese Angaben sind noch nicht vollständig. Die Kontakte stammen von der Grundschule Herpf. Wer die Website betreibt, welche Verbindungsdaten gespeichert werden und wie lange sie bleiben, muss noch ergänzt werden.",
"controller-title": "1. Verantwortliche Stelle und Datenschutzkontakt",
"connection-title": "2. Wenn du Wegwichtel öffnest",
"connection-data": "Wenn du Wegwichtel öffnest, können Verbindungsdaten verarbeitet werden. Dazu gehören zum Beispiel die Internetadresse deines Geräts, Datum, Uhrzeit und Zeitzone, die aufgerufene Seite, übertragene Daten und ob der Aufruf geklappt hat. Auch die vorherige Seite und Angaben zu deinem Gerät, Internetbrowser und dessen Sprache können dazugehören. Diese Daten helfen dabei, die Website bereitzustellen, Fehler zu finden und sie zu schützen.",
"provider-label": "Wer die Website bereitstellt und welche Verbindungsdaten wie lange gespeichert werden:",
"provider-placeholder": "[Anbieter, Anschrift, gespeicherte Daten und Speicherdauer ergänzen]",
"location-title": "3. Standortdaten",
"location-data": "Du entscheidest, ob Wegwichtel deinen Standort nutzen darf. Damit findest du Strecken in deiner Nähe und siehst den Weg zur nächsten Station. Dein laufender Standort wird auf deinem Gerät verarbeitet. Wegwichtel speichert daraus keinen Verlauf auf der Website.",
"progress-title": "4. Deinen Fortschritt merken",
"progress-data": "Wenn du „Meinen Fortschritt merken“ einschaltest, speichert Wegwichtel deine gewählte Strecke, besuchte und angesehene Stationen sowie angehörte Tonaufnahmen. Auch ob du unterwegs bist, eine Pause machst oder fertig bist und der Zeitpunkt werden gespeichert. Diese Angaben bleiben auf deinem Gerät und werden nicht an die Website gesendet.",
"progress-deletion": "Wenn du deinen Fortschritt mindestens einen Tag lang nicht verändert hast, wird er beim nächsten Öffnen gelöscht. Beim Ausschalten wird er sofort gelöscht. Deine eigenen und für unterwegs gespeicherten Strecken bleiben erhalten.",
"drafts-title": "5. Deine eigenen Strecken",
"draft-data": "Neue Strecken bleiben zuerst auf deinem Gerät. Gespeichert werden der Weg, die Orte der Stationen, Texte sowie ausgewählte Bilder und Tonaufnahmen.",
"draft-deletion": "Deine Entwürfe bleiben erhalten, wenn alter Fortschritt gelöscht wird oder du „Meinen Fortschritt merken“ ausschaltest. Sie bleiben auf diesem Gerät, bis du sie löschst oder die gespeicherten Daten von Wegwichtel in deinem Internetbrowser entfernst. Nur wenn du „Zur Prüfung senden“ wählst, wird deine Strecke an die Website gesendet.",
"settings-title": "6. Werbung und gespeicherte Einstellungen",
"settings-data": "Wegwichtel verwendet keine Cookies. Es zählt keine Besuche für Werbung und verfolgt nicht, welche anderen Seiten du besuchst. Auf deinem Gerät wird gespeichert, ob du „Meinen Fortschritt merken“ eingeschaltet hast.",
"children-title": "7. Inhalte von Schülerinnen und Schülern",
"children-permission": "Bilder, Texte und Tonaufnahmen dürfen nur geteilt werden, wenn das erlaubt ist. Wenn Kinder auf Bildern oder in Aufnahmen erkannt werden können, müssen die Regeln der Schule beachtet werden. Auch die Eltern oder anderen Sorgeberechtigten müssen die nötige Erlaubnis geben.",
"rights-title": "8. Deine Rechte",
"rights-data": "Du kannst fragen, welche Daten über dich gespeichert sind. Unter den gesetzlichen Voraussetzungen kannst du verlangen, dass sie berichtigt, gelöscht, nur noch eingeschränkt genutzt oder dir zur Weitergabe bereitgestellt werden. Du kannst der Nutzung widersprechen und eine erteilte Erlaubnis für die Zukunft zurücknehmen. Wenn du dich über den Umgang mit deinen Daten beschweren möchtest, kannst du dich an die folgende Datenschutzbehörde wenden."
},
"progress": {
"cookies-label": "Optionale Cookies",
"cookies-unused": "Diese Funktion wird nicht verwendet.",
@@ -428,60 +406,7 @@ globalThis.Wegwichtel ??= {};
"unsupported": "Dieses Gerät kann den Ton leider nicht abspielen."
},
"navigation": {
"station-direction": "Richtung zum nächsten Station"
},
"contacts": {
"school": {
"address": "Zum Eichig 3",
"city": "98617 Meiningen / OT Herpf",
"phone": "Telefon: (036943) 63722",
"email": "E-Mail: info@grundschule-herpf.de",
"name": "Staatliche Grundschule Herpf",
"privacy-label": "Datenschutzangaben der Schule:",
"fax": "Telefax: (036943) 63843",
"head": "Frau Wichert, Schulleiterin",
"content-owner": "Frau Wichert"
},
"privacy": {
"role": "Datenschutzbeauftragter",
"address": "Hölderlinstraße 1",
"city": "98527 Suhl",
"phone": "Telefon: 03681 / 7341 25",
"fax": "Fax: 03681 / 7341 09",
"email": "E-Mail: datenschutz.suedthueriungen@schulamt.thueringen.de",
"name": "Staatliches Schulamt Südthüringen",
"postal-hint": "Der Datenschutzbeauftragte kann außerdem über die Postadresse der Schule mit dem Zusatz „der Datenschutzbeauftragte“ erreicht werden."
},
"authority": {
"label": "Zuständige Datenschutzaufsicht:",
"postal-address": "Postfach 90 04 55, 99107 Erfurt",
"address": "Häßlerstraße 8, 99096 Erfurt",
"phone": "Telefon: 0361 / 57 311 29 00",
"fax": "Telefax: 0361 / 57 311 29 04",
"email": "E-Mail: poststelle@datenschutz.thueringen.de",
"website": "Website: www.tlfdi.de",
"name": "Thüringer Landesbeauftragter für den Datenschutz und die Informationsfreiheit (TlfDI)"
},
"sponsor": {
"role": "Landratsamt",
"address": "Obertshäuser Platz 1",
"city": "98617 Meiningen",
"name": "Landkreis Schmalkalden-Meiningen"
}
},
"imprint": {
"incomplete-notice": "Diese Angaben stammen von der Grundschule Herpf. Vor der Veröffentlichung muss geprüft werden, ob für Wegwichtel noch weitere oder andere Angaben nötig sind.",
"controller-title": "Wer für Wegwichtel verantwortlich ist",
"contact-title": "Kontakt",
"head-title": "Leitung der Schule",
"sponsor-title": "Schulträger",
"content-title": "Inhaltlich verantwortlich",
"required-title": "Weitere Pflichtangaben",
"required-data": "Je nachdem, wer Wegwichtel betreibt, können weitere Angaben nötig sein. Dazu gehören zum Beispiel die Verantwortlichen für den Betrieb der Website. Diese Angaben müssen noch geprüft werden.",
"liability-title": "Haftung für Inhalte und Verweise",
"liability-data": "Die Inhalte werden sorgfältig erstellt und regelmäßig geprüft. Falls etwas falsch, veraltet oder nicht erlaubt ist, melde dich beim Kontakt oben. Für andere verlinkte Seiten sind deren Betreiber verantwortlich.",
"copyright-title": "Urheber- und Nutzungsrechte",
"copyright-data": "Wenn nichts anderes angegeben ist, liegen die Rechte bei der Grundschule Herpf. Du darfst Texte, Bilder, Tonaufnahmen und Wege nur so nutzen, wie es jeweils erlaubt wurde. Auch die Rechte der beteiligten Kinder, Familien, Lehrkräfte und anderen Personen bleiben bestehen."
"station-direction": "Richtung zur nächsten Station"
},
"storage-help": {
"progress-title": "Was merkt sich Wegwichtel?",
@@ -521,6 +446,24 @@ globalThis.Wegwichtel ??= {};
"read": {
"failed": "Die Datei konnte nicht geöffnet werden. Wähle sie noch einmal aus."
}
},
"recorder": {
"recording": "Dein Weg wird aufgenommen.",
"paused": "Die Aufnahme macht eine Pause.",
"pause": "Aufnahme pausieren",
"resume": "Weiter aufnehmen",
"stop": "Aufnahme beenden",
"no-location": "Dein Standort fehlt gerade. Die Aufnahme läuft weiter.",
"save-error": "Die Aufnahme wurde pausiert. Schaffe Platz auf deinem Gerät und versuche es erneut.",
"manual-location": "Ort ergänzen oder ändern",
"latitude": "Breitengrad",
"longitude": "Längengrad",
"path-direction": "Dem Weg folgen",
"no-station": "Keine weitere Station mit bekanntem Ort",
"edit-path": "Orte auf dem Weg ergänzen",
"edit-help": "Fehlende Orte kannst du später eintragen. Leere Felder bleiben gespeichert.",
"add-position": "Ort auf dem Weg hinzufügen",
"position-number": "Ort {number}"
}
}
);
+19 -76
View File
@@ -7,6 +7,7 @@ globalThis.Wegwichtel ??= {};
static messages = Object.freeze(
{
"app": {
"no-tracking": "There are no adverts here. We do not track which other websites you visit.",
"resume-hint": "You can carry on where you left off.",
"draft": "My draft",
"choose-route": "Choose a trail",
@@ -367,29 +368,6 @@ globalThis.Wegwichtel ??= {};
"experiences": "Adventures"
}
},
"privacy": {
"no-tracking": "There are no adverts here. We do not track which other websites you visit.",
"incomplete-notice": "This information is not complete yet. The contact details are from Grundschule Herpf. The website operator, the connection data stored and how long it is kept still need to be added.",
"controller-title": "1. Who is responsible and the privacy contact",
"connection-title": "2. When you open Wegwichtel",
"connection-data": "When you open Wegwichtel, connection data may be processed. This may include your device’s internet address, the date, time and time zone, the page you opened, data transferred and whether the request worked. It may also include the previous page and details of your device, internet app and its language. This data helps to provide the website, find problems and keep it safe.",
"provider-label": "Who provides the website, which connection data is stored and how long it is kept:",
"provider-placeholder": "[Add provider, address, stored data and retention period]",
"location-title": "3. Location data",
"location-data": "You decide whether Wegwichtel may use your location. This helps you find trails nearby and see the way to the next stop. Your current location is processed on your device. Wegwichtel does not save a history of your movements on the website.",
"progress-title": "4. Remembering your progress",
"progress-data": "When you turn on “Remember my progress”, Wegwichtel saves your selected trail, stops you have reached or viewed and recordings you have listened to. It also saves whether you are on the trail, taking a break or finished, and the time. This information stays on your device and is not sent to the website.",
"progress-deletion": "If your progress has not changed for at least one day, it is deleted the next time you open the app. Turning the switch off deletes it straight away. Your own trails and trails saved for your trip are kept.",
"drafts-title": "5. Your own trails",
"draft-data": "New trails stay on your device for now. The path, stop locations, text and selected pictures and recordings are saved.",
"draft-deletion": "Your drafts are kept when old progress is deleted or you turn off “Remember my progress”. They stay on this device until you delete them or clear Wegwichtel’s saved data in your internet app. Your trail is only sent to the website when you choose “Send for review”.",
"settings-title": "6. Adverts and saved settings",
"settings-data": "Wegwichtel does not use cookies. It does not count visits for advertising or track which other websites you visit. Your device saves whether you have turned on “Remember my progress”.",
"children-title": "7. Content from schoolchildren",
"children-permission": "Pictures, text and recordings may only be shared when permission has been given. If children can be recognised in pictures or recordings, the school’s rules must be followed. Parents or other people with parental responsibility must also give the necessary permission.",
"rights-title": "8. Your rights",
"rights-data": "You can ask what data about you is stored. Where the law allows, you can ask for it to be corrected, deleted, used only in a limited way or provided to you so you can pass it on. You can object to its use and withdraw permission for future use. If you want to complain about how your data is handled, you can contact the data protection authority below."
},
"progress": {
"cookies-label": "Optional cookies",
"cookies-unused": "This feature is not used.",
@@ -430,59 +408,6 @@ globalThis.Wegwichtel ??= {};
"navigation": {
"station-direction": "Direction to the next stop"
},
"contacts": {
"school": {
"address": "Zum Eichig 3",
"city": "98617 Meiningen / OT Herpf",
"phone": "Phone: (036943) 63722",
"email": "Email: info@grundschule-herpf.de",
"name": "Staatliche Grundschule Herpf",
"privacy-label": "The school’s privacy contact:",
"fax": "Fax: (036943) 63843",
"head": "Ms Wichert, headteacher",
"content-owner": "Ms Wichert"
},
"privacy": {
"role": "Data protection officer",
"address": "Hölderlinstraße 1",
"city": "98527 Suhl",
"phone": "Phone: 03681 / 7341 25",
"fax": "Fax: 03681 / 7341 09",
"email": "Email: datenschutz.suedthueriungen@schulamt.thueringen.de",
"name": "Staatliches Schulamt Südthüringen",
"postal-hint": "You can also contact the data protection officer by writing to the school’s postal address and adding “der Datenschutzbeauftragte”."
},
"authority": {
"label": "Responsible data protection authority:",
"postal-address": "Postfach 90 04 55, 99107 Erfurt",
"address": "Häßlerstraße 8, 99096 Erfurt",
"phone": "Phone: 0361 / 57 311 29 00",
"fax": "Fax: 0361 / 57 311 29 04",
"email": "Email: poststelle@datenschutz.thueringen.de",
"website": "Website: www.tlfdi.de",
"name": "Thüringer Landesbeauftragter für den Datenschutz und die Informationsfreiheit (TlfDI)"
},
"sponsor": {
"role": "District administration",
"address": "Obertshäuser Platz 1",
"city": "98617 Meiningen",
"name": "Landkreis Schmalkalden-Meiningen"
}
},
"imprint": {
"incomplete-notice": "This information comes from Grundschule Herpf. Before publication, check whether Wegwichtel needs any additional or different information.",
"controller-title": "Who is responsible for Wegwichtel",
"contact-title": "Contact",
"head-title": "School leadership",
"sponsor-title": "School authority",
"content-title": "Responsible for content",
"required-title": "Further required information",
"required-data": "Depending on who operates Wegwichtel, further information may be required. This may include the people responsible for operating the website. These details still need to be checked.",
"liability-title": "Responsibility for content and links",
"liability-data": "The content is prepared carefully and checked regularly. If something is wrong, out of date or not allowed, please use the contact details above to let us know. Other website operators are responsible for their own linked pages.",
"copyright-title": "Copyright and rights of use",
"copyright-data": "Unless stated otherwise, the rights belong to Grundschule Herpf. You may only use text, pictures, recordings and paths in the ways permitted for each item. The rights of the children, families, teachers and other people involved remain in place."
},
"storage-help": {
"progress-title": "What does Wegwichtel remember?",
"progress-data": "You can save your progress on this device. Next time you open Wegwichtel, it will know which stops you have visited or viewed and which recordings you have listened to.",
@@ -521,6 +446,24 @@ globalThis.Wegwichtel ??= {};
"read": {
"failed": "The file could not be opened. Choose it again."
}
},
"recorder": {
"recording": "Your trail is being recorded.",
"paused": "Recording is paused.",
"pause": "Pause recording",
"resume": "Resume recording",
"stop": "Finish recording",
"no-location": "Your location is unavailable. Recording will continue.",
"save-error": "Recording was paused. Free up space on your device and try again.",
"manual-location": "Add or change the location",
"latitude": "Latitude",
"longitude": "Longitude",
"path-direction": "Follow the trail",
"no-station": "No further stop with a known location",
"edit-path": "Add locations along the trail",
"edit-help": "You can fill in missing locations later. Empty fields remain saved.",
"add-position": "Add a location along the trail",
"position-number": "Location {number}"
}
}
);