mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-12-02 10:02:12 +00:00
Compare commits
10 Commits
3a65f8dc16
...
09b8aa65fc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09b8aa65fc | ||
|
|
6c15193f32 | ||
|
|
4442e29b66 | ||
|
|
b6f96d9f4d | ||
|
|
36b66100f9 | ||
|
|
49d8143cc6 | ||
|
|
fc279fecaf | ||
|
|
bd307daa57 | ||
|
|
7a72f44722 | ||
|
|
8e63fc6946 |
217
gpx/src/gpx.ts
217
gpx/src/gpx.ts
@@ -818,9 +818,6 @@ 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.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++) {
|
||||||
points[i]._data['index'] = i;
|
points[i]._data['index'] = i;
|
||||||
@@ -835,21 +832,6 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
|
|
||||||
statistics.local.distance.total.push(statistics.global.distance.total);
|
statistics.local.distance.total.push(statistics.global.distance.total);
|
||||||
|
|
||||||
// elevation
|
|
||||||
if (i > 0) {
|
|
||||||
const ele =
|
|
||||||
statistics.local.elevation.smoothed[i] -
|
|
||||||
statistics.local.elevation.smoothed[i - 1];
|
|
||||||
if (ele > 0) {
|
|
||||||
statistics.global.elevation.gain += ele;
|
|
||||||
} else if (ele < 0) {
|
|
||||||
statistics.global.elevation.loss -= ele;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
|
|
||||||
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
|
|
||||||
|
|
||||||
// time
|
// time
|
||||||
if (points[i].time === undefined) {
|
if (points[i].time === undefined) {
|
||||||
statistics.local.time.total.push(0);
|
statistics.local.time.total.push(0);
|
||||||
@@ -960,8 +942,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[statistics.local.slope.segment, statistics.local.slope.length] =
|
this._elevationComputation(statistics);
|
||||||
this._computeSlopeSegments(statistics);
|
|
||||||
|
|
||||||
statistics.global.time.total =
|
statistics.global.time.total =
|
||||||
statistics.global.time.start && statistics.global.time.end
|
statistics.global.time.start && statistics.global.time.end
|
||||||
@@ -977,73 +958,82 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
? statistics.global.distance.moving / (statistics.global.time.moving / 3600)
|
? statistics.global.distance.moving / (statistics.global.time.moving / 3600)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
statistics.local.speed = distanceWindowSmoothingWithDistanceAccumulator(
|
statistics.local.speed = timeWindowSmoothing(points, 10000, (start, end) =>
|
||||||
points,
|
|
||||||
200,
|
|
||||||
(accumulated, start, end) =>
|
|
||||||
points[start].time && points[end].time
|
points[start].time && points[end].time
|
||||||
? (3600 * accumulated) /
|
? (3600 *
|
||||||
(points[end].time.getTime() - points[start].time.getTime())
|
(statistics.local.distance.total[end] -
|
||||||
|
statistics.local.distance.total[start])) /
|
||||||
|
Math.max((points[end].time.getTime() - points[start].time.getTime()) / 1000, 1)
|
||||||
: undefined
|
: undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
return statistics;
|
return statistics;
|
||||||
}
|
}
|
||||||
|
|
||||||
_computeSmoothedElevation(): number[] {
|
_elevationComputation(statistics: GPXStatistics) {
|
||||||
const points = this.trkpt;
|
|
||||||
|
|
||||||
let smoothed = distanceWindowSmoothing(
|
|
||||||
points,
|
|
||||||
100,
|
|
||||||
(index) => points[index].ele ?? 0,
|
|
||||||
(accumulated, start, end) => accumulated / (end - start + 1)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (points.length > 0) {
|
|
||||||
smoothed[0] = points[0].ele ?? 0;
|
|
||||||
smoothed[points.length - 1] = points[points.length - 1].ele ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return smoothed;
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeSlope(): number[] {
|
|
||||||
const points = this.trkpt;
|
|
||||||
|
|
||||||
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[]] {
|
|
||||||
let simplified = ramerDouglasPeucker(
|
let simplified = ramerDouglasPeucker(
|
||||||
this.trkpt,
|
this.trkpt,
|
||||||
20,
|
5,
|
||||||
getElevationDistanceFunction(statistics)
|
getElevationDistanceFunction(statistics)
|
||||||
);
|
);
|
||||||
|
|
||||||
let slope = [];
|
|
||||||
let length = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < simplified.length - 1; i++) {
|
for (let i = 0; i < simplified.length - 1; i++) {
|
||||||
let start = simplified[i].point._data.index;
|
let start = simplified[i].point._data.index;
|
||||||
let end = simplified[i + 1].point._data.index;
|
let end = simplified[i + 1].point._data.index;
|
||||||
|
|
||||||
|
const ele = this.trkpt[end].ele - this.trkpt[start].ele || 0;
|
||||||
|
const dist =
|
||||||
|
statistics.local.distance.total[end] - statistics.local.distance.total[start];
|
||||||
|
|
||||||
|
for (let j = start; j < end + (i + 1 == simplified.length - 1 ? 1 : 0); j++) {
|
||||||
|
const localDist =
|
||||||
|
statistics.local.distance.total[j] - statistics.local.distance.total[start];
|
||||||
|
const localEle = dist > 0 ? (localDist / dist) * ele : 0;
|
||||||
|
statistics.local.elevation.gain.push(
|
||||||
|
statistics.global.elevation.gain + (localEle > 0 ? localEle : 0)
|
||||||
|
);
|
||||||
|
statistics.local.elevation.loss.push(
|
||||||
|
statistics.global.elevation.loss + (localEle < 0 ? -localEle : 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ele > 0) {
|
||||||
|
statistics.global.elevation.gain += ele;
|
||||||
|
} else if (ele < 0) {
|
||||||
|
statistics.global.elevation.loss -= ele;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let slope = [];
|
||||||
|
let length = [];
|
||||||
|
for (let a = 0; a < simplified.length - 1; ) {
|
||||||
|
let b = a + 1;
|
||||||
|
while (b < simplified.length - 1 && simplified[b].distance < 20) {
|
||||||
|
b++;
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = simplified[a].point._data.index;
|
||||||
|
let end = simplified[b].point._data.index;
|
||||||
let dist =
|
let dist =
|
||||||
statistics.local.distance.total[end] - statistics.local.distance.total[start];
|
statistics.local.distance.total[end] - statistics.local.distance.total[start];
|
||||||
let ele = (simplified[i + 1].point.ele ?? 0) - (simplified[i].point.ele ?? 0);
|
let ele = (simplified[b].point.ele ?? 0) - (simplified[a].point.ele ?? 0);
|
||||||
|
|
||||||
for (let j = start; j < end + (i + 1 === simplified.length - 1 ? 1 : 0); j++) {
|
for (let j = start; j < end + (b === simplified.length - 1 ? 1 : 0); j++) {
|
||||||
slope.push((0.1 * ele) / dist);
|
slope.push((0.1 * ele) / dist);
|
||||||
length.push(dist);
|
length.push(dist);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a = b;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [slope, length];
|
statistics.local.slope.segment = slope;
|
||||||
|
statistics.local.slope.length = length;
|
||||||
|
statistics.local.slope.at = distanceWindowSmoothing(statistics, 0.05, (start, end) => {
|
||||||
|
const ele = this.trkpt[end].ele - this.trkpt[start].ele || 0;
|
||||||
|
const dist =
|
||||||
|
statistics.local.distance.total[end] - statistics.local.distance.total[start];
|
||||||
|
return dist > 0 ? (0.1 * ele) / dist : 0;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getNumberOfTrackPoints(): number {
|
getNumberOfTrackPoints(): number {
|
||||||
@@ -1290,8 +1280,14 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
lastPoint: TrackPoint | undefined
|
lastPoint: TrackPoint | undefined
|
||||||
) {
|
) {
|
||||||
let og = getOriginal(this); // Read as much as possible from the original object because it is faster
|
let og = getOriginal(this); // Read as much as possible from the original object because it is faster
|
||||||
let slope = og._computeSlope();
|
let statistics = og._computeStatistics();
|
||||||
let trkpt = withArtificialTimestamps(og.trkpt, totalTime, lastPoint, startTime, slope);
|
let trkpt = withArtificialTimestamps(
|
||||||
|
og.trkpt,
|
||||||
|
totalTime,
|
||||||
|
lastPoint,
|
||||||
|
startTime,
|
||||||
|
statistics.local.slope.at
|
||||||
|
);
|
||||||
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1647,7 +1643,6 @@ export class GPXStatistics {
|
|||||||
};
|
};
|
||||||
speed: number[];
|
speed: number[];
|
||||||
elevation: {
|
elevation: {
|
||||||
smoothed: number[];
|
|
||||||
gain: number[];
|
gain: number[];
|
||||||
loss: number[];
|
loss: number[];
|
||||||
};
|
};
|
||||||
@@ -1718,7 +1713,6 @@ export class GPXStatistics {
|
|||||||
},
|
},
|
||||||
speed: [],
|
speed: [],
|
||||||
elevation: {
|
elevation: {
|
||||||
smoothed: [],
|
|
||||||
gain: [],
|
gain: [],
|
||||||
loss: [],
|
loss: [],
|
||||||
},
|
},
|
||||||
@@ -1753,9 +1747,6 @@ 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.slope.at = this.local.slope.at.concat(other.local.slope.at);
|
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.segment = this.local.slope.segment.concat(other.local.slope.segment);
|
||||||
this.local.slope.length = this.local.slope.length.concat(other.local.slope.length);
|
this.local.slope.length = this.local.slope.length.concat(other.local.slope.length);
|
||||||
@@ -1911,11 +1902,15 @@ export function distance(
|
|||||||
const rad = Math.PI / 180;
|
const rad = Math.PI / 180;
|
||||||
const lat1 = coord1.lat * rad;
|
const lat1 = coord1.lat * rad;
|
||||||
const lat2 = coord2.lat * rad;
|
const lat2 = coord2.lat * rad;
|
||||||
|
const dLat = lat2 - lat1;
|
||||||
|
const dLon = (coord2.lon - coord1.lon) * rad;
|
||||||
|
|
||||||
|
// Haversine formula - better numerical stability for small distances
|
||||||
const a =
|
const a =
|
||||||
Math.sin(lat1) * Math.sin(lat2) +
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||||
Math.cos(lat1) * Math.cos(lat2) * Math.cos((coord2.lon - coord1.lon) * rad);
|
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||||
const maxMeters = earthRadius * Math.acos(Math.min(a, 1));
|
const c = 2 * Math.asin(Math.sqrt(Math.min(a, 1)));
|
||||||
return maxMeters;
|
return earthRadius * c;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
||||||
@@ -1942,57 +1937,55 @@ export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function distanceWindowSmoothing(
|
function windowSmoothing(
|
||||||
points: TrackPoint[],
|
length: number,
|
||||||
distanceWindow: number,
|
distance: (index1: number, index2: number) => number,
|
||||||
accumulate: (index: number) => number,
|
window: number,
|
||||||
compute: (accumulated: number, start: number, end: number) => number,
|
compute: (start: number, end: number) => number
|
||||||
remove?: (index: number) => number
|
|
||||||
): number[] {
|
): number[] {
|
||||||
let result = [];
|
let result = [];
|
||||||
|
|
||||||
let start = 0,
|
let start = 0,
|
||||||
end = 0,
|
end = 0;
|
||||||
accumulated = 0;
|
for (var i = 0; i < length; i++) {
|
||||||
for (var i = 0; i < points.length; i++) {
|
while (start + 1 < i && distance(start, i) > window) {
|
||||||
while (
|
|
||||||
start + 1 < i &&
|
|
||||||
distance(points[start].getCoordinates(), points[i].getCoordinates()) > distanceWindow
|
|
||||||
) {
|
|
||||||
if (remove) {
|
|
||||||
accumulated -= remove(start);
|
|
||||||
} else {
|
|
||||||
accumulated -= accumulate(start);
|
|
||||||
}
|
|
||||||
start++;
|
start++;
|
||||||
}
|
}
|
||||||
while (
|
end = Math.min(i + 2, length);
|
||||||
end < points.length &&
|
while (end < length && distance(i, end) <= window) {
|
||||||
distance(points[i].getCoordinates(), points[end].getCoordinates()) <= distanceWindow
|
|
||||||
) {
|
|
||||||
accumulated += accumulate(end);
|
|
||||||
end++;
|
end++;
|
||||||
}
|
}
|
||||||
result[i] = compute(accumulated, start, end - 1);
|
result[i] = compute(start, end - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function distanceWindowSmoothingWithDistanceAccumulator(
|
function distanceWindowSmoothing(
|
||||||
points: TrackPoint[],
|
statistics: GPXStatistics,
|
||||||
distanceWindow: number,
|
window: number,
|
||||||
compute: (accumulated: number, start: number, end: number) => number
|
compute: (start: number, end: number) => number
|
||||||
): number[] {
|
): number[] {
|
||||||
return distanceWindowSmoothing(
|
return windowSmoothing(
|
||||||
points,
|
statistics.local.points.length,
|
||||||
distanceWindow,
|
(index1, index2) =>
|
||||||
(index) =>
|
statistics.local.distance.total[index2] - statistics.local.distance.total[index1],
|
||||||
index > 0
|
window,
|
||||||
? distance(points[index - 1].getCoordinates(), points[index].getCoordinates())
|
compute
|
||||||
: 0,
|
);
|
||||||
compute,
|
}
|
||||||
(index) => distance(points[index].getCoordinates(), points[index + 1].getCoordinates())
|
|
||||||
|
function timeWindowSmoothing(
|
||||||
|
points: TrackPoint[],
|
||||||
|
window: number,
|
||||||
|
compute: (start: number, end: number) => number
|
||||||
|
): number[] {
|
||||||
|
return windowSmoothing(
|
||||||
|
points.length,
|
||||||
|
(index1, index2) =>
|
||||||
|
points[index2].time?.getTime() - points[index1].time?.getTime() || 2 * window,
|
||||||
|
window,
|
||||||
|
compute
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { Coordinates } from './types';
|
|||||||
|
|
||||||
export type SimplifiedTrackPoint = { point: TrackPoint; distance?: number };
|
export type SimplifiedTrackPoint = { point: TrackPoint; distance?: number };
|
||||||
|
|
||||||
const earthRadius = 6371008.8;
|
|
||||||
|
|
||||||
export function ramerDouglasPeucker(
|
export function ramerDouglasPeucker(
|
||||||
points: TrackPoint[],
|
points: TrackPoint[],
|
||||||
epsilon: number = 50,
|
epsilon: number = 50,
|
||||||
@@ -72,65 +70,45 @@ export function crossarcDistance(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metersPerLatitudeDegree = 111320;
|
||||||
|
|
||||||
|
function getMetersPerLongitudeDegree(latitude: number): number {
|
||||||
|
return Math.cos((latitude * Math.PI) / 180) * metersPerLatitudeDegree;
|
||||||
|
}
|
||||||
|
|
||||||
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 perpendicular distance in meters
|
||||||
// between an arc (defined by p1 and p2) and a third point, p3.
|
// between a line segment (defined by p1 and p2) and a third point, p3.
|
||||||
// Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
|
// Uses simple planar geometry (ignores earth curvature).
|
||||||
|
|
||||||
const rad = Math.PI / 180;
|
// Convert to meters using approximate scaling
|
||||||
const lat1 = coord1.lat * rad;
|
const metersPerLongitudeDegree = getMetersPerLongitudeDegree(coord1.lat);
|
||||||
const lat2 = coord2.lat * rad;
|
|
||||||
const lat3 = coord3.lat * rad;
|
|
||||||
|
|
||||||
const lon1 = coord1.lon * rad;
|
const x1 = coord1.lon * metersPerLongitudeDegree;
|
||||||
const lon2 = coord2.lon * rad;
|
const y1 = coord1.lat * metersPerLatitudeDegree;
|
||||||
const lon3 = coord3.lon * rad;
|
const x2 = coord2.lon * metersPerLongitudeDegree;
|
||||||
|
const y2 = coord2.lat * metersPerLatitudeDegree;
|
||||||
|
const x3 = coord3.lon * metersPerLongitudeDegree;
|
||||||
|
const y3 = coord3.lat * metersPerLatitudeDegree;
|
||||||
|
|
||||||
// Prerequisites for the formulas
|
const dx = x2 - x1;
|
||||||
const bear12 = bearing(lat1, lon1, lat2, lon2);
|
const dy = y2 - y1;
|
||||||
const bear13 = bearing(lat1, lon1, lat3, lon3);
|
const segmentLengthSquared = dx * dx + dy * dy;
|
||||||
let dis13 = distance(lat1, lon1, lat3, lon3);
|
|
||||||
|
|
||||||
let diff = Math.abs(bear13 - bear12);
|
if (segmentLengthSquared === 0) {
|
||||||
if (diff > Math.PI) {
|
// p1 and p2 are the same point
|
||||||
diff = 2 * Math.PI - diff;
|
return Math.sqrt((x3 - x1) * (x3 - x1) + (y3 - y1) * (y3 - y1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is relative bearing obtuse?
|
// Project p3 onto the line defined by p1-p2
|
||||||
if (diff > Math.PI / 2) {
|
const t = Math.max(0, Math.min(1, ((x3 - x1) * dx + (y3 - y1) * dy) / segmentLengthSquared));
|
||||||
return dis13;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the cross-track distance.
|
// Find the closest point on the segment
|
||||||
let dxt = Math.asin(Math.sin(dis13 / earthRadius) * Math.sin(bear13 - bear12)) * earthRadius;
|
const projX = x1 + t * dx;
|
||||||
|
const projY = y1 + t * dy;
|
||||||
|
|
||||||
// Is p4 beyond the arc?
|
// Return distance from p3 to the projected point
|
||||||
let dis12 = distance(lat1, lon1, lat2, lon2);
|
return Math.sqrt((x3 - projX) * (x3 - projX) + (y3 - projY) * (y3 - projY));
|
||||||
let dis14 =
|
|
||||||
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
|
|
||||||
if (dis14 > dis12) {
|
|
||||||
return distance(lat2, lon2, lat3, lon3);
|
|
||||||
} else {
|
|
||||||
return Math.abs(dxt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function distance(latA: number, lonA: number, latB: number, lonB: number): number {
|
|
||||||
// Finds the distance between two lat / lon points.
|
|
||||||
return (
|
|
||||||
Math.acos(
|
|
||||||
Math.sin(latA) * Math.sin(latB) +
|
|
||||||
Math.cos(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
|
|
||||||
) * earthRadius
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function bearing(latA: number, lonA: number, latB: number, lonB: number): number {
|
|
||||||
// Finds the bearing from one lat / lon point to another.
|
|
||||||
return Math.atan2(
|
|
||||||
Math.sin(lonB - lonA) * Math.cos(latB),
|
|
||||||
Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function projectedPoint(
|
export function projectedPoint(
|
||||||
@@ -146,56 +124,39 @@ export function projectedPoint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates {
|
function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates {
|
||||||
// Calculates the point on the line defined by p1 and p2
|
// Calculates the point on the line segment defined by p1 and p2
|
||||||
// that is closest to the third point, p3.
|
// that is closest to the third point, p3.
|
||||||
// Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
|
// Uses simple planar geometry (ignores earth curvature).
|
||||||
|
|
||||||
const rad = Math.PI / 180;
|
// Convert to meters using approximate scaling
|
||||||
const lat1 = coord1.lat * rad;
|
const metersPerLongitudeDegree = getMetersPerLongitudeDegree(coord1.lat);
|
||||||
const lat2 = coord2.lat * rad;
|
|
||||||
const lat3 = coord3.lat * rad;
|
|
||||||
|
|
||||||
const lon1 = coord1.lon * rad;
|
const x1 = coord1.lon * metersPerLongitudeDegree;
|
||||||
const lon2 = coord2.lon * rad;
|
const y1 = coord1.lat * metersPerLatitudeDegree;
|
||||||
const lon3 = coord3.lon * rad;
|
const x2 = coord2.lon * metersPerLongitudeDegree;
|
||||||
|
const y2 = coord2.lat * metersPerLatitudeDegree;
|
||||||
|
const x3 = coord3.lon * metersPerLongitudeDegree;
|
||||||
|
const y3 = coord3.lat * metersPerLatitudeDegree;
|
||||||
|
|
||||||
// Prerequisites for the formulas
|
const dx = x2 - x1;
|
||||||
const bear12 = bearing(lat1, lon1, lat2, lon2);
|
const dy = y2 - y1;
|
||||||
const bear13 = bearing(lat1, lon1, lat3, lon3);
|
const segmentLengthSquared = dx * dx + dy * dy;
|
||||||
let dis13 = distance(lat1, lon1, lat3, lon3);
|
|
||||||
|
|
||||||
let diff = Math.abs(bear13 - bear12);
|
if (segmentLengthSquared === 0) {
|
||||||
if (diff > Math.PI) {
|
// p1 and p2 are the same point
|
||||||
diff = 2 * Math.PI - diff;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is relative bearing obtuse?
|
|
||||||
if (diff > Math.PI / 2) {
|
|
||||||
return coord1;
|
return coord1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the cross-track distance.
|
// Project p3 onto the line defined by p1-p2
|
||||||
let dxt = Math.asin(Math.sin(dis13 / earthRadius) * Math.sin(bear13 - bear12)) * earthRadius;
|
const t = Math.max(0, Math.min(1, ((x3 - x1) * dx + (y3 - y1) * dy) / segmentLengthSquared));
|
||||||
|
|
||||||
// Is p4 beyond the arc?
|
// Find the closest point on the segment
|
||||||
let dis12 = distance(lat1, lon1, lat2, lon2);
|
const projX = x1 + t * dx;
|
||||||
let dis14 =
|
const projY = y1 + t * dy;
|
||||||
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
|
|
||||||
if (dis14 > dis12) {
|
|
||||||
return coord2;
|
|
||||||
} else {
|
|
||||||
// Determine the closest point (p4) on the great circle
|
|
||||||
const f = dis14 / earthRadius;
|
|
||||||
const lat4 = Math.asin(
|
|
||||||
Math.sin(lat1) * Math.cos(f) + Math.cos(lat1) * Math.sin(f) * Math.cos(bear12)
|
|
||||||
);
|
|
||||||
const lon4 =
|
|
||||||
lon1 +
|
|
||||||
Math.atan2(
|
|
||||||
Math.sin(bear12) * Math.sin(f) * Math.cos(lat1),
|
|
||||||
Math.cos(f) - Math.sin(lat1) * Math.sin(lat4)
|
|
||||||
);
|
|
||||||
|
|
||||||
return { lat: lat4 / rad, lon: lon4 / rad };
|
// Convert back to degrees
|
||||||
}
|
return {
|
||||||
|
lat: projY / metersPerLatitudeDegree,
|
||||||
|
lon: projX / metersPerLongitudeDegree,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
let endTime: string | undefined = $state(undefined);
|
let endTime: string | undefined = $state(undefined);
|
||||||
let movingTime: number | undefined = $state(undefined);
|
let movingTime: number | undefined = $state(undefined);
|
||||||
let speed: number | undefined = $state(undefined);
|
let speed: number | undefined = $state(undefined);
|
||||||
let artificial = $state(false);
|
let artificial = $state(true);
|
||||||
|
|
||||||
function toCalendarDate(date: Date): CalendarDate {
|
function toCalendarDate(date: Date): CalendarDate {
|
||||||
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
||||||
@@ -346,7 +346,7 @@
|
|||||||
let fileId = item.getFileId();
|
let fileId = item.getFileId();
|
||||||
fileActionManager.applyToFile(fileId, (file) => {
|
fileActionManager.applyToFile(fileId, (file) => {
|
||||||
if (item instanceof ListFileItem) {
|
if (item instanceof ListFileItem) {
|
||||||
if (artificial || !$gpxStatistics.global.time.moving) {
|
if (artificial && !$gpxStatistics.global.time.moving) {
|
||||||
file.createArtificialTimestamps(
|
file.createArtificialTimestamps(
|
||||||
getDate(startDate!, startTime!),
|
getDate(startDate!, startTime!),
|
||||||
movingTime!
|
movingTime!
|
||||||
@@ -359,7 +359,7 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (item instanceof ListTrackItem) {
|
} else if (item instanceof ListTrackItem) {
|
||||||
if (artificial || !$gpxStatistics.global.time.moving) {
|
if (artificial && !$gpxStatistics.global.time.moving) {
|
||||||
file.createArtificialTimestamps(
|
file.createArtificialTimestamps(
|
||||||
getDate(startDate!, startTime!),
|
getDate(startDate!, startTime!),
|
||||||
movingTime!,
|
movingTime!,
|
||||||
@@ -374,7 +374,7 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (item instanceof ListTrackSegmentItem) {
|
} else if (item instanceof ListTrackSegmentItem) {
|
||||||
if (artificial || !$gpxStatistics.global.time.moving) {
|
if (artificial && !$gpxStatistics.global.time.moving) {
|
||||||
file.createArtificialTimestamps(
|
file.createArtificialTimestamps(
|
||||||
getDate(startDate!, startTime!),
|
getDate(startDate!, startTime!),
|
||||||
movingTime!,
|
movingTime!,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import { minTolerance, ReducedGPXLayerCollection, tolerance } from './reduce.svelte';
|
import { minTolerance, ReducedGPXLayerCollection, tolerance } from './utils.svelte';
|
||||||
|
|
||||||
let props: { class?: string } = $props();
|
let props: { class?: string } = $props();
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/fi
|
|||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
|
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
|
||||||
import type { GeoJSONSource } from 'mapbox-gl';
|
import type { GeoJSONSource } from 'mapbox-gl';
|
||||||
import { get, writable, type Writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
|
|
||||||
export const minTolerance = 0.1;
|
export const minTolerance = 0.1;
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fixed flex flex-row w-screen h-screen supports-dvh:h-dvh">
|
<div class="fixed flex flex-row w-dvw h-dvh">
|
||||||
<div class="flex flex-col grow h-full min-w-0">
|
<div class="flex flex-col grow h-full min-w-0">
|
||||||
<div class="grow relative">
|
<div class="grow relative">
|
||||||
<Menu />
|
<Menu />
|
||||||
|
|||||||
@@ -73,9 +73,6 @@ const config = {
|
|||||||
'caret-blink': 'caret-blink 1.25s ease-out infinite',
|
'caret-blink': 'caret-blink 1.25s ease-out infinite',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
supports: {
|
|
||||||
dvh: 'height: 100dvh',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
plugins: [tailwindcssAnimate],
|
plugins: [tailwindcssAnimate],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user