5 Commits

Author SHA1 Message Date
vcoppe
9ab24ed8ca New translations file.mdx (Basque) 2025-12-15 17:33:23 +01:00
vcoppe
6cb9716d30 New translations en.json (Basque) 2025-12-15 17:33:22 +01:00
vcoppe
7e60a8fc33 New translations edit.mdx (Dutch) 2025-12-12 19:46:08 +01:00
vcoppe
d57121dc73 New translations getting-started.mdx (Dutch) 2025-12-12 19:46:07 +01:00
vcoppe
41b55157f5 New translations en.json (Spanish) 2025-12-11 14:52:24 +01:00
46 changed files with 973 additions and 1151 deletions

View File

@@ -1,3 +1,6 @@
website/src/lib/components/ui # Ignore files for PNPM, NPM and YARN
website/src/lib/docs/**/*.mdx pnpm-lock.yaml
**/*.webmanifest package-lock.json
yarn.lock
src/lib/components/ui
*.mdx

View File

@@ -25,7 +25,7 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"postinstall": "npm run build", "postinstall": "npm run build",
"lint": "prettier --check . --config ../.prettierrc && eslint .", "lint": "prettier --check . && eslint .",
"format": "prettier --write . --config ../.prettierrc" "format": "prettier --write ."
} }
} }

View File

@@ -1,5 +1,4 @@
import { ramerDouglasPeucker } from './simplify'; import { ramerDouglasPeucker } from './simplify';
import { GPXStatistics, GPXStatisticsGroup, TrackPointLocalStatistics } from './statistics';
import { import {
Coordinates, Coordinates,
GPXFileAttributes, GPXFileAttributes,
@@ -37,6 +36,7 @@ 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,6 +76,14 @@ 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());
} }
@@ -140,9 +148,7 @@ export class GPXFile extends GPXTreeNode<Track> {
}, },
}, },
}; };
this.wpt = gpx.wpt this.wpt = gpx.wpt ? gpx.wpt.map((waypoint) => new Waypoint(waypoint)) : [];
? 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)));
@@ -180,6 +186,9 @@ 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> {
@@ -200,16 +209,8 @@ 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 {
const style = this.trk return this.trk
.map((track) => track.getStyle()) .map((track) => track.getStyle())
.reduce( .reduce(
(acc, style) => { (acc, style) => {
@@ -219,6 +220,8 @@ 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 &&
@@ -242,10 +245,6 @@ export class GPXFile extends GPXTreeNode<Track> {
width: [], width: [],
} }
); );
if (style.color.length === 0 && defaultColor) {
style.color.push(defaultColor);
}
return style;
} }
clone(): GPXFile { clone(): GPXFile {
@@ -808,7 +807,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, index) => new TrackPoint(point, index)); this.trkpt = segment.trkpt.map((point) => new TrackPoint(point));
if (segment.hasOwnProperty('_data')) { if (segment.hasOwnProperty('_data')) {
this._data = segment._data; this._data = segment._data;
} }
@@ -820,12 +819,12 @@ export class TrackSegment extends GPXTreeLeaf {
_computeStatistics(): GPXStatistics { _computeStatistics(): GPXStatistics {
let statistics = new GPXStatistics(); let statistics = new GPXStatistics();
statistics.global.length = this.trkpt.length; statistics.local.points = this.trkpt.map((point) => point);
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) {
@@ -834,18 +833,19 @@ export class TrackSegment extends GPXTreeLeaf {
statistics.global.distance.total += dist; statistics.global.distance.total += dist;
} }
statistics.local.data[i].distance.total = statistics.global.distance.total; statistics.local.distance.total.push(statistics.global.distance.total);
// time // time
if (points[i].time === undefined) { if (points[i].time === undefined) {
statistics.local.data[i].time.total = 0; statistics.local.time.total.push(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.data[i].time.total = statistics.local.time.total.push(
(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.data[i].distance.moving = statistics.global.distance.moving; statistics.local.distance.moving.push(statistics.global.distance.moving);
statistics.local.data[i].time.moving = statistics.global.time.moving; statistics.local.time.moving.push(statistics.global.time.moving);
// bounds // bounds
statistics.global.bounds.southWest.lat = Math.min( statistics.global.bounds.southWest.lat = Math.min(
@@ -961,22 +961,13 @@ 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;
timeWindowSmoothing( statistics.local.speed = timeWindowSmoothing(points, 10000, (start, end) =>
points, points[start].time && points[end].time
10000, ? (3600 *
(start, end) => (statistics.local.distance.total[end] -
points[start].time && points[end].time statistics.local.distance.total[start])) /
? (3600 * Math.max((points[end].time.getTime() - points[start].time.getTime()) / 1000, 1)
(statistics.local.data[end].distance.total - : undefined
statistics.local.data[start].distance.total)) /
Math.max(
(points[end].time.getTime() - points[start].time.getTime()) / 1000,
1
)
: undefined,
(value, index) => {
statistics.local.data[index].speed = value;
}
); );
return statistics; return statistics;
@@ -996,65 +987,53 @@ export class TrackSegment extends GPXTreeLeaf {
let cumulEle = 0; let cumulEle = 0;
let currentStart = start; let currentStart = start;
let currentEnd = start; let currentEnd = start;
let prevSmoothedEle = 0; let smoothedEle = distanceWindowSmoothing(start, end + 1, statistics, 0.1, (s, e) => {
distanceWindowSmoothing( for (let i = currentStart; i < s; i++) {
start, cumulEle -= this.trkpt[i].ele ?? 0;
end + 1,
statistics,
0.1,
(s, e) => {
for (let i = currentStart; i < s; i++) {
cumulEle -= this.trkpt[i].ele ?? 0;
}
for (let i = currentEnd; i <= e; i++) {
cumulEle += this.trkpt[i].ele ?? 0;
}
currentStart = s;
currentEnd = e + 1;
return cumulEle / (e - s + 1);
},
(smoothedEle, j) => {
if (j === start) {
smoothedEle = this.trkpt[start].ele ?? 0;
prevSmoothedEle = smoothedEle;
} else if (j === end) {
smoothedEle = this.trkpt[end].ele ?? 0;
}
const ele = smoothedEle - prevSmoothedEle;
if (ele > 0) {
statistics.global.elevation.gain += ele;
} else if (ele < 0) {
statistics.global.elevation.loss -= ele;
}
prevSmoothedEle = smoothedEle;
if (j < end) {
statistics.local.data[j].elevation.gain = statistics.global.elevation.gain;
statistics.local.data[j].elevation.loss = statistics.global.elevation.loss;
}
} }
); for (let i = currentEnd; i <= e; i++) {
} cumulEle += this.trkpt[i].ele ?? 0;
if (statistics.global.length > 0) { }
statistics.local.data[statistics.global.length - 1].elevation.gain = currentStart = s;
statistics.global.elevation.gain; currentEnd = e + 1;
statistics.local.data[statistics.global.length - 1].elevation.loss = return cumulEle / (e - s + 1);
statistics.global.elevation.loss; });
} smoothedEle[0] = this.trkpt[start].ele ?? 0;
smoothedEle[smoothedEle.length - 1] = this.trkpt[end].ele ?? 0;
for (let j = start; j < end; j++) {
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
const ele = smoothedEle[j - start + 1] - smoothedEle[j - start];
if (ele > 0) {
statistics.global.elevation.gain += ele;
} else if (ele < 0) {
statistics.global.elevation.loss -= ele;
}
}
}
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
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.data[end].distance.total - statistics.local.distance.total[end] - statistics.local.distance.total[start];
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++) {
statistics.local.data[j].slope.segment = (0.1 * ele) / dist; slope.push((0.1 * ele) / dist);
statistics.local.data[j].slope.length = dist; length.push(dist);
} }
} }
distanceWindowSmoothing( statistics.local.slope.segment = slope;
statistics.local.slope.length = length;
statistics.local.slope.at = distanceWindowSmoothing(
0, 0,
this.trkpt.length, this.trkpt.length,
statistics, statistics,
@@ -1062,12 +1041,8 @@ 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.data[end].distance.total - statistics.local.distance.total[end] - statistics.local.distance.total[start];
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;
} }
); );
} }
@@ -1317,7 +1292,13 @@ 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(og.trkpt, totalTime, lastPoint, startTime, statistics); let trkpt = withArtificialTimestamps(
og.trkpt,
totalTime,
lastPoint,
startTime,
statistics.local.slope.at
);
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
} }
@@ -1326,7 +1307,6 @@ export class TrackSegment extends GPXTreeLeaf {
} }
} }
const emptyExtensions: Record<string, string> = {};
export class TrackPoint { export class TrackPoint {
[immerable] = true; [immerable] = true;
@@ -1337,7 +1317,7 @@ export class TrackPoint {
_data: { [key: string]: any } = {}; _data: { [key: string]: any } = {};
constructor(point: (TrackPointType & { _data?: any }) | TrackPoint, index?: number) { constructor(point: (TrackPointType & { _data?: any }) | TrackPoint) {
this.attributes = point.attributes; this.attributes = point.attributes;
this.ele = point.ele; this.ele = point.ele;
this.time = point.time; this.time = point.time;
@@ -1345,9 +1325,6 @@ 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 {
@@ -1421,7 +1398,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 {
@@ -1491,18 +1468,11 @@ export class TrackPoint {
clone(): TrackPoint { clone(): TrackPoint {
return new TrackPoint({ return new TrackPoint({
attributes: { attributes: cloneJSON(this.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: this.extensions ? cloneJSON(this.extensions) : undefined, extensions: cloneJSON(this.extensions),
_data: { _data: cloneJSON(this._data),
index: this._data?.index,
anchor: this._data?.anchor,
zoom: this._data?.zoom,
},
}); });
} }
} }
@@ -1521,7 +1491,7 @@ export class Waypoint {
type?: string; type?: string;
_data: { [key: string]: any } = {}; _data: { [key: string]: any } = {};
constructor(waypoint: (WaypointType & { _data?: any }) | Waypoint, index?: number) { constructor(waypoint: (WaypointType & { _data?: any }) | Waypoint) {
this.attributes = waypoint.attributes; this.attributes = waypoint.attributes;
this.ele = waypoint.ele; this.ele = waypoint.ele;
this.time = waypoint.time; this.time = waypoint.time;
@@ -1540,9 +1510,6 @@ 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 {
@@ -1590,10 +1557,7 @@ export class Waypoint {
clone(): Waypoint { clone(): Waypoint {
return new Waypoint({ return new Waypoint({
attributes: { attributes: cloneJSON(this.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,
@@ -1642,6 +1606,305 @@ 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,
@@ -1675,9 +1938,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.data[point1._data.index].distance.total * 1000; let x1 = statistics.local.distance.total[point1._data.index] * 1000;
let x2 = statistics.local.data[point2._data.index].distance.total * 1000; let x2 = statistics.local.distance.total[point2._data.index] * 1000;
let x3 = statistics.local.data[point3._data.index].distance.total * 1000; let x3 = statistics.local.distance.total[point3._data.index] * 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;
@@ -1696,9 +1959,10 @@ 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
callback: (value: number, index: number) => void ): number[] {
): void { let result = [];
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) {
@@ -1708,8 +1972,10 @@ function windowSmoothing(
while (end < right && distance(i, end) <= window) { while (end < right && distance(i, end) <= window) {
end++; end++;
} }
callback(compute(start, end - 1), i); result.push(compute(start, end - 1));
} }
return result;
} }
function distanceWindowSmoothing( function distanceWindowSmoothing(
@@ -1717,35 +1983,30 @@ 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
callback: (value: number, index: number) => void ): number[] {
): void { return windowSmoothing(
windowSmoothing(
left, left,
right, right,
(index1, index2) => (index1, index2) =>
statistics.local.data[index2].distance.total - statistics.local.distance.total[index2] - statistics.local.distance.total[index1],
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
callback: (value: number, index: number) => void ): number[] {
): void { return windowSmoothing(
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
); );
} }
@@ -1797,14 +2058,14 @@ function withArtificialTimestamps(
totalTime: number, totalTime: number,
lastPoint: TrackPoint | undefined, lastPoint: TrackPoint | undefined,
startTime: Date, startTime: Date,
statistics: GPXStatistics slope: number[]
): 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 * statistics.local.data[i].slope.at))); let w = dist * (0.5 + 1 / (1 + Math.exp(-0.2 * slope[i])));
weight.push(w); weight.push(w);
totalWeight += w; totalWeight += w;
} }

View File

@@ -1,5 +1,4 @@
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';

View File

@@ -59,13 +59,13 @@ function ramerDouglasPeuckerRecursive(
} }
export function crossarcDistance( export function crossarcDistance(
point1: TrackPoint | Coordinates, point1: TrackPoint,
point2: TrackPoint | Coordinates, point2: TrackPoint,
point3: TrackPoint | Coordinates point3: TrackPoint | Coordinates
): number { ): number {
return crossarc( return crossarc(
point1 instanceof TrackPoint ? point1.getCoordinates() : point1, point1.getCoordinates(),
point2 instanceof TrackPoint ? point2.getCoordinates() : point2, point2.getCoordinates(),
point3 instanceof TrackPoint ? point3.getCoordinates() : point3 point3 instanceof TrackPoint ? point3.getCoordinates() : point3
); );
} }

View File

@@ -1,391 +0,0 @@
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
)
);
}
}
}

View File

@@ -1,17 +1,17 @@
{ {
"$schema": "https://shadcn-svelte.com/schema.json", "$schema": "https://shadcn-svelte.com/schema.json",
"style": "default", "style": "default",
"tailwind": { "tailwind": {
"css": "src/app.css", "css": "src/app.css",
"baseColor": "slate" "baseColor": "slate"
}, },
"aliases": { "aliases": {
"components": "$lib/components", "components": "$lib/components",
"utils": "$lib/utils", "utils": "$lib/utils",
"ui": "$lib/components/ui", "ui": "$lib/components/ui",
"hooks": "$lib/hooks", "hooks": "$lib/hooks",
"lib": "$lib" "lib": "$lib"
}, },
"typescript": true, "typescript": true,
"registry": "https://shadcn-svelte.com/registry" "registry": "https://shadcn-svelte.com/registry"
} }

View File

@@ -14,7 +14,7 @@
"@mapbox/sphericalmercator": "^2.0.1", "@mapbox/sphericalmercator": "^2.0.1",
"@mapbox/tilebelt": "^2.0.2", "@mapbox/tilebelt": "^2.0.2",
"@types/mapbox__sphericalmercator": "^1.2.3", "@types/mapbox__sphericalmercator": "^1.2.3",
"chart.js": "^4.5.1", "chart.js": "^4.4.9",
"chartjs-plugin-zoom": "^2.2.0", "chartjs-plugin-zoom": "^2.2.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dexie": "^4.0.11", "dexie": "^4.0.11",
@@ -22,7 +22,7 @@
"gpx": "file:../gpx", "gpx": "file:../gpx",
"immer": "^10.1.1", "immer": "^10.1.1",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"mapbox-gl": "^3.17.0", "mapbox-gl": "^3.16.0",
"mapillary-js": "^4.1.2", "mapillary-js": "^4.1.2",
"png.js": "^0.2.1", "png.js": "^0.2.1",
"sanitize-html": "^2.17.0", "sanitize-html": "^2.17.0",
@@ -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.14.4", "bits-ui": "^2.12.0",
"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.14.4", "version": "2.12.0",
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.14.4.tgz", "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.12.0.tgz",
"integrity": "sha512-W6kenhnbd/YVvur+DKkaVJ6GldE53eLewur5AhUCqslYQ0vjZr8eWlOfwZnMiPB+PF5HMVqf61vXBvmyrAmPWg==", "integrity": "sha512-8NF4ILNyAJlIxDXpl/akGXGBV5QmZAe+8gTfPttM5P6/+LrijumcSfFXY5cr4QkXwTmLA7H5stYpbgJf2XFJvg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -3664,9 +3664,9 @@
} }
}, },
"node_modules/chart.js": { "node_modules/chart.js": {
"version": "4.5.1", "version": "4.4.9",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@kurkle/color": "^0.3.0" "@kurkle/color": "^0.3.0"
@@ -6069,14 +6069,12 @@
} }
}, },
"node_modules/mapbox-gl": { "node_modules/mapbox-gl": {
"version": "3.17.0", "version": "3.16.0",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.17.0.tgz", "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.16.0.tgz",
"integrity": "sha512-nCrDKRlr5di6xUksUDslNWwxroJ5yv1hT8pyVFtcpWJOOKsYQxF/wOFTMie8oxMnXeFkrz1Tl1TwA1XN1yX0KA==", "integrity": "sha512-rluV1Zp/0oHf1Y9BV+nePRNnKyTdljko3E19CzO5rBqtQaNUYS0ePCMPRtxOuWRwSdKp3f9NWJkOCjemM8nmjw==",
"license": "SEE LICENSE IN LICENSE.txt", "license": "SEE LICENSE IN LICENSE.txt",
"workspaces": [ "workspaces": [
"src/style-spec", "src/style-spec",
"test/build/vite",
"test/build/webpack",
"test/build/typings" "test/build/typings"
], ],
"dependencies": { "dependencies": {
@@ -6104,6 +6102,7 @@
"pbf": "^4.0.1", "pbf": "^4.0.1",
"potpack": "^2.0.0", "potpack": "^2.0.0",
"quickselect": "^3.0.0", "quickselect": "^3.0.0",
"serialize-to-js": "^3.1.2",
"supercluster": "^8.0.1", "supercluster": "^8.0.1",
"tinyqueue": "^3.0.0" "tinyqueue": "^3.0.0"
} }
@@ -7635,6 +7634,14 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/serialize-to-js": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/serialize-to-js/-/serialize-to-js-3.1.2.tgz",
"integrity": "sha512-owllqNuDDEimQat7EPG0tH7JjO090xKNzUtYz6X+Sk2BXDnOCilDdNLwjWeFywG9xkJul1ULvtUQa9O4pUaY0w==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/set-cookie-parser": { "node_modules/set-cookie-parser": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz",

View File

@@ -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 --ignore-path ../.prettierignore --ignore-path ./.gitignore && eslint .", "lint": "prettier --check . && eslint .",
"format": "prettier --write . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore" "format": "prettier --write ."
}, },
"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.14.4", "bits-ui": "^2.12.0",
"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",
@@ -66,7 +66,7 @@
"@mapbox/sphericalmercator": "^2.0.1", "@mapbox/sphericalmercator": "^2.0.1",
"@mapbox/tilebelt": "^2.0.2", "@mapbox/tilebelt": "^2.0.2",
"@types/mapbox__sphericalmercator": "^1.2.3", "@types/mapbox__sphericalmercator": "^1.2.3",
"chart.js": "^4.5.1", "chart.js": "^4.4.9",
"chartjs-plugin-zoom": "^2.2.0", "chartjs-plugin-zoom": "^2.2.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dexie": "^4.0.11", "dexie": "^4.0.11",
@@ -74,7 +74,7 @@
"gpx": "file:../gpx", "gpx": "file:../gpx",
"immer": "^10.1.1", "immer": "^10.1.1",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"mapbox-gl": "^3.17.0", "mapbox-gl": "^3.16.0",
"mapillary-js": "^4.1.2", "mapillary-js": "^4.1.2",
"png.js": "^0.2.1", "png.js": "^0.2.1",
"sanitize-html": "^2.17.0", "sanitize-html": "^2.17.0",

View File

@@ -1,126 +1,126 @@
@import 'tailwindcss'; @import "tailwindcss";
@import 'tw-animate-css'; @import "tw-animate-css";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
:root { :root {
--background: hsl(0 0% 100%) /* <- Wrap in HSL */; --background: hsl(0 0% 100%) /* <- Wrap in HSL */;
--foreground: hsl(240 10% 3.9%); --foreground: hsl(240 10% 3.9%);
--muted: hsl(240 4.8% 95.9%); --muted: hsl(240 4.8% 95.9%);
--muted-foreground: hsl(240 3.8% 46.1%); --muted-foreground: hsl(240 3.8% 46.1%);
--popover: hsl(0 0% 100%); --popover: hsl(0 0% 100%);
--popover-foreground: hsl(240 10% 3.9%); --popover-foreground: hsl(240 10% 3.9%);
--card: hsl(0 0% 100%); --card: hsl(0 0% 100%);
--card-foreground: hsl(240 10% 3.9%); --card-foreground: hsl(240 10% 3.9%);
--border: hsl(240 5.9% 90%); --border: hsl(240 5.9% 90%);
--input: hsl(240 5.9% 90%); --input: hsl(240 5.9% 90%);
--primary: hsl(240 5.9% 10%); --primary: hsl(240 5.9% 10%);
--primary-foreground: hsl(0 0% 98%); --primary-foreground: hsl(0 0% 98%);
--secondary: hsl(240 4.8% 95.9%); --secondary: hsl(240 4.8% 95.9%);
--secondary-foreground: hsl(240 5.9% 10%); --secondary-foreground: hsl(240 5.9% 10%);
--accent: hsl(240 4.8% 95.9%); --accent: hsl(240 4.8% 95.9%);
--accent-foreground: hsl(240 5.9% 10%); --accent-foreground: hsl(240 5.9% 10%);
--destructive: hsl(0 72.2% 50.6%); --destructive: hsl(0 72.2% 50.6%);
--destructive-foreground: hsl(0 0% 98%); --destructive-foreground: hsl(0 0% 98%);
--ring: hsl(240 10% 3.9%); --ring: hsl(240 10% 3.9%);
--sidebar: hsl(0 0% 98%); --sidebar: hsl(0 0% 98%);
--sidebar-foreground: hsl(240 5.3% 26.1%); --sidebar-foreground: hsl(240 5.3% 26.1%);
--sidebar-primary: hsl(240 5.9% 10%); --sidebar-primary: hsl(240 5.9% 10%);
--sidebar-primary-foreground: hsl(0 0% 98%); --sidebar-primary-foreground: hsl(0 0% 98%);
--sidebar-accent: hsl(240 4.8% 95.9%); --sidebar-accent: hsl(240 4.8% 95.9%);
--sidebar-accent-foreground: hsl(240 5.9% 10%); --sidebar-accent-foreground: hsl(240 5.9% 10%);
--sidebar-border: hsl(220 13% 91%); --sidebar-border: hsl(220 13% 91%);
--sidebar-ring: hsl(217.2 91.2% 59.8%); --sidebar-ring: hsl(217.2 91.2% 59.8%);
--support: rgb(220 15 130); --support: rgb(220 15 130);
--link: rgb(0 110 180); --link: rgb(0 110 180);
--selection: hsl(240 4.8% 93%); --selection: hsl(240 4.8% 93%);
--radius: 0.5rem; --radius: 0.5rem;
} }
.dark { .dark {
--background: hsl(240 10% 3.9%); --background: hsl(240 10% 3.9%);
--foreground: hsl(0 0% 98%); --foreground: hsl(0 0% 98%);
--muted: hsl(240 3.7% 15.9%); --muted: hsl(240 3.7% 15.9%);
--muted-foreground: hsl(240 5% 64.9%); --muted-foreground: hsl(240 5% 64.9%);
--popover: hsl(240 10% 3.9%); --popover: hsl(240 10% 3.9%);
--popover-foreground: hsl(0 0% 98%); --popover-foreground: hsl(0 0% 98%);
--card: hsl(240 10% 3.9%); --card: hsl(240 10% 3.9%);
--card-foreground: hsl(0 0% 98%); --card-foreground: hsl(0 0% 98%);
--border: hsl(240 3.7% 15.9%); --border: hsl(240 3.7% 15.9%);
--input: hsl(240 3.7% 15.9%); --input: hsl(240 3.7% 15.9%);
--primary: hsl(0 0% 98%); --primary: hsl(0 0% 98%);
--primary-foreground: hsl(240 5.9% 10%); --primary-foreground: hsl(240 5.9% 10%);
--secondary: hsl(240 3.7% 15.9%); --secondary: hsl(240 3.7% 15.9%);
--secondary-foreground: hsl(0 0% 98%); --secondary-foreground: hsl(0 0% 98%);
--accent: hsl(240 3.7% 15.9%); --accent: hsl(240 3.7% 15.9%);
--accent-foreground: hsl(0 0% 98%); --accent-foreground: hsl(0 0% 98%);
--destructive: hsl(0 62.8% 30.6%); --destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(0 0% 98%); --destructive-foreground: hsl(0 0% 98%);
--ring: hsl(240 4.9% 83.9%); --ring: hsl(240 4.9% 83.9%);
--sidebar: hsl(240 5.9% 10%); --sidebar: hsl(240 5.9% 10%);
--sidebar-foreground: hsl(240 4.8% 95.9%); --sidebar-foreground: hsl(240 4.8% 95.9%);
--sidebar-primary: hsl(224.3 76.3% 48%); --sidebar-primary: hsl(224.3 76.3% 48%);
--sidebar-primary-foreground: hsl(0 0% 100%); --sidebar-primary-foreground: hsl(0 0% 100%);
--sidebar-accent: hsl(240 3.7% 15.9%); --sidebar-accent: hsl(240 3.7% 15.9%);
--sidebar-accent-foreground: hsl(240 4.8% 95.9%); --sidebar-accent-foreground: hsl(240 4.8% 95.9%);
--sidebar-border: hsl(240 3.7% 15.9%); --sidebar-border: hsl(240 3.7% 15.9%);
--sidebar-ring: hsl(217.2 91.2% 59.8%); --sidebar-ring: hsl(217.2 91.2% 59.8%);
--support: rgb(255 110 190); --support: rgb(255 110 190);
--link: rgb(80 190 255); --link: rgb(80 190 255);
--selection: hsl(240 3.7% 22%); --selection: hsl(240 3.7% 22%);
} }
@theme inline { @theme inline {
/* Radius (for rounded-*) */ /* Radius (for rounded-*) */
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius); --radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px); --radius-xl: calc(var(--radius) + 4px);
/* Colors */ /* Colors */
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--color-muted: var(--muted); --color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground); --color-muted-foreground: var(--muted-foreground);
--color-popover: var(--popover); --color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground); --color-popover-foreground: var(--popover-foreground);
--color-card: var(--card); --color-card: var(--card);
--color-card-foreground: var(--card-foreground); --color-card-foreground: var(--card-foreground);
--color-border: var(--border); --color-border: var(--border);
--color-input: var(--input); --color-input: var(--input);
--color-primary: var(--primary); --color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground); --color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary); --color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground); --color-secondary-foreground: var(--secondary-foreground);
--color-accent: var(--accent); --color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground); --color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive); --color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground); --color-destructive-foreground: var(--destructive-foreground);
--color-ring: var(--ring); --color-ring: var(--ring);
--color-radius: var(--radius); --color-radius: var(--radius);
--color-sidebar: var(--sidebar); --color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary); --color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground); --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent); --color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border); --color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
--color-support: var(--support); --color-support: var(--support);
--color-link: var(--link); --color-link: var(--link);
--breakpoint-xs: 540px; --breakpoint-xs: 540px;
} }
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }

