overhaul gpx creation and usage
This commit is contained in:
+83
-20
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user