Files
gpx.studio/gpx/src/gpx.ts

1047 lines
40 KiB
TypeScript
Raw Normal View History

2024-04-18 17:11:23 +02:00
import { Coordinates, GPXFileAttributes, GPXFileType, Link, Metadata, TrackExtensions, TrackPointExtensions, TrackPointType, TrackSegmentType, TrackType, WaypointType } from "./types";
2024-05-16 10:41:49 +02:00
import { Draft, immerable, isDraft, original, produce, freeze } from "immer";
2024-04-18 17:11:23 +02:00
function cloneJSON<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') {
return null;
}
return JSON.parse(JSON.stringify(obj));
}
2024-04-16 13:54:48 +02:00
2024-04-16 15:41:03 +02:00
// An abstract class that groups functions that need to be computed recursively in the GPX file hierarchy
2024-04-25 19:02:34 +02:00
export abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
2024-04-30 15:19:50 +02:00
_data: { [key: string]: any } = {};
2024-04-16 15:41:03 +02:00
abstract isLeaf(): boolean;
2024-05-15 17:20:49 +02:00
abstract get children(): ReadonlyArray<T>;
2024-04-16 15:41:03 +02:00
2024-06-10 20:03:57 +02:00
abstract getNumberOfTrackPoints(): number;
2024-04-16 15:41:03 +02:00
abstract getStartTimestamp(): Date;
abstract getEndTimestamp(): Date;
2024-05-03 22:15:47 +02:00
abstract getStatistics(): GPXStatistics;
2024-04-25 16:41:06 +02:00
abstract getSegments(): TrackSegment[];
2024-04-16 22:57:28 +02:00
2024-04-25 13:48:31 +02:00
abstract toGeoJSON(): GeoJSON.Feature | GeoJSON.Feature[] | GeoJSON.FeatureCollection | GeoJSON.FeatureCollection[];
2024-05-15 15:30:02 +02:00
// Producers
2024-05-24 16:37:26 +02:00
abstract _reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date);
2024-04-16 15:41:03 +02:00
}
2024-04-25 19:02:34 +02:00
export type AnyGPXTreeElement = GPXTreeElement<GPXTreeElement<any>>;
2024-04-16 15:41:03 +02:00
// An abstract class that can be extended to facilitate functions working similarly with Tracks and TrackSegments
abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement<T> {
isLeaf(): boolean {
return false;
}
2024-06-10 20:03:57 +02:00
getNumberOfTrackPoints(): number {
return this.children.reduce((acc, child) => acc + child.getNumberOfTrackPoints(), 0);
}
2024-04-16 15:41:03 +02:00
getStartTimestamp(): Date {
2024-05-15 17:20:49 +02:00
return this.children[0].getStartTimestamp();
2024-04-16 15:41:03 +02:00
}
getEndTimestamp(): Date {
2024-05-15 17:20:49 +02:00
return this.children[this.children.length - 1].getEndTimestamp();
2024-04-16 15:41:03 +02:00
}
2024-04-20 23:17:11 +02:00
2024-04-25 13:48:31 +02:00
getStatistics(): GPXStatistics {
let statistics = new GPXStatistics();
2024-05-15 17:20:49 +02:00
for (let child of this.children) {
2024-04-25 13:48:31 +02:00
statistics.mergeWith(child.getStatistics());
}
return statistics;
}
2024-04-25 16:41:06 +02:00
getSegments(): TrackSegment[] {
2024-05-15 17:20:49 +02:00
return this.children.flatMap((child) => child.getSegments());
2024-04-25 16:41:06 +02:00
}
2024-05-15 15:30:02 +02:00
// Producers
2024-05-24 16:37:26 +02:00
_reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date) {
2024-05-15 15:30:02 +02:00
return produce(this, (draft: Draft<GPXTreeNode<T>>) => {
2024-05-16 10:41:49 +02:00
let og = getOriginal(draft);
2024-05-15 15:30:02 +02:00
if (!originalNextTimestamp && !newPreviousTimestamp) {
2024-05-16 10:41:49 +02:00
originalNextTimestamp = og.children[og.children.length - 1].getEndTimestamp();
newPreviousTimestamp = og.children[0].getStartTimestamp();
2024-05-15 15:30:02 +02:00
}
2024-05-15 17:20:49 +02:00
draft.children.reverse();
2024-05-15 15:30:02 +02:00
2024-05-16 10:41:49 +02:00
for (let i = 0; i < og.children.length; i++) {
let originalStartTimestamp = og.children[og.children.length - i - 1].getStartTimestamp();
2024-05-15 15:30:02 +02:00
2024-05-24 16:37:26 +02:00
draft.children[i] = draft.children[i]._reverse(originalNextTimestamp, newPreviousTimestamp);
2024-05-15 15:30:02 +02:00
originalNextTimestamp = originalStartTimestamp;
2024-05-15 17:20:49 +02:00
newPreviousTimestamp = draft.children[i].getEndTimestamp();
2024-05-15 15:30:02 +02:00
}
});
}
2024-04-16 15:41:03 +02:00
}
// An abstract class that TrackSegment extends to implement the GPXTreeElement interface
abstract class GPXTreeLeaf extends GPXTreeElement<GPXTreeLeaf> {
isLeaf(): boolean {
return true;
}
2024-05-15 17:20:49 +02:00
get children(): ReadonlyArray<GPXTreeLeaf> {
2024-04-16 15:41:03 +02:00
return [];
}
}
// A class that represents a set of GPX files
export class GPXFiles extends GPXTreeNode<GPXFile> {
2024-05-15 15:30:02 +02:00
readonly files: ReadonlyArray<GPXFile>;
constructor(files: GPXFile[]) {
super();
this.files = files;
}
2024-05-15 17:20:49 +02:00
get children(): ReadonlyArray<GPXFile> {
return this.files;
}
2024-04-25 13:48:31 +02:00
toGeoJSON(): GeoJSON.FeatureCollection[] {
2024-05-15 17:20:49 +02:00
return this.children.map((child) => child.toGeoJSON());
}
}
2024-04-16 15:41:03 +02:00
// A class that represents a GPX file
export class GPXFile extends GPXTreeNode<Track>{
2024-05-07 18:14:47 +02:00
[immerable] = true;
2024-05-24 22:53:30 +02:00
attributes: GPXFileAttributes;
metadata: Metadata;
2024-05-15 15:30:02 +02:00
readonly wpt: ReadonlyArray<Readonly<Waypoint>>;
readonly trk: ReadonlyArray<Track>;
2024-04-16 13:54:48 +02:00
2024-05-15 15:30:02 +02:00
constructor(gpx?: GPXFileType & { _data?: any } | GPXFile) {
2024-04-16 15:41:03 +02:00
super();
2024-04-27 12:18:40 +02:00
if (gpx) {
2024-05-03 15:59:34 +02:00
this.attributes = gpx.attributes
this.metadata = gpx.metadata;
2024-05-24 13:16:41 +02:00
this.wpt = gpx.wpt ? gpx.wpt.map((waypoint, index) => new Waypoint(waypoint, index)) : [];
2024-05-03 22:15:47 +02:00
this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : [];
2024-05-03 15:59:34 +02:00
if (gpx.hasOwnProperty('_data')) {
this._data = gpx._data;
2024-04-30 15:19:50 +02:00
}
2024-04-27 12:18:40 +02:00
} else {
this.attributes = {};
this.metadata = {};
this.wpt = [];
this.trk = [new Track()];
}
2024-04-16 13:54:48 +02:00
}
2024-05-15 17:20:49 +02:00
get children(): ReadonlyArray<Track> {
2024-04-16 15:41:03 +02:00
return this.trk;
2024-04-16 13:54:48 +02:00
}
2024-05-24 16:37:26 +02:00
getSegment(trackIndex: number, segmentIndex: number): TrackSegment {
return this.trk[trackIndex].children[segmentIndex];
}
forEachSegment(callback: (segment: TrackSegment, trackIndex: number, segmentIndex: number) => void) {
this.trk.forEach((track, trackIndex) => {
track.children.forEach((segment, segmentIndex) => {
callback(segment, trackIndex, segmentIndex);
});
});
}
2024-04-16 13:54:48 +02:00
clone(): GPXFile {
2024-05-03 15:59:34 +02:00
return new GPXFile({
attributes: cloneJSON(this.attributes),
metadata: cloneJSON(this.metadata),
wpt: this.wpt.map((waypoint) => waypoint.clone()),
trk: this.trk.map((track) => track.clone()),
_data: cloneJSON(this._data),
});
2024-04-16 13:54:48 +02:00
}
2024-04-16 22:57:28 +02:00
2024-04-25 13:48:31 +02:00
toGeoJSON(): GeoJSON.FeatureCollection {
2024-04-16 22:57:28 +02:00
return {
type: "FeatureCollection",
2024-05-15 17:20:49 +02:00
features: this.children.flatMap((child) => child.toGeoJSON())
2024-04-16 22:57:28 +02:00
};
}
toGPXFileType(): GPXFileType {
return {
attributes: this.attributes,
metadata: this.metadata,
wpt: this.wpt,
trk: this.trk.map((track) => track.toTrackType())
};
}
2024-05-23 14:44:07 +02:00
// Producers
2024-06-04 16:11:47 +02:00
replaceTracks(start: number, end: number, tracks: Track[]): [GPXFile, Track[]] {
let removed = [];
let result = produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trk = og.trk.slice();
2024-06-04 16:11:47 +02:00
removed = trk.splice(start, end - start + 1, ...tracks);
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
});
2024-06-04 16:11:47 +02:00
return [result, removed];
}
2024-06-04 16:11:47 +02:00
replaceTrackSegments(trackIndex: number, start: number, end: number, segments: TrackSegment[]): [GPXFile, TrackSegment[]] {
let removed = [];
let result = produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trk = og.trk.slice();
2024-06-04 16:11:47 +02:00
let [result, rmv] = trk[trackIndex].replaceTrackSegments(start, end, segments);
trk[trackIndex] = result;
removed = rmv;
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
});
2024-06-04 16:11:47 +02:00
return [result, removed];
}
2024-05-24 16:37:26 +02:00
replaceTrackPoints(trackIndex: number, segmentIndex: number, start: number, end: number, points: TrackPoint[]) {
return produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trk = og.trk.slice();
trk[trackIndex] = trk[trackIndex].replaceTrackPoints(segmentIndex, start, end, points);
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
});
}
2024-06-04 16:11:47 +02:00
replaceWaypoints(start: number, end: number, waypoints: Waypoint[]): [GPXFile, Waypoint[]] {
let removed = [];
let result = produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let wpt = og.wpt.slice();
2024-06-04 16:11:47 +02:00
removed = wpt.splice(start, end - start + 1, ...waypoints);
draft.wpt = freeze(wpt); // Pre-freeze the array, faster as well
});
2024-06-04 16:11:47 +02:00
return [result, removed];
}
2024-05-24 16:37:26 +02:00
reverse() {
2024-06-05 23:37:55 +02:00
return this._reverse(this.getEndTimestamp(), this.getStartTimestamp());
2024-05-24 16:37:26 +02:00
}
reverseTrack(trackIndex: number) {
return produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trk = og.trk.slice();
trk[trackIndex] = trk[trackIndex]._reverse();
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
});
}
reverseTrackSegment(trackIndex: number, segmentIndex: number) {
return produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trk = og.trk.slice();
trk[trackIndex] = trk[trackIndex].reverseTrackSegment(segmentIndex);
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
});
}
2024-06-10 20:03:57 +02:00
crop(start: number, end: number, trackIndices?: number[], segmentIndices?: number[]) {
return produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trk = og.trk.slice();
let i = 0;
let trackIndex = 0;
while (i < trk.length) {
let length = trk[i].getNumberOfTrackPoints();
if (trackIndices === undefined || trackIndices.includes(trackIndex)) {
if (start >= length || end < 0) {
trk.splice(i, 1);
} else {
if (start > 0 || end < length - 1) {
trk[i] = trk[i].crop(Math.max(0, start), Math.min(length - 1, end), segmentIndices);
}
i++;
}
start -= length;
end -= length;
} else {
i++;
}
trackIndex++;
}
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
});
}
2024-06-11 16:33:06 +02:00
clean(bounds: [Coordinates, Coordinates], inside: boolean, deleteTrackPoints: boolean, deleteWaypoints: boolean, trackIndices?: number[], segmentIndices?: number[], waypointIndices?: number[]) {
return produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
if (deleteTrackPoints) {
let trk = og.trk.slice();
let i = 0;
let trackIndex = 0;
while (i < trk.length) {
if (trackIndices === undefined || trackIndices.includes(trackIndex)) {
trk[i] = trk[i].clean(bounds, inside, segmentIndices);
if (trk[i].getNumberOfTrackPoints() === 0) {
trk.splice(i, 1);
} else {
i++;
}
} else {
i++;
}
trackIndex++;
}
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
}
if (deleteWaypoints) {
let wpt = og.wpt.filter((point, waypointIndex) => {
if (waypointIndices === undefined || waypointIndices.includes(waypointIndex)) {
let inBounds = point.attributes.lat >= bounds[0].lat && point.attributes.lat <= bounds[1].lat && point.attributes.lon >= bounds[0].lon && point.attributes.lon <= bounds[1].lon;
return inBounds !== inside;
} else {
return true;
}
});
draft.wpt = freeze(wpt); // Pre-freeze the array, faster as well
}
});
}
2024-04-16 13:54:48 +02:00
};
2024-04-16 15:41:03 +02:00
// A class that represents a Track in a GPX file
export class Track extends GPXTreeNode<TrackSegment> {
2024-05-07 18:14:47 +02:00
[immerable] = true;
2024-05-15 15:30:02 +02:00
readonly name?: string;
readonly cmt?: string;
readonly desc?: string;
readonly src?: string;
readonly link?: Link;
readonly type?: string;
readonly trkseg: ReadonlyArray<TrackSegment>;
readonly extensions?: TrackExtensions;
2024-04-16 13:54:48 +02:00
2024-05-15 15:30:02 +02:00
constructor(track?: TrackType & { _data?: any } | Track) {
2024-04-16 15:41:03 +02:00
super();
2024-04-27 12:18:40 +02:00
if (track) {
this.name = track.name;
this.cmt = track.cmt;
this.desc = track.desc;
this.src = track.src;
2024-05-03 15:59:34 +02:00
this.link = track.link;
2024-04-27 12:18:40 +02:00
this.type = track.type;
2024-05-03 22:15:47 +02:00
this.trkseg = track.trkseg ? track.trkseg.map((seg) => new TrackSegment(seg)) : [];
2024-05-04 00:06:03 +02:00
this.extensions = track.extensions;
2024-05-03 15:59:34 +02:00
if (track.hasOwnProperty('_data')) {
2024-05-04 00:06:03 +02:00
this._data = track._data;
2024-04-30 15:19:50 +02:00
}
2024-04-27 12:18:40 +02:00
} else {
this.trkseg = [new TrackSegment()];
}
2024-04-16 13:54:48 +02:00
}
2024-05-15 17:20:49 +02:00
get children(): ReadonlyArray<TrackSegment> {
2024-04-16 15:41:03 +02:00
return this.trkseg;
2024-04-16 13:54:48 +02:00
}
2024-05-03 15:59:34 +02:00
clone(): Track {
return new Track({
name: this.name,
cmt: this.cmt,
desc: this.desc,
src: this.src,
link: cloneJSON(this.link),
type: this.type,
trkseg: this.trkseg.map((seg) => seg.clone()),
extensions: cloneJSON(this.extensions),
_data: cloneJSON(this._data),
});
}
2024-04-25 13:48:31 +02:00
toGeoJSON(): GeoJSON.Feature[] {
2024-05-15 17:20:49 +02:00
return this.children.map((child) => {
2024-04-17 16:46:51 +02:00
let geoJSON = child.toGeoJSON();
if (this.extensions && this.extensions['gpx_style:line']) {
if (this.extensions['gpx_style:line'].color) {
geoJSON.properties['color'] = this.extensions['gpx_style:line'].color;
}
if (this.extensions['gpx_style:line'].opacity) {
geoJSON.properties['opacity'] = this.extensions['gpx_style:line'].opacity;
}
if (this.extensions['gpx_style:line'].weight) {
geoJSON.properties['weight'] = this.extensions['gpx_style:line'].weight;
}
}
return geoJSON;
});
2024-04-16 22:57:28 +02:00
}
toTrackType(): TrackType {
return {
name: this.name,
cmt: this.cmt,
desc: this.desc,
src: this.src,
link: this.link,
type: this.type,
trkseg: this.trkseg.map((seg) => seg.toTrackSegmentType()),
extensions: this.extensions,
};
}
2024-05-23 14:44:07 +02:00
// Producers
2024-06-04 16:11:47 +02:00
replaceTrackSegments(start: number, end: number, segments: TrackSegment[]): [Track, TrackSegment[]] {
let removed = [];
let result = produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trkseg = og.trkseg.slice();
2024-06-04 16:11:47 +02:00
removed = trkseg.splice(start, end - start + 1, ...segments);
draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
});
2024-06-04 16:11:47 +02:00
return [result, removed];
}
2024-05-24 16:37:26 +02:00
replaceTrackPoints(segmentIndex: number, start: number, end: number, points: TrackPoint[]) {
return produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trkseg = og.trkseg.slice();
trkseg[segmentIndex] = trkseg[segmentIndex].replaceTrackPoints(start, end, points);
draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
});
}
reverseTrackSegment(segmentIndex: number) {
return produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trkseg = og.trkseg.slice();
2024-06-05 23:37:55 +02:00
trkseg[segmentIndex] = trkseg[segmentIndex]._reverse(trkseg[segmentIndex].getEndTimestamp(), trkseg[segmentIndex].getStartTimestamp());
2024-05-24 16:37:26 +02:00
draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
});
}
2024-06-10 20:03:57 +02:00
crop(start: number, end: number, segmentIndices?: number[]) {
return produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trkseg = og.trkseg.slice();
let i = 0;
let segmentIndex = 0;
while (i < trkseg.length) {
let length = trkseg[i].getNumberOfTrackPoints();
if (segmentIndices === undefined || segmentIndices.includes(segmentIndex)) {
if (start >= length || end < 0) {
trkseg.splice(i, 1);
} else {
if (start > 0 || end < length - 1) {
trkseg[i] = trkseg[i].crop(Math.max(0, start), Math.min(length - 1, end));
}
i++;
}
start -= length;
end -= length;
} else {
i++;
}
segmentIndex++;
}
draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
});
}
2024-06-11 16:33:06 +02:00
clean(bounds: [Coordinates, Coordinates], inside: boolean, segmentIndices?: number[]) {
return produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trkseg = og.trkseg.slice();
let i = 0;
let segmentIndex = 0;
while (i < trkseg.length) {
if (segmentIndices === undefined || segmentIndices.includes(segmentIndex)) {
trkseg[i] = trkseg[i].clean(bounds, inside);
if (trkseg[i].getNumberOfTrackPoints() === 0) {
trkseg.splice(i, 1);
} else {
i++;
}
} else {
i++;
}
segmentIndex++;
}
draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
});
}
2024-04-16 15:41:03 +02:00
};
2024-04-16 13:54:48 +02:00
2024-04-16 15:41:03 +02:00
// A class that represents a TrackSegment in a GPX file
export class TrackSegment extends GPXTreeLeaf {
2024-05-07 18:14:47 +02:00
[immerable] = true;
2024-05-15 15:30:02 +02:00
readonly trkpt: ReadonlyArray<Readonly<TrackPoint>>;
2024-04-16 13:54:48 +02:00
2024-05-15 15:30:02 +02:00
constructor(segment?: TrackSegmentType & { _data?: any } | TrackSegment) {
2024-04-16 15:41:03 +02:00
super();
2024-04-27 12:18:40 +02:00
if (segment) {
2024-05-03 22:15:47 +02:00
this.trkpt = segment.trkpt.map((point) => new TrackPoint(point));
2024-05-03 15:59:34 +02:00
if (segment.hasOwnProperty('_data')) {
2024-05-04 00:06:03 +02:00
this._data = segment._data;
2024-04-30 15:19:50 +02:00
}
2024-04-27 12:18:40 +02:00
} else {
this.trkpt = [];
}
2024-04-16 13:54:48 +02:00
}
2024-05-03 22:15:47 +02:00
_computeStatistics(): GPXStatistics {
2024-04-18 19:15:01 +02:00
let statistics = new GPXStatistics();
2024-05-15 15:30:02 +02:00
statistics.local.points = this.trkpt.map((point) => point);
2024-05-03 22:15:47 +02:00
statistics.local.elevation.smoothed = this._computeSmoothedElevation();
statistics.local.slope = this._computeSlope();
2024-04-18 19:15:01 +02:00
const points = this.trkpt;
for (let i = 0; i < points.length; i++) {
2024-04-25 16:41:06 +02:00
points[i]._data['index'] = i;
2024-04-18 19:15:01 +02:00
// distance
let dist = 0;
if (i > 0) {
2024-04-19 13:50:28 +02:00
dist = distance(points[i - 1].getCoordinates(), points[i].getCoordinates()) / 1000;
2024-04-18 19:15:01 +02:00
2024-05-03 22:15:47 +02:00
statistics.global.distance.total += dist;
2024-04-18 19:15:01 +02:00
}
2024-06-10 20:03:57 +02:00
statistics.local.distance.total.push(statistics.global.distance.total);
2024-04-18 19:15:01 +02:00
// elevation
if (i > 0) {
2024-05-03 22:15:47 +02:00
const ele = statistics.local.elevation.smoothed[i] - statistics.local.elevation.smoothed[i - 1];
2024-04-18 19:15:01 +02:00
if (ele > 0) {
2024-05-03 22:15:47 +02:00
statistics.global.elevation.gain += ele;
2024-06-10 20:03:57 +02:00
} else if (ele < 0) {
2024-05-03 22:15:47 +02:00
statistics.global.elevation.loss -= ele;
2024-04-18 19:15:01 +02:00
}
}
2024-05-03 22:15:47 +02:00
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
2024-04-18 19:15:01 +02:00
// time
2024-06-13 17:36:43 +02:00
if (points[i].time === undefined) {
2024-06-10 20:03:57 +02:00
statistics.local.time.total.push(undefined);
2024-06-13 17:36:43 +02:00
} else {
if (statistics.global.time.start === undefined) {
statistics.global.time.start = points[i].time;
}
statistics.global.time.end = points[i].time;
statistics.local.time.total.push((points[i].time.getTime() - statistics.global.time.start.getTime()) / 1000);
2024-04-18 19:15:01 +02:00
}
// speed
let speed = 0;
if (i > 0 && points[i - 1].time !== undefined && points[i].time !== undefined) {
2024-04-19 13:50:28 +02:00
const time = (points[i].time.getTime() - points[i - 1].time.getTime()) / 1000;
speed = dist / (time / 3600);
2024-04-18 19:15:01 +02:00
2024-04-19 13:50:28 +02:00
if (speed >= 0.5) {
2024-05-03 22:15:47 +02:00
statistics.global.distance.moving += dist;
statistics.global.time.moving += time;
2024-04-18 19:15:01 +02:00
}
}
2024-06-10 20:03:57 +02:00
statistics.local.distance.moving.push(statistics.global.distance.moving);
statistics.local.time.moving.push(statistics.global.time.moving);
2024-04-22 10:45:02 +02:00
// bounds
2024-05-03 22:15:47 +02:00
statistics.global.bounds.southWest.lat = Math.min(statistics.global.bounds.southWest.lat, points[i].attributes.lat);
2024-05-07 12:16:30 +02:00
statistics.global.bounds.southWest.lon = Math.min(statistics.global.bounds.southWest.lon, points[i].attributes.lon);
2024-05-03 22:15:47 +02:00
statistics.global.bounds.northEast.lat = Math.max(statistics.global.bounds.northEast.lat, points[i].attributes.lat);
2024-05-07 12:16:30 +02:00
statistics.global.bounds.northEast.lon = Math.max(statistics.global.bounds.northEast.lon, points[i].attributes.lon);
2024-04-18 19:15:01 +02:00
}
2024-06-13 17:36:43 +02:00
statistics.global.time.total = statistics.global.time.start && statistics.global.time.end ? (statistics.global.time.end.getTime() - statistics.global.time.start.getTime()) / 1000 : 0;
2024-06-10 20:03:57 +02:00
statistics.global.speed.total = statistics.global.time.total > 0 ? statistics.global.distance.total / (statistics.global.time.total / 3600) : 0;
statistics.global.speed.moving = statistics.global.time.moving > 0 ? statistics.global.distance.moving / (statistics.global.time.moving / 3600) : 0;
2024-04-18 19:15:01 +02:00
2024-05-03 22:15:47 +02:00
statistics.local.speed = distanceWindowSmoothingWithDistanceAccumulator(points, 200, (accumulated, start, end) => (points[start].time && points[end].time) ? 3600 * accumulated / (points[end].time.getTime() - points[start].time.getTime()) : undefined);
2024-04-21 12:13:58 +02:00
2024-05-03 22:15:47 +02:00
return statistics;
2024-04-18 19:15:01 +02:00
}
2024-04-25 13:48:31 +02:00
_computeSmoothedElevation(): number[] {
2024-04-18 19:15:01 +02:00
const points = this.trkpt;
2024-04-21 12:13:58 +02:00
let smoothed = distanceWindowSmoothing(points, 100, (index) => points[index].ele, (accumulated, start, end) => accumulated / (end - start + 1));
2024-04-18 19:15:01 +02:00
2024-04-19 13:20:31 +02:00
if (points.length > 0) {
smoothed[0] = points[0].ele;
smoothed[points.length - 1] = points[points.length - 1].ele;
2024-04-18 19:15:01 +02:00
}
return smoothed;
}
2024-04-25 13:48:31 +02:00
_computeSlope(): number[] {
2024-04-18 19:15:01 +02:00
const points = this.trkpt;
return distanceWindowSmoothingWithDistanceAccumulator(points, 50, (accumulated, start, end) => 100 * (points[end].ele - points[start].ele) / accumulated);
2024-04-18 19:15:01 +02:00
}
2024-06-10 20:03:57 +02:00
getNumberOfTrackPoints(): number {
return this.trkpt.length;
}
2024-04-16 15:41:03 +02:00
getStartTimestamp(): Date {
return this.trkpt[0].time;
}
getEndTimestamp(): Date {
return this.trkpt[this.trkpt.length - 1].time;
}
2024-04-25 13:48:31 +02:00
getStatistics(): GPXStatistics {
2024-05-03 22:15:47 +02:00
return this._computeStatistics();
2024-04-20 23:17:11 +02:00
}
2024-04-25 16:41:06 +02:00
getSegments(): TrackSegment[] {
return [this];
}
2024-04-25 13:48:31 +02:00
toGeoJSON(): GeoJSON.Feature {
2024-04-16 22:57:28 +02:00
return {
type: "Feature",
geometry: {
type: "LineString",
2024-04-18 19:24:08 +02:00
coordinates: this.trkpt.map((point) => [point.attributes.lon, point.attributes.lat])
2024-04-16 22:57:28 +02:00
},
properties: {}
};
}
toTrackSegmentType(): TrackSegmentType {
return {
2024-04-27 11:33:14 +02:00
trkpt: this.trkpt.map((point) => point.toTrackPointType())
};
}
2024-04-16 15:41:03 +02:00
clone(): TrackSegment {
2024-05-03 15:59:34 +02:00
return new TrackSegment({
trkpt: this.trkpt.map((point) => point.clone()),
_data: cloneJSON(this._data),
});
2024-04-16 15:41:03 +02:00
}
2024-05-15 15:30:02 +02:00
// Producers
2024-05-24 16:37:26 +02:00
replaceTrackPoints(start: number, end: number, points: TrackPoint[]) {
2024-05-15 15:30:02 +02:00
return produce(this, (draft) => {
2024-05-16 13:27:12 +02:00
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
2024-05-16 11:04:12 +02:00
let trkpt = og.trkpt.slice();
trkpt.splice(start, end - start + 1, ...points);
2024-05-16 13:27:12 +02:00
draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
2024-05-15 15:30:02 +02:00
});
}
2024-05-24 16:37:26 +02:00
_reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date) {
2024-05-15 15:30:02 +02:00
return produce(this, (draft) => {
if (originalNextTimestamp !== undefined && newPreviousTimestamp !== undefined) {
2024-05-16 10:41:49 +02:00
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let originalEndTimestamp = og.getEndTimestamp();
2024-05-15 15:30:02 +02:00
let newStartTimestamp = new Date(
newPreviousTimestamp.getTime() + originalNextTimestamp.getTime() - originalEndTimestamp.getTime()
);
2024-05-16 10:41:49 +02:00
let trkpt = og.trkpt.map((point, i) => new TrackPoint({
attributes: cloneJSON(point.attributes),
ele: point.ele,
time: new Date(
newStartTimestamp.getTime() + (originalEndTimestamp.getTime() - og.trkpt[i].time.getTime())
),
extensions: cloneJSON(point.extensions),
_data: cloneJSON(point._data),
}));
trkpt.reverse();
draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
} else {
draft.trkpt.reverse();
2024-05-15 15:30:02 +02:00
}
});
}
2024-06-10 20:03:57 +02:00
crop(start: number, end: number) {
return produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trkpt = og.trkpt.slice(start, end + 1);
draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
});
}
2024-06-11 16:33:06 +02:00
clean(bounds: [Coordinates, Coordinates], inside: boolean) {
return produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trkpt = og.trkpt.filter((point) => {
let inBounds = point.attributes.lat >= bounds[0].lat && point.attributes.lat <= bounds[1].lat && point.attributes.lon >= bounds[0].lon && point.attributes.lon <= bounds[1].lon;
return inBounds !== inside;
});
draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
});
}
2024-04-16 15:41:03 +02:00
};
export class TrackPoint {
2024-05-07 18:14:47 +02:00
[immerable] = true;
2024-04-16 15:41:03 +02:00
attributes: Coordinates;
ele?: number;
time?: Date;
extensions?: TrackPointExtensions;
2024-04-30 15:19:50 +02:00
_data: { [key: string]: any } = {};
2024-04-16 15:41:03 +02:00
2024-05-15 15:30:02 +02:00
constructor(point: TrackPointType & { _data?: any } | TrackPoint) {
2024-05-03 15:59:34 +02:00
this.attributes = point.attributes;
2024-04-16 15:41:03 +02:00
this.ele = point.ele;
2024-05-03 15:59:34 +02:00
this.time = point.time;
this.extensions = point.extensions;
if (point.hasOwnProperty('_data')) {
this._data = point._data;
2024-04-30 15:19:50 +02:00
}
2024-04-16 13:54:48 +02:00
}
2024-04-18 19:15:01 +02:00
getCoordinates(): Coordinates {
return this.attributes;
}
2024-04-25 19:02:34 +02:00
setCoordinates(coordinates: Coordinates): void {
this.attributes = coordinates;
}
getLatitude(): number {
return this.attributes.lat;
}
getLongitude(): number {
return this.attributes.lon;
}
getHeartRate(): number {
return this.extensions && this.extensions['gpxtpx:TrackPointExtension'] && this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:hr'] ? this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:hr'] : undefined;
}
getCadence(): number {
return this.extensions && this.extensions['gpxtpx:TrackPointExtension'] && this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:cad'] ? this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:cad'] : undefined;
}
getTemperature(): number {
return this.extensions && this.extensions['gpxtpx:TrackPointExtension'] && this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:atemp'] ? this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:atemp'] : undefined;
}
getPower(): number {
return this.extensions && this.extensions["gpxpx:PowerExtension"] && this.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"] ? this.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"] : undefined;
}
getSurface(): string {
return this.extensions && this.extensions["gpxtpx:TrackPointExtension"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"].surface ? this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"].surface : undefined;
}
2024-04-26 22:35:42 +02:00
setSurface(surface: string): void {
if (!this.extensions) {
this.extensions = {};
}
if (!this.extensions["gpxtpx:TrackPointExtension"]) {
this.extensions["gpxtpx:TrackPointExtension"] = {};
}
if (!this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"]) {
this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"] = {};
}
this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"]["surface"] = surface;
}
2024-04-27 11:33:14 +02:00
toTrackPointType(): TrackPointType {
return {
attributes: this.attributes,
ele: this.ele,
time: this.time,
extensions: this.extensions,
};
}
2024-05-03 15:59:34 +02:00
clone(): TrackPoint {
return new TrackPoint({
attributes: cloneJSON(this.attributes),
ele: this.ele,
time: this.time ? new Date(this.time.getTime()) : undefined,
extensions: cloneJSON(this.extensions),
_data: cloneJSON(this._data),
});
}
2024-04-18 17:11:23 +02:00
};
export class Waypoint {
2024-05-24 13:16:41 +02:00
[immerable] = true;
2024-04-18 17:11:23 +02:00
attributes: Coordinates;
ele?: number;
time?: Date;
name?: string;
cmt?: string;
desc?: string;
link?: Link;
sym?: string;
type?: string;
2024-05-24 13:16:41 +02:00
_data: { [key: string]: any } = {};
2024-04-18 17:11:23 +02:00
2024-05-24 13:16:41 +02:00
constructor(waypoint: WaypointType & { _data?: any } | Waypoint, index?: number) {
2024-05-03 15:59:34 +02:00
this.attributes = waypoint.attributes;
2024-04-18 17:11:23 +02:00
this.ele = waypoint.ele;
2024-05-03 22:15:47 +02:00
this.time = waypoint.time;
2024-04-18 17:11:23 +02:00
this.name = waypoint.name;
this.cmt = waypoint.cmt;
this.desc = waypoint.desc;
2024-05-03 15:59:34 +02:00
this.link = waypoint.link;
2024-04-18 17:11:23 +02:00
this.sym = waypoint.sym;
this.type = waypoint.type;
2024-05-24 13:16:41 +02:00
if (waypoint.hasOwnProperty('_data')) {
this._data = waypoint._data;
}
if (index !== undefined) {
this._data['index'] = index;
}
2024-04-18 17:11:23 +02:00
}
getCoordinates(): Coordinates {
return this.attributes;
}
setCoordinates(coordinates: Coordinates): void {
this.attributes = coordinates;
}
2024-05-03 15:59:34 +02:00
2024-05-13 19:43:10 +02:00
getLatitude(): number {
return this.attributes.lat;
}
getLongitude(): number {
return this.attributes.lon;
}
2024-05-03 15:59:34 +02:00
clone(): Waypoint {
return new Waypoint({
attributes: cloneJSON(this.attributes),
ele: this.ele,
time: this.time ? new Date(this.time.getTime()) : undefined,
name: this.name,
cmt: this.cmt,
desc: this.desc,
link: cloneJSON(this.link),
sym: this.sym,
type: this.type,
});
}
2024-04-18 19:15:01 +02:00
}
2024-04-19 13:20:31 +02:00
export class GPXStatistics {
2024-05-03 22:15:47 +02:00
global: {
distance: {
moving: number,
total: number,
},
time: {
2024-06-13 17:36:43 +02:00
start: Date | undefined,
end: Date | undefined,
2024-05-03 22:15:47 +02:00
moving: number,
total: number,
},
speed: {
moving: number,
total: number,
},
elevation: {
gain: number,
loss: number,
},
bounds: {
southWest: Coordinates,
northEast: Coordinates,
},
2024-04-18 19:15:01 +02:00
};
2024-05-03 22:15:47 +02:00
local: {
points: TrackPoint[],
2024-06-10 20:03:57 +02:00
distance: {
moving: number[],
total: number[],
},
time: {
moving: number[],
total: number[],
},
2024-05-03 22:15:47 +02:00
speed: number[],
elevation: {
smoothed: number[],
gain: number[],
loss: number[],
},
slope: number[],
2024-04-18 19:15:01 +02:00
};
constructor() {
2024-05-03 22:15:47 +02:00
this.global = {
distance: {
moving: 0,
total: 0,
},
time: {
2024-06-13 17:36:43 +02:00
start: undefined,
end: undefined,
2024-05-03 22:15:47 +02:00
moving: 0,
total: 0,
2024-04-22 10:45:02 +02:00
},
2024-05-03 22:15:47 +02:00
speed: {
moving: 0,
total: 0,
},
elevation: {
gain: 0,
loss: 0,
},
bounds: {
southWest: {
lat: 90,
2024-05-07 12:16:30 +02:00
lon: 180,
2024-05-03 22:15:47 +02:00
},
northEast: {
lat: -90,
2024-05-07 12:16:30 +02:00
lon: -180,
2024-05-03 22:15:47 +02:00
},
2024-04-22 10:45:02 +02:00
},
};
2024-05-03 22:15:47 +02:00
this.local = {
points: [],
2024-06-10 20:03:57 +02:00
distance: {
moving: [],
total: [],
},
time: {
moving: [],
total: [],
},
2024-05-03 22:15:47 +02:00
speed: [],
elevation: {
smoothed: [],
gain: [],
loss: [],
},
slope: [],
};
2024-04-18 19:15:01 +02:00
}
mergeWith(other: GPXStatistics): void {
2024-05-03 22:15:47 +02:00
this.local.points = this.local.points.concat(other.local.points);
2024-04-18 19:15:01 +02:00
2024-06-10 20:03:57 +02:00
this.local.distance.total = this.local.distance.total.concat(other.local.distance.total.map((distance) => distance + this.global.distance.total));
this.local.distance.moving = this.local.distance.moving.concat(other.local.distance.moving.map((distance) => distance + this.global.distance.moving));
this.local.time.total = this.local.time.total.concat(other.local.time.total.map((time) => time + this.global.time.total));
this.local.time.moving = this.local.time.moving.concat(other.local.time.moving.map((time) => time + this.global.time.moving));
2024-05-03 22:15:47 +02:00
this.local.elevation.gain = this.local.elevation.gain.concat(other.local.elevation.gain.map((gain) => gain + this.global.elevation.gain));
this.local.elevation.loss = this.local.elevation.loss.concat(other.local.elevation.loss.map((loss) => loss + this.global.elevation.loss));
2024-04-18 19:15:01 +02:00
2024-05-03 22:15:47 +02:00
this.local.speed = this.local.speed.concat(other.local.speed);
this.local.elevation.smoothed = this.local.elevation.smoothed.concat(other.local.elevation.smoothed);
this.local.slope = this.local.slope.concat(other.local.slope);
2024-04-22 10:45:02 +02:00
2024-05-03 22:15:47 +02:00
this.global.distance.total += other.global.distance.total;
this.global.distance.moving += other.global.distance.moving;
2024-04-18 19:15:01 +02:00
2024-06-13 17:36:43 +02:00
this.global.time.start = this.global.time.start !== undefined && other.global.time.start !== undefined ? new Date(Math.min(this.global.time.start.getTime(), other.global.time.start.getTime())) : this.global.time.start ?? other.global.time.start;
this.global.time.end = this.global.time.end !== undefined && other.global.time.end !== undefined ? new Date(Math.max(this.global.time.end.getTime(), other.global.time.end.getTime())) : this.global.time.end ?? other.global.time.end;
2024-05-03 22:15:47 +02:00
this.global.time.total += other.global.time.total;
this.global.time.moving += other.global.time.moving;
2024-06-10 20:03:57 +02:00
this.global.speed.moving = this.global.time.moving > 0 ? this.global.distance.moving / (this.global.time.moving / 3600) : 0;
this.global.speed.total = this.global.time.total > 0 ? this.global.distance.total / (this.global.time.total / 3600) : 0;
2024-05-03 22:15:47 +02:00
this.global.elevation.gain += other.global.elevation.gain;
this.global.elevation.loss += other.global.elevation.loss;
this.global.bounds.southWest.lat = Math.min(this.global.bounds.southWest.lat, other.global.bounds.southWest.lat);
2024-05-07 12:16:30 +02:00
this.global.bounds.southWest.lon = Math.min(this.global.bounds.southWest.lon, other.global.bounds.southWest.lon);
2024-05-03 22:15:47 +02:00
this.global.bounds.northEast.lat = Math.max(this.global.bounds.northEast.lat, other.global.bounds.northEast.lat);
2024-05-07 12:16:30 +02:00
this.global.bounds.northEast.lon = Math.max(this.global.bounds.northEast.lon, other.global.bounds.northEast.lon);
2024-05-03 22:15:47 +02:00
}
2024-06-10 20:03:57 +02:00
slice(start: number, end: number): GPXStatistics {
let statistics = new GPXStatistics();
statistics.local.points = this.local.points.slice(start, end);
statistics.global.distance.total = this.local.distance.total[end - 1] - this.local.distance.total[start];
statistics.global.distance.moving = this.local.distance.moving[end - 1] - this.local.distance.moving[start];
2024-06-13 17:36:43 +02:00
statistics.global.time.start = this.local.points[start].time;
statistics.global.time.end = this.local.points[end - 1].time;
2024-06-10 20:03:57 +02:00
statistics.global.time.total = this.local.time.total[end - 1] - this.local.time.total[start];
statistics.global.time.moving = this.local.time.moving[end - 1] - this.local.time.moving[start];
statistics.global.speed.moving = statistics.global.time.moving > 0 ? statistics.global.distance.moving / (statistics.global.time.moving / 3600) : 0;
statistics.global.speed.total = statistics.global.time.total > 0 ? statistics.global.distance.total / (statistics.global.time.total / 3600) : 0;
statistics.global.elevation.gain = this.local.elevation.gain[end - 1] - this.local.elevation.gain[start];
statistics.global.elevation.loss = this.local.elevation.loss[end - 1] - this.local.elevation.loss[start];
statistics.global.bounds.southWest.lat = this.global.bounds.southWest.lat;
statistics.global.bounds.southWest.lon = this.global.bounds.southWest.lon;
statistics.global.bounds.northEast.lat = this.global.bounds.northEast.lat;
statistics.global.bounds.northEast.lon = this.global.bounds.northEast.lon;
return statistics;
}
2024-04-18 19:15:01 +02:00
}
const earthRadius = 6371008.8;
2024-04-25 19:02:34 +02:00
export function distance(coord1: Coordinates, coord2: Coordinates): number {
2024-04-18 19:15:01 +02:00
const rad = Math.PI / 180;
const lat1 = coord1.lat * rad;
const lat2 = coord2.lat * rad;
2024-04-18 19:24:08 +02:00
const a = Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1) * Math.cos(lat2) * Math.cos((coord2.lon - coord1.lon) * rad);
2024-04-18 19:15:01 +02:00
const maxMeters = earthRadius * Math.acos(Math.min(a, 1));
return maxMeters;
2024-04-21 12:13:58 +02:00
}
2024-05-15 15:30:02 +02:00
function distanceWindowSmoothing(points: ReadonlyArray<Readonly<TrackPoint>>, distanceWindow: number, accumulate: (index: number) => number, compute: (accumulated: number, start: number, end: number) => number, remove?: (index: number) => number): number[] {
2024-04-21 12:13:58 +02:00
let result = [];
let start = 0, end = 0, accumulated = 0;
for (var i = 0; i < points.length; i++) {
while (start < i && distance(points[start].getCoordinates(), points[i].getCoordinates()) > distanceWindow) {
if (remove) {
accumulated -= remove(start);
} else {
accumulated -= accumulate(start);
}
start++;
}
while (end < points.length && distance(points[i].getCoordinates(), points[end].getCoordinates()) <= distanceWindow) {
accumulated += accumulate(end);
end++;
}
result[i] = compute(accumulated, start, end - 1);
}
return result;
}
2024-05-15 15:30:02 +02:00
function distanceWindowSmoothingWithDistanceAccumulator(points: ReadonlyArray<Readonly<TrackPoint>>, distanceWindow: number, compute: (accumulated: number, start: number, end: number) => number): number[] {
2024-04-21 12:13:58 +02:00
return distanceWindowSmoothing(points, distanceWindow, (index) => index > 0 ? distance(points[index - 1].getCoordinates(), points[index].getCoordinates()) : 0, compute, (index) => distance(points[index].getCoordinates(), points[index + 1].getCoordinates()));
2024-05-16 10:41:49 +02:00
}
function getOriginal(obj: any): any {
while (isDraft(obj)) {
obj = original(obj);
}
return obj;
2024-04-18 17:11:23 +02:00
}