diff --git a/gpx/src/gpx.ts b/gpx/src/gpx.ts index bd25bbc9..6cc3fed8 100644 --- a/gpx/src/gpx.ts +++ b/gpx/src/gpx.ts @@ -19,9 +19,8 @@ export abstract class GPXTreeElement> { abstract getStartTimestamp(): Date; abstract getEndTimestamp(): Date; - abstract getStatistics(): GPXStatistics; abstract getTrackPoints(): TrackPoint[]; - abstract getTrackPointsAndStatistics(): { points: TrackPoint[], point_statistics: TrackPointStatistics, statistics: GPXStatistics }; + abstract getStatistics(): GPXStatistics; abstract getSegments(): TrackSegment[]; abstract toGeoJSON(): GeoJSON.Feature | GeoJSON.Feature[] | GeoJSON.FeatureCollection | GeoJSON.FeatureCollection[]; @@ -85,40 +84,6 @@ abstract class GPXTreeNode> extends GPXTreeElement return statistics; } - getTrackPointsAndStatistics(): { points: TrackPoint[], point_statistics: TrackPointStatistics, statistics: GPXStatistics } { - let points: TrackPoint[] = []; - let point_statistics: TrackPointStatistics = { - distance: [], - time: [], - speed: [], - elevation: { - smoothed: [], - gain: [], - loss: [], - }, - slope: [], - }; - let statistics = new GPXStatistics(); - - for (let child of this.getChildren()) { - let childData = child.getTrackPointsAndStatistics(); - points = points.concat(childData.points); - - point_statistics.distance = point_statistics.distance.concat(childData.point_statistics.distance.map((distance) => distance + statistics.distance.total)); - point_statistics.time = point_statistics.time.concat(childData.point_statistics.time.map((time) => time + statistics.time.total)); - point_statistics.elevation.gain = point_statistics.elevation.gain.concat(childData.point_statistics.elevation.gain.map((gain) => gain + statistics.elevation.gain)); - point_statistics.elevation.loss = point_statistics.elevation.loss.concat(childData.point_statistics.elevation.loss.map((loss) => loss + statistics.elevation.loss)); - - point_statistics.speed = point_statistics.speed.concat(childData.point_statistics.speed); - point_statistics.elevation.smoothed = point_statistics.elevation.smoothed.concat(childData.point_statistics.elevation.smoothed); - point_statistics.slope = point_statistics.slope.concat(childData.point_statistics.slope); - - statistics.mergeWith(childData.statistics); - } - - return { points, point_statistics, statistics }; - } - getSegments(): TrackSegment[] { return this.getChildren().flatMap((child) => child.getSegments()); } @@ -165,13 +130,8 @@ export class GPXFile extends GPXTreeNode{ if (gpx) { this.attributes = gpx.attributes this.metadata = gpx.metadata; - if (gpx instanceof GPXFile) { - this.wpt = gpx.wpt; - this.trk = gpx.trk; - } else { - this.wpt = gpx.wpt ? gpx.wpt.map((waypoint) => new Waypoint(waypoint)) : []; - this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : []; - } + this.wpt = gpx.wpt ? gpx.wpt.map((waypoint) => new Waypoint(waypoint)) : []; + this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : []; if (gpx.hasOwnProperty('_data')) { this._data = gpx._data; } @@ -234,11 +194,7 @@ export class Track extends GPXTreeNode { this.src = track.src; this.link = track.link; this.type = track.type; - if (track instanceof Track) { - this.trkseg = track.trkseg; - } else { - this.trkseg = track.trkseg ? track.trkseg.map((seg) => new TrackSegment(seg)) : []; - } + this.trkseg = track.trkseg ? track.trkseg.map((seg) => new TrackSegment(seg)) : []; this.extensions = cloneJSON(track.extensions); if (track.hasOwnProperty('_data')) { this._data = cloneJSON(track._data); @@ -301,43 +257,26 @@ export class Track extends GPXTreeNode { // A class that represents a TrackSegment in a GPX file export class TrackSegment extends GPXTreeLeaf { trkpt: TrackPoint[]; - trkptStatistics: TrackPointStatistics; - statistics: GPXStatistics; constructor(segment?: TrackSegmentType | TrackSegment) { super(); if (segment) { - if (segment instanceof TrackSegment) { - this.trkpt = segment.trkpt; - } else { - this.trkpt = segment.trkpt.map((point) => new TrackPoint(point)); - } + this.trkpt = segment.trkpt.map((point) => new TrackPoint(point)); if (segment.hasOwnProperty('_data')) { this._data = cloneJSON(segment._data); } } else { this.trkpt = []; } - - this._computeStatistics(); } - _computeStatistics(): void { + _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(); + statistics.local.points = this.trkpt; + + statistics.local.elevation.smoothed = this._computeSmoothedElevation(); + statistics.local.slope = this._computeSlope(); const points = this.trkpt; for (let i = 0; i < points.length; i++) { @@ -348,29 +287,29 @@ export class TrackSegment extends GPXTreeLeaf { if (i > 0) { dist = distance(points[i - 1].getCoordinates(), points[i].getCoordinates()) / 1000; - statistics.distance.total += dist; + statistics.global.distance.total += dist; } - trkptStatistics.distance.push(statistics.distance.total); + statistics.local.distance.push(statistics.global.distance.total); // elevation if (i > 0) { - const ele = trkptStatistics.elevation.smoothed[i] - trkptStatistics.elevation.smoothed[i - 1]; + const ele = statistics.local.elevation.smoothed[i] - statistics.local.elevation.smoothed[i - 1]; if (ele > 0) { - statistics.elevation.gain += ele; + statistics.global.elevation.gain += ele; } else { - statistics.elevation.loss -= ele; + statistics.global.elevation.loss -= ele; } } - trkptStatistics.elevation.gain.push(statistics.elevation.gain); - trkptStatistics.elevation.loss.push(statistics.elevation.loss); + statistics.local.elevation.gain.push(statistics.global.elevation.gain); + statistics.local.elevation.loss.push(statistics.global.elevation.loss); // time if (points[0].time !== undefined && points[i].time !== undefined) { const time = (points[i].time.getTime() - points[0].time.getTime()) / 1000; - trkptStatistics.time.push(time); + statistics.local.time.push(time); } // speed @@ -380,26 +319,25 @@ export class TrackSegment extends GPXTreeLeaf { speed = dist / (time / 3600); if (speed >= 0.5) { - statistics.distance.moving += dist; - statistics.time.moving += time; + statistics.global.distance.moving += dist; + statistics.global.time.moving += time; } } // bounds - statistics.bounds.southWest.lat = Math.min(statistics.bounds.southWest.lat, points[i].attributes.lat); - statistics.bounds.southWest.lon = Math.max(statistics.bounds.southWest.lon, points[i].attributes.lon); - statistics.bounds.northEast.lat = Math.max(statistics.bounds.northEast.lat, points[i].attributes.lat); - statistics.bounds.northEast.lon = Math.min(statistics.bounds.northEast.lon, points[i].attributes.lon); + statistics.global.bounds.southWest.lat = Math.min(statistics.global.bounds.southWest.lat, points[i].attributes.lat); + statistics.global.bounds.southWest.lon = Math.max(statistics.global.bounds.southWest.lon, points[i].attributes.lon); + statistics.global.bounds.northEast.lat = Math.max(statistics.global.bounds.northEast.lat, points[i].attributes.lat); + statistics.global.bounds.northEast.lon = Math.min(statistics.global.bounds.northEast.lon, points[i].attributes.lon); } - statistics.time.total = trkptStatistics.time[trkptStatistics.time.length - 1]; - statistics.speed.total = statistics.distance.total / (statistics.time.total / 3600); - statistics.speed.moving = statistics.distance.moving / (statistics.time.moving / 3600); + statistics.global.time.total = statistics.local.time[statistics.local.time.length - 1]; + statistics.global.speed.total = statistics.global.distance.total / (statistics.global.time.total / 3600); + statistics.global.speed.moving = statistics.global.distance.moving / (statistics.global.time.moving / 3600); - trkptStatistics.speed = distanceWindowSmoothingWithDistanceAccumulator(points, 200, (accumulated, start, end) => (points[start].time && points[end].time) ? 3600 * accumulated / (points[end].time.getTime() - points[start].time.getTime()) : undefined); + 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); - this.statistics = statistics; - this.trkptStatistics = trkptStatistics; + return statistics; } _computeSmoothedElevation(): number[] { @@ -461,15 +399,7 @@ export class TrackSegment extends GPXTreeLeaf { } getStatistics(): GPXStatistics { - return this.statistics; - } - - getTrackPointsAndStatistics(): { points: TrackPoint[], point_statistics: TrackPointStatistics, statistics: GPXStatistics } { - return { - points: this.trkpt, - point_statistics: this.trkptStatistics, - statistics: this.statistics, - }; + return this._computeStatistics(); } getSegments(): TrackSegment[] { @@ -509,7 +439,6 @@ export class TrackPoint { _data: { [key: string]: any } = {}; constructor(point: TrackPointType | TrackPoint) { - this.attributes = point.attributes; this.attributes = point.attributes; this.ele = point.ele; this.time = point.time; @@ -602,9 +531,7 @@ export class Waypoint { constructor(waypoint: WaypointType | Waypoint) { this.attributes = waypoint.attributes; this.ele = waypoint.ele; - if (waypoint.time) { - this.time = new Date(waypoint.time.getTime()); - } + this.time = waypoint.time; this.name = waypoint.name; this.cmt = waypoint.cmt; this.desc = waypoint.desc; @@ -637,88 +564,116 @@ export class Waypoint { } export class GPXStatistics { - distance: { - moving: number; - total: number; + global: { + distance: { + moving: number, + total: number, + }, + time: { + moving: number, + total: number, + }, + speed: { + moving: number, + total: number, + }, + elevation: { + gain: number, + loss: number, + }, + bounds: { + southWest: Coordinates, + northEast: Coordinates, + }, }; - time: { - moving: number; - total: number; + local: { + points: TrackPoint[], + distance: number[], + time: number[], + speed: number[], + elevation: { + smoothed: number[], + gain: number[], + loss: number[], + }, + slope: number[], }; - speed: { - moving: number; - total: number; - }; - elevation: { - gain: number; - loss: number; - }; - bounds: { - southWest: Coordinates; - northEast: Coordinates; - } 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, - }; - this.bounds = { - southWest: { - lat: 90, - lon: -180, + this.global = { + distance: { + moving: 0, + total: 0, }, - northEast: { - lat: -90, - lon: 180, + time: { + moving: 0, + total: 0, }, + speed: { + moving: 0, + total: 0, + }, + elevation: { + gain: 0, + loss: 0, + }, + bounds: { + southWest: { + lat: 90, + lon: -180, + }, + northEast: { + lat: -90, + lon: 180, + }, + }, + }; + this.local = { + points: [], + distance: [], + time: [], + speed: [], + elevation: { + smoothed: [], + gain: [], + loss: [], + }, + slope: [], }; } 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.local.points = this.local.points.concat(other.local.points); - this.speed.moving = this.distance.moving / (this.time.moving / 3600); - this.speed.total = this.distance.total / (this.time.total / 3600); + this.local.distance = this.local.distance.concat(other.local.distance.map((distance) => distance + this.global.distance.total)); + this.local.time = this.local.time.concat(other.local.time.map((time) => time + this.global.time.total)); + 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)); - this.elevation.gain += other.elevation.gain; - this.elevation.loss += other.elevation.loss; + 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); - this.bounds.southWest.lat = Math.min(this.bounds.southWest.lat, other.bounds.southWest.lat); - this.bounds.southWest.lon = Math.max(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.min(this.bounds.northEast.lon, other.bounds.northEast.lon); + this.global.distance.total += other.global.distance.total; + this.global.distance.moving += other.global.distance.moving; + + this.global.time.total += other.global.time.total; + this.global.time.moving += other.global.time.moving; + + this.global.speed.moving = this.global.distance.moving / (this.global.time.moving / 3600); + this.global.speed.total = this.global.distance.total / (this.global.time.total / 3600); + + 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); + this.global.bounds.southWest.lon = Math.max(this.global.bounds.southWest.lon, other.global.bounds.southWest.lon); + this.global.bounds.northEast.lat = Math.max(this.global.bounds.northEast.lat, other.global.bounds.northEast.lat); + this.global.bounds.northEast.lon = Math.min(this.global.bounds.northEast.lon, other.global.bounds.northEast.lon); } } -export type TrackPointStatistics = { - distance: number[], - time: number[], - speed: number[], - elevation: { - smoothed: number[], - gain: number[], - loss: number[], - }, - slope: number[], -} - const earthRadius = 6371008.8; export function distance(coord1: Coordinates, coord2: Coordinates): number { const rad = Math.PI / 180; diff --git a/website/src/lib/components/App.svelte b/website/src/lib/components/App.svelte index 239b3936..2686b3a5 100644 --- a/website/src/lib/components/App.svelte +++ b/website/src/lib/components/App.svelte @@ -2,7 +2,7 @@ import GPXLayers from '$lib/components/gpx-layer/GPXLayers.svelte'; import ElevationProfile from '$lib/components/ElevationProfile.svelte'; import FileList from '$lib/components/FileList.svelte'; - import GPXData from '$lib/components/GPXData.svelte'; + import GPXStatistics from '$lib/components/GPXStatistics.svelte'; import Map from '$lib/components/Map.svelte'; import Menu from '$lib/components/Menu.svelte'; import Toolbar from '$lib/components/toolbar/Toolbar.svelte'; @@ -21,7 +21,7 @@
- +
diff --git a/website/src/lib/components/ElevationProfile.svelte b/website/src/lib/components/ElevationProfile.svelte index 000c6f2c..08c6b48f 100644 --- a/website/src/lib/components/ElevationProfile.svelte +++ b/website/src/lib/components/ElevationProfile.svelte @@ -6,7 +6,7 @@ import Chart from 'chart.js/auto'; import mapboxgl from 'mapbox-gl'; - import { map, settings, gpxData } from '$lib/stores'; + import { map, settings, gpxStatistics } from '$lib/stores'; import { onDestroy, onMount } from 'svelte'; import { @@ -233,16 +233,16 @@ }); $: if (chart && $settings) { - let data = $gpxData; + let data = $gpxStatistics; // update data chart.data.datasets[0] = { label: $_('quantities.elevation'), - data: data.points.map((point, index) => { + data: data.local.points.map((point, index) => { return { - x: getConvertedDistance(data.point_statistics.distance[index]), + x: getConvertedDistance(data.local.distance[index]), y: point.ele ? getConvertedElevation(point.ele) : 0, - slope: data.point_statistics.slope[index], + slope: data.local.slope[index], surface: point.getSurface(), coordinates: point.getCoordinates() }; @@ -253,10 +253,10 @@ }; chart.data.datasets[1] = { label: datasets.speed.getLabel(), - data: data.points.map((point, index) => { + data: data.local.points.map((point, index) => { return { - x: getConvertedDistance(data.point_statistics.distance[index]), - y: getConvertedVelocity(data.point_statistics.speed[index]) + x: getConvertedDistance(data.local.distance[index]), + y: getConvertedVelocity(data.local.speed[index]) }; }), normalized: true, @@ -265,9 +265,9 @@ }; chart.data.datasets[2] = { label: datasets.hr.getLabel(), - data: data.points.map((point, index) => { + data: data.local.points.map((point, index) => { return { - x: getConvertedDistance(data.point_statistics.distance[index]), + x: getConvertedDistance(data.local.distance[index]), y: point.getHeartRate() }; }), @@ -277,9 +277,9 @@ }; chart.data.datasets[3] = { label: datasets.cad.getLabel(), - data: data.points.map((point, index) => { + data: data.local.points.map((point, index) => { return { - x: getConvertedDistance(data.point_statistics.distance[index]), + x: getConvertedDistance(data.local.distance[index]), y: point.getCadence() }; }), @@ -289,9 +289,9 @@ }; chart.data.datasets[4] = { label: datasets.atemp.getLabel(), - data: data.points.map((point, index) => { + data: data.local.points.map((point, index) => { return { - x: getConvertedDistance(data.point_statistics.distance[index]), + x: getConvertedDistance(data.local.distance[index]), y: getConvertedTemperature(point.getTemperature()) }; }), @@ -301,9 +301,9 @@ }; chart.data.datasets[5] = { label: datasets.power.getLabel(), - data: data.points.map((point, index) => { + data: data.local.points.map((point, index) => { return { - x: getConvertedDistance(data.point_statistics.distance[index]), + x: getConvertedDistance(data.local.distance[index]), y: point.getPower() }; }), @@ -312,7 +312,7 @@ hidden: true }; chart.options.scales.x['min'] = 0; - chart.options.scales.x['max'] = getConvertedDistance(data.statistics.distance.total); + chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total); // update units for (let [id, dataset] of Object.entries(datasets)) { diff --git a/website/src/lib/components/GPXData.svelte b/website/src/lib/components/GPXStatistics.svelte similarity index 72% rename from website/src/lib/components/GPXData.svelte rename to website/src/lib/components/GPXStatistics.svelte index 9c1a94fa..92a3a7c3 100644 --- a/website/src/lib/components/GPXData.svelte +++ b/website/src/lib/components/GPXStatistics.svelte @@ -3,17 +3,11 @@ import Tooltip from '$lib/components/Tooltip.svelte'; import WithUnits from '$lib/components/WithUnits.svelte'; - import { GPXStatistics } from 'gpx'; - - import { gpxData, settings } from '$lib/stores'; + import { gpxStatistics, settings } from '$lib/stores'; import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from 'lucide-svelte'; import { _ } from 'svelte-i18n'; - - let data: GPXStatistics; - - $: data = $gpxData.statistics; @@ -21,25 +15,25 @@ - + {$_('quantities.distance')} - + - + {$_('quantities.elevation')} - + / - + {$settings.velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_( @@ -50,9 +44,9 @@ - + / - + {$_('quantities.time')} ({$_('quantities.total')} / {$_('quantities.moving')})(null); @@ -29,18 +28,15 @@ fileObservers.subscribe((files) => { // Update selectedFiles automatically when } }); -export const gpxData = writable(new GPXFiles([]).getTrackPointsAndStatistics()); +const fileStatistics: Map = new Map(); +export const gpxStatistics: Writable = writable(new GPXStatistics()); function updateGPXData() { - let fileIds: string[] = get(fileOrder).filter((f) => get(selectedFiles).has(f)); - let files: GPXFile[] = fileIds - .map((id) => { - let fileObserver = get(fileObservers).get(id); - return fileObserver ? get(fileObserver) : null; - }) - .filter((f) => f) as GPXFile[]; - let gpxFiles = new GPXFiles(files); - gpxData.set(gpxFiles.getTrackPointsAndStatistics()); + let fileIds: string[] = get(fileOrder).filter((f) => fileStatistics.has(f) && get(selectedFiles).has(f)); + gpxStatistics.set(fileIds.reduce((stats: GPXStatistics, fileId: string) => { + stats.mergeWith(fileStatistics.get(fileId) ?? new GPXStatistics()); + return stats; + }, new GPXStatistics())); } let selectedFilesUnsubscribe: Function[] = []; @@ -49,8 +45,11 @@ selectedFiles.subscribe((selectedFiles) => { selectedFiles.forEach((fileId) => { let fileObserver = get(fileObservers).get(fileId); if (fileObserver) { - let unsubscribe = fileObserver.subscribe(() => { - updateGPXData(); + let unsubscribe = fileObserver.subscribe((file) => { + if (file) { + fileStatistics.set(fileId, file.getStatistics()); + updateGPXData(); + } }); selectedFilesUnsubscribe.push(unsubscribe); } @@ -116,23 +115,23 @@ export async function loadFiles(list: FileList) { if (file) { files.push(file); - let fileBounds = file.getStatistics().bounds; + /*let fileBounds = file.getStatistics().bounds; bounds.extend(fileBounds.southWest); bounds.extend(fileBounds.northEast); bounds.extend([fileBounds.southWest.lon, fileBounds.northEast.lat]); - bounds.extend([fileBounds.northEast.lon, fileBounds.southWest.lat]); + bounds.extend([fileBounds.northEast.lon, fileBounds.southWest.lat]);*/ } } dbUtils.addMultiple(files); - if (!mapBounds.contains(bounds.getSouthWest()) || !mapBounds.contains(bounds.getNorthEast()) || !mapBounds.contains(bounds.getSouthEast()) || !mapBounds.contains(bounds.getNorthWest())) { + /*if (!mapBounds.contains(bounds.getSouthWest()) || !mapBounds.contains(bounds.getNorthEast()) || !mapBounds.contains(bounds.getSouthEast()) || !mapBounds.contains(bounds.getNorthWest())) { get(map)?.fitBounds(bounds, { padding: 80, linear: true, easing: () => 1 }); - } + }*/ selectFileWhenLoaded(files[0]._data.id); } @@ -190,7 +189,7 @@ export function exportAllFiles() { }); } -export function exportFile(file: FreezedObject) { +export function exportFile(file: GPXFile) { let blob = new Blob([buildGPX(file)], { type: 'application/gpx+xml' }); let url = URL.createObjectURL(blob); let a = document.createElement('a');