mirror of
				https://github.com/gpxstudio/gpx.studio.git
				synced 2025-11-04 05:21:09 +00:00 
			
		
		
		
	gpx stats
This commit is contained in:
		
							
								
								
									
										229
									
								
								gpx/src/gpx.ts
									
									
									
									
									
								
							
							
						
						
									
										229
									
								
								gpx/src/gpx.ts
									
									
									
									
									
								
							@@ -13,6 +13,8 @@ abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
 | 
			
		||||
    abstract isLeaf(): boolean;
 | 
			
		||||
    abstract getChildren(): T[];
 | 
			
		||||
 | 
			
		||||
    abstract computeStatistics(): GPXStatistics;
 | 
			
		||||
 | 
			
		||||
    abstract reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date): void;
 | 
			
		||||
 | 
			
		||||
    abstract getStartTimestamp(): Date;
 | 
			
		||||
@@ -27,6 +29,16 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    computeStatistics(): GPXStatistics {
 | 
			
		||||
        let statistics = new GPXStatistics();
 | 
			
		||||
 | 
			
		||||
        for (let child of this.getChildren()) {
 | 
			
		||||
            statistics.mergeWith(child.computeStatistics());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return statistics;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date): void {
 | 
			
		||||
        const children = this.getChildren();
 | 
			
		||||
 | 
			
		||||
@@ -73,6 +85,7 @@ export class GPXFile extends GPXTreeNode<Track>{
 | 
			
		||||
    metadata: Metadata;
 | 
			
		||||
    wpt: Waypoint[];
 | 
			
		||||
    trk: Track[];
 | 
			
		||||
    statistics: GPXStatistics;
 | 
			
		||||
 | 
			
		||||
    constructor(gpx: GPXFileType | GPXFile) {
 | 
			
		||||
        super();
 | 
			
		||||
@@ -80,6 +93,8 @@ export class GPXFile extends GPXTreeNode<Track>{
 | 
			
		||||
        this.metadata = cloneJSON(gpx.metadata);
 | 
			
		||||
        this.wpt = gpx.wpt.map((waypoint) => new Waypoint(waypoint));
 | 
			
		||||
        this.trk = gpx.trk.map((track) => new Track(track));
 | 
			
		||||
 | 
			
		||||
        this.statistics = this.computeStatistics();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getChildren(): Track[] {
 | 
			
		||||
@@ -151,12 +166,146 @@ export class Track extends GPXTreeNode<TrackSegment> {
 | 
			
		||||
// A class that represents a TrackSegment in a GPX file
 | 
			
		||||
export class TrackSegment extends GPXTreeLeaf {
 | 
			
		||||
    trkpt: TrackPoint[];
 | 
			
		||||
    trkptStatistics: TrackPointStatistics;
 | 
			
		||||
 | 
			
		||||
    constructor(segment: TrackSegmentType | TrackSegment) {
 | 
			
		||||
        super();
 | 
			
		||||
        this.trkpt = segment.trkpt.map((point) => new TrackPoint(point));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    computeStatistics(): GPXStatistics {
 | 
			
		||||
        let statistics = new GPXStatistics();
 | 
			
		||||
        let trkptStatistics: TrackPointStatistics = {
 | 
			
		||||
            distance: [],
 | 
			
		||||
            time: [],
 | 
			
		||||
            speed: [],
 | 
			
		||||
            elevation: {
 | 
			
		||||
                smoothed: [],
 | 
			
		||||
                gain: [],
 | 
			
		||||
                loss: [],
 | 
			
		||||
            },
 | 
			
		||||
            slope: [],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        trkptStatistics.elevation.smoothed = this.computeSmoothedElevation();
 | 
			
		||||
        trkptStatistics.slope = this.computeSlope();
 | 
			
		||||
 | 
			
		||||
        const points = this.trkpt;
 | 
			
		||||
        for (let i = 0; i < points.length; i++) {
 | 
			
		||||
 | 
			
		||||
            // distance
 | 
			
		||||
            let dist = 0;
 | 
			
		||||
            if (i > 0) {
 | 
			
		||||
                dist = distance(points[i - 1].getCoordinates(), points[i].getCoordinates());
 | 
			
		||||
 | 
			
		||||
                statistics.distance.total += dist;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            trkptStatistics.distance.push(statistics.distance.total);
 | 
			
		||||
 | 
			
		||||
            // elevation
 | 
			
		||||
            if (i > 0) {
 | 
			
		||||
                const ele = trkptStatistics.elevation.smoothed[i] - trkptStatistics.elevation.smoothed[i - 1];
 | 
			
		||||
                if (ele > 0) {
 | 
			
		||||
                    statistics.elevation.gain += ele;
 | 
			
		||||
                } else {
 | 
			
		||||
                    statistics.elevation.loss -= ele;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            trkptStatistics.elevation.gain.push(statistics.elevation.gain);
 | 
			
		||||
            trkptStatistics.elevation.loss.push(statistics.elevation.loss);
 | 
			
		||||
 | 
			
		||||
            // time
 | 
			
		||||
            if (points[0].time !== undefined && points[i].time !== undefined) {
 | 
			
		||||
                const time = points[i].time.getTime() - points[0].time.getTime();
 | 
			
		||||
 | 
			
		||||
                trkptStatistics.time.push(time);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // speed
 | 
			
		||||
            let speed = 0;
 | 
			
		||||
            if (i > 0 && points[i - 1].time !== undefined && points[i].time !== undefined) {
 | 
			
		||||
                const time = points[i].time.getTime() - points[i - 1].time.getTime();
 | 
			
		||||
                speed = dist / time;
 | 
			
		||||
 | 
			
		||||
                if (speed > 0.1) {
 | 
			
		||||
                    statistics.distance.moving += dist;
 | 
			
		||||
                    statistics.time.moving += time;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            trkptStatistics.speed.push(speed);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        statistics.time.total = trkptStatistics.time[trkptStatistics.time.length - 1];
 | 
			
		||||
        statistics.speed.total = statistics.distance.total / statistics.time.total;
 | 
			
		||||
        statistics.speed.moving = statistics.distance.moving / statistics.time.moving;
 | 
			
		||||
 | 
			
		||||
        this.trkptStatistics = trkptStatistics;
 | 
			
		||||
 | 
			
		||||
        return statistics;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    computeSmoothedElevation(): number[] {
 | 
			
		||||
        const ELEVATION_SMOOTHING_DISTANCE_THRESHOLD = 100;
 | 
			
		||||
 | 
			
		||||
        let smoothed = [];
 | 
			
		||||
 | 
			
		||||
        const points = this.trkpt;
 | 
			
		||||
        for (var i = 0; i < points.length; i++) {
 | 
			
		||||
            let weightedSum = 0;
 | 
			
		||||
            let totalWeight = 0;
 | 
			
		||||
 | 
			
		||||
            for (let j = 0; ; j++) {
 | 
			
		||||
                let left = i - j, right = i + j + 1;
 | 
			
		||||
                let contributed = false;
 | 
			
		||||
                for (let k of [left, right]) {
 | 
			
		||||
                    let dist = distance(points[i].getCoordinates(), points[k].getCoordinates());
 | 
			
		||||
                    if (dist > ELEVATION_SMOOTHING_DISTANCE_THRESHOLD) {
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    let weight = ELEVATION_SMOOTHING_DISTANCE_THRESHOLD - dist;
 | 
			
		||||
                    weightedSum += points[j].ele * weight;
 | 
			
		||||
                    totalWeight += weight;
 | 
			
		||||
                    contributed = true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!contributed) {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            smoothed.push(weightedSum / totalWeight);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return smoothed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    computeSlope(): number[] {
 | 
			
		||||
        let slope = [];
 | 
			
		||||
 | 
			
		||||
        const SLOPE_DISTANCE_THRESHOLD = 100;
 | 
			
		||||
 | 
			
		||||
        const points = this.trkpt;
 | 
			
		||||
 | 
			
		||||
        let start = 0, end = 0, windowDistance = 0;
 | 
			
		||||
        for (var i = 0; i < points.length; i++) {
 | 
			
		||||
            while (start < i && distance(points[start].getCoordinates(), points[i].getCoordinates()) > SLOPE_DISTANCE_THRESHOLD) {
 | 
			
		||||
                windowDistance -= distance(points[start].getCoordinates(), points[start + 1].getCoordinates());
 | 
			
		||||
                start++;
 | 
			
		||||
            }
 | 
			
		||||
            while (end + 1 < points.length && distance(points[i].getCoordinates(), points[end + 1].getCoordinates()) <= SLOPE_DISTANCE_THRESHOLD) {
 | 
			
		||||
                windowDistance += distance(points[end].getCoordinates(), points[end + 1].getCoordinates());
 | 
			
		||||
                end++;
 | 
			
		||||
            }
 | 
			
		||||
            slope[i] = windowDistance > 1e-3 ? 100 * (points[end].ele - points[start].ele) / windowDistance : 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return slope;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    reverse(originalNextTimestamp: Date | undefined, newPreviousTimestamp: Date | undefined): void {
 | 
			
		||||
        if (originalNextTimestamp !== undefined && newPreviousTimestamp !== undefined) {
 | 
			
		||||
            let originalEndTimestamp = this.getEndTimestamp();
 | 
			
		||||
@@ -189,7 +338,7 @@ export class TrackSegment extends GPXTreeLeaf {
 | 
			
		||||
            type: "Feature",
 | 
			
		||||
            geometry: {
 | 
			
		||||
                type: "LineString",
 | 
			
		||||
                coordinates: this.trkpt.map((point) => [point.attributes.lon, point.attributes.lat])
 | 
			
		||||
                coordinates: this.trkpt.map((point) => [point.attributes.lng, point.attributes.lat])
 | 
			
		||||
            },
 | 
			
		||||
            properties: {}
 | 
			
		||||
        };
 | 
			
		||||
@@ -214,6 +363,10 @@ export class TrackPoint {
 | 
			
		||||
        }
 | 
			
		||||
        this.extensions = cloneJSON(point.extensions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getCoordinates(): Coordinates {
 | 
			
		||||
        return this.attributes;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export class Waypoint {
 | 
			
		||||
@@ -240,4 +393,78 @@ export class Waypoint {
 | 
			
		||||
        this.sym = waypoint.sym;
 | 
			
		||||
        this.type = waypoint.type;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class GPXStatistics {
 | 
			
		||||
    distance: {
 | 
			
		||||
        moving: number;
 | 
			
		||||
        total: number;
 | 
			
		||||
    };
 | 
			
		||||
    time: {
 | 
			
		||||
        moving: number;
 | 
			
		||||
        total: number;
 | 
			
		||||
    };
 | 
			
		||||
    speed: {
 | 
			
		||||
        moving: number;
 | 
			
		||||
        total: number;
 | 
			
		||||
    };
 | 
			
		||||
    elevation: {
 | 
			
		||||
        gain: number;
 | 
			
		||||
        loss: number;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.distance = {
 | 
			
		||||
            moving: 0,
 | 
			
		||||
            total: 0,
 | 
			
		||||
        };
 | 
			
		||||
        this.time = {
 | 
			
		||||
            moving: 0,
 | 
			
		||||
            total: 0,
 | 
			
		||||
        };
 | 
			
		||||
        this.speed = {
 | 
			
		||||
            moving: 0,
 | 
			
		||||
            total: 0,
 | 
			
		||||
        };
 | 
			
		||||
        this.elevation = {
 | 
			
		||||
            gain: 0,
 | 
			
		||||
            loss: 0,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    mergeWith(other: GPXStatistics): void {
 | 
			
		||||
        this.distance.total += other.distance.total;
 | 
			
		||||
        this.distance.moving += other.distance.moving;
 | 
			
		||||
 | 
			
		||||
        this.time.total += other.time.total;
 | 
			
		||||
        this.time.moving += other.time.moving;
 | 
			
		||||
 | 
			
		||||
        this.speed.moving = this.distance.moving / this.time.moving;
 | 
			
		||||
        this.speed.total = this.distance.total / this.time.total;
 | 
			
		||||
 | 
			
		||||
        this.elevation.gain += other.elevation.gain;
 | 
			
		||||
        this.elevation.loss += other.elevation.loss;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TrackPointStatistics = {
 | 
			
		||||
    distance: number[],
 | 
			
		||||
    time: number[],
 | 
			
		||||
    speed: number[],
 | 
			
		||||
    elevation: {
 | 
			
		||||
        smoothed: number[],
 | 
			
		||||
        gain: number[],
 | 
			
		||||
        loss: number[],
 | 
			
		||||
    },
 | 
			
		||||
    slope: number[],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const earthRadius = 6371008.8;
 | 
			
		||||
function distance(coord1: Coordinates, coord2: Coordinates): number {
 | 
			
		||||
    const rad = Math.PI / 180;
 | 
			
		||||
    const lat1 = coord1.lat * rad;
 | 
			
		||||
    const lat2 = coord2.lat * rad;
 | 
			
		||||
    const a = Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1) * Math.cos(lat2) * Math.cos((coord2.lng - coord1.lng) * rad);
 | 
			
		||||
    const maxMeters = earthRadius * Math.acos(Math.min(a, 1));
 | 
			
		||||
    return maxMeters;
 | 
			
		||||
}
 | 
			
		||||
@@ -42,7 +42,7 @@ export type WaypointType = {
 | 
			
		||||
 | 
			
		||||
export type Coordinates = {
 | 
			
		||||
    lat: number;
 | 
			
		||||
    lon: number;
 | 
			
		||||
    lng: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type TrackType = {
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@ describe('GPX operations', () => {
 | 
			
		||||
                    const reversedPoint = reversedSegment.trkpt[originalSegment.trkpt.length - k - 1];
 | 
			
		||||
 | 
			
		||||
                    expect(reversedPoint.attributes.lat).toBe(originalPoint.attributes.lat);
 | 
			
		||||
                    expect(reversedPoint.attributes.lon).toBe(originalPoint.attributes.lon);
 | 
			
		||||
                    expect(reversedPoint.attributes.lng).toBe(originalPoint.attributes.lng);
 | 
			
		||||
                    expect(reversedPoint.ele).toBe(originalPoint.ele);
 | 
			
		||||
 | 
			
		||||
                    expect(reversed.getEndTimestamp().getTime() - reversedPoint.time.getTime()).toBe(originalPoint.time.getTime() - original.getStartTimestamp().getTime());
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ describe("Parsing", () => {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        expect(segment.trkpt[0].attributes.lat).toBe(50.790867);
 | 
			
		||||
        expect(segment.trkpt[0].attributes.lon).toBe(4.404968);
 | 
			
		||||
        expect(segment.trkpt[0].attributes.lng).toBe(4.404968);
 | 
			
		||||
        expect(segment.trkpt[0].ele).toBe(109.0);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -76,7 +76,7 @@ describe("Parsing", () => {
 | 
			
		||||
 | 
			
		||||
        const waypoint = result.wpt[0];
 | 
			
		||||
        expect(waypoint.attributes.lat).toBe(50.7836710064975);
 | 
			
		||||
        expect(waypoint.attributes.lon).toBe(4.410764082658738);
 | 
			
		||||
        expect(waypoint.attributes.lng).toBe(4.410764082658738);
 | 
			
		||||
        expect(waypoint.ele).toBe(122.0);
 | 
			
		||||
        expect(waypoint.name).toBe("Waypoint");
 | 
			
		||||
        expect(waypoint.cmt).toBe("Comment");
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user