create slope segments

This commit is contained in:
vcoppe
2024-06-25 19:41:37 +02:00
parent 42fe819a72
commit f823bcade5
6 changed files with 101 additions and 26 deletions

View File

@@ -1,3 +1,4 @@
import { ramerDouglasPeucker } from "./simplify";
import { Coordinates, GPXFileAttributes, GPXFileType, LineStyleExtension, Link, Metadata, TrackExtensions, TrackPointExtensions, TrackPointType, TrackSegmentType, TrackType, WaypointType } from "./types"; import { Coordinates, GPXFileAttributes, GPXFileType, LineStyleExtension, Link, Metadata, TrackExtensions, TrackPointExtensions, TrackPointType, TrackSegmentType, TrackType, WaypointType } from "./types";
import { Draft, immerable, isDraft, original, produce, freeze } from "immer"; 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.points = this.trkpt.map((point) => point);
statistics.local.elevation.smoothed = this._computeSmoothedElevation(); statistics.local.elevation.smoothed = this._computeSmoothedElevation();
statistics.local.slope = this._computeSlope(); statistics.local.slope.at = this._computeSlope();
const points = this.trkpt; const points = this.trkpt;
for (let i = 0; i < points.length; i++) { 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.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.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.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; 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)); 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 { getNumberOfTrackPoints(): number {
return this.trkpt.length; return this.trkpt.length;
} }
@@ -1026,7 +1076,11 @@ export class GPXStatistics {
gain: number[], gain: number[],
loss: number[], loss: number[],
}, },
slope: number[], slope: {
at: number[],
segment: number[],
length: number[],
}
}; };
constructor() { constructor() {
@@ -1076,7 +1130,11 @@ export class GPXStatistics {
gain: [], gain: [],
loss: [], 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.speed = this.local.speed.concat(other.local.speed);
this.local.elevation.smoothed = this.local.elevation.smoothed.concat(other.local.elevation.smoothed); 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.total += other.global.distance.total;
this.global.distance.moving += other.global.distance.moving; this.global.distance.moving += other.global.distance.moving;

View File

@@ -1,5 +1,5 @@
export * from './gpx'; export * from './gpx';
export { Coordinates, LineStyleExtension } from './types'; export { Coordinates, LineStyleExtension } from './types';
export { parseGPX, buildGPX } from './io'; export { parseGPX, buildGPX } from './io';
export * from './simplify';

View File

@@ -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 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) { if (points.length == 0) {
return []; return [];
} else if (points.length == 1) { } else if (points.length == 1) {
@@ -14,36 +15,42 @@ export function ramerDouglasPeucker(points: readonly TrackPoint[], epsilon: numb
} }
let simplified = [{ 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({ simplified.push({
point: points[end] point: points[points.length - 1]
}); });
return simplified; 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 = { let largest = {
index: 0, index: 0,
distance: 0 distance: 0
}; };
for (let i = start + 1; i < end; i++) { for (let i = start + 1; i < end; i++) {
let distance = crossarc(points[start].getCoordinates(), points[end].getCoordinates(), points[i].getCoordinates()); if (canSplit(points[start], points[end], points[i])) {
if (distance > largest.distance) { let distance = measure(points[start], points[end], points[i]);
largest.index = i; if (distance > largest.distance) {
largest.distance = distance; largest.index = i;
largest.distance = distance;
}
} }
} }
if (largest.distance > epsilon && largest.index != 0) { 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 }); 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 { function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number {
// Calculates the shortest distance in meters // Calculates the shortest distance in meters
// between an arc (defined by p1 and p2) and a third point, p3. // between an arc (defined by p1 and p2) and a third point, p3.

View File

@@ -137,12 +137,16 @@
let context = contexts.filter((context) => context.datasetIndex === 0); let context = contexts.filter((context) => context.datasetIndex === 0);
if (context.length === 0) return; if (context.length === 0) return;
let point = context[0].raw; 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'; let surface = point.surface ? point.surface : 'unknown';
return [ return [
` ${$_('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`, ` ${$_('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}`)}` ` ${$_('quantities.surface')}: ${$_(`toolbar.routing.surface.${surface}`)}`
]; ];
} }
@@ -317,7 +321,11 @@
return { return {
x: getConvertedDistance(data.local.distance.total[index]), x: getConvertedDistance(data.local.distance.total[index]),
y: point.ele ? getConvertedElevation(point.ele) : 0, 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(), surface: point.getSurface(),
coordinates: point.getCoordinates(), coordinates: point.getCoordinates(),
index: index index: index
@@ -421,7 +429,7 @@
]; ];
function slopeFillCallback(context) { function slopeFillCallback(context) {
let slope = context.p0.raw.slope; let slope = context.p0.raw.slope.segment;
if (slope <= 1 && slope >= -1) return slopeColors[6]; if (slope <= 1 && slope >= -1) return slopeColors[6];
else if (slope > 0) { else if (slope > 0) {
if (slope <= 3) return slopeColors[7]; if (slope <= 3) return slopeColors[7];

View File

@@ -8,11 +8,10 @@
import { Filter } from 'lucide-svelte'; import { Filter } from 'lucide-svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import WithUnits from '$lib/components/WithUnits.svelte'; import WithUnits from '$lib/components/WithUnits.svelte';
import { ramerDouglasPeucker, type SimplifiedTrackPoint } from '$lib/simplify';
import { dbUtils, fileObservers } from '$lib/db'; import { dbUtils, fileObservers } from '$lib/db';
import { map } from '$lib/stores'; import { map } from '$lib/stores';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { TrackPoint } from 'gpx'; import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
import { derived } from 'svelte/store'; import { derived } from 'svelte/store';
let sliderValue = [50]; let sliderValue = [50];

View File

@@ -1,5 +1,6 @@
import { earthRadius, ramerDouglasPeucker } from "$lib/simplify"; import { ramerDouglasPeucker, type GPXFile, type TrackSegment } from "gpx";
import type { GPXFile, TrackSegment } from "gpx";
const earthRadius = 6371008.8;
export function getZoomLevelForDistance(latitude: number, distance?: number): number { export function getZoomLevelForDistance(latitude: number, distance?: number): number {
if (distance === undefined) { if (distance === undefined) {