mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-08-31 23:53:25 +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 isLeaf(): boolean;
|
||||||
abstract getChildren(): T[];
|
abstract getChildren(): T[];
|
||||||
|
|
||||||
|
abstract computeStatistics(): GPXStatistics;
|
||||||
|
|
||||||
abstract reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date): void;
|
abstract reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date): void;
|
||||||
|
|
||||||
abstract getStartTimestamp(): Date;
|
abstract getStartTimestamp(): Date;
|
||||||
@@ -27,6 +29,16 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
|
|||||||
return false;
|
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 {
|
reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date): void {
|
||||||
const children = this.getChildren();
|
const children = this.getChildren();
|
||||||
|
|
||||||
@@ -73,6 +85,7 @@ export class GPXFile extends GPXTreeNode<Track>{
|
|||||||
metadata: Metadata;
|
metadata: Metadata;
|
||||||
wpt: Waypoint[];
|
wpt: Waypoint[];
|
||||||
trk: Track[];
|
trk: Track[];
|
||||||
|
statistics: GPXStatistics;
|
||||||
|
|
||||||
constructor(gpx: GPXFileType | GPXFile) {
|
constructor(gpx: GPXFileType | GPXFile) {
|
||||||
super();
|
super();
|
||||||
@@ -80,6 +93,8 @@ export class GPXFile extends GPXTreeNode<Track>{
|
|||||||
this.metadata = cloneJSON(gpx.metadata);
|
this.metadata = cloneJSON(gpx.metadata);
|
||||||
this.wpt = gpx.wpt.map((waypoint) => new Waypoint(waypoint));
|
this.wpt = gpx.wpt.map((waypoint) => new Waypoint(waypoint));
|
||||||
this.trk = gpx.trk.map((track) => new Track(track));
|
this.trk = gpx.trk.map((track) => new Track(track));
|
||||||
|
|
||||||
|
this.statistics = this.computeStatistics();
|
||||||
}
|
}
|
||||||
|
|
||||||
getChildren(): Track[] {
|
getChildren(): Track[] {
|
||||||
@@ -151,12 +166,146 @@ export class Track extends GPXTreeNode<TrackSegment> {
|
|||||||
// A class that represents a TrackSegment in a GPX file
|
// A class that represents a TrackSegment in a GPX file
|
||||||
export class TrackSegment extends GPXTreeLeaf {
|
export class TrackSegment extends GPXTreeLeaf {
|
||||||
trkpt: TrackPoint[];
|
trkpt: TrackPoint[];
|
||||||
|
trkptStatistics: TrackPointStatistics;
|
||||||
|
|
||||||
constructor(segment: TrackSegmentType | TrackSegment) {
|
constructor(segment: TrackSegmentType | TrackSegment) {
|
||||||
super();
|
super();
|
||||||
this.trkpt = segment.trkpt.map((point) => new TrackPoint(point));
|
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 {
|
reverse(originalNextTimestamp: Date | undefined, newPreviousTimestamp: Date | undefined): void {
|
||||||
if (originalNextTimestamp !== undefined && newPreviousTimestamp !== undefined) {
|
if (originalNextTimestamp !== undefined && newPreviousTimestamp !== undefined) {
|
||||||
let originalEndTimestamp = this.getEndTimestamp();
|
let originalEndTimestamp = this.getEndTimestamp();
|
||||||
@@ -189,7 +338,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
type: "Feature",
|
type: "Feature",
|
||||||
geometry: {
|
geometry: {
|
||||||
type: "LineString",
|
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: {}
|
properties: {}
|
||||||
};
|
};
|
||||||
@@ -214,6 +363,10 @@ export class TrackPoint {
|
|||||||
}
|
}
|
||||||
this.extensions = cloneJSON(point.extensions);
|
this.extensions = cloneJSON(point.extensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCoordinates(): Coordinates {
|
||||||
|
return this.attributes;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Waypoint {
|
export class Waypoint {
|
||||||
@@ -240,4 +393,78 @@ export class Waypoint {
|
|||||||
this.sym = waypoint.sym;
|
this.sym = waypoint.sym;
|
||||||
this.type = waypoint.type;
|
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 = {
|
export type Coordinates = {
|
||||||
lat: number;
|
lat: number;
|
||||||
lon: number;
|
lng: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TrackType = {
|
export type TrackType = {
|
||||||
|
@@ -48,7 +48,7 @@ describe('GPX operations', () => {
|
|||||||
const reversedPoint = reversedSegment.trkpt[originalSegment.trkpt.length - k - 1];
|
const reversedPoint = reversedSegment.trkpt[originalSegment.trkpt.length - k - 1];
|
||||||
|
|
||||||
expect(reversedPoint.attributes.lat).toBe(originalPoint.attributes.lat);
|
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(reversedPoint.ele).toBe(originalPoint.ele);
|
||||||
|
|
||||||
expect(reversed.getEndTimestamp().getTime() - reversedPoint.time.getTime()).toBe(originalPoint.time.getTime() - original.getStartTimestamp().getTime());
|
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.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);
|
expect(segment.trkpt[0].ele).toBe(109.0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ describe("Parsing", () => {
|
|||||||
|
|
||||||
const waypoint = result.wpt[0];
|
const waypoint = result.wpt[0];
|
||||||
expect(waypoint.attributes.lat).toBe(50.7836710064975);
|
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.ele).toBe(122.0);
|
||||||
expect(waypoint.name).toBe("Waypoint");
|
expect(waypoint.name).toBe("Waypoint");
|
||||||
expect(waypoint.cmt).toBe("Comment");
|
expect(waypoint.cmt).toBe("Comment");
|
||||||
|
Reference in New Issue
Block a user