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
+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,