mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-01-15 14:18:41 +00:00
Compare commits
17 Commits
e7a1d0488b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59f31caf26 | ||
|
|
f24956c58d | ||
|
|
9019317e5c | ||
|
|
2a0227c1de | ||
|
|
f70db42b91 | ||
|
|
9cd87742f0 | ||
|
|
5dcb93ca5d | ||
|
|
256d62b29b | ||
|
|
595ea8e2d3 | ||
|
|
d3e733aa3e | ||
|
|
a011768d2d | ||
|
|
4b45b5d716 | ||
|
|
ebe9681c12 | ||
|
|
51c85e4cd5 | ||
|
|
2e171dfbee | ||
|
|
a6a3917986 | ||
|
|
21f2448213 |
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
website/src/lib/components/ui
|
||||||
|
website/src/lib/docs/**/*.mdx
|
||||||
|
**/*.webmanifest
|
||||||
515
gpx/src/gpx.ts
515
gpx/src/gpx.ts
@@ -1,4 +1,5 @@
|
|||||||
import { ramerDouglasPeucker } from './simplify';
|
import { ramerDouglasPeucker } from './simplify';
|
||||||
|
import { GPXStatistics, GPXStatisticsGroup, TrackPointLocalStatistics } from './statistics';
|
||||||
import {
|
import {
|
||||||
Coordinates,
|
Coordinates,
|
||||||
GPXFileAttributes,
|
GPXFileAttributes,
|
||||||
@@ -36,7 +37,6 @@ export abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
|
|||||||
abstract getNumberOfTrackPoints(): number;
|
abstract getNumberOfTrackPoints(): number;
|
||||||
abstract getStartTimestamp(): Date | undefined;
|
abstract getStartTimestamp(): Date | undefined;
|
||||||
abstract getEndTimestamp(): Date | undefined;
|
abstract getEndTimestamp(): Date | undefined;
|
||||||
abstract getStatistics(): GPXStatistics;
|
|
||||||
abstract getSegments(): TrackSegment[];
|
abstract getSegments(): TrackSegment[];
|
||||||
abstract getTrackPoints(): TrackPoint[];
|
abstract getTrackPoints(): TrackPoint[];
|
||||||
|
|
||||||
@@ -76,14 +76,6 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
|
|||||||
return this.children[this.children.length - 1].getEndTimestamp();
|
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[] {
|
getSegments(): TrackSegment[] {
|
||||||
return this.children.flatMap((child) => child.getSegments());
|
return this.children.flatMap((child) => child.getSegments());
|
||||||
}
|
}
|
||||||
@@ -148,7 +140,9 @@ export class GPXFile extends GPXTreeNode<Track> {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.wpt = gpx.wpt ? gpx.wpt.map((waypoint) => new Waypoint(waypoint)) : [];
|
this.wpt = gpx.wpt
|
||||||
|
? gpx.wpt.map((waypoint, index) => new Waypoint(waypoint, index))
|
||||||
|
: [];
|
||||||
this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : [];
|
this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : [];
|
||||||
if (gpx.rte && gpx.rte.length > 0) {
|
if (gpx.rte && gpx.rte.length > 0) {
|
||||||
this.trk = this.trk.concat(gpx.rte.map((route) => convertRouteToTrack(route)));
|
this.trk = this.trk.concat(gpx.rte.map((route) => convertRouteToTrack(route)));
|
||||||
@@ -186,9 +180,6 @@ export class GPXFile extends GPXTreeNode<Track> {
|
|||||||
segment._data['segmentIndex'] = segmentIndex;
|
segment._data['segmentIndex'] = segmentIndex;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.wpt.forEach((waypoint, waypointIndex) => {
|
|
||||||
waypoint._data['index'] = waypointIndex;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get children(): Array<Track> {
|
get children(): Array<Track> {
|
||||||
@@ -209,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 {
|
getStyle(defaultColor?: string): MergedLineStyles {
|
||||||
return this.trk
|
const style = this.trk
|
||||||
.map((track) => track.getStyle())
|
.map((track) => track.getStyle())
|
||||||
.reduce(
|
.reduce(
|
||||||
(acc, style) => {
|
(acc, style) => {
|
||||||
@@ -220,8 +219,6 @@ export class GPXFile extends GPXTreeNode<Track> {
|
|||||||
!acc.color.includes(style['gpx_style:color'])
|
!acc.color.includes(style['gpx_style:color'])
|
||||||
) {
|
) {
|
||||||
acc.color.push(style['gpx_style:color']);
|
acc.color.push(style['gpx_style:color']);
|
||||||
} else if (defaultColor && !acc.color.includes(defaultColor)) {
|
|
||||||
acc.color.push(defaultColor);
|
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
style &&
|
style &&
|
||||||
@@ -245,6 +242,10 @@ export class GPXFile extends GPXTreeNode<Track> {
|
|||||||
width: [],
|
width: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
if (style.color.length === 0 && defaultColor) {
|
||||||
|
style.color.push(defaultColor);
|
||||||
|
}
|
||||||
|
return style;
|
||||||
}
|
}
|
||||||
|
|
||||||
clone(): GPXFile {
|
clone(): GPXFile {
|
||||||
@@ -807,7 +808,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
constructor(segment?: (TrackSegmentType & { _data?: any }) | TrackSegment) {
|
constructor(segment?: (TrackSegmentType & { _data?: any }) | TrackSegment) {
|
||||||
super();
|
super();
|
||||||
if (segment) {
|
if (segment) {
|
||||||
this.trkpt = segment.trkpt.map((point) => new TrackPoint(point));
|
this.trkpt = segment.trkpt.map((point, index) => new TrackPoint(point, index));
|
||||||
if (segment.hasOwnProperty('_data')) {
|
if (segment.hasOwnProperty('_data')) {
|
||||||
this._data = segment._data;
|
this._data = segment._data;
|
||||||
}
|
}
|
||||||
@@ -819,12 +820,12 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
_computeStatistics(): GPXStatistics {
|
_computeStatistics(): GPXStatistics {
|
||||||
let statistics = new GPXStatistics();
|
let statistics = new GPXStatistics();
|
||||||
|
|
||||||
statistics.local.points = this.trkpt.map((point) => point);
|
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;
|
const points = this.trkpt;
|
||||||
for (let i = 0; i < points.length; i++) {
|
for (let i = 0; i < points.length; i++) {
|
||||||
points[i]._data['index'] = i;
|
|
||||||
|
|
||||||
// distance
|
// distance
|
||||||
let dist = 0;
|
let dist = 0;
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
@@ -833,19 +834,18 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
statistics.global.distance.total += dist;
|
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
|
// time
|
||||||
if (points[i].time === undefined) {
|
if (points[i].time === undefined) {
|
||||||
statistics.local.time.total.push(0);
|
statistics.local.data[i].time.total = 0;
|
||||||
} else {
|
} else {
|
||||||
if (statistics.global.time.start === undefined) {
|
if (statistics.global.time.start === undefined) {
|
||||||
statistics.global.time.start = points[i].time;
|
statistics.global.time.start = points[i].time;
|
||||||
}
|
}
|
||||||
statistics.global.time.end = points[i].time;
|
statistics.global.time.end = points[i].time;
|
||||||
statistics.local.time.total.push(
|
statistics.local.data[i].time.total =
|
||||||
(points[i].time.getTime() - statistics.global.time.start.getTime()) / 1000
|
(points[i].time.getTime() - statistics.global.time.start.getTime()) / 1000;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// speed
|
// speed
|
||||||
@@ -860,8 +860,8 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statistics.local.distance.moving.push(statistics.global.distance.moving);
|
statistics.local.data[i].distance.moving = statistics.global.distance.moving;
|
||||||
statistics.local.time.moving.push(statistics.global.time.moving);
|
statistics.local.data[i].time.moving = statistics.global.time.moving;
|
||||||
|
|
||||||
// bounds
|
// bounds
|
||||||
statistics.global.bounds.southWest.lat = Math.min(
|
statistics.global.bounds.southWest.lat = Math.min(
|
||||||
@@ -961,13 +961,22 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
? statistics.global.distance.moving / (statistics.global.time.moving / 3600)
|
? statistics.global.distance.moving / (statistics.global.time.moving / 3600)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
statistics.local.speed = timeWindowSmoothing(points, 10000, (start, end) =>
|
timeWindowSmoothing(
|
||||||
|
points,
|
||||||
|
10000,
|
||||||
|
(start, end) =>
|
||||||
points[start].time && points[end].time
|
points[start].time && points[end].time
|
||||||
? (3600 *
|
? (3600 *
|
||||||
(statistics.local.distance.total[end] -
|
(statistics.local.data[end].distance.total -
|
||||||
statistics.local.distance.total[start])) /
|
statistics.local.data[start].distance.total)) /
|
||||||
Math.max((points[end].time.getTime() - points[start].time.getTime()) / 1000, 1)
|
Math.max(
|
||||||
: undefined
|
(points[end].time.getTime() - points[start].time.getTime()) / 1000,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
(value, index) => {
|
||||||
|
statistics.local.data[index].speed = value;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return statistics;
|
return statistics;
|
||||||
@@ -987,7 +996,13 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
let cumulEle = 0;
|
let cumulEle = 0;
|
||||||
let currentStart = start;
|
let currentStart = start;
|
||||||
let currentEnd = 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++) {
|
for (let i = currentStart; i < s; i++) {
|
||||||
cumulEle -= this.trkpt[i].ele ?? 0;
|
cumulEle -= this.trkpt[i].ele ?? 0;
|
||||||
}
|
}
|
||||||
@@ -997,43 +1012,49 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
currentStart = s;
|
currentStart = s;
|
||||||
currentEnd = e + 1;
|
currentEnd = e + 1;
|
||||||
return cumulEle / (e - s + 1);
|
return cumulEle / (e - s + 1);
|
||||||
});
|
},
|
||||||
smoothedEle[0] = this.trkpt[start].ele ?? 0;
|
(smoothedEle, j) => {
|
||||||
smoothedEle[smoothedEle.length - 1] = this.trkpt[end].ele ?? 0;
|
if (j === start) {
|
||||||
|
smoothedEle = this.trkpt[start].ele ?? 0;
|
||||||
for (let j = start; j < end; j++) {
|
prevSmoothedEle = smoothedEle;
|
||||||
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
|
} else if (j === end) {
|
||||||
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
|
smoothedEle = this.trkpt[end].ele ?? 0;
|
||||||
|
}
|
||||||
const ele = smoothedEle[j - start + 1] - smoothedEle[j - start];
|
const ele = smoothedEle - prevSmoothedEle;
|
||||||
if (ele > 0) {
|
if (ele > 0) {
|
||||||
statistics.global.elevation.gain += ele;
|
statistics.global.elevation.gain += ele;
|
||||||
} else if (ele < 0) {
|
} else if (ele < 0) {
|
||||||
statistics.global.elevation.loss -= ele;
|
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++) {
|
for (let i = 0; i < simplified.length - 1; i++) {
|
||||||
let start = simplified[i].point._data.index;
|
let start = simplified[i].point._data.index;
|
||||||
let end = simplified[i + 1].point._data.index;
|
let end = simplified[i + 1].point._data.index;
|
||||||
let dist =
|
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);
|
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++) {
|
for (let j = start; j < end + (i + 1 === simplified.length - 1 ? 1 : 0); j++) {
|
||||||
slope.push((0.1 * ele) / dist);
|
statistics.local.data[j].slope.segment = (0.1 * ele) / dist;
|
||||||
length.push(dist);
|
statistics.local.data[j].slope.length = dist;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statistics.local.slope.segment = slope;
|
distanceWindowSmoothing(
|
||||||
statistics.local.slope.length = length;
|
|
||||||
statistics.local.slope.at = distanceWindowSmoothing(
|
|
||||||
0,
|
0,
|
||||||
this.trkpt.length,
|
this.trkpt.length,
|
||||||
statistics,
|
statistics,
|
||||||
@@ -1041,8 +1062,12 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
(start, end) => {
|
(start, end) => {
|
||||||
const ele = this.trkpt[end].ele - this.trkpt[start].ele || 0;
|
const ele = this.trkpt[end].ele - this.trkpt[start].ele || 0;
|
||||||
const dist =
|
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;
|
return dist > 0 ? (0.1 * ele) / dist : 0;
|
||||||
|
},
|
||||||
|
(value, index) => {
|
||||||
|
statistics.local.data[index].slope.at = value;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1292,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 og = getOriginal(this); // Read as much as possible from the original object because it is faster
|
||||||
let statistics = og._computeStatistics();
|
let statistics = og._computeStatistics();
|
||||||
let trkpt = withArtificialTimestamps(
|
let trkpt = withArtificialTimestamps(og.trkpt, totalTime, lastPoint, startTime, statistics);
|
||||||
og.trkpt,
|
|
||||||
totalTime,
|
|
||||||
lastPoint,
|
|
||||||
startTime,
|
|
||||||
statistics.local.slope.at
|
|
||||||
);
|
|
||||||
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1307,6 +1326,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emptyExtensions: Record<string, string> = {};
|
||||||
export class TrackPoint {
|
export class TrackPoint {
|
||||||
[immerable] = true;
|
[immerable] = true;
|
||||||
|
|
||||||
@@ -1317,7 +1337,7 @@ export class TrackPoint {
|
|||||||
|
|
||||||
_data: { [key: string]: any } = {};
|
_data: { [key: string]: any } = {};
|
||||||
|
|
||||||
constructor(point: (TrackPointType & { _data?: any }) | TrackPoint) {
|
constructor(point: (TrackPointType & { _data?: any }) | TrackPoint, index?: number) {
|
||||||
this.attributes = point.attributes;
|
this.attributes = point.attributes;
|
||||||
this.ele = point.ele;
|
this.ele = point.ele;
|
||||||
this.time = point.time;
|
this.time = point.time;
|
||||||
@@ -1325,6 +1345,9 @@ export class TrackPoint {
|
|||||||
if (point.hasOwnProperty('_data')) {
|
if (point.hasOwnProperty('_data')) {
|
||||||
this._data = point._data;
|
this._data = point._data;
|
||||||
}
|
}
|
||||||
|
if (index !== undefined) {
|
||||||
|
this._data.index = index;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getCoordinates(): Coordinates {
|
getCoordinates(): Coordinates {
|
||||||
@@ -1398,7 +1421,7 @@ export class TrackPoint {
|
|||||||
this.extensions['gpxtpx:TrackPointExtension'] &&
|
this.extensions['gpxtpx:TrackPointExtension'] &&
|
||||||
this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']
|
this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']
|
||||||
? this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']
|
? this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']
|
||||||
: {};
|
: emptyExtensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
toTrackPointType(exclude: string[] = []): TrackPointType {
|
toTrackPointType(exclude: string[] = []): TrackPointType {
|
||||||
@@ -1468,11 +1491,18 @@ export class TrackPoint {
|
|||||||
|
|
||||||
clone(): TrackPoint {
|
clone(): TrackPoint {
|
||||||
return new TrackPoint({
|
return new TrackPoint({
|
||||||
attributes: cloneJSON(this.attributes),
|
attributes: {
|
||||||
|
lat: this.attributes.lat,
|
||||||
|
lon: this.attributes.lon,
|
||||||
|
},
|
||||||
ele: this.ele,
|
ele: this.ele,
|
||||||
time: this.time ? new Date(this.time.getTime()) : undefined,
|
time: this.time ? new Date(this.time.getTime()) : undefined,
|
||||||
extensions: cloneJSON(this.extensions),
|
extensions: this.extensions ? cloneJSON(this.extensions) : undefined,
|
||||||
_data: cloneJSON(this._data),
|
_data: {
|
||||||
|
index: this._data?.index,
|
||||||
|
anchor: this._data?.anchor,
|
||||||
|
zoom: this._data?.zoom,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1491,7 +1521,7 @@ export class Waypoint {
|
|||||||
type?: string;
|
type?: string;
|
||||||
_data: { [key: string]: any } = {};
|
_data: { [key: string]: any } = {};
|
||||||
|
|
||||||
constructor(waypoint: (WaypointType & { _data?: any }) | Waypoint) {
|
constructor(waypoint: (WaypointType & { _data?: any }) | Waypoint, index?: number) {
|
||||||
this.attributes = waypoint.attributes;
|
this.attributes = waypoint.attributes;
|
||||||
this.ele = waypoint.ele;
|
this.ele = waypoint.ele;
|
||||||
this.time = waypoint.time;
|
this.time = waypoint.time;
|
||||||
@@ -1510,6 +1540,9 @@ export class Waypoint {
|
|||||||
if (waypoint.hasOwnProperty('_data')) {
|
if (waypoint.hasOwnProperty('_data')) {
|
||||||
this._data = waypoint._data;
|
this._data = waypoint._data;
|
||||||
}
|
}
|
||||||
|
if (index !== undefined) {
|
||||||
|
this._data.index = index;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getCoordinates(): Coordinates {
|
getCoordinates(): Coordinates {
|
||||||
@@ -1557,7 +1590,10 @@ export class Waypoint {
|
|||||||
|
|
||||||
clone(): Waypoint {
|
clone(): Waypoint {
|
||||||
return new Waypoint({
|
return new Waypoint({
|
||||||
attributes: cloneJSON(this.attributes),
|
attributes: {
|
||||||
|
lat: this.attributes.lat,
|
||||||
|
lon: this.attributes.lon,
|
||||||
|
},
|
||||||
ele: this.ele,
|
ele: this.ele,
|
||||||
time: this.time ? new Date(this.time.getTime()) : undefined,
|
time: this.time ? new Date(this.time.getTime()) : undefined,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
@@ -1606,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;
|
const earthRadius = 6371008.8;
|
||||||
export function distance(
|
export function distance(
|
||||||
coord1: TrackPoint | Coordinates,
|
coord1: TrackPoint | Coordinates,
|
||||||
@@ -1938,9 +1675,9 @@ export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
|||||||
if (point1.ele === undefined || point2.ele === undefined || point3.ele === undefined) {
|
if (point1.ele === undefined || point2.ele === undefined || point3.ele === undefined) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let x1 = statistics.local.distance.total[point1._data.index] * 1000;
|
let x1 = statistics.local.data[point1._data.index].distance.total * 1000;
|
||||||
let x2 = statistics.local.distance.total[point2._data.index] * 1000;
|
let x2 = statistics.local.data[point2._data.index].distance.total * 1000;
|
||||||
let x3 = statistics.local.distance.total[point3._data.index] * 1000;
|
let x3 = statistics.local.data[point3._data.index].distance.total * 1000;
|
||||||
let y1 = point1.ele;
|
let y1 = point1.ele;
|
||||||
let y2 = point2.ele;
|
let y2 = point2.ele;
|
||||||
let y3 = point3.ele;
|
let y3 = point3.ele;
|
||||||
@@ -1959,10 +1696,9 @@ function windowSmoothing(
|
|||||||
right: number,
|
right: number,
|
||||||
distance: (index1: number, index2: number) => number,
|
distance: (index1: number, index2: number) => number,
|
||||||
window: number,
|
window: number,
|
||||||
compute: (start: number, end: number) => number
|
compute: (start: number, end: number) => number,
|
||||||
): number[] {
|
callback: (value: number, index: number) => void
|
||||||
let result = [];
|
): void {
|
||||||
|
|
||||||
let start = left;
|
let start = left;
|
||||||
for (var i = left; i < right; i++) {
|
for (var i = left; i < right; i++) {
|
||||||
while (start + 1 < i && distance(start, i) > window) {
|
while (start + 1 < i && distance(start, i) > window) {
|
||||||
@@ -1972,10 +1708,8 @@ function windowSmoothing(
|
|||||||
while (end < right && distance(i, end) <= window) {
|
while (end < right && distance(i, end) <= window) {
|
||||||
end++;
|
end++;
|
||||||
}
|
}
|
||||||
result.push(compute(start, end - 1));
|
callback(compute(start, end - 1), i);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function distanceWindowSmoothing(
|
function distanceWindowSmoothing(
|
||||||
@@ -1983,30 +1717,35 @@ function distanceWindowSmoothing(
|
|||||||
right: number,
|
right: number,
|
||||||
statistics: GPXStatistics,
|
statistics: GPXStatistics,
|
||||||
window: number,
|
window: number,
|
||||||
compute: (start: number, end: number) => number
|
compute: (start: number, end: number) => number,
|
||||||
): number[] {
|
callback: (value: number, index: number) => void
|
||||||
return windowSmoothing(
|
): void {
|
||||||
|
windowSmoothing(
|
||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
(index1, index2) =>
|
(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,
|
window,
|
||||||
compute
|
compute,
|
||||||
|
callback
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeWindowSmoothing(
|
function timeWindowSmoothing(
|
||||||
points: TrackPoint[],
|
points: TrackPoint[],
|
||||||
window: number,
|
window: number,
|
||||||
compute: (start: number, end: number) => number
|
compute: (start: number, end: number) => number,
|
||||||
): number[] {
|
callback: (value: number, index: number) => void
|
||||||
return windowSmoothing(
|
): void {
|
||||||
|
windowSmoothing(
|
||||||
0,
|
0,
|
||||||
points.length,
|
points.length,
|
||||||
(index1, index2) =>
|
(index1, index2) =>
|
||||||
points[index2].time?.getTime() - points[index1].time?.getTime() || 2 * window,
|
points[index2].time?.getTime() - points[index1].time?.getTime() || 2 * window,
|
||||||
window,
|
window,
|
||||||
compute
|
compute,
|
||||||
|
callback
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2058,14 +1797,14 @@ function withArtificialTimestamps(
|
|||||||
totalTime: number,
|
totalTime: number,
|
||||||
lastPoint: TrackPoint | undefined,
|
lastPoint: TrackPoint | undefined,
|
||||||
startTime: Date,
|
startTime: Date,
|
||||||
slope: number[]
|
statistics: GPXStatistics
|
||||||
): TrackPoint[] {
|
): TrackPoint[] {
|
||||||
let weight = [];
|
let weight = [];
|
||||||
let totalWeight = 0;
|
let totalWeight = 0;
|
||||||
|
|
||||||
for (let i = 0; i < points.length - 1; i++) {
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
let dist = distance(points[i].getCoordinates(), points[i + 1].getCoordinates());
|
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);
|
weight.push(w);
|
||||||
totalWeight += w;
|
totalWeight += w;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './gpx';
|
export * from './gpx';
|
||||||
|
export * from './statistics';
|
||||||
export { Coordinates, LineStyleExtension, WaypointType } from './types';
|
export { Coordinates, LineStyleExtension, WaypointType } from './types';
|
||||||
export { parseGPX, buildGPX } from './io';
|
export { parseGPX, buildGPX } from './io';
|
||||||
export * from './simplify';
|
export * from './simplify';
|
||||||
|
|||||||
@@ -59,13 +59,13 @@ function ramerDouglasPeuckerRecursive(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function crossarcDistance(
|
export function crossarcDistance(
|
||||||
point1: TrackPoint,
|
point1: TrackPoint | Coordinates,
|
||||||
point2: TrackPoint,
|
point2: TrackPoint | Coordinates,
|
||||||
point3: TrackPoint | Coordinates
|
point3: TrackPoint | Coordinates
|
||||||
): number {
|
): number {
|
||||||
return crossarc(
|
return crossarc(
|
||||||
point1.getCoordinates(),
|
point1 instanceof TrackPoint ? point1.getCoordinates() : point1,
|
||||||
point2.getCoordinates(),
|
point2 instanceof TrackPoint ? point2.getCoordinates() : point2,
|
||||||
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
|
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
|
||||||
8
website/package-lock.json
generated
8
website/package-lock.json
generated
@@ -47,7 +47,7 @@
|
|||||||
"@types/sortablejs": "^1.15.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
||||||
"@typescript-eslint/parser": "^8.33.1",
|
"@typescript-eslint/parser": "^8.33.1",
|
||||||
"bits-ui": "^2.12.0",
|
"bits-ui": "^2.14.4",
|
||||||
"eslint": "^9.28.0",
|
"eslint": "^9.28.0",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-svelte": "^3.9.1",
|
"eslint-plugin-svelte": "^3.9.1",
|
||||||
@@ -3241,9 +3241,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/bits-ui": {
|
"node_modules/bits-ui": {
|
||||||
"version": "2.12.0",
|
"version": "2.14.4",
|
||||||
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.14.4.tgz",
|
||||||
"integrity": "sha512-8NF4ILNyAJlIxDXpl/akGXGBV5QmZAe+8gTfPttM5P6/+LrijumcSfFXY5cr4QkXwTmLA7H5stYpbgJf2XFJvg==",
|
"integrity": "sha512-W6kenhnbd/YVvur+DKkaVJ6GldE53eLewur5AhUCqslYQ0vjZr8eWlOfwZnMiPB+PF5HMVqf61vXBvmyrAmPWg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"lint": "prettier --check . --config ../.prettierrc && eslint .",
|
"lint": "prettier --check . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore && eslint .",
|
||||||
"format": "prettier --write . --config ../.prettierrc"
|
"format": "prettier --write . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lucide/svelte": "^0.544.0",
|
"@lucide/svelte": "^0.544.0",
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"@types/sortablejs": "^1.15.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
||||||
"@typescript-eslint/parser": "^8.33.1",
|
"@typescript-eslint/parser": "^8.33.1",
|
||||||
"bits-ui": "^2.12.0",
|
"bits-ui": "^2.14.4",
|
||||||
"eslint": "^9.28.0",
|
"eslint": "^9.28.0",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-svelte": "^3.9.1",
|
"eslint-plugin-svelte": "^3.9.1",
|
||||||
|
|||||||
@@ -368,6 +368,26 @@ export const overlays: { [key: string]: string | StyleSpecification } = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
bikerouterGravel: bikerouterGravel as 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: {
|
swisstopoSlope: {
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {
|
sources: {
|
||||||
@@ -801,6 +821,7 @@ export const overlayTree: LayerTreeType = {
|
|||||||
},
|
},
|
||||||
cyclOSMlite: true,
|
cyclOSMlite: true,
|
||||||
bikerouterGravel: true,
|
bikerouterGravel: true,
|
||||||
|
openRailwayMap: true,
|
||||||
},
|
},
|
||||||
countries: {
|
countries: {
|
||||||
france: {
|
france: {
|
||||||
@@ -885,6 +906,7 @@ export const defaultOverlays: LayerTreeType = {
|
|||||||
},
|
},
|
||||||
cyclOSMlite: false,
|
cyclOSMlite: false,
|
||||||
bikerouterGravel: false,
|
bikerouterGravel: false,
|
||||||
|
openRailwayMap: false,
|
||||||
},
|
},
|
||||||
countries: {
|
countries: {
|
||||||
france: {
|
france: {
|
||||||
@@ -1020,6 +1042,7 @@ export const defaultOverlayTree: LayerTreeType = {
|
|||||||
},
|
},
|
||||||
cyclOSMlite: false,
|
cyclOSMlite: false,
|
||||||
bikerouterGravel: false,
|
bikerouterGravel: false,
|
||||||
|
openRailwayMap: false,
|
||||||
},
|
},
|
||||||
countries: {
|
countries: {
|
||||||
france: {
|
france: {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from '@lucide/svelte';
|
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from '@lucide/svelte';
|
||||||
|
|
||||||
import { i18n } from '$lib/i18n.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 type { Readable } from 'svelte/store';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
|
|
||||||
@@ -18,14 +18,14 @@
|
|||||||
orientation,
|
orientation,
|
||||||
panelSize,
|
panelSize,
|
||||||
}: {
|
}: {
|
||||||
gpxStatistics: Readable<GPXStatistics>;
|
gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||||
slicedGPXStatistics: Readable<[GPXStatistics, number, number] | undefined>;
|
slicedGPXStatistics: Readable<[GPXGlobalStatistics, number, number] | undefined>;
|
||||||
orientation: 'horizontal' | 'vertical';
|
orientation: 'horizontal' | 'vertical';
|
||||||
panelSize: number;
|
panelSize: number;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let statistics = $derived(
|
let statistics = $derived(
|
||||||
$slicedGPXStatistics !== undefined ? $slicedGPXStatistics[0] : $gpxStatistics
|
$slicedGPXStatistics !== undefined ? $slicedGPXStatistics[0] : $gpxStatistics.global
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -42,15 +42,15 @@
|
|||||||
<Tooltip label={i18n._('quantities.distance')}>
|
<Tooltip label={i18n._('quantities.distance')}>
|
||||||
<span class="flex flex-row items-center">
|
<span class="flex flex-row items-center">
|
||||||
<Ruler size="16" class="mr-1" />
|
<Ruler size="16" class="mr-1" />
|
||||||
<WithUnits value={statistics.global.distance.total} type="distance" />
|
<WithUnits value={statistics.distance.total} type="distance" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label={i18n._('quantities.elevation_gain_loss')}>
|
<Tooltip label={i18n._('quantities.elevation_gain_loss')}>
|
||||||
<span class="flex flex-row items-center">
|
<span class="flex flex-row items-center">
|
||||||
<MoveUpRight size="16" class="mr-1" />
|
<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" />
|
<MoveDownRight size="16" class="mx-1" />
|
||||||
<WithUnits value={statistics.global.elevation.loss} type="elevation" />
|
<WithUnits value={statistics.elevation.loss} type="elevation" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{#if panelSize > 120 || orientation === 'horizontal'}
|
{#if panelSize > 120 || orientation === 'horizontal'}
|
||||||
@@ -64,13 +64,9 @@
|
|||||||
>
|
>
|
||||||
<span class="flex flex-row items-center">
|
<span class="flex flex-row items-center">
|
||||||
<Zap size="16" class="mr-1" />
|
<Zap size="16" class="mr-1" />
|
||||||
<WithUnits
|
<WithUnits value={statistics.speed.moving} type="speed" showUnits={false} />
|
||||||
value={statistics.global.speed.moving}
|
|
||||||
type="speed"
|
|
||||||
showUnits={false}
|
|
||||||
/>
|
|
||||||
<span class="mx-1">/</span>
|
<span class="mx-1">/</span>
|
||||||
<WithUnits value={statistics.global.speed.total} type="speed" />
|
<WithUnits value={statistics.speed.total} type="speed" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -83,9 +79,9 @@
|
|||||||
>
|
>
|
||||||
<span class="flex flex-row items-center">
|
<span class="flex flex-row items-center">
|
||||||
<Timer size="16" class="mr-1" />
|
<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>
|
<span class="mx-1">/</span>
|
||||||
<WithUnits value={statistics.global.time.total} type="time" />
|
<WithUnits value={statistics.time.total} type="time" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
Construction,
|
Construction,
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import type { Readable, Writable } from 'svelte/store';
|
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 { settings } from '$lib/logic/settings';
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import { ElevationProfile } from '$lib/components/elevation-profile/elevation-profile';
|
import { ElevationProfile } from '$lib/components/elevation-profile/elevation-profile';
|
||||||
@@ -32,8 +32,8 @@
|
|||||||
elevationFill,
|
elevationFill,
|
||||||
showControls = true,
|
showControls = true,
|
||||||
}: {
|
}: {
|
||||||
gpxStatistics: Readable<GPXStatistics>;
|
gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||||
slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
|
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
||||||
additionalDatasets: Writable<string[]>;
|
additionalDatasets: Writable<string[]>;
|
||||||
elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>;
|
elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>;
|
||||||
showControls?: boolean;
|
showControls?: boolean;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import Chart, {
|
|||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { get, type Readable, type Writable } from 'svelte/store';
|
import { get, type Readable, type Writable } from 'svelte/store';
|
||||||
import { map } from '$lib/components/map/map';
|
import { map } from '$lib/components/map/map';
|
||||||
import type { GPXStatistics } from 'gpx';
|
import type { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
||||||
import { mode } from 'mode-watcher';
|
import { mode } from 'mode-watcher';
|
||||||
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
|
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
|
||||||
|
|
||||||
@@ -54,14 +54,14 @@ export class ElevationProfile {
|
|||||||
private _dragging = false;
|
private _dragging = false;
|
||||||
private _panning = false;
|
private _panning = false;
|
||||||
|
|
||||||
private _gpxStatistics: Readable<GPXStatistics>;
|
private _gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||||
private _slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
|
private _slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
||||||
private _additionalDatasets: Readable<string[]>;
|
private _additionalDatasets: Readable<string[]>;
|
||||||
private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>;
|
private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
gpxStatistics: Readable<GPXStatistics>,
|
gpxStatistics: Readable<GPXStatisticsGroup>,
|
||||||
slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>,
|
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>,
|
||||||
additionalDatasets: Readable<string[]>,
|
additionalDatasets: Readable<string[]>,
|
||||||
elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>,
|
elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>,
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
@@ -342,7 +342,7 @@ export class ElevationProfile {
|
|||||||
if (evt.x - rect.left <= this._chart.chartArea.left) {
|
if (evt.x - rect.left <= this._chart.chartArea.left) {
|
||||||
return 0;
|
return 0;
|
||||||
} else if (evt.x - rect.left >= this._chart.chartArea.right) {
|
} 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 {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -375,7 +375,7 @@ export class ElevationProfile {
|
|||||||
startIndex = endIndex;
|
startIndex = endIndex;
|
||||||
} else if (startIndex !== endIndex) {
|
} else if (startIndex !== endIndex) {
|
||||||
this._slicedGPXStatistics.set([
|
this._slicedGPXStatistics.set([
|
||||||
get(this._gpxStatistics).slice(
|
get(this._gpxStatistics).sliced(
|
||||||
Math.min(startIndex, endIndex),
|
Math.min(startIndex, endIndex),
|
||||||
Math.max(startIndex, endIndex)
|
Math.max(startIndex, endIndex)
|
||||||
),
|
),
|
||||||
@@ -405,85 +405,99 @@ export class ElevationProfile {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = get(this._gpxStatistics);
|
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] = {
|
this._chart.data.datasets[0] = {
|
||||||
label: i18n._('quantities.elevation'),
|
label: i18n._('quantities.elevation'),
|
||||||
data: data.local.points.map((point, index) => {
|
data: datasets[0],
|
||||||
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,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
normalized: true,
|
normalized: true,
|
||||||
fill: 'start',
|
fill: 'start',
|
||||||
order: 1,
|
order: 1,
|
||||||
segment: {},
|
segment: {},
|
||||||
};
|
};
|
||||||
this._chart.data.datasets[1] = {
|
this._chart.data.datasets[1] = {
|
||||||
data: data.local.points.map((point, index) => {
|
data: datasets[1],
|
||||||
return {
|
|
||||||
x: getConvertedDistance(data.local.distance.total[index]),
|
|
||||||
y: getConvertedVelocity(data.local.speed[index]),
|
|
||||||
index: index,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
normalized: true,
|
normalized: true,
|
||||||
yAxisID: 'yspeed',
|
yAxisID: 'yspeed',
|
||||||
};
|
};
|
||||||
this._chart.data.datasets[2] = {
|
this._chart.data.datasets[2] = {
|
||||||
data: data.local.points.map((point, index) => {
|
data: datasets[2],
|
||||||
return {
|
|
||||||
x: getConvertedDistance(data.local.distance.total[index]),
|
|
||||||
y: point.getHeartRate(),
|
|
||||||
index: index,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
normalized: true,
|
normalized: true,
|
||||||
yAxisID: 'yhr',
|
yAxisID: 'yhr',
|
||||||
};
|
};
|
||||||
this._chart.data.datasets[3] = {
|
this._chart.data.datasets[3] = {
|
||||||
data: data.local.points.map((point, index) => {
|
data: datasets[3],
|
||||||
return {
|
|
||||||
x: getConvertedDistance(data.local.distance.total[index]),
|
|
||||||
y: point.getCadence(),
|
|
||||||
index: index,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
normalized: true,
|
normalized: true,
|
||||||
yAxisID: 'ycad',
|
yAxisID: 'ycad',
|
||||||
};
|
};
|
||||||
this._chart.data.datasets[4] = {
|
this._chart.data.datasets[4] = {
|
||||||
data: data.local.points.map((point, index) => {
|
data: datasets[4],
|
||||||
return {
|
|
||||||
x: getConvertedDistance(data.local.distance.total[index]),
|
|
||||||
y: getConvertedTemperature(point.getTemperature()),
|
|
||||||
index: index,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
normalized: true,
|
normalized: true,
|
||||||
yAxisID: 'yatemp',
|
yAxisID: 'yatemp',
|
||||||
};
|
};
|
||||||
this._chart.data.datasets[5] = {
|
this._chart.data.datasets[5] = {
|
||||||
data: data.local.points.map((point, index) => {
|
data: datasets[5],
|
||||||
return {
|
|
||||||
x: getConvertedDistance(data.local.distance.total[index]),
|
|
||||||
y: point.getPower(),
|
|
||||||
index: index,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
normalized: true,
|
normalized: true,
|
||||||
yAxisID: 'ypower',
|
yAxisID: 'ypower',
|
||||||
};
|
};
|
||||||
|
|
||||||
this._chart.options.scales!.x!['min'] = 0;
|
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.setVisibility();
|
||||||
this.setFill();
|
this.setFill();
|
||||||
@@ -576,10 +590,12 @@ export class ElevationProfile {
|
|||||||
|
|
||||||
const gpxStatistics = get(this._gpxStatistics);
|
const gpxStatistics = get(this._gpxStatistics);
|
||||||
let startPixel = this._chart.scales.x.getPixelForValue(
|
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(
|
let endPixel = this._chart.scales.x.getPixelForValue(
|
||||||
getConvertedDistance(gpxStatistics.local.distance.total[endIndex])
|
getConvertedDistance(gpxStatistics.getTrackPoint(endIndex)?.distance.total ?? 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
selectionContext.fillRect(
|
selectionContext.fillRect(
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
SquareActivity,
|
SquareActivity,
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import { i18n } from '$lib/i18n.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 { ListRootItem } from '$lib/components/file-list/file-list';
|
||||||
import { fileStateCollection } from '$lib/logic/file-state';
|
import { fileStateCollection } from '$lib/logic/file-state';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
@@ -48,24 +48,24 @@
|
|||||||
extensions: false,
|
extensions: false,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
let statistics = $gpxStatistics;
|
let statistics = $gpxStatistics.global;
|
||||||
if (exportState.current === ExportState.ALL) {
|
if (exportState.current === ExportState.ALL) {
|
||||||
statistics = Array.from(get(fileStateCollection).values())
|
statistics = Array.from(get(fileStateCollection).values())
|
||||||
.map((file) => file.statistics)
|
.map((file) => file.statistics)
|
||||||
.reduce((acc, cur) => {
|
.reduce((acc, cur) => {
|
||||||
if (cur !== undefined) {
|
if (cur !== undefined) {
|
||||||
acc.mergeWith(cur.getStatisticsFor(new ListRootItem()));
|
acc.mergeWith(cur.getStatisticsFor(new ListRootItem()).global);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, new GPXStatistics());
|
}, new GPXGlobalStatistics());
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
time: statistics.global.time.total === 0,
|
time: statistics.time.total === 0,
|
||||||
hr: statistics.global.hr.count === 0,
|
hr: statistics.hr.count === 0,
|
||||||
cad: statistics.global.cad.count === 0,
|
cad: statistics.cad.count === 0,
|
||||||
atemp: statistics.global.atemp.count === 0,
|
atemp: statistics.atemp.count === 0,
|
||||||
power: statistics.global.power.count === 0,
|
power: statistics.power.count === 0,
|
||||||
extensions: Object.keys(statistics.global.extensions).length === 0,
|
extensions: Object.keys(statistics.extensions).length === 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -72,18 +72,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let style = node.getStyle(defaultColor);
|
let style = node.getStyle(defaultColor);
|
||||||
style.color.forEach((c) => {
|
colors = style.color;
|
||||||
if (!colors.includes(c)) {
|
|
||||||
colors.push(c);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (node instanceof Track) {
|
} else if (node instanceof Track) {
|
||||||
let style = node.getStyle();
|
let style = node.getStyle();
|
||||||
if (style) {
|
if (
|
||||||
if (style['gpx_style:color'] && !colors.includes(style['gpx_style:color'])) {
|
style &&
|
||||||
|
style['gpx_style:color'] &&
|
||||||
|
!colors.includes(style['gpx_style:color'])
|
||||||
|
) {
|
||||||
colors.push(style['gpx_style:color']);
|
colors.push(style['gpx_style:color']);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (colors.length === 0) {
|
if (colors.length === 0) {
|
||||||
let layer = gpxLayers.getLayer(item.getFileId());
|
let layer = gpxLayers.getLayer(item.getFileId());
|
||||||
if (layer) {
|
if (layer) {
|
||||||
|
|||||||
@@ -101,23 +101,17 @@ export class DistanceMarkers {
|
|||||||
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
|
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
|
||||||
let statistics = get(gpxStatistics);
|
let statistics = get(gpxStatistics);
|
||||||
|
|
||||||
let features = [];
|
let features: GeoJSON.Feature[] = [];
|
||||||
let currentTargetDistance = 1;
|
let currentTargetDistance = 1;
|
||||||
for (let i = 0; i < statistics.local.distance.total.length; i++) {
|
statistics.forEachTrackPoint((trkpt, dist) => {
|
||||||
if (
|
if (dist >= getConvertedDistanceToKilometers(currentTargetDistance)) {
|
||||||
statistics.local.distance.total[i] >=
|
|
||||||
getConvertedDistanceToKilometers(currentTargetDistance)
|
|
||||||
) {
|
|
||||||
let distance = currentTargetDistance.toFixed(0);
|
let distance = currentTargetDistance.toFixed(0);
|
||||||
let level = levels.find((level) => currentTargetDistance % level === 0) || 1;
|
let level = levels.find((level) => currentTargetDistance % level === 0) || 1;
|
||||||
features.push({
|
features.push({
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: {
|
geometry: {
|
||||||
type: 'Point',
|
type: 'Point',
|
||||||
coordinates: [
|
coordinates: [trkpt.getLongitude(), trkpt.getLatitude()],
|
||||||
statistics.local.points[i].getLongitude(),
|
|
||||||
statistics.local.points[i].getLatitude(),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
distance,
|
distance,
|
||||||
@@ -126,7 +120,7 @@ export class DistanceMarkers {
|
|||||||
} as GeoJSON.Feature);
|
} as GeoJSON.Feature);
|
||||||
currentTargetDistance += 1;
|
currentTargetDistance += 1;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
|
|||||||
@@ -153,8 +153,6 @@ export class GPXLayer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadIcons();
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
file._data.style &&
|
file._data.style &&
|
||||||
file._data.style.color &&
|
file._data.style.color &&
|
||||||
@@ -164,6 +162,8 @@ export class GPXLayer {
|
|||||||
this.layerColor = `#${file._data.style.color}`;
|
this.layerColor = `#${file._data.style.color}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loadIcons();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let source = _map.getSource(this.fileId) as mapboxgl.GeoJSONSource | undefined;
|
let source = _map.getSource(this.fileId) as mapboxgl.GeoJSONSource | undefined;
|
||||||
if (source) {
|
if (source) {
|
||||||
@@ -702,7 +702,7 @@ export class GPXLayer {
|
|||||||
properties: {
|
properties: {
|
||||||
fileId: this.fileId,
|
fileId: this.fileId,
|
||||||
waypointIndex: index,
|
waypointIndex: index,
|
||||||
icon: `${this.fileId}-waypoint-${getSymbolKey(waypoint.sym) ?? 'default'}`,
|
icon: `waypoint-${getSymbolKey(waypoint.sym) ?? 'default'}-${this.layerColor}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -723,7 +723,7 @@ export class GPXLayer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
symbols.forEach((symbol) => {
|
symbols.forEach((symbol) => {
|
||||||
const iconId = `${this.fileId}-waypoint-${symbol ?? 'default'}`;
|
const iconId = `waypoint-${symbol ?? 'default'}-${this.layerColor}`;
|
||||||
if (!_map.hasImage(iconId)) {
|
if (!_map.hasImage(iconId)) {
|
||||||
let icon = new Image(100, 100);
|
let icon = new Image(100, 100);
|
||||||
icon.onload = () => {
|
icon.onload = () => {
|
||||||
|
|||||||
@@ -34,13 +34,20 @@ export class StartEndMarkers {
|
|||||||
if (!map_) return;
|
if (!map_) return;
|
||||||
|
|
||||||
const tool = get(currentTool);
|
const tool = get(currentTool);
|
||||||
const statistics = get(slicedGPXStatistics)?.[0] ?? get(gpxStatistics);
|
const statistics = get(gpxStatistics);
|
||||||
|
const slicedStatistics = get(slicedGPXStatistics);
|
||||||
const hidden = get(allHidden);
|
const hidden = get(allHidden);
|
||||||
if (statistics.local.points.length > 0 && tool !== Tool.ROUTING && !hidden) {
|
if (statistics.global.length > 0 && tool !== Tool.ROUTING && !hidden) {
|
||||||
this.start.setLngLat(statistics.local.points[0].getCoordinates()).addTo(map_);
|
this.start
|
||||||
|
.setLngLat(
|
||||||
|
statistics.getTrackPoint(slicedStatistics?.[1] ?? 0)!.trkpt.getCoordinates()
|
||||||
|
)
|
||||||
|
.addTo(map_);
|
||||||
this.end
|
this.end
|
||||||
.setLngLat(
|
.setLngLat(
|
||||||
statistics.local.points[statistics.local.points.length - 1].getCoordinates()
|
statistics
|
||||||
|
.getTrackPoint(slicedStatistics?.[2] ?? statistics.global.length - 1)!
|
||||||
|
.trkpt.getCoordinates()
|
||||||
)
|
)
|
||||||
.addTo(map_);
|
.addTo(map_);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -101,9 +101,7 @@
|
|||||||
acc: Record<string, ImportSpecification>,
|
acc: Record<string, ImportSpecification>,
|
||||||
imprt: ImportSpecification
|
imprt: ImportSpecification
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (!['basemap', 'overlays'].includes(imprt.id)) {
|
||||||
!['basemap', 'overlays', 'glyphs-and-sprite'].includes(imprt.id)
|
|
||||||
) {
|
|
||||||
acc[imprt.id] = imprt;
|
acc[imprt.id] = imprt;
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($selectedBasemapTree && $currentBasemap) {
|
if (open && $selectedBasemapTree && $currentBasemap) {
|
||||||
if (!isSelected($selectedBasemapTree, $currentBasemap)) {
|
if (!isSelected($selectedBasemapTree, $currentBasemap)) {
|
||||||
if (!isSelected($selectedBasemapTree, defaultBasemap)) {
|
if (!isSelected($selectedBasemapTree, defaultBasemap)) {
|
||||||
$selectedBasemapTree = toggle($selectedBasemapTree, defaultBasemap);
|
$selectedBasemapTree = toggle($selectedBasemapTree, defaultBasemap);
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($selectedOverlayTree) {
|
if (open && $selectedOverlayTree) {
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
if ($currentOverlays) {
|
if ($currentOverlays) {
|
||||||
let overlayLayers = getLayers($currentOverlays);
|
let overlayLayers = getLayers($currentOverlays);
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($selectedOverpassTree) {
|
if (open && $selectedOverpassTree) {
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
if ($currentOverpassQueries) {
|
if ($currentOverpassQueries) {
|
||||||
let overlayLayers = getLayers($currentOverpassQueries);
|
let overlayLayers = getLayers($currentOverpassQueries);
|
||||||
|
|||||||
@@ -35,17 +35,6 @@ export class MapboxGLMap {
|
|||||||
sources: {},
|
sources: {},
|
||||||
layers: [],
|
layers: [],
|
||||||
imports: [
|
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',
|
id: 'basemap',
|
||||||
url: '',
|
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', () => {
|
map.on('load', () => {
|
||||||
this._map.set(map); // only set the store after the map has loaded
|
this._map.set(map); // only set the store after the map has loaded
|
||||||
window._map = map; // entry point for extensions
|
window._map = map; // entry point for extensions
|
||||||
|
|||||||
@@ -28,17 +28,15 @@ export class ReducedGPXLayer {
|
|||||||
|
|
||||||
update() {
|
update() {
|
||||||
const file = this._fileState.file;
|
const file = this._fileState.file;
|
||||||
const stats = this._fileState.statistics;
|
if (!file) {
|
||||||
if (!file || !stats) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||||
let segmentItem = new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex);
|
let segmentItem = new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex);
|
||||||
let statistics = stats.getStatisticsFor(segmentItem);
|
|
||||||
this._updateSimplified(segmentItem.getFullId(), [
|
this._updateSimplified(segmentItem.getFullId(), [
|
||||||
segmentItem,
|
segmentItem,
|
||||||
statistics.local.points.length,
|
segment.trkpt.length,
|
||||||
ramerDouglasPeucker(statistics.local.points, minTolerance),
|
ramerDouglasPeucker(segment.trkpt, minTolerance),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -793,24 +793,25 @@ export class RoutingControls {
|
|||||||
replacingDistance +=
|
replacingDistance +=
|
||||||
distance(response[i - 1].getCoordinates(), response[i].getCoordinates()) / 1000;
|
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 =
|
let replacedDistance =
|
||||||
stats.local.distance.moving[anchors[anchors.length - 1].point._data.index] -
|
endAnchorStats.distance.moving - startAnchorStats.distance.moving;
|
||||||
stats.local.distance.moving[anchors[0].point._data.index];
|
|
||||||
|
|
||||||
let newDistance = stats.global.distance.moving + replacingDistance - replacedDistance;
|
let newDistance = stats.global.distance.moving + replacingDistance - replacedDistance;
|
||||||
let newTime = (newDistance / stats.global.speed.moving) * 3600;
|
let newTime = (newDistance / stats.global.speed.moving) * 3600;
|
||||||
|
|
||||||
let remainingTime =
|
let remainingTime =
|
||||||
stats.global.time.moving -
|
stats.global.time.moving -
|
||||||
(stats.local.time.moving[anchors[anchors.length - 1].point._data.index] -
|
(endAnchorStats.time.moving - startAnchorStats.time.moving);
|
||||||
stats.local.time.moving[anchors[0].point._data.index]);
|
|
||||||
let replacingTime = newTime - remainingTime;
|
let replacingTime = newTime - remainingTime;
|
||||||
|
|
||||||
if (replacingTime <= 0) {
|
if (replacingTime <= 0) {
|
||||||
// Fallback to simple time difference
|
// Fallback to simple time difference
|
||||||
replacingTime =
|
replacingTime = endAnchorStats.time.total - startAnchorStats.time.total;
|
||||||
stats.local.time.total[anchors[anchors.length - 1].point._data.index] -
|
|
||||||
stats.local.time.total[anchors[0].point._data.index];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
speed = (replacingDistance / replacingTime) * 3600;
|
speed = (replacingDistance / replacingTime) * 3600;
|
||||||
@@ -820,9 +821,7 @@ export class RoutingControls {
|
|||||||
let endIndex = anchors[anchors.length - 1].point._data.index;
|
let endIndex = anchors[anchors.length - 1].point._data.index;
|
||||||
startTime = new Date(
|
startTime = new Date(
|
||||||
(segment.trkpt[endIndex].time?.getTime() ?? 0) -
|
(segment.trkpt[endIndex].time?.getTime() ?? 0) -
|
||||||
(replacingTime +
|
(replacingTime + endAnchorStats.time.total - endAnchorStats.time.moving) *
|
||||||
stats.local.time.total[endIndex] -
|
|
||||||
stats.local.time.moving[endIndex]) *
|
|
||||||
1000
|
1000
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,12 +26,10 @@
|
|||||||
|
|
||||||
let validSelection = $derived(
|
let validSelection = $derived(
|
||||||
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
|
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
|
||||||
$gpxStatistics.local.points.length > 0
|
$gpxStatistics.global.length > 0
|
||||||
);
|
);
|
||||||
let maxSliderValue = $derived(
|
let maxSliderValue = $derived(
|
||||||
validSelection && $gpxStatistics.local.points.length > 0
|
validSelection && $gpxStatistics.global.length > 0 ? $gpxStatistics.global.length - 1 : 1
|
||||||
? $gpxStatistics.local.points.length - 1
|
|
||||||
: 1
|
|
||||||
);
|
);
|
||||||
let sliderValues = $derived([0, maxSliderValue]);
|
let sliderValues = $derived([0, maxSliderValue]);
|
||||||
let canCrop = $derived(sliderValues[0] != 0 || sliderValues[1] != maxSliderValue);
|
let canCrop = $derived(sliderValues[0] != 0 || sliderValues[1] != maxSliderValue);
|
||||||
@@ -45,7 +43,7 @@
|
|||||||
function updateSlicedGPXStatistics() {
|
function updateSlicedGPXStatistics() {
|
||||||
if (validSelection && canCrop) {
|
if (validSelection && canCrop) {
|
||||||
$slicedGPXStatistics = [
|
$slicedGPXStatistics = [
|
||||||
get(gpxStatistics).slice(sliderValues[0], sliderValues[1]),
|
get(gpxStatistics).sliced(sliderValues[0], sliderValues[1]),
|
||||||
sliderValues[0],
|
sliderValues[0],
|
||||||
sliderValues[1],
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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">
|
<div class="h-48 w-full">
|
||||||
<ElevationProfile
|
<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.
|
- Una **traccia** è composta da una sequenza di segmenti scollegati.
|
||||||
Inoltre, può contenere metadati come un **nome**, una **descrizione**, e **proprietà di visualizzazione**.
|
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 **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.
|
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.
|
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à)
|
## <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.
|
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à.
|
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! ❤️
|
Grazie mille per il vostro supporto! ❤️
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { HeartHandshake } from '@lucide/svelte';
|
import { HeartHandshake } from '@lucide/svelte';
|
||||||
</script>
|
</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.
|
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.
|
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.
|
Mapbox là công ty cung cấp một số bản đồ đẹp trên trang web này.
|
||||||
They also develop the <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">map engine</a> which powers **gpx.studio**.
|
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.
|
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.
|
||||||
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.
|
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 }
|
# { title }
|
||||||
|
|
||||||
Unlike the file actions, the edit actions can potentially modify the content of the currently selected files.
|
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.
|
||||||
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).
|
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_.
|
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.
|
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" /> 删除全部
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> 删除全部
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import { freeze, type WritableDraft } from 'immer';
|
import { freeze, type WritableDraft } from 'immer';
|
||||||
import {
|
import {
|
||||||
distance,
|
|
||||||
GPXFile,
|
GPXFile,
|
||||||
parseGPX,
|
parseGPX,
|
||||||
Track,
|
Track,
|
||||||
@@ -30,7 +29,7 @@ import {
|
|||||||
} from 'gpx';
|
} from 'gpx';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { getClosestLinePoint, getElevation } from '$lib/utils';
|
import { getClosestLinePoint, getClosestTrackSegments, getElevation } from '$lib/utils';
|
||||||
import { gpxStatistics } from '$lib/logic/statistics';
|
import { gpxStatistics } from '$lib/logic/statistics';
|
||||||
import { boundsManager } from './bounds';
|
import { boundsManager } from './bounds';
|
||||||
|
|
||||||
@@ -216,7 +215,7 @@ export const fileActions = {
|
|||||||
reverseSelection: () => {
|
reverseSelection: () => {
|
||||||
if (
|
if (
|
||||||
!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) ||
|
!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) ||
|
||||||
get(gpxStatistics).local.points?.length <= 1
|
get(gpxStatistics).global.length <= 1
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -346,19 +345,20 @@ export const fileActions = {
|
|||||||
let startTime: Date | undefined = undefined;
|
let startTime: Date | undefined = undefined;
|
||||||
if (speed !== undefined) {
|
if (speed !== undefined) {
|
||||||
if (
|
if (
|
||||||
statistics.local.points.length > 0 &&
|
statistics.global.length > 0 &&
|
||||||
statistics.local.points[0].time !== undefined
|
statistics.getTrackPoint(0)!.trkpt.time !== undefined
|
||||||
) {
|
) {
|
||||||
startTime = statistics.local.points[0].time;
|
startTime = statistics.getTrackPoint(0)!.trkpt.time;
|
||||||
} else {
|
} else {
|
||||||
let index = statistics.local.points.findIndex(
|
for (let i = 0; i < statistics.global.length; i++) {
|
||||||
(point) => point.time !== undefined
|
const point = statistics.getTrackPoint(i)!;
|
||||||
);
|
if (point.trkpt.time !== undefined) {
|
||||||
if (index !== -1 && statistics.local.points[index].time) {
|
|
||||||
startTime = new Date(
|
startTime = new Date(
|
||||||
statistics.local.points[index].time.getTime() -
|
point.trkpt.time.getTime() -
|
||||||
(1000 * 3600 * statistics.local.distance.total[index]) / speed
|
(1000 * 3600 * point.distance.total) / speed
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -453,34 +453,13 @@ export const fileActions = {
|
|||||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||||
if (level === ListLevel.FILE) {
|
if (level === ListLevel.FILE) {
|
||||||
let file = fileStateCollection.getFile(fileId);
|
let file = fileStateCollection.getFile(fileId);
|
||||||
if (file) {
|
let statistics = fileStateCollection.getStatistics(fileId);
|
||||||
|
if (file && statistics) {
|
||||||
if (file.trk.length > 1) {
|
if (file.trk.length > 1) {
|
||||||
let fileIds = getFileIds(file.trk.length);
|
let fileIds = getFileIds(file.trk.length);
|
||||||
let closest = file.wpt.map((wpt, wptIndex) => {
|
let closest = file.wpt.map((wpt) =>
|
||||||
return {
|
getClosestTrackSegments(file, statistics, wpt.getCoordinates())
|
||||||
wptIndex: wptIndex,
|
|
||||||
index: [0],
|
|
||||||
distance: Number.MAX_VALUE,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
file.trk.forEach((track, index) => {
|
|
||||||
track.getSegments().forEach((segment) => {
|
|
||||||
segment.trkpt.forEach((point) => {
|
|
||||||
file.wpt.forEach((wpt, wptIndex) => {
|
|
||||||
let dist = distance(
|
|
||||||
point.getCoordinates(),
|
|
||||||
wpt.getCoordinates()
|
|
||||||
);
|
);
|
||||||
if (dist < closest[wptIndex].distance) {
|
|
||||||
closest[wptIndex].distance = dist;
|
|
||||||
closest[wptIndex].index = [index];
|
|
||||||
} else if (dist === closest[wptIndex].distance) {
|
|
||||||
closest[wptIndex].index.push(index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
file.trk.forEach((track, index) => {
|
file.trk.forEach((track, index) => {
|
||||||
let newFile = file.clone();
|
let newFile = file.clone();
|
||||||
let tracks = track.trkseg.map((segment, segmentIndex) => {
|
let tracks = track.trkseg.map((segment, segmentIndex) => {
|
||||||
@@ -495,9 +474,11 @@ export const fileActions = {
|
|||||||
newFile.replaceWaypoints(
|
newFile.replaceWaypoints(
|
||||||
0,
|
0,
|
||||||
file.wpt.length - 1,
|
file.wpt.length - 1,
|
||||||
closest
|
file.wpt.filter((wpt, wptIndex) =>
|
||||||
.filter((c) => c.index.includes(index))
|
closest[wptIndex].some(
|
||||||
.map((c) => file.wpt[c.wptIndex])
|
([trackIndex, segmentIndex]) => trackIndex === index
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
newFile._data.id = fileIds[index];
|
newFile._data.id = fileIds[index];
|
||||||
newFile.metadata.name =
|
newFile.metadata.name =
|
||||||
@@ -506,29 +487,9 @@ export const fileActions = {
|
|||||||
});
|
});
|
||||||
} else if (file.trk.length === 1) {
|
} else if (file.trk.length === 1) {
|
||||||
let fileIds = getFileIds(file.trk[0].trkseg.length);
|
let fileIds = getFileIds(file.trk[0].trkseg.length);
|
||||||
let closest = file.wpt.map((wpt, wptIndex) => {
|
let closest = file.wpt.map((wpt) =>
|
||||||
return {
|
getClosestTrackSegments(file, statistics, wpt.getCoordinates())
|
||||||
wptIndex: wptIndex,
|
|
||||||
index: [0],
|
|
||||||
distance: Number.MAX_VALUE,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
file.trk[0].trkseg.forEach((segment, index) => {
|
|
||||||
segment.trkpt.forEach((point) => {
|
|
||||||
file.wpt.forEach((wpt, wptIndex) => {
|
|
||||||
let dist = distance(
|
|
||||||
point.getCoordinates(),
|
|
||||||
wpt.getCoordinates()
|
|
||||||
);
|
);
|
||||||
if (dist < closest[wptIndex].distance) {
|
|
||||||
closest[wptIndex].distance = dist;
|
|
||||||
closest[wptIndex].index = [index];
|
|
||||||
} else if (dist === closest[wptIndex].distance) {
|
|
||||||
closest[wptIndex].index.push(index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
file.trk[0].trkseg.forEach((segment, index) => {
|
file.trk[0].trkseg.forEach((segment, index) => {
|
||||||
let newFile = file.clone();
|
let newFile = file.clone();
|
||||||
newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [
|
newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [
|
||||||
@@ -537,9 +498,11 @@ export const fileActions = {
|
|||||||
newFile.replaceWaypoints(
|
newFile.replaceWaypoints(
|
||||||
0,
|
0,
|
||||||
file.wpt.length - 1,
|
file.wpt.length - 1,
|
||||||
closest
|
file.wpt.filter((wpt, wptIndex) =>
|
||||||
.filter((c) => c.index.includes(index))
|
closest[wptIndex].some(
|
||||||
.map((c) => file.wpt[c.wptIndex])
|
([trackIndex, segmentIndex]) => segmentIndex === index
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
newFile._data.id = fileIds[index];
|
newFile._data.id = fileIds[index];
|
||||||
newFile.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`;
|
newFile.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ListItem, ListLevel } from '$lib/components/file-list/file-list';
|
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 {
|
export class GPXStatisticsTree {
|
||||||
level: ListLevel;
|
level: ListLevel;
|
||||||
@@ -21,23 +21,23 @@ export class GPXStatisticsTree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatisticsFor(item: ListItem): GPXStatistics {
|
getStatisticsFor(item: ListItem): GPXStatisticsGroup {
|
||||||
let statistics = new GPXStatistics();
|
let statistics = new GPXStatisticsGroup();
|
||||||
let id = item.getIdAtLevel(this.level);
|
let id = item.getIdAtLevel(this.level);
|
||||||
if (id === undefined || id === 'waypoints') {
|
if (id === undefined || id === 'waypoints') {
|
||||||
Object.keys(this.statistics).forEach((key) => {
|
Object.keys(this.statistics).forEach((key) => {
|
||||||
if (this.statistics[key] instanceof GPXStatistics) {
|
if (this.statistics[key] instanceof GPXStatistics) {
|
||||||
statistics.mergeWith(this.statistics[key]);
|
statistics.add(this.statistics[key]);
|
||||||
} else {
|
} else {
|
||||||
statistics.mergeWith(this.statistics[key].getStatisticsFor(item));
|
statistics.add(this.statistics[key].getStatisticsFor(item));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let child = this.statistics[id];
|
let child = this.statistics[id];
|
||||||
if (child instanceof GPXStatistics) {
|
if (child instanceof GPXStatistics) {
|
||||||
statistics.mergeWith(child);
|
statistics.add(child);
|
||||||
} else if (child !== undefined) {
|
} else if (child !== undefined) {
|
||||||
statistics.mergeWith(child.getStatisticsFor(item));
|
statistics.add(child.getStatisticsFor(item));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return statistics;
|
return statistics;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import { GPXStatistics } from 'gpx';
|
import { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
||||||
import { fileStateCollection, GPXFileState } from '$lib/logic/file-state';
|
import { fileStateCollection, GPXFileState } from '$lib/logic/file-state';
|
||||||
import {
|
import {
|
||||||
ListFileItem,
|
ListFileItem,
|
||||||
@@ -12,7 +12,7 @@ import { settings } from '$lib/logic/settings';
|
|||||||
const { fileOrder } = settings;
|
const { fileOrder } = settings;
|
||||||
|
|
||||||
export class SelectedGPXStatistics {
|
export class SelectedGPXStatistics {
|
||||||
private _statistics: Writable<GPXStatistics>;
|
private _statistics: Writable<GPXStatisticsGroup>;
|
||||||
private _files: Map<
|
private _files: Map<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
@@ -22,18 +22,21 @@ export class SelectedGPXStatistics {
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._statistics = writable(new GPXStatistics());
|
this._statistics = writable(new GPXStatisticsGroup());
|
||||||
this._files = new Map();
|
this._files = new Map();
|
||||||
selection.subscribe(() => this.update());
|
selection.subscribe(() => this.update());
|
||||||
fileOrder.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);
|
return this._statistics.subscribe(run, invalidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
let statistics = new GPXStatistics();
|
let statistics = new GPXStatisticsGroup();
|
||||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||||
let stats = fileStateCollection.getStatistics(fileId);
|
let stats = fileStateCollection.getStatistics(fileId);
|
||||||
if (stats) {
|
if (stats) {
|
||||||
@@ -43,7 +46,7 @@ export class SelectedGPXStatistics {
|
|||||||
!(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) ||
|
!(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) ||
|
||||||
first
|
first
|
||||||
) {
|
) {
|
||||||
statistics.mergeWith(stats.getStatisticsFor(item));
|
statistics.add(stats.getStatisticsFor(item));
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -76,7 +79,7 @@ export class SelectedGPXStatistics {
|
|||||||
|
|
||||||
export const gpxStatistics = new SelectedGPXStatistics();
|
export const gpxStatistics = new SelectedGPXStatistics();
|
||||||
|
|
||||||
export const slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined> =
|
export const slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined> =
|
||||||
writable(undefined);
|
writable(undefined);
|
||||||
|
|
||||||
gpxStatistics.subscribe(() => {
|
gpxStatistics.subscribe(() => {
|
||||||
|
|||||||
@@ -229,6 +229,9 @@ export function getConvertedVelocity(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConvertedTemperature(value: number) {
|
export function getConvertedTemperature(
|
||||||
return get(temperatureUnits) === 'celsius' ? value : celsiusToFahrenheit(value);
|
value: number,
|
||||||
|
targetTemperatureUnits = get(temperatureUnits)
|
||||||
|
) {
|
||||||
|
return targetTemperatureUnits === 'celsius' ? value : celsiusToFahrenheit(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import { type ClassValue, clsx } from 'clsx';
|
|||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
import { base } from '$app/paths';
|
import { base } from '$app/paths';
|
||||||
import { languages } from '$lib/languages';
|
import { languages } from '$lib/languages';
|
||||||
import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance } from 'gpx';
|
import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance, GPXFile } from 'gpx';
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt';
|
import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt';
|
||||||
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||||
import PNGReader from 'png.js';
|
import PNGReader from 'png.js';
|
||||||
|
import type { GPXStatisticsTree } from '$lib/logic/statistics-tree';
|
||||||
|
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
@@ -47,6 +49,59 @@ export function getClosestLinePoint(
|
|||||||
return closest;
|
return closest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getClosestTrackSegments(
|
||||||
|
file: GPXFile,
|
||||||
|
statistics: GPXStatisticsTree,
|
||||||
|
point: Coordinates
|
||||||
|
): [number, number][] {
|
||||||
|
let segmentBoundsDistances: [number, number, number][] = [];
|
||||||
|
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||||
|
let segmentStatistics = statistics.getStatisticsFor(
|
||||||
|
new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex)
|
||||||
|
);
|
||||||
|
let segmentBounds = segmentStatistics.global.bounds;
|
||||||
|
let northEast = segmentBounds.northEast;
|
||||||
|
let southWest = segmentBounds.southWest;
|
||||||
|
let bounds = new mapboxgl.LngLatBounds(southWest, northEast);
|
||||||
|
if (bounds.contains(point)) {
|
||||||
|
segmentBoundsDistances.push([0, trackIndex, segmentIndex]);
|
||||||
|
} else {
|
||||||
|
let northWest: Coordinates = { lat: northEast.lat, lon: southWest.lon };
|
||||||
|
let southEast: Coordinates = { lat: southWest.lat, lon: northEast.lon };
|
||||||
|
let distanceToBounds = Math.min(
|
||||||
|
crossarcDistance(northWest, northEast, point),
|
||||||
|
crossarcDistance(northEast, southEast, point),
|
||||||
|
crossarcDistance(southEast, southWest, point),
|
||||||
|
crossarcDistance(southWest, northWest, point)
|
||||||
|
);
|
||||||
|
segmentBoundsDistances.push([distanceToBounds, trackIndex, segmentIndex]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
segmentBoundsDistances.sort((a, b) => a[0] - b[0]);
|
||||||
|
|
||||||
|
let closest: { distance: number; indices: [number, number][] } = {
|
||||||
|
distance: Number.MAX_VALUE,
|
||||||
|
indices: [],
|
||||||
|
};
|
||||||
|
for (let s = 0; s < segmentBoundsDistances.length; s++) {
|
||||||
|
if (segmentBoundsDistances[s][0] > closest.distance) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const segment = file.getSegment(segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]);
|
||||||
|
segment.trkpt.forEach((pt) => {
|
||||||
|
let dist = distance(pt.getCoordinates(), point);
|
||||||
|
if (dist < closest.distance) {
|
||||||
|
closest.distance = dist;
|
||||||
|
closest.indices = [[segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]]];
|
||||||
|
} else if (dist === closest.distance) {
|
||||||
|
closest.indices.push([segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return closest.indices;
|
||||||
|
}
|
||||||
|
|
||||||
export function getElevation(
|
export function getElevation(
|
||||||
points: (TrackPoint | Waypoint | Coordinates)[],
|
points: (TrackPoint | Waypoint | Coordinates)[],
|
||||||
ELEVATION_ZOOM: number = 13,
|
ELEVATION_ZOOM: number = 13,
|
||||||
|
|||||||
@@ -473,7 +473,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": {
|
"homepage": {
|
||||||
"website": "Webseite",
|
"website": "Webseite",
|
||||||
"home": "Zuhause",
|
"home": "Startseite",
|
||||||
"app": "App",
|
"app": "App",
|
||||||
"contact": "Kontakt",
|
"contact": "Kontakt",
|
||||||
"reddit": "Reddit",
|
"reddit": "Reddit",
|
||||||
|
|||||||
@@ -324,6 +324,7 @@
|
|||||||
"bgMountains": "BGMountains",
|
"bgMountains": "BGMountains",
|
||||||
"usgs": "USGS",
|
"usgs": "USGS",
|
||||||
"bikerouterGravel": "bikerouter.de Gravel",
|
"bikerouterGravel": "bikerouter.de Gravel",
|
||||||
|
"openRailwayMap": "OpenRailwayMap",
|
||||||
"cyclOSMlite": "CyclOSM Lite",
|
"cyclOSMlite": "CyclOSM Lite",
|
||||||
"swisstopoSlope": "swisstopo Slope",
|
"swisstopoSlope": "swisstopo Slope",
|
||||||
"swisstopoHiking": "swisstopo Hiking",
|
"swisstopoHiking": "swisstopo Hiking",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"metadata": {
|
"metadata": {
|
||||||
"home_title": "el editor online de archivos GPX",
|
"home_title": "el editor online de archivos GPX",
|
||||||
"app_title": "app",
|
"app_title": "app",
|
||||||
"embed_title": "El editor online de archivos GPX",
|
"embed_title": " editor online de archivos GPX",
|
||||||
"help_title": "ayuda",
|
"help_title": "ayuda",
|
||||||
"404_title": "página no encontrada",
|
"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."
|
"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",
|
"eat-and-drink": "Nourriture et boissons",
|
||||||
"amenities": "Commodités",
|
"amenities": "Commodités",
|
||||||
"toilets": "Toilettes",
|
"toilets": "Toilettes",
|
||||||
"water": "Cours d'eau",
|
"water": "Eau potable",
|
||||||
"shower": "Douche",
|
"shower": "Douche",
|
||||||
"shelter": "Abri",
|
"shelter": "Abri",
|
||||||
"cemetery": "Cimetière",
|
"cemetery": "Cimetière",
|
||||||
@@ -443,7 +443,7 @@
|
|||||||
"convenience_store": "Épicerie",
|
"convenience_store": "Épicerie",
|
||||||
"crossing": "Croisement",
|
"crossing": "Croisement",
|
||||||
"department_store": "Grand magasin",
|
"department_store": "Grand magasin",
|
||||||
"drinking_water": "Cours d'eau",
|
"drinking_water": "Eau potable",
|
||||||
"exit": "Sortie",
|
"exit": "Sortie",
|
||||||
"lodge": "Refuge",
|
"lodge": "Refuge",
|
||||||
"lodging": "Hébergement",
|
"lodging": "Hébergement",
|
||||||
@@ -468,7 +468,7 @@
|
|||||||
"summit": "Sommet",
|
"summit": "Sommet",
|
||||||
"telephone": "Téléphone",
|
"telephone": "Téléphone",
|
||||||
"tunnel": "Tunnel",
|
"tunnel": "Tunnel",
|
||||||
"water_source": "Source d'eau"
|
"water_source": "Point d'eau"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"homepage": {
|
"homepage": {
|
||||||
|
|||||||
@@ -34,8 +34,8 @@
|
|||||||
"elevation_profile": "Profilo altimetrico",
|
"elevation_profile": "Profilo altimetrico",
|
||||||
"tree_file_view": "Vista ad albero",
|
"tree_file_view": "Vista ad albero",
|
||||||
"switch_basemap": "Passa alla mappa di base precedente",
|
"switch_basemap": "Passa alla mappa di base precedente",
|
||||||
"toggle_overlays": "Attiva/disattiva le sovrapposizioni",
|
"toggle_overlays": "Attiva / disattiva le sovrapposizioni",
|
||||||
"toggle_3d": "Attiva/disattiva 3D",
|
"toggle_3d": "Attiva / disattiva 3D",
|
||||||
"settings": "Impostazioni",
|
"settings": "Impostazioni",
|
||||||
"distance_units": "Unità distanza",
|
"distance_units": "Unità distanza",
|
||||||
"metric": "Metrico",
|
"metric": "Metrico",
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
"street_view_source": "Sorgente della vista stradale",
|
"street_view_source": "Sorgente della vista stradale",
|
||||||
"mapillary": "Mapillary",
|
"mapillary": "Mapillary",
|
||||||
"google": "Google",
|
"google": "Google",
|
||||||
"toggle_street_view": "Street View",
|
"toggle_street_view": "Vista stradale",
|
||||||
"layers": "Livelli della mappa...",
|
"layers": "Livelli della mappa...",
|
||||||
"distance_markers": "Indicatori di distanza",
|
"distance_markers": "Indicatori di distanza",
|
||||||
"direction_markers": "Frecce direzionali",
|
"direction_markers": "Frecce direzionali",
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
"tooltip": "Pianifica o modifica un percorsoo",
|
"tooltip": "Pianifica o modifica un percorsoo",
|
||||||
"activity": "Attività",
|
"activity": "Attività",
|
||||||
"use_routing": "Instradamento",
|
"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",
|
"allow_private": "Consenti strade private",
|
||||||
"reverse": {
|
"reverse": {
|
||||||
"button": "Inverti la traccia",
|
"button": "Inverti la traccia",
|
||||||
@@ -235,18 +235,18 @@
|
|||||||
"help_no_selection": "Seleziona un file per richiedere i dati di altitudine."
|
"help_no_selection": "Seleziona un file per richiedere i dati di altitudine."
|
||||||
},
|
},
|
||||||
"waypoint": {
|
"waypoint": {
|
||||||
"tooltip": "Creare e modificare punti di interesse",
|
"tooltip": "Creare e modificare punti d'interesse",
|
||||||
"icon": "Icona",
|
"icon": "Icona",
|
||||||
"link": "Collegamento",
|
"link": "Collegamento",
|
||||||
"longitude": "Longitudine",
|
"longitude": "Longitudine",
|
||||||
"latitude": "Latitudine",
|
"latitude": "Latitudine",
|
||||||
"create": "Creare un punto di interesse",
|
"create": "Creare un punto d'interesse",
|
||||||
"add": "Aggiungi punto di interesse al file",
|
"add": "Aggiungi punto d'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": "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 di interesse."
|
"help_no_selection": "Selezionare un file per creare o modificare punti d'interesse."
|
||||||
},
|
},
|
||||||
"reduce": {
|
"reduce": {
|
||||||
"tooltip": "Riduci il numero di punti della traccia",
|
"tooltip": "Riduci il numero di punti GPS",
|
||||||
"tolerance": "Tolleranza",
|
"tolerance": "Tolleranza",
|
||||||
"number_of_points": "Numero di punti GPS",
|
"number_of_points": "Numero di punti GPS",
|
||||||
"button": "Minimizza",
|
"button": "Minimizza",
|
||||||
@@ -254,14 +254,14 @@
|
|||||||
"help_no_selection": "Selezionare una traccia per ridurre il numero dei suoi punti GPS."
|
"help_no_selection": "Selezionare una traccia per ridurre il numero dei suoi punti GPS."
|
||||||
},
|
},
|
||||||
"clean": {
|
"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_trackpoints": "Eliminare punti GPS",
|
||||||
"delete_waypoints": "Cancella punti d'interesse",
|
"delete_waypoints": "Cancella punti d'interesse",
|
||||||
"delete_inside": "Elimina all'interno della selezione",
|
"delete_inside": "Elimina all'interno della selezione",
|
||||||
"delete_outside": "Elimina fuori dalla selezione",
|
"delete_outside": "Elimina fuori dalla selezione",
|
||||||
"button": "Elimina",
|
"button": "Elimina",
|
||||||
"help": "Selezionare un'area rettangolare sulla mappa per rimuovere 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 di interesse."
|
"help_no_selection": "Seleziona una traccia per pulire i punti GPS e i punti d'interesse."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layers": {
|
"layers": {
|
||||||
@@ -273,7 +273,7 @@
|
|||||||
"new": "Nuovo livello personalizzato",
|
"new": "Nuovo livello personalizzato",
|
||||||
"edit": "Modifica livello personalizzato",
|
"edit": "Modifica livello personalizzato",
|
||||||
"urls": "URL(s)",
|
"urls": "URL(s)",
|
||||||
"url_placeholder": "WMTS, WMS o Mapbox stile JSON",
|
"url_placeholder": "WMTS, WMS o JSON in stile Mapbox",
|
||||||
"max_zoom": "Zoom massimo",
|
"max_zoom": "Zoom massimo",
|
||||||
"layer_type": "Tipo del layer",
|
"layer_type": "Tipo del layer",
|
||||||
"basemap": "Mappa Base",
|
"basemap": "Mappa Base",
|
||||||
@@ -309,7 +309,7 @@
|
|||||||
"linz": "LINZ Topo",
|
"linz": "LINZ Topo",
|
||||||
"linzTopo": "LINZ Topo50",
|
"linzTopo": "LINZ Topo50",
|
||||||
"swisstopoRaster": "swisstopo Raster",
|
"swisstopoRaster": "swisstopo Raster",
|
||||||
"swisstopoVector": "Swisstopo Vector",
|
"swisstopoVector": "swisstopo Vector",
|
||||||
"swisstopoSatellite": "swisstopo Satellite",
|
"swisstopoSatellite": "swisstopo Satellite",
|
||||||
"ignBe": "IGN Topo",
|
"ignBe": "IGN Topo",
|
||||||
"ignFrPlan": "IGN Plan",
|
"ignFrPlan": "IGN Plan",
|
||||||
@@ -318,25 +318,25 @@
|
|||||||
"ignFrSatellite": "Satellitare IGN",
|
"ignFrSatellite": "Satellitare IGN",
|
||||||
"ignEs": "IGN Topo",
|
"ignEs": "IGN Topo",
|
||||||
"ignEsSatellite": "Satellitare IGN",
|
"ignEsSatellite": "Satellitare IGN",
|
||||||
"ordnanceSurvey": "Sondaggio Ordnance",
|
"ordnanceSurvey": "Ordnance Survey",
|
||||||
"norwayTopo": "Topografisk Norgeskart 4",
|
"norwayTopo": "Topografisk Norgeskart 4",
|
||||||
"finlandTopo": "Carta topografica del vecchio Catasto svedese",
|
"finlandTopo": "Lantmäteriverket Terrängkarta",
|
||||||
"bgMountains": "BGMountains",
|
"bgMountains": "BGMountains",
|
||||||
"usgs": "USGS",
|
"usgs": "USGS",
|
||||||
"bikerouterGravel": "bikerouter.de Gravel",
|
"bikerouterGravel": "bikerouter.de Gravel",
|
||||||
"cyclOSMlite": "CyclOSM Lite",
|
"cyclOSMlite": "CyclOSM Lite",
|
||||||
"swisstopoSlope": "Carta topografica Svizzera Pendenza",
|
"swisstopoSlope": "swisstopo Pendenza",
|
||||||
"swisstopoHiking": "Carta topografica Svedese Escursione",
|
"swisstopoHiking": "swisstopo Escursione",
|
||||||
"swisstopoHikingClosures": "Carta topografica Svizzera Fine escursione",
|
"swisstopoHikingClosures": "swisstopo Fine escursione",
|
||||||
"swisstopoCycling": "Carta topografica Svizzera Ciclabile",
|
"swisstopoCycling": "swisstopo Ciclabile",
|
||||||
"swisstopoCyclingClosures": "Carta topografica Svizzera fine ciclabile",
|
"swisstopoCyclingClosures": "swisstopo Fine ciclabile",
|
||||||
"swisstopoMountainBike": "Carta topografica Svizzera MTB",
|
"swisstopoMountainBike": "swisstopo MTB",
|
||||||
"swisstopoMountainBikeClosures": "Carta topografica Svizzera fine MTB",
|
"swisstopoMountainBikeClosures": "swisstopo Fine MTB",
|
||||||
"swisstopoSkiTouring": "Carta topografica Svizzera pista sci",
|
"swisstopoSkiTouring": "swisstopo Sci Alpinismo",
|
||||||
"ignFrCadastre": "IGN Catasto",
|
"ignFrCadastre": "IGN Catasto",
|
||||||
"ignSlope": "Pendenza IGN",
|
"ignSlope": "IGN Pendenza",
|
||||||
"ignSkiTouring": "IGN Sciescursionismo",
|
"ignSkiTouring": "IGN Sci Alpinismo",
|
||||||
"waymarked_trails": "Waymarked Trails",
|
"waymarked_trails": "Sentieri Segnalati",
|
||||||
"waymarkedTrailsHiking": "Escursionismo",
|
"waymarkedTrailsHiking": "Escursionismo",
|
||||||
"waymarkedTrailsCycling": "Ciclismo",
|
"waymarkedTrailsCycling": "Ciclismo",
|
||||||
"waymarkedTrailsMTB": "MTB",
|
"waymarkedTrailsMTB": "MTB",
|
||||||
@@ -406,11 +406,11 @@
|
|||||||
"feet": "ft",
|
"feet": "ft",
|
||||||
"kilometers": "km",
|
"kilometers": "km",
|
||||||
"miles": "mi",
|
"miles": "mi",
|
||||||
"nautical_miles": "nm",
|
"nautical_miles": "NM",
|
||||||
"celsius": "°C",
|
"celsius": "°C",
|
||||||
"fahrenheit": "°F",
|
"fahrenheit": "°F",
|
||||||
"kilometers_per_hour": "km/h",
|
"kilometers_per_hour": "km/h",
|
||||||
"miles_per_hour": "mph",
|
"miles_per_hour": "mi/h",
|
||||||
"minutes_per_kilometer": "min/km",
|
"minutes_per_kilometer": "min/km",
|
||||||
"minutes_per_mile": "min/mi",
|
"minutes_per_mile": "min/mi",
|
||||||
"minutes_per_nautical_mile": "min/nm",
|
"minutes_per_nautical_mile": "min/nm",
|
||||||
@@ -426,8 +426,8 @@
|
|||||||
"tracks": "Tracce",
|
"tracks": "Tracce",
|
||||||
"segment": "Segmento",
|
"segment": "Segmento",
|
||||||
"segments": "Segmenti",
|
"segments": "Segmenti",
|
||||||
"waypoint": "Punto di interesse",
|
"waypoint": "Punto d'interesse",
|
||||||
"waypoints": "Punti di interesse",
|
"waypoints": "Punti d'interesse",
|
||||||
"symbol": {
|
"symbol": {
|
||||||
"alert": "Avviso",
|
"alert": "Avviso",
|
||||||
"anchor": "Ancora",
|
"anchor": "Ancora",
|
||||||
@@ -483,13 +483,13 @@
|
|||||||
"email": "Email",
|
"email": "Email",
|
||||||
"contribute": "Contribuire",
|
"contribute": "Contribuire",
|
||||||
"supported_by": "supportato da",
|
"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": "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": "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.",
|
"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": "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": "Visualizzazione dei dati",
|
||||||
"data_visualization_description": "Un profilo di elevazione interattivo con statistiche dettagliate per analizzare attività registrate e obiettivi futuri.",
|
"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",
|
"identity": "Gratuito, senza pubblicità e open source",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"metadata": {
|
"metadata": {
|
||||||
"home_title": "edytor online plików GPX",
|
"home_title": "edytor online plików GPX",
|
||||||
"app_title": "Aplikacja",
|
"app_title": "Aplikacja",
|
||||||
"embed_title": "Online'owy edytor plików GPX",
|
"embed_title": "Edytor plików GPX online",
|
||||||
"help_title": "pomoc",
|
"help_title": "pomoc",
|
||||||
"404_title": "nie odnaleziono strony",
|
"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."
|
"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",
|
"undo": "Cofnij",
|
||||||
"redo": "Ponów",
|
"redo": "Ponów",
|
||||||
"delete": "Usuń",
|
"delete": "Usuń",
|
||||||
"delete_all": "Delete all",
|
"delete_all": "Usuń wszystko",
|
||||||
"select_all": "Zaznacz wszystko",
|
"select_all": "Zaznacz wszystko",
|
||||||
"view": "Widok",
|
"view": "Widok",
|
||||||
"elevation_profile": "Profil wysokości",
|
"elevation_profile": "Profil wysokości",
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
"center": "Wyśrodkuj",
|
"center": "Wyśrodkuj",
|
||||||
"open_in": "Otwórz w",
|
"open_in": "Otwórz w",
|
||||||
"copy_coordinates": "Kopiuj współrzędne",
|
"copy_coordinates": "Kopiuj współrzędne",
|
||||||
"edit_osm": "Edit in OpenStreetMap"
|
"edit_osm": "Edytuj w OpenStreetMap"
|
||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"routing": {
|
"routing": {
|
||||||
@@ -353,7 +353,7 @@
|
|||||||
"water": "Woda",
|
"water": "Woda",
|
||||||
"shower": "Prysznic",
|
"shower": "Prysznic",
|
||||||
"shelter": "Schronienie",
|
"shelter": "Schronienie",
|
||||||
"cemetery": "Cemetery",
|
"cemetery": "Cmentarz",
|
||||||
"motorized": "Samochody i motocykle",
|
"motorized": "Samochody i motocykle",
|
||||||
"fuel-station": "Stacja paliw",
|
"fuel-station": "Stacja paliw",
|
||||||
"parking": "Parking",
|
"parking": "Parking",
|
||||||
|
|||||||
@@ -21,18 +21,18 @@
|
|||||||
"export_all": "Xuất tất cả...",
|
"export_all": "Xuất tất cả...",
|
||||||
"export_options": "Tùy chọn xuất",
|
"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_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_file": "Tải tệp xuống",
|
||||||
"download_files": "Tải xuống tất cả",
|
"download_files": "Tải xuống tất cả",
|
||||||
"edit": "Chỉnh sửa",
|
"edit": "Chỉnh sửa",
|
||||||
"undo": "Hoàn tác",
|
"undo": "Hoàn tác",
|
||||||
"redo": "Khôi phục",
|
"redo": "Khôi phục",
|
||||||
"delete": "Xóa",
|
"delete": "Xóa",
|
||||||
"delete_all": "Delete all",
|
"delete_all": "Xóa tất cả",
|
||||||
"select_all": "Chọn tất cả",
|
"select_all": "Chọn tất cả",
|
||||||
"view": "Xem",
|
"view": "Xem",
|
||||||
"elevation_profile": "Thông tin độ cao",
|
"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 đó",
|
"switch_basemap": "Quay lại bản đồ trước đó",
|
||||||
"toggle_overlays": "Thay đổi lớp phủ",
|
"toggle_overlays": "Thay đổi lớp phủ",
|
||||||
"toggle_3d": "Chuyển đổi 3D",
|
"toggle_3d": "Chuyển đổi 3D",
|
||||||
@@ -57,35 +57,35 @@
|
|||||||
"layers": "Lớp bản đồ...",
|
"layers": "Lớp bản đồ...",
|
||||||
"distance_markers": "Đánh dấu khoảng cách",
|
"distance_markers": "Đánh dấu khoảng cách",
|
||||||
"direction_markers": "Mũi tên định hướng",
|
"direction_markers": "Mũi tên định hướng",
|
||||||
"help": "Help",
|
"help": "Trợ giúp",
|
||||||
"more": "More...",
|
"more": "Chi tiết...",
|
||||||
"donate": "Donate",
|
"donate": "Ủng hộ",
|
||||||
"ctrl": "Ctrl",
|
"ctrl": "Ctrl",
|
||||||
"click": "Click",
|
"click": "Nhấp chuột",
|
||||||
"drag": "Drag",
|
"drag": "Kéo",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"button": "Info...",
|
"button": "Thông tin...",
|
||||||
"name": "Name",
|
"name": "Tên",
|
||||||
"description": "Description",
|
"description": "Mô tả",
|
||||||
"save": "Save"
|
"save": "Lưu"
|
||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
"button": "Appearance...",
|
"button": "Diện mạo...",
|
||||||
"color": "Color",
|
"color": "Màu sắc",
|
||||||
"opacity": "Opacity",
|
"opacity": "Độ trong suốt",
|
||||||
"width": "Width"
|
"width": "Độ rộng"
|
||||||
},
|
},
|
||||||
"hide": "Hide",
|
"hide": "Ẩn",
|
||||||
"unhide": "Unhide",
|
"unhide": "Bỏ ẩn",
|
||||||
"center": "Center",
|
"center": "Giữa",
|
||||||
"open_in": "Open in",
|
"open_in": "Mở ra",
|
||||||
"copy_coordinates": "Copy coordinates",
|
"copy_coordinates": "Sao chép tọa độ",
|
||||||
"edit_osm": "Edit in OpenStreetMap"
|
"edit_osm": ""
|
||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"routing": {
|
"routing": {
|
||||||
"tooltip": "Plan or edit a route",
|
"tooltip": "Plan or edit a route",
|
||||||
"activity": "Activity",
|
"activity": "Hoạt động",
|
||||||
"use_routing": "Routing",
|
"use_routing": "Routing",
|
||||||
"use_routing_tooltip": "Connect anchor points via road network, or in a straight line if disabled",
|
"use_routing_tooltip": "Connect anchor points via road network, or in a straight line if disabled",
|
||||||
"allow_private": "Allow private roads",
|
"allow_private": "Allow private roads",
|
||||||
@@ -94,14 +94,14 @@
|
|||||||
"tooltip": "Reverse the direction of the route"
|
"tooltip": "Reverse the direction of the route"
|
||||||
},
|
},
|
||||||
"route_back_to_start": {
|
"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"
|
"tooltip": "Connect the last point of the route with the starting point"
|
||||||
},
|
},
|
||||||
"round_trip": {
|
"round_trip": {
|
||||||
"button": "Round trip",
|
"button": "Round trip",
|
||||||
"tooltip": "Return to the starting point by the same route"
|
"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_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.",
|
"help": "Click on the map to add a new anchor point, or drag existing ones to change the route.",
|
||||||
"activities": {
|
"activities": {
|
||||||
@@ -127,15 +127,15 @@
|
|||||||
"wood": "Wood",
|
"wood": "Wood",
|
||||||
"compacted": "Compacted gravel",
|
"compacted": "Compacted gravel",
|
||||||
"fine_gravel": "Fine gravel",
|
"fine_gravel": "Fine gravel",
|
||||||
"gravel": "Gravel",
|
"gravel": "Sỏi",
|
||||||
"pebblestone": "Pebblestone",
|
"pebblestone": "Pebblestone",
|
||||||
"rock": "Rock",
|
"rock": "Đá",
|
||||||
"dirt": "Dirt",
|
"dirt": "Đất",
|
||||||
"ground": "Ground",
|
"ground": "Ground",
|
||||||
"earth": "Earth",
|
"earth": "Earth",
|
||||||
"mud": "Mud",
|
"mud": "Mud",
|
||||||
"sand": "Sand",
|
"sand": "Sand",
|
||||||
"grass": "Grass",
|
"grass": "Cỏ",
|
||||||
"grass_paver": "Grass paver",
|
"grass_paver": "Grass paver",
|
||||||
"clay": "Clay",
|
"clay": "Clay",
|
||||||
"stone": "Stone"
|
"stone": "Stone"
|
||||||
|
|||||||
@@ -230,8 +230,8 @@
|
|||||||
"help_invalid_selection": "须先选择包含多个轨迹的文件以提取。"
|
"help_invalid_selection": "须先选择包含多个轨迹的文件以提取。"
|
||||||
},
|
},
|
||||||
"elevation": {
|
"elevation": {
|
||||||
"button": "请求数据",
|
"button": "请求海拔数据",
|
||||||
"help": "请求成功后将使用 Mapbox 海拔数据替换原有数据。",
|
"help": "请求成功后将移除原有的海拔数据,并使用 Mapbox 的海拔数据替换原有数据。",
|
||||||
"help_no_selection": "选择要请求海拔数据的文件。"
|
"help_no_selection": "选择要请求海拔数据的文件。"
|
||||||
},
|
},
|
||||||
"waypoint": {
|
"waypoint": {
|
||||||
|
|||||||
Reference in New Issue
Block a user