1 Commits

Author SHA1 Message Date
vcoppe
c9472e10be New translations file.mdx (Czech) 2025-11-12 20:02:06 +01:00
123 changed files with 1835 additions and 2755 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

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2026 gpx.studio Copyright (c) 2024 gpx.studio
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

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,
@@ -18,9 +17,6 @@ import {
import { immerable, isDraft, original, freeze } from 'immer'; import { immerable, isDraft, original, freeze } from 'immer';
function cloneJSON<T>(obj: T): T { function cloneJSON<T>(obj: T): T {
if (obj === undefined) {
return undefined;
}
if (obj === null || typeof obj !== 'object') { if (obj === null || typeof obj !== 'object') {
return null; return null;
} }
@@ -37,6 +33,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 +73,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 +145,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 +183,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 +206,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 +217,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 +242,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 +804,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 +816,15 @@ 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()); statistics.local.elevation.smoothed = this._computeSmoothedElevation();
statistics.local.slope.at = this._computeSlope();
const points = this.trkpt; const points = this.trkpt;
for (let i = 0; i < points.length; i++) { for (let i = 0; i < points.length; i++) {
points[i]._data['index'] = i;
// distance // distance
let dist = 0; let dist = 0;
if (i > 0) { if (i > 0) {
@@ -834,18 +833,34 @@ 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);
// elevation
if (i > 0) {
const ele =
statistics.local.elevation.smoothed[i] -
statistics.local.elevation.smoothed[i - 1];
if (ele > 0) {
statistics.global.elevation.gain += ele;
} else if (ele < 0) {
statistics.global.elevation.loss -= ele;
}
}
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
// time // time
if (points[i].time === undefined) { if (points[i].time === undefined) {
statistics.local.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 +875,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(
@@ -945,7 +960,8 @@ export class TrackSegment extends GPXTreeLeaf {
} }
} }
this._elevationComputation(statistics); [statistics.local.slope.segment, statistics.local.slope.length] =
this._computeSlopeSegments(statistics);
statistics.global.time.total = statistics.global.time.total =
statistics.global.time.start && statistics.global.time.end statistics.global.time.start && statistics.global.time.end
@@ -961,115 +977,73 @@ 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 = distanceWindowSmoothingWithDistanceAccumulator(
points, points,
10000, 200,
(start, end) => (accumulated, start, end) =>
points[start].time && points[end].time points[start].time && points[end].time
? (3600 * ? (3600 * accumulated) /
(statistics.local.data[end].distance.total - (points[end].time.getTime() - points[start].time.getTime())
statistics.local.data[start].distance.total)) / : undefined
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;
} }
_elevationComputation(statistics: GPXStatistics) { _computeSmoothedElevation(): number[] {
const points = this.trkpt;
let smoothed = distanceWindowSmoothing(
points,
100,
(index) => points[index].ele ?? 0,
(accumulated, start, end) => accumulated / (end - start + 1)
);
if (points.length > 0) {
smoothed[0] = points[0].ele ?? 0;
smoothed[points.length - 1] = points[points.length - 1].ele ?? 0;
}
return smoothed;
}
_computeSlope(): number[] {
const points = this.trkpt;
return distanceWindowSmoothingWithDistanceAccumulator(
points,
50,
(accumulated, start, end) =>
(100 * ((points[end].ele ?? 0) - (points[start].ele ?? 0))) /
(accumulated > 0 ? accumulated : 1)
);
}
_computeSlopeSegments(statistics: GPXStatistics): [number[], number[]] {
let simplified = ramerDouglasPeucker( let simplified = ramerDouglasPeucker(
this.trkpt, this.trkpt,
20, 20,
getElevationDistanceFunction(statistics) getElevationDistanceFunction(statistics)
); );
for (let i = 0; i < simplified.length - 1; i++) { let slope = [];
let start = simplified[i].point._data.index; let length = [];
let end = simplified[i + 1].point._data.index;
let cumulEle = 0;
let currentStart = start;
let currentEnd = start;
let prevSmoothedEle = 0;
distanceWindowSmoothing(
start,
end + 1,
statistics,
0.1,
(s, e) => {
for (let i = currentStart; i < s; i++) {
cumulEle -= this.trkpt[i].ele ?? 0;
}
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;
}
}
);
}
if (statistics.global.length > 0) {
statistics.local.data[statistics.global.length - 1].elevation.gain =
statistics.global.elevation.gain;
statistics.local.data[statistics.global.length - 1].elevation.loss =
statistics.global.elevation.loss;
}
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( return [slope, length];
0,
this.trkpt.length,
statistics,
0.05,
(start, end) => {
const ele = this.trkpt[end].ele - this.trkpt[start].ele || 0;
const dist =
statistics.local.data[end].distance.total -
statistics.local.data[start].distance.total;
return dist > 0 ? (0.1 * ele) / dist : 0;
},
(value, index) => {
statistics.local.data[index].slope.at = value;
}
);
} }
getNumberOfTrackPoints(): number { getNumberOfTrackPoints(): number {
@@ -1316,8 +1290,8 @@ export class TrackSegment extends GPXTreeLeaf {
lastPoint: TrackPoint | undefined lastPoint: TrackPoint | undefined
) { ) {
let og = getOriginal(this); // Read as much as possible from the original object because it is faster let og = getOriginal(this); // Read as much as possible from the original object because it is faster
let statistics = og._computeStatistics(); let slope = og._computeSlope();
let trkpt = withArtificialTimestamps(og.trkpt, totalTime, lastPoint, startTime, statistics); let trkpt = withArtificialTimestamps(og.trkpt, totalTime, lastPoint, startTime, slope);
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
} }
@@ -1326,7 +1300,6 @@ export class TrackSegment extends GPXTreeLeaf {
} }
} }
const emptyExtensions: Record<string, string> = {};
export class TrackPoint { export class TrackPoint {
[immerable] = true; [immerable] = true;
@@ -1337,7 +1310,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 +1318,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 +1391,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 +1461,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,28 +1484,19 @@ 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;
this.name = waypoint.name === '' ? undefined : waypoint.name; this.name = waypoint.name;
this.cmt = waypoint.cmt === '' ? undefined : waypoint.cmt; this.cmt = waypoint.cmt;
this.desc = waypoint.desc === '' ? undefined : waypoint.desc; this.desc = waypoint.desc;
this.link = this.link = waypoint.link;
!waypoint.link || this.sym = waypoint.sym;
!waypoint.link.attributes || this.type = waypoint.type;
!waypoint.link.attributes.href ||
waypoint.link.attributes.href === ''
? undefined
: waypoint.link;
this.sym = waypoint.sym === '' ? undefined : waypoint.sym;
this.type = waypoint.type === '' ? undefined : waypoint.type;
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 +1544,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 +1593,310 @@ 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: {
smoothed: number[];
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: {
smoothed: [],
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.elevation.smoothed = this.local.elevation.smoothed.concat(
other.local.elevation.smoothed
);
this.local.slope.at = this.local.slope.at.concat(other.local.slope.at);
this.local.slope.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,
@@ -1656,15 +1911,11 @@ export function distance(
const rad = Math.PI / 180; const rad = Math.PI / 180;
const lat1 = coord1.lat * rad; const lat1 = coord1.lat * rad;
const lat2 = coord2.lat * rad; const lat2 = coord2.lat * rad;
const dLat = lat2 - lat1;
const dLon = (coord2.lon - coord1.lon) * rad;
// Haversine formula - better numerical stability for small distances
const a = const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(lat1) * Math.sin(lat2) +
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2); Math.cos(lat1) * Math.cos(lat2) * Math.cos((coord2.lon - coord1.lon) * rad);
const c = 2 * Math.asin(Math.sqrt(Math.min(a, 1))); const maxMeters = earthRadius * Math.acos(Math.min(a, 1));
return earthRadius * c; return maxMeters;
} }
export function getElevationDistanceFunction(statistics: GPXStatistics) { export function getElevationDistanceFunction(statistics: GPXStatistics) {
@@ -1675,9 +1926,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;
@@ -1691,61 +1942,57 @@ export function getElevationDistanceFunction(statistics: GPXStatistics) {
}; };
} }
function windowSmoothing( function distanceWindowSmoothing(
left: number, points: TrackPoint[],
right: number, distanceWindow: number,
distance: (index1: number, index2: number) => number, accumulate: (index: number) => number,
window: number, compute: (accumulated: number, start: number, end: number) => number,
compute: (start: number, end: number) => number, remove?: (index: number) => number
callback: (value: number, index: number) => void ): number[] {
): void { let result = [];
let start = left;
for (var i = left; i < right; i++) { let start = 0,
while (start + 1 < i && distance(start, i) > window) { end = 0,
accumulated = 0;
for (var i = 0; i < points.length; i++) {
while (
start + 1 < i &&
distance(points[start].getCoordinates(), points[i].getCoordinates()) > distanceWindow
) {
if (remove) {
accumulated -= remove(start);
} else {
accumulated -= accumulate(start);
}
start++; start++;
} }
let end = Math.min(i + 2, right); while (
while (end < right && distance(i, end) <= window) { end < points.length &&
distance(points[i].getCoordinates(), points[end].getCoordinates()) <= distanceWindow
) {
accumulated += accumulate(end);
end++; end++;
} }
callback(compute(start, end - 1), i); result[i] = compute(accumulated, start, end - 1);
} }
return result;
} }
function distanceWindowSmoothing( function distanceWindowSmoothingWithDistanceAccumulator(
left: number,
right: number,
statistics: GPXStatistics,
window: number,
compute: (start: number, end: number) => number,
callback: (value: number, index: number) => void
): void {
windowSmoothing(
left,
right,
(index1, index2) =>
statistics.local.data[index2].distance.total -
statistics.local.data[index1].distance.total,
window,
compute,
callback
);
}
function timeWindowSmoothing(
points: TrackPoint[], points: TrackPoint[],
window: number, distanceWindow: number,
compute: (start: number, end: number) => number, compute: (accumulated: number, start: number, end: number) => number
callback: (value: number, index: number) => void ): number[] {
): void { return distanceWindowSmoothing(
windowSmoothing( points,
0, distanceWindow,
points.length, (index) =>
(index1, index2) => index > 0
points[index2].time?.getTime() - points[index1].time?.getTime() || 2 * window, ? distance(points[index - 1].getCoordinates(), points[index].getCoordinates())
window, : 0,
compute, compute,
callback (index) => distance(points[index].getCoordinates(), points[index + 1].getCoordinates())
); );
} }
@@ -1797,14 +2044,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

@@ -3,6 +3,8 @@ import { Coordinates } from './types';
export type SimplifiedTrackPoint = { point: TrackPoint; distance?: number }; export type SimplifiedTrackPoint = { point: TrackPoint; distance?: number };
const earthRadius = 6371008.8;
export function ramerDouglasPeucker( export function ramerDouglasPeucker(
points: TrackPoint[], points: TrackPoint[],
epsilon: number = 50, epsilon: number = 50,
@@ -59,56 +61,76 @@ 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
); );
} }
const metersPerLatitudeDegree = 111320;
function getMetersPerLongitudeDegree(latitude: number): number {
return Math.cos((latitude * Math.PI) / 180) * metersPerLatitudeDegree;
}
function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number { function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number {
// Calculates the perpendicular distance in meters // Calculates the shortest distance in meters
// between a line segment (defined by p1 and p2) and a third point, p3. // between an arc (defined by p1 and p2) and a third point, p3.
// Uses simple planar geometry (ignores earth curvature). // Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
// Convert to meters using approximate scaling const rad = Math.PI / 180;
const metersPerLongitudeDegree = getMetersPerLongitudeDegree(coord1.lat); const lat1 = coord1.lat * rad;
const lat2 = coord2.lat * rad;
const lat3 = coord3.lat * rad;
const x1 = coord1.lon * metersPerLongitudeDegree; const lon1 = coord1.lon * rad;
const y1 = coord1.lat * metersPerLatitudeDegree; const lon2 = coord2.lon * rad;
const x2 = coord2.lon * metersPerLongitudeDegree; const lon3 = coord3.lon * rad;
const y2 = coord2.lat * metersPerLatitudeDegree;
const x3 = coord3.lon * metersPerLongitudeDegree;
const y3 = coord3.lat * metersPerLatitudeDegree;
const dx = x2 - x1; // Prerequisites for the formulas
const dy = y2 - y1; const bear12 = bearing(lat1, lon1, lat2, lon2);
const segmentLengthSquared = dx * dx + dy * dy; const bear13 = bearing(lat1, lon1, lat3, lon3);
let dis13 = distance(lat1, lon1, lat3, lon3);
if (segmentLengthSquared === 0) { let diff = Math.abs(bear13 - bear12);
// p1 and p2 are the same point if (diff > Math.PI) {
return Math.sqrt((x3 - x1) * (x3 - x1) + (y3 - y1) * (y3 - y1)); diff = 2 * Math.PI - diff;
} }
// Project p3 onto the line defined by p1-p2 // Is relative bearing obtuse?
const t = Math.max(0, Math.min(1, ((x3 - x1) * dx + (y3 - y1) * dy) / segmentLengthSquared)); if (diff > Math.PI / 2) {
return dis13;
}
// Find the closest point on the segment // Find the cross-track distance.
const projX = x1 + t * dx; let dxt = Math.asin(Math.sin(dis13 / earthRadius) * Math.sin(bear13 - bear12)) * earthRadius;
const projY = y1 + t * dy;
// Return distance from p3 to the projected point // Is p4 beyond the arc?
return Math.sqrt((x3 - projX) * (x3 - projX) + (y3 - projY) * (y3 - projY)); let dis12 = distance(lat1, lon1, lat2, lon2);
let dis14 =
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
if (dis14 > dis12) {
return distance(lat2, lon2, lat3, lon3);
} else {
return Math.abs(dxt);
}
}
function distance(latA: number, lonA: number, latB: number, lonB: number): number {
// Finds the distance between two lat / lon points.
return (
Math.acos(
Math.sin(latA) * Math.sin(latB) +
Math.cos(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
) * earthRadius
);
}
function bearing(latA: number, lonA: number, latB: number, lonB: number): number {
// Finds the bearing from one lat / lon point to another.
return Math.atan2(
Math.sin(lonB - lonA) * Math.cos(latB),
Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
);
} }
export function projectedPoint( export function projectedPoint(
@@ -124,39 +146,56 @@ export function projectedPoint(
} }
function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates { function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates {
// Calculates the point on the line segment defined by p1 and p2 // Calculates the point on the line defined by p1 and p2
// that is closest to the third point, p3. // that is closest to the third point, p3.
// Uses simple planar geometry (ignores earth curvature). // Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
// Convert to meters using approximate scaling const rad = Math.PI / 180;
const metersPerLongitudeDegree = getMetersPerLongitudeDegree(coord1.lat); const lat1 = coord1.lat * rad;
const lat2 = coord2.lat * rad;
const lat3 = coord3.lat * rad;
const x1 = coord1.lon * metersPerLongitudeDegree; const lon1 = coord1.lon * rad;
const y1 = coord1.lat * metersPerLatitudeDegree; const lon2 = coord2.lon * rad;
const x2 = coord2.lon * metersPerLongitudeDegree; const lon3 = coord3.lon * rad;
const y2 = coord2.lat * metersPerLatitudeDegree;
const x3 = coord3.lon * metersPerLongitudeDegree;
const y3 = coord3.lat * metersPerLatitudeDegree;
const dx = x2 - x1; // Prerequisites for the formulas
const dy = y2 - y1; const bear12 = bearing(lat1, lon1, lat2, lon2);
const segmentLengthSquared = dx * dx + dy * dy; const bear13 = bearing(lat1, lon1, lat3, lon3);
let dis13 = distance(lat1, lon1, lat3, lon3);
if (segmentLengthSquared === 0) { let diff = Math.abs(bear13 - bear12);
// p1 and p2 are the same point if (diff > Math.PI) {
diff = 2 * Math.PI - diff;
}
// Is relative bearing obtuse?
if (diff > Math.PI / 2) {
return coord1; return coord1;
} }
// Project p3 onto the line defined by p1-p2 // Find the cross-track distance.
const t = Math.max(0, Math.min(1, ((x3 - x1) * dx + (y3 - y1) * dy) / segmentLengthSquared)); let dxt = Math.asin(Math.sin(dis13 / earthRadius) * Math.sin(bear13 - bear12)) * earthRadius;
// Find the closest point on the segment // Is p4 beyond the arc?
const projX = x1 + t * dx; let dis12 = distance(lat1, lon1, lat2, lon2);
const projY = y1 + t * dy; let dis14 =
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
if (dis14 > dis12) {
return coord2;
} else {
// Determine the closest point (p4) on the great circle
const f = dis14 / earthRadius;
const lat4 = Math.asin(
Math.sin(lat1) * Math.cos(f) + Math.cos(lat1) * Math.sin(f) * Math.cos(bear12)
);
const lon4 =
lon1 +
Math.atan2(
Math.sin(bear12) * Math.sin(f) * Math.cos(lat1),
Math.cos(f) - Math.sin(lat1) * Math.sin(lat4)
);
// Convert back to degrees return { lat: lat4 / rad, lon: lon4 / rad };
return { }
lat: projY / metersPerLatitudeDegree,
lon: projX / metersPerLongitudeDegree,
};
} }

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
)
);
}
}
}

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "gpx.studio",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

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.12.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",
@@ -1701,10 +1701,9 @@
} }
}, },
"node_modules/@mapbox/point-geometry": { "node_modules/@mapbox/point-geometry": {
"version": "1.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
"integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ=="
"license": "ISC"
}, },
"node_modules/@mapbox/polyline": { "node_modules/@mapbox/polyline": {
"version": "1.2.1", "version": "1.2.1",
@@ -1739,26 +1738,11 @@
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==" "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw=="
}, },
"node_modules/@mapbox/vector-tile": { "node_modules/@mapbox/vector-tile": {
"version": "2.0.4", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
"integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==", "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
"license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"@mapbox/point-geometry": "~1.1.0", "@mapbox/point-geometry": "~0.1.0"
"@types/geojson": "^7946.0.16",
"pbf": "^4.0.1"
}
},
"node_modules/@mapbox/vector-tile/node_modules/pbf": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
"integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
"license": "BSD-3-Clause",
"dependencies": {
"resolve-protobuf-schema": "^2.1.0"
},
"bin": {
"pbf": "bin/pbf"
} }
}, },
"node_modules/@mapbox/whoots-js": { "node_modules/@mapbox/whoots-js": {
@@ -2660,8 +2644,7 @@
"node_modules/@types/mapbox__point-geometry": { "node_modules/@types/mapbox__point-geometry": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
"integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA=="
"license": "MIT"
}, },
"node_modules/@types/mapbox__sphericalmercator": { "node_modules/@types/mapbox__sphericalmercator": {
"version": "1.2.3", "version": "1.2.3",
@@ -2677,6 +2660,16 @@
"@types/geojson": "*" "@types/geojson": "*"
} }
}, },
"node_modules/@types/mapbox__vector-tile": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz",
"integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==",
"dependencies": {
"@types/geojson": "*",
"@types/mapbox__point-geometry": "*",
"@types/pbf": "*"
}
},
"node_modules/@types/mapbox-gl": { "node_modules/@types/mapbox-gl": {
"version": "3.4.1", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz", "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz",
@@ -3241,9 +3234,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 +3657,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"
@@ -4954,10 +4947,9 @@
} }
}, },
"node_modules/gl-matrix": { "node_modules/gl-matrix": {
"version": "3.4.4", "version": "3.4.3",
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
"license": "MIT"
}, },
"node_modules/glob": { "node_modules/glob": {
"version": "11.0.2", "version": "11.0.2",
@@ -6069,55 +6061,44 @@
} }
}, },
"node_modules/mapbox-gl": { "node_modules/mapbox-gl": {
"version": "3.17.0", "version": "3.12.0",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.17.0.tgz", "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.12.0.tgz",
"integrity": "sha512-nCrDKRlr5di6xUksUDslNWwxroJ5yv1hT8pyVFtcpWJOOKsYQxF/wOFTMie8oxMnXeFkrz1Tl1TwA1XN1yX0KA==", "integrity": "sha512-DV6TRr+xoPrLSKuGiUcbyLVkoLdNaNNpn6O7+ZC27yQH7BOOIF7l6JKbTCMhfMJuZBVJfL8YRJjlMJ6MZCTggA==",
"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": {
"@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2",
"@mapbox/mapbox-gl-supported": "^3.0.0", "@mapbox/mapbox-gl-supported": "^3.0.0",
"@mapbox/point-geometry": "^1.1.0", "@mapbox/point-geometry": "^0.1.0",
"@mapbox/tiny-sdf": "^2.0.6", "@mapbox/tiny-sdf": "^2.0.6",
"@mapbox/unitbezier": "^0.0.1", "@mapbox/unitbezier": "^0.0.1",
"@mapbox/vector-tile": "^2.0.4", "@mapbox/vector-tile": "^1.3.1",
"@mapbox/whoots-js": "^3.1.0", "@mapbox/whoots-js": "^3.1.0",
"@types/geojson": "^7946.0.16", "@types/geojson": "^7946.0.16",
"@types/geojson-vt": "^3.2.5", "@types/geojson-vt": "^3.2.5",
"@types/mapbox__point-geometry": "^0.1.4", "@types/mapbox__point-geometry": "^0.1.4",
"@types/mapbox__vector-tile": "^1.3.4",
"@types/pbf": "^3.0.5", "@types/pbf": "^3.0.5",
"@types/supercluster": "^7.1.3", "@types/supercluster": "^7.1.3",
"cheap-ruler": "^4.0.0", "cheap-ruler": "^4.0.0",
"csscolorparser": "~1.0.3", "csscolorparser": "~1.0.3",
"earcut": "^3.0.1", "earcut": "^3.0.1",
"geojson-vt": "^4.0.2", "geojson-vt": "^4.0.2",
"gl-matrix": "^3.4.4", "gl-matrix": "^3.4.3",
"grid-index": "^1.1.0", "grid-index": "^1.1.0",
"kdbush": "^4.0.2", "kdbush": "^4.0.2",
"martinez-polygon-clipping": "^0.7.4", "martinez-polygon-clipping": "^0.7.4",
"murmurhash-js": "^1.0.0", "murmurhash-js": "^1.0.0",
"pbf": "^4.0.1", "pbf": "^3.2.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",
} "vt-pbf": "^3.1.3"
},
"node_modules/mapbox-gl/node_modules/pbf": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
"integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
"license": "BSD-3-Clause",
"dependencies": {
"resolve-protobuf-schema": "^2.1.0"
},
"bin": {
"pbf": "bin/pbf"
} }
}, },
"node_modules/mapillary-js": { "node_modules/mapillary-js": {
@@ -7635,6 +7616,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",
@@ -9032,6 +9021,16 @@
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
"dev": true "dev": true
}, },
"node_modules/vt-pbf": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
"integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
"dependencies": {
"@mapbox/point-geometry": "0.1.0",
"@mapbox/vector-tile": "^1.3.1",
"pbf": "^3.2.1"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.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.12.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,124 @@
@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%);
--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%);
} }
@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 */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-ring: var(--ring);
--color-radius: var(--radius);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--color-support: var(--support);
--color-link: var(--link);
/* Colors */ --breakpoint-xs: 540px;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-ring: var(--ring);
--color-radius: var(--radius);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--color-support: var(--support);
--color-link: var(--link);
--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

