adds upload dialog and duplicates

This commit is contained in:
Christian Beutel
2025-02-01 18:06:27 +01:00
parent ae3ef0e0a1
commit 0cf17cbd86
25 changed files with 499 additions and 147 deletions

View File

@@ -4,6 +4,8 @@ import Route from './route';
import Track from './track';
import { allDatesToISOString, haversineDistance, removeEmpty } from './utils';
import Waypoint from './waypoint';
//@ts-ignore
import geohash from "ngeohash"
const defaultAttributes = {
version: '1.1',
@@ -14,6 +16,17 @@ const defaultAttributes = {
'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd'
}
type GPXFeature = {
centroid: { lat: number; lon: number };
boundingBox: { minLat: number; maxLat: number; minLon: number; maxLon: number };
distance: number;
elevationGain?: number;
elevationLoss?: number;
duration: number;
hash: string; // MinHash or Geohash for track shape
};
export default class GPX {
$: {
version: string;
@@ -27,6 +40,7 @@ export default class GPX {
wpt?: Waypoint[];
rte?: Route[];
trk?: Track[];
features: GPXFeature
constructor(object: {
$?: {
@@ -69,18 +83,26 @@ export default class GPX {
this.trk = object.trk.map(trk => new Track(trk))
}
this.features = this.getTotals();
removeEmpty(this);
}
getTotals() {
getTotals(): GPXFeature {
let totalElevationGain = 0;
let totalElevationLoss = 0;
let totalDuration = 0;
let totalDistance = 0;
let totalLat = 0
let totalLon = 0
let minLat = Infinity, maxLat = -Infinity, minLon = Infinity, maxLon = -Infinity;
const allPoints: Waypoint[] = []
for (const track of this.trk ?? []) {
for (const segment of track.trkseg ?? []) {
const points = segment.trkpt ?? [];
allPoints.push(...points);
if (points.length >= 2) {
const startTime = points[0].time;
@@ -110,12 +132,51 @@ export default class GPX {
point.$.lat ?? 0,
point.$.lon ?? 0,
);
totalLat += point.$.lat ?? 0;
totalLon += point.$.lon ?? 0;
minLat = Math.min(minLat, point.$.lat ?? Infinity);
maxLat = Math.max(maxLat, point.$.lat ?? -Infinity);
minLon = Math.min(minLon, point.$.lon ?? Infinity);
maxLon = Math.max(maxLon, point.$.lon ?? -Infinity);
totalDistance += distance;
}
}
}
return { distance: totalDistance, elevationGain: totalElevationGain, elevationLoss: totalElevationLoss, duration: totalDuration }
const boundingBox = { minLat, maxLat, minLon, maxLon };
const centroid = { lat: totalLat / allPoints.length, lon: totalLon / allPoints.length };
return {
centroid,
boundingBox,
distance: totalDistance,
elevationGain: totalElevationGain,
elevationLoss: totalElevationLoss,
duration: totalDuration,
hash: this.generateMinHash(allPoints)
}
}
private generateMinHash(points: Waypoint[]): string {
const hashes = points.map(pt => geohash.encode(pt.$.lat, pt.$.lon));
return hashes.sort().join('').slice(0, 10);
}
isSimilar(other: GPX, distanceThreshold = 50): boolean {
const f1 = this.features;
const f2 = other.features;
const centroidDistance = haversineDistance(f1.centroid.lat, f1.centroid.lon, f2.centroid.lat, f2.centroid.lon);
const boundingBoxOverlap =
f1.boundingBox.minLat <= f2.boundingBox.maxLat && f1.boundingBox.maxLat >= f2.boundingBox.minLat &&
f1.boundingBox.minLon <= f2.boundingBox.maxLon && f1.boundingBox.maxLon >= f2.boundingBox.minLon;
const lengthDifference = Math.abs(f1.distance - f2.distance);
const hashSimilarity = f1.hash === f2.hash;
return centroidDistance < distanceThreshold || boundingBoxOverlap || (lengthDifference < 100 && hashSimilarity);
}
static parse(gpxString: string): Promise<GPX | Error> {