mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-01-15 14:18:41 +00:00
Compare commits
9 Commits
d3e733aa3e
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59f31caf26 | ||
|
|
f24956c58d | ||
|
|
9019317e5c | ||
|
|
2a0227c1de | ||
|
|
f70db42b91 | ||
|
|
9cd87742f0 | ||
|
|
5dcb93ca5d | ||
|
|
256d62b29b | ||
|
|
595ea8e2d3 |
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
website/src/lib/components/ui
|
||||
website/src/lib/docs/**/*.mdx
|
||||
**/*.webmanifest
|
||||
514
gpx/src/gpx.ts
514
gpx/src/gpx.ts
@@ -1,4 +1,5 @@
|
||||
import { ramerDouglasPeucker } from './simplify';
|
||||
import { GPXStatistics, GPXStatisticsGroup, TrackPointLocalStatistics } from './statistics';
|
||||
import {
|
||||
Coordinates,
|
||||
GPXFileAttributes,
|
||||
@@ -16,6 +17,16 @@ import {
|
||||
} from './types';
|
||||
import { immerable, isDraft, original, freeze } from 'immer';
|
||||
|
||||
function cloneJSON<T>(obj: T): T {
|
||||
if (obj === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
// An abstract class that groups functions that need to be computed recursively in the GPX file hierarchy
|
||||
export abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
|
||||
_data: { [key: string]: any } = {};
|
||||
@@ -26,7 +37,6 @@ export abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
|
||||
abstract getNumberOfTrackPoints(): number;
|
||||
abstract getStartTimestamp(): Date | undefined;
|
||||
abstract getEndTimestamp(): Date | undefined;
|
||||
abstract getStatistics(): GPXStatistics;
|
||||
abstract getSegments(): TrackSegment[];
|
||||
abstract getTrackPoints(): TrackPoint[];
|
||||
|
||||
@@ -66,14 +76,6 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
|
||||
return this.children[this.children.length - 1].getEndTimestamp();
|
||||
}
|
||||
|
||||
getStatistics(): GPXStatistics {
|
||||
let statistics = new GPXStatistics();
|
||||
for (let child of this.children) {
|
||||
statistics.mergeWith(child.getStatistics());
|
||||
}
|
||||
return statistics;
|
||||
}
|
||||
|
||||
getSegments(): TrackSegment[] {
|
||||
return this.children.flatMap((child) => child.getSegments());
|
||||
}
|
||||
@@ -198,8 +200,16 @@ export class GPXFile extends GPXTreeNode<Track> {
|
||||
});
|
||||
}
|
||||
|
||||
getStatistics(): GPXStatisticsGroup {
|
||||
let statistics = new GPXStatisticsGroup();
|
||||
this.forEachSegment((segment) => {
|
||||
statistics.add(segment.getStatistics());
|
||||
});
|
||||
return statistics;
|
||||
}
|
||||
|
||||
getStyle(defaultColor?: string): MergedLineStyles {
|
||||
return this.trk
|
||||
const style = this.trk
|
||||
.map((track) => track.getStyle())
|
||||
.reduce(
|
||||
(acc, style) => {
|
||||
@@ -209,8 +219,6 @@ export class GPXFile extends GPXTreeNode<Track> {
|
||||
!acc.color.includes(style['gpx_style:color'])
|
||||
) {
|
||||
acc.color.push(style['gpx_style:color']);
|
||||
} else if (defaultColor && !acc.color.includes(defaultColor)) {
|
||||
acc.color.push(defaultColor);
|
||||
}
|
||||
if (
|
||||
style &&
|
||||
@@ -234,16 +242,20 @@ export class GPXFile extends GPXTreeNode<Track> {
|
||||
width: [],
|
||||
}
|
||||
);
|
||||
if (style.color.length === 0 && defaultColor) {
|
||||
style.color.push(defaultColor);
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
clone(): GPXFile {
|
||||
return new GPXFile({
|
||||
attributes: structuredClone(this.attributes),
|
||||
metadata: structuredClone(this.metadata),
|
||||
attributes: cloneJSON(this.attributes),
|
||||
metadata: cloneJSON(this.metadata),
|
||||
wpt: this.wpt.map((waypoint) => waypoint.clone()),
|
||||
trk: this.trk.map((track) => track.clone()),
|
||||
rte: [],
|
||||
_data: structuredClone(this._data),
|
||||
_data: cloneJSON(this._data),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -256,7 +268,7 @@ export class GPXFile extends GPXTreeNode<Track> {
|
||||
|
||||
toGPXFileType(exclude: string[] = []): GPXFileType {
|
||||
let file: GPXFileType = {
|
||||
attributes: structuredClone(this.attributes),
|
||||
attributes: cloneJSON(this.attributes),
|
||||
metadata: {},
|
||||
wpt: this.wpt.map((wpt) => wpt.toWaypointType(exclude)),
|
||||
trk: this.trk.map((track) => track.toTrackType(exclude)),
|
||||
@@ -270,10 +282,10 @@ export class GPXFile extends GPXTreeNode<Track> {
|
||||
file.metadata.desc = this.metadata.desc;
|
||||
}
|
||||
if (this.metadata.author) {
|
||||
file.metadata.author = structuredClone(this.metadata.author);
|
||||
file.metadata.author = cloneJSON(this.metadata.author);
|
||||
}
|
||||
if (this.metadata.link) {
|
||||
file.metadata.link = structuredClone(this.metadata.link);
|
||||
file.metadata.link = cloneJSON(this.metadata.link);
|
||||
}
|
||||
if (this.metadata.time && !exclude.includes('time')) {
|
||||
file.metadata.time = this.metadata.time;
|
||||
@@ -566,11 +578,11 @@ export class Track extends GPXTreeNode<TrackSegment> {
|
||||
cmt: this.cmt,
|
||||
desc: this.desc,
|
||||
src: this.src,
|
||||
link: structuredClone(this.link),
|
||||
link: cloneJSON(this.link),
|
||||
type: this.type,
|
||||
extensions: structuredClone(this.extensions),
|
||||
extensions: cloneJSON(this.extensions),
|
||||
trkseg: this.trkseg.map((seg) => seg.clone()),
|
||||
_data: structuredClone(this._data),
|
||||
_data: cloneJSON(this._data),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -808,7 +820,9 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
_computeStatistics(): GPXStatistics {
|
||||
let statistics = new GPXStatistics();
|
||||
|
||||
statistics.global.length = this.trkpt.length;
|
||||
statistics.local.points = this.trkpt.slice(0);
|
||||
statistics.local.data = this.trkpt.map(() => new TrackPointLocalStatistics());
|
||||
|
||||
const points = this.trkpt;
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
@@ -820,19 +834,18 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
statistics.global.distance.total += dist;
|
||||
}
|
||||
|
||||
statistics.local.distance.total.push(statistics.global.distance.total);
|
||||
statistics.local.data[i].distance.total = statistics.global.distance.total;
|
||||
|
||||
// time
|
||||
if (points[i].time === undefined) {
|
||||
statistics.local.time.total.push(0);
|
||||
statistics.local.data[i].time.total = 0;
|
||||
} else {
|
||||
if (statistics.global.time.start === undefined) {
|
||||
statistics.global.time.start = points[i].time;
|
||||
}
|
||||
statistics.global.time.end = points[i].time;
|
||||
statistics.local.time.total.push(
|
||||
(points[i].time.getTime() - statistics.global.time.start.getTime()) / 1000
|
||||
);
|
||||
statistics.local.data[i].time.total =
|
||||
(points[i].time.getTime() - statistics.global.time.start.getTime()) / 1000;
|
||||
}
|
||||
|
||||
// speed
|
||||
@@ -847,8 +860,8 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
}
|
||||
}
|
||||
|
||||
statistics.local.distance.moving.push(statistics.global.distance.moving);
|
||||
statistics.local.time.moving.push(statistics.global.time.moving);
|
||||
statistics.local.data[i].distance.moving = statistics.global.distance.moving;
|
||||
statistics.local.data[i].time.moving = statistics.global.time.moving;
|
||||
|
||||
// bounds
|
||||
statistics.global.bounds.southWest.lat = Math.min(
|
||||
@@ -948,13 +961,22 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
? statistics.global.distance.moving / (statistics.global.time.moving / 3600)
|
||||
: 0;
|
||||
|
||||
statistics.local.speed = timeWindowSmoothing(points, 10000, (start, end) =>
|
||||
timeWindowSmoothing(
|
||||
points,
|
||||
10000,
|
||||
(start, end) =>
|
||||
points[start].time && points[end].time
|
||||
? (3600 *
|
||||
(statistics.local.distance.total[end] -
|
||||
statistics.local.distance.total[start])) /
|
||||
Math.max((points[end].time.getTime() - points[start].time.getTime()) / 1000, 1)
|
||||
: undefined
|
||||
(statistics.local.data[end].distance.total -
|
||||
statistics.local.data[start].distance.total)) /
|
||||
Math.max(
|
||||
(points[end].time.getTime() - points[start].time.getTime()) / 1000,
|
||||
1
|
||||
)
|
||||
: undefined,
|
||||
(value, index) => {
|
||||
statistics.local.data[index].speed = value;
|
||||
}
|
||||
);
|
||||
|
||||
return statistics;
|
||||
@@ -974,7 +996,13 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
let cumulEle = 0;
|
||||
let currentStart = start;
|
||||
let currentEnd = start;
|
||||
let smoothedEle = distanceWindowSmoothing(start, end + 1, statistics, 0.1, (s, e) => {
|
||||
let prevSmoothedEle = 0;
|
||||
distanceWindowSmoothing(
|
||||
start,
|
||||
end + 1,
|
||||
statistics,
|
||||
0.1,
|
||||
(s, e) => {
|
||||
for (let i = currentStart; i < s; i++) {
|
||||
cumulEle -= this.trkpt[i].ele ?? 0;
|
||||
}
|
||||
@@ -984,43 +1012,49 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
currentStart = s;
|
||||
currentEnd = e + 1;
|
||||
return cumulEle / (e - s + 1);
|
||||
});
|
||||
smoothedEle[0] = this.trkpt[start].ele ?? 0;
|
||||
smoothedEle[smoothedEle.length - 1] = this.trkpt[end].ele ?? 0;
|
||||
|
||||
for (let j = start; j < end; j++) {
|
||||
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
|
||||
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
|
||||
|
||||
const ele = smoothedEle[j - start + 1] - smoothedEle[j - start];
|
||||
},
|
||||
(smoothedEle, j) => {
|
||||
if (j === start) {
|
||||
smoothedEle = this.trkpt[start].ele ?? 0;
|
||||
prevSmoothedEle = smoothedEle;
|
||||
} else if (j === end) {
|
||||
smoothedEle = this.trkpt[end].ele ?? 0;
|
||||
}
|
||||
const ele = smoothedEle - prevSmoothedEle;
|
||||
if (ele > 0) {
|
||||
statistics.global.elevation.gain += ele;
|
||||
} else if (ele < 0) {
|
||||
statistics.global.elevation.loss -= ele;
|
||||
}
|
||||
prevSmoothedEle = smoothedEle;
|
||||
if (j < end) {
|
||||
statistics.local.data[j].elevation.gain = statistics.global.elevation.gain;
|
||||
statistics.local.data[j].elevation.loss = statistics.global.elevation.loss;
|
||||
}
|
||||
}
|
||||
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
|
||||
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
|
||||
);
|
||||
}
|
||||
if (statistics.global.length > 0) {
|
||||
statistics.local.data[statistics.global.length - 1].elevation.gain =
|
||||
statistics.global.elevation.gain;
|
||||
statistics.local.data[statistics.global.length - 1].elevation.loss =
|
||||
statistics.global.elevation.loss;
|
||||
}
|
||||
|
||||
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];
|
||||
statistics.local.data[end].distance.total -
|
||||
statistics.local.data[start].distance.total;
|
||||
let ele = (simplified[i + 1].point.ele ?? 0) - (simplified[i].point.ele ?? 0);
|
||||
|
||||
for (let j = start; j < end + (i + 1 === simplified.length - 1 ? 1 : 0); j++) {
|
||||
slope.push((0.1 * ele) / dist);
|
||||
length.push(dist);
|
||||
statistics.local.data[j].slope.segment = (0.1 * ele) / dist;
|
||||
statistics.local.data[j].slope.length = dist;
|
||||
}
|
||||
}
|
||||
|
||||
statistics.local.slope.segment = slope;
|
||||
statistics.local.slope.length = length;
|
||||
statistics.local.slope.at = distanceWindowSmoothing(
|
||||
distanceWindowSmoothing(
|
||||
0,
|
||||
this.trkpt.length,
|
||||
statistics,
|
||||
@@ -1028,8 +1062,12 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
(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];
|
||||
statistics.local.data[end].distance.total -
|
||||
statistics.local.data[start].distance.total;
|
||||
return dist > 0 ? (0.1 * ele) / dist : 0;
|
||||
},
|
||||
(value, index) => {
|
||||
statistics.local.data[index].slope.at = value;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1087,7 +1125,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
clone(): TrackSegment {
|
||||
return new TrackSegment({
|
||||
trkpt: this.trkpt.map((point) => point.clone()),
|
||||
_data: structuredClone(this._data),
|
||||
_data: cloneJSON(this._data),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1213,14 +1251,14 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
let trkpt = og.trkpt.map(
|
||||
(point, i) =>
|
||||
new TrackPoint({
|
||||
attributes: structuredClone(point.attributes),
|
||||
attributes: cloneJSON(point.attributes),
|
||||
ele: point.ele,
|
||||
time: new Date(
|
||||
newStartTimestamp.getTime() +
|
||||
(originalEndTimestamp.getTime() - og.trkpt[i].time.getTime())
|
||||
),
|
||||
extensions: structuredClone(point.extensions),
|
||||
_data: structuredClone(point._data),
|
||||
extensions: cloneJSON(point.extensions),
|
||||
_data: cloneJSON(point._data),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1279,13 +1317,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
) {
|
||||
let og = getOriginal(this); // Read as much as possible from the original object because it is faster
|
||||
let statistics = og._computeStatistics();
|
||||
let trkpt = withArtificialTimestamps(
|
||||
og.trkpt,
|
||||
totalTime,
|
||||
lastPoint,
|
||||
startTime,
|
||||
statistics.local.slope.at
|
||||
);
|
||||
let trkpt = withArtificialTimestamps(og.trkpt, totalTime, lastPoint, startTime, statistics);
|
||||
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
||||
}
|
||||
|
||||
@@ -1294,6 +1326,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
}
|
||||
}
|
||||
|
||||
const emptyExtensions: Record<string, string> = {};
|
||||
export class TrackPoint {
|
||||
[immerable] = true;
|
||||
|
||||
@@ -1388,7 +1421,7 @@ export class TrackPoint {
|
||||
this.extensions['gpxtpx:TrackPointExtension'] &&
|
||||
this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']
|
||||
? this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']
|
||||
: {};
|
||||
: emptyExtensions;
|
||||
}
|
||||
|
||||
toTrackPointType(exclude: string[] = []): TrackPointType {
|
||||
@@ -1464,7 +1497,7 @@ export class TrackPoint {
|
||||
},
|
||||
ele: this.ele,
|
||||
time: this.time ? new Date(this.time.getTime()) : undefined,
|
||||
extensions: this.extensions ? structuredClone(this.extensions) : undefined,
|
||||
extensions: this.extensions ? cloneJSON(this.extensions) : undefined,
|
||||
_data: {
|
||||
index: this._data?.index,
|
||||
anchor: this._data?.anchor,
|
||||
@@ -1566,7 +1599,7 @@ export class Waypoint {
|
||||
name: this.name,
|
||||
cmt: this.cmt,
|
||||
desc: this.desc,
|
||||
link: structuredClone(this.link),
|
||||
link: cloneJSON(this.link),
|
||||
sym: this.sym,
|
||||
type: this.type,
|
||||
});
|
||||
@@ -1609,305 +1642,6 @@ export class Waypoint {
|
||||
}
|
||||
}
|
||||
|
||||
export class GPXStatistics {
|
||||
global: {
|
||||
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>>;
|
||||
};
|
||||
local: {
|
||||
points: TrackPoint[];
|
||||
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.global = {
|
||||
distance: {
|
||||
moving: 0,
|
||||
total: 0,
|
||||
},
|
||||
time: {
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
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,
|
||||
},
|
||||
},
|
||||
atemp: {
|
||||
avg: 0,
|
||||
count: 0,
|
||||
},
|
||||
hr: {
|
||||
avg: 0,
|
||||
count: 0,
|
||||
},
|
||||
cad: {
|
||||
avg: 0,
|
||||
count: 0,
|
||||
},
|
||||
power: {
|
||||
avg: 0,
|
||||
count: 0,
|
||||
},
|
||||
extensions: {},
|
||||
};
|
||||
this.local = {
|
||||
points: [],
|
||||
distance: {
|
||||
moving: [],
|
||||
total: [],
|
||||
},
|
||||
time: {
|
||||
moving: [],
|
||||
total: [],
|
||||
},
|
||||
speed: [],
|
||||
elevation: {
|
||||
gain: [],
|
||||
loss: [],
|
||||
},
|
||||
slope: {
|
||||
at: [],
|
||||
segment: [],
|
||||
length: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
mergeWith(other: GPXStatistics): void {
|
||||
this.local.points = this.local.points.concat(other.local.points);
|
||||
|
||||
this.local.distance.total = this.local.distance.total.concat(
|
||||
other.local.distance.total.map((distance) => distance + this.global.distance.total)
|
||||
);
|
||||
this.local.distance.moving = this.local.distance.moving.concat(
|
||||
other.local.distance.moving.map((distance) => distance + this.global.distance.moving)
|
||||
);
|
||||
this.local.time.total = this.local.time.total.concat(
|
||||
other.local.time.total.map((time) => time + this.global.time.total)
|
||||
);
|
||||
this.local.time.moving = this.local.time.moving.concat(
|
||||
other.local.time.moving.map((time) => time + this.global.time.moving)
|
||||
);
|
||||
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.local.speed = this.local.speed.concat(other.local.speed);
|
||||
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;
|
||||
|
||||
this.global.time.start =
|
||||
this.global.time.start !== undefined && other.global.time.start !== undefined
|
||||
? new Date(
|
||||
Math.min(this.global.time.start.getTime(), other.global.time.start.getTime())
|
||||
)
|
||||
: (this.global.time.start ?? other.global.time.start);
|
||||
this.global.time.end =
|
||||
this.global.time.end !== undefined && other.global.time.end !== undefined
|
||||
? new Date(
|
||||
Math.max(this.global.time.end.getTime(), other.global.time.end.getTime())
|
||||
)
|
||||
: (this.global.time.end ?? other.global.time.end);
|
||||
|
||||
this.global.time.total += other.global.time.total;
|
||||
this.global.time.moving += other.global.time.moving;
|
||||
|
||||
this.global.speed.moving =
|
||||
this.global.time.moving > 0
|
||||
? this.global.distance.moving / (this.global.time.moving / 3600)
|
||||
: 0;
|
||||
this.global.speed.total =
|
||||
this.global.time.total > 0
|
||||
? this.global.distance.total / (this.global.time.total / 3600)
|
||||
: 0;
|
||||
|
||||
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.min(
|
||||
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.max(
|
||||
this.global.bounds.northEast.lon,
|
||||
other.global.bounds.northEast.lon
|
||||
);
|
||||
|
||||
this.global.atemp.avg =
|
||||
(this.global.atemp.count * this.global.atemp.avg +
|
||||
other.global.atemp.count * other.global.atemp.avg) /
|
||||
Math.max(1, this.global.atemp.count + other.global.atemp.count);
|
||||
this.global.atemp.count += other.global.atemp.count;
|
||||
this.global.hr.avg =
|
||||
(this.global.hr.count * this.global.hr.avg +
|
||||
other.global.hr.count * other.global.hr.avg) /
|
||||
Math.max(1, this.global.hr.count + other.global.hr.count);
|
||||
this.global.hr.count += other.global.hr.count;
|
||||
this.global.cad.avg =
|
||||
(this.global.cad.count * this.global.cad.avg +
|
||||
other.global.cad.count * other.global.cad.avg) /
|
||||
Math.max(1, this.global.cad.count + other.global.cad.count);
|
||||
this.global.cad.count += other.global.cad.count;
|
||||
this.global.power.avg =
|
||||
(this.global.power.count * this.global.power.avg +
|
||||
other.global.power.count * other.global.power.avg) /
|
||||
Math.max(1, this.global.power.count + other.global.power.count);
|
||||
this.global.power.count += other.global.power.count;
|
||||
Object.keys(other.global.extensions).forEach((extension) => {
|
||||
if (this.global.extensions[extension] === undefined) {
|
||||
this.global.extensions[extension] = {};
|
||||
}
|
||||
Object.keys(other.global.extensions[extension]).forEach((value) => {
|
||||
if (this.global.extensions[extension][value] === undefined) {
|
||||
this.global.extensions[extension][value] = 0;
|
||||
}
|
||||
this.global.extensions[extension][value] +=
|
||||
other.global.extensions[extension][value];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
slice(start: number, end: number): GPXStatistics {
|
||||
if (start < 0) {
|
||||
start = 0;
|
||||
} else if (start >= this.local.points.length) {
|
||||
return new GPXStatistics();
|
||||
}
|
||||
if (end < start) {
|
||||
return new GPXStatistics();
|
||||
} else if (end >= this.local.points.length) {
|
||||
end = this.local.points.length - 1;
|
||||
}
|
||||
|
||||
let statistics = new GPXStatistics();
|
||||
|
||||
statistics.local.points = this.local.points.slice(start, end + 1);
|
||||
|
||||
statistics.global.distance.total =
|
||||
this.local.distance.total[end] - this.local.distance.total[start];
|
||||
statistics.global.distance.moving =
|
||||
this.local.distance.moving[end] - this.local.distance.moving[start];
|
||||
|
||||
statistics.global.time.start = this.local.points[start].time;
|
||||
statistics.global.time.end = this.local.points[end].time;
|
||||
|
||||
statistics.global.time.total = this.local.time.total[end] - this.local.time.total[start];
|
||||
statistics.global.time.moving = this.local.time.moving[end] - this.local.time.moving[start];
|
||||
|
||||
statistics.global.speed.moving =
|
||||
statistics.global.time.moving > 0
|
||||
? statistics.global.distance.moving / (statistics.global.time.moving / 3600)
|
||||
: 0;
|
||||
statistics.global.speed.total =
|
||||
statistics.global.time.total > 0
|
||||
? statistics.global.distance.total / (statistics.global.time.total / 3600)
|
||||
: 0;
|
||||
|
||||
statistics.global.elevation.gain =
|
||||
this.local.elevation.gain[end] - this.local.elevation.gain[start];
|
||||
statistics.global.elevation.loss =
|
||||
this.local.elevation.loss[end] - this.local.elevation.loss[start];
|
||||
|
||||
statistics.global.bounds.southWest.lat = this.global.bounds.southWest.lat;
|
||||
statistics.global.bounds.southWest.lon = this.global.bounds.southWest.lon;
|
||||
statistics.global.bounds.northEast.lat = this.global.bounds.northEast.lat;
|
||||
statistics.global.bounds.northEast.lon = this.global.bounds.northEast.lon;
|
||||
|
||||
statistics.global.atemp = this.global.atemp;
|
||||
statistics.global.hr = this.global.hr;
|
||||
statistics.global.cad = this.global.cad;
|
||||
statistics.global.power = this.global.power;
|
||||
|
||||
return statistics;
|
||||
}
|
||||
}
|
||||
|
||||
const earthRadius = 6371008.8;
|
||||
export function distance(
|
||||
coord1: TrackPoint | Coordinates,
|
||||
@@ -1941,9 +1675,9 @@ export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
||||
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 x1 = statistics.local.data[point1._data.index].distance.total * 1000;
|
||||
let x2 = statistics.local.data[point2._data.index].distance.total * 1000;
|
||||
let x3 = statistics.local.data[point3._data.index].distance.total * 1000;
|
||||
let y1 = point1.ele;
|
||||
let y2 = point2.ele;
|
||||
let y3 = point3.ele;
|
||||
@@ -1962,10 +1696,9 @@ function windowSmoothing(
|
||||
right: number,
|
||||
distance: (index1: number, index2: number) => number,
|
||||
window: number,
|
||||
compute: (start: number, end: number) => number
|
||||
): number[] {
|
||||
let result = [];
|
||||
|
||||
compute: (start: number, end: number) => number,
|
||||
callback: (value: number, index: number) => void
|
||||
): void {
|
||||
let start = left;
|
||||
for (var i = left; i < right; i++) {
|
||||
while (start + 1 < i && distance(start, i) > window) {
|
||||
@@ -1975,10 +1708,8 @@ function windowSmoothing(
|
||||
while (end < right && distance(i, end) <= window) {
|
||||
end++;
|
||||
}
|
||||
result.push(compute(start, end - 1));
|
||||
callback(compute(start, end - 1), i);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function distanceWindowSmoothing(
|
||||
@@ -1986,30 +1717,35 @@ function distanceWindowSmoothing(
|
||||
right: number,
|
||||
statistics: GPXStatistics,
|
||||
window: number,
|
||||
compute: (start: number, end: number) => number
|
||||
): number[] {
|
||||
return windowSmoothing(
|
||||
compute: (start: number, end: number) => number,
|
||||
callback: (value: number, index: number) => void
|
||||
): void {
|
||||
windowSmoothing(
|
||||
left,
|
||||
right,
|
||||
(index1, index2) =>
|
||||
statistics.local.distance.total[index2] - statistics.local.distance.total[index1],
|
||||
statistics.local.data[index2].distance.total -
|
||||
statistics.local.data[index1].distance.total,
|
||||
window,
|
||||
compute
|
||||
compute,
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
function timeWindowSmoothing(
|
||||
points: TrackPoint[],
|
||||
window: number,
|
||||
compute: (start: number, end: number) => number
|
||||
): number[] {
|
||||
return windowSmoothing(
|
||||
compute: (start: number, end: number) => number,
|
||||
callback: (value: number, index: number) => void
|
||||
): void {
|
||||
windowSmoothing(
|
||||
0,
|
||||
points.length,
|
||||
(index1, index2) =>
|
||||
points[index2].time?.getTime() - points[index1].time?.getTime() || 2 * window,
|
||||
window,
|
||||
compute
|
||||
compute,
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2061,14 +1797,14 @@ function withArtificialTimestamps(
|
||||
totalTime: number,
|
||||
lastPoint: TrackPoint | undefined,
|
||||
startTime: Date,
|
||||
slope: number[]
|
||||
statistics: GPXStatistics
|
||||
): TrackPoint[] {
|
||||
let weight = [];
|
||||
let totalWeight = 0;
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
let dist = distance(points[i].getCoordinates(), points[i + 1].getCoordinates());
|
||||
let w = dist * (0.5 + 1 / (1 + Math.exp(-0.2 * slope[i])));
|
||||
let w = dist * (0.5 + 1 / (1 + Math.exp(-0.2 * statistics.local.data[i].slope.at)));
|
||||
weight.push(w);
|
||||
totalWeight += w;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './gpx';
|
||||
export * from './statistics';
|
||||
export { Coordinates, LineStyleExtension, WaypointType } from './types';
|
||||
export { parseGPX, buildGPX } from './io';
|
||||
export * from './simplify';
|
||||
|
||||
391
gpx/src/statistics.ts
Normal file
391
gpx/src/statistics.ts
Normal file
@@ -0,0 +1,391 @@
|
||||
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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
src/lib/components/ui
|
||||
src/lib/docs/**/*.mdx
|
||||
**/*.webmanifest
|
||||
@@ -10,8 +10,8 @@
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . --config ../.prettierrc && eslint .",
|
||||
"format": "prettier --write . --config ../.prettierrc"
|
||||
"lint": "prettier --check . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore && eslint .",
|
||||
"format": "prettier --write . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lucide/svelte": "^0.544.0",
|
||||
|
||||
@@ -368,6 +368,26 @@ export const overlays: { [key: string]: string | StyleSpecification } = {
|
||||
],
|
||||
},
|
||||
bikerouterGravel: bikerouterGravel as StyleSpecification,
|
||||
openRailwayMap: {
|
||||
version: 8,
|
||||
sources: {
|
||||
openRailwayMap: {
|
||||
type: 'raster',
|
||||
tiles: ['https://tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png'],
|
||||
tileSize: 256,
|
||||
maxzoom: 19,
|
||||
attribution:
|
||||
'Data <a href="https://www.openstreetmap.org/copyright">© OpenStreetMap contributors</a>, Style: <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA 2.0</a> <a href="http://www.openrailwaymap.org/">OpenRailwayMap</a>',
|
||||
},
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
id: 'openRailwayMap',
|
||||
type: 'raster',
|
||||
source: 'openRailwayMap',
|
||||
},
|
||||
],
|
||||
},
|
||||
swisstopoSlope: {
|
||||
version: 8,
|
||||
sources: {
|
||||
@@ -801,6 +821,7 @@ export const overlayTree: LayerTreeType = {
|
||||
},
|
||||
cyclOSMlite: true,
|
||||
bikerouterGravel: true,
|
||||
openRailwayMap: true,
|
||||
},
|
||||
countries: {
|
||||
france: {
|
||||
@@ -885,6 +906,7 @@ export const defaultOverlays: LayerTreeType = {
|
||||
},
|
||||
cyclOSMlite: false,
|
||||
bikerouterGravel: false,
|
||||
openRailwayMap: false,
|
||||
},
|
||||
countries: {
|
||||
france: {
|
||||
@@ -1020,6 +1042,7 @@ export const defaultOverlayTree: LayerTreeType = {
|
||||
},
|
||||
cyclOSMlite: false,
|
||||
bikerouterGravel: false,
|
||||
openRailwayMap: false,
|
||||
},
|
||||
countries: {
|
||||
france: {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from '@lucide/svelte';
|
||||
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import type { GPXStatistics } from 'gpx';
|
||||
import type { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
||||
import type { Readable } from 'svelte/store';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
|
||||
@@ -18,14 +18,14 @@
|
||||
orientation,
|
||||
panelSize,
|
||||
}: {
|
||||
gpxStatistics: Readable<GPXStatistics>;
|
||||
slicedGPXStatistics: Readable<[GPXStatistics, number, number] | undefined>;
|
||||
gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||
slicedGPXStatistics: Readable<[GPXGlobalStatistics, number, number] | undefined>;
|
||||
orientation: 'horizontal' | 'vertical';
|
||||
panelSize: number;
|
||||
} = $props();
|
||||
|
||||
let statistics = $derived(
|
||||
$slicedGPXStatistics !== undefined ? $slicedGPXStatistics[0] : $gpxStatistics
|
||||
$slicedGPXStatistics !== undefined ? $slicedGPXStatistics[0] : $gpxStatistics.global
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -42,15 +42,15 @@
|
||||
<Tooltip label={i18n._('quantities.distance')}>
|
||||
<span class="flex flex-row items-center">
|
||||
<Ruler size="16" class="mr-1" />
|
||||
<WithUnits value={statistics.global.distance.total} type="distance" />
|
||||
<WithUnits value={statistics.distance.total} type="distance" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label={i18n._('quantities.elevation_gain_loss')}>
|
||||
<span class="flex flex-row items-center">
|
||||
<MoveUpRight size="16" class="mr-1" />
|
||||
<WithUnits value={statistics.global.elevation.gain} type="elevation" />
|
||||
<WithUnits value={statistics.elevation.gain} type="elevation" />
|
||||
<MoveDownRight size="16" class="mx-1" />
|
||||
<WithUnits value={statistics.global.elevation.loss} type="elevation" />
|
||||
<WithUnits value={statistics.elevation.loss} type="elevation" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
{#if panelSize > 120 || orientation === 'horizontal'}
|
||||
@@ -64,13 +64,9 @@
|
||||
>
|
||||
<span class="flex flex-row items-center">
|
||||
<Zap size="16" class="mr-1" />
|
||||
<WithUnits
|
||||
value={statistics.global.speed.moving}
|
||||
type="speed"
|
||||
showUnits={false}
|
||||
/>
|
||||
<WithUnits value={statistics.speed.moving} type="speed" showUnits={false} />
|
||||
<span class="mx-1">/</span>
|
||||
<WithUnits value={statistics.global.speed.total} type="speed" />
|
||||
<WithUnits value={statistics.speed.total} type="speed" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
@@ -83,9 +79,9 @@
|
||||
>
|
||||
<span class="flex flex-row items-center">
|
||||
<Timer size="16" class="mr-1" />
|
||||
<WithUnits value={statistics.global.time.moving} type="time" />
|
||||
<WithUnits value={statistics.time.moving} type="time" />
|
||||
<span class="mx-1">/</span>
|
||||
<WithUnits value={statistics.global.time.total} type="time" />
|
||||
<WithUnits value={statistics.time.total} type="time" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
Construction,
|
||||
} from '@lucide/svelte';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import type { GPXStatistics } from 'gpx';
|
||||
import type { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { ElevationProfile } from '$lib/components/elevation-profile/elevation-profile';
|
||||
@@ -32,8 +32,8 @@
|
||||
elevationFill,
|
||||
showControls = true,
|
||||
}: {
|
||||
gpxStatistics: Readable<GPXStatistics>;
|
||||
slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
|
||||
gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
||||
additionalDatasets: Writable<string[]>;
|
||||
elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>;
|
||||
showControls?: boolean;
|
||||
|
||||
@@ -23,7 +23,7 @@ import Chart, {
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { get, type Readable, type Writable } from 'svelte/store';
|
||||
import { map } from '$lib/components/map/map';
|
||||
import type { GPXStatistics } from 'gpx';
|
||||
import type { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
||||
import { mode } from 'mode-watcher';
|
||||
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
|
||||
|
||||
@@ -54,14 +54,14 @@ export class ElevationProfile {
|
||||
private _dragging = false;
|
||||
private _panning = false;
|
||||
|
||||
private _gpxStatistics: Readable<GPXStatistics>;
|
||||
private _slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
|
||||
private _gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||
private _slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
||||
private _additionalDatasets: Readable<string[]>;
|
||||
private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>;
|
||||
|
||||
constructor(
|
||||
gpxStatistics: Readable<GPXStatistics>,
|
||||
slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>,
|
||||
gpxStatistics: Readable<GPXStatisticsGroup>,
|
||||
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>,
|
||||
additionalDatasets: Readable<string[]>,
|
||||
elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>,
|
||||
canvas: HTMLCanvasElement,
|
||||
@@ -342,7 +342,7 @@ export class ElevationProfile {
|
||||
if (evt.x - rect.left <= this._chart.chartArea.left) {
|
||||
return 0;
|
||||
} else if (evt.x - rect.left >= this._chart.chartArea.right) {
|
||||
return get(this._gpxStatistics).local.points.length - 1;
|
||||
return this._chart.data.datasets[0].data.length - 1;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
@@ -375,7 +375,7 @@ export class ElevationProfile {
|
||||
startIndex = endIndex;
|
||||
} else if (startIndex !== endIndex) {
|
||||
this._slicedGPXStatistics.set([
|
||||
get(this._gpxStatistics).slice(
|
||||
get(this._gpxStatistics).sliced(
|
||||
Math.min(startIndex, endIndex),
|
||||
Math.max(startIndex, endIndex)
|
||||
),
|
||||
@@ -405,100 +405,99 @@ export class ElevationProfile {
|
||||
return;
|
||||
}
|
||||
const data = get(this._gpxStatistics);
|
||||
const units = {
|
||||
distance: get(distanceUnits),
|
||||
velocity: get(velocityUnits),
|
||||
temperature: get(temperatureUnits),
|
||||
};
|
||||
|
||||
const datasets: Array<Array<any>> = [[], [], [], [], [], []];
|
||||
data.forEachTrackPoint((trkpt, distance, speed, slope, index) => {
|
||||
datasets[0].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: trkpt.ele ? getConvertedElevation(trkpt.ele, units.distance) : 0,
|
||||
time: trkpt.time,
|
||||
slope: slope,
|
||||
extensions: trkpt.getExtensions(),
|
||||
coordinates: trkpt.getCoordinates(),
|
||||
index: index,
|
||||
});
|
||||
if (data.global.time.total > 0) {
|
||||
datasets[1].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: getConvertedVelocity(speed, units.velocity, units.distance),
|
||||
index: index,
|
||||
});
|
||||
}
|
||||
if (data.global.hr.count > 0) {
|
||||
datasets[2].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: trkpt.getHeartRate(),
|
||||
index: index,
|
||||
});
|
||||
}
|
||||
if (data.global.cad.count > 0) {
|
||||
datasets[3].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: trkpt.getCadence(),
|
||||
index: index,
|
||||
});
|
||||
}
|
||||
if (data.global.atemp.count > 0) {
|
||||
datasets[4].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: getConvertedTemperature(trkpt.getTemperature(), units.temperature),
|
||||
index: index,
|
||||
});
|
||||
}
|
||||
if (data.global.power.count > 0) {
|
||||
datasets[5].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: trkpt.getPower(),
|
||||
index: index,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this._chart.data.datasets[0] = {
|
||||
label: i18n._('quantities.elevation'),
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.ele ? getConvertedElevation(point.ele) : 0,
|
||||
time: point.time,
|
||||
slope: {
|
||||
at: data.local.slope.at[index],
|
||||
segment: data.local.slope.segment[index],
|
||||
length: data.local.slope.length[index],
|
||||
},
|
||||
extensions: point.getExtensions(),
|
||||
coordinates: point.getCoordinates(),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
data: datasets[0],
|
||||
normalized: true,
|
||||
fill: 'start',
|
||||
order: 1,
|
||||
segment: {},
|
||||
};
|
||||
this._chart.data.datasets[1] = {
|
||||
data:
|
||||
data.global.time.total > 0
|
||||
? data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: getConvertedVelocity(data.local.speed[index]),
|
||||
index: index,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
data: datasets[1],
|
||||
normalized: true,
|
||||
yAxisID: 'yspeed',
|
||||
};
|
||||
this._chart.data.datasets[2] = {
|
||||
data:
|
||||
data.global.hr.count > 0
|
||||
? data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getHeartRate(),
|
||||
index: index,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
data: datasets[2],
|
||||
normalized: true,
|
||||
yAxisID: 'yhr',
|
||||
};
|
||||
this._chart.data.datasets[3] = {
|
||||
data:
|
||||
data.global.cad.count > 0
|
||||
? data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getCadence(),
|
||||
index: index,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
data: datasets[3],
|
||||
normalized: true,
|
||||
yAxisID: 'ycad',
|
||||
};
|
||||
this._chart.data.datasets[4] = {
|
||||
data:
|
||||
data.global.atemp.count > 0
|
||||
? data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: getConvertedTemperature(point.getTemperature()),
|
||||
index: index,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
data: datasets[4],
|
||||
normalized: true,
|
||||
yAxisID: 'yatemp',
|
||||
};
|
||||
this._chart.data.datasets[5] = {
|
||||
data:
|
||||
data.global.power.count > 0
|
||||
? data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getPower(),
|
||||
index: index,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
data: datasets[5],
|
||||
normalized: true,
|
||||
yAxisID: 'ypower',
|
||||
};
|
||||
|
||||
this._chart.options.scales!.x!['min'] = 0;
|
||||
this._chart.options.scales!.x!['max'] = getConvertedDistance(data.global.distance.total);
|
||||
this._chart.options.scales!.x!['max'] = getConvertedDistance(
|
||||
data.global.distance.total,
|
||||
units.distance
|
||||
);
|
||||
|
||||
this.setVisibility();
|
||||
this.setFill();
|
||||
@@ -591,10 +590,12 @@ export class ElevationProfile {
|
||||
|
||||
const gpxStatistics = get(this._gpxStatistics);
|
||||
let startPixel = this._chart.scales.x.getPixelForValue(
|
||||
getConvertedDistance(gpxStatistics.local.distance.total[startIndex])
|
||||
getConvertedDistance(
|
||||
gpxStatistics.getTrackPoint(startIndex)?.distance.total ?? 0
|
||||
)
|
||||
);
|
||||
let endPixel = this._chart.scales.x.getPixelForValue(
|
||||
getConvertedDistance(gpxStatistics.local.distance.total[endIndex])
|
||||
getConvertedDistance(gpxStatistics.getTrackPoint(endIndex)?.distance.total ?? 0)
|
||||
);
|
||||
|
||||
selectionContext.fillRect(
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
SquareActivity,
|
||||
} from '@lucide/svelte';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { GPXStatistics } from 'gpx';
|
||||
import { GPXGlobalStatistics } from 'gpx';
|
||||
import { ListRootItem } from '$lib/components/file-list/file-list';
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
import { selection } from '$lib/logic/selection';
|
||||
@@ -48,24 +48,24 @@
|
||||
extensions: false,
|
||||
};
|
||||
} else {
|
||||
let statistics = $gpxStatistics;
|
||||
let statistics = $gpxStatistics.global;
|
||||
if (exportState.current === ExportState.ALL) {
|
||||
statistics = Array.from(get(fileStateCollection).values())
|
||||
.map((file) => file.statistics)
|
||||
.reduce((acc, cur) => {
|
||||
if (cur !== undefined) {
|
||||
acc.mergeWith(cur.getStatisticsFor(new ListRootItem()));
|
||||
acc.mergeWith(cur.getStatisticsFor(new ListRootItem()).global);
|
||||
}
|
||||
return acc;
|
||||
}, new GPXStatistics());
|
||||
}, new GPXGlobalStatistics());
|
||||
}
|
||||
return {
|
||||
time: statistics.global.time.total === 0,
|
||||
hr: statistics.global.hr.count === 0,
|
||||
cad: statistics.global.cad.count === 0,
|
||||
atemp: statistics.global.atemp.count === 0,
|
||||
power: statistics.global.power.count === 0,
|
||||
extensions: Object.keys(statistics.global.extensions).length === 0,
|
||||
time: statistics.time.total === 0,
|
||||
hr: statistics.hr.count === 0,
|
||||
cad: statistics.cad.count === 0,
|
||||
atemp: statistics.atemp.count === 0,
|
||||
power: statistics.power.count === 0,
|
||||
extensions: Object.keys(statistics.extensions).length === 0,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -72,18 +72,16 @@
|
||||
}
|
||||
|
||||
let style = node.getStyle(defaultColor);
|
||||
style.color.forEach((c) => {
|
||||
if (!colors.includes(c)) {
|
||||
colors.push(c);
|
||||
}
|
||||
});
|
||||
colors = style.color;
|
||||
} else if (node instanceof Track) {
|
||||
let style = node.getStyle();
|
||||
if (style) {
|
||||
if (style['gpx_style:color'] && !colors.includes(style['gpx_style:color'])) {
|
||||
if (
|
||||
style &&
|
||||
style['gpx_style:color'] &&
|
||||
!colors.includes(style['gpx_style:color'])
|
||||
) {
|
||||
colors.push(style['gpx_style:color']);
|
||||
}
|
||||
}
|
||||
if (colors.length === 0) {
|
||||
let layer = gpxLayers.getLayer(item.getFileId());
|
||||
if (layer) {
|
||||
|
||||
@@ -101,23 +101,17 @@ export class DistanceMarkers {
|
||||
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
|
||||
let statistics = get(gpxStatistics);
|
||||
|
||||
let features = [];
|
||||
let features: GeoJSON.Feature[] = [];
|
||||
let currentTargetDistance = 1;
|
||||
for (let i = 0; i < statistics.local.distance.total.length; i++) {
|
||||
if (
|
||||
statistics.local.distance.total[i] >=
|
||||
getConvertedDistanceToKilometers(currentTargetDistance)
|
||||
) {
|
||||
statistics.forEachTrackPoint((trkpt, dist) => {
|
||||
if (dist >= getConvertedDistanceToKilometers(currentTargetDistance)) {
|
||||
let distance = currentTargetDistance.toFixed(0);
|
||||
let level = levels.find((level) => currentTargetDistance % level === 0) || 1;
|
||||
features.push({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [
|
||||
statistics.local.points[i].getLongitude(),
|
||||
statistics.local.points[i].getLatitude(),
|
||||
],
|
||||
coordinates: [trkpt.getLongitude(), trkpt.getLatitude()],
|
||||
},
|
||||
properties: {
|
||||
distance,
|
||||
@@ -126,7 +120,7 @@ export class DistanceMarkers {
|
||||
} as GeoJSON.Feature);
|
||||
currentTargetDistance += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
|
||||
@@ -34,13 +34,20 @@ export class StartEndMarkers {
|
||||
if (!map_) return;
|
||||
|
||||
const tool = get(currentTool);
|
||||
const statistics = get(slicedGPXStatistics)?.[0] ?? get(gpxStatistics);
|
||||
const statistics = get(gpxStatistics);
|
||||
const slicedStatistics = get(slicedGPXStatistics);
|
||||
const hidden = get(allHidden);
|
||||
if (statistics.local.points.length > 0 && tool !== Tool.ROUTING && !hidden) {
|
||||
this.start.setLngLat(statistics.local.points[0].getCoordinates()).addTo(map_);
|
||||
if (statistics.global.length > 0 && tool !== Tool.ROUTING && !hidden) {
|
||||
this.start
|
||||
.setLngLat(
|
||||
statistics.getTrackPoint(slicedStatistics?.[1] ?? 0)!.trkpt.getCoordinates()
|
||||
)
|
||||
.addTo(map_);
|
||||
this.end
|
||||
.setLngLat(
|
||||
statistics.local.points[statistics.local.points.length - 1].getCoordinates()
|
||||
statistics
|
||||
.getTrackPoint(slicedStatistics?.[2] ?? statistics.global.length - 1)!
|
||||
.trkpt.getCoordinates()
|
||||
)
|
||||
.addTo(map_);
|
||||
} else {
|
||||
|
||||
@@ -101,9 +101,7 @@
|
||||
acc: Record<string, ImportSpecification>,
|
||||
imprt: ImportSpecification
|
||||
) => {
|
||||
if (
|
||||
!['basemap', 'overlays', 'glyphs-and-sprite'].includes(imprt.id)
|
||||
) {
|
||||
if (!['basemap', 'overlays'].includes(imprt.id)) {
|
||||
acc[imprt.id] = imprt;
|
||||
}
|
||||
return acc;
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if ($selectedBasemapTree && $currentBasemap) {
|
||||
if (open && $selectedBasemapTree && $currentBasemap) {
|
||||
if (!isSelected($selectedBasemapTree, $currentBasemap)) {
|
||||
if (!isSelected($selectedBasemapTree, defaultBasemap)) {
|
||||
$selectedBasemapTree = toggle($selectedBasemapTree, defaultBasemap);
|
||||
@@ -65,7 +65,7 @@
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if ($selectedOverlayTree) {
|
||||
if (open && $selectedOverlayTree) {
|
||||
untrack(() => {
|
||||
if ($currentOverlays) {
|
||||
let overlayLayers = getLayers($currentOverlays);
|
||||
@@ -86,7 +86,7 @@
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if ($selectedOverpassTree) {
|
||||
if (open && $selectedOverpassTree) {
|
||||
untrack(() => {
|
||||
if ($currentOverpassQueries) {
|
||||
let overlayLayers = getLayers($currentOverpassQueries);
|
||||
|
||||
@@ -35,17 +35,6 @@ export class MapboxGLMap {
|
||||
sources: {},
|
||||
layers: [],
|
||||
imports: [
|
||||
{
|
||||
id: 'glyphs-and-sprite', // make Mapbox glyphs and sprite available to other styles
|
||||
url: '',
|
||||
data: {
|
||||
version: 8,
|
||||
sources: {},
|
||||
layers: [],
|
||||
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
|
||||
sprite: 'mapbox://sprites/mapbox/outdoors-v12',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'basemap',
|
||||
url: '',
|
||||
@@ -163,6 +152,12 @@ export class MapboxGLMap {
|
||||
}
|
||||
});
|
||||
});
|
||||
map.on('style.import.load', () => {
|
||||
const basemap = map.getStyle().imports?.find((imprt) => imprt.id === 'basemap');
|
||||
if (basemap && basemap.data && basemap.data.glyphs) {
|
||||
map.setGlyphsUrl(basemap.data.glyphs);
|
||||
}
|
||||
});
|
||||
map.on('load', () => {
|
||||
this._map.set(map); // only set the store after the map has loaded
|
||||
window._map = map; // entry point for extensions
|
||||
|
||||
@@ -28,17 +28,15 @@ export class ReducedGPXLayer {
|
||||
|
||||
update() {
|
||||
const file = this._fileState.file;
|
||||
const stats = this._fileState.statistics;
|
||||
if (!file || !stats) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||
let segmentItem = new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex);
|
||||
let statistics = stats.getStatisticsFor(segmentItem);
|
||||
this._updateSimplified(segmentItem.getFullId(), [
|
||||
segmentItem,
|
||||
statistics.local.points.length,
|
||||
ramerDouglasPeucker(statistics.local.points, minTolerance),
|
||||
segment.trkpt.length,
|
||||
ramerDouglasPeucker(segment.trkpt, minTolerance),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -793,24 +793,25 @@ export class RoutingControls {
|
||||
replacingDistance +=
|
||||
distance(response[i - 1].getCoordinates(), response[i].getCoordinates()) / 1000;
|
||||
}
|
||||
let startAnchorStats = stats.getTrackPoint(anchors[0].point._data.index)!;
|
||||
let endAnchorStats = stats.getTrackPoint(
|
||||
anchors[anchors.length - 1].point._data.index
|
||||
)!;
|
||||
|
||||
let replacedDistance =
|
||||
stats.local.distance.moving[anchors[anchors.length - 1].point._data.index] -
|
||||
stats.local.distance.moving[anchors[0].point._data.index];
|
||||
endAnchorStats.distance.moving - startAnchorStats.distance.moving;
|
||||
|
||||
let newDistance = stats.global.distance.moving + replacingDistance - replacedDistance;
|
||||
let newTime = (newDistance / stats.global.speed.moving) * 3600;
|
||||
|
||||
let remainingTime =
|
||||
stats.global.time.moving -
|
||||
(stats.local.time.moving[anchors[anchors.length - 1].point._data.index] -
|
||||
stats.local.time.moving[anchors[0].point._data.index]);
|
||||
(endAnchorStats.time.moving - startAnchorStats.time.moving);
|
||||
let replacingTime = newTime - remainingTime;
|
||||
|
||||
if (replacingTime <= 0) {
|
||||
// Fallback to simple time difference
|
||||
replacingTime =
|
||||
stats.local.time.total[anchors[anchors.length - 1].point._data.index] -
|
||||
stats.local.time.total[anchors[0].point._data.index];
|
||||
replacingTime = endAnchorStats.time.total - startAnchorStats.time.total;
|
||||
}
|
||||
|
||||
speed = (replacingDistance / replacingTime) * 3600;
|
||||
@@ -820,9 +821,7 @@ export class RoutingControls {
|
||||
let endIndex = anchors[anchors.length - 1].point._data.index;
|
||||
startTime = new Date(
|
||||
(segment.trkpt[endIndex].time?.getTime() ?? 0) -
|
||||
(replacingTime +
|
||||
stats.local.time.total[endIndex] -
|
||||
stats.local.time.moving[endIndex]) *
|
||||
(replacingTime + endAnchorStats.time.total - endAnchorStats.time.moving) *
|
||||
1000
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,12 +26,10 @@
|
||||
|
||||
let validSelection = $derived(
|
||||
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
|
||||
$gpxStatistics.local.points.length > 0
|
||||
$gpxStatistics.global.length > 0
|
||||
);
|
||||
let maxSliderValue = $derived(
|
||||
validSelection && $gpxStatistics.local.points.length > 0
|
||||
? $gpxStatistics.local.points.length - 1
|
||||
: 1
|
||||
validSelection && $gpxStatistics.global.length > 0 ? $gpxStatistics.global.length - 1 : 1
|
||||
);
|
||||
let sliderValues = $derived([0, maxSliderValue]);
|
||||
let canCrop = $derived(sliderValues[0] != 0 || sliderValues[1] != maxSliderValue);
|
||||
@@ -45,7 +43,7 @@
|
||||
function updateSlicedGPXStatistics() {
|
||||
if (validSelection && canCrop) {
|
||||
$slicedGPXStatistics = [
|
||||
get(gpxStatistics).slice(sliderValues[0], sliderValues[1]),
|
||||
get(gpxStatistics).sliced(sliderValues[0], sliderValues[1]),
|
||||
sliderValues[0],
|
||||
sliderValues[1],
|
||||
];
|
||||
|
||||
@@ -50,7 +50,7 @@ Facendo clic destro su una scheda file, è possibile accedere alle stesse azioni
|
||||
|
||||
Come accennato nella [sezione opzioni di visualizzazione](./menu/view), è possibile passare a un layout ad albero per l'elenco dei file.
|
||||
Questo layout è ideale per gestire un gran numero di file aperti, organizzandoli in una lista verticale sul lato destro della mappa.
|
||||
Inoltre, la vista ad albero dei file consente di ispezionare [tracce, segmenti e punti di interesse](./gpx) all'interno dei file attraverso sezioni espandibili.
|
||||
Inoltre, la vista ad albero dei file consente d'ispezionare [tracce, segmenti e punti di interesse](./gpx) all'interno dei file attraverso sezioni espandibili.
|
||||
|
||||
Puoi anche applicare [modifiche](./menu/edit) e [strumenti](./toolbar) agli elementi interni del file.
|
||||
Inoltre, è possibile trascinare e rilasciare gli elementi per riordinarli, o spostarli nella gerarchia o anche in un altro file.
|
||||
@@ -78,7 +78,7 @@ Quando si passa sopra il profilo di elevazione, un suggerimento mostrerà le sta
|
||||
Per ottenere le statistiche per una sezione specifica del profilo di elevazione, è possibile trascinare un rettangolo di selezione sul profilo.
|
||||
Fare clic sul profilo per resettare la selezione.
|
||||
|
||||
È inoltre possibile utilizzare la rotellina del mouse per ingrandire e rimpicciolire sul profilo di elevazione, e spostarsi a sinistra e a destra trascinando il profilo tenendo premuto il tasto <kbd>Maiusc</kbd>.
|
||||
È inoltre possibile utilizzare la rotellina del mouse per ingrandire e rimpicciolire sul profilo di elevazione, e spostarsi a sinistra e a destra trascinando il profilo tenendo premuto il tasto <kbd>Maiuscolo</kbd>.
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
|
||||
@@ -21,7 +21,7 @@ Queste sono organizzate in una struttura gerarchica, con le tracce stesse al liv
|
||||
- Una **traccia** è composta da una sequenza di segmenti scollegati.
|
||||
Inoltre, può contenere metadati come un **nome**, una **descrizione**, e **proprietà di visualizzazione**.
|
||||
- Un **segmento** è una sequenza di punti GPS che formano un percorso continuo.
|
||||
- Un **punto GPS** è una posizione con una latitudine, una longitudine, ed eventualmente un timestamp e un'altitudine.
|
||||
- Un **punto GPS** è una posizione con una latitudine, una longitudine, ed eventualmente una marcatura temporale e un'altitudine.
|
||||
Alcuni dispositivi memorizzano anche informazioni aggiuntive come frequenza cardiaca, cadenza, temperatura e potenza.
|
||||
|
||||
Nella maggior parte dei casi, i file GPX contengono una singola traccia con un singolo segmento.
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Aiuta a mantenere il sito gratuito (e senza pubblicità)
|
||||
|
||||
Ogni volta che aggiungi o sposti i punti GPS, i nostri server calcolano il percorso migliore sulla rete stradale.
|
||||
Utilizziamo anche le API di <a href="https://mapbox.com" target="_blank">Mapbox</a> per visualizzare mappe gradevoli, recuperare i dati altimetrici e consentire la ricerca di luoghi.
|
||||
Utilizziamo anche le API di <a href="https://mapbox.com" target="_blank">Mapbox</a> per visualizzare mappe stupende, recuperare i dati altimetrici e consentire la ricerca di luoghi.
|
||||
|
||||
Sfortunatamente, questo è costoso.
|
||||
Sfortunatamente, fare tutto ciò è costoso.
|
||||
Se ti piace utilizzare questo strumento e lo trovi utile, per favore considera di fare una piccola donazione per aiutare a mantenere il sito web gratuito e senza pubblicità.
|
||||
|
||||
Grazie mille per il vostro supporto! ❤️
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { HeartHandshake } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Hãy giúp duy trì trang web miễn phí (và không có quảng cáo)
|
||||
|
||||
Khi bạn thêm hoặc di chuyển các điểm định vị, máy chủ của chúng tôi sẽ tính toán đoạn đường tốt nhất trên mạng lưới giao thông.
|
||||
Chúng tôi cũng sử dụng các API từ <a href="https://mapbox.com" target="_blank">Mapbox</a> để hiển thị đa dạng các bản đồ, lưu trữ các dữ liệu độ cao cũng như giúp bạn có thể tìm kiếm các địa điểm khác nhau.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Mapbox is the company that provides some of the beautiful maps on this website.
|
||||
They also develop the <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">map engine</a> which powers **gpx.studio**.
|
||||
Mapbox là công ty cung cấp một số bản đồ đẹp trên trang web này.
|
||||
Họ cũng phát triển <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">công cụ bản đồ</a> cung cấp sức mạnh cho **gpx.studio**.
|
||||
|
||||
We are incredibly fortunate and grateful to be part of their <a href="https://mapbox.com/community" target="_blank">Community</a> program, which supports nonprofits, educational institutions, and positive impact organizations.
|
||||
This partnership allows **gpx.studio** to benefit from Mapbox tools at discounted prices, greatly contributing to the financial viability of the project and enabling us to offer the best possible user experience.
|
||||
Chúng tôi vô cùng may mắn và biết ơn khi được tham gia chương trình <a href="https://mapbox.com/community" target="_blank">Cộng đồng</a> của họ, chương trình hỗ trợ các tổ chức phi lợi nhuận, các tổ chức giáo dục và các tổ chức tạo ra tác động tích cực.
|
||||
Sự hợp tác này cho phép **gpx.studio** được hưởng lợi từ các công cụ của Mapbox với giá ưu đãi, góp phần đáng kể vào tính khả thi về tài chính của dự án và giúp chúng tôi mang đến trải nghiệm người dùng tốt nhất có thể.
|
||||
|
||||
@@ -9,8 +9,8 @@ title: Edit actions
|
||||
|
||||
# { title }
|
||||
|
||||
Unlike the file actions, the edit actions can potentially modify the content of the currently selected files.
|
||||
Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, and points of interest](../gpx).
|
||||
Không giống như các thao tác trên tệp, các thao tác chỉnh sửa có thể thay đổi nội dung của các tệp hiện đang được chọn.
|
||||
Hơn nữa, khi bố cục dạng cây của danh sách tệp được bật (xem [Tệp và thống kê](../files-and-stats)), chúng cũng có thể được áp dụng cho [đường đi, đoạn đường và điểm quan tâm](../gpx).
|
||||
Therefore, we will refer to the elements that can be modified by these actions as _file items_.
|
||||
Note that except for the undo and redo actions, the edit actions are also accessible through the context menu (right-click) of the file items.
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ title: 文件
|
||||
|
||||
创建当前选中文件的副本。
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> 删除
|
||||
|
||||
Delete the currently selected files.
|
||||
删除当前选中的文件。
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> 删除全部
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ export const fileActions = {
|
||||
reverseSelection: () => {
|
||||
if (
|
||||
!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) ||
|
||||
get(gpxStatistics).local.points?.length <= 1
|
||||
get(gpxStatistics).global.length <= 1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -345,19 +345,20 @@ export const fileActions = {
|
||||
let startTime: Date | undefined = undefined;
|
||||
if (speed !== undefined) {
|
||||
if (
|
||||
statistics.local.points.length > 0 &&
|
||||
statistics.local.points[0].time !== undefined
|
||||
statistics.global.length > 0 &&
|
||||
statistics.getTrackPoint(0)!.trkpt.time !== undefined
|
||||
) {
|
||||
startTime = statistics.local.points[0].time;
|
||||
startTime = statistics.getTrackPoint(0)!.trkpt.time;
|
||||
} else {
|
||||
let index = statistics.local.points.findIndex(
|
||||
(point) => point.time !== undefined
|
||||
);
|
||||
if (index !== -1 && statistics.local.points[index].time) {
|
||||
for (let i = 0; i < statistics.global.length; i++) {
|
||||
const point = statistics.getTrackPoint(i)!;
|
||||
if (point.trkpt.time !== undefined) {
|
||||
startTime = new Date(
|
||||
statistics.local.points[index].time.getTime() -
|
||||
(1000 * 3600 * statistics.local.distance.total[index]) / speed
|
||||
point.trkpt.time.getTime() -
|
||||
(1000 * 3600 * point.distance.total) / speed
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ListItem, ListLevel } from '$lib/components/file-list/file-list';
|
||||
import { GPXFile, GPXStatistics, type Track } from 'gpx';
|
||||
import { GPXFile, GPXStatistics, GPXStatisticsGroup, type Track } from 'gpx';
|
||||
|
||||
export class GPXStatisticsTree {
|
||||
level: ListLevel;
|
||||
@@ -21,35 +21,26 @@ export class GPXStatisticsTree {
|
||||
}
|
||||
}
|
||||
|
||||
getStatisticsFor(item: ListItem): GPXStatistics {
|
||||
let statistics = [];
|
||||
getStatisticsFor(item: ListItem): GPXStatisticsGroup {
|
||||
let statistics = new GPXStatisticsGroup();
|
||||
let id = item.getIdAtLevel(this.level);
|
||||
if (id === undefined || id === 'waypoints') {
|
||||
Object.keys(this.statistics).forEach((key) => {
|
||||
if (this.statistics[key] instanceof GPXStatistics) {
|
||||
statistics.push(this.statistics[key]);
|
||||
statistics.add(this.statistics[key]);
|
||||
} else {
|
||||
statistics.push(this.statistics[key].getStatisticsFor(item));
|
||||
statistics.add(this.statistics[key].getStatisticsFor(item));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let child = this.statistics[id];
|
||||
if (child instanceof GPXStatistics) {
|
||||
statistics.push(child);
|
||||
statistics.add(child);
|
||||
} else if (child !== undefined) {
|
||||
statistics.push(child.getStatisticsFor(item));
|
||||
statistics.add(child.getStatisticsFor(item));
|
||||
}
|
||||
}
|
||||
if (statistics.length === 0) {
|
||||
return new GPXStatistics();
|
||||
} else if (statistics.length === 1) {
|
||||
return statistics[0];
|
||||
} else {
|
||||
return statistics.reduce((acc, curr) => {
|
||||
acc.mergeWith(curr);
|
||||
return acc;
|
||||
}, new GPXStatistics());
|
||||
}
|
||||
return statistics;
|
||||
}
|
||||
}
|
||||
export type GPXFileWithStatistics = { file: GPXFile; statistics: GPXStatisticsTree };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { selection } from '$lib/logic/selection';
|
||||
import { GPXStatistics } from 'gpx';
|
||||
import { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
||||
import { fileStateCollection, GPXFileState } from '$lib/logic/file-state';
|
||||
import {
|
||||
ListFileItem,
|
||||
@@ -12,7 +12,7 @@ import { settings } from '$lib/logic/settings';
|
||||
const { fileOrder } = settings;
|
||||
|
||||
export class SelectedGPXStatistics {
|
||||
private _statistics: Writable<GPXStatistics>;
|
||||
private _statistics: Writable<GPXStatisticsGroup>;
|
||||
private _files: Map<
|
||||
string,
|
||||
{
|
||||
@@ -22,18 +22,21 @@ export class SelectedGPXStatistics {
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
this._statistics = writable(new GPXStatistics());
|
||||
this._statistics = writable(new GPXStatisticsGroup());
|
||||
this._files = new Map();
|
||||
selection.subscribe(() => this.update());
|
||||
fileOrder.subscribe(() => this.update());
|
||||
}
|
||||
|
||||
subscribe(run: (value: GPXStatistics) => void, invalidate?: (value?: GPXStatistics) => void) {
|
||||
subscribe(
|
||||
run: (value: GPXStatisticsGroup) => void,
|
||||
invalidate?: (value?: GPXStatisticsGroup) => void
|
||||
) {
|
||||
return this._statistics.subscribe(run, invalidate);
|
||||
}
|
||||
|
||||
update() {
|
||||
let statistics = new GPXStatistics();
|
||||
let statistics = new GPXStatisticsGroup();
|
||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||
let stats = fileStateCollection.getStatistics(fileId);
|
||||
if (stats) {
|
||||
@@ -43,7 +46,7 @@ export class SelectedGPXStatistics {
|
||||
!(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) ||
|
||||
first
|
||||
) {
|
||||
statistics.mergeWith(stats.getStatisticsFor(item));
|
||||
statistics.add(stats.getStatisticsFor(item));
|
||||
first = false;
|
||||
}
|
||||
});
|
||||
@@ -76,7 +79,7 @@ export class SelectedGPXStatistics {
|
||||
|
||||
export const gpxStatistics = new SelectedGPXStatistics();
|
||||
|
||||
export const slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined> =
|
||||
export const slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined> =
|
||||
writable(undefined);
|
||||
|
||||
gpxStatistics.subscribe(() => {
|
||||
|
||||
@@ -229,6 +229,9 @@ export function getConvertedVelocity(
|
||||
}
|
||||
}
|
||||
|
||||
export function getConvertedTemperature(value: number) {
|
||||
return get(temperatureUnits) === 'celsius' ? value : celsiusToFahrenheit(value);
|
||||
export function getConvertedTemperature(
|
||||
value: number,
|
||||
targetTemperatureUnits = get(temperatureUnits)
|
||||
) {
|
||||
return targetTemperatureUnits === 'celsius' ? value : celsiusToFahrenheit(value);
|
||||
}
|
||||
|
||||
@@ -473,7 +473,7 @@
|
||||
},
|
||||
"homepage": {
|
||||
"website": "Webseite",
|
||||
"home": "Zuhause",
|
||||
"home": "Startseite",
|
||||
"app": "App",
|
||||
"contact": "Kontakt",
|
||||
"reddit": "Reddit",
|
||||
|
||||
@@ -324,6 +324,7 @@
|
||||
"bgMountains": "BGMountains",
|
||||
"usgs": "USGS",
|
||||
"bikerouterGravel": "bikerouter.de Gravel",
|
||||
"openRailwayMap": "OpenRailwayMap",
|
||||
"cyclOSMlite": "CyclOSM Lite",
|
||||
"swisstopoSlope": "swisstopo Slope",
|
||||
"swisstopoHiking": "swisstopo Hiking",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"metadata": {
|
||||
"home_title": "el editor online de archivos GPX",
|
||||
"app_title": "app",
|
||||
"embed_title": "El editor online de archivos GPX",
|
||||
"embed_title": " editor online de archivos GPX",
|
||||
"help_title": "ayuda",
|
||||
"404_title": "página no encontrada",
|
||||
"description": "Mira, edita y crea archivos GPX online con planificación avanzada de rutas y herramientas de procesamiento de archivos, bonitos mapas y visualizaciones detalladas de datos."
|
||||
|
||||
@@ -350,7 +350,7 @@
|
||||
"eat-and-drink": "Nourriture et boissons",
|
||||
"amenities": "Commodités",
|
||||
"toilets": "Toilettes",
|
||||
"water": "Cours d'eau",
|
||||
"water": "Eau potable",
|
||||
"shower": "Douche",
|
||||
"shelter": "Abri",
|
||||
"cemetery": "Cimetière",
|
||||
@@ -443,7 +443,7 @@
|
||||
"convenience_store": "Épicerie",
|
||||
"crossing": "Croisement",
|
||||
"department_store": "Grand magasin",
|
||||
"drinking_water": "Cours d'eau",
|
||||
"drinking_water": "Eau potable",
|
||||
"exit": "Sortie",
|
||||
"lodge": "Refuge",
|
||||
"lodging": "Hébergement",
|
||||
@@ -468,7 +468,7 @@
|
||||
"summit": "Sommet",
|
||||
"telephone": "Téléphone",
|
||||
"tunnel": "Tunnel",
|
||||
"water_source": "Source d'eau"
|
||||
"water_source": "Point d'eau"
|
||||
}
|
||||
},
|
||||
"homepage": {
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
"street_view_source": "Sorgente della vista stradale",
|
||||
"mapillary": "Mapillary",
|
||||
"google": "Google",
|
||||
"toggle_street_view": "Street View",
|
||||
"toggle_street_view": "Vista stradale",
|
||||
"layers": "Livelli della mappa...",
|
||||
"distance_markers": "Indicatori di distanza",
|
||||
"direction_markers": "Frecce direzionali",
|
||||
@@ -87,7 +87,7 @@
|
||||
"tooltip": "Pianifica o modifica un percorsoo",
|
||||
"activity": "Attività",
|
||||
"use_routing": "Instradamento",
|
||||
"use_routing_tooltip": "Collega i punti di ancoraggio tramite la rete stradale o in linea retta se disabilitato",
|
||||
"use_routing_tooltip": "Collega i punti di ancoraggio tramite la rete stradale (o in linea retta se disabilitato)",
|
||||
"allow_private": "Consenti strade private",
|
||||
"reverse": {
|
||||
"button": "Inverti la traccia",
|
||||
@@ -235,18 +235,18 @@
|
||||
"help_no_selection": "Seleziona un file per richiedere i dati di altitudine."
|
||||
},
|
||||
"waypoint": {
|
||||
"tooltip": "Creare e modificare punti di interesse",
|
||||
"tooltip": "Creare e modificare punti d'interesse",
|
||||
"icon": "Icona",
|
||||
"link": "Collegamento",
|
||||
"longitude": "Longitudine",
|
||||
"latitude": "Latitudine",
|
||||
"create": "Creare un punto di interesse",
|
||||
"add": "Aggiungi punto di interesse al file",
|
||||
"help": "Compila il modulo per creare un nuovo punto di interesse, oppure fai clic su uno esistente per modificarlo. Fare clic sulla mappa per inserire le coordinate o trascinare i punti di interesse per spostarli.",
|
||||
"help_no_selection": "Selezionare un file per creare o modificare punti di interesse."
|
||||
"create": "Creare un punto d'interesse",
|
||||
"add": "Aggiungi punto d'interesse al file",
|
||||
"help": "Compila il modulo per creare un nuovo punto d'interesse, oppure fai clic su uno esistente per modificarlo. Fare clic sulla mappa per inserire le coordinate o trascinare i punti d'interesse per spostarli.",
|
||||
"help_no_selection": "Selezionare un file per creare o modificare punti d'interesse."
|
||||
},
|
||||
"reduce": {
|
||||
"tooltip": "Riduci il numero di punti della traccia",
|
||||
"tooltip": "Riduci il numero di punti GPS",
|
||||
"tolerance": "Tolleranza",
|
||||
"number_of_points": "Numero di punti GPS",
|
||||
"button": "Minimizza",
|
||||
@@ -254,14 +254,14 @@
|
||||
"help_no_selection": "Selezionare una traccia per ridurre il numero dei suoi punti GPS."
|
||||
},
|
||||
"clean": {
|
||||
"tooltip": "Pulire i punti GPS e i punti di interesse con una selezione rettangolare",
|
||||
"tooltip": "Pulire i punti GPS e i punti d'interesse con una selezione rettangolare",
|
||||
"delete_trackpoints": "Eliminare punti GPS",
|
||||
"delete_waypoints": "Cancella punti d'interesse",
|
||||
"delete_inside": "Elimina all'interno della selezione",
|
||||
"delete_outside": "Elimina fuori dalla selezione",
|
||||
"button": "Elimina",
|
||||
"help": "Selezionare un'area rettangolare sulla mappa per rimuovere i punti GPS e i punti di interesse.",
|
||||
"help_no_selection": "Seleziona una traccia per pulire i punti GPS e i punti di interesse."
|
||||
"help": "Selezionare un'area rettangolare sulla mappa per rimuovere i punti GPS e i punti d'interesse.",
|
||||
"help_no_selection": "Seleziona una traccia per pulire i punti GPS e i punti d'interesse."
|
||||
}
|
||||
},
|
||||
"layers": {
|
||||
@@ -273,7 +273,7 @@
|
||||
"new": "Nuovo livello personalizzato",
|
||||
"edit": "Modifica livello personalizzato",
|
||||
"urls": "URL(s)",
|
||||
"url_placeholder": "WMTS, WMS o Mapbox stile JSON",
|
||||
"url_placeholder": "WMTS, WMS o JSON in stile Mapbox",
|
||||
"max_zoom": "Zoom massimo",
|
||||
"layer_type": "Tipo del layer",
|
||||
"basemap": "Mappa Base",
|
||||
@@ -309,7 +309,7 @@
|
||||
"linz": "LINZ Topo",
|
||||
"linzTopo": "LINZ Topo50",
|
||||
"swisstopoRaster": "swisstopo Raster",
|
||||
"swisstopoVector": "Swisstopo Vector",
|
||||
"swisstopoVector": "swisstopo Vector",
|
||||
"swisstopoSatellite": "swisstopo Satellite",
|
||||
"ignBe": "IGN Topo",
|
||||
"ignFrPlan": "IGN Plan",
|
||||
@@ -318,25 +318,25 @@
|
||||
"ignFrSatellite": "Satellitare IGN",
|
||||
"ignEs": "IGN Topo",
|
||||
"ignEsSatellite": "Satellitare IGN",
|
||||
"ordnanceSurvey": "Sondaggio Ordnance",
|
||||
"ordnanceSurvey": "Ordnance Survey",
|
||||
"norwayTopo": "Topografisk Norgeskart 4",
|
||||
"finlandTopo": "Carta topografica del vecchio Catasto svedese",
|
||||
"finlandTopo": "Lantmäteriverket Terrängkarta",
|
||||
"bgMountains": "BGMountains",
|
||||
"usgs": "USGS",
|
||||
"bikerouterGravel": "bikerouter.de Gravel",
|
||||
"cyclOSMlite": "CyclOSM Lite",
|
||||
"swisstopoSlope": "Carta topografica Svizzera Pendenza",
|
||||
"swisstopoHiking": "Carta topografica Svedese Escursione",
|
||||
"swisstopoHikingClosures": "Carta topografica Svizzera Fine escursione",
|
||||
"swisstopoCycling": "Carta topografica Svizzera Ciclabile",
|
||||
"swisstopoCyclingClosures": "Carta topografica Svizzera fine ciclabile",
|
||||
"swisstopoMountainBike": "Carta topografica Svizzera MTB",
|
||||
"swisstopoMountainBikeClosures": "Carta topografica Svizzera fine MTB",
|
||||
"swisstopoSkiTouring": "Carta topografica Svizzera pista sci",
|
||||
"swisstopoSlope": "swisstopo Pendenza",
|
||||
"swisstopoHiking": "swisstopo Escursione",
|
||||
"swisstopoHikingClosures": "swisstopo Fine escursione",
|
||||
"swisstopoCycling": "swisstopo Ciclabile",
|
||||
"swisstopoCyclingClosures": "swisstopo Fine ciclabile",
|
||||
"swisstopoMountainBike": "swisstopo MTB",
|
||||
"swisstopoMountainBikeClosures": "swisstopo Fine MTB",
|
||||
"swisstopoSkiTouring": "swisstopo Sci Alpinismo",
|
||||
"ignFrCadastre": "IGN Catasto",
|
||||
"ignSlope": "Pendenza IGN",
|
||||
"ignSkiTouring": "IGN Sciescursionismo",
|
||||
"waymarked_trails": "Waymarked Trails",
|
||||
"ignSlope": "IGN Pendenza",
|
||||
"ignSkiTouring": "IGN Sci Alpinismo",
|
||||
"waymarked_trails": "Sentieri Segnalati",
|
||||
"waymarkedTrailsHiking": "Escursionismo",
|
||||
"waymarkedTrailsCycling": "Ciclismo",
|
||||
"waymarkedTrailsMTB": "MTB",
|
||||
@@ -406,11 +406,11 @@
|
||||
"feet": "ft",
|
||||
"kilometers": "km",
|
||||
"miles": "mi",
|
||||
"nautical_miles": "nm",
|
||||
"nautical_miles": "NM",
|
||||
"celsius": "°C",
|
||||
"fahrenheit": "°F",
|
||||
"kilometers_per_hour": "km/h",
|
||||
"miles_per_hour": "mph",
|
||||
"miles_per_hour": "mi/h",
|
||||
"minutes_per_kilometer": "min/km",
|
||||
"minutes_per_mile": "min/mi",
|
||||
"minutes_per_nautical_mile": "min/nm",
|
||||
@@ -426,8 +426,8 @@
|
||||
"tracks": "Tracce",
|
||||
"segment": "Segmento",
|
||||
"segments": "Segmenti",
|
||||
"waypoint": "Punto di interesse",
|
||||
"waypoints": "Punti di interesse",
|
||||
"waypoint": "Punto d'interesse",
|
||||
"waypoints": "Punti d'interesse",
|
||||
"symbol": {
|
||||
"alert": "Avviso",
|
||||
"anchor": "Ancora",
|
||||
@@ -483,13 +483,13 @@
|
||||
"email": "Email",
|
||||
"contribute": "Contribuire",
|
||||
"supported_by": "supportato da",
|
||||
"support_button": "Supporto di gpx.studio su Ko-fi",
|
||||
"support_button": "Supporta gpx.studio su Ko-fi",
|
||||
"route_planning": "Pianificazione del percorso",
|
||||
"route_planning_description": "Un'interfaccia intuitiva per creare itinerari su misura per ogni sport, basati sui dati OpenStreetMap.",
|
||||
"route_planning_description": "Un'interfaccia intuitiva per creare itinerari su misura per ogni sport, basata sui dati OpenStreetMap.",
|
||||
"file_processing": "Elaborazione avanzata dei file",
|
||||
"file_processing_description": "Una serie di strumenti per eseguire tutte le attività comuni di elaborazione dei file e che possono essere applicati a più file contemporaneamente.",
|
||||
"maps": "Mappe globali e locali",
|
||||
"maps_description": "Una vasta collezione di mappe di base, sovrapposizioni e punti d'interesse per aiutarti a creare la tua prossima avventura all'aperto o visualizzare il tuo ultimo risultato.",
|
||||
"maps_description": "Una vasta collezione di mappe di base, sovrapposizioni e punti d'interesse per aiutarti a creare la tua prossima avventura all'aperto o visualizzare la tua ultima impresa.",
|
||||
"data_visualization": "Visualizzazione dei dati",
|
||||
"data_visualization_description": "Un profilo di elevazione interattivo con statistiche dettagliate per analizzare attività registrate e obiettivi futuri.",
|
||||
"identity": "Gratuito, senza pubblicità e open source",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"metadata": {
|
||||
"home_title": "edytor online plików GPX",
|
||||
"app_title": "Aplikacja",
|
||||
"embed_title": "Online'owy edytor plików GPX",
|
||||
"embed_title": "Edytor plików GPX online",
|
||||
"help_title": "pomoc",
|
||||
"404_title": "nie odnaleziono strony",
|
||||
"description": "Przeglądaj, edytuj i twórz pliki GPX online z zaawansowanymi możliwościami planowania trasy i narzędziami do przetwarzania plików, pięknymi mapami i szczegółowymi wizualizacjami danych."
|
||||
@@ -28,7 +28,7 @@
|
||||
"undo": "Cofnij",
|
||||
"redo": "Ponów",
|
||||
"delete": "Usuń",
|
||||
"delete_all": "Delete all",
|
||||
"delete_all": "Usuń wszystko",
|
||||
"select_all": "Zaznacz wszystko",
|
||||
"view": "Widok",
|
||||
"elevation_profile": "Profil wysokości",
|
||||
@@ -80,7 +80,7 @@
|
||||
"center": "Wyśrodkuj",
|
||||
"open_in": "Otwórz w",
|
||||
"copy_coordinates": "Kopiuj współrzędne",
|
||||
"edit_osm": "Edit in OpenStreetMap"
|
||||
"edit_osm": "Edytuj w OpenStreetMap"
|
||||
},
|
||||
"toolbar": {
|
||||
"routing": {
|
||||
@@ -353,7 +353,7 @@
|
||||
"water": "Woda",
|
||||
"shower": "Prysznic",
|
||||
"shelter": "Schronienie",
|
||||
"cemetery": "Cemetery",
|
||||
"cemetery": "Cmentarz",
|
||||
"motorized": "Samochody i motocykle",
|
||||
"fuel-station": "Stacja paliw",
|
||||
"parking": "Parking",
|
||||
|
||||
@@ -21,18 +21,18 @@
|
||||
"export_all": "Xuất tất cả...",
|
||||
"export_options": "Tùy chọn xuất",
|
||||
"support_message": "Công cụ này miễn phí, nhưng nó tốn phí để duy trì hoạt động. Nếu bạn dùng công cụ này thường xuyên, bạn có thể xem xét đóng góp để hỗ trợ chúng tôi. Chúng tôi rất biết ơn vì điều đó!",
|
||||
"support_button": "Help keep the website free",
|
||||
"support_button": "Hãy giúp duy trì trang web miễn phí",
|
||||
"download_file": "Tải tệp xuống",
|
||||
"download_files": "Tải xuống tất cả",
|
||||
"edit": "Chỉnh sửa",
|
||||
"undo": "Hoàn tác",
|
||||
"redo": "Khôi phục",
|
||||
"delete": "Xóa",
|
||||
"delete_all": "Delete all",
|
||||
"delete_all": "Xóa tất cả",
|
||||
"select_all": "Chọn tất cả",
|
||||
"view": "Xem",
|
||||
"elevation_profile": "Thông tin độ cao",
|
||||
"tree_file_view": "File tree",
|
||||
"tree_file_view": "Cây thư mục",
|
||||
"switch_basemap": "Quay lại bản đồ trước đó",
|
||||
"toggle_overlays": "Thay đổi lớp phủ",
|
||||
"toggle_3d": "Chuyển đổi 3D",
|
||||
@@ -57,35 +57,35 @@
|
||||
"layers": "Lớp bản đồ...",
|
||||
"distance_markers": "Đánh dấu khoảng cách",
|
||||
"direction_markers": "Mũi tên định hướng",
|
||||
"help": "Help",
|
||||
"more": "More...",
|
||||
"donate": "Donate",
|
||||
"help": "Trợ giúp",
|
||||
"more": "Chi tiết...",
|
||||
"donate": "Ủng hộ",
|
||||
"ctrl": "Ctrl",
|
||||
"click": "Click",
|
||||
"drag": "Drag",
|
||||
"click": "Nhấp chuột",
|
||||
"drag": "Kéo",
|
||||
"metadata": {
|
||||
"button": "Info...",
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
"save": "Save"
|
||||
"button": "Thông tin...",
|
||||
"name": "Tên",
|
||||
"description": "Mô tả",
|
||||
"save": "Lưu"
|
||||
},
|
||||
"style": {
|
||||
"button": "Appearance...",
|
||||
"color": "Color",
|
||||
"opacity": "Opacity",
|
||||
"width": "Width"
|
||||
"button": "Diện mạo...",
|
||||
"color": "Màu sắc",
|
||||
"opacity": "Độ trong suốt",
|
||||
"width": "Độ rộng"
|
||||
},
|
||||
"hide": "Hide",
|
||||
"unhide": "Unhide",
|
||||
"center": "Center",
|
||||
"open_in": "Open in",
|
||||
"copy_coordinates": "Copy coordinates",
|
||||
"edit_osm": "Edit in OpenStreetMap"
|
||||
"hide": "Ẩn",
|
||||
"unhide": "Bỏ ẩn",
|
||||
"center": "Giữa",
|
||||
"open_in": "Mở ra",
|
||||
"copy_coordinates": "Sao chép tọa độ",
|
||||
"edit_osm": ""
|
||||
},
|
||||
"toolbar": {
|
||||
"routing": {
|
||||
"tooltip": "Plan or edit a route",
|
||||
"activity": "Activity",
|
||||
"activity": "Hoạt động",
|
||||
"use_routing": "Routing",
|
||||
"use_routing_tooltip": "Connect anchor points via road network, or in a straight line if disabled",
|
||||
"allow_private": "Allow private roads",
|
||||
@@ -94,14 +94,14 @@
|
||||
"tooltip": "Reverse the direction of the route"
|
||||
},
|
||||
"route_back_to_start": {
|
||||
"button": "Back to start",
|
||||
"button": "Quay về Bắt đầu",
|
||||
"tooltip": "Connect the last point of the route with the starting point"
|
||||
},
|
||||
"round_trip": {
|
||||
"button": "Round trip",
|
||||
"tooltip": "Return to the starting point by the same route"
|
||||
},
|
||||
"start_loop_here": "Start loop here",
|
||||
"start_loop_here": "Bắt đầu lặp ở đây",
|
||||
"help_no_file": "Select a trace to use the routing tool, or click on the map to start creating a new route.",
|
||||
"help": "Click on the map to add a new anchor point, or drag existing ones to change the route.",
|
||||
"activities": {
|
||||
@@ -127,15 +127,15 @@
|
||||
"wood": "Wood",
|
||||
"compacted": "Compacted gravel",
|
||||
"fine_gravel": "Fine gravel",
|
||||
"gravel": "Gravel",
|
||||
"gravel": "Sỏi",
|
||||
"pebblestone": "Pebblestone",
|
||||
"rock": "Rock",
|
||||
"dirt": "Dirt",
|
||||
"rock": "Đá",
|
||||
"dirt": "Đất",
|
||||
"ground": "Ground",
|
||||
"earth": "Earth",
|
||||
"mud": "Mud",
|
||||
"sand": "Sand",
|
||||
"grass": "Grass",
|
||||
"grass": "Cỏ",
|
||||
"grass_paver": "Grass paver",
|
||||
"clay": "Clay",
|
||||
"stone": "Stone"
|
||||
|
||||
@@ -230,8 +230,8 @@
|
||||
"help_invalid_selection": "须先选择包含多个轨迹的文件以提取。"
|
||||
},
|
||||
"elevation": {
|
||||
"button": "请求数据",
|
||||
"help": "请求成功后将使用 Mapbox 海拔数据替换原有数据。",
|
||||
"button": "请求海拔数据",
|
||||
"help": "请求成功后将移除原有的海拔数据,并使用 Mapbox 的海拔数据替换原有数据。",
|
||||
"help_no_selection": "选择要请求海拔数据的文件。"
|
||||
},
|
||||
"waypoint": {
|
||||
|
||||
Reference in New Issue
Block a user