@@ -24,14 +24,6 @@ export async function handle({ event, resolve }) {
let headTag = `<head> let headTag = `<head>
<title>gpx.studio — ${title}</title> <title>gpx.studio — ${title}</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "gpx.studio",
"url": "https://gpx.studio"
}
</script>
<meta name="description" content="${description}" /> <meta name="description" content="${description}" />
<meta property="og:title" content="gpx.studio — ${title}" /> <meta property="og:title" content="gpx.studio — ${title}" />
<meta property="og:description" content="${description}" /> <meta property="og:description" content="${description}" />

View File

@@ -17,6 +17,7 @@
} }
}, },
"sprite": "https://demotiles.maplibre.org/styles/osm-bright-gl-style/sprite", "sprite": "https://demotiles.maplibre.org/styles/osm-bright-gl-style/sprite",
"glyphs": "https://api.maptiler.com/fonts/{fontstack}/{range}.pbf?key={key}",
"layers": [ "layers": [
{ {
"id": "background", "id": "background",

View File

@@ -22,7 +22,7 @@ import {
Binoculars, Binoculars,
Toilet, Toilet,
} from 'lucide-static'; } from 'lucide-static';
import { type RasterDEMSourceSpecification, type StyleSpecification } from 'mapbox-gl'; import { type StyleSpecification } from 'mapbox-gl';
import ignFrTopo from './custom/ign-fr-topo.json'; import ignFrTopo from './custom/ign-fr-topo.json';
import ignFrPlan from './custom/ign-fr-plan.json'; import ignFrPlan from './custom/ign-fr-plan.json';
import ignFrSatellite from './custom/ign-fr-satellite.json'; import ignFrSatellite from './custom/ign-fr-satellite.json';
@@ -145,7 +145,7 @@ export const basemaps: { [key: string]: string | StyleSpecification } = {
swisstopoVector: 'https://vectortiles.geo.admin.ch/styles/ch.swisstopo.basemap.vt/style.json', swisstopoVector: 'https://vectortiles.geo.admin.ch/styles/ch.swisstopo.basemap.vt/style.json',
swisstopoSatellite: swisstopoSatellite:
'https://vectortiles.geo.admin.ch/styles/ch.swisstopo.imagerybasemap.vt/style.json', 'https://vectortiles.geo.admin.ch/styles/ch.swisstopo.imagerybasemap.vt/style.json',
linz: 'https://basemaps.linz.govt.nz/v1/styles/topographic-v2.json?api=d01fbtg0ar23gctac5m0jgyy2ds', linz: 'https://basemaps.linz.govt.nz/v1/tiles/topographic/EPSG:3857/style/topographic.json?api=d01fbtg0ar23gctac5m0jgyy2ds',
linzTopo: { linzTopo: {
version: 8, version: 8,
sources: { sources: {
@@ -368,42 +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',
},
],
},
mapterhornHillshade: {
version: 8,
sources: {
mapterhornHillshade: {
type: 'raster-dem',
url: 'https://tiles.mapterhorn.com/tilejson.json',
},
},
layers: [
{
id: 'mapterhornHillshade',
type: 'hillshade',
source: 'mapterhornHillshade',
},
],
},
swisstopoSlope: { swisstopoSlope: {
version: 8, version: 8,
sources: { sources: {
@@ -835,10 +799,8 @@ export const overlayTree: LayerTreeType = {
waymarkedTrailsHorseRiding: true, waymarkedTrailsHorseRiding: true,
waymarkedTrailsWinter: true, waymarkedTrailsWinter: true,
}, },
bikerouterGravel: true,
cyclOSMlite: true, cyclOSMlite: true,
mapterhornHillshade: true, bikerouterGravel: true,
openRailwayMap: true,
}, },
countries: { countries: {
france: { france: {
@@ -874,7 +836,6 @@ export const overpassTree: LayerTreeType = {
shower: true, shower: true,
shelter: true, shelter: true,
barrier: true, barrier: true,
cemetery: true,
}, },
tourism: { tourism: {
attraction: true, attraction: true,
@@ -921,10 +882,8 @@ export const defaultOverlays: LayerTreeType = {
waymarkedTrailsHorseRiding: false, waymarkedTrailsHorseRiding: false,
waymarkedTrailsWinter: false, waymarkedTrailsWinter: false,
}, },
bikerouterGravel: false,
cyclOSMlite: false, cyclOSMlite: false,
mapterhornHillshade: false, bikerouterGravel: false,
openRailwayMap: false,
}, },
countries: { countries: {
france: { france: {
@@ -960,7 +919,6 @@ export const defaultOverpassQueries: LayerTreeType = {
shower: false, shower: false,
shelter: false, shelter: false,
barrier: false, barrier: false,
cemetery: false,
}, },
tourism: { tourism: {
attraction: false, attraction: false,
@@ -1058,10 +1016,8 @@ export const defaultOverlayTree: LayerTreeType = {
waymarkedTrailsHorseRiding: false, waymarkedTrailsHorseRiding: false,
waymarkedTrailsWinter: false, waymarkedTrailsWinter: false,
}, },
bikerouterGravel: false,
cyclOSMlite: false, cyclOSMlite: false,
mapterhornHillshade: false, bikerouterGravel: false,
openRailwayMap: false,
}, },
countries: { countries: {
france: { france: {
@@ -1097,7 +1053,6 @@ export const defaultOverpassTree: LayerTreeType = {
shower: false, shower: false,
shelter: false, shelter: false,
barrier: false, barrier: false,
cemetery: false,
}, },
tourism: { tourism: {
attraction: false, attraction: false,
@@ -1144,7 +1099,9 @@ type OverpassQueryData = {
svg: string; svg: string;
color: string; color: string;
}; };
tags: Record<string, string | string[]> | Record<string, string | string[]>[]; tags:
| Record<string, string | boolean | string[]>
| Record<string, string | boolean | string[]>[];
symbol?: string; symbol?: string;
}; };
@@ -1225,20 +1182,6 @@ export const overpassQueryData: Record<string, OverpassQueryData> = {
}, },
symbol: 'Shelter', symbol: 'Shelter',
}, },
cemetery: {
icon: {
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 17v-10a6 5 0 1 1 12 0v10"/><path d="M 4 21 a 1 1 0 0 0 1 1 h 14 a 1 1 0 0 0 1-1 v -1 a 2 2 0 0 0-2-2 H6 a 2 2 0 0 0-2 2 z"/></svg>',
color: '#000000',
},
tags: [
{
landuse: 'cemetery',
},
{
amenity: 'grave_yard',
},
],
},
'fuel-station': { 'fuel-station': {
icon: { icon: {
svg: Fuel, svg: Fuel,
@@ -1275,25 +1218,7 @@ export const overpassQueryData: Record<string, OverpassQueryData> = {
color: '#000000', color: '#000000',
}, },
tags: { tags: {
barrier: [ barrier: true,
'bar',
'barrier_board',
'block',
'chain',
'cycle_barrier',
'gate',
'hampshire_gate',
'horse_stile',
'kissing_gate',
'lift_gate',
'motorcycle_barrier',
'sliding_beam',
'sliding_gate',
'stile',
'swing_gate',
'turnstile',
'wicket_gate',
],
}, },
}, },
attraction: { attraction: {
@@ -1453,18 +1378,3 @@ export const overpassQueryData: Record<string, OverpassQueryData> = {
symbol: 'Anchor', symbol: 'Anchor',
}, },
}; };
export const terrainSources: { [key: string]: RasterDEMSourceSpecification } = {
'mapbox-dem': {
type: 'raster-dem',
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
tileSize: 512,
maxzoom: 14,
},
mapterhorn: {
type: 'raster-dem',
url: 'https://tiles.mapterhorn.com/tilejson.json',
},
};
export const defaultTerrainSource = 'mapbox-dem';

View File

@@ -1,5 +1,6 @@
import { import {
Landmark, Landmark,
Icon,
Shell, Shell,
Bike, Bike,
Building, Building,
@@ -28,7 +29,6 @@ import {
TriangleAlert, TriangleAlert,
Anchor, Anchor,
Toilet, Toilet,
X,
type IconProps, type IconProps,
} from '@lucide/svelte'; } from '@lucide/svelte';
import { import {
@@ -61,7 +61,6 @@ import {
TriangleAlert as TriangleAlertSvg, TriangleAlert as TriangleAlertSvg,
Anchor as AnchorSvg, Anchor as AnchorSvg,
Toilet as ToiletSvg, Toilet as ToiletSvg,
X as XSvg,
} from 'lucide-static'; } from 'lucide-static';
import type { Component } from 'svelte'; import type { Component } from 'svelte';
@@ -88,11 +87,7 @@ export const symbols: { [key: string]: Symbol } = {
icon: ShoppingBasket, icon: ShoppingBasket,
iconSvg: ShoppingBasketSvg, iconSvg: ShoppingBasketSvg,
}, },
crossing: { crossing: { value: 'Crossing' },
value: 'Crossing',
icon: X,
iconSvg: XSvg,
},
department_store: { department_store: {
value: 'Department Store', value: 'Department Store',
icon: ShoppingBasket, icon: ShoppingBasket,

View File

@@ -18,7 +18,7 @@
href="https://github.com/gpxstudio/gpx.studio/blob/main/LICENSE" href="https://github.com/gpxstudio/gpx.studio/blob/main/LICENSE"
target="_blank" target="_blank"
> >
MIT © 2026 gpx.studio MIT © 2025 gpx.studio
</Button> </Button>
<LanguageSelect class="w-40 mt-3" /> <LanguageSelect class="w-40 mt-3" />
</div> </div>
@@ -34,7 +34,6 @@
{i18n._('homepage.home')} {i18n._('homepage.home')}
</Button> </Button>
<Button <Button
data-sveltekit-reload
variant="link" variant="link"
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href={getURLForLanguage(i18n.lang, '/app')} href={getURLForLanguage(i18n.lang, '/app')}
@@ -71,6 +70,15 @@
<Logo company="facebook" class="h-4 fill-muted-foreground" /> <Logo company="facebook" class="h-4 fill-muted-foreground" />
{i18n._('homepage.facebook')} {i18n._('homepage.facebook')}
</Button> </Button>
<Button
variant="link"
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://x.com/gpxstudio"
target="_blank"
>
<Logo company="x" class="h-4 fill-muted-foreground" />
{i18n._('homepage.x')}
</Button>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"

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

@@ -8,7 +8,7 @@
...others ...others
}: { }: {
iconOnly?: boolean; iconOnly?: boolean;
company?: 'gpx.studio' | 'mapbox' | 'github' | 'crowdin' | 'facebook' | 'reddit'; company?: 'gpx.studio' | 'mapbox' | 'github' | 'crowdin' | 'facebook' | 'x' | 'reddit';
[key: string]: any; [key: string]: any;
} = $props(); } = $props();
</script> </script>
@@ -55,6 +55,16 @@
d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z" d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"
/></svg /></svg
> >
{:else if company === 'x'}
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {others.class ?? ''}"
><title>X</title><path
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/></svg
>
{:else if company === 'reddit'} {:else if company === 'reddit'}
<svg <svg
role="img" role="img"

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' ||
@@ -645,19 +644,6 @@
} else if (e.key === 'F5') { } else if (e.key === 'F5') {
$routing = !$routing; $routing = !$routing;
e.preventDefault(); e.preventDefault();
} else if (
e.key === 'ArrowRight' ||
e.key === 'ArrowDown' ||
e.key === 'ArrowLeft' ||
e.key === 'ArrowUp'
) {
if (!targetInput) {
selection.updateFromKey(
e.key === 'ArrowRight' || e.key === 'ArrowDown',
e.shiftKey
);
e.preventDefault();
}
} }
}} }}
on:dragover={(e) => e.preventDefault()} on:dragover={(e) => e.preventDefault()}

View File

@@ -23,7 +23,6 @@
{i18n._('homepage.home')} {i18n._('homepage.home')}
</Button> </Button>
<Button <Button
data-sveltekit-reload
variant="link" variant="link"
class="text-base px-0 has-[>svg]:px-0" class="text-base px-0 has-[>svg]:px-0"
href={getURLForLanguage(i18n.lang, '/app')} href={getURLForLanguage(i18n.lang, '/app')}

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

@@ -20,7 +20,6 @@
import { loadFile } from '$lib/logic/file-actions'; import { loadFile } from '$lib/logic/file-actions';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { isSelected, toggle } from '$lib/components/map/layer-control/utils';
let { let {
useHash = true, useHash = true,
@@ -33,7 +32,6 @@
const { const {
currentBasemap, currentBasemap,
selectedBasemapTree,
distanceUnits, distanceUnits,
velocityUnits, velocityUnits,
temperatureUnits, temperatureUnits,
@@ -68,9 +66,6 @@
if (allowedEmbeddingBasemaps.includes(options.basemap)) { if (allowedEmbeddingBasemaps.includes(options.basemap)) {
$currentBasemap = options.basemap; $currentBasemap = options.basemap;
} }
if (!isSelected($selectedBasemapTree, options.basemap)) {
$selectedBasemapTree = toggle($selectedBasemapTree, options.basemap);
}
$distanceMarkers = options.distanceMarkers; $distanceMarkers = options.distanceMarkers;
$directionMarkers = options.directionMarkers; $directionMarkers = options.directionMarkers;
$distanceUnits = options.distanceUnits; $distanceUnits = options.distanceUnits;

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

@@ -121,16 +121,20 @@
} }
.vertical :global(button) { .vertical :global(button) {
@apply hover:bg-[var(--selection)]; @apply hover:bg-muted;
}
.vertical :global(.sortable-selected button) {
@apply hover:bg-accent;
} }
.vertical :global(.sortable-selected) { .vertical :global(.sortable-selected) {
@apply bg-[var(--selection)]; @apply bg-accent;
} }
.horizontal :global(button) { .horizontal :global(button) {
@apply bg-[var(--selection)]; @apply bg-accent;
@apply hover:bg-background; @apply hover:bg-muted;
} }
.horizontal :global(.sortable-selected button) { .horizontal :global(.sortable-selected button) {

View File

@@ -34,10 +34,11 @@
import { editStyle } from '$lib/components/file-list/style/utils.svelte'; import { editStyle } from '$lib/components/file-list/style/utils.svelte';
import { getSymbolKey, symbols } from '$lib/assets/symbols'; import { getSymbolKey, symbols } from '$lib/assets/symbols';
import { selection, copied, cut } from '$lib/logic/selection'; import { selection, copied, cut } from '$lib/logic/selection';
import { map } from '$lib/components/map/map';
import { fileActions, pasteSelection } from '$lib/logic/file-actions'; import { fileActions, pasteSelection } from '$lib/logic/file-actions';
import { allHidden } from '$lib/logic/hidden'; import { allHidden } from '$lib/logic/hidden';
import { boundsManager } from '$lib/logic/bounds'; import { boundsManager } from '$lib/logic/bounds';
import { gpxColors, gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers'; import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
import { fileStateCollection } from '$lib/logic/file-state'; import { fileStateCollection } from '$lib/logic/file-state';
import { waypointPopup } from '$lib/components/map/gpx-layer/gpx-layer-popup'; import { waypointPopup } from '$lib/components/map/gpx-layer/gpx-layer-popup';
import { allowedPastes } from './sortable-file-list'; import { allowedPastes } from './sortable-file-list';
@@ -57,31 +58,41 @@
let singleSelection = $derived($selection.size === 1); let singleSelection = $derived($selection.size === 1);
let nodeColors: string[] = $derived.by(() => { let nodeColors: string[] = $state([]);
$effect.pre(() => {
let colors: string[] = []; let colors: string[] = [];
if (node) { if (node && $map) {
if (node instanceof GPXFile) { if (node instanceof GPXFile) {
let defaultColor = $gpxColors.get(item.getFileId()); let defaultColor = undefined;
let layer = gpxLayers.getLayer(item.getFileId());
if (layer) {
defaultColor = layer.layerColor;
}
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 defaultColor = $gpxColors.get(item.getFileId()); let layer = gpxLayers.getLayer(item.getFileId());
if (defaultColor) { if (layer) {
colors.push(defaultColor); colors.push(layer.layerColor);
} }
} }
} }
} }
return colors; nodeColors = colors;
}); });
let symbolKey = $derived(node instanceof Waypoint ? getSymbolKey(node.sym) : undefined); let symbolKey = $derived(node instanceof Waypoint ? getSymbolKey(node.sym) : undefined);
@@ -164,7 +175,7 @@
let file = fileStateCollection.getFile(item.getFileId()); let file = fileStateCollection.getFile(item.getFileId());
if (layer && file) { if (layer && file) {
let waypoint = file.wpt[item.getWaypointIndex()]; let waypoint = file.wpt[item.getWaypointIndex()];
if (waypoint && !waypoint._data.hidden) { if (waypoint) {
waypointPopup?.setItem({ waypointPopup?.setItem({
item: waypoint, item: waypoint,
fileId: item.getFileId(), fileId: item.getFileId(),

View File

@@ -48,7 +48,7 @@
language = 'en'; language = 'en';
} }
map.init(language, hash, geocoder, geolocate); map.init(PUBLIC_MAPBOX_TOKEN, language, hash, geocoder, geolocate);
}); });
onDestroy(() => { onDestroy(() => {

View File

@@ -16,8 +16,7 @@
</script> </script>
<Button <Button
size="sm" class="p-1 has-[>svg]:px-2 h-8 justify-start {className}"
class="justify-start {className}"
variant="outline" variant="outline"
onclick={() => { onclick={() => {
navigator.clipboard.writeText( navigator.clipboard.writeText(

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onDestroy } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers'; import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
import { DistanceMarkers } from '$lib/components/map/gpx-layer/distance-markers'; import { DistanceMarkers } from '$lib/components/map/gpx-layer/distance-markers';
import { StartEndMarkers } from '$lib/components/map/gpx-layer/start-end-markers'; import { StartEndMarkers } from '$lib/components/map/gpx-layer/start-end-markers';
@@ -9,10 +9,13 @@
let distanceMarkers: DistanceMarkers; let distanceMarkers: DistanceMarkers;
let startEndMarkers: StartEndMarkers; let startEndMarkers: StartEndMarkers;
map.onLoad((map_) => { onMount(() => {
gpxLayers.init(); gpxLayers.init();
startEndMarkers = new StartEndMarkers(); startEndMarkers = new StartEndMarkers();
distanceMarkers = new DistanceMarkers(); distanceMarkers = new DistanceMarkers();
});
map.onLoad((map_) => {
createPopups(map_); createPopups(map_);
}); });

View File

@@ -1,13 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { TrackPoint } from 'gpx'; import type { TrackPoint } from 'gpx';
import { Button } from '$lib/components/ui/button';
import CopyCoordinates from '$lib/components/map/gpx-layer/CopyCoordinates.svelte'; import CopyCoordinates from '$lib/components/map/gpx-layer/CopyCoordinates.svelte';
import * as Card from '$lib/components/ui/card'; import * as Card from '$lib/components/ui/card';
import WithUnits from '$lib/components/WithUnits.svelte'; import WithUnits from '$lib/components/WithUnits.svelte';
import { Compass, Earth, Mountain, Timer } from '@lucide/svelte'; import { Compass, Mountain, Timer } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import type { PopupItem } from '$lib/components/map/map-popup'; import type { PopupItem } from '$lib/components/map/map-popup';
import { map } from '$lib/components/map/map';
let { trackpoint }: { trackpoint: PopupItem<TrackPoint> } = $props(); let { trackpoint }: { trackpoint: PopupItem<TrackPoint> } = $props();
</script> </script>
@@ -37,17 +35,5 @@
onCopy={() => trackpoint.hide?.()} onCopy={() => trackpoint.hide?.()}
class="mt-0.5" class="mt-0.5"
/> />
{#if trackpoint.fileId === undefined}
<Button
size="sm"
variant="outline"
class="justify-start"
href={`https://www.openstreetmap.org/edit?#map=${(($map?.getZoom() ?? 17) + 1).toFixed(0)}/${trackpoint.item.getLatitude().toFixed(5)}/${trackpoint.item.getLongitude().toFixed(5)}`}
target="_blank"
>
<Earth size="14" />
{i18n._('menu.edit_osm')}
</Button>
{/if}
</Card.Content> </Card.Content>
</Card.Root> </Card.Root>

View File

@@ -13,8 +13,6 @@
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js'; import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import type { PopupItem } from '$lib/components/map/map-popup'; import type { PopupItem } from '$lib/components/map/map-popup';
import { selection } from '$lib/logic/selection';
import { ListFileItem } from '$lib/components/file-list/file-list';
let { let {
waypoint, waypoint,
@@ -22,9 +20,6 @@
waypoint: PopupItem<Waypoint>; waypoint: PopupItem<Waypoint>;
} = $props(); } = $props();
let selected = $derived(
waypoint.fileId ? $selection.hasAnyChildren(new ListFileItem(waypoint.fileId)) : false
);
let symbolKey = $derived(waypoint ? getSymbolKey(waypoint.item.sym) : undefined); let symbolKey = $derived(waypoint ? getSymbolKey(waypoint.item.sym) : undefined);
function sanitize(text: string | undefined): string { function sanitize(text: string | undefined): string {
@@ -86,7 +81,7 @@
</ScrollArea> </ScrollArea>
<div class="mt-2 flex flex-col gap-1"> <div class="mt-2 flex flex-col gap-1">
<CopyCoordinates coordinates={waypoint.item.attributes} /> <CopyCoordinates coordinates={waypoint.item.attributes} />
{#if $currentTool === Tool.WAYPOINT && selected} {#if $currentTool === Tool.WAYPOINT}
<Button <Button
class="p-1 has-[>svg]:px-2 h-8" class="p-1 has-[>svg]:px-2 h-8"
variant="outline" variant="outline"

View File

@@ -3,12 +3,19 @@ import { gpxStatistics } from '$lib/logic/statistics';
import { getConvertedDistanceToKilometers } from '$lib/units'; import { getConvertedDistanceToKilometers } from '$lib/units';
import type { GeoJSONSource } from 'mapbox-gl'; import type { GeoJSONSource } from 'mapbox-gl';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import { allHidden } from '$lib/logic/hidden'; import { allHidden } from '$lib/logic/hidden';
const { distanceMarkers, distanceUnits } = settings; const { distanceMarkers, distanceUnits } = settings;
const levels = [100, 50, 25, 10, 5, 1]; const stops = [
[100, 0],
[50, 7],
[25, 8, 10],
[10, 10],
[5, 11],
[1, 13],
];
export class DistanceMarkers { export class DistanceMarkers {
updateBinded: () => void = this.update.bind(this); updateBinded: () => void = this.update.bind(this);
@@ -43,33 +50,22 @@ export class DistanceMarkers {
data: this.getDistanceMarkersGeoJSON(), data: this.getDistanceMarkersGeoJSON(),
}); });
} }
if (!map_.getLayer('distance-markers')) { stops.forEach(([d, minzoom, maxzoom]) => {
map_.addLayer( if (!map_.getLayer(`distance-markers-${d}`)) {
{ map_.addLayer({
id: 'distance-markers', id: `distance-markers-${d}`,
type: 'symbol', type: 'symbol',
source: 'distance-markers', source: 'distance-markers',
filter: [ filter:
'match', d === 5
['get', 'level'], ? [
100, 'any',
['>=', ['zoom'], 0], ['==', ['get', 'level'], 5],
50, ['==', ['get', 'level'], 25],
['>=', ['zoom'], 7], ]
25, : ['==', ['get', 'level'], d],
[ minzoom: minzoom,
'any', maxzoom: maxzoom ?? 24,
['all', ['>=', ['zoom'], 8], ['<=', ['zoom'], 9]],
['>=', ['zoom'], 11],
],
10,
['>=', ['zoom'], 10],
5,
['>=', ['zoom'], 11],
1,
['>=', ['zoom'], 13],
false,
],
layout: { layout: {
'text-field': ['get', 'distance'], 'text-field': ['get', 'distance'],
'text-size': 14, 'text-size': 14,
@@ -80,14 +76,17 @@ export class DistanceMarkers {
'text-halo-width': 2, 'text-halo-width': 2,
'text-halo-color': 'white', 'text-halo-color': 'white',
}, },
}, });
ANCHOR_LAYER_KEY.distanceMarkers } else {
); map_.moveLayer(`distance-markers-${d}`);
} }
});
} else { } else {
if (map_.getLayer('distance-markers')) { stops.forEach(([d]) => {
map_.removeLayer('distance-markers'); if (map_.getLayer(`distance-markers-${d}`)) {
} map_.removeLayer(`distance-markers-${d}`);
}
});
} }
} 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
@@ -102,26 +101,35 @@ 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, minzoom] = stops.find(([d]) => currentTargetDistance % d === 0) ?? [
0, 0,
];
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,
level, level,
minzoom,
}, },
} as GeoJSON.Feature); } as GeoJSON.Feature);
currentTargetDistance += 1; currentTargetDistance += 1;
} }
}); }
return { return {
type: 'FeatureCollection', type: 'FeatureCollection',

View File

@@ -1,6 +1,6 @@
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 { ANCHOR_LAYER_KEY, 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 {
ListTrackSegmentItem, ListTrackSegmentItem,
@@ -22,7 +22,6 @@ import { fileActionManager } from '$lib/logic/file-action-manager';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors'; import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import { gpxColors } from '$lib/components/map/gpx-layer/gpx-layers';
const colors = [ const colors = [
'#ff0000', '#ff0000',
@@ -44,49 +43,26 @@ for (let color of colors) {
} }
// Get the color with the least amount of uses // Get the color with the least amount of uses
function getColor(fileId: string) { function getColor() {
let color = colors.reduce((a, b) => (colorCount[a] <= colorCount[b] ? a : b)); let color = colors.reduce((a, b) => (colorCount[a] <= colorCount[b] ? a : b));
colorCount[color]++; colorCount[color]++;
gpxColors.update((colors) => {
colors.set(fileId, color);
return colors;
});
return color; return color;
} }
function replaceColor(fileId: string, oldColor: string, newColor: string) { function decrementColor(color: string) {
if (colorCount.hasOwnProperty(oldColor)) {
colorCount[oldColor]--;
}
colorCount[newColor]++;
gpxColors.update((colors) => {
colors.set(fileId, newColor);
return colors;
});
}
function removeColor(fileId: string, color: string) {
if (colorCount.hasOwnProperty(color)) { if (colorCount.hasOwnProperty(color)) {
colorCount[color]--; colorCount[color]--;
} }
gpxColors.update((colors) => {
colors.delete(fileId);
return colors;
});
} }
export function getSvgForSymbol(symbol?: string | undefined, layerColor?: string | undefined) { function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined; let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined;
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
${ ${Square.replace('width="24"', 'width="12"')
layerColor .replace('height="24"', 'height="12"')
? Square.replace('width="24"', 'width="12"') .replace('stroke="currentColor"', 'stroke="SteelBlue"')
.replace('height="24"', 'height="12"') .replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
.replace('stroke="currentColor"', 'stroke="SteelBlue"') .replace('fill="none"', `fill="${layerColor}"`)}
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
.replace('fill="none"', `fill="${layerColor}"`)
: ''
}
${MapPin.replace('width="24"', '') ${MapPin.replace('width="24"', '')
.replace('height="24"', '') .replace('height="24"', '')
.replace('stroke="currentColor"', '') .replace('stroke="currentColor"', '')
@@ -111,10 +87,9 @@ export class GPXLayer {
fileId: string; fileId: string;
file: Readable<GPXFileWithStatistics | undefined>; file: Readable<GPXFileWithStatistics | undefined>;
layerColor: string; layerColor: string;
markers: mapboxgl.Marker[] = [];
selected: boolean = false; selected: boolean = false;
currentWaypointData: GeoJSON.FeatureCollection | null = null; draggable: boolean;
draggedWaypointIndex: number | null = null;
draggingStartingPosition: mapboxgl.Point = new mapboxgl.Point(0, 0);
unsubscribe: Function[] = []; unsubscribe: Function[] = [];
updateBinded: () => void = this.update.bind(this); updateBinded: () => void = this.update.bind(this);
@@ -123,25 +98,11 @@ export class GPXLayer {
layerOnMouseMoveBinded: (e: any) => void = this.layerOnMouseMove.bind(this); layerOnMouseMoveBinded: (e: any) => void = this.layerOnMouseMove.bind(this);
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this); layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this); layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
waypointLayerOnMouseEnterBinded: (e: mapboxgl.MapMouseEvent) => void =
this.waypointLayerOnMouseEnter.bind(this);
waypointLayerOnMouseLeaveBinded: (e: mapboxgl.MapMouseEvent) => void =
this.waypointLayerOnMouseLeave.bind(this);
waypointLayerOnClickBinded: (e: mapboxgl.MapMouseEvent) => void =
this.waypointLayerOnClick.bind(this);
waypointLayerOnMouseDownBinded: (e: mapboxgl.MapMouseEvent) => void =
this.waypointLayerOnMouseDown.bind(this);
waypointLayerOnTouchStartBinded: (e: mapboxgl.MapTouchEvent) => void =
this.waypointLayerOnTouchStart.bind(this);
waypointLayerOnMouseMoveBinded: (e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) => void =
this.waypointLayerOnMouseMove.bind(this);
waypointLayerOnMouseUpBinded: (e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) => void =
this.waypointLayerOnMouseUp.bind(this);
constructor(fileId: string, file: Readable<GPXFileWithStatistics | undefined>) { constructor(fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
this.fileId = fileId; this.fileId = fileId;
this.file = file; this.file = file;
this.layerColor = getColor(fileId); this.layerColor = getColor();
this.unsubscribe.push( this.unsubscribe.push(
map.subscribe(($map) => { map.subscribe(($map) => {
if ($map) { if ($map) {
@@ -164,6 +125,18 @@ export class GPXLayer {
}) })
); );
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded)); this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
this.unsubscribe.push(
currentTool.subscribe((tool) => {
if (tool === Tool.WAYPOINT && !this.draggable) {
this.draggable = true;
this.markers.forEach((marker) => marker.setDraggable(true));
} else if (tool !== Tool.WAYPOINT && this.draggable) {
this.draggable = false;
this.markers.forEach((marker) => marker.setDraggable(false));
}
})
);
this.draggable = get(currentTool) === Tool.WAYPOINT;
} }
update() { update() {
@@ -178,12 +151,10 @@ export class GPXLayer {
file._data.style.color && file._data.style.color &&
this.layerColor !== `#${file._data.style.color}` this.layerColor !== `#${file._data.style.color}`
) { ) {
replaceColor(this.fileId, this.layerColor, `#${file._data.style.color}`); decrementColor(this.layerColor);
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) {
@@ -196,23 +167,20 @@ export class GPXLayer {
} }
if (!_map.getLayer(this.fileId)) { if (!_map.getLayer(this.fileId)) {
_map.addLayer( _map.addLayer({
{ id: this.fileId,
id: this.fileId, type: 'line',
type: 'line', source: this.fileId,
source: this.fileId, layout: {
layout: { 'line-join': 'round',
'line-join': 'round', 'line-cap': 'round',
'line-cap': 'round',
},
paint: {
'line-color': ['get', 'color'],
'line-width': ['get', 'width'],
'line-opacity': ['get', 'opacity'],
},
}, },
ANCHOR_LAYER_KEY.tracks paint: {
); 'line-color': ['get', 'color'],
'line-width': ['get', 'width'],
'line-opacity': ['get', 'opacity'],
},
});
_map.on('click', this.fileId, this.layerOnClickBinded); _map.on('click', this.fileId, this.layerOnClickBinded);
_map.on('contextmenu', this.fileId, this.layerOnContextMenuBinded); _map.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
@@ -221,59 +189,6 @@ export class GPXLayer {
_map.on('mousemove', this.fileId, this.layerOnMouseMoveBinded); _map.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
} }
let waypointSource = _map.getSource(this.fileId + '-waypoints') as
| mapboxgl.GeoJSONSource
| undefined;
this.currentWaypointData = this.getWaypointsGeoJSON();
if (waypointSource) {
waypointSource.setData(this.currentWaypointData);
} else {
_map.addSource(this.fileId + '-waypoints', {
type: 'geojson',
data: this.currentWaypointData,
});
}
if (!_map.getLayer(this.fileId + '-waypoints')) {
_map.addLayer(
{
id: this.fileId + '-waypoints',
type: 'symbol',
source: this.fileId + '-waypoints',
layout: {
'icon-image': ['get', 'icon'],
'icon-size': 0.3,
'icon-anchor': 'bottom',
'icon-padding': 0,
'icon-allow-overlap': true,
},
},
ANCHOR_LAYER_KEY.waypoints
);
_map.on(
'mouseenter',
this.fileId + '-waypoints',
this.waypointLayerOnMouseEnterBinded
);
_map.on(
'mouseleave',
this.fileId + '-waypoints',
this.waypointLayerOnMouseLeaveBinded
);
_map.on('click', this.fileId + '-waypoints', this.waypointLayerOnClickBinded);
_map.on(
'mousedown',
this.fileId + '-waypoints',
this.waypointLayerOnMouseDownBinded
);
_map.on(
'touchstart',
this.fileId + '-waypoints',
this.waypointLayerOnTouchStartBinded
);
}
if (get(directionMarkers)) { if (get(directionMarkers)) {
if (!_map.getLayer(this.fileId + '-direction')) { if (!_map.getLayer(this.fileId + '-direction')) {
_map.addLayer( _map.addLayer(
@@ -298,7 +213,7 @@ export class GPXLayer {
'text-halo-color': 'white', 'text-halo-color': 'white',
}, },
}, },
ANCHOR_LAYER_KEY.directionMarkers _map.getLayer('distance-markers-100') ? 'distance-markers-100' : undefined
); );
} }
} else { } else {
@@ -307,40 +222,151 @@ export class GPXLayer {
} }
} }
let visibleTrackSegmentIds: string[] = []; let visibleItems: [number, number][] = [];
file.forEachSegment((segment, trackIndex, segmentIndex) => { file.forEachSegment((segment, trackIndex, segmentIndex) => {
if (!segment._data.hidden) { if (!segment._data.hidden) {
visibleTrackSegmentIds.push(`${trackIndex}-${segmentIndex}`); visibleItems.push([trackIndex, segmentIndex]);
}
});
const segmentFilter: FilterSpecification = [
'in',
['get', 'trackSegmentId'],
['literal', visibleTrackSegmentIds],
];
_map.setFilter(this.fileId, segmentFilter, { validate: false });
if (_map.getLayer(this.fileId + '-direction')) {
_map.setFilter(this.fileId + '-direction', segmentFilter, { validate: false });
}
let visibleWaypoints: number[] = [];
file.wpt.forEach((waypoint, waypointIndex) => {
if (!waypoint._data.hidden) {
visibleWaypoints.push(waypointIndex);
} }
}); });
_map.setFilter( _map.setFilter(
this.fileId + '-waypoints', this.fileId,
['in', ['get', 'waypointIndex'], ['literal', visibleWaypoints]], [
'any',
...visibleItems.map(([trackIndex, segmentIndex]) => [
'all',
['==', 'trackIndex', trackIndex],
['==', 'segmentIndex', segmentIndex],
]),
],
{ validate: false } { validate: false }
); );
if (_map.getLayer(this.fileId + '-direction')) {
_map.setFilter(
this.fileId + '-direction',
[
'any',
...visibleItems.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;
} }
let markerIndex = 0;
if (get(selection).hasAnyChildren(new ListFileItem(this.fileId))) {
file.wpt.forEach((waypoint) => {
// Update markers
let symbolKey = getSymbolKey(waypoint.sym);
if (markerIndex < this.markers.length) {
this.markers[markerIndex].getElement().innerHTML = getMarkerForSymbol(
symbolKey,
this.layerColor
);
this.markers[markerIndex].setLngLat(waypoint.getCoordinates());
Object.defineProperty(this.markers[markerIndex], '_waypoint', {
value: waypoint,
writable: true,
});
} else {
let element = document.createElement('div');
element.classList.add('w-8', 'h-8', 'drop-shadow-xl');
element.innerHTML = getMarkerForSymbol(symbolKey, this.layerColor);
let marker = new mapboxgl.Marker({
draggable: this.draggable,
element,
anchor: 'bottom',
}).setLngLat(waypoint.getCoordinates());
Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true });
let dragEndTimestamp = 0;
marker.getElement().addEventListener('mousemove', (e) => {
if (marker._isDragging) {
return;
}
waypointPopup?.setItem({ item: marker._waypoint, fileId: this.fileId });
e.stopPropagation();
});
marker.getElement().addEventListener('click', (e) => {
if (dragEndTimestamp && Date.now() - dragEndTimestamp < 1000) {
return;
}
if (get(currentTool) === Tool.WAYPOINT && e.shiftKey) {
fileActions.deleteWaypoint(this.fileId, marker._waypoint._data.index);
e.stopPropagation();
return;
}
if (get(treeFileView)) {
if (
(e.ctrlKey || e.metaKey) &&
get(selection).hasAnyChildren(
new ListWaypointsItem(this.fileId),
false
)
) {
selection.addSelectItem(
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
);
} else {
selection.selectItem(
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
);
}
} else if (get(currentTool) === Tool.WAYPOINT) {
selectedWaypoint.set([marker._waypoint, this.fileId]);
} else {
waypointPopup?.setItem({ item: marker._waypoint, fileId: this.fileId });
}
e.stopPropagation();
});
marker.on('dragstart', () => {
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, true);
marker.getElement().style.cursor = 'grabbing';
waypointPopup?.hide();
});
marker.on('dragend', (e) => {
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
marker.getElement().style.cursor = '';
getElevation([marker._waypoint]).then((ele) => {
fileActionManager.applyToFile(this.fileId, (file) => {
let latLng = marker.getLngLat();
let wpt = file.wpt[marker._waypoint._data.index];
wpt.setCoordinates({
lat: latLng.lat,
lon: latLng.lng,
});
wpt.ele = ele[0];
});
});
dragEndTimestamp = Date.now();
});
this.markers.push(marker);
}
markerIndex++;
});
}
while (markerIndex < this.markers.length) {
// Remove extra markers
this.markers.pop()?.remove();
}
this.markers.forEach((marker) => {
if (!marker._waypoint._data.hidden) {
marker.addTo(_map);
} else {
marker.remove();
}
});
} }
remove() { remove() {
@@ -353,24 +379,6 @@ export class GPXLayer {
_map.off('mousemove', this.fileId, this.layerOnMouseMoveBinded); _map.off('mousemove', this.fileId, this.layerOnMouseMoveBinded);
_map.off('style.import.load', this.updateBinded); _map.off('style.import.load', this.updateBinded);
_map.off(
'mouseenter',
this.fileId + '-waypoints',
this.waypointLayerOnMouseEnterBinded
);
_map.off(
'mouseleave',
this.fileId + '-waypoints',
this.waypointLayerOnMouseLeaveBinded
);
_map.off('click', this.fileId + '-waypoints', this.waypointLayerOnClickBinded);
_map.off('mousedown', this.fileId + '-waypoints', this.waypointLayerOnMouseDownBinded);
_map.off(
'touchstart',
this.fileId + '-waypoints',
this.waypointLayerOnTouchStartBinded
);
if (_map.getLayer(this.fileId + '-direction')) { if (_map.getLayer(this.fileId + '-direction')) {
_map.removeLayer(this.fileId + '-direction'); _map.removeLayer(this.fileId + '-direction');
} }
@@ -380,17 +388,15 @@ export class GPXLayer {
if (_map.getSource(this.fileId)) { if (_map.getSource(this.fileId)) {
_map.removeSource(this.fileId); _map.removeSource(this.fileId);
} }
if (_map.getLayer(this.fileId + '-waypoints')) {
_map.removeLayer(this.fileId + '-waypoints');
}
if (_map.getSource(this.fileId + '-waypoints')) {
_map.removeSource(this.fileId + '-waypoints');
}
} }
this.markers.forEach((marker) => {
marker.remove();
});
this.unsubscribe.forEach((unsubscribe) => unsubscribe()); this.unsubscribe.forEach((unsubscribe) => unsubscribe());
removeColor(this.fileId, this.layerColor); decrementColor(this.layerColor);
} }
moveToFront() { moveToFront() {
@@ -399,13 +405,10 @@ export class GPXLayer {
return; return;
} }
if (_map.getLayer(this.fileId)) { if (_map.getLayer(this.fileId)) {
_map.moveLayer(this.fileId, ANCHOR_LAYER_KEY.tracks); _map.moveLayer(this.fileId);
}
if (_map.getLayer(this.fileId + '-waypoints')) {
_map.moveLayer(this.fileId + '-waypoints', ANCHOR_LAYER_KEY.waypoints);
} }
if (_map.getLayer(this.fileId + '-direction')) { if (_map.getLayer(this.fileId + '-direction')) {
_map.moveLayer(this.fileId + '-direction', ANCHOR_LAYER_KEY.directionMarkers); _map.moveLayer(this.fileId + '-direction');
} }
} }
@@ -446,7 +449,7 @@ export class GPXLayer {
} }
} }
layerOnClick(e: mapboxgl.MapMouseEvent) { layerOnClick(e: any) {
if ( if (
get(currentTool) === Tool.ROUTING && get(currentTool) === Tool.ROUTING &&
get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])
@@ -454,8 +457,8 @@ export class GPXLayer {
return; return;
} }
let trackIndex = e.features![0].properties!.trackIndex; let trackIndex = e.features[0].properties.trackIndex;
let segmentIndex = e.features![0].properties!.segmentIndex; let segmentIndex = e.features[0].properties.segmentIndex;
if ( if (
get(currentTool) === Tool.SCISSORS && get(currentTool) === Tool.SCISSORS &&
@@ -463,11 +466,6 @@ export class GPXLayer {
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
) )
) { ) {
if (get(map)?.queryRenderedFeatures(e.point, { layers: ['split-controls'] }).length) {
// Clicked on split control, ignoring
return;
}
fileActions.split(get(splitAs), this.fileId, trackIndex, segmentIndex, { fileActions.split(get(splitAs), this.fileId, trackIndex, segmentIndex, {
lat: e.lngLat.lat, lat: e.lngLat.lat,
lon: e.lngLat.lng, lon: e.lngLat.lng,
@@ -504,160 +502,6 @@ export class GPXLayer {
} }
} }
waypointLayerOnMouseEnter(e: mapboxgl.MapMouseEvent) {
if (this.draggedWaypointIndex !== null) {
return;
}
let file = get(this.file)?.file;
if (!file) {
return;
}
let waypointIndex = e.features![0].properties!.waypointIndex;
let waypoint = file.wpt[waypointIndex];
waypointPopup?.setItem({ item: waypoint, fileId: this.fileId });
mapCursor.notify(MapCursorState.WAYPOINT_HOVER, true);
}
waypointLayerOnMouseLeave() {
mapCursor.notify(MapCursorState.WAYPOINT_HOVER, false);
}
waypointLayerOnClick(e: mapboxgl.MapMouseEvent) {
e.preventDefault();
let waypointIndex = e.features![0].properties!.waypointIndex;
let file = get(this.file)?.file;
if (!file) {
return;
}
let waypoint = file.wpt[waypointIndex];
if (get(currentTool) === Tool.WAYPOINT) {
if (this.selected) {
if (e.originalEvent.shiftKey) {
fileActions.deleteWaypoint(this.fileId, waypointIndex);
} else {
selection.selectItem(new ListWaypointItem(this.fileId, waypointIndex));
selectedWaypoint.set([waypoint, this.fileId]);
}
} else {
if (get(treeFileView)) {
selection.selectItem(new ListWaypointItem(this.fileId, waypointIndex));
} else {
selection.selectItem(new ListFileItem(this.fileId));
}
selectedWaypoint.set([waypoint, this.fileId]);
}
} else {
if (get(treeFileView)) {
if ((e.originalEvent.ctrlKey || e.originalEvent.metaKey) && this.selected) {
selection.addSelectItem(new ListWaypointItem(this.fileId, waypointIndex));
} else {
selection.selectItem(new ListWaypointItem(this.fileId, waypointIndex));
}
} else {
if (!this.selected) {
selection.selectItem(new ListFileItem(this.fileId));
}
waypointPopup?.setItem({ item: waypoint, fileId: this.fileId });
}
}
}
waypointLayerOnMouseDown(e: mapboxgl.MapMouseEvent) {
if (get(currentTool) !== Tool.WAYPOINT || !this.selected) {
return;
}
const _map = get(map);
if (!_map) {
return;
}
e.preventDefault();
this.draggedWaypointIndex = e.features![0].properties!.waypointIndex;
this.draggingStartingPosition = e.point;
waypointPopup?.hide();
_map.on('mousemove', this.waypointLayerOnMouseMoveBinded);
_map.once('mouseup', this.waypointLayerOnMouseUpBinded);
}
waypointLayerOnTouchStart(e: mapboxgl.MapTouchEvent) {
if (e.points.length !== 1 || get(currentTool) !== Tool.WAYPOINT || !this.selected) {
return;
}
const _map = get(map);
if (!_map) {
return;
}
this.draggedWaypointIndex = e.features![0].properties!.waypointIndex;
this.draggingStartingPosition = e.point;
waypointPopup?.hide();
e.preventDefault();
_map.on('touchmove', this.waypointLayerOnMouseMoveBinded);
_map.once('touchend', this.waypointLayerOnMouseUpBinded);
}
waypointLayerOnMouseMove(e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) {
if (this.draggedWaypointIndex === null || e.point.equals(this.draggingStartingPosition)) {
return;
}
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, true);
(
this.currentWaypointData!.features[this.draggedWaypointIndex].geometry as GeoJSON.Point
).coordinates = [e.lngLat.lng, e.lngLat.lat];
let waypointSource = get(map)?.getSource(this.fileId + '-waypoints') as
| mapboxgl.GeoJSONSource
| undefined;
if (waypointSource) {
waypointSource.setData(this.currentWaypointData!);
}
}
waypointLayerOnMouseUp(e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) {
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
get(map)?.off('mousemove', this.waypointLayerOnMouseMoveBinded);
get(map)?.off('touchmove', this.waypointLayerOnMouseMoveBinded);
if (this.draggedWaypointIndex === null) {
return;
}
if (e.point.equals(this.draggingStartingPosition)) {
this.draggedWaypointIndex = null;
return;
}
getElevation([
{
lat: e.lngLat.lat,
lon: e.lngLat.lng,
},
]).then((ele) => {
if (this.draggedWaypointIndex === null) {
return;
}
fileActionManager.applyToFile(this.fileId, (file) => {
let wpt = file.wpt[this.draggedWaypointIndex!];
wpt.setCoordinates({
lat: e.lngLat.lat,
lon: e.lngLat.lng,
});
wpt.ele = ele[0];
});
this.draggedWaypointIndex = null;
});
}
getGeoJSON(): GeoJSON.FeatureCollection { getGeoJSON(): GeoJSON.FeatureCollection {
let file = get(this.file)?.file; let file = get(this.file)?.file;
if (!file) { if (!file) {
@@ -695,7 +539,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) {
@@ -705,65 +548,4 @@ export class GPXLayer {
} }
return data; return data;
} }
getWaypointsGeoJSON(): GeoJSON.FeatureCollection {
let file = get(this.file)?.file;
let data: GeoJSON.FeatureCollection = {
type: 'FeatureCollection',
features: [],
};
if (!file) {
return data;
}
file.wpt.forEach((waypoint, index) => {
data.features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [waypoint.getLongitude(), waypoint.getLatitude()],
},
properties: {
fileId: this.fileId,
waypointIndex: index,
icon: `waypoint-${getSymbolKey(waypoint.sym) ?? 'default'}-${this.layerColor}`,
},
});
});
return data;
}
loadIcons() {
const _map = get(map);
let file = get(this.file)?.file;
if (!_map || !file) {
return;
}
let symbols = new Set<string | undefined>();
file.wpt.forEach((waypoint) => {
symbols.add(getSymbolKey(waypoint.sym));
});
symbols.forEach((symbol) => {
const iconId = `waypoint-${symbol ?? 'default'}-${this.layerColor}`;
if (!_map.hasImage(iconId)) {
let icon = new Image(100, 100);
icon.onload = () => {
if (!_map.hasImage(iconId)) {
_map.addImage(iconId, icon);
}
};
// Lucide icons are SVG files with a 24x24 viewBox
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
icon.src =
'data:image/svg+xml,' +
encodeURIComponent(getSvgForSymbol(symbol, this.layerColor));
}
});
}
} }

View File

@@ -1,5 +1,4 @@
import { GPXFileStateCollectionObserver } from '$lib/logic/file-state'; import { GPXFileStateCollectionObserver } from '$lib/logic/file-state';
import { writable } from 'svelte/store';
import { GPXLayer } from './gpx-layer'; import { GPXLayer } from './gpx-layer';
export class GPXLayerCollection { export class GPXLayerCollection {
@@ -43,4 +42,3 @@ export class GPXLayerCollection {
} }
export const gpxLayers = new GPXLayerCollection(); export const gpxLayers = new GPXLayerCollection();
export const gpxColors = writable(new Map<string, string>());

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

@@ -13,7 +13,6 @@
overlays, overlays,
overlayTree, overlayTree,
overpassTree, overpassTree,
terrainSources,
} from '$lib/assets/layers'; } from '$lib/assets/layers';
import { getLayers, isSelected, toggle } from '$lib/components/map/layer-control/utils'; import { getLayers, isSelected, toggle } from '$lib/components/map/layer-control/utils';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
@@ -32,7 +31,6 @@
currentOverpassQueries, currentOverpassQueries,
customLayers, customLayers,
opacities, opacities,
terrainSource,
} = settings; } = settings;
const { isLayerFromExtension, getLayerName } = extensionAPI; const { isLayerFromExtension, getLayerName } = extensionAPI;
@@ -56,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);
@@ -67,7 +65,7 @@
}); });
$effect(() => { $effect(() => {
if (open && $selectedOverlayTree) { if ($selectedOverlayTree) {
untrack(() => { untrack(() => {
if ($currentOverlays) { if ($currentOverlays) {
let overlayLayers = getLayers($currentOverlays); let overlayLayers = getLayers($currentOverlays);
@@ -88,7 +86,7 @@
}); });
$effect(() => { $effect(() => {
if (open && $selectedOverpassTree) { if ($selectedOverpassTree) {
untrack(() => { untrack(() => {
if ($currentOverpassQueries) { if ($currentOverpassQueries) {
let overlayLayers = getLayers($currentOverpassQueries); let overlayLayers = getLayers($currentOverpassQueries);
@@ -162,7 +160,7 @@
type="single" type="single"
onValueChange={setOpacityFromSelection} onValueChange={setOpacityFromSelection}
> >
<Select.Trigger class="mr-1 w-full" size="sm"> <Select.Trigger class="h-8 mr-1 w-full">
{#if selectedOverlay} {#if selectedOverlay}
{#if isSelected($selectedOverlayTree, selectedOverlay)} {#if isSelected($selectedOverlayTree, selectedOverlay)}
{#if $isLayerFromExtension(selectedOverlay)} {#if $isLayerFromExtension(selectedOverlay)}
@@ -235,23 +233,6 @@
</ScrollArea> </ScrollArea>
</Accordion.Content> </Accordion.Content>
</Accordion.Item> </Accordion.Item>
<Accordion.Item value="terrain-source">
<Accordion.Trigger>{i18n._('layers.terrain')}</Accordion.Trigger>
<Accordion.Content class="flex flex-col gap-3 overflow-visible">
<Select.Root bind:value={$terrainSource} type="single">
<Select.Trigger class="mr-1 w-full" size="sm">
{i18n._(`layers.label.${$terrainSource}`)}
</Select.Trigger>
<Select.Content class="h-fit max-h-[40dvh] overflow-y-auto">
{#each Object.keys(terrainSources) as id}
<Select.Item value={id}>
{i18n._(`layers.label.${id}`)}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root> </Accordion.Root>
</ScrollArea> </ScrollArea>
</Sheet.Header> </Sheet.Header>

View File

@@ -85,7 +85,7 @@
{:else if anySelectedLayer(node[id])} {:else if anySelectedLayer(node[id])}
<CollapsibleTreeNode {id}> <CollapsibleTreeNode {id}>
{#snippet trigger()} {#snippet trigger()}
<span>{i18n._(`layers.label.${id}`, id)}</span> <span>{i18n._(`layers.label.${id}`)}</span>
{/snippet} {/snippet}
{#snippet content()} {#snippet content()}
<div class="ml-2"> <div class="ml-2">

View File

@@ -54,27 +54,28 @@
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw] gap-0"> <Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw] gap-0">
<Card.Header class="p-0 gap-0"> <Card.Header class="p-0 gap-0">
<Card.Title class="text-md flex flex-row"> <Card.Title class="text-md">
<div class="flex flex-col"> <div class="flex flex-row gap-3">
<p>{name}</p> <div class="flex flex-col">
<div class="text-muted-foreground text-xs font-normal"> {name}
{poi.item.lat.toFixed(6)}&deg; {poi.item.lon.toFixed(6)}&deg; <div class="text-muted-foreground text-xs font-normal">
{poi.item.lat.toFixed(6)}&deg; {poi.item.lon.toFixed(6)}&deg;
</div>
</div> </div>
<Button
class="ml-auto"
variant="outline"
size="icon"
href="https://www.openstreetmap.org/edit?editor=id&{poi.item.type ??
'node'}={poi.item.id}"
target="_blank"
>
<PencilLine size="16" />
</Button>
</div> </div>
<Button
class="ml-auto"
variant="outline"
size="icon-sm"
href="https://www.openstreetmap.org/edit?editor=id&{poi.item.type ?? 'node'}={poi
.item.id}"
target="_blank"
>
<PencilLine size="16" />
</Button>
</Card.Title> </Card.Title>
</Card.Header> </Card.Header>
<Card.Content class="flex flex-col gap-1 p-0 text-sm whitespace-normal break-all"> <Card.Content class="flex flex-col p-0 text-sm mt-1 whitespace-normal break-all">
<ScrollArea class="flex flex-col max-h-[30dvh]"> <ScrollArea class="flex flex-col max-h-[30dvh]">
{#if tags.image || tags['image:0']} {#if tags.image || tags['image:0']}
<div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto"> <div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto">
@@ -99,14 +100,8 @@
{/each} {/each}
</div> </div>
</ScrollArea> </ScrollArea>
<Button <Button class="mt-2" variant="outline" disabled={$selection.size === 0} onclick={addToFile}>
size="sm" <MapPin size="16" />
class="mt-1 justify-start"
variant="outline"
disabled={$selection.size === 0}
onclick={addToFile}
>
<MapPin size="14" />
{i18n._('toolbar.waypoint.add')} {i18n._('toolbar.waypoint.add')}
</Button> </Button>
</Card.Content> </Card.Content>

View File

@@ -8,7 +8,6 @@ import { map } from '$lib/components/map/map';
const { currentOverlays, previousOverlays, selectedOverlayTree } = settings; const { currentOverlays, previousOverlays, selectedOverlayTree } = settings;
export type CustomOverlay = { export type CustomOverlay = {
extensionName: string;
id: string; id: string;
name: string; name: string;
tileUrls: string[]; tileUrls: string[];
@@ -47,16 +46,8 @@ export class ExtensionAPI {
} }
addOrUpdateOverlay(overlay: CustomOverlay) { addOrUpdateOverlay(overlay: CustomOverlay) {
if ( if (!overlay.id || !overlay.name || !overlay.tileUrls || overlay.tileUrls.length === 0) {
!overlay.extensionName || throw new Error('Overlay must have an id, name, and at least one tile URL.');
!overlay.id ||
!overlay.name ||
!overlay.tileUrls ||
overlay.tileUrls.length === 0
) {
throw new Error(
'Overlay must have an extensionName, id, name, and at least one tile URL.'
);
} }
overlay.id = this.getOverlayId(overlay.id); overlay.id = this.getOverlayId(overlay.id);
@@ -84,17 +75,10 @@ export class ExtensionAPI {
], ],
}; };
if (!overlayTree.overlays.hasOwnProperty(overlay.extensionName)) { overlayTree.overlays.world[overlay.id] = true;
overlayTree.overlays[overlay.extensionName] = {};
}
overlayTree.overlays[overlay.extensionName][overlay.id] = true;
selectedOverlayTree.update((selected) => { selectedOverlayTree.update((selected) => {
if (!selected.overlays.hasOwnProperty(overlay.extensionName)) { selected.overlays.world[overlay.id] = true;
selected.overlays[overlay.extensionName] = {};
}
selected.overlays[overlay.extensionName][overlay.id] = true;
return selected; return selected;
}); });
@@ -110,10 +94,7 @@ export class ExtensionAPI {
} }
currentOverlays.update((current) => { currentOverlays.update((current) => {
if (!current.overlays.hasOwnProperty(overlay.extensionName)) { current.overlays.world[overlay.id] = show;
current.overlays[overlay.extensionName] = {};
}
current.overlays[overlay.extensionName][overlay.id] = show;
return current; return current;
}); });
} }
@@ -152,29 +133,6 @@ export class ExtensionAPI {
}); });
} }
updateOverlaysOrder(ids: string[]) {
ids = ids.map((id) => this.getOverlayId(id));
selectedOverlayTree.update((selected) => {
let isSelected: Record<string, boolean> = {};
ids.forEach((id) => {
const overlay = get(this._overlays).get(id);
if (
overlay &&
selected.overlays.hasOwnProperty(overlay.extensionName) &&
selected.overlays[overlay.extensionName].hasOwnProperty(id)
) {
isSelected[id] = selected.overlays[overlay.extensionName][id];
delete selected.overlays[overlay.extensionName][id];
}
});
Object.entries(isSelected).forEach(([id, value]) => {
const overlay = get(this._overlays).get(id)!;
selected.overlays[overlay.extensionName][id] = value;
});
return selected;
});
}
isLayerFromExtension = derived(this._overlays, ($overlays) => { isLayerFromExtension = derived(this._overlays, ($overlays) => {
return (id: string) => $overlays.has(id); return (id: string) => $overlays.has(id);
}); });

View File

@@ -6,7 +6,6 @@ import { overpassQueryData } from '$lib/assets/layers';
import { MapPopup } from '$lib/components/map/map-popup'; import { MapPopup } from '$lib/components/map/map-popup';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { db } from '$lib/db'; import { db } from '$lib/db';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/map';
const { currentOverpassQueries } = settings; const { currentOverpassQueries } = settings;
@@ -86,28 +85,23 @@ export class OverpassLayer {
} }
if (!this.map.getLayer('overpass')) { if (!this.map.getLayer('overpass')) {
this.map.addLayer( this.map.addLayer({
{ id: 'overpass',
id: 'overpass', type: 'symbol',
type: 'symbol', source: 'overpass',
source: 'overpass', layout: {
layout: { 'icon-image': ['get', 'icon'],
'icon-image': ['get', 'icon'], 'icon-size': 0.25,
'icon-size': 0.25, 'icon-padding': 0,
'icon-padding': 0, 'icon-allow-overlap': ['step', ['zoom'], false, 14, true],
'icon-allow-overlap': ['step', ['zoom'], false, 14, true],
},
}, },
ANCHOR_LAYER_KEY.overpass });
);
this.map.on('mouseenter', 'overpass', this.onHoverBinded); this.map.on('mouseenter', 'overpass', this.onHoverBinded);
this.map.on('click', 'overpass', this.onHoverBinded); this.map.on('click', 'overpass', this.onHoverBinded);
} }
this.map.setFilter('overpass', ['in', 'query', ...getCurrentQueries()], { this.map.setFilter('overpass', ['in', 'query', ...getCurrentQueries()]);
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
} }
@@ -289,12 +283,10 @@ function getQuery(query: string) {
} }
} }
function getQueryItem(tags: Record<string, string | string[]>) { function getQueryItem(tags: Record<string, string | boolean | string[]>) {
let arrayEntry = Object.entries(tags).find((entry): entry is [string, string[]] => let arrayEntry = Object.values(tags).find((value) => Array.isArray(value));
Array.isArray(entry[1])
);
if (arrayEntry !== undefined) { if (arrayEntry !== undefined) {
return arrayEntry[1] return arrayEntry
.map( .map(
(val) => (val) =>
`nwr${Object.entries(tags) `nwr${Object.entries(tags)
@@ -317,7 +309,7 @@ function belongsToQuery(element: any, query: string) {
} }
} }
function belongsToQueryItem(element: any, tags: Record<string, string | string[]>) { function belongsToQueryItem(element: any, tags: Record<string, string | boolean | string[]>) {
return Object.entries(tags).every(([tag, value]) => return Object.entries(tags).every(([tag, value]) =>
Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value
); );

View File

@@ -3,16 +3,8 @@ import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
import { get, writable, type Writable } from 'svelte/store'; import { get, writable, type Writable } from 'svelte/store';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { tick } from 'svelte'; import { tick } from 'svelte';
import { terrainSources } from '$lib/assets/layers';
const { const { treeFileView, elevationProfile, bottomPanelSize, rightPanelSize, distanceUnits } = settings;
treeFileView,
elevationProfile,
bottomPanelSize,
rightPanelSize,
distanceUnits,
terrainSource,
} = settings;
let fitBoundsOptions: mapboxgl.MapOptions['fitBoundsOptions'] = { let fitBoundsOptions: mapboxgl.MapOptions['fitBoundsOptions'] = {
maxZoom: 15, maxZoom: 15,
@@ -20,28 +12,6 @@ let fitBoundsOptions: mapboxgl.MapOptions['fitBoundsOptions'] = {
easing: () => 1, easing: () => 1,
}; };
const emptySource: mapboxgl.GeoJSONSourceSpecification = {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [],
},
};
export const ANCHOR_LAYER_KEY = {
mapillary: 'mapillary-end',
tracks: 'tracks-end',
directionMarkers: 'direction-markers-end',
distanceMarkers: 'distance-markers-end',
interactions: 'interactions-end',
overpass: 'overpass-end',
waypoints: 'waypoints-end',
};
const anchorLayers: mapboxgl.LayerSpecification[] = Object.values(ANCHOR_LAYER_KEY).map((id) => ({
id: id,
type: 'symbol',
source: 'empty-source',
}));
export class MapboxGLMap { export class MapboxGLMap {
private _map: Writable<mapboxgl.Map | null> = writable(null); private _map: Writable<mapboxgl.Map | null> = writable(null);
private _onLoadCallbacks: ((map: mapboxgl.Map) => void)[] = []; private _onLoadCallbacks: ((map: mapboxgl.Map) => void)[] = [];
@@ -51,16 +21,31 @@ export class MapboxGLMap {
return this._map.subscribe(run, invalidate); return this._map.subscribe(run, invalidate);
} }
init(language: string, hash: boolean, geocoder: boolean, geolocate: boolean) { init(
accessToken: string,
language: string,
hash: boolean,
geocoder: boolean,
geolocate: boolean
) {
const map = new mapboxgl.Map({ const map = new mapboxgl.Map({
container: 'map', container: 'map',
style: { style: {
version: 8, version: 8,
sources: { sources: {},
'empty-source': emptySource, layers: [],
},
layers: anchorLayers,
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: `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${accessToken}`,
},
},
{ {
id: 'basemap', id: 'basemap',
url: '', url: '',
@@ -68,6 +53,11 @@ export class MapboxGLMap {
{ {
id: 'overlays', id: 'overlays',
url: '', url: '',
data: {
version: 8,
sources: {},
layers: [],
},
}, },
], ],
}, },
@@ -144,26 +134,39 @@ export class MapboxGLMap {
}); });
map.addControl(scaleControl); map.addControl(scaleControl);
map.on('style.load', () => { map.on('style.load', () => {
map.addSource('mapbox-dem', {
type: 'raster-dem',
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
tileSize: 512,
maxzoom: 14,
});
if (map.getPitch() > 0) {
map.setTerrain({
source: 'mapbox-dem',
exaggeration: 1,
});
}
map.setFog({ map.setFog({
color: 'rgb(186, 210, 235)', color: 'rgb(186, 210, 235)',
'high-color': 'rgb(36, 92, 223)', 'high-color': 'rgb(36, 92, 223)',
'horizon-blend': 0.1, 'horizon-blend': 0.1,
'space-color': 'rgb(156, 240, 255)', 'space-color': 'rgb(156, 240, 255)',
}); });
map.on('pitch', this.setTerrain.bind(this)); map.on('pitch', () => {
this.setTerrain(); if (map.getPitch() > 0) {
}); map.setTerrain({
map.on('style.import.load', () => { source: 'mapbox-dem',
const basemap = map.getStyle().imports?.find((imprt) => imprt.id === 'basemap'); exaggeration: 1,
if (basemap && basemap.data && basemap.data.glyphs) { });
map.setGlyphsUrl(basemap.data.glyphs); } else {
} map.setTerrain(null);
}
});
}); });
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
this.resize(); this.resize();
this.setTerrain();
scaleControl.setUnit(get(distanceUnits)); scaleControl.setUnit(get(distanceUnits));
this._onLoadCallbacks.forEach((callback) => callback(map)); this._onLoadCallbacks.forEach((callback) => callback(map));
@@ -179,7 +182,6 @@ export class MapboxGLMap {
scaleControl.setUnit(units); scaleControl.setUnit(units);
}) })
); );
this._unsubscribes.push(terrainSource.subscribe(() => this.setTerrain()));
} }
onLoad(callback: (map: mapboxgl.Map) => void) { onLoad(callback: (map: mapboxgl.Map) => void) {
@@ -220,29 +222,6 @@ export class MapboxGLMap {
} }
} }
} }
setTerrain() {
const map = get(this._map);
if (map) {
const source = get(terrainSource);
try {
if (!map.getSource(source)) {
map.addSource(source, terrainSources[source]);
}
if (map.getPitch() > 0) {
map.setTerrain({
source: source,
exaggeration: 1,
});
} else {
map.setTerrain(null);
}
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
return;
}
}
}
} }
export const map = new MapboxGLMap(); export const map = new MapboxGLMap();

View File

@@ -2,7 +2,6 @@ import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } fro
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module'; import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
import 'mapillary-js/dist/mapillary.css'; import 'mapillary-js/dist/mapillary.css';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/map';
const mapillarySource: VectorSourceSpecification = { const mapillarySource: VectorSourceSpecification = {
type: 'vector', type: 'vector',
@@ -100,10 +99,10 @@ export class MapillaryLayer {
this.map.addSource('mapillary', mapillarySource); this.map.addSource('mapillary', mapillarySource);
} }
if (!this.map.getLayer('mapillary-sequence')) { if (!this.map.getLayer('mapillary-sequence')) {
this.map.addLayer(mapillarySequenceLayer, ANCHOR_LAYER_KEY.mapillary); this.map.addLayer(mapillarySequenceLayer);
} }
if (!this.map.getLayer('mapillary-image')) { if (!this.map.getLayer('mapillary-image')) {
this.map.addLayer(mapillaryImageLayer, ANCHOR_LAYER_KEY.mapillary); this.map.addLayer(mapillaryImageLayer);
} }
this.map.on('style.load', this.addBinded); this.map.on('style.load', this.addBinded);
this.map.on('mouseenter', 'mapillary-image', this.onMouseEnterBinded); this.map.on('mouseenter', 'mapillary-image', this.onMouseEnterBinded);

View File

@@ -15,7 +15,7 @@
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { Trash2 } from '@lucide/svelte'; import { Trash2 } from '@lucide/svelte';
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import type { GeoJSONSource } from 'mapbox-gl'; import type { GeoJSONSource } from 'mapbox-gl';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
@@ -63,18 +63,15 @@
}); });
} }
if (!$map.getLayer('rectangle')) { if (!$map.getLayer('rectangle')) {
$map.addLayer( $map.addLayer({
{ id: 'rectangle',
id: 'rectangle', type: 'fill',
type: 'fill', source: 'rectangle',
source: 'rectangle', paint: {
paint: { 'fill-color': 'SteelBlue',
'fill-color': 'SteelBlue', 'fill-opacity': 0.5,
'fill-opacity': 0.5,
},
}, },
ANCHOR_LAYER_KEY.interactions });
);
} }
} }
} }

