diff --git a/gpx/src/gpx.ts b/gpx/src/gpx.ts index cd763c69..b31c1fea 100644 --- a/gpx/src/gpx.ts +++ b/gpx/src/gpx.ts @@ -1,3 +1,4 @@ +import { ramerDouglasPeucker } from "./simplify"; import { Coordinates, GPXFileAttributes, GPXFileType, LineStyleExtension, Link, Metadata, TrackExtensions, TrackPointExtensions, TrackPointType, TrackSegmentType, TrackType, WaypointType } from "./types"; import { Draft, immerable, isDraft, original, produce, freeze } from "immer"; @@ -601,7 +602,7 @@ export class TrackSegment extends GPXTreeLeaf { statistics.local.points = this.trkpt.map((point) => point); statistics.local.elevation.smoothed = this._computeSmoothedElevation(); - statistics.local.slope = this._computeSlope(); + statistics.local.slope.at = this._computeSlope(); const points = this.trkpt; for (let i = 0; i < points.length; i++) { @@ -663,6 +664,8 @@ export class TrackSegment extends GPXTreeLeaf { statistics.global.bounds.northEast.lon = Math.max(statistics.global.bounds.northEast.lon, points[i].attributes.lon); } + [statistics.local.slope.segment, statistics.local.slope.length] = this._computeSlopeSegments(statistics); + statistics.global.time.total = statistics.global.time.start && statistics.global.time.end ? (statistics.global.time.end.getTime() - statistics.global.time.start.getTime()) / 1000 : 0; 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; @@ -691,6 +694,53 @@ export class TrackSegment extends GPXTreeLeaf { return distanceWindowSmoothingWithDistanceAccumulator(points, 50, (accumulated, start, end) => 100 * ((points[end].ele ?? 0) - (points[start].ele ?? 0)) / (accumulated > 0 ? accumulated : 1)); } + _computeSlopeSegments(statistics: GPXStatistics): [number[], number[]] { + function canSplit(point1: TrackPoint, point2: TrackPoint, point3: TrackPoint): boolean { + return statistics.local.distance.total[point3._data.index] - statistics.local.distance.total[point1._data.index] >= 0.5 && statistics.local.distance.total[point2._data.index] - statistics.local.distance.total[point3._data.index] >= 0.5; + } + + // x-coordinates are given by: statistics.local.distance.total[point._data.index] * 1000 + // y-coordinates are given by: point.ele + // Compute the distance between point3 and the line defined by point1 and point2 + function elevationDistance(point1: TrackPoint, point2: TrackPoint, point3: TrackPoint): number { + if (point1.ele === undefined || point2.ele === undefined || point3.ele === undefined) { + return 0; + } + let x1 = statistics.local.distance.total[point1._data.index] * 1000; + let x2 = statistics.local.distance.total[point2._data.index] * 1000; + let x3 = statistics.local.distance.total[point3._data.index] * 1000; + let y1 = point1.ele; + let y2 = point2.ele; + let y3 = point3.ele; + + let dist = Math.sqrt(Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2)); + if (dist === 0) { + return Math.sqrt(Math.pow(x3 - x1, 2) + Math.pow(y3 - y1, 2)); + } + + return Math.abs((y2 - y1) * x3 - (x2 - x1) * y3 + x2 * y1 - y2 * x1) / dist; + } + + let simplified = ramerDouglasPeucker(this.trkpt, 25, elevationDistance, canSplit); + + let slope = []; + let length = []; + + for (let i = 0; i < simplified.length - 1; i++) { + let start = simplified[i].point._data.index; + let end = simplified[i + 1].point._data.index; + let dist = statistics.local.distance.total[end] - statistics.local.distance.total[start]; + let ele = simplified[i + 1].point.ele - simplified[i].point.ele; + + for (let j = start; j < end + (i + 1 === simplified.length - 1 ? 1 : 0); j++) { + slope.push(0.1 * ele / dist); + length.push(dist); + } + } + + return [slope, length]; + } + getNumberOfTrackPoints(): number { return this.trkpt.length; } @@ -1026,7 +1076,11 @@ export class GPXStatistics { gain: number[], loss: number[], }, - slope: number[], + slope: { + at: number[], + segment: number[], + length: number[], + } }; constructor() { @@ -1076,7 +1130,11 @@ export class GPXStatistics { gain: [], loss: [], }, - slope: [], + slope: { + at: [], + segment: [], + length: [], + } }; } @@ -1092,7 +1150,9 @@ export class GPXStatistics { 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.local.slope.at = this.local.slope.at.concat(other.local.slope.at); + this.local.slope.segment = this.local.slope.segment.concat(other.local.slope.segment); + this.local.slope.length = this.local.slope.length.concat(other.local.slope.length); this.global.distance.total += other.global.distance.total; this.global.distance.moving += other.global.distance.moving; diff --git a/gpx/src/index.ts b/gpx/src/index.ts index 0a263eac..f2fe5f3f 100644 --- a/gpx/src/index.ts +++ b/gpx/src/index.ts @@ -1,5 +1,5 @@ export * from './gpx'; export { Coordinates, LineStyleExtension } from './types'; - export { parseGPX, buildGPX } from './io'; +export * from './simplify'; diff --git a/website/src/lib/simplify.ts b/gpx/src/simplify.ts similarity index 65% rename from website/src/lib/simplify.ts rename to gpx/src/simplify.ts index 4b0669c7..a7d5b90d 100644 --- a/website/src/lib/simplify.ts +++ b/gpx/src/simplify.ts @@ -1,10 +1,11 @@ -import type { Coordinates, TrackPoint } from "gpx"; +import { TrackPoint } from "./gpx"; +import { Coordinates } from "./types"; export type SimplifiedTrackPoint = { point: TrackPoint, distance?: number }; -export const earthRadius = 6371008.8; +const earthRadius = 6371008.8; -export function ramerDouglasPeucker(points: readonly TrackPoint[], epsilon: number = 50, start: number = 0, end: number = points.length - 1): SimplifiedTrackPoint[] { +export function ramerDouglasPeucker(points: readonly TrackPoint[], epsilon: number = 50, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number = computeCrossarc, canSplit: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => boolean = () => true): SimplifiedTrackPoint[] { if (points.length == 0) { return []; } else if (points.length == 1) { @@ -14,36 +15,42 @@ export function ramerDouglasPeucker(points: readonly TrackPoint[], epsilon: numb } let simplified = [{ - point: points[start] + point: points[0] }]; - ramerDouglasPeuckerRecursive(points, epsilon, start, end, simplified); + ramerDouglasPeuckerRecursive(points, epsilon, measure, canSplit, 0, points.length - 1, simplified); simplified.push({ - point: points[end] + point: points[points.length - 1] }); return simplified; } -function ramerDouglasPeuckerRecursive(points: readonly TrackPoint[], epsilon: number, start: number, end: number, simplified: SimplifiedTrackPoint[]) { +function ramerDouglasPeuckerRecursive(points: readonly TrackPoint[], epsilon: number, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number, canSplit: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => boolean, start: number, end: number, simplified: SimplifiedTrackPoint[]) { let largest = { index: 0, distance: 0 }; for (let i = start + 1; i < end; i++) { - let distance = crossarc(points[start].getCoordinates(), points[end].getCoordinates(), points[i].getCoordinates()); - if (distance > largest.distance) { - largest.index = i; - largest.distance = distance; + if (canSplit(points[start], points[end], points[i])) { + let distance = measure(points[start], points[end], points[i]); + if (distance > largest.distance) { + largest.index = i; + largest.distance = distance; + } } } if (largest.distance > epsilon && largest.index != 0) { - ramerDouglasPeuckerRecursive(points, epsilon, start, largest.index, simplified); + ramerDouglasPeuckerRecursive(points, epsilon, measure, canSplit, start, largest.index, simplified); simplified.push({ point: points[largest.index], distance: largest.distance }); - ramerDouglasPeuckerRecursive(points, epsilon, largest.index, end, simplified); + ramerDouglasPeuckerRecursive(points, epsilon, measure, canSplit, largest.index, end, simplified); } } +function computeCrossarc(point1: TrackPoint, point2: TrackPoint, point3: TrackPoint): number { + return crossarc(point1.getCoordinates(), point2.getCoordinates(), point3.getCoordinates()); +} + function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number { // Calculates the shortest distance in meters // between an arc (defined by p1 and p2) and a third point, p3. diff --git a/website/src/lib/components/ElevationProfile.svelte b/website/src/lib/components/ElevationProfile.svelte index 18200904..6d6d381b 100644 --- a/website/src/lib/components/ElevationProfile.svelte +++ b/website/src/lib/components/ElevationProfile.svelte @@ -137,12 +137,16 @@ let context = contexts.filter((context) => context.datasetIndex === 0); if (context.length === 0) return; let point = context[0].raw; - let slope = point.slope.toFixed(1); + let slope = { + at: point.slope.at.toFixed(1), + segment: point.slope.segment.toFixed(1), + length: getDistanceWithUnits(point.slope.length) + }; let surface = point.surface ? point.surface : 'unknown'; return [ ` ${$_('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`, - ` ${$_('quantities.slope')}: ${slope} %`, + ` ${$_('quantities.slope')}: ${slope.at} %${elevationFill === 'slope' ? ` (${slope.length} @${slope.segment} %)` : ''}`, ` ${$_('quantities.surface')}: ${$_(`toolbar.routing.surface.${surface}`)}` ]; } @@ -317,7 +321,11 @@ return { x: getConvertedDistance(data.local.distance.total[index]), y: point.ele ? getConvertedElevation(point.ele) : 0, - slope: data.local.slope[index], + slope: { + at: data.local.slope.at[index], + segment: data.local.slope.segment[index], + length: data.local.slope.length[index] + }, surface: point.getSurface(), coordinates: point.getCoordinates(), index: index @@ -421,7 +429,7 @@ ]; function slopeFillCallback(context) { - let slope = context.p0.raw.slope; + let slope = context.p0.raw.slope.segment; if (slope <= 1 && slope >= -1) return slopeColors[6]; else if (slope > 0) { if (slope <= 3) return slopeColors[7]; diff --git a/website/src/lib/components/toolbar/tools/Reduce.svelte b/website/src/lib/components/toolbar/tools/Reduce.svelte index 9d6d61f3..cbc75b03 100644 --- a/website/src/lib/components/toolbar/tools/Reduce.svelte +++ b/website/src/lib/components/toolbar/tools/Reduce.svelte @@ -8,11 +8,10 @@ import { Filter } from 'lucide-svelte'; import { _ } from 'svelte-i18n'; import WithUnits from '$lib/components/WithUnits.svelte'; - import { ramerDouglasPeucker, type SimplifiedTrackPoint } from '$lib/simplify'; import { dbUtils, fileObservers } from '$lib/db'; import { map } from '$lib/stores'; import { onDestroy } from 'svelte'; - import { TrackPoint } from 'gpx'; + import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx'; import { derived } from 'svelte/store'; let sliderValue = [50]; diff --git a/website/src/lib/components/toolbar/tools/routing/Simplify.ts b/website/src/lib/components/toolbar/tools/routing/Simplify.ts index 1cf42a82..19b9ed16 100644 --- a/website/src/lib/components/toolbar/tools/routing/Simplify.ts +++ b/website/src/lib/components/toolbar/tools/routing/Simplify.ts @@ -1,5 +1,6 @@ -import { earthRadius, ramerDouglasPeucker } from "$lib/simplify"; -import type { GPXFile, TrackSegment } from "gpx"; +import { ramerDouglasPeucker, type GPXFile, type TrackSegment } from "gpx"; + +const earthRadius = 6371008.8; export function getZoomLevelForDistance(latitude: number, distance?: number): number { if (distance === undefined) {