mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-01-22 17:48:41 +00:00
392 lines
12 KiB
TypeScript
392 lines
12 KiB
TypeScript
import { TrackPoint } from './gpx';
|
|
import { Coordinates } from './types';
|
|
|
|
export class GPXGlobalStatistics {
|
|
length: number;
|
|
distance: {
|
|
moving: number;
|
|
total: number;
|
|
};
|
|
time: {
|
|
start: Date | undefined;
|
|
end: Date | undefined;
|
|
moving: number;
|
|
total: number;
|
|
};
|
|
speed: {
|
|
moving: number;
|
|
total: number;
|
|
};
|
|
elevation: {
|
|
gain: number;
|
|
loss: number;
|
|
};
|
|
bounds: {
|
|
southWest: Coordinates;
|
|
northEast: Coordinates;
|
|
};
|
|
atemp: {
|
|
avg: number;
|
|
count: number;
|
|
};
|
|
hr: {
|
|
avg: number;
|
|
count: number;
|
|
};
|
|
cad: {
|
|
avg: number;
|
|
count: number;
|
|
};
|
|
power: {
|
|
avg: number;
|
|
count: number;
|
|
};
|
|
extensions: Record<string, Record<string, number>>;
|
|
|
|
constructor() {
|
|
this.length = 0;
|
|
this.distance = {
|
|
moving: 0,
|
|
total: 0,
|
|
};
|
|
this.time = {
|
|
start: undefined,
|
|
end: undefined,
|
|
moving: 0,
|
|
total: 0,
|
|
};
|
|
this.speed = {
|
|
moving: 0,
|
|
total: 0,
|
|
};
|
|
this.elevation = {
|
|
gain: 0,
|
|
loss: 0,
|
|
};
|
|
this.bounds = {
|
|
southWest: {
|
|
lat: 90,
|
|
lon: 180,
|
|
},
|
|
northEast: {
|
|
lat: -90,
|
|
lon: -180,
|
|
},
|
|
};
|
|
this.atemp = {
|
|
avg: 0,
|
|
count: 0,
|
|
};
|
|
this.hr = {
|
|
avg: 0,
|
|
count: 0,
|
|
};
|
|
this.cad = {
|
|
avg: 0,
|
|
count: 0,
|
|
};
|
|
this.power = {
|
|
avg: 0,
|
|
count: 0,
|
|
};
|
|
this.extensions = {};
|
|
}
|
|
|
|
mergeWith(other: GPXGlobalStatistics): void {
|
|
this.length += other.length;
|
|
|
|
this.distance.total += other.distance.total;
|
|
this.distance.moving += other.distance.moving;
|
|
|
|
this.time.start =
|
|
this.time.start !== undefined && other.time.start !== undefined
|
|
? new Date(Math.min(this.time.start.getTime(), other.time.start.getTime()))
|
|
: (this.time.start ?? other.time.start);
|
|
this.time.end =
|
|
this.time.end !== undefined && other.time.end !== undefined
|
|
? new Date(Math.max(this.time.end.getTime(), other.time.end.getTime()))
|
|
: (this.time.end ?? other.time.end);
|
|
|
|
this.time.total += other.time.total;
|
|
this.time.moving += other.time.moving;
|
|
|
|
this.speed.moving =
|
|
this.time.moving > 0 ? this.distance.moving / (this.time.moving / 3600) : 0;
|
|
this.speed.total = this.time.total > 0 ? this.distance.total / (this.time.total / 3600) : 0;
|
|
|
|
this.elevation.gain += other.elevation.gain;
|
|
this.elevation.loss += other.elevation.loss;
|
|
|
|
this.bounds.southWest.lat = Math.min(this.bounds.southWest.lat, other.bounds.southWest.lat);
|
|
this.bounds.southWest.lon = Math.min(this.bounds.southWest.lon, other.bounds.southWest.lon);
|
|
this.bounds.northEast.lat = Math.max(this.bounds.northEast.lat, other.bounds.northEast.lat);
|
|
this.bounds.northEast.lon = Math.max(this.bounds.northEast.lon, other.bounds.northEast.lon);
|
|
|
|
this.atemp.avg =
|
|
(this.atemp.count * this.atemp.avg + other.atemp.count * other.atemp.avg) /
|
|
Math.max(1, this.atemp.count + other.atemp.count);
|
|
this.atemp.count += other.atemp.count;
|
|
this.hr.avg =
|
|
(this.hr.count * this.hr.avg + other.hr.count * other.hr.avg) /
|
|
Math.max(1, this.hr.count + other.hr.count);
|
|
this.hr.count += other.hr.count;
|
|
this.cad.avg =
|
|
(this.cad.count * this.cad.avg + other.cad.count * other.cad.avg) /
|
|
Math.max(1, this.cad.count + other.cad.count);
|
|
this.cad.count += other.cad.count;
|
|
this.power.avg =
|
|
(this.power.count * this.power.avg + other.power.count * other.power.avg) /
|
|
Math.max(1, this.power.count + other.power.count);
|
|
this.power.count += other.power.count;
|
|
|
|
Object.keys(other.extensions).forEach((extension) => {
|
|
if (this.extensions[extension] === undefined) {
|
|
this.extensions[extension] = {};
|
|
}
|
|
Object.keys(other.extensions[extension]).forEach((value) => {
|
|
if (this.extensions[extension][value] === undefined) {
|
|
this.extensions[extension][value] = 0;
|
|
}
|
|
this.extensions[extension][value] += other.extensions[extension][value];
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
export class TrackPointLocalStatistics {
|
|
distance: {
|
|
moving: number;
|
|
total: number;
|
|
};
|
|
time: {
|
|
moving: number;
|
|
total: number;
|
|
};
|
|
speed: number;
|
|
elevation: {
|
|
gain: number;
|
|
loss: number;
|
|
};
|
|
slope: {
|
|
at: number;
|
|
segment: number;
|
|
length: number;
|
|
};
|
|
|
|
constructor() {
|
|
this.distance = {
|
|
moving: 0,
|
|
total: 0,
|
|
};
|
|
this.time = {
|
|
moving: 0,
|
|
total: 0,
|
|
};
|
|
this.speed = 0;
|
|
this.elevation = {
|
|
gain: 0,
|
|
loss: 0,
|
|
};
|
|
this.slope = {
|
|
at: 0,
|
|
segment: 0,
|
|
length: 0,
|
|
};
|
|
}
|
|
}
|
|
|
|
export class GPXLocalStatistics {
|
|
points: TrackPoint[];
|
|
data: TrackPointLocalStatistics[];
|
|
|
|
constructor() {
|
|
this.points = [];
|
|
this.data = [];
|
|
}
|
|
}
|
|
|
|
export type TrackPointWithLocalStatistics = {
|
|
trkpt: TrackPoint;
|
|
} & TrackPointLocalStatistics;
|
|
|
|
export class GPXStatistics {
|
|
global: GPXGlobalStatistics;
|
|
local: GPXLocalStatistics;
|
|
|
|
constructor() {
|
|
this.global = new GPXGlobalStatistics();
|
|
this.local = new GPXLocalStatistics();
|
|
}
|
|
|
|
sliced(start: number, end: number): GPXGlobalStatistics {
|
|
if (start < 0) {
|
|
start = 0;
|
|
} else if (start >= this.global.length) {
|
|
return new GPXGlobalStatistics();
|
|
}
|
|
|
|
if (end < start) {
|
|
return new GPXGlobalStatistics();
|
|
} else if (end >= this.global.length) {
|
|
end = this.global.length - 1;
|
|
}
|
|
|
|
if (start === 0 && end === this.global.length - 1) {
|
|
return this.global;
|
|
}
|
|
|
|
let statistics = new GPXGlobalStatistics();
|
|
|
|
statistics.length = end - start + 1;
|
|
|
|
statistics.distance.total =
|
|
this.local.data[end].distance.total - this.local.data[start].distance.total;
|
|
statistics.distance.moving =
|
|
this.local.data[end].distance.moving - this.local.data[start].distance.moving;
|
|
|
|
statistics.time.start = this.local.points[start].time;
|
|
statistics.time.end = this.local.points[end].time;
|
|
|
|
statistics.time.total = this.local.data[end].time.total - this.local.data[start].time.total;
|
|
statistics.time.moving =
|
|
this.local.data[end].time.moving - this.local.data[start].time.moving;
|
|
|
|
statistics.speed.moving =
|
|
statistics.time.moving > 0
|
|
? statistics.distance.moving / (statistics.time.moving / 3600)
|
|
: 0;
|
|
statistics.speed.total =
|
|
statistics.time.total > 0
|
|
? statistics.distance.total / (statistics.time.total / 3600)
|
|
: 0;
|
|
|
|
statistics.elevation.gain =
|
|
this.local.data[end].elevation.gain - this.local.data[start].elevation.gain;
|
|
statistics.elevation.loss =
|
|
this.local.data[end].elevation.loss - this.local.data[start].elevation.loss;
|
|
|
|
statistics.bounds.southWest.lat = this.global.bounds.southWest.lat;
|
|
statistics.bounds.southWest.lon = this.global.bounds.southWest.lon;
|
|
statistics.bounds.northEast.lat = this.global.bounds.northEast.lat;
|
|
statistics.bounds.northEast.lon = this.global.bounds.northEast.lon;
|
|
|
|
statistics.atemp = this.global.atemp;
|
|
statistics.hr = this.global.hr;
|
|
statistics.cad = this.global.cad;
|
|
statistics.power = this.global.power;
|
|
|
|
return statistics;
|
|
}
|
|
}
|
|
|
|
export class GPXStatisticsGroup {
|
|
private _statistics: GPXStatistics[];
|
|
private _cumulative: GPXGlobalStatistics[];
|
|
private _slice: [number, number] | null = null;
|
|
global: GPXGlobalStatistics;
|
|
|
|
constructor() {
|
|
this._statistics = [];
|
|
this._cumulative = [new GPXGlobalStatistics()];
|
|
this.global = new GPXGlobalStatistics();
|
|
}
|
|
|
|
add(statistics: GPXStatistics | GPXStatisticsGroup): void {
|
|
if (statistics instanceof GPXStatisticsGroup) {
|
|
statistics._statistics.forEach((stats) => this._add(stats));
|
|
} else {
|
|
this._add(statistics);
|
|
}
|
|
}
|
|
|
|
_add(statistics: GPXStatistics): void {
|
|
this._statistics.push(statistics);
|
|
const cumulative = new GPXGlobalStatistics();
|
|
cumulative.mergeWith(this._cumulative[this._cumulative.length - 1]);
|
|
cumulative.mergeWith(statistics.global);
|
|
this._cumulative.push(cumulative);
|
|
this.global.mergeWith(statistics.global);
|
|
}
|
|
|
|
sliced(start: number, end: number): GPXGlobalStatistics {
|
|
let sliced = new GPXGlobalStatistics();
|
|
for (let i = 0; i < this._statistics.length; i++) {
|
|
const statistics = this._statistics[i];
|
|
const cumulative = this._cumulative[i];
|
|
if (start < cumulative.length + statistics.global.length && end >= cumulative.length) {
|
|
const localStart = Math.max(0, start - cumulative.length);
|
|
const localEnd = Math.min(statistics.global.length - 1, end - cumulative.length);
|
|
sliced.mergeWith(statistics.sliced(localStart, localEnd));
|
|
}
|
|
}
|
|
return sliced;
|
|
}
|
|
|
|
getTrackPoint(index: number): TrackPointWithLocalStatistics | undefined {
|
|
if (this._slice !== null) {
|
|
index += this._slice[0];
|
|
}
|
|
for (let i = 0; i < this._statistics.length; i++) {
|
|
const statistics = this._statistics[i];
|
|
const cumulative = this._cumulative[i];
|
|
if (index < cumulative.length + statistics.global.length) {
|
|
return this._getTrackPoint(cumulative, statistics, index - cumulative.length);
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
_getTrackPoint(
|
|
cumulative: GPXGlobalStatistics,
|
|
statistics: GPXStatistics,
|
|
index: number
|
|
): TrackPointWithLocalStatistics {
|
|
const point = statistics.local.points[index];
|
|
return {
|
|
trkpt: point,
|
|
distance: {
|
|
moving: statistics.local.data[index].distance.moving + cumulative.distance.moving,
|
|
total: statistics.local.data[index].distance.total + cumulative.distance.total,
|
|
},
|
|
time: {
|
|
moving: statistics.local.data[index].time.moving + cumulative.time.moving,
|
|
total: statistics.local.data[index].time.total + cumulative.time.total,
|
|
},
|
|
speed: statistics.local.data[index].speed,
|
|
elevation: {
|
|
gain: statistics.local.data[index].elevation.gain + cumulative.elevation.gain,
|
|
loss: statistics.local.data[index].elevation.loss + cumulative.elevation.loss,
|
|
},
|
|
slope: {
|
|
at: statistics.local.data[index].slope.at,
|
|
segment: statistics.local.data[index].slope.segment,
|
|
length: statistics.local.data[index].slope.length,
|
|
},
|
|
};
|
|
}
|
|
|
|
forEachTrackPoint(
|
|
callback: (
|
|
point: TrackPoint,
|
|
distance: number,
|
|
speed: number,
|
|
slope: { at: number; segment: number; length: number },
|
|
index: number
|
|
) => void
|
|
): void {
|
|
for (let i = 0; i < this._statistics.length; i++) {
|
|
const statistics = this._statistics[i];
|
|
const cumulative = this._cumulative[i];
|
|
statistics.local.points.forEach((point, index) =>
|
|
callback(
|
|
point,
|
|
cumulative.distance.total + statistics.local.data[index].distance.total,
|
|
statistics.local.data[index].speed,
|
|
statistics.local.data[index].slope,
|
|
cumulative.length + index
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|