View File

@@ -2,6 +2,7 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { MountainSnow } from '@lucide/svelte'; import { MountainSnow } from '@lucide/svelte';
import { map } from '$lib/components/map/map';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
@@ -19,7 +20,11 @@
variant="outline" variant="outline"
class="whitespace-normal h-fit" class="whitespace-normal h-fit"
disabled={!validSelection} disabled={!validSelection}
onclick={() => fileActions.addElevationToSelection()} onclick={() => {
if ($map) {
fileActions.addElevationToSelection($map);
}
}}
> >
<MountainSnow size="16" class="shrink-0" /> <MountainSnow size="16" class="shrink-0" />
{i18n._('toolbar.elevation.button')} {i18n._('toolbar.elevation.button')}

View File

@@ -38,7 +38,7 @@
let endTime: string | undefined = $state(undefined); let endTime: string | undefined = $state(undefined);
let movingTime: number | undefined = $state(undefined); let movingTime: number | undefined = $state(undefined);
let speed: number | undefined = $state(undefined); let speed: number | undefined = $state(undefined);
let artificial = $state(true); let artificial = $state(false);
function toCalendarDate(date: Date): CalendarDate { function toCalendarDate(date: Date): CalendarDate {
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate()); return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
@@ -346,7 +346,7 @@
let fileId = item.getFileId(); let fileId = item.getFileId();
fileActionManager.applyToFile(fileId, (file) => { fileActionManager.applyToFile(fileId, (file) => {
if (item instanceof ListFileItem) { if (item instanceof ListFileItem) {
if (artificial && !$gpxStatistics.global.time.moving) { if (artificial || !$gpxStatistics.global.time.moving) {
file.createArtificialTimestamps( file.createArtificialTimestamps(
getDate(startDate!, startTime!), getDate(startDate!, startTime!),
movingTime! movingTime!
@@ -359,7 +359,7 @@
); );
} }
} else if (item instanceof ListTrackItem) { } else if (item instanceof ListTrackItem) {
if (artificial && !$gpxStatistics.global.time.moving) { if (artificial || !$gpxStatistics.global.time.moving) {
file.createArtificialTimestamps( file.createArtificialTimestamps(
getDate(startDate!, startTime!), getDate(startDate!, startTime!),
movingTime!, movingTime!,
@@ -374,7 +374,7 @@
); );
} }
} else if (item instanceof ListTrackSegmentItem) { } else if (item instanceof ListTrackSegmentItem) {
if (artificial && !$gpxStatistics.global.time.moving) { if (artificial || !$gpxStatistics.global.time.moving) {
file.createArtificialTimestamps( file.createArtificialTimestamps(
getDate(startDate!, startTime!), getDate(startDate!, startTime!),
movingTime!, movingTime!,

View File

@@ -10,7 +10,7 @@
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { minTolerance, ReducedGPXLayerCollection, tolerance } from './utils.svelte'; import { minTolerance, ReducedGPXLayerCollection, tolerance } from './reduce.svelte';
let props: { class?: string } = $props(); let props: { class?: string } = $props();

View File

@@ -1,11 +1,11 @@
import { ListItem, ListTrackSegmentItem } from '$lib/components/file-list/file-list'; import { ListItem, ListTrackSegmentItem } from '$lib/components/file-list/file-list';
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/file-state'; import { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/file-state';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx'; import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
import type { GeoJSONSource } from 'mapbox-gl'; import type { GeoJSONSource } from 'mapbox-gl';
import { get, writable } from 'svelte/store'; import { get, writable, type Writable } from 'svelte/store';
export const minTolerance = 0.1; export const minTolerance = 0.1;
@@ -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),
]); ]);
}); });
} }
@@ -144,18 +146,17 @@ export class ReducedGPXLayerCollection {
}); });
} }
if (!map_.getLayer('simplified')) { if (!map_.getLayer('simplified')) {
map_.addLayer( map_.addLayer({
{ id: 'simplified',
id: 'simplified', type: 'line',
type: 'line', source: 'simplified',
source: 'simplified', paint: {
paint: { 'line-color': 'white',
'line-color': 'white', 'line-width': 3,
'line-width': 3,
},
}, },
ANCHOR_LAYER_KEY.interactions });
); } else {
map_.moveLayer('simplified');
} }
} }

