|
|
@@ -0,0 +1,106 @@
|
|
|
+const xml2js = require('xml2js');
|
|
|
+const { haversineDistance, bearing, bearingDiff } = require('./geo');
|
|
|
+
|
|
|
+async function parseGPX(xmlString) {
|
|
|
+ const parser = new xml2js.Parser({ explicitArray: true, mergeAttrs: false });
|
|
|
+ return parser.parseStringPromise(xmlString);
|
|
|
+}
|
|
|
+
|
|
|
+function extractPoints(gpx) {
|
|
|
+ const trks = gpx.gpx.trk || [];
|
|
|
+ const name = (trks[0]?.name?.[0]) || (gpx.gpx.metadata?.[0]?.name?.[0]) || 'Track';
|
|
|
+ const segments = [];
|
|
|
+ for (const trk of trks) {
|
|
|
+ for (const seg of (trk.trkseg || [])) {
|
|
|
+ const pts = [];
|
|
|
+ for (const pt of (seg.trkpt || [])) {
|
|
|
+ const lat = parseFloat(pt.$.lat);
|
|
|
+ const lon = parseFloat(pt.$.lon);
|
|
|
+ const ele = pt.ele ? parseFloat(pt.ele[0]) : null;
|
|
|
+ const timeStr = pt.time ? pt.time[0] : null;
|
|
|
+ const time = timeStr ? new Date(timeStr) : null;
|
|
|
+ if (!isNaN(lat) && !isNaN(lon)) {
|
|
|
+ pts.push({ lat, lon, elevation: ele, time });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (pts.length > 0) segments.push(pts);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return { name, segments };
|
|
|
+}
|
|
|
+
|
|
|
+function compressSegment(points) {
|
|
|
+ if (points.length === 0) return [];
|
|
|
+ if (points.length === 1) return [points[0]];
|
|
|
+
|
|
|
+ const kept = [points[0]];
|
|
|
+
|
|
|
+ for (let i = 1; i < points.length; i++) {
|
|
|
+ const last = kept[kept.length - 1];
|
|
|
+ const curr = points[i];
|
|
|
+ const next = points[i + 1] || null;
|
|
|
+
|
|
|
+ // Distance check
|
|
|
+ const dist = haversineDistance(last.lat, last.lon, curr.lat, curr.lon);
|
|
|
+ if (dist < 2) continue;
|
|
|
+
|
|
|
+ // Time check
|
|
|
+ if (last.time && curr.time) {
|
|
|
+ const timeDiff = (curr.time.getTime() - last.time.getTime()) / 1000;
|
|
|
+ if (timeDiff < 30) {
|
|
|
+ // Check for sharp turn
|
|
|
+ if (next) {
|
|
|
+ const b1 = bearing(last.lat, last.lon, curr.lat, curr.lon);
|
|
|
+ const b2 = bearing(curr.lat, curr.lon, next.lat, next.lon);
|
|
|
+ if (bearingDiff(b1, b2) <= 30) continue;
|
|
|
+ } else {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ kept.push(curr);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Always ensure last point is included
|
|
|
+ const lastOrig = points[points.length - 1];
|
|
|
+ if (kept[kept.length - 1] !== lastOrig) {
|
|
|
+ const dist = haversineDistance(kept[kept.length-1].lat, kept[kept.length-1].lon, lastOrig.lat, lastOrig.lon);
|
|
|
+ if (dist >= 2) kept.push(lastOrig);
|
|
|
+ }
|
|
|
+
|
|
|
+ return kept;
|
|
|
+}
|
|
|
+
|
|
|
+function calcDistance(points) {
|
|
|
+ let total = 0;
|
|
|
+ for (let i = 1; i < points.length; i++) {
|
|
|
+ total += haversineDistance(points[i-1].lat, points[i-1].lon, points[i].lat, points[i].lon);
|
|
|
+ }
|
|
|
+ return total;
|
|
|
+}
|
|
|
+
|
|
|
+async function parseAndCompress(xmlString) {
|
|
|
+ const gpx = await parseGPX(xmlString);
|
|
|
+ const { name, segments } = extractPoints(gpx);
|
|
|
+
|
|
|
+ let trackDate = null;
|
|
|
+ let totalDistance = 0;
|
|
|
+ let sequence = 0;
|
|
|
+ const resultSegments = [];
|
|
|
+
|
|
|
+ for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
|
+ const seg = segments[segIdx];
|
|
|
+ if (!trackDate && seg[0]?.time) trackDate = seg[0].time;
|
|
|
+ const compressed = compressSegment(seg);
|
|
|
+ totalDistance += calcDistance(compressed);
|
|
|
+ const points = compressed.map(p => ({ ...p, sequence: sequence++, segmentId: segIdx }));
|
|
|
+ resultSegments.push({ points });
|
|
|
+ }
|
|
|
+
|
|
|
+ const pointCount = resultSegments.reduce((s, seg) => s + seg.points.length, 0);
|
|
|
+
|
|
|
+ return { segments: resultSegments, trackName: name, trackDate, totalDistance, pointCount };
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = { parseAndCompress };
|