View File

@@ -368,26 +368,6 @@ 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">&copy; 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: {
@@ -821,7 +801,6 @@ export const overlayTree: LayerTreeType = {
}, },
cyclOSMlite: true, cyclOSMlite: true,
bikerouterGravel: true, bikerouterGravel: true,
openRailwayMap: true,
}, },
countries: { countries: {
france: { france: {
@@ -906,7 +885,6 @@ export const defaultOverlays: LayerTreeType = {
}, },
cyclOSMlite: false, cyclOSMlite: false,
bikerouterGravel: false, bikerouterGravel: false,
openRailwayMap: false,
}, },
countries: { countries: {
france: { france: {
@@ -1042,7 +1020,6 @@ export const defaultOverlayTree: LayerTreeType = {
}, },
cyclOSMlite: false, cyclOSMlite: false,
bikerouterGravel: false, bikerouterGravel: false,
openRailwayMap: false,
}, },
countries: { countries: {
france: { france: {

View File

@@ -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 { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx'; import type { GPXStatistics } 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<GPXStatisticsGroup>; gpxStatistics: Readable<GPXStatistics>;
slicedGPXStatistics: Readable<[GPXGlobalStatistics, number, number] | undefined>; slicedGPXStatistics: Readable<[GPXStatistics, 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.global $slicedGPXStatistics !== undefined ? $slicedGPXStatistics[0] : $gpxStatistics
); );
</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.distance.total} type="distance" /> <WithUnits value={statistics.global.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.elevation.gain} type="elevation" /> <WithUnits value={statistics.global.elevation.gain} type="elevation" />
<MoveDownRight size="16" class="mx-1" /> <MoveDownRight size="16" class="mx-1" />
<WithUnits value={statistics.elevation.loss} type="elevation" /> <WithUnits value={statistics.global.elevation.loss} type="elevation" />
</span> </span>
</Tooltip> </Tooltip>
{#if panelSize > 120 || orientation === 'horizontal'} {#if panelSize > 120 || orientation === 'horizontal'}
@@ -64,9 +64,13 @@
> >
<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 value={statistics.speed.moving} type="speed" showUnits={false} /> <WithUnits
value={statistics.global.speed.moving}
type="speed"
showUnits={false}
/>
<span class="mx-1">/</span> <span class="mx-1">/</span>
<WithUnits value={statistics.speed.total} type="speed" /> <WithUnits value={statistics.global.speed.total} type="speed" />
</span> </span>
</Tooltip> </Tooltip>
{/if} {/if}
@@ -79,9 +83,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.time.moving} type="time" /> <WithUnits value={statistics.global.time.moving} type="time" />
<span class="mx-1">/</span> <span class="mx-1">/</span>
<WithUnits value={statistics.time.total} type="time" /> <WithUnits value={statistics.global.time.total} type="time" />
</span> </span>
</Tooltip> </Tooltip>
{/if} {/if}

View File

@@ -538,7 +538,6 @@
let targetInput = let targetInput =
e && e &&
e.target && e.target &&
e.target instanceof HTMLElement &&
(e.target.tagName === 'INPUT' || (e.target.tagName === 'INPUT' ||
e.target.tagName === 'TEXTAREA' || e.target.tagName === 'TEXTAREA' ||
e.target.tagName === 'SELECT' || e.target.tagName === 'SELECT' ||

View File

@@ -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 { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx'; import type { GPXStatistics } 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<GPXStatisticsGroup>; gpxStatistics: Readable<GPXStatistics>;
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>; slicedGPXStatistics: Writable<[GPXStatistics, 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;

View File

@@ -14,16 +14,11 @@ import {
getTemperatureWithUnits, getTemperatureWithUnits,
getVelocityWithUnits, getVelocityWithUnits,
} from '$lib/units'; } from '$lib/units';
import Chart, { import Chart from 'chart.js/auto';
type ChartEvent,
type ChartOptions,
type ScriptableLineSegmentContext,
type TooltipItem,
} from 'chart.js/auto';
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 { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx'; import type { GPXStatistics } 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';
@@ -32,20 +27,6 @@ const { distanceUnits, velocityUnits, temperatureUnits } = settings;
Chart.defaults.font.family = Chart.defaults.font.family =
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; // Tailwind CSS font 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; // Tailwind CSS font
interface ElevationProfilePoint {
x: number;
y: number;
time?: Date;
slope: {
at: number;
segment: number;
length: number;
};
extensions: Record<string, any>;
coordinates: [number, number];
index: number;
}
export class ElevationProfile { export class ElevationProfile {
private _chart: Chart | null = null; private _chart: Chart | null = null;
private _canvas: HTMLCanvasElement; private _canvas: HTMLCanvasElement;
@@ -54,14 +35,14 @@ export class ElevationProfile {
private _dragging = false; private _dragging = false;
private _panning = false; private _panning = false;
private _gpxStatistics: Readable<GPXStatisticsGroup>; private _gpxStatistics: Readable<GPXStatistics>;
private _slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>; private _slicedGPXStatistics: Writable<[GPXStatistics, 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<GPXStatisticsGroup>, gpxStatistics: Readable<GPXStatistics>,
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>, slicedGPXStatistics: Writable<[GPXStatistics, 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,
@@ -109,7 +90,7 @@ export class ElevationProfile {
} }
initialize() { initialize() {
let options: ChartOptions<'line'> = { let options = {
animation: false, animation: false,
parsing: false, parsing: false,
maintainAspectRatio: false, maintainAspectRatio: false,
@@ -117,8 +98,8 @@ export class ElevationProfile {
x: { x: {
type: 'linear', type: 'linear',
ticks: { ticks: {
callback: function (value: number | string) { callback: function (value: number) {
return `${(value as number).toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`; return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
}, },
align: 'inner', align: 'inner',
maxRotation: 0, maxRotation: 0,
@@ -127,8 +108,8 @@ export class ElevationProfile {
y: { y: {
type: 'linear', type: 'linear',
ticks: { ticks: {
callback: function (value: number | string) { callback: function (value: number) {
return getElevationWithUnits(value as number, false); return getElevationWithUnits(value, false);
}, },
}, },
}, },
@@ -159,8 +140,8 @@ export class ElevationProfile {
title: () => { title: () => {
return ''; return '';
}, },
label: (context: TooltipItem<'line'>) => { label: (context: Chart.TooltipContext) => {
let point = context.raw as ElevationProfilePoint; let point = context.raw;
if (context.datasetIndex === 0) { if (context.datasetIndex === 0) {
const map_ = get(map); const map_ = get(map);
if (map_ && this._marker) { if (map_ && this._marker) {
@@ -184,10 +165,10 @@ export class ElevationProfile {
return `${i18n._('quantities.power')}: ${getPowerWithUnits(point.y)}`; return `${i18n._('quantities.power')}: ${getPowerWithUnits(point.y)}`;
} }
}, },
afterBody: (contexts: TooltipItem<'line'>[]) => { afterBody: (contexts: Chart.TooltipContext[]) => {
let context = contexts.filter((context) => context.datasetIndex === 0); let context = contexts.filter((context) => context.datasetIndex === 0);
if (context.length === 0) return; if (context.length === 0) return;
let point = context[0].raw as ElevationProfilePoint; let point = context[0].raw;
let slope = { let slope = {
at: point.slope.at.toFixed(1), at: point.slope.at.toFixed(1),
segment: point.slope.segment.toFixed(1), segment: point.slope.segment.toFixed(1),
@@ -246,7 +227,6 @@ export class ElevationProfile {
onPanStart: () => { onPanStart: () => {
this._panning = true; this._panning = true;
this._slicedGPXStatistics.set(undefined); this._slicedGPXStatistics.set(undefined);
return true;
}, },
onPanComplete: () => { onPanComplete: () => {
this._panning = false; this._panning = false;
@@ -258,13 +238,13 @@ export class ElevationProfile {
}, },
mode: 'x', mode: 'x',
onZoomStart: ({ chart, event }: { chart: Chart; event: any }) => { onZoomStart: ({ chart, event }: { chart: Chart; event: any }) => {
if (!this._chart) {
return false;
}
const maxZoom = this._chart.getInitialScaleBounds()?.x?.max ?? 0;
if ( if (
event.deltaY < 0 && event.deltaY < 0 &&
Math.abs(maxZoom / this._chart.getZoomLevel()) < 0.01 Math.abs(
this._chart.getInitialScaleBounds().x.max /
this._chart.options.plugins.zoom.limits.x.minRange -
this._chart.getZoomLevel()
) < 0.01
) { ) {
// Disable wheel pan if zoomed in to the max, and zooming in // Disable wheel pan if zoomed in to the max, and zooming in
return false; return false;
@@ -282,6 +262,7 @@ export class ElevationProfile {
}, },
}, },
}, },
stacked: false,
onResize: () => { onResize: () => {
this.updateOverlay(); this.updateOverlay();
}, },
@@ -289,7 +270,7 @@ export class ElevationProfile {
let datasets: string[] = ['speed', 'hr', 'cad', 'atemp', 'power']; let datasets: string[] = ['speed', 'hr', 'cad', 'atemp', 'power'];
datasets.forEach((id) => { datasets.forEach((id) => {
options.scales![`y${id}`] = { options.scales[`y${id}`] = {
type: 'linear', type: 'linear',
position: 'right', position: 'right',
grid: { grid: {
@@ -310,7 +291,7 @@ export class ElevationProfile {
{ {
id: 'toggleMarker', id: 'toggleMarker',
events: ['mouseout'], events: ['mouseout'],
afterEvent: (chart: Chart, args: { event: ChartEvent }) => { afterEvent: (chart: Chart, args: { event: Chart.ChartEvent }) => {
if (args.event.type === 'mouseout') { if (args.event.type === 'mouseout') {
const map_ = get(map); const map_ = get(map);
if (map_ && this._marker) { if (map_ && this._marker) {
@@ -324,7 +305,7 @@ export class ElevationProfile {
let startIndex = 0; let startIndex = 0;
let endIndex = 0; let endIndex = 0;
const getIndex = (evt: PointerEvent) => { const getIndex = (evt) => {
if (!this._chart) { if (!this._chart) {
return undefined; return undefined;
} }
@@ -342,22 +323,22 @@ 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 this._chart.data.datasets[0].data.length - 1; return get(this._gpxStatistics).local.points.length - 1;
} else { } else {
return undefined; return undefined;
} }
} }
const point = points.find((point) => (point.element as any).raw); let point = points.find((point) => point.element.raw);
if (point) { if (point) {
return (point.element as any).raw.index; return point.element.raw.index;
} else { } else {
return points[0].index; return points[0].index;
} }
}; };
let dragStarted = false; let dragStarted = false;
const onMouseDown = (evt: PointerEvent) => { const onMouseDown = (evt) => {
if (evt.shiftKey) { if (evt.shiftKey) {
// Panning interaction // Panning interaction
return; return;
@@ -366,7 +347,7 @@ export class ElevationProfile {
this._canvas.style.cursor = 'col-resize'; this._canvas.style.cursor = 'col-resize';
startIndex = getIndex(evt); startIndex = getIndex(evt);
}; };
const onMouseMove = (evt: PointerEvent) => { const onMouseMove = (evt) => {
if (dragStarted) { if (dragStarted) {
this._dragging = true; this._dragging = true;
endIndex = getIndex(evt); endIndex = getIndex(evt);
@@ -375,7 +356,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).sliced( get(this._gpxStatistics).slice(
Math.min(startIndex, endIndex), Math.min(startIndex, endIndex),
Math.max(startIndex, endIndex) Math.max(startIndex, endIndex)
), ),
@@ -386,7 +367,7 @@ export class ElevationProfile {
} }
} }
}; };
const onMouseUp = (evt: PointerEvent) => { const onMouseUp = (evt) => {
dragStarted = false; dragStarted = false;
this._dragging = false; this._dragging = false;
this._canvas.style.cursor = ''; this._canvas.style.cursor = '';
@@ -405,99 +386,85 @@ 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: datasets[0], data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.ele ? getConvertedElevation(point.ele) : 0,
time: point.time,
slope: {
at: data.local.slope.at[index],
segment: data.local.slope.segment[index],
length: data.local.slope.length[index],
},
extensions: point.getExtensions(),
coordinates: point.getCoordinates(),
index: index,
};
}),
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: datasets[1], data: data.local.points.map((point, index) => {
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: datasets[2], data: data.local.points.map((point, index) => {
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: datasets[3], data: data.local.points.map((point, index) => {
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: datasets[4], data: data.local.points.map((point, index) => {
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: datasets[5], data: data.local.points.map((point, index) => {
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();
@@ -546,24 +513,21 @@ export class ElevationProfile {
return; return;
} }
const elevationFill = get(this._elevationFill); const elevationFill = get(this._elevationFill);
const dataset = this._chart.data.datasets[0];
let segment: any = {};
if (elevationFill === 'slope') { if (elevationFill === 'slope') {
segment = { this._chart.data.datasets[0]['segment'] = {
backgroundColor: this.slopeFillCallback, backgroundColor: this.slopeFillCallback,
}; };
} else if (elevationFill === 'surface') { } else if (elevationFill === 'surface') {
segment = { this._chart.data.datasets[0]['segment'] = {
backgroundColor: this.surfaceFillCallback, backgroundColor: this.surfaceFillCallback,
}; };
} else if (elevationFill === 'highway') { } else if (elevationFill === 'highway') {
segment = { this._chart.data.datasets[0]['segment'] = {
backgroundColor: this.highwayFillCallback, backgroundColor: this.highwayFillCallback,
}; };
} else { } else {
segment = {}; this._chart.data.datasets[0]['segment'] = {};
} }
Object.assign(dataset, { segment });
} }
updateOverlay() { updateOverlay() {
@@ -590,12 +554,10 @@ 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( getConvertedDistance(gpxStatistics.local.distance.total[startIndex])
gpxStatistics.getTrackPoint(startIndex)?.distance.total ?? 0
)
); );
let endPixel = this._chart.scales.x.getPixelForValue( let endPixel = this._chart.scales.x.getPixelForValue(
getConvertedDistance(gpxStatistics.getTrackPoint(endIndex)?.distance.total ?? 0) getConvertedDistance(gpxStatistics.local.distance.total[endIndex])
); );
selectionContext.fillRect( selectionContext.fillRect(
@@ -613,22 +575,19 @@ export class ElevationProfile {
} }
} }
slopeFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) { slopeFillCallback(context) {
const point = context.p0.raw as ElevationProfilePoint; return getSlopeColor(context.p0.raw.slope.segment);
return getSlopeColor(point.slope.segment);
} }
surfaceFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) { surfaceFillCallback(context) {
const point = context.p0.raw as ElevationProfilePoint; return getSurfaceColor(context.p0.raw.extensions.surface);
return getSurfaceColor(point.extensions.surface);
} }
highwayFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) { highwayFillCallback(context) {
const point = context.p0.raw as ElevationProfilePoint;
return getHighwayColor( return getHighwayColor(
point.extensions.highway, context.p0.raw.extensions.highway,
point.extensions.sac_scale, context.p0.raw.extensions.sac_scale,
point.extensions.mtb_scale context.p0.raw.extensions.mtb_scale
); );
} }

View File

@@ -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 { GPXGlobalStatistics } from 'gpx'; import { GPXStatistics } 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.global; let statistics = $gpxStatistics;
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()).global); acc.mergeWith(cur.getStatisticsFor(new ListRootItem()));
} }
return acc; return acc;
}, new GPXGlobalStatistics()); }, new GPXStatistics());
} }
return { return {
time: statistics.time.total === 0, time: statistics.global.time.total === 0,
hr: statistics.hr.count === 0, hr: statistics.global.hr.count === 0,
cad: statistics.cad.count === 0, cad: statistics.global.cad.count === 0,
atemp: statistics.atemp.count === 0, atemp: statistics.global.atemp.count === 0,
power: statistics.power.count === 0, power: statistics.global.power.count === 0,
extensions: Object.keys(statistics.extensions).length === 0, extensions: Object.keys(statistics.global.extensions).length === 0,
}; };
} }
}); });

View File

@@ -72,15 +72,17 @@
} }
let style = node.getStyle(defaultColor); let style = node.getStyle(defaultColor);
colors = style.color; style.color.forEach((c) => {
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 ( if (style) {
style && if (style['gpx_style:color'] && !colors.includes(style['gpx_style:color'])) {
style['gpx_style:color'] && colors.push(style['gpx_style:color']);
!colors.includes(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());

View File

@@ -101,17 +101,23 @@ export class DistanceMarkers {
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection { getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
let statistics = get(gpxStatistics); let statistics = get(gpxStatistics);
let features: GeoJSON.Feature[] = []; let features = [];
let currentTargetDistance = 1; let currentTargetDistance = 1;
statistics.forEachTrackPoint((trkpt, dist) => { for (let i = 0; i < statistics.local.distance.total.length; i++) {
if (dist >= getConvertedDistanceToKilometers(currentTargetDistance)) { if (
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: [trkpt.getLongitude(), trkpt.getLatitude()], coordinates: [
statistics.local.points[i].getLongitude(),
statistics.local.points[i].getLatitude(),
],
}, },
properties: { properties: {
distance, distance,
@@ -120,7 +126,7 @@ export class DistanceMarkers {
} as GeoJSON.Feature); } as GeoJSON.Feature);
currentTargetDistance += 1; currentTargetDistance += 1;
} }
}); }
return { return {
type: 'FeatureCollection', type: 'FeatureCollection',

View File

@@ -1,5 +1,5 @@
import { get, type Readable } from 'svelte/store'; import { get, type Readable } from 'svelte/store';
import mapboxgl, { type FilterSpecification } from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import { waypointPopup, trackpointPopup } from './gpx-layer-popup'; import { waypointPopup, trackpointPopup } from './gpx-layer-popup';
import { import {
@@ -153,6 +153,8 @@ export class GPXLayer {
return; return;
} }
this.loadIcons();
if ( if (
file._data.style && file._data.style &&
file._data.style.color && file._data.style.color &&
@@ -162,8 +164,6 @@ 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) {
@@ -281,23 +281,25 @@ export class GPXLayer {
} }
} }
let visibleTrackSegmentIds: string[] = []; let visibleSegments: [number, number][] = [];
file.forEachSegment((segment, trackIndex, segmentIndex) => { file.forEachSegment((segment, trackIndex, segmentIndex) => {
if (!segment._data.hidden) { if (!segment._data.hidden) {
visibleTrackSegmentIds.push(`${trackIndex}-${segmentIndex}`); visibleSegments.push([trackIndex, segmentIndex]);
} }
}); });
const segmentFilter: FilterSpecification = [
'in',
['get', 'trackSegmentId'],
['literal', visibleTrackSegmentIds],
];
_map.setFilter(this.fileId, segmentFilter, { validate: false }); _map.setFilter(
this.fileId,
if (_map.getLayer(this.fileId + '-direction')) { [
_map.setFilter(this.fileId + '-direction', segmentFilter, { validate: false }); 'any',
} ...visibleSegments.map(([trackIndex, segmentIndex]) => [
'all',
['==', 'trackIndex', trackIndex],
['==', 'segmentIndex', segmentIndex],
]),
],
{ validate: false }
);
let visibleWaypoints: number[] = []; let visibleWaypoints: number[] = [];
file.wpt.forEach((waypoint, waypointIndex) => { file.wpt.forEach((waypoint, waypointIndex) => {
@@ -311,6 +313,21 @@ export class GPXLayer {
['in', ['get', 'waypointIndex'], ['literal', visibleWaypoints]], ['in', ['get', 'waypointIndex'], ['literal', visibleWaypoints]],
{ validate: false } { validate: false }
); );
if (_map.getLayer(this.fileId + '-direction')) {
_map.setFilter(
this.fileId + '-direction',
[
'any',
...visibleSegments.map(([trackIndex, segmentIndex]) => [
'all',
['==', 'trackIndex', trackIndex],
['==', 'segmentIndex', segmentIndex],
]),
],
{ validate: false }
);
}
} catch (e) { } catch (e) {
// No reliable way to check if the map is ready to add sources and layers // No reliable way to check if the map is ready to add sources and layers
return; return;
@@ -669,7 +686,6 @@ export class GPXLayer {
} }
feature.properties.trackIndex = trackIndex; feature.properties.trackIndex = trackIndex;
feature.properties.segmentIndex = segmentIndex; feature.properties.segmentIndex = segmentIndex;
feature.properties.trackSegmentId = `${trackIndex}-${segmentIndex}`;
segmentIndex++; segmentIndex++;
if (segmentIndex >= file.trk[trackIndex].trkseg.length) { if (segmentIndex >= file.trk[trackIndex].trkseg.length) {
@@ -702,7 +718,7 @@ export class GPXLayer {
properties: { properties: {
fileId: this.fileId, fileId: this.fileId,
waypointIndex: index, waypointIndex: index,
icon: `waypoint-${getSymbolKey(waypoint.sym) ?? 'default'}-${this.layerColor}`, icon: `${this.fileId}-waypoint-${getSymbolKey(waypoint.sym) ?? 'default'}`,
}, },
}); });
}); });
@@ -723,7 +739,7 @@ export class GPXLayer {
}); });
symbols.forEach((symbol) => { symbols.forEach((symbol) => {
const iconId = `waypoint-${symbol ?? 'default'}-${this.layerColor}`; const iconId = `${this.fileId}-waypoint-${symbol ?? 'default'}`;
if (!_map.hasImage(iconId)) { if (!_map.hasImage(iconId)) {
let icon = new Image(100, 100); let icon = new Image(100, 100);
icon.onload = () => { icon.onload = () => {

View File

@@ -34,20 +34,13 @@ export class StartEndMarkers {
if (!map_) return; if (!map_) return;
const tool = get(currentTool); const tool = get(currentTool);
const statistics = get(gpxStatistics); const statistics = get(slicedGPXStatistics)?.[0] ?? get(gpxStatistics);
const slicedStatistics = get(slicedGPXStatistics);
const hidden = get(allHidden); const hidden = get(allHidden);
if (statistics.global.length > 0 && tool !== Tool.ROUTING && !hidden) { if (statistics.local.points.length > 0 && tool !== Tool.ROUTING && !hidden) {
this.start this.start.setLngLat(statistics.local.points[0].getCoordinates()).addTo(map_);
.setLngLat(
statistics.getTrackPoint(slicedStatistics?.[1] ?? 0)!.trkpt.getCoordinates()
)
.addTo(map_);
this.end this.end
.setLngLat( .setLngLat(
statistics statistics.local.points[statistics.local.points.length - 1].getCoordinates()
.getTrackPoint(slicedStatistics?.[2] ?? statistics.global.length - 1)!
.trkpt.getCoordinates()
) )
.addTo(map_); .addTo(map_);
} else { } else {

View File

@@ -101,7 +101,9 @@
acc: Record<string, ImportSpecification>, acc: Record<string, ImportSpecification>,
imprt: ImportSpecification imprt: ImportSpecification
) => { ) => {
if (!['basemap', 'overlays'].includes(imprt.id)) { if (
!['basemap', 'overlays', 'glyphs-and-sprite'].includes(imprt.id)
) {
acc[imprt.id] = imprt; acc[imprt.id] = imprt;
} }
return acc; return acc;

View File

@@ -54,7 +54,7 @@
} }
$effect(() => { $effect(() => {
if (open && $selectedBasemapTree && $currentBasemap) { if ($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 (open && $selectedOverlayTree) { if ($selectedOverlayTree) {
untrack(() => { untrack(() => {
if ($currentOverlays) { if ($currentOverlays) {
let overlayLayers = getLayers($currentOverlays); let overlayLayers = getLayers($currentOverlays);
@@ -86,7 +86,7 @@
}); });
$effect(() => { $effect(() => {
if (open && $selectedOverpassTree) { if ($selectedOverpassTree) {
untrack(() => { untrack(() => {
if ($currentOverpassQueries) { if ($currentOverpassQueries) {
let overlayLayers = getLayers($currentOverpassQueries); let overlayLayers = getLayers($currentOverpassQueries);

View File

@@ -35,6 +35,17 @@ 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: '',
@@ -152,12 +163,6 @@ 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

View File

@@ -28,15 +28,17 @@ export class ReducedGPXLayer {
update() { update() {
const file = this._fileState.file; const file = this._fileState.file;
if (!file) { const stats = this._fileState.statistics;
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,
segment.trkpt.length, statistics.local.points.length,
ramerDouglasPeucker(segment.trkpt, minTolerance), ramerDouglasPeucker(statistics.local.points, minTolerance),
]); ]);
}); });
} }

View File

@@ -793,25 +793,24 @@ 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 =
endAnchorStats.distance.moving - startAnchorStats.distance.moving; stats.local.distance.moving[anchors[anchors.length - 1].point._data.index] -
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 -
(endAnchorStats.time.moving - startAnchorStats.time.moving); (stats.local.time.moving[anchors[anchors.length - 1].point._data.index] -
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 = endAnchorStats.time.total - startAnchorStats.time.total; replacingTime =
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;
@@ -821,7 +820,9 @@ 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 + endAnchorStats.time.total - endAnchorStats.time.moving) * (replacingTime +
stats.local.time.total[endIndex] -
stats.local.time.moving[endIndex]) *
1000 1000
); );
} }

View File

@@ -26,10 +26,12 @@
let validSelection = $derived( let validSelection = $derived(
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) && $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
$gpxStatistics.global.length > 0 $gpxStatistics.local.points.length > 0
); );
let maxSliderValue = $derived( let maxSliderValue = $derived(
validSelection && $gpxStatistics.global.length > 0 ? $gpxStatistics.global.length - 1 : 1 validSelection && $gpxStatistics.local.points.length > 0
? $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);
@@ -43,7 +45,7 @@
function updateSlicedGPXStatistics() { function updateSlicedGPXStatistics() {
if (validSelection && canCrop) { if (validSelection && canCrop) {
$slicedGPXStatistics = [ $slicedGPXStatistics = [
get(gpxStatistics).sliced(sliderValues[0], sliderValues[1]), get(gpxStatistics).slice(sliderValues[0], sliderValues[1]),
sliderValues[0], sliderValues[0],
sliderValues[1], sliderValues[1],
]; ];

View File

@@ -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 d'ispezionare [tracce, segmenti e punti di interesse](./gpx) all'interno dei file attraverso sezioni espandibili. Inoltre, la vista ad albero dei file consente di 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>Maiuscolo</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>Maiusc</kbd>.
<div class="h-48 w-full"> <div class="h-48 w-full">
<ElevationProfile <ElevationProfile

View File

@@ -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 una marcatura temporale e un'altitudine. - Un **punto GPS** è una posizione con una latitudine, una longitudine, ed eventualmente un timestamp 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.

View File

@@ -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 stupende, 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 gradevoli, recuperare i dati altimetrici e consentire la ricerca di luoghi.
Sfortunatamente, fare tutto ciò è costoso. Sfortunatamente, questo è 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! ❤️

View File

@@ -2,7 +2,7 @@
import { HeartHandshake } from '@lucide/svelte'; import { HeartHandshake } from '@lucide/svelte';
</script> </script>
## <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) ## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
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.

View File

@@ -1,5 +1,5 @@
Mapbox là công ty cung cấp một số bản đồ đẹp trên trang web này. Mapbox is the company that provides some of the beautiful maps on this website.
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**. They also develop the <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">map engine</a> which powers **gpx.studio**.
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. 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.
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ể. 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.

View File

@@ -9,8 +9,8 @@ title: Edit actions
# { title } # { title }
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. Unlike the file actions, the edit actions can potentially modify the content of the currently selected files.
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). 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).
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.

View File

@@ -29,13 +29,13 @@ title: 文件
创建当前选中文件的副本。 创建当前选中文件的副本。
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> 删除 ### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
删除当前选中的文件。 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" /> Delete all
删除全部文件。 Delete all files.
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> 导出... ### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> 导出...

View File

@@ -17,6 +17,7 @@ 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,
@@ -29,7 +30,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, getClosestTrackSegments, getElevation } from '$lib/utils'; import { getClosestLinePoint, getElevation } from '$lib/utils';
import { gpxStatistics } from '$lib/logic/statistics'; import { gpxStatistics } from '$lib/logic/statistics';
import { boundsManager } from './bounds'; import { boundsManager } from './bounds';
@@ -215,7 +216,7 @@ export const fileActions = {
reverseSelection: () => { reverseSelection: () => {
if ( if (
!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) || !get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) ||
get(gpxStatistics).global.length <= 1 get(gpxStatistics).local.points?.length <= 1
) { ) {
return; return;
} }
@@ -345,20 +346,19 @@ export const fileActions = {
let startTime: Date | undefined = undefined; let startTime: Date | undefined = undefined;
if (speed !== undefined) { if (speed !== undefined) {
if ( if (
statistics.global.length > 0 && statistics.local.points.length > 0 &&
statistics.getTrackPoint(0)!.trkpt.time !== undefined statistics.local.points[0].time !== undefined
) { ) {
startTime = statistics.getTrackPoint(0)!.trkpt.time; startTime = statistics.local.points[0].time;
} else { } else {
for (let i = 0; i < statistics.global.length; i++) { let index = statistics.local.points.findIndex(
const point = statistics.getTrackPoint(i)!; (point) => point.time !== undefined
if (point.trkpt.time !== undefined) { );
startTime = new Date( if (index !== -1 && statistics.local.points[index].time) {
point.trkpt.time.getTime() - startTime = new Date(
(1000 * 3600 * point.distance.total) / speed statistics.local.points[index].time.getTime() -
); (1000 * 3600 * statistics.local.distance.total[index]) / speed
break; );
}
} }
} }
} }
@@ -453,13 +453,34 @@ 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);
let statistics = fileStateCollection.getStatistics(fileId); if (file) {
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) => let closest = file.wpt.map((wpt, wptIndex) => {
getClosestTrackSegments(file, statistics, wpt.getCoordinates()) return {
); 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) => {
@@ -474,11 +495,9 @@ export const fileActions = {
newFile.replaceWaypoints( newFile.replaceWaypoints(
0, 0,
file.wpt.length - 1, file.wpt.length - 1,
file.wpt.filter((wpt, wptIndex) => closest
closest[wptIndex].some( .filter((c) => c.index.includes(index))
([trackIndex, segmentIndex]) => trackIndex === index .map((c) => file.wpt[c.wptIndex])
)
)
); );
newFile._data.id = fileIds[index]; newFile._data.id = fileIds[index];
newFile.metadata.name = newFile.metadata.name =
@@ -487,9 +506,29 @@ 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) => let closest = file.wpt.map((wpt, wptIndex) => {
getClosestTrackSegments(file, statistics, wpt.getCoordinates()) return {
); 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, [
@@ -498,11 +537,9 @@ export const fileActions = {
newFile.replaceWaypoints( newFile.replaceWaypoints(
0, 0,
file.wpt.length - 1, file.wpt.length - 1,
file.wpt.filter((wpt, wptIndex) => closest
closest[wptIndex].some( .filter((c) => c.index.includes(index))
([trackIndex, segmentIndex]) => segmentIndex === index .map((c) => file.wpt[c.wptIndex])
)
)
); );
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})`;

View File

@@ -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, GPXStatisticsGroup, type Track } from 'gpx'; import { GPXFile, GPXStatistics, type Track } from 'gpx';
export class GPXStatisticsTree { export class GPXStatisticsTree {
level: ListLevel; level: ListLevel;
@@ -21,23 +21,23 @@ export class GPXStatisticsTree {
} }
} }
getStatisticsFor(item: ListItem): GPXStatisticsGroup { getStatisticsFor(item: ListItem): GPXStatistics {
let statistics = new GPXStatisticsGroup(); let statistics = new GPXStatistics();
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.add(this.statistics[key]); statistics.mergeWith(this.statistics[key]);
} else { } else {
statistics.add(this.statistics[key].getStatisticsFor(item)); statistics.mergeWith(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.add(child); statistics.mergeWith(child);
} else if (child !== undefined) { } else if (child !== undefined) {
statistics.add(child.getStatisticsFor(item)); statistics.mergeWith(child.getStatisticsFor(item));
} }
} }
return statistics; return statistics;

View File

@@ -1,5 +1,5 @@
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx'; import { GPXStatistics } 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<GPXStatisticsGroup>; private _statistics: Writable<GPXStatistics>;
private _files: Map< private _files: Map<
string, string,
{ {
@@ -22,21 +22,18 @@ export class SelectedGPXStatistics {
>; >;
constructor() { constructor() {
this._statistics = writable(new GPXStatisticsGroup()); this._statistics = writable(new GPXStatistics());
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( subscribe(run: (value: GPXStatistics) => void, invalidate?: (value?: GPXStatistics) => void) {
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 GPXStatisticsGroup(); let statistics = new GPXStatistics();
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
let stats = fileStateCollection.getStatistics(fileId); let stats = fileStateCollection.getStatistics(fileId);
if (stats) { if (stats) {
@@ -46,7 +43,7 @@ export class SelectedGPXStatistics {
!(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) || !(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) ||
first first
) { ) {
statistics.add(stats.getStatisticsFor(item)); statistics.mergeWith(stats.getStatisticsFor(item));
first = false; first = false;
} }
}); });
@@ -79,7 +76,7 @@ export class SelectedGPXStatistics {
export const gpxStatistics = new SelectedGPXStatistics(); export const gpxStatistics = new SelectedGPXStatistics();
export const slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined> = export const slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined> =
writable(undefined); writable(undefined);
gpxStatistics.subscribe(() => { gpxStatistics.subscribe(() => {

View File

@@ -229,9 +229,6 @@ export function getConvertedVelocity(
} }
} }
export function getConvertedTemperature( export function getConvertedTemperature(value: number) {
value: number, return get(temperatureUnits) === 'celsius' ? value : celsiusToFahrenheit(value);
targetTemperatureUnits = get(temperatureUnits)
) {
return targetTemperatureUnits === 'celsius' ? value : celsiusToFahrenheit(value);
} }

View File

@@ -2,13 +2,11 @@ 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, GPXFile } from 'gpx'; import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance } 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));
@@ -49,59 +47,6 @@ 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,

View File

@@ -473,7 +473,7 @@
}, },
"homepage": { "homepage": {
"website": "Webseite", "website": "Webseite",
"home": "Startseite", "home": "Zuhause",
"app": "App", "app": "App",
"contact": "Kontakt", "contact": "Kontakt",
"reddit": "Reddit", "reddit": "Reddit",

View File

@@ -324,7 +324,6 @@
"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",

View File

@@ -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": " editor online de archivos GPX", "embed_title": "El 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."

View File

@@ -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": "Eau potable", "water": "Cours d'eau",
"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": "Eau potable", "drinking_water": "Cours d'eau",
"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": "Point d'eau" "water_source": "Source d'eau"
} }
}, },
"homepage": { "homepage": {

View File

@@ -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": "Vista stradale", "toggle_street_view": "Street View",
"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 d'interesse", "tooltip": "Creare e modificare punti di interesse",
"icon": "Icona", "icon": "Icona",
"link": "Collegamento", "link": "Collegamento",
"longitude": "Longitudine", "longitude": "Longitudine",
"latitude": "Latitudine", "latitude": "Latitudine",
"create": "Creare un punto d'interesse", "create": "Creare un punto di interesse",
"add": "Aggiungi punto d'interesse al file", "add": "Aggiungi punto di interesse al file",
"help": "Compila il modulo per creare un nuovo punto d'interesse, oppure fai clic su uno esistente per modificarlo. Fare clic sulla mappa per inserire le coordinate o trascinare i punti d'interesse per spostarli.", "help": "Compila il modulo per creare un nuovo punto di interesse, oppure fai clic su uno esistente per modificarlo. Fare clic sulla mappa per inserire le coordinate o trascinare i punti di interesse per spostarli.",
"help_no_selection": "Selezionare un file per creare o modificare punti d'interesse." "help_no_selection": "Selezionare un file per creare o modificare punti di interesse."
}, },
"reduce": { "reduce": {
"tooltip": "Riduci il numero di punti GPS", "tooltip": "Riduci il numero di punti della traccia",
"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 d'interesse con una selezione rettangolare", "tooltip": "Pulire i punti GPS e i punti di 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 d'interesse.", "help": "Selezionare un'area rettangolare sulla mappa per rimuovere i punti GPS e i punti di interesse.",
"help_no_selection": "Seleziona una traccia per pulire i punti GPS e i punti d'interesse." "help_no_selection": "Seleziona una traccia per pulire i punti GPS e i punti di 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 JSON in stile Mapbox", "url_placeholder": "WMTS, WMS o Mapbox stile JSON",
"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": "Ordnance Survey", "ordnanceSurvey": "Sondaggio Ordnance",
"norwayTopo": "Topografisk Norgeskart 4", "norwayTopo": "Topografisk Norgeskart 4",
"finlandTopo": "Lantmäteriverket Terrängkarta", "finlandTopo": "Carta topografica del vecchio Catasto svedese",
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite", "cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Pendenza", "swisstopoSlope": "Carta topografica Svizzera Pendenza",
"swisstopoHiking": "swisstopo Escursione", "swisstopoHiking": "Carta topografica Svedese Escursione",
"swisstopoHikingClosures": "swisstopo Fine escursione", "swisstopoHikingClosures": "Carta topografica Svizzera Fine escursione",
"swisstopoCycling": "swisstopo Ciclabile", "swisstopoCycling": "Carta topografica Svizzera Ciclabile",
"swisstopoCyclingClosures": "swisstopo Fine ciclabile", "swisstopoCyclingClosures": "Carta topografica Svizzera fine ciclabile",
"swisstopoMountainBike": "swisstopo MTB", "swisstopoMountainBike": "Carta topografica Svizzera MTB",
"swisstopoMountainBikeClosures": "swisstopo Fine MTB", "swisstopoMountainBikeClosures": "Carta topografica Svizzera fine MTB",
"swisstopoSkiTouring": "swisstopo Sci Alpinismo", "swisstopoSkiTouring": "Carta topografica Svizzera pista sci",
"ignFrCadastre": "IGN Catasto", "ignFrCadastre": "IGN Catasto",
"ignSlope": "IGN Pendenza", "ignSlope": "Pendenza IGN",
"ignSkiTouring": "IGN Sci Alpinismo", "ignSkiTouring": "IGN Sciescursionismo",
"waymarked_trails": "Sentieri Segnalati", "waymarked_trails": "Waymarked Trails",
"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": "mi/h", "miles_per_hour": "mph",
"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 d'interesse", "waypoint": "Punto di interesse",
"waypoints": "Punti d'interesse", "waypoints": "Punti di 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": "Supporta gpx.studio su Ko-fi", "support_button": "Supporto di 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, basata sui dati OpenStreetMap.", "route_planning_description": "Un'interfaccia intuitiva per creare itinerari su misura per ogni sport, basati 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 la tua ultima impresa.", "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.",
"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",

View File

@@ -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": "Edytor plików GPX online", "embed_title": "Online'owy edytor plików GPX",
"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": "Usuń wszystko", "delete_all": "Delete all",
"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": "Edytuj w OpenStreetMap" "edit_osm": "Edit in OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -353,7 +353,7 @@
"water": "Woda", "water": "Woda",
"shower": "Prysznic", "shower": "Prysznic",
"shelter": "Schronienie", "shelter": "Schronienie",
"cemetery": "Cmentarz", "cemetery": "Cemetery",
"motorized": "Samochody i motocykle", "motorized": "Samochody i motocykle",
"fuel-station": "Stacja paliw", "fuel-station": "Stacja paliw",
"parking": "Parking", "parking": "Parking",

View File

@@ -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": "Hãy giúp duy trì trang web miễn phí", "support_button": "Help keep the website free",
"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": "Xóa tất cả", "delete_all": "Delete all",
"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": "Cây thư mục", "tree_file_view": "File tree",
"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": "Trợ giúp", "help": "Help",
"more": "Chi tiết...", "more": "More...",
"donate": "Ủng hộ", "donate": "Donate",
"ctrl": "Ctrl", "ctrl": "Ctrl",
"click": "Nhấp chuột", "click": "Click",
"drag": "Kéo", "drag": "Drag",
"metadata": { "metadata": {
"button": "Thông tin...", "button": "Info...",
"name": "Tên", "name": "Name",
"description": "Mô tả", "description": "Description",
"save": "Lưu" "save": "Save"
}, },
"style": { "style": {
"button": "Diện mạo...", "button": "Appearance...",
"color": "Màu sắc", "color": "Color",
"opacity": "Độ trong suốt", "opacity": "Opacity",
"width": "Độ rộng" "width": "Width"
}, },
"hide": "Ẩn", "hide": "Hide",
"unhide": "Bỏ ẩn", "unhide": "Unhide",
"center": "Giữa", "center": "Center",
"open_in": "Mở ra", "open_in": "Open in",
"copy_coordinates": "Sao chép tọa độ", "copy_coordinates": "Copy coordinates",
"edit_osm": "" "edit_osm": "Edit in OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
"tooltip": "Plan or edit a route", "tooltip": "Plan or edit a route",
"activity": "Hoạt động", "activity": "Activity",
"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": "Quay về Bắt đầu", "button": "Back to start",
"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": "Bắt đầu lặp ở đây", "start_loop_here": "Start loop here",
"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": "Sỏi", "gravel": "Gravel",
"pebblestone": "Pebblestone", "pebblestone": "Pebblestone",
"rock": "Đá", "rock": "Rock",
"dirt": "Đất", "dirt": "Dirt",
"ground": "Ground", "ground": "Ground",
"earth": "Earth", "earth": "Earth",
"mud": "Mud", "mud": "Mud",
"sand": "Sand", "sand": "Sand",
"grass": "Cỏ", "grass": "Grass",
"grass_paver": "Grass paver", "grass_paver": "Grass paver",
"clay": "Clay", "clay": "Clay",
"stone": "Stone" "stone": "Stone"

View File

@@ -28,7 +28,7 @@
"undo": "撤销", "undo": "撤销",
"redo": "恢复", "redo": "恢复",
"delete": "删除", "delete": "删除",
"delete_all": "全部删除", "delete_all": "",
"select_all": "全选", "select_all": "全选",
"view": "显示", "view": "显示",
"elevation_profile": "海拔剖面图", "elevation_profile": "海拔剖面图",
@@ -80,7 +80,7 @@
"center": "居中", "center": "居中",
"open_in": "打开于", "open_in": "打开于",
"copy_coordinates": "复制坐标", "copy_coordinates": "复制坐标",
"edit_osm": " OpenStreetMap 中编辑" "edit_osm": "Edit in OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -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": {
@@ -353,7 +353,7 @@
"water": "饮用水", "water": "饮用水",
"shower": "淋浴", "shower": "淋浴",
"shelter": "庇护所", "shelter": "庇护所",
"cemetery": "墓地", "cemetery": "Cemetery",
"motorized": "汽车和摩托车", "motorized": "汽车和摩托车",
"fuel-station": "加油站", "fuel-station": "加油站",
"parking": "停车场", "parking": "停车场",