View File

@@ -163,7 +163,7 @@
{i18n._('toolbar.routing.activity')} {i18n._('toolbar.routing.activity')}
</span> </span>
<Select.Root type="single" bind:value={$routingProfile}> <Select.Root type="single" bind:value={$routingProfile}>
<Select.Trigger class="grow" size="sm"> <Select.Trigger class="h-8 grow">
{i18n._(`toolbar.routing.activities.${$routingProfile}`)} {i18n._(`toolbar.routing.activities.${$routingProfile}`)}
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
@@ -195,7 +195,7 @@
disabled={!validSelection} disabled={!validSelection}
onclick={fileActions.reverseSelection} onclick={fileActions.reverseSelection}
> >
<ArrowRightLeft class="size-3" />{i18n._('toolbar.routing.reverse.button')} <ArrowRightLeft size="12" />{i18n._('toolbar.routing.reverse.button')}
</ButtonWithTooltip> </ButtonWithTooltip>
<ButtonWithTooltip <ButtonWithTooltip
label={i18n._('toolbar.routing.route_back_to_start.tooltip')} label={i18n._('toolbar.routing.route_back_to_start.tooltip')}
@@ -231,7 +231,7 @@
} }
}} }}
> >
<House class="size-3" />{i18n._('toolbar.routing.route_back_to_start.button')} <House size="12" />{i18n._('toolbar.routing.route_back_to_start.button')}
</ButtonWithTooltip> </ButtonWithTooltip>
<ButtonWithTooltip <ButtonWithTooltip
label={i18n._('toolbar.routing.round_trip.tooltip')} label={i18n._('toolbar.routing.round_trip.tooltip')}
@@ -240,7 +240,7 @@
disabled={!validSelection} disabled={!validSelection}
onclick={fileActions.createRoundTripForSelection} onclick={fileActions.createRoundTripForSelection}
> >
<Repeat class="size-3" />{i18n._('toolbar.routing.round_trip.button')} <Repeat size="12" />{i18n._('toolbar.routing.round_trip.button')}
</ButtonWithTooltip> </ButtonWithTooltip>
</div> </div>
<div class="w-full flex flex-row gap-2 items-end justify-between"> <div class="w-full flex flex-row gap-2 items-end justify-between">

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],
]; ];
@@ -105,7 +107,7 @@
{i18n._('toolbar.scissors.split_as')} {i18n._('toolbar.scissors.split_as')}
</span> </span>
<Select.Root bind:value={$splitAs} type="single"> <Select.Root bind:value={$splitAs} type="single">
<Select.Trigger class="w-fit grow" size="sm"> <Select.Trigger class="h-8 w-fit grow">
{i18n._('gpx.' + $splitAs)} {i18n._('gpx.' + $splitAs)}
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>

View File

@@ -1,3 +1,5 @@
import { TrackPoint, TrackSegment } from 'gpx';
import mapboxgl from 'mapbox-gl';
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list'; import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
import { currentTool, Tool } from '$lib/components/toolbar/tools'; import { currentTool, Tool } from '$lib/components/toolbar/tools';
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors'; import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
@@ -7,42 +9,20 @@ import { gpxStatistics } from '$lib/logic/statistics';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { fileStateCollection } from '$lib/logic/file-state'; import { fileStateCollection } from '$lib/logic/file-state';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/map';
export class SplitControls { export class SplitControls {
active: boolean = false;
map: mapboxgl.Map; map: mapboxgl.Map;
controls: ControlWithMarker[] = [];
shownControls: ControlWithMarker[] = [];
unsubscribes: Function[] = []; unsubscribes: Function[] = [];
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this); toggleControlsForZoomLevelAndBoundsBinded: () => void =
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this); this.toggleControlsForZoomLevelAndBounds.bind(this);
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
constructor(map: mapboxgl.Map) { constructor(map: mapboxgl.Map) {
this.map = map; this.map = map;
if (!this.map.hasImage('split-control')) {
let icon = new Image(100, 100);
icon.onload = () => {
if (!this.map.hasImage('split-control')) {
this.map.addImage('split-control', icon);
}
};
// Lucide icons are SVG files with a 24x24 viewBox
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
icon.src =
'data:image/svg+xml,' +
encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
<circle cx="20" cy="20" r="20" fill="white" />
<g transform="translate(8 8)">
${Scissors.replace('stroke="currentColor"', 'stroke="black"')}
</g>
</svg>
`);
}
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this))); this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this))); this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this))); this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this)));
@@ -51,18 +31,29 @@ export class SplitControls {
addIfNeeded() { addIfNeeded() {
let scissors = get(currentTool) === Tool.SCISSORS; let scissors = get(currentTool) === Tool.SCISSORS;
if (!scissors) { if (!scissors) {
this.remove(); if (this.active) {
this.remove();
}
return; return;
} }
this.updateControls(); if (this.active) {
this.updateControls();
} else {
this.add();
}
}
add() {
this.active = true;
this.map.on('zoom', this.toggleControlsForZoomLevelAndBoundsBinded);
this.map.on('move', this.toggleControlsForZoomLevelAndBoundsBinded);
} }
updateControls() { updateControls() {
let data: GeoJSON.FeatureCollection = { // Update the markers when the files change
type: 'FeatureCollection', let controlIndex = 0;
features: [],
};
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
let file = fileStateCollection.getFile(fileId); let file = fileStateCollection.getFile(fileId);
@@ -73,23 +64,30 @@ export class SplitControls {
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex) new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
) )
) { ) {
for (let i = 1; i < segment.trkpt.length - 1; i++) { for (let point of segment.trkpt.slice(1, -1)) {
let point = segment.trkpt[i]; // Update the existing controls (could be improved by matching the existing controls with the new ones?)
if (point._data.anchor) { if (point._data.anchor) {
data.features.push({ if (controlIndex < this.controls.length) {
type: 'Feature', this.controls[controlIndex].fileId = fileId;
geometry: { this.controls[controlIndex].point = point;
type: 'Point', this.controls[controlIndex].segment = segment;
coordinates: [point.getLongitude(), point.getLatitude()], this.controls[controlIndex].trackIndex = trackIndex;
}, this.controls[controlIndex].segmentIndex = segmentIndex;
properties: { this.controls[controlIndex].marker.setLngLat(
fileId: fileId, point.getCoordinates()
trackIndex: trackIndex, );
segmentIndex: segmentIndex, } else {
pointIndex: i, this.controls.push(
minZoom: point._data.zoom, this.createControl(
}, point,
}); segment,
fileId,
trackIndex,
segmentIndex
)
);
}
controlIndex++;
} }
} }
} }
@@ -97,78 +95,86 @@ export class SplitControls {
} }
}, false); }, false);
try { while (controlIndex < this.controls.length) {
let source = this.map.getSource('split-controls') as mapboxgl.GeoJSONSource | undefined; // Remove the extra controls
if (source) { this.controls.pop()?.marker.remove();
source.setData(data);
} else {
this.map.addSource('split-controls', {
type: 'geojson',
data: data,
});
}
if (!this.map.getLayer('split-controls')) {
this.map.addLayer(
{
id: 'split-controls',
type: 'symbol',
source: 'split-controls',
layout: {
'icon-image': 'split-control',
'icon-size': 0.25,
'icon-padding': 0,
},
filter: ['<=', ['get', 'minZoom'], ['zoom']],
},
ANCHOR_LAYER_KEY.interactions
);
this.map.on('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
this.map.on('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
this.map.on('click', 'split-controls', this.layerOnClickBinded);
}
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
} }
this.toggleControlsForZoomLevelAndBounds();
} }
remove() { remove() {
this.map.off('mouseenter', 'split-controls', this.layerOnMouseEnterBinded); this.active = false;
this.map.off('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
this.map.off('click', 'split-controls', this.layerOnClickBinded);
try { for (let control of this.controls) {
if (this.map.getLayer('split-controls')) { control.marker.remove();
this.map.removeLayer('split-controls');
}
if (this.map.getSource('split-controls')) {
this.map.removeSource('split-controls');
}
} catch (e) {
// No reliable way to check if the map is ready to remove sources and layers
} }
this.map.off('zoom', this.toggleControlsForZoomLevelAndBoundsBinded);
this.map.off('move', this.toggleControlsForZoomLevelAndBoundsBinded);
} }
layerOnMouseEnter(e: any) { toggleControlsForZoomLevelAndBounds() {
mapCursor.notify(MapCursorState.SPLIT_CONTROL, true); // Show markers only if they are in the current zoom level and bounds
this.shownControls.splice(0, this.shownControls.length);
let southWest = this.map.unproject([0, this.map.getCanvas().height]);
let northEast = this.map.unproject([this.map.getCanvas().width, 0]);
let bounds = new mapboxgl.LngLatBounds(southWest, northEast);
let zoom = this.map.getZoom();
this.controls.forEach((control) => {
control.inZoom = control.point._data.zoom <= zoom;
if (control.inZoom && bounds.contains(control.marker.getLngLat())) {
control.marker.addTo(this.map);
this.shownControls.push(control);
} else {
control.marker.remove();
}
});
} }
layerOnMouseLeave() { createControl(
mapCursor.notify(MapCursorState.SPLIT_CONTROL, false); point: TrackPoint,
} segment: TrackSegment,
fileId: string,
trackIndex: number,
segmentIndex: number
): ControlWithMarker {
let element = document.createElement('div');
element.className = `h-6 w-6 p-0.5 rounded-full bg-white border-2 border-black cursor-pointer`;
element.innerHTML = Scissors.replace('width="24"', '')
.replace('height="24"', '')
.replace('stroke="currentColor"', 'stroke="black"');
layerOnClick(e: mapboxgl.MapMouseEvent) { let marker = new mapboxgl.Marker({
let coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates; draggable: true,
fileActions.split( className: 'z-10',
get(splitAs), element,
e.features![0].properties!.fileId, }).setLngLat(point.getCoordinates());
e.features![0].properties!.trackIndex,
e.features![0].properties!.segmentIndex, let control = {
{ lon: coordinates[0], lat: coordinates[1] }, point,
e.features![0].properties!.pointIndex segment,
); fileId,
trackIndex,
segmentIndex,
marker,
inZoom: false,
};
marker.getElement().addEventListener('click', (e) => {
e.stopPropagation();
fileActions.split(
get(splitAs),
control.fileId,
control.trackIndex,
control.segmentIndex,
control.point.getCoordinates(),
control.point._data.index
);
});
return control;
} }
destroy() { destroy() {
@@ -176,3 +182,16 @@ export class SplitControls {
this.unsubscribes.forEach((unsubscribe) => unsubscribe()); this.unsubscribes.forEach((unsubscribe) => unsubscribe());
} }
} }
type Control = {
segment: TrackSegment;
fileId: string;
trackIndex: number;
segmentIndex: number;
point: TrackPoint;
};
type ControlWithMarker = Control & {
marker: mapboxgl.Marker;
inZoom: boolean;
};

View File

@@ -16,8 +16,6 @@
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import mapboxgl from 'mapbox-gl';
import { getSvgForSymbol } from '$lib/components/map/gpx-layer/gpx-layer';
let props: { let props: {
class?: string; class?: string;
@@ -41,21 +39,6 @@
}) })
); );
let marker: mapboxgl.Marker | null = null;
function reset() {
if ($selectedWaypoint) {
selectedWaypoint.reset();
} else {
name = '';
description = '';
link = '';
sym = '';
longitude = 0;
latitude = 0;
}
}
$effect(() => { $effect(() => {
if ($selectedWaypoint) { if ($selectedWaypoint) {
const wpt = $selectedWaypoint[0]; const wpt = $selectedWaypoint[0];
@@ -71,7 +54,14 @@
latitude = parseFloat(wpt.getLatitude().toFixed(6)); latitude = parseFloat(wpt.getLatitude().toFixed(6));
}); });
} else { } else {
untrack(reset); untrack(() => {
name = '';
description = '';
link = '';
sym = '';
longitude = 0;
latitude = 0;
});
} }
}); });
@@ -95,14 +85,14 @@
desc: description.length > 0 ? description : undefined, desc: description.length > 0 ? description : undefined,
cmt: description.length > 0 ? description : undefined, cmt: description.length > 0 ? description : undefined,
link: link.length > 0 ? { attributes: { href: link } } : undefined, link: link.length > 0 ? { attributes: { href: link } } : undefined,
sym: sym.length > 0 ? sym : undefined, sym: sym,
}, },
selectedWaypoint.wpt && selectedWaypoint.fileId selectedWaypoint.wpt && selectedWaypoint.fileId
? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index) ? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index)
: undefined : undefined
); );
reset(); selectedWaypoint.reset();
} }
function setCoordinates(e: any) { function setCoordinates(e: any) {
@@ -110,37 +100,6 @@
longitude = e.lngLat.lng.toFixed(6); longitude = e.lngLat.lng.toFixed(6);
} }
$effect(() => {
if ($selectedWaypoint) {
if (marker) {
marker.remove();
marker = null;
}
} else if (latitude != 0 || longitude != 0) {
if ($map) {
if (marker) {
marker.setLngLat([longitude, latitude]).getElement().innerHTML =
getSvgForSymbol(symbolKey);
} else {
let element = document.createElement('div');
element.classList.add('w-8', 'h-8');
element.innerHTML = getSvgForSymbol(symbolKey);
marker = new mapboxgl.Marker({
element,
anchor: 'bottom',
})
.setLngLat([longitude, latitude])
.addTo($map);
}
}
} else {
if (marker) {
marker.remove();
marker = null;
}
}
});
onMount(() => { onMount(() => {
if ($map) { if ($map) {
$map.on('click', setCoordinates); $map.on('click', setCoordinates);
@@ -153,10 +112,6 @@
$map.off('click', setCoordinates); $map.off('click', setCoordinates);
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false); mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
} }
if (marker) {
marker.remove();
marker = null;
}
}); });
</script> </script>
@@ -174,27 +129,19 @@
bind:value={description} bind:value={description}
id="description" id="description"
disabled={!canCreate && !$selectedWaypoint} disabled={!canCreate && !$selectedWaypoint}
class="min-h-8 h-8 py-1 px-3 text-sm"
/> />
<Label for="symbol">{i18n._('toolbar.waypoint.icon')}</Label> <Label for="symbol">{i18n._('toolbar.waypoint.icon')}</Label>
<Select.Root bind:value={sym} type="single"> <Select.Root bind:value={sym} type="single">
<Select.Trigger <Select.Trigger
id="symbol" id="symbol"
size="sm" class="w-full h-8"
class="w-full"
disabled={!canCreate && !$selectedWaypoint} disabled={!canCreate && !$selectedWaypoint}
> >
<span class="flex flex-row gap-1.5 items-center"> {#if symbolKey}
{#if symbolKey} {i18n._(`gpx.symbol.${symbolKey}`)}
{#if symbols[symbolKey].icon} {:else}
{@const Component = symbols[symbolKey].icon} {sym}
<Component size="14" /> {/if}
{/if}
{i18n._(`gpx.symbol.${symbolKey}`)}
{:else}
{sym}
{/if}
</span>
</Select.Trigger> </Select.Trigger>
<Select.Content class="max-h-60 overflow-y-scroll"> <Select.Content class="max-h-60 overflow-y-scroll">
{#each sortedSymbols as [key, symbol]} {#each sortedSymbols as [key, symbol]}
@@ -202,7 +149,7 @@
<span> <span>
{#if symbol.icon} {#if symbol.icon}
{@const Component = symbol.icon} {@const Component = symbol.icon}
<Component size="14" class="inline-block align-sub" /> <Component size="14" class="inline-block align-sub mr-0.5" />
{:else} {:else}
<span class="w-4 inline-block"></span> <span class="w-4 inline-block"></span>
{/if} {/if}
@@ -263,7 +210,7 @@
{i18n._('toolbar.waypoint.create')} {i18n._('toolbar.waypoint.create')}
{/if} {/if}
</Button> </Button>
<Button variant="outline" size="icon" onclick={reset}> <Button variant="outline" size="icon" onclick={() => selectedWaypoint.reset()}>
<CircleX size="16" /> <CircleX size="16" />
</Button> </Button>
</div> </div>

View File

@@ -29,13 +29,13 @@ Pots arrossegar y deixar arxius directament des del seu sistema d'arxius cap a l
Crear una còpia dels arxius seleccionats. Crear una còpia dels arxius seleccionats.
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Esborra ### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
Esborra l'arxiu seleccinat. Delete the currently selected files.
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Esborra-ho tot ### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
Esborra tots els fitxers. Delete all files.
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportar... ### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportar...

View File

@@ -1,5 +1,5 @@
Mapbox stellt einige der auf dieser Website verwendeten Karten bereit. Mapbox ist das Unternehmen, das einige der schönen Karten auf dieser Website zur Verfügung stellt.
Sie entwickeln auch die <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">Karten-Engine</a>, die **gpx.studio** unterstützt. Sie entwickeln auch die <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">Karten-Engine</a> welche **gpx.studio** unterstützt.
Wir sind froh und dankbar, Teil ihres <a href="https://mapbox.com/community" target="_blank">Community</a> Programms zu sein, das gemeinnützige Organisationen, Bildungseinrichtungen und Organisationen unterstützt. Wir sind äusserst glücklich und dankbar, Teil ihres <a href="https://mapbox.com/community" target="_blank">Community</a> Programms zu sein, das gemeinnützige Organisationen, Bildungseinrichtungen und Organisationen mit positivem Einfluss unterstützt.
Diese Partnerschaft ermöglicht es **gpx.studio**, von den Mapbox-Tools zu ermäßigten Preisen zu profitieren, was erheblich zur finanziellen Tragfähigkeit des Projekts beiträgt und es uns ermöglicht, die bestmögliche Benutzererfahrung zu bieten. Diese Partnerschaft ermöglicht es **gpx.studio**, von den Mapbox-Tools zu ermäßigten Preisen zu profitieren, was erheblich zur finanziellen Tragfähigkeit des Projekts beiträgt und es uns ermöglicht, die bestmögliche Benutzererfahrung zu bieten.

View File

@@ -1,5 +1,5 @@
--- ---
title: Opciones de vista title: View options
--- ---
<script lang="ts"> <script lang="ts">

View File

@@ -29,13 +29,13 @@ Beste era batez, fitxategiak zuzenean arrastatu eta jaregin ditzakezu zure fitxa
Sortu hautatutako fitxategien kopia bat. Sortu hautatutako fitxategien kopia bat.
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Ezabatu ### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
Ezabatu hautatutako fitxategiak. Delete the currently selected files.
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Ezabatu guztiak ### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
Ezabatu fitxategi guztiak. Delete all files.
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Esportatu... ### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Esportatu...

View File

@@ -35,7 +35,7 @@ Supprimer les fichiers sélectionnés.
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Supprimer tout ### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Supprimer tout
Supprimer tous les fichiers. Supprimer toutes les fichiers.
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exporter... ### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exporter...

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

@@ -29,13 +29,13 @@ cÈ inoltre possibile trascinare i file direttamente dal file system del tuo Pc
Crea una copia dei file attualmente selezionati. Crea una copia dei file attualmente selezionati.
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" />Elimina ### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
Elimina i file attualmente selezionati. Delete the currently selected files.
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" />Cancella tutto ### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
Elimina tutti i file. Delete all files.
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Esporta... ### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Esporta...

View File

@@ -14,7 +14,7 @@ Deze handleiding zal je door alle componenten en gereedschappen van de interface
<DocsImage src="getting-started/interface" alt="De gpx.studio interface." /> <DocsImage src="getting-started/interface" alt="De gpx.studio interface." />
Zoals weergegeven in bovenstaande scherm, is de interface verdeeld in vier hoofddelen rond de kaart. Zoals weergegeven in bovenstaande scherm, is de interface verdeeld in vier hoofddelen rond de kaart.
Voordat we in de details van elke sectie duiken, eerst een snel overzicht van de interface. Voordat we in de details van elke sectie duiken, hebben we een snel overzicht van de interface.
## Menu ## Menu

View File

@@ -83,7 +83,7 @@ Deze actie is alleen beschikbaar wanneer de verticale indeling van de bestandsli
### <ClipboardPaste size="16" class="inline-block" style="margin-bottom: 2px" /> Plakken ### <ClipboardPaste size="16" class="inline-block" style="margin-bottom: 2px" /> Plakken
Plak de bestandsitems van het klembord naar het huidige hiërarchieniveau indien compatibel. Plak de bestandsitems van het klembord naar het huidige hiërarchie niveau indien compatibel.
<DocsNote> <DocsNote>

View File

@@ -50,7 +50,7 @@ Clicando com o botão direito em uma aba arquivo, você pode acessar as mesmas a
As mentioned in the [view options section](./menu/view), you can switch to a tree layout for the files list. As mentioned in the [view options section](./menu/view), you can switch to a tree layout for the files list.
This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map. This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map.
Além disso, a exibição de árvore de arquivos permite que você inspecione as [faixas, segmentos, e pontos de interesse](./gpx) contidos dentro dos arquivos através de seções recolhidas. In addition, the file tree view enables you to inspect the [tracks, segments, and points of interest](./gpx) contained inside the files through collapsible sections.
Você também pode aplicar as [ações de edição](./menu/edit) e [ferramentas](./toolbar) para itens de arquivos internos. Você também pode aplicar as [ações de edição](./menu/edit) e [ferramentas](./toolbar) para itens de arquivos internos.
Além disso, você pode arrastar e soltar os itens internos para reordená-los, ou movê-los na hierarquia ou até mesmo para outro arquivo. Além disso, você pode arrastar e soltar os itens internos para reordená-los, ou movê-los na hierarquia ou até mesmo para outro arquivo.
@@ -105,6 +105,6 @@ Using the <kbd><ChartNoAxesColumn size="16" class="inline-block" style="margin-b
- **slope** information computed from the elevation data, or - **slope** information computed from the elevation data, or
- **surface** or **category** data coming from <a href="https://www.openstreetmap.org/" target="_blank">OpenStreetMap</a>'s <a href="https://wiki.openstreetmap.org/wiki/Key:surface" target="_blank">surface</a> and <a href="https://wiki.openstreetmap.org/wiki/Key:highway" target="_blank">highway</a> tags. - **surface** or **category** data coming from <a href="https://www.openstreetmap.org/" target="_blank">OpenStreetMap</a>'s <a href="https://wiki.openstreetmap.org/wiki/Key:surface" target="_blank">surface</a> and <a href="https://wiki.openstreetmap.org/wiki/Key:highway" target="_blank">highway</a> tags.
Isso só está disponível para arquivos criados com **gpx.studio**. This is only available for files created with **gpx.studio**.
If your selection includes it, you can also visualize: **speed**, **heart rate**, **cadence**, **temperature** and **power** data on the elevation profile. If your selection includes it, you can also visualize: **speed**, **heart rate**, **cadence**, **temperature** and **power** data on the elevation profile.

View File

@@ -35,7 +35,7 @@ Delete the currently selected files.
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all ### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
Apagar todos os arquivos. Delete all files.
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportar... ### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportar...

View File

@@ -2,8 +2,7 @@
import { HeartHandshake } from '@lucide/svelte'; import { HeartHandshake } from '@lucide/svelte';
</script> </script>
## <HeartHandshake size="18" class="inline-block align-baseline" /> ## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
Допоможіть нам залишати цей сайт безкоштовним (та без реклами)
Кожного разу, коли ви додаєте або переміщуєте GPS точки, наші сервери обчислюють найкращий маршрут на мережі доріг. Кожного разу, коли ви додаєте або переміщуєте GPS точки, наші сервери обчислюють найкращий маршрут на мережі доріг.
Ми також використовуємо API від <a href="https://mapbox.com" target="_blank">Mapbox</a> для зображення красивих карт, отримання даних висот та можливості пошуку місць. Ми також використовуємо API від <a href="https://mapbox.com" target="_blank">Mapbox</a> для зображення красивих карт, отримання даних висот та можливості пошуку місць.

View File

@@ -2,7 +2,7 @@
import { Languages } from '@lucide/svelte'; import { Languages } from '@lucide/svelte';
</script> </script>
## <Languages size="18" class="inline-block align-baseline" /> Переклад ## <Languages size="18" class="inline-block align-baseline" /> Translation
Сайт перекладається волонтерами з використанням платформи для спільного перекладу. Сайт перекладається волонтерами з використанням платформи для спільного перекладу.
Ви можете зробити свій внесок, додаючи або покращуючи переклади в нашому <a href="https://crowdin.com/project/gpxstudio" target="_blank">проєкті Crowdin</a>. Ви можете зробити свій внесок, додаючи або покращуючи переклади в нашому <a href="https://crowdin.com/project/gpxstudio" target="_blank">проєкті Crowdin</a>.

View File

@@ -47,6 +47,6 @@ Open the export dialog to save all files to your computer.
<DocsNote type="warning"> <DocsNote type="warning">
Якщо завантаження не починається після натискання кнопки завантаження, будь ласка, перевірте налаштування браузера, щоб дозволити завантаження з <b>gpx.studio</b>. If your download does not start after clicking the download button, please check your browser settings to allow downloads from <b>gpx.studio</b>.
</DocsNote> </DocsNote>

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

@@ -31,7 +31,7 @@ Create a copy of the currently selected files.
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete ### <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" /> Delete all ### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all

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

@@ -14,7 +14,7 @@ class Locale {
private _isLoadingInitial = $state(true); private _isLoadingInitial = $state(true);
private _isLoading = $state(true); private _isLoading = $state(true);
private dictionary: Dictionary = $state({}); private dictionary: Dictionary = $state({});
private _t = $derived((key: string, fallback?: string) => { private _t = $derived((key: string) => {
const keys = key.split('.'); const keys = key.split('.');
let value: string | Dictionary = this.dictionary; let value: string | Dictionary = this.dictionary;
@@ -22,7 +22,7 @@ class Locale {
if (value && typeof value === 'object' && k in value) { if (value && typeof value === 'object' && k in value) {
value = value[k]; value = value[k];
} else { } else {
return fallback || key; return key;
} }
} }

View File

@@ -66,8 +66,10 @@ export class BoundsManager {
finalizeFitBounds() { finalizeFitBounds() {
if ( if (
this._bounds.getSouth() >= this._bounds.getNorth() && this._bounds.getSouth() === 90 &&
this._bounds.getWest() >= this._bounds.getEast() this._bounds.getWest() === 180 &&
this._bounds.getNorth() === -90 &&
this._bounds.getEast() === -180
) { ) {
return; return;
} }

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})`;
@@ -807,7 +844,7 @@ export const fileActions = {
}); });
}); });
}, },
addElevationToSelection: async () => { addElevationToSelection: async (map: mapboxgl.Map) => {
if (get(selection).size === 0) { if (get(selection).size === 0) {
return; return;
} }

View File

@@ -4,12 +4,10 @@ import { get, writable, type Writable } from 'svelte/store';
export enum MapCursorState { export enum MapCursorState {
DEFAULT, DEFAULT,
LAYER_HOVER, LAYER_HOVER,
TOOL_WITH_CROSSHAIR,
WAYPOINT_HOVER,
WAYPOINT_DRAGGING, WAYPOINT_DRAGGING,
TRACKPOINT_DRAGGING, TRACKPOINT_DRAGGING,
TOOL_WITH_CROSSHAIR,
SCISSORS, SCISSORS,
SPLIT_CONTROL,
MAPILLARY_HOVER, MAPILLARY_HOVER,
STREET_VIEW_CROSSHAIR, STREET_VIEW_CROSSHAIR,
} }
@@ -18,12 +16,10 @@ const scissorsCursor = `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/20
const cursorStyles = { const cursorStyles = {
[MapCursorState.DEFAULT]: 'default', [MapCursorState.DEFAULT]: 'default',
[MapCursorState.LAYER_HOVER]: 'pointer', [MapCursorState.LAYER_HOVER]: 'pointer',
[MapCursorState.WAYPOINT_HOVER]: 'pointer',
[MapCursorState.WAYPOINT_DRAGGING]: 'grabbing', [MapCursorState.WAYPOINT_DRAGGING]: 'grabbing',
[MapCursorState.TRACKPOINT_DRAGGING]: 'grabbing', [MapCursorState.TRACKPOINT_DRAGGING]: 'grabbing',
[MapCursorState.TOOL_WITH_CROSSHAIR]: 'crosshair', [MapCursorState.TOOL_WITH_CROSSHAIR]: 'crosshair',
[MapCursorState.SCISSORS]: scissorsCursor, [MapCursorState.SCISSORS]: scissorsCursor,
[MapCursorState.SPLIT_CONTROL]: 'pointer',
[MapCursorState.MAPILLARY_HOVER]: 'pointer', [MapCursorState.MAPILLARY_HOVER]: 'pointer',
[MapCursorState.STREET_VIEW_CROSSHAIR]: 'crosshair', [MapCursorState.STREET_VIEW_CROSSHAIR]: 'crosshair',
}; };
@@ -34,8 +30,8 @@ export class MapCursor {
constructor() { constructor() {
this._states = writable(new Set()); this._states = writable(new Set());
this._states.subscribe((states) => { this._states.subscribe((states) => {
let state = Array.from(states.values()).reduce((max, value) => { let state = states.entries().reduce((max, entry) => {
return value > max ? value : max; return entry[0] > max ? entry[0] : max;
}, MapCursorState.DEFAULT); }, MapCursorState.DEFAULT);
let canvas = get(map)?.getCanvas(); let canvas = get(map)?.getCanvas();
if (canvas) { if (canvas) {

View File

@@ -179,112 +179,6 @@ export class Selection {
} }
} }
updateFromKey(down: boolean, shift: boolean) {
let selected = get(this._selection).getSelected();
if (selected.length === 0) {
return;
}
let next: ListItem | undefined = undefined;
if (selected[0] instanceof ListFileItem) {
let order = get(settings.fileOrder);
let limitIndex: number | undefined = undefined;
selected.forEach((item) => {
let index = order.indexOf(item.getFileId());
if (
limitIndex === undefined ||
(down && index > limitIndex) ||
(!down && index < limitIndex)
) {
limitIndex = index;
}
});
if (limitIndex !== undefined) {
let nextIndex = down ? limitIndex + 1 : limitIndex - 1;
while (true) {
if (nextIndex < 0) {
nextIndex = order.length - 1;
} else if (nextIndex >= order.length) {
nextIndex = 0;
}
if (nextIndex === limitIndex) {
break;
}
next = new ListFileItem(order[nextIndex]);
if (!get(selection).has(next)) {
break;
}
nextIndex += down ? 1 : -1;
}
}
} else if (
selected[0] instanceof ListTrackItem &&
selected[selected.length - 1] instanceof ListTrackItem
) {
let fileId = selected[0].getFileId();
let file = fileStateCollection.getFile(fileId);
if (file) {
let numberOfTracks = file.trk.length;
let trackIndex = down
? selected[selected.length - 1].getTrackIndex()
: selected[0].getTrackIndex();
if (down && trackIndex < numberOfTracks - 1) {
next = new ListTrackItem(fileId, trackIndex + 1);
} else if (!down && trackIndex > 0) {
next = new ListTrackItem(fileId, trackIndex - 1);
}
}
} else if (
selected[0] instanceof ListTrackSegmentItem &&
selected[selected.length - 1] instanceof ListTrackSegmentItem
) {
let fileId = selected[0].getFileId();
let file = fileStateCollection.getFile(fileId);
if (file) {
let trackIndex = selected[0].getTrackIndex();
let numberOfSegments = file.trk[trackIndex].trkseg.length;
let segmentIndex = down
? selected[selected.length - 1].getSegmentIndex()
: selected[0].getSegmentIndex();
if (down && segmentIndex < numberOfSegments - 1) {
next = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex + 1);
} else if (!down && segmentIndex > 0) {
next = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex - 1);
}
}
} else if (
selected[0] instanceof ListWaypointItem &&
selected[selected.length - 1] instanceof ListWaypointItem
) {
let fileId = selected[0].getFileId();
let file = fileStateCollection.getFile(fileId);
if (file) {
let numberOfWaypoints = file.wpt.length;
let waypointIndex = down
? selected[selected.length - 1].getWaypointIndex()
: selected[0].getWaypointIndex();
if (down && waypointIndex < numberOfWaypoints - 1) {
next = new ListWaypointItem(fileId, waypointIndex + 1);
} else if (!down && waypointIndex > 0) {
next = new ListWaypointItem(fileId, waypointIndex - 1);
}
}
}
if (next && (!get(this._selection).has(next) || !shift)) {
if (shift) {
this.addSelectItem(next);
} else {
this.selectItem(next);
}
}
}
getOrderedSelection(reverse: boolean = false): ListItem[] { getOrderedSelection(reverse: boolean = false): ListItem[] {
let selected: ListItem[] = []; let selected: ListItem[] = [];
this.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { this.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {

View File

@@ -8,7 +8,6 @@ import {
defaultOverlayTree, defaultOverlayTree,
defaultOverpassQueries, defaultOverpassQueries,
defaultOverpassTree, defaultOverpassTree,
defaultTerrainSource,
type CustomLayer, type CustomLayer,
} from '$lib/assets/layers'; } from '$lib/assets/layers';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
@@ -155,7 +154,6 @@ export const settings = {
customLayers: new Setting<Record<string, CustomLayer>>('customLayers', {}), customLayers: new Setting<Record<string, CustomLayer>>('customLayers', {}),
customBasemapOrder: new Setting<string[]>('customBasemapOrder', []), customBasemapOrder: new Setting<string[]>('customBasemapOrder', []),
customOverlayOrder: new Setting<string[]>('customOverlayOrder', []), customOverlayOrder: new Setting<string[]>('customOverlayOrder', []),
terrainSource: new Setting('terrainSource', defaultTerrainSource),
directionMarkers: new Setting('directionMarkers', false), directionMarkers: new Setting('directionMarkers', false),
distanceMarkers: new Setting('distanceMarkers', false), distanceMarkers: new Setting('distanceMarkers', false),
streetViewSource: new Setting('streetViewSource', 'mapillary'), streetViewSource: new Setting('streetViewSource', 'mapillary'),

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

@@ -79,8 +79,7 @@
"unhide": "Паказаць", "unhide": "Паказаць",
"center": "Center", "center": "Center",
"open_in": "Адчыніць у", "open_in": "Адчыніць у",
"copy_coordinates": "Copy coordinates", "copy_coordinates": "Copy coordinates"
"edit_osm": "Edit in OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -282,7 +281,6 @@
"update": "Update layer" "update": "Update layer"
}, },
"opacity": "Overlay opacity", "opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Basemaps", "basemaps": "Basemaps",
"overlays": "Overlays", "overlays": "Overlays",
@@ -326,8 +324,6 @@
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite", "cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -356,7 +352,6 @@
"water": "Water", "water": "Water",
"shower": "Shower", "shower": "Shower",
"shelter": "Shelter", "shelter": "Shelter",
"cemetery": "Cemetery",
"motorized": "Cars and Motorcycles", "motorized": "Cars and Motorcycles",
"fuel-station": "Fuel Station", "fuel-station": "Fuel Station",
"parking": "Parking", "parking": "Parking",
@@ -380,9 +375,7 @@
"railway-station": "Railway Station", "railway-station": "Railway Station",
"tram-stop": "Tram Stop", "tram-stop": "Tram Stop",
"bus-stop": "Bus Stop", "bus-stop": "Bus Stop",
"ferry": "Ferry", "ferry": "Ferry"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {
@@ -482,6 +475,7 @@
"app": "App", "app": "App",
"contact": "Contact", "contact": "Contact",
"reddit": "Reddit", "reddit": "Reddit",
"x": "X",
"facebook": "Facebook", "facebook": "Facebook",
"github": "GitHub", "github": "GitHub",
"crowdin": "Crowdin", "crowdin": "Crowdin",

View File

@@ -28,7 +28,7 @@
"undo": "Desfer", "undo": "Desfer",
"redo": "Refer", "redo": "Refer",
"delete": "Elimina el track", "delete": "Elimina el track",
"delete_all": "Esborra-ho tot", "delete_all": "Delete all",
"select_all": "Seleccionar-ho tot", "select_all": "Seleccionar-ho tot",
"view": "Vista", "view": "Vista",
"elevation_profile": "Perfil delevacions", "elevation_profile": "Perfil delevacions",
@@ -79,8 +79,7 @@
"unhide": "Veure", "unhide": "Veure",
"center": "Centrar", "center": "Centrar",
"open_in": "Obrir amb", "open_in": "Obrir amb",
"copy_coordinates": "Copiar coordenades", "copy_coordinates": "Copiar coordenades"
"edit_osm": "Edita a OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -282,7 +281,6 @@
"update": "Actualitza la capa" "update": "Actualitza la capa"
}, },
"opacity": "Opacitat de la superposició", "opacity": "Opacitat de la superposició",
"terrain": "Terreny",
"label": { "label": {
"basemaps": "Mapes base", "basemaps": "Mapes base",
"overlays": "Capes", "overlays": "Capes",
@@ -326,8 +324,6 @@
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite", "cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -356,7 +352,6 @@
"water": "Aigua", "water": "Aigua",
"shower": "Dutxa", "shower": "Dutxa",
"shelter": "Refugi", "shelter": "Refugi",
"cemetery": "Cementiri",
"motorized": "Cotxes i motos", "motorized": "Cotxes i motos",
"fuel-station": "Gasolinera", "fuel-station": "Gasolinera",
"parking": "Aparcament", "parking": "Aparcament",
@@ -380,9 +375,7 @@
"railway-station": "Estació de tren", "railway-station": "Estació de tren",
"tram-stop": "Parada de tramvia", "tram-stop": "Parada de tramvia",
"bus-stop": "Parada d'autobús", "bus-stop": "Parada d'autobús",
"ferry": "Ferri", "ferry": "Ferri"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {
@@ -482,6 +475,7 @@
"app": "App", "app": "App",
"contact": "Contacte", "contact": "Contacte",
"reddit": "Reddit", "reddit": "Reddit",
"x": "X",
"facebook": "Facebook", "facebook": "Facebook",
"github": "GitHub", "github": "GitHub",
"crowdin": "Crowdin", "crowdin": "Crowdin",

View File

@@ -79,8 +79,7 @@
"unhide": "Zobrazit skryté", "unhide": "Zobrazit skryté",
"center": "Vycentrovat", "center": "Vycentrovat",
"open_in": "Otevřít v", "open_in": "Otevřít v",
"copy_coordinates": "Zkopírovat souřadnice", "copy_coordinates": "Zkopírovat souřadnice"
"edit_osm": "Upravit v OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -282,7 +281,6 @@
"update": "Aktualizovat vrstvu" "update": "Aktualizovat vrstvu"
}, },
"opacity": "Průhlednost překryvu", "opacity": "Průhlednost překryvu",
"terrain": "Zdroj terénu",
"label": { "label": {
"basemaps": "Základní mapy", "basemaps": "Základní mapy",
"overlays": "Překrytí", "overlays": "Překrytí",
@@ -326,8 +324,6 @@
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite", "cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Vrstevnice", "swisstopoSlope": "swisstopo Vrstevnice",
"swisstopoHiking": "swisstopo Turistická", "swisstopoHiking": "swisstopo Turistická",
"swisstopoHikingClosures": "swisstopo Turistické uzávěry", "swisstopoHikingClosures": "swisstopo Turistické uzávěry",
@@ -356,7 +352,6 @@
"water": "Voda", "water": "Voda",
"shower": "Sprcha", "shower": "Sprcha",
"shelter": "Přístřeší", "shelter": "Přístřeší",
"cemetery": "Hřbitov",
"motorized": "Automobily a motocykly", "motorized": "Automobily a motocykly",
"fuel-station": "Čerpací stanice", "fuel-station": "Čerpací stanice",
"parking": "Parkoviště", "parking": "Parkoviště",
@@ -380,9 +375,7 @@
"railway-station": "Železniční stanice", "railway-station": "Železniční stanice",
"tram-stop": "Zastávka tramvaje", "tram-stop": "Zastávka tramvaje",
"bus-stop": "Autobusová zastávka", "bus-stop": "Autobusová zastávka",
"ferry": "Trajekt", "ferry": "Trajekt"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {
@@ -482,6 +475,7 @@
"app": "Aplikace", "app": "Aplikace",
"contact": "Kontakt", "contact": "Kontakt",
"reddit": "Reddit", "reddit": "Reddit",
"x": "X",
"facebook": "Facebook", "facebook": "Facebook",
"github": "GitHub", "github": "GitHub",
"crowdin": "Crowdin", "crowdin": "Crowdin",

View File

@@ -79,8 +79,7 @@
"unhide": "Unhide", "unhide": "Unhide",
"center": "Center", "center": "Center",
"open_in": "Open in", "open_in": "Open in",
"copy_coordinates": "Kopier koordinater", "copy_coordinates": "Kopier koordinater"
"edit_osm": "Edit in OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -282,7 +281,6 @@
"update": "Update layer" "update": "Update layer"
}, },
"opacity": "Overlay opacity", "opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Basemaps", "basemaps": "Basemaps",
"overlays": "Overlays", "overlays": "Overlays",
@@ -326,8 +324,6 @@
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite", "cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -356,7 +352,6 @@
"water": "Water", "water": "Water",
"shower": "Shower", "shower": "Shower",
"shelter": "Shelter", "shelter": "Shelter",
"cemetery": "Cemetery",
"motorized": "Cars and Motorcycles", "motorized": "Cars and Motorcycles",
"fuel-station": "Fuel Station", "fuel-station": "Fuel Station",
"parking": "Parking", "parking": "Parking",
@@ -380,9 +375,7 @@
"railway-station": "Railway Station", "railway-station": "Railway Station",
"tram-stop": "Tram Stop", "tram-stop": "Tram Stop",
"bus-stop": "Bus Stop", "bus-stop": "Bus Stop",
"ferry": "Ferry", "ferry": "Ferry"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {
@@ -482,6 +475,7 @@
"app": "App", "app": "App",
"contact": "Contact", "contact": "Contact",
"reddit": "Reddit", "reddit": "Reddit",
"x": "X",
"facebook": "Facebook", "facebook": "Facebook",
"github": "GitHub", "github": "GitHub",
"crowdin": "Crowdin", "crowdin": "Crowdin",

View File

@@ -21,14 +21,14 @@
"export_all": "Alle exportieren...", "export_all": "Alle exportieren...",
"export_options": "Export-Einstellungen", "export_options": "Export-Einstellungen",
"support_message": "Das Tool darf frei benutzt werden, aber es darf nicht woanders aufgesetzt werden. Bitte unterstützen Sie die Website, wenn Sie sie häufig benutzen. Vielen Dank!", "support_message": "Das Tool darf frei benutzt werden, aber es darf nicht woanders aufgesetzt werden. Bitte unterstützen Sie die Website, wenn Sie sie häufig benutzen. Vielen Dank!",
"support_button": "Hilf uns, die Website weiterhin kostenlos bereitzustellen", "support_button": "Hilf dabei, die Webseite kostenlos zu belassen",
"download_file": "Datei herunterladen", "download_file": "Datei herunterladen",
"download_files": "Dateien herunterladen", "download_files": "Dateien herunterladen",
"edit": "Bearbeiten", "edit": "Bearbeiten",
"undo": "Rückgängig", "undo": "Rückgängig",
"redo": "Wiederholen", "redo": "Wiederholen",
"delete": "Löschen", "delete": "Löschen",
"delete_all": "Alle löschen", "delete_all": "Delete all",
"select_all": "Alle auswählen", "select_all": "Alle auswählen",
"view": "Ansicht", "view": "Ansicht",
"elevation_profile": "Höhenprofil", "elevation_profile": "Höhenprofil",
@@ -79,8 +79,7 @@
"unhide": "Einblenden", "unhide": "Einblenden",
"center": "Zentrieren", "center": "Zentrieren",
"open_in": "Öffnen in", "open_in": "Öffnen in",
"copy_coordinates": "Koordinaten kopieren", "copy_coordinates": "Koordinaten kopieren"
"edit_osm": "In OpenStreetMap bearbeiten"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -282,7 +281,6 @@
"update": "Layer aktualisieren" "update": "Layer aktualisieren"
}, },
"opacity": "Deckkraft der Überlagerung", "opacity": "Deckkraft der Überlagerung",
"terrain": "Geländequelle",
"label": { "label": {
"basemaps": "Basiskarte", "basemaps": "Basiskarte",
"overlays": "Ebenen", "overlays": "Ebenen",
@@ -326,8 +324,6 @@
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite", "cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "MapTiler Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Neigung", "swisstopoSlope": "swisstopo Neigung",
"swisstopoHiking": "swisstopo Wandern", "swisstopoHiking": "swisstopo Wandern",
"swisstopoHikingClosures": "swisstopo Wanderungen Schließungen", "swisstopoHikingClosures": "swisstopo Wanderungen Schließungen",
@@ -356,7 +352,6 @@
"water": "Trinkwasser", "water": "Trinkwasser",
"shower": "Dusche", "shower": "Dusche",
"shelter": "Unterstand", "shelter": "Unterstand",
"cemetery": "Friedhof",
"motorized": "Autos und Motorräder", "motorized": "Autos und Motorräder",
"fuel-station": "Tankstelle", "fuel-station": "Tankstelle",
"parking": "Parken", "parking": "Parken",
@@ -380,9 +375,7 @@
"railway-station": "Bahnhof", "railway-station": "Bahnhof",
"tram-stop": "Straßenbahnhaltestelle", "tram-stop": "Straßenbahnhaltestelle",
"bus-stop": "Bushaltestelle", "bus-stop": "Bushaltestelle",
"ferry": "Fähre", "ferry": "Fähre"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {
@@ -478,10 +471,11 @@
}, },
"homepage": { "homepage": {
"website": "Webseite", "website": "Webseite",
"home": "Startseite", "home": "Zuhause",
"app": "App", "app": "App",
"contact": "Kontakt", "contact": "Kontakt",
"reddit": "Reddit", "reddit": "Reddit",
"x": "X",
"facebook": "Facebook", "facebook": "Facebook",
"github": "GitHub", "github": "GitHub",
"crowdin": "Crowdin", "crowdin": "Crowdin",

View File

@@ -79,8 +79,7 @@
"unhide": "Unhide", "unhide": "Unhide",
"center": "Center", "center": "Center",
"open_in": "Open in", "open_in": "Open in",
"copy_coordinates": "Copy coordinates", "copy_coordinates": "Copy coordinates"
"edit_osm": "Edit in OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -282,7 +281,6 @@
"update": "Update layer" "update": "Update layer"
}, },
"opacity": "Overlay opacity", "opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Basemaps", "basemaps": "Basemaps",
"overlays": "Overlays", "overlays": "Overlays",
@@ -326,8 +324,6 @@
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite", "cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -356,7 +352,6 @@
"water": "Water", "water": "Water",
"shower": "Shower", "shower": "Shower",
"shelter": "Shelter", "shelter": "Shelter",
"cemetery": "Cemetery",
"motorized": "Cars and Motorcycles", "motorized": "Cars and Motorcycles",
"fuel-station": "Fuel Station", "fuel-station": "Fuel Station",
"parking": "Parking", "parking": "Parking",
@@ -380,9 +375,7 @@
"railway-station": "Railway Station", "railway-station": "Railway Station",
"tram-stop": "Tram Stop", "tram-stop": "Tram Stop",
"bus-stop": "Bus Stop", "bus-stop": "Bus Stop",
"ferry": "Ferry", "ferry": "Ferry"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {
@@ -482,6 +475,7 @@
"app": "App", "app": "App",
"contact": "Contact", "contact": "Contact",
"reddit": "Reddit", "reddit": "Reddit",
"x": "X",
"facebook": "Facebook", "facebook": "Facebook",
"github": "GitHub", "github": "GitHub",
"crowdin": "Crowdin", "crowdin": "Crowdin",

View File

@@ -79,8 +79,7 @@
"unhide": "Unhide", "unhide": "Unhide",
"center": "Center", "center": "Center",
"open_in": "Open in", "open_in": "Open in",
"copy_coordinates": "Copy coordinates", "copy_coordinates": "Copy coordinates"
"edit_osm": "Edit in OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -282,7 +281,6 @@
"update": "Update layer" "update": "Update layer"
}, },
"opacity": "Overlay opacity", "opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Basemaps", "basemaps": "Basemaps",
"overlays": "Overlays", "overlays": "Overlays",
@@ -326,8 +324,6 @@
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite", "cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -356,7 +352,6 @@
"water": "Water", "water": "Water",
"shower": "Shower", "shower": "Shower",
"shelter": "Shelter", "shelter": "Shelter",
"cemetery": "Cemetery",
"motorized": "Cars and Motorcycles", "motorized": "Cars and Motorcycles",
"fuel-station": "Fuel Station", "fuel-station": "Fuel Station",
"parking": "Parking", "parking": "Parking",
@@ -380,9 +375,7 @@
"railway-station": "Railway Station", "railway-station": "Railway Station",
"tram-stop": "Tram Stop", "tram-stop": "Tram Stop",
"bus-stop": "Bus Stop", "bus-stop": "Bus Stop",
"ferry": "Ferry", "ferry": "Ferry"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {
@@ -482,6 +475,7 @@
"app": "App", "app": "App",
"contact": "Contact", "contact": "Contact",
"reddit": "Reddit", "reddit": "Reddit",
"x": "X",
"facebook": "Facebook", "facebook": "Facebook",
"github": "GitHub", "github": "GitHub",
"crowdin": "Crowdin", "crowdin": "Crowdin",

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."
@@ -36,7 +36,7 @@
"switch_basemap": "Cambiar al mapa base anterior", "switch_basemap": "Cambiar al mapa base anterior",
"toggle_overlays": "Alternar capas", "toggle_overlays": "Alternar capas",
"toggle_3d": "Alternar 3D", "toggle_3d": "Alternar 3D",
"settings": "Configuración", "settings": "Configuraciones",
"distance_units": "Unidades de distancia", "distance_units": "Unidades de distancia",
"metric": "Métrico", "metric": "Métrico",
"imperial": "Imperial", "imperial": "Imperial",
@@ -79,8 +79,7 @@
"unhide": "Mostrar", "unhide": "Mostrar",
"center": "Centrar", "center": "Centrar",
"open_in": "Abrir en", "open_in": "Abrir en",
"copy_coordinates": "Copiar coordenadas", "copy_coordinates": "Copiar coordenadas"
"edit_osm": "Editar en OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -282,7 +281,6 @@
"update": "Actualizar capa" "update": "Actualizar capa"
}, },
"opacity": "Opacidad de la capa superpuesta", "opacity": "Opacidad de la capa superpuesta",
"terrain": "Origen del terreno",
"label": { "label": {
"basemaps": "Mapas base", "basemaps": "Mapas base",
"overlays": "Capas", "overlays": "Capas",
@@ -326,8 +324,6 @@
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "Gravel bikerouter.de", "bikerouterGravel": "Gravel bikerouter.de",
"cyclOSMlite": "CyclOSM Lite", "cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Senderismo", "swisstopoHiking": "swisstopo Senderismo",
"swisstopoHikingClosures": "swisstopo Rutas Senderismo", "swisstopoHikingClosures": "swisstopo Rutas Senderismo",
@@ -356,7 +352,6 @@
"water": "Agua", "water": "Agua",
"shower": "Ducha", "shower": "Ducha",
"shelter": "Refugio", "shelter": "Refugio",
"cemetery": "Cementerio",
"motorized": "Coches y motos", "motorized": "Coches y motos",
"fuel-station": "Gasolinera", "fuel-station": "Gasolinera",
"parking": "Aparcamiento", "parking": "Aparcamiento",
@@ -380,9 +375,7 @@
"railway-station": "Estación de tren", "railway-station": "Estación de tren",
"tram-stop": "Parada de tranvía", "tram-stop": "Parada de tranvía",
"bus-stop": "Parada de autobús", "bus-stop": "Parada de autobús",
"ferry": "Ferri", "ferry": "Ferri"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {
@@ -482,6 +475,7 @@
"app": "App", "app": "App",
"contact": "Contacto", "contact": "Contacto",
"reddit": "Reddit", "reddit": "Reddit",
"x": "X",
"facebook": "Facebook", "facebook": "Facebook",
"github": "GitHub", "github": "GitHub",
"crowdin": "Crowdin", "crowdin": "Crowdin",

View File

@@ -28,7 +28,7 @@
"undo": "Desegin", "undo": "Desegin",
"redo": "Berregin", "redo": "Berregin",
"delete": "Ezabatu", "delete": "Ezabatu",
"delete_all": "Ezabatu guztiak", "delete_all": "Delete all",
"select_all": "Hautatu dena", "select_all": "Hautatu dena",
"view": "Ikusi", "view": "Ikusi",
"elevation_profile": "Altuera profila", "elevation_profile": "Altuera profila",
@@ -79,8 +79,7 @@
"unhide": "Erakutsi", "unhide": "Erakutsi",
"center": "Erdiratu", "center": "Erdiratu",
"open_in": "Ireki hemen", "open_in": "Ireki hemen",
"copy_coordinates": "Kopiatu koordenatuak", "copy_coordinates": "Kopiatu koordenatuak"
"edit_osm": "Editatu OpenStreeMapen"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -282,7 +281,6 @@
"update": "Eguneratu geruza" "update": "Eguneratu geruza"
}, },
"opacity": "Geruzaren opakutasuna", "opacity": "Geruzaren opakutasuna",
"terrain": "Lurrazala",
"label": { "label": {
"basemaps": "Oinarrizko mapak", "basemaps": "Oinarrizko mapak",
"overlays": "Geruzak", "overlays": "Geruzak",
@@ -326,8 +324,6 @@
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite", "cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Malda", "swisstopoSlope": "swisstopo Malda",
"swisstopoHiking": "swisstopo Mendi ibilaldiak", "swisstopoHiking": "swisstopo Mendi ibilaldiak",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -356,7 +352,6 @@
"water": "Ura", "water": "Ura",
"shower": "Dutxa", "shower": "Dutxa",
"shelter": "Babeslekua", "shelter": "Babeslekua",
"cemetery": "Hilerria",
"motorized": "Kotxeak eta motorrak", "motorized": "Kotxeak eta motorrak",
"fuel-station": "Gasolindegia", "fuel-station": "Gasolindegia",
"parking": "Aparkalekua", "parking": "Aparkalekua",
@@ -380,9 +375,7 @@
"railway-station": "Tren geltokia", "railway-station": "Tren geltokia",
"tram-stop": "Tranbia geltokia", "tram-stop": "Tranbia geltokia",
"bus-stop": "Autobus geltokia", "bus-stop": "Autobus geltokia",
"ferry": "Ferria", "ferry": "Ferria"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {
@@ -482,6 +475,7 @@
"app": "App", "app": "App",
"contact": "Kontaktua", "contact": "Kontaktua",
"reddit": "Reddit", "reddit": "Reddit",
"x": "X",
"facebook": "Facebook", "facebook": "Facebook",
"github": "GitHub", "github": "GitHub",
"crowdin": "Crowdin", "crowdin": "Crowdin",

View File

@@ -79,8 +79,7 @@
"unhide": "Näytä", "unhide": "Näytä",
"center": "Keskitä", "center": "Keskitä",
"open_in": "Avaa", "open_in": "Avaa",
"copy_coordinates": "Copy coordinates", "copy_coordinates": "Copy coordinates"
"edit_osm": "Edit in OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -282,7 +281,6 @@
"update": "Päivitä karttataso" "update": "Päivitä karttataso"
}, },
"opacity": "Peitetason läpinäkyvyys", "opacity": "Peitetason läpinäkyvyys",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Taustakartat", "basemaps": "Taustakartat",
"overlays": "Peitetasot", "overlays": "Peitetasot",
@@ -326,8 +324,6 @@
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite", "cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Rinnekaltevuus", "swisstopoSlope": "swisstopo Rinnekaltevuus",
"swisstopoHiking": "swisstopo Retkeilyreitit", "swisstopoHiking": "swisstopo Retkeilyreitit",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -356,7 +352,6 @@
"water": "Water", "water": "Water",
"shower": "Shower", "shower": "Shower",
"shelter": "Shelter", "shelter": "Shelter",
"cemetery": "Cemetery",
"motorized": "Cars and Motorcycles", "motorized": "Cars and Motorcycles",
"fuel-station": "Fuel Station", "fuel-station": "Fuel Station",
"parking": "Parking", "parking": "Parking",
@@ -380,9 +375,7 @@
"railway-station": "Rautatieasemat", "railway-station": "Rautatieasemat",
"tram-stop": "Raitiovaunupysäkit", "tram-stop": "Raitiovaunupysäkit",
"bus-stop": "Linja-autopysäkit", "bus-stop": "Linja-autopysäkit",
"ferry": "Lautat", "ferry": "Lautat"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {
@@ -482,6 +475,7 @@
"app": "App", "app": "App",
"contact": "Yhteystiedot", "contact": "Yhteystiedot",
"reddit": "Reddit", "reddit": "Reddit",
"x": "X",
"facebook": "Facebook", "facebook": "Facebook",
"github": "GitHub", "github": "GitHub",
"crowdin": "Crowdin", "crowdin": "Crowdin",

View File

@@ -79,8 +79,7 @@
"unhide": "Afficher", "unhide": "Afficher",
"center": "Centrer", "center": "Centrer",
"open_in": "Ouvrir avec", "open_in": "Ouvrir avec",
"copy_coordinates": "Copier les coordonnées", "copy_coordinates": "Copier les coordonnées"
"edit_osm": "Éditer dans OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -282,7 +281,6 @@
"update": "Mettre à jour la couche" "update": "Mettre à jour la couche"
}, },
"opacity": "Opacité de la surcouche", "opacity": "Opacité de la surcouche",
"terrain": "Source du relief",
"label": { "label": {
"basemaps": "Fonds de carte", "basemaps": "Fonds de carte",
"overlays": "Surcouches", "overlays": "Surcouches",
@@ -326,8 +324,6 @@
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite", "cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Relief",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Pente", "swisstopoSlope": "swisstopo Pente",
"swisstopoHiking": "swisstopo Randonnée", "swisstopoHiking": "swisstopo Randonnée",
"swisstopoHikingClosures": "swisstopo Fermetures de randonnée", "swisstopoHikingClosures": "swisstopo Fermetures de randonnée",
@@ -353,10 +349,9 @@
"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",
"motorized": "Voitures et motos", "motorized": "Voitures et motos",
"fuel-station": "Station-service", "fuel-station": "Station-service",
"parking": "Parking", "parking": "Parking",
@@ -380,9 +375,7 @@
"railway-station": "Gare", "railway-station": "Gare",
"tram-stop": "Arrêt de tram", "tram-stop": "Arrêt de tram",
"bus-stop": "Arrêt de bus", "bus-stop": "Arrêt de bus",
"ferry": "Ferry", "ferry": "Ferry"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {
@@ -448,7 +441,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",
@@ -473,7 +466,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": {
@@ -482,6 +475,7 @@
"app": "Application", "app": "Application",
"contact": "Contact", "contact": "Contact",
"reddit": "Reddit", "reddit": "Reddit",
"x": "X",
"facebook": "Facebook", "facebook": "Facebook",
"github": "GitHub", "github": "GitHub",
"crowdin": "Crowdin", "crowdin": "Crowdin",

View File

@@ -79,8 +79,7 @@
"unhide": "Unhide", "unhide": "Unhide",
"center": "Center", "center": "Center",
"open_in": "Open in", "open_in": "Open in",
"copy_coordinates": "Copy coordinates", "copy_coordinates": "Copy coordinates"
"edit_osm": "Edit in OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -282,7 +281,6 @@
"update": "Update layer" "update": "Update layer"
}, },
"opacity": "Overlay opacity", "opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Basemaps", "basemaps": "Basemaps",
"overlays": "Overlays", "overlays": "Overlays",
@@ -326,8 +324,6 @@
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite", "cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -356,7 +352,6 @@
"water": "Water", "water": "Water",
"shower": "גשם", "shower": "גשם",
"shelter": "Shelter", "shelter": "Shelter",
"cemetery": "Cemetery",
"motorized": "Cars and Motorcycles", "motorized": "Cars and Motorcycles",
"fuel-station": "Fuel Station", "fuel-station": "Fuel Station",
"parking": "Parking", "parking": "Parking",
@@ -380,9 +375,7 @@
"railway-station": "Railway Station", "railway-station": "Railway Station",
"tram-stop": "Tram Stop", "tram-stop": "Tram Stop",
"bus-stop": "Bus Stop", "bus-stop": "Bus Stop",
"ferry": "Ferry", "ferry": "Ferry"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {
@@ -482,6 +475,7 @@
"app": "App", "app": "App",
"contact": "Contact", "contact": "Contact",
"reddit": "Reddit", "reddit": "Reddit",
"x": "X",
"facebook": "Facebook", "facebook": "Facebook",
"github": "GitHub", "github": "GitHub",
"crowdin": "Crowdin", "crowdin": "Crowdin",

View File

@@ -79,8 +79,7 @@
"unhide": "Felfedés ", "unhide": "Felfedés ",
"center": "Középre ", "center": "Középre ",
"open_in": "Megnyitás itt ", "open_in": "Megnyitás itt ",
"copy_coordinates": "Koordináták másolása", "copy_coordinates": "Koordináták másolása"
"edit_osm": "Edit in OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -235,13 +234,13 @@
"help_no_selection": "Select a file item to request elevation data." "help_no_selection": "Select a file item to request elevation data."
}, },
"waypoint": { "waypoint": {
"tooltip": "PoI létrehozása és módosítása", "tooltip": "Create and edit points of interest",
"icon": "Ikon", "icon": "Icon",
"link": "Link", "link": "Link",
"longitude": "Földrajzi hosszúság", "longitude": "Longitude",
"latitude": "Szélesség", "latitude": "Szélesség",
"create": "POI fájlba mentése", "create": "POI fájlba mentése",
"add": "PoI fájlhoz adása", "add": "Add point of interest to file",
"help": "Töltsd ki az űrlapot egy új POI létrehozásához, vagy kattints egy meglévőre a szerkesztéshez. Kattints a térképre a koordináták megadásához, vagy áthelyezéshez egérrel húzd el a POI-kat.", "help": "Töltsd ki az űrlapot egy új POI létrehozásához, vagy kattints egy meglévőre a szerkesztéshez. Kattints a térképre a koordináták megadásához, vagy áthelyezéshez egérrel húzd el a POI-kat.",
"help_no_selection": "Select a file to create or edit points of interest." "help_no_selection": "Select a file to create or edit points of interest."
}, },
@@ -249,8 +248,8 @@
"tooltip": "Reduce the number of GPS points", "tooltip": "Reduce the number of GPS points",
"tolerance": "Tűréshatár", "tolerance": "Tűréshatár",
"number_of_points": "GPS pontok száma", "number_of_points": "GPS pontok száma",
"button": "Minimalizálás", "button": "Minify",
"help": "", "help": "Use the slider to choose the number of GPS points to keep.",
"help_no_selection": "Select a trace to reduce the number of its GPS points." "help_no_selection": "Select a trace to reduce the number of its GPS points."
}, },
"clean": { "clean": {
@@ -282,7 +281,6 @@
"update": "Réteg feltöltése" "update": "Réteg feltöltése"
}, },
"opacity": "Átfedés átlátszósága", "opacity": "Átfedés átlátszósága",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Alaptérkép", "basemaps": "Alaptérkép",
"overlays": "Térkép rétegek", "overlays": "Térkép rétegek",
@@ -326,8 +324,6 @@
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "kerékpár és terepkerékpár út", "bikerouterGravel": "kerékpár és terepkerékpár út",
"cyclOSMlite": "CyclOSM Lite", "cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Lejtő", "swisstopoSlope": "swisstopo Lejtő",
"swisstopoHiking": "swisstopo Túra", "swisstopoHiking": "swisstopo Túra",
"swisstopoHikingClosures": "swisstopo túralezárások", "swisstopoHikingClosures": "swisstopo túralezárások",
@@ -356,7 +352,6 @@
"water": "Víz", "water": "Víz",
"shower": "Zuhanyozó", "shower": "Zuhanyozó",
"shelter": "Menedék", "shelter": "Menedék",
"cemetery": "Cemetery",
"motorized": "Autók és Motorok", "motorized": "Autók és Motorok",
"fuel-station": "Benzinkút", "fuel-station": "Benzinkút",
"parking": "Parkoló", "parking": "Parkoló",
@@ -380,9 +375,7 @@
"railway-station": "Vasútállomás", "railway-station": "Vasútállomás",
"tram-stop": "Villamos megálló", "tram-stop": "Villamos megálló",
"bus-stop": "Buszmegálló", "bus-stop": "Buszmegálló",
"ferry": "Komp", "ferry": "Komp"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {
@@ -482,6 +475,7 @@
"app": "App", "app": "App",
"contact": "Kapcsolat", "contact": "Kapcsolat",
"reddit": "Reddit", "reddit": "Reddit",
"x": "X",
"facebook": "Facebook", "facebook": "Facebook",
"github": "GitHub", "github": "GitHub",
"crowdin": "Crowdin", "crowdin": "Crowdin",

View File

@@ -79,8 +79,7 @@
"unhide": "Tampilkan", "unhide": "Tampilkan",
"center": "Tengah", "center": "Tengah",
"open_in": "Buka di", "open_in": "Buka di",
"copy_coordinates": "Salin koordinat", "copy_coordinates": "Salin koordinat"
"edit_osm": "Edit in OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -282,7 +281,6 @@
"update": "Perbarui lapisan" "update": "Perbarui lapisan"
}, },
"opacity": "Opasitas Overlay", "opacity": "Opasitas Overlay",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Peta dasar", "basemaps": "Peta dasar",
"overlays": "Overlay", "overlays": "Overlay",
@@ -326,8 +324,6 @@
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite", "cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Kemiringan", "swisstopoSlope": "swisstopo Kemiringan",
"swisstopoHiking": "swisstopo Pendakian", "swisstopoHiking": "swisstopo Pendakian",
"swisstopoHikingClosures": "Penutupan Jalur Pendakian swisstopo", "swisstopoHikingClosures": "Penutupan Jalur Pendakian swisstopo",
@@ -356,7 +352,6 @@
"water": "Air", "water": "Air",
"shower": "Mandi", "shower": "Mandi",
"shelter": "Penampungan", "shelter": "Penampungan",
"cemetery": "Cemetery",
"motorized": "Mobil dan Motor", "motorized": "Mobil dan Motor",
"fuel-station": "Stasiun bahan bakar", "fuel-station": "Stasiun bahan bakar",
"parking": "Parkir", "parking": "Parkir",
@@ -380,9 +375,7 @@
"railway-station": "Stasiun kereta api", "railway-station": "Stasiun kereta api",
"tram-stop": "Halt trem", "tram-stop": "Halt trem",
"bus-stop": "Pemberhentian Bus", "bus-stop": "Pemberhentian Bus",
"ferry": "Feri", "ferry": "Feri"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {
@@ -482,6 +475,7 @@
"app": "App", "app": "App",
"contact": "Contact", "contact": "Contact",
"reddit": "Reddit", "reddit": "Reddit",
"x": "X",
"facebook": "Facebook", "facebook": "Facebook",
"github": "GitHub", "github": "GitHub",
"crowdin": "Crowdin", "crowdin": "Crowdin",
@@ -491,7 +485,7 @@
"support_button": "Support gpx.studio on Ko-fi", "support_button": "Support gpx.studio on Ko-fi",
"route_planning": "Route planning", "route_planning": "Route planning",
"route_planning_description": "An intuitive interface to create itineraries tailored to each sport, based on OpenStreetMap data.", "route_planning_description": "An intuitive interface to create itineraries tailored to each sport, based on OpenStreetMap data.",
"file_processing": "Pengolahan file lanjutan", "file_processing": "Advanced file processing",
"file_processing_description": "A suite of tools for performing all common file processing tasks, and which can be applied to multiple files at once.", "file_processing_description": "A suite of tools for performing all common file processing tasks, and which can be applied to multiple files at once.",
"maps": "Global and local maps", "maps": "Global and local maps",
"maps_description": "A large collection of basemaps, overlays and points of interest to help you craft your next outdoor adventure, or visualize your latest achievement.", "maps_description": "A large collection of basemaps, overlays and points of interest to help you craft your next outdoor adventure, or visualize your latest achievement.",

View File

@@ -28,14 +28,14 @@
"undo": "Annulla", "undo": "Annulla",
"redo": "Ripeti", "redo": "Ripeti",
"delete": "Elimina", "delete": "Elimina",
"delete_all": "Cancella tutto", "delete_all": "Delete all",
"select_all": "Seleziona tutto", "select_all": "Seleziona tutto",
"view": "Visualizza", "view": "Visualizza",
"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",
@@ -79,15 +79,14 @@
"unhide": "Mostra", "unhide": "Mostra",
"center": "Centra", "center": "Centra",
"open_in": "Apri con", "open_in": "Apri con",
"copy_coordinates": "Copia le coordinate", "copy_coordinates": "Copia le coordinate"
"edit_osm": "Modifica in OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
"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 +234,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 +253,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 +272,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",
@@ -282,7 +281,6 @@
"update": "Aggiorna livello" "update": "Aggiorna livello"
}, },
"opacity": "Opacità di sovrapposizione", "opacity": "Opacità di sovrapposizione",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Mappe di base", "basemaps": "Mappe di base",
"overlays": "Sovrapposizioni", "overlays": "Sovrapposizioni",
@@ -310,7 +308,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",
@@ -319,27 +317,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",
"mapterhornHillshade": "Mapterhorn Hillshade", "swisstopoSlope": "Carta topografica Svizzera Pendenza",
"openRailwayMap": "OpenRailwayMap", "swisstopoHiking": "Carta topografica Svedese Escursione",
"swisstopoSlope": "swisstopo Pendenza", "swisstopoHikingClosures": "Carta topografica Svizzera Fine escursione",
"swisstopoHiking": "swisstopo Escursione", "swisstopoCycling": "Carta topografica Svizzera Ciclabile",
"swisstopoHikingClosures": "swisstopo Fine escursione", "swisstopoCyclingClosures": "Carta topografica Svizzera fine ciclabile",
"swisstopoCycling": "swisstopo Ciclabile", "swisstopoMountainBike": "Carta topografica Svizzera MTB",
"swisstopoCyclingClosures": "swisstopo Fine ciclabile", "swisstopoMountainBikeClosures": "Carta topografica Svizzera fine MTB",
"swisstopoMountainBike": "swisstopo MTB", "swisstopoSkiTouring": "Carta topografica Svizzera pista sci",
"swisstopoMountainBikeClosures": "swisstopo Fine MTB",
"swisstopoSkiTouring": "swisstopo Sci Alpinismo",
"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",
@@ -356,7 +352,6 @@
"water": "Acqua", "water": "Acqua",
"shower": "Doccia", "shower": "Doccia",
"shelter": "Riparo", "shelter": "Riparo",
"cemetery": "Cimitero",
"motorized": "Auto e Motocicli", "motorized": "Auto e Motocicli",
"fuel-station": "Stazione di Rifornimento", "fuel-station": "Stazione di Rifornimento",
"parking": "Parcheggio", "parking": "Parcheggio",
@@ -380,9 +375,7 @@
"railway-station": "Stazione ferroviaria", "railway-station": "Stazione ferroviaria",
"tram-stop": "Fermata del tram", "tram-stop": "Fermata del tram",
"bus-stop": "Fermata dell'autobus", "bus-stop": "Fermata dell'autobus",
"ferry": "Traghetto", "ferry": "Traghetto"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {
@@ -411,11 +404,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",
@@ -431,8 +424,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",
@@ -482,19 +475,20 @@
"app": "App", "app": "App",
"contact": "Contatto", "contact": "Contatto",
"reddit": "Reddit", "reddit": "Reddit",
"x": "X",
"facebook": "Facebook", "facebook": "Facebook",
"github": "GitHub", "github": "GitHub",
"crowdin": "Crowdin", "crowdin": "Crowdin",
"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",

Some files were not shown because too many files have changed in this diff Show More