13 Commits

Author SHA1 Message Date
vcoppe
a01ca79a82 finer-grained road access 2026-01-18 15:23:39 +01:00
vcoppe
c91baf7c83 switch gravel to graphhopper 2026-01-17 11:58:47 +01:00
vcoppe
5062de8ddf Merge branch 'dev' into graphhopper 2026-01-17 11:42:30 +01:00
vcoppe
f0f1ecb2df New Crowdin updates (#303)
* New translations en.json (Spanish)

* New translations en.json (German)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Belarusian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Basque)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Chinese Simplified)

* New translations en.json (Polish)

* New translations en.json (Italian)

* New translations en.json (Hungarian)

* New translations en.json (Korean)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Thai)

* New translations en.json (Latvian)

* New translations en.json (Chinese Traditional, Hong Kong)

* New translations en.json (Serbian (Latin))

* New translations mapbox.mdx (German)

* New translations en.json (Chinese Simplified)

* New translations en.json (Polish)

* New translations en.json (Spanish)

* New translations en.json (German)

* New translations en.json (Italian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Belarusian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Basque)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Korean)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Thai)

* New translations en.json (Latvian)

* New translations en.json (Chinese Traditional, Hong Kong)

* New translations en.json (Serbian (Latin))

* New translations en.json (French)
2026-01-16 20:06:44 +01:00
vcoppe
2eb6ef6f03 new setting for selecting terrain source 2026-01-16 19:16:28 +01:00
vcoppe
f7c0805161 add mapterhorn hillshade overlay, closes #292 2026-01-16 18:32:32 +01:00
vcoppe
4e18e3c8a0 update year 2026-01-16 18:25:27 +01:00
vcoppe
59f31caf26 add openrailwaymap overlay, closes #298 2026-01-11 20:18:00 +01:00
vcoppe
f24956c58d improve grouping statistics performance 2026-01-11 19:48:48 +01:00
vcoppe
9019317e5c fix prettier paths, continued 2026-01-11 19:06:54 +01:00
vcoppe
2a0227c1de New Crowdin updates (#300)
* New translations en.json (Spanish)

* New translations en.json (German)

* New translations en.json (Italian)

* New translations files-and-stats.mdx (Italian)

* New translations gpx.mdx (Italian)

* New translations funding.mdx (Italian)

* New translations en.json (French)
2026-01-11 11:09:41 +01:00
vcoppe
9ca46b9d35 small fix 2025-12-24 17:21:26 +01:00
vcoppe
7c2e24bbc4 draft support for graphhopper 2025-12-23 16:49:47 +01:00
61 changed files with 1165 additions and 687 deletions

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
website/src/lib/components/ui
website/src/lib/docs/**/*.mdx
**/*.webmanifest

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2025 gpx.studio Copyright (c) 2026 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

@@ -1,4 +1,5 @@
import { ramerDouglasPeucker } from './simplify'; import { ramerDouglasPeucker } from './simplify';
import { GPXStatistics, GPXStatisticsGroup, TrackPointLocalStatistics } from './statistics';
import { import {
Coordinates, Coordinates,
GPXFileAttributes, GPXFileAttributes,
@@ -36,7 +37,6 @@ export abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
abstract getNumberOfTrackPoints(): number; abstract getNumberOfTrackPoints(): number;
abstract getStartTimestamp(): Date | undefined; abstract getStartTimestamp(): Date | undefined;
abstract getEndTimestamp(): Date | undefined; abstract getEndTimestamp(): Date | undefined;
abstract getStatistics(): GPXStatistics;
abstract getSegments(): TrackSegment[]; abstract getSegments(): TrackSegment[];
abstract getTrackPoints(): TrackPoint[]; abstract getTrackPoints(): TrackPoint[];
@@ -76,14 +76,6 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
return this.children[this.children.length - 1].getEndTimestamp(); return this.children[this.children.length - 1].getEndTimestamp();
} }
getStatistics(): GPXStatistics {
let statistics = new GPXStatistics();
for (let child of this.children) {
statistics.mergeWith(child.getStatistics());
}
return statistics;
}
getSegments(): TrackSegment[] { getSegments(): TrackSegment[] {
return this.children.flatMap((child) => child.getSegments()); return this.children.flatMap((child) => child.getSegments());
} }
@@ -208,8 +200,16 @@ export class GPXFile extends GPXTreeNode<Track> {
}); });
} }
getStatistics(): GPXStatisticsGroup {
let statistics = new GPXStatisticsGroup();
this.forEachSegment((segment) => {
statistics.add(segment.getStatistics());
});
return statistics;
}
getStyle(defaultColor?: string): MergedLineStyles { getStyle(defaultColor?: string): MergedLineStyles {
return this.trk const style = this.trk
.map((track) => track.getStyle()) .map((track) => track.getStyle())
.reduce( .reduce(
(acc, style) => { (acc, style) => {
@@ -219,8 +219,6 @@ export class GPXFile extends GPXTreeNode<Track> {
!acc.color.includes(style['gpx_style:color']) !acc.color.includes(style['gpx_style:color'])
) { ) {
acc.color.push(style['gpx_style:color']); acc.color.push(style['gpx_style:color']);
} else if (defaultColor && !acc.color.includes(defaultColor)) {
acc.color.push(defaultColor);
} }
if ( if (
style && style &&
@@ -244,6 +242,10 @@ export class GPXFile extends GPXTreeNode<Track> {
width: [], width: [],
} }
); );
if (style.color.length === 0 && defaultColor) {
style.color.push(defaultColor);
}
return style;
} }
clone(): GPXFile { clone(): GPXFile {
@@ -818,7 +820,9 @@ 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.slice(0); statistics.local.points = this.trkpt.slice(0);
statistics.local.data = this.trkpt.map(() => new TrackPointLocalStatistics());
const points = this.trkpt; const points = this.trkpt;
for (let i = 0; i < points.length; i++) { for (let i = 0; i < points.length; i++) {
@@ -830,19 +834,18 @@ export class TrackSegment extends GPXTreeLeaf {
statistics.global.distance.total += dist; statistics.global.distance.total += dist;
} }
statistics.local.distance.total.push(statistics.global.distance.total); statistics.local.data[i].distance.total = statistics.global.distance.total;
// time // time
if (points[i].time === undefined) { if (points[i].time === undefined) {
statistics.local.time.total.push(0); statistics.local.data[i].time.total = 0;
} else { } else {
if (statistics.global.time.start === undefined) { if (statistics.global.time.start === undefined) {
statistics.global.time.start = points[i].time; statistics.global.time.start = points[i].time;
} }
statistics.global.time.end = points[i].time; statistics.global.time.end = points[i].time;
statistics.local.time.total.push( statistics.local.data[i].time.total =
(points[i].time.getTime() - statistics.global.time.start.getTime()) / 1000 (points[i].time.getTime() - statistics.global.time.start.getTime()) / 1000;
);
} }
// speed // speed
@@ -857,8 +860,8 @@ export class TrackSegment extends GPXTreeLeaf {
} }
} }
statistics.local.distance.moving.push(statistics.global.distance.moving); statistics.local.data[i].distance.moving = statistics.global.distance.moving;
statistics.local.time.moving.push(statistics.global.time.moving); statistics.local.data[i].time.moving = statistics.global.time.moving;
// bounds // bounds
statistics.global.bounds.southWest.lat = Math.min( statistics.global.bounds.southWest.lat = Math.min(
@@ -958,13 +961,22 @@ export class TrackSegment extends GPXTreeLeaf {
? statistics.global.distance.moving / (statistics.global.time.moving / 3600) ? statistics.global.distance.moving / (statistics.global.time.moving / 3600)
: 0; : 0;
statistics.local.speed = timeWindowSmoothing(points, 10000, (start, end) => timeWindowSmoothing(
points[start].time && points[end].time points,
? (3600 * 10000,
(statistics.local.distance.total[end] - (start, end) =>
statistics.local.distance.total[start])) / points[start].time && points[end].time
Math.max((points[end].time.getTime() - points[start].time.getTime()) / 1000, 1) ? (3600 *
: undefined (statistics.local.data[end].distance.total -
statistics.local.data[start].distance.total)) /
Math.max(
(points[end].time.getTime() - points[start].time.getTime()) / 1000,
1
)
: undefined,
(value, index) => {
statistics.local.data[index].speed = value;
}
); );
return statistics; return statistics;
@@ -984,53 +996,65 @@ export class TrackSegment extends GPXTreeLeaf {
let cumulEle = 0; let cumulEle = 0;
let currentStart = start; let currentStart = start;
let currentEnd = start; let currentEnd = start;
let smoothedEle = distanceWindowSmoothing(start, end + 1, statistics, 0.1, (s, e) => { let prevSmoothedEle = 0;
for (let i = currentStart; i < s; i++) { distanceWindowSmoothing(
cumulEle -= this.trkpt[i].ele ?? 0; 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;
}
} }
for (let i = currentEnd; i <= e; i++) { );
cumulEle += this.trkpt[i].ele ?? 0; }
} if (statistics.global.length > 0) {
currentStart = s; statistics.local.data[statistics.global.length - 1].elevation.gain =
currentEnd = e + 1; statistics.global.elevation.gain;
return cumulEle / (e - s + 1); statistics.local.data[statistics.global.length - 1].elevation.loss =
}); statistics.global.elevation.loss;
smoothedEle[0] = this.trkpt[start].ele ?? 0;
smoothedEle[smoothedEle.length - 1] = this.trkpt[end].ele ?? 0;
for (let j = start; j < end; j++) {
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
const ele = smoothedEle[j - start + 1] - smoothedEle[j - start];
if (ele > 0) {
statistics.global.elevation.gain += ele;
} else if (ele < 0) {
statistics.global.elevation.loss -= ele;
}
}
} }
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
let slope = [];
let length = [];
for (let i = 0; i < simplified.length - 1; i++) { for (let i = 0; i < simplified.length - 1; i++) {
let start = simplified[i].point._data.index; let start = simplified[i].point._data.index;
let end = simplified[i + 1].point._data.index; let end = simplified[i + 1].point._data.index;
let dist = let dist =
statistics.local.distance.total[end] - statistics.local.distance.total[start]; statistics.local.data[end].distance.total -
statistics.local.data[start].distance.total;
let ele = (simplified[i + 1].point.ele ?? 0) - (simplified[i].point.ele ?? 0); let ele = (simplified[i + 1].point.ele ?? 0) - (simplified[i].point.ele ?? 0);
for (let j = start; j < end + (i + 1 === simplified.length - 1 ? 1 : 0); j++) { for (let j = start; j < end + (i + 1 === simplified.length - 1 ? 1 : 0); j++) {
slope.push((0.1 * ele) / dist); statistics.local.data[j].slope.segment = (0.1 * ele) / dist;
length.push(dist); statistics.local.data[j].slope.length = dist;
} }
} }
statistics.local.slope.segment = slope; distanceWindowSmoothing(
statistics.local.slope.length = length;
statistics.local.slope.at = distanceWindowSmoothing(
0, 0,
this.trkpt.length, this.trkpt.length,
statistics, statistics,
@@ -1038,8 +1062,12 @@ export class TrackSegment extends GPXTreeLeaf {
(start, end) => { (start, end) => {
const ele = this.trkpt[end].ele - this.trkpt[start].ele || 0; const ele = this.trkpt[end].ele - this.trkpt[start].ele || 0;
const dist = const dist =
statistics.local.distance.total[end] - statistics.local.distance.total[start]; statistics.local.data[end].distance.total -
statistics.local.data[start].distance.total;
return dist > 0 ? (0.1 * ele) / dist : 0; return dist > 0 ? (0.1 * ele) / dist : 0;
},
(value, index) => {
statistics.local.data[index].slope.at = value;
} }
); );
} }
@@ -1289,13 +1317,7 @@ export class TrackSegment extends GPXTreeLeaf {
) { ) {
let og = getOriginal(this); // Read as much as possible from the original object because it is faster let og = getOriginal(this); // Read as much as possible from the original object because it is faster
let statistics = og._computeStatistics(); let statistics = og._computeStatistics();
let trkpt = withArtificialTimestamps( let trkpt = withArtificialTimestamps(og.trkpt, totalTime, lastPoint, startTime, statistics);
og.trkpt,
totalTime,
lastPoint,
startTime,
statistics.local.slope.at
);
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
} }
@@ -1304,6 +1326,7 @@ export class TrackSegment extends GPXTreeLeaf {
} }
} }
const emptyExtensions: Record<string, string> = {};
export class TrackPoint { export class TrackPoint {
[immerable] = true; [immerable] = true;
@@ -1375,10 +1398,7 @@ export class TrackPoint {
: undefined; : undefined;
} }
setExtensions(extensions: Record<string, string>) { setExtension(key: string, value: string) {
if (Object.keys(extensions).length === 0) {
return;
}
if (!this.extensions) { if (!this.extensions) {
this.extensions = {}; this.extensions = {};
} }
@@ -1388,8 +1408,12 @@ export class TrackPoint {
if (!this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']) { if (!this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']) {
this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions'] = {}; this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions'] = {};
} }
this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions'][key] = value;
}
setExtensions(extensions: Record<string, string>) {
Object.entries(extensions).forEach(([key, value]) => { Object.entries(extensions).forEach(([key, value]) => {
this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions'][key] = value; this.setExtension(key, value);
}); });
} }
@@ -1398,7 +1422,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 {
@@ -1619,305 +1643,6 @@ export class Waypoint {
} }
} }
export class GPXStatistics {
global: {
distance: {
moving: number;
total: number;
};
time: {
start: Date | undefined;
end: Date | undefined;
moving: number;
total: number;
};
speed: {
moving: number;
total: number;
};
elevation: {
gain: number;
loss: number;
};
bounds: {
southWest: Coordinates;
northEast: Coordinates;
};
atemp: {
avg: number;
count: number;
};
hr: {
avg: number;
count: number;
};
cad: {
avg: number;
count: number;
};
power: {
avg: number;
count: number;
};
extensions: Record<string, Record<string, number>>;
};
local: {
points: TrackPoint[];
distance: {
moving: number[];
total: number[];
};
time: {
moving: number[];
total: number[];
};
speed: number[];
elevation: {
gain: number[];
loss: number[];
};
slope: {
at: number[];
segment: number[];
length: number[];
};
};
constructor() {
this.global = {
distance: {
moving: 0,
total: 0,
},
time: {
start: undefined,
end: undefined,
moving: 0,
total: 0,
},
speed: {
moving: 0,
total: 0,
},
elevation: {
gain: 0,
loss: 0,
},
bounds: {
southWest: {
lat: 90,
lon: 180,
},
northEast: {
lat: -90,
lon: -180,
},
},
atemp: {
avg: 0,
count: 0,
},
hr: {
avg: 0,
count: 0,
},
cad: {
avg: 0,
count: 0,
},
power: {
avg: 0,
count: 0,
},
extensions: {},
};
this.local = {
points: [],
distance: {
moving: [],
total: [],
},
time: {
moving: [],
total: [],
},
speed: [],
elevation: {
gain: [],
loss: [],
},
slope: {
at: [],
segment: [],
length: [],
},
};
}
mergeWith(other: GPXStatistics): void {
this.local.points = this.local.points.concat(other.local.points);
this.local.distance.total = this.local.distance.total.concat(
other.local.distance.total.map((distance) => distance + this.global.distance.total)
);
this.local.distance.moving = this.local.distance.moving.concat(
other.local.distance.moving.map((distance) => distance + this.global.distance.moving)
);
this.local.time.total = this.local.time.total.concat(
other.local.time.total.map((time) => time + this.global.time.total)
);
this.local.time.moving = this.local.time.moving.concat(
other.local.time.moving.map((time) => time + this.global.time.moving)
);
this.local.elevation.gain = this.local.elevation.gain.concat(
other.local.elevation.gain.map((gain) => gain + this.global.elevation.gain)
);
this.local.elevation.loss = this.local.elevation.loss.concat(
other.local.elevation.loss.map((loss) => loss + this.global.elevation.loss)
);
this.local.speed = this.local.speed.concat(other.local.speed);
this.local.slope.at = this.local.slope.at.concat(other.local.slope.at);
this.local.slope.segment = this.local.slope.segment.concat(other.local.slope.segment);
this.local.slope.length = this.local.slope.length.concat(other.local.slope.length);
this.global.distance.total += other.global.distance.total;
this.global.distance.moving += other.global.distance.moving;
this.global.time.start =
this.global.time.start !== undefined && other.global.time.start !== undefined
? new Date(
Math.min(this.global.time.start.getTime(), other.global.time.start.getTime())
)
: (this.global.time.start ?? other.global.time.start);
this.global.time.end =
this.global.time.end !== undefined && other.global.time.end !== undefined
? new Date(
Math.max(this.global.time.end.getTime(), other.global.time.end.getTime())
)
: (this.global.time.end ?? other.global.time.end);
this.global.time.total += other.global.time.total;
this.global.time.moving += other.global.time.moving;
this.global.speed.moving =
this.global.time.moving > 0
? this.global.distance.moving / (this.global.time.moving / 3600)
: 0;
this.global.speed.total =
this.global.time.total > 0
? this.global.distance.total / (this.global.time.total / 3600)
: 0;
this.global.elevation.gain += other.global.elevation.gain;
this.global.elevation.loss += other.global.elevation.loss;
this.global.bounds.southWest.lat = Math.min(
this.global.bounds.southWest.lat,
other.global.bounds.southWest.lat
);
this.global.bounds.southWest.lon = Math.min(
this.global.bounds.southWest.lon,
other.global.bounds.southWest.lon
);
this.global.bounds.northEast.lat = Math.max(
this.global.bounds.northEast.lat,
other.global.bounds.northEast.lat
);
this.global.bounds.northEast.lon = Math.max(
this.global.bounds.northEast.lon,
other.global.bounds.northEast.lon
);
this.global.atemp.avg =
(this.global.atemp.count * this.global.atemp.avg +
other.global.atemp.count * other.global.atemp.avg) /
Math.max(1, this.global.atemp.count + other.global.atemp.count);
this.global.atemp.count += other.global.atemp.count;
this.global.hr.avg =
(this.global.hr.count * this.global.hr.avg +
other.global.hr.count * other.global.hr.avg) /
Math.max(1, this.global.hr.count + other.global.hr.count);
this.global.hr.count += other.global.hr.count;
this.global.cad.avg =
(this.global.cad.count * this.global.cad.avg +
other.global.cad.count * other.global.cad.avg) /
Math.max(1, this.global.cad.count + other.global.cad.count);
this.global.cad.count += other.global.cad.count;
this.global.power.avg =
(this.global.power.count * this.global.power.avg +
other.global.power.count * other.global.power.avg) /
Math.max(1, this.global.power.count + other.global.power.count);
this.global.power.count += other.global.power.count;
Object.keys(other.global.extensions).forEach((extension) => {
if (this.global.extensions[extension] === undefined) {
this.global.extensions[extension] = {};
}
Object.keys(other.global.extensions[extension]).forEach((value) => {
if (this.global.extensions[extension][value] === undefined) {
this.global.extensions[extension][value] = 0;
}
this.global.extensions[extension][value] +=
other.global.extensions[extension][value];
});
});
}
slice(start: number, end: number): GPXStatistics {
if (start < 0) {
start = 0;
} else if (start >= this.local.points.length) {
return new GPXStatistics();
}
if (end < start) {
return new GPXStatistics();
} else if (end >= this.local.points.length) {
end = this.local.points.length - 1;
}
let statistics = new GPXStatistics();
statistics.local.points = this.local.points.slice(start, end + 1);
statistics.global.distance.total =
this.local.distance.total[end] - this.local.distance.total[start];
statistics.global.distance.moving =
this.local.distance.moving[end] - this.local.distance.moving[start];
statistics.global.time.start = this.local.points[start].time;
statistics.global.time.end = this.local.points[end].time;
statistics.global.time.total = this.local.time.total[end] - this.local.time.total[start];
statistics.global.time.moving = this.local.time.moving[end] - this.local.time.moving[start];
statistics.global.speed.moving =
statistics.global.time.moving > 0
? statistics.global.distance.moving / (statistics.global.time.moving / 3600)
: 0;
statistics.global.speed.total =
statistics.global.time.total > 0
? statistics.global.distance.total / (statistics.global.time.total / 3600)
: 0;
statistics.global.elevation.gain =
this.local.elevation.gain[end] - this.local.elevation.gain[start];
statistics.global.elevation.loss =
this.local.elevation.loss[end] - this.local.elevation.loss[start];
statistics.global.bounds.southWest.lat = this.global.bounds.southWest.lat;
statistics.global.bounds.southWest.lon = this.global.bounds.southWest.lon;
statistics.global.bounds.northEast.lat = this.global.bounds.northEast.lat;
statistics.global.bounds.northEast.lon = this.global.bounds.northEast.lon;
statistics.global.atemp = this.global.atemp;
statistics.global.hr = this.global.hr;
statistics.global.cad = this.global.cad;
statistics.global.power = this.global.power;
return statistics;
}
}
const earthRadius = 6371008.8; const earthRadius = 6371008.8;
export function distance( export function distance(
coord1: TrackPoint | Coordinates, coord1: TrackPoint | Coordinates,
@@ -1951,9 +1676,9 @@ export function getElevationDistanceFunction(statistics: GPXStatistics) {
if (point1.ele === undefined || point2.ele === undefined || point3.ele === undefined) { if (point1.ele === undefined || point2.ele === undefined || point3.ele === undefined) {
return 0; return 0;
} }
let x1 = statistics.local.distance.total[point1._data.index] * 1000; let x1 = statistics.local.data[point1._data.index].distance.total * 1000;
let x2 = statistics.local.distance.total[point2._data.index] * 1000; let x2 = statistics.local.data[point2._data.index].distance.total * 1000;
let x3 = statistics.local.distance.total[point3._data.index] * 1000; let x3 = statistics.local.data[point3._data.index].distance.total * 1000;
let y1 = point1.ele; let y1 = point1.ele;
let y2 = point2.ele; let y2 = point2.ele;
let y3 = point3.ele; let y3 = point3.ele;
@@ -1972,10 +1697,9 @@ function windowSmoothing(
right: number, right: number,
distance: (index1: number, index2: number) => number, distance: (index1: number, index2: number) => number,
window: number, window: number,
compute: (start: number, end: number) => number compute: (start: number, end: number) => number,
): number[] { callback: (value: number, index: number) => void
let result = []; ): void {
let start = left; let start = left;
for (var i = left; i < right; i++) { for (var i = left; i < right; i++) {
while (start + 1 < i && distance(start, i) > window) { while (start + 1 < i && distance(start, i) > window) {
@@ -1985,10 +1709,8 @@ function windowSmoothing(
while (end < right && distance(i, end) <= window) { while (end < right && distance(i, end) <= window) {
end++; end++;
} }
result.push(compute(start, end - 1)); callback(compute(start, end - 1), i);
} }
return result;
} }
function distanceWindowSmoothing( function distanceWindowSmoothing(
@@ -1996,30 +1718,35 @@ function distanceWindowSmoothing(
right: number, right: number,
statistics: GPXStatistics, statistics: GPXStatistics,
window: number, window: number,
compute: (start: number, end: number) => number compute: (start: number, end: number) => number,
): number[] { callback: (value: number, index: number) => void
return windowSmoothing( ): void {
windowSmoothing(
left, left,
right, right,
(index1, index2) => (index1, index2) =>
statistics.local.distance.total[index2] - statistics.local.distance.total[index1], statistics.local.data[index2].distance.total -
statistics.local.data[index1].distance.total,
window, window,
compute compute,
callback
); );
} }
function timeWindowSmoothing( function timeWindowSmoothing(
points: TrackPoint[], points: TrackPoint[],
window: number, window: number,
compute: (start: number, end: number) => number compute: (start: number, end: number) => number,
): number[] { callback: (value: number, index: number) => void
return windowSmoothing( ): void {
windowSmoothing(
0, 0,
points.length, points.length,
(index1, index2) => (index1, index2) =>
points[index2].time?.getTime() - points[index1].time?.getTime() || 2 * window, points[index2].time?.getTime() - points[index1].time?.getTime() || 2 * window,
window, window,
compute compute,
callback
); );
} }
@@ -2071,14 +1798,14 @@ function withArtificialTimestamps(
totalTime: number, totalTime: number,
lastPoint: TrackPoint | undefined, lastPoint: TrackPoint | undefined,
startTime: Date, startTime: Date,
slope: number[] statistics: GPXStatistics
): TrackPoint[] { ): TrackPoint[] {
let weight = []; let weight = [];
let totalWeight = 0; let totalWeight = 0;
for (let i = 0; i < points.length - 1; i++) { for (let i = 0; i < points.length - 1; i++) {
let dist = distance(points[i].getCoordinates(), points[i + 1].getCoordinates()); let dist = distance(points[i].getCoordinates(), points[i + 1].getCoordinates());
let w = dist * (0.5 + 1 / (1 + Math.exp(-0.2 * slope[i]))); let w = dist * (0.5 + 1 / (1 + Math.exp(-0.2 * statistics.local.data[i].slope.at)));
weight.push(w); weight.push(w);
totalWeight += w; totalWeight += w;
} }

View File

@@ -1,4 +1,5 @@
export * from './gpx'; export * from './gpx';
export * from './statistics';
export { Coordinates, LineStyleExtension, WaypointType } from './types'; export { Coordinates, LineStyleExtension, WaypointType } from './types';
export { parseGPX, buildGPX } from './io'; export { parseGPX, buildGPX } from './io';
export * from './simplify'; export * from './simplify';

391
gpx/src/statistics.ts Normal file
View File

@@ -0,0 +1,391 @@
import { TrackPoint } from './gpx';
import { Coordinates } from './types';
export class GPXGlobalStatistics {
length: number;
distance: {
moving: number;
total: number;
};
time: {
start: Date | undefined;
end: Date | undefined;
moving: number;
total: number;
};
speed: {
moving: number;
total: number;
};
elevation: {
gain: number;
loss: number;
};
bounds: {
southWest: Coordinates;
northEast: Coordinates;
};
atemp: {
avg: number;
count: number;
};
hr: {
avg: number;
count: number;
};
cad: {
avg: number;
count: number;
};
power: {
avg: number;
count: number;
};
extensions: Record<string, Record<string, number>>;
constructor() {
this.length = 0;
this.distance = {
moving: 0,
total: 0,
};
this.time = {
start: undefined,
end: undefined,
moving: 0,
total: 0,
};
this.speed = {
moving: 0,
total: 0,
};
this.elevation = {
gain: 0,
loss: 0,
};
this.bounds = {
southWest: {
lat: 90,
lon: 180,
},
northEast: {
lat: -90,
lon: -180,
},
};
this.atemp = {
avg: 0,
count: 0,
};
this.hr = {
avg: 0,
count: 0,
};
this.cad = {
avg: 0,
count: 0,
};
this.power = {
avg: 0,
count: 0,
};
this.extensions = {};
}
mergeWith(other: GPXGlobalStatistics): void {
this.length += other.length;
this.distance.total += other.distance.total;
this.distance.moving += other.distance.moving;
this.time.start =
this.time.start !== undefined && other.time.start !== undefined
? new Date(Math.min(this.time.start.getTime(), other.time.start.getTime()))
: (this.time.start ?? other.time.start);
this.time.end =
this.time.end !== undefined && other.time.end !== undefined
? new Date(Math.max(this.time.end.getTime(), other.time.end.getTime()))
: (this.time.end ?? other.time.end);
this.time.total += other.time.total;
this.time.moving += other.time.moving;
this.speed.moving =
this.time.moving > 0 ? this.distance.moving / (this.time.moving / 3600) : 0;
this.speed.total = this.time.total > 0 ? this.distance.total / (this.time.total / 3600) : 0;
this.elevation.gain += other.elevation.gain;
this.elevation.loss += other.elevation.loss;
this.bounds.southWest.lat = Math.min(this.bounds.southWest.lat, other.bounds.southWest.lat);
this.bounds.southWest.lon = Math.min(this.bounds.southWest.lon, other.bounds.southWest.lon);
this.bounds.northEast.lat = Math.max(this.bounds.northEast.lat, other.bounds.northEast.lat);
this.bounds.northEast.lon = Math.max(this.bounds.northEast.lon, other.bounds.northEast.lon);
this.atemp.avg =
(this.atemp.count * this.atemp.avg + other.atemp.count * other.atemp.avg) /
Math.max(1, this.atemp.count + other.atemp.count);
this.atemp.count += other.atemp.count;
this.hr.avg =
(this.hr.count * this.hr.avg + other.hr.count * other.hr.avg) /
Math.max(1, this.hr.count + other.hr.count);
this.hr.count += other.hr.count;
this.cad.avg =
(this.cad.count * this.cad.avg + other.cad.count * other.cad.avg) /
Math.max(1, this.cad.count + other.cad.count);
this.cad.count += other.cad.count;
this.power.avg =
(this.power.count * this.power.avg + other.power.count * other.power.avg) /
Math.max(1, this.power.count + other.power.count);
this.power.count += other.power.count;
Object.keys(other.extensions).forEach((extension) => {
if (this.extensions[extension] === undefined) {
this.extensions[extension] = {};
}
Object.keys(other.extensions[extension]).forEach((value) => {
if (this.extensions[extension][value] === undefined) {
this.extensions[extension][value] = 0;
}
this.extensions[extension][value] += other.extensions[extension][value];
});
});
}
}
export class TrackPointLocalStatistics {
distance: {
moving: number;
total: number;
};
time: {
moving: number;
total: number;
};
speed: number;
elevation: {
gain: number;
loss: number;
};
slope: {
at: number;
segment: number;
length: number;
};
constructor() {
this.distance = {
moving: 0,
total: 0,
};
this.time = {
moving: 0,
total: 0,
};
this.speed = 0;
this.elevation = {
gain: 0,
loss: 0,
};
this.slope = {
at: 0,
segment: 0,
length: 0,
};
}
}
export class GPXLocalStatistics {
points: TrackPoint[];
data: TrackPointLocalStatistics[];
constructor() {
this.points = [];
this.data = [];
}
}
export type TrackPointWithLocalStatistics = {
trkpt: TrackPoint;
} & TrackPointLocalStatistics;
export class GPXStatistics {
global: GPXGlobalStatistics;
local: GPXLocalStatistics;
constructor() {
this.global = new GPXGlobalStatistics();
this.local = new GPXLocalStatistics();
}
sliced(start: number, end: number): GPXGlobalStatistics {
if (start < 0) {
start = 0;
} else if (start >= this.global.length) {
return new GPXGlobalStatistics();
}
if (end < start) {
return new GPXGlobalStatistics();
} else if (end >= this.global.length) {
end = this.global.length - 1;
}
if (start === 0 && end === this.global.length - 1) {
return this.global;
}
let statistics = new GPXGlobalStatistics();
statistics.length = end - start + 1;
statistics.distance.total =
this.local.data[end].distance.total - this.local.data[start].distance.total;
statistics.distance.moving =
this.local.data[end].distance.moving - this.local.data[start].distance.moving;
statistics.time.start = this.local.points[start].time;
statistics.time.end = this.local.points[end].time;
statistics.time.total = this.local.data[end].time.total - this.local.data[start].time.total;
statistics.time.moving =
this.local.data[end].time.moving - this.local.data[start].time.moving;
statistics.speed.moving =
statistics.time.moving > 0
? statistics.distance.moving / (statistics.time.moving / 3600)
: 0;
statistics.speed.total =
statistics.time.total > 0
? statistics.distance.total / (statistics.time.total / 3600)
: 0;
statistics.elevation.gain =
this.local.data[end].elevation.gain - this.local.data[start].elevation.gain;
statistics.elevation.loss =
this.local.data[end].elevation.loss - this.local.data[start].elevation.loss;
statistics.bounds.southWest.lat = this.global.bounds.southWest.lat;
statistics.bounds.southWest.lon = this.global.bounds.southWest.lon;
statistics.bounds.northEast.lat = this.global.bounds.northEast.lat;
statistics.bounds.northEast.lon = this.global.bounds.northEast.lon;
statistics.atemp = this.global.atemp;
statistics.hr = this.global.hr;
statistics.cad = this.global.cad;
statistics.power = this.global.power;
return statistics;
}
}
export class GPXStatisticsGroup {
private _statistics: GPXStatistics[];
private _cumulative: GPXGlobalStatistics[];
private _slice: [number, number] | null = null;
global: GPXGlobalStatistics;
constructor() {
this._statistics = [];
this._cumulative = [new GPXGlobalStatistics()];
this.global = new GPXGlobalStatistics();
}
add(statistics: GPXStatistics | GPXStatisticsGroup): void {
if (statistics instanceof GPXStatisticsGroup) {
statistics._statistics.forEach((stats) => this._add(stats));
} else {
this._add(statistics);
}
}
_add(statistics: GPXStatistics): void {
this._statistics.push(statistics);
const cumulative = new GPXGlobalStatistics();
cumulative.mergeWith(this._cumulative[this._cumulative.length - 1]);
cumulative.mergeWith(statistics.global);
this._cumulative.push(cumulative);
this.global.mergeWith(statistics.global);
}
sliced(start: number, end: number): GPXGlobalStatistics {
let sliced = new GPXGlobalStatistics();
for (let i = 0; i < this._statistics.length; i++) {
const statistics = this._statistics[i];
const cumulative = this._cumulative[i];
if (start < cumulative.length + statistics.global.length && end >= cumulative.length) {
const localStart = Math.max(0, start - cumulative.length);
const localEnd = Math.min(statistics.global.length - 1, end - cumulative.length);
sliced.mergeWith(statistics.sliced(localStart, localEnd));
}
}
return sliced;
}
getTrackPoint(index: number): TrackPointWithLocalStatistics | undefined {
if (this._slice !== null) {
index += this._slice[0];
}
for (let i = 0; i < this._statistics.length; i++) {
const statistics = this._statistics[i];
const cumulative = this._cumulative[i];
if (index < cumulative.length + statistics.global.length) {
return this._getTrackPoint(cumulative, statistics, index - cumulative.length);
}
}
return undefined;
}
_getTrackPoint(
cumulative: GPXGlobalStatistics,
statistics: GPXStatistics,
index: number
): TrackPointWithLocalStatistics {
const point = statistics.local.points[index];
return {
trkpt: point,
distance: {
moving: statistics.local.data[index].distance.moving + cumulative.distance.moving,
total: statistics.local.data[index].distance.total + cumulative.distance.total,
},
time: {
moving: statistics.local.data[index].time.moving + cumulative.time.moving,
total: statistics.local.data[index].time.total + cumulative.time.total,
},
speed: statistics.local.data[index].speed,
elevation: {
gain: statistics.local.data[index].elevation.gain + cumulative.elevation.gain,
loss: statistics.local.data[index].elevation.loss + cumulative.elevation.loss,
},
slope: {
at: statistics.local.data[index].slope.at,
segment: statistics.local.data[index].slope.segment,
length: statistics.local.data[index].slope.length,
},
};
}
forEachTrackPoint(
callback: (
point: TrackPoint,
distance: number,
speed: number,
slope: { at: number; segment: number; length: number },
index: number
) => void
): void {
for (let i = 0; i < this._statistics.length; i++) {
const statistics = this._statistics[i];
const cumulative = this._cumulative[i];
statistics.local.points.forEach((point, index) =>
callback(
point,
cumulative.distance.total + statistics.local.data[index].distance.total,
statistics.local.data[index].speed,
statistics.local.data[index].slope,
cumulative.length + index
)
);
}
}
}

View File

@@ -1,3 +0,0 @@
src/lib/components/ui
src/lib/docs/**/*.mdx
**/*.webmanifest

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 && eslint .", "lint": "prettier --check . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore && eslint .",
"format": "prettier --write . --config ../.prettierrc" "format": "prettier --write . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore"
}, },
"devDependencies": { "devDependencies": {
"@lucide/svelte": "^0.544.0", "@lucide/svelte": "^0.544.0",

View File

@@ -22,7 +22,7 @@ import {
Binoculars, Binoculars,
Toilet, Toilet,
} from 'lucide-static'; } from 'lucide-static';
import { type StyleSpecification } from 'mapbox-gl'; import { type RasterDEMSourceSpecification, 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';
@@ -368,6 +368,42 @@ 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: {
@@ -799,8 +835,10 @@ export const overlayTree: LayerTreeType = {
waymarkedTrailsHorseRiding: true, waymarkedTrailsHorseRiding: true,
waymarkedTrailsWinter: true, waymarkedTrailsWinter: true,
}, },
cyclOSMlite: true,
bikerouterGravel: true, bikerouterGravel: true,
cyclOSMlite: true,
mapterhornHillshade: true,
openRailwayMap: true,
}, },
countries: { countries: {
france: { france: {
@@ -883,8 +921,10 @@ export const defaultOverlays: LayerTreeType = {
waymarkedTrailsHorseRiding: false, waymarkedTrailsHorseRiding: false,
waymarkedTrailsWinter: false, waymarkedTrailsWinter: false,
}, },
cyclOSMlite: false,
bikerouterGravel: false, bikerouterGravel: false,
cyclOSMlite: false,
mapterhornHillshade: false,
openRailwayMap: false,
}, },
countries: { countries: {
france: { france: {
@@ -1018,8 +1058,10 @@ export const defaultOverlayTree: LayerTreeType = {
waymarkedTrailsHorseRiding: false, waymarkedTrailsHorseRiding: false,
waymarkedTrailsWinter: false, waymarkedTrailsWinter: false,
}, },
cyclOSMlite: false,
bikerouterGravel: false, bikerouterGravel: false,
cyclOSMlite: false,
mapterhornHillshade: false,
openRailwayMap: false,
}, },
countries: { countries: {
france: { france: {
@@ -1411,3 +1453,18 @@ 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

@@ -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 © 2025 gpx.studio MIT © 2026 gpx.studio
</Button> </Button>
<LanguageSelect class="w-40 mt-3" /> <LanguageSelect class="w-40 mt-3" />
</div> </div>

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 { GPXStatistics } from 'gpx'; import type { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
import type { Readable } from 'svelte/store'; import type { Readable } from 'svelte/store';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
@@ -18,14 +18,14 @@
orientation, orientation,
panelSize, panelSize,
}: { }: {
gpxStatistics: Readable<GPXStatistics>; gpxStatistics: Readable<GPXStatisticsGroup>;
slicedGPXStatistics: Readable<[GPXStatistics, number, number] | undefined>; slicedGPXStatistics: Readable<[GPXGlobalStatistics, number, number] | undefined>;
orientation: 'horizontal' | 'vertical'; orientation: 'horizontal' | 'vertical';
panelSize: number; panelSize: number;
} = $props(); } = $props();
let statistics = $derived( let statistics = $derived(
$slicedGPXStatistics !== undefined ? $slicedGPXStatistics[0] : $gpxStatistics $slicedGPXStatistics !== undefined ? $slicedGPXStatistics[0] : $gpxStatistics.global
); );
</script> </script>
@@ -42,15 +42,15 @@
<Tooltip label={i18n._('quantities.distance')}> <Tooltip label={i18n._('quantities.distance')}>
<span class="flex flex-row items-center"> <span class="flex flex-row items-center">
<Ruler size="16" class="mr-1" /> <Ruler size="16" class="mr-1" />
<WithUnits value={statistics.global.distance.total} type="distance" /> <WithUnits value={statistics.distance.total} type="distance" />
</span> </span>
</Tooltip> </Tooltip>
<Tooltip label={i18n._('quantities.elevation_gain_loss')}> <Tooltip label={i18n._('quantities.elevation_gain_loss')}>
<span class="flex flex-row items-center"> <span class="flex flex-row items-center">
<MoveUpRight size="16" class="mr-1" /> <MoveUpRight size="16" class="mr-1" />
<WithUnits value={statistics.global.elevation.gain} type="elevation" /> <WithUnits value={statistics.elevation.gain} type="elevation" />
<MoveDownRight size="16" class="mx-1" /> <MoveDownRight size="16" class="mx-1" />
<WithUnits value={statistics.global.elevation.loss} type="elevation" /> <WithUnits value={statistics.elevation.loss} type="elevation" />
</span> </span>
</Tooltip> </Tooltip>
{#if panelSize > 120 || orientation === 'horizontal'} {#if panelSize > 120 || orientation === 'horizontal'}
@@ -64,13 +64,9 @@
> >
<span class="flex flex-row items-center"> <span class="flex flex-row items-center">
<Zap size="16" class="mr-1" /> <Zap size="16" class="mr-1" />
<WithUnits <WithUnits value={statistics.speed.moving} type="speed" showUnits={false} />
value={statistics.global.speed.moving}
type="speed"
showUnits={false}
/>
<span class="mx-1">/</span> <span class="mx-1">/</span>
<WithUnits value={statistics.global.speed.total} type="speed" /> <WithUnits value={statistics.speed.total} type="speed" />
</span> </span>
</Tooltip> </Tooltip>
{/if} {/if}
@@ -83,9 +79,9 @@
> >
<span class="flex flex-row items-center"> <span class="flex flex-row items-center">
<Timer size="16" class="mr-1" /> <Timer size="16" class="mr-1" />
<WithUnits value={statistics.global.time.moving} type="time" /> <WithUnits value={statistics.time.moving} type="time" />
<span class="mx-1">/</span> <span class="mx-1">/</span>
<WithUnits value={statistics.global.time.total} type="time" /> <WithUnits value={statistics.time.total} type="time" />
</span> </span>
</Tooltip> </Tooltip>
{/if} {/if}

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 { GPXStatistics } from 'gpx'; import type { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { ElevationProfile } from '$lib/components/elevation-profile/elevation-profile'; import { ElevationProfile } from '$lib/components/elevation-profile/elevation-profile';
@@ -32,8 +32,8 @@
elevationFill, elevationFill,
showControls = true, showControls = true,
}: { }: {
gpxStatistics: Readable<GPXStatistics>; gpxStatistics: Readable<GPXStatisticsGroup>;
slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>; slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
additionalDatasets: Writable<string[]>; additionalDatasets: Writable<string[]>;
elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>; elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>;
showControls?: boolean; showControls?: boolean;

View File

@@ -23,7 +23,7 @@ import Chart, {
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import { get, type Readable, type Writable } from 'svelte/store'; import { get, type Readable, type Writable } from 'svelte/store';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import type { GPXStatistics } from 'gpx'; import type { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
import { mode } from 'mode-watcher'; import { mode } from 'mode-watcher';
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors'; import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
@@ -54,14 +54,14 @@ export class ElevationProfile {
private _dragging = false; private _dragging = false;
private _panning = false; private _panning = false;
private _gpxStatistics: Readable<GPXStatistics>; private _gpxStatistics: Readable<GPXStatisticsGroup>;
private _slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>; private _slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
private _additionalDatasets: Readable<string[]>; private _additionalDatasets: Readable<string[]>;
private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>; private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>;
constructor( constructor(
gpxStatistics: Readable<GPXStatistics>, gpxStatistics: Readable<GPXStatisticsGroup>,
slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>, slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>,
additionalDatasets: Readable<string[]>, additionalDatasets: Readable<string[]>,
elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>, elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>,
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
@@ -342,7 +342,7 @@ export class ElevationProfile {
if (evt.x - rect.left <= this._chart.chartArea.left) { if (evt.x - rect.left <= this._chart.chartArea.left) {
return 0; return 0;
} else if (evt.x - rect.left >= this._chart.chartArea.right) { } else if (evt.x - rect.left >= this._chart.chartArea.right) {
return get(this._gpxStatistics).local.points.length - 1; return this._chart.data.datasets[0].data.length - 1;
} else { } else {
return undefined; return undefined;
} }
@@ -375,7 +375,7 @@ export class ElevationProfile {
startIndex = endIndex; startIndex = endIndex;
} else if (startIndex !== endIndex) { } else if (startIndex !== endIndex) {
this._slicedGPXStatistics.set([ this._slicedGPXStatistics.set([
get(this._gpxStatistics).slice( get(this._gpxStatistics).sliced(
Math.min(startIndex, endIndex), Math.min(startIndex, endIndex),
Math.max(startIndex, endIndex) Math.max(startIndex, endIndex)
), ),
@@ -410,117 +410,89 @@ export class ElevationProfile {
velocity: get(velocityUnits), velocity: get(velocityUnits),
temperature: get(temperatureUnits), temperature: get(temperatureUnits),
}; };
const datasets: Array<Array<any>> = [[], [], [], [], [], []];
data.forEachTrackPoint((trkpt, distance, speed, slope, index) => {
datasets[0].push({
x: getConvertedDistance(distance, units.distance),
y: trkpt.ele ? getConvertedElevation(trkpt.ele, units.distance) : 0,
time: trkpt.time,
slope: slope,
extensions: trkpt.getExtensions(),
coordinates: trkpt.getCoordinates(),
index: index,
});
if (data.global.time.total > 0) {
datasets[1].push({
x: getConvertedDistance(distance, units.distance),
y: getConvertedVelocity(speed, units.velocity, units.distance),
index: index,
});
}
if (data.global.hr.count > 0) {
datasets[2].push({
x: getConvertedDistance(distance, units.distance),
y: trkpt.getHeartRate(),
index: index,
});
}
if (data.global.cad.count > 0) {
datasets[3].push({
x: getConvertedDistance(distance, units.distance),
y: trkpt.getCadence(),
index: index,
});
}
if (data.global.atemp.count > 0) {
datasets[4].push({
x: getConvertedDistance(distance, units.distance),
y: getConvertedTemperature(trkpt.getTemperature(), units.temperature),
index: index,
});
}
if (data.global.power.count > 0) {
datasets[5].push({
x: getConvertedDistance(distance, units.distance),
y: trkpt.getPower(),
index: index,
});
}
});
this._chart.data.datasets[0] = { this._chart.data.datasets[0] = {
label: i18n._('quantities.elevation'), label: i18n._('quantities.elevation'),
data: data.local.points.map((point, index) => { data: datasets[0],
return {
x: getConvertedDistance(data.local.distance.total[index], units.distance),
y: point.ele ? getConvertedElevation(point.ele, units.distance) : 0,
time: point.time,
slope: {
at: data.local.slope.at[index],
segment: data.local.slope.segment[index],
length: data.local.slope.length[index],
},
extensions: point.getExtensions(),
coordinates: point.getCoordinates(),
index: index,
};
}),
normalized: true, normalized: true,
fill: 'start', fill: 'start',
order: 1, order: 1,
segment: {}, segment: {},
}; };
this._chart.data.datasets[1] = { this._chart.data.datasets[1] = {
data: data: datasets[1],
data.global.time.total > 0
? data.local.points.map((point, index) => {
return {
x: getConvertedDistance(
data.local.distance.total[index],
units.distance
),
y: getConvertedVelocity(
data.local.speed[index],
units.velocity,
units.distance
),
index: index,
};
})
: [],
normalized: true, normalized: true,
yAxisID: 'yspeed', yAxisID: 'yspeed',
}; };
this._chart.data.datasets[2] = { this._chart.data.datasets[2] = {
data: data: datasets[2],
data.global.hr.count > 0
? data.local.points.map((point, index) => {
return {
x: getConvertedDistance(
data.local.distance.total[index],
units.distance
),
y: point.getHeartRate(),
index: index,
};
})
: [],
normalized: true, normalized: true,
yAxisID: 'yhr', yAxisID: 'yhr',
}; };
this._chart.data.datasets[3] = { this._chart.data.datasets[3] = {
data: data: datasets[3],
data.global.cad.count > 0
? data.local.points.map((point, index) => {
return {
x: getConvertedDistance(
data.local.distance.total[index],
units.distance
),
y: point.getCadence(),
index: index,
};
})
: [],
normalized: true, normalized: true,
yAxisID: 'ycad', yAxisID: 'ycad',
}; };
this._chart.data.datasets[4] = { this._chart.data.datasets[4] = {
data: data: datasets[4],
data.global.atemp.count > 0
? data.local.points.map((point, index) => {
return {
x: getConvertedDistance(
data.local.distance.total[index],
units.distance
),
y: getConvertedTemperature(point.getTemperature(), units.temperature),
index: index,
};
})
: [],
normalized: true, normalized: true,
yAxisID: 'yatemp', yAxisID: 'yatemp',
}; };
this._chart.data.datasets[5] = { this._chart.data.datasets[5] = {
data: data: datasets[5],
data.global.power.count > 0
? data.local.points.map((point, index) => {
return {
x: getConvertedDistance(
data.local.distance.total[index],
units.distance
),
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( this._chart.options.scales!.x!['max'] = getConvertedDistance(
data.global.distance.total, data.global.distance.total,
@@ -618,10 +590,12 @@ export class ElevationProfile {
const gpxStatistics = get(this._gpxStatistics); const gpxStatistics = get(this._gpxStatistics);
let startPixel = this._chart.scales.x.getPixelForValue( let startPixel = this._chart.scales.x.getPixelForValue(
getConvertedDistance(gpxStatistics.local.distance.total[startIndex]) getConvertedDistance(
gpxStatistics.getTrackPoint(startIndex)?.distance.total ?? 0
)
); );
let endPixel = this._chart.scales.x.getPixelForValue( let endPixel = this._chart.scales.x.getPixelForValue(
getConvertedDistance(gpxStatistics.local.distance.total[endIndex]) getConvertedDistance(gpxStatistics.getTrackPoint(endIndex)?.distance.total ?? 0)
); );
selectionContext.fillRect( selectionContext.fillRect(

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 { GPXStatistics } from 'gpx'; import { GPXGlobalStatistics } from 'gpx';
import { ListRootItem } from '$lib/components/file-list/file-list'; import { ListRootItem } from '$lib/components/file-list/file-list';
import { fileStateCollection } from '$lib/logic/file-state'; import { fileStateCollection } from '$lib/logic/file-state';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
@@ -48,24 +48,24 @@
extensions: false, extensions: false,
}; };
} else { } else {
let statistics = $gpxStatistics; let statistics = $gpxStatistics.global;
if (exportState.current === ExportState.ALL) { if (exportState.current === ExportState.ALL) {
statistics = Array.from(get(fileStateCollection).values()) statistics = Array.from(get(fileStateCollection).values())
.map((file) => file.statistics) .map((file) => file.statistics)
.reduce((acc, cur) => { .reduce((acc, cur) => {
if (cur !== undefined) { if (cur !== undefined) {
acc.mergeWith(cur.getStatisticsFor(new ListRootItem())); acc.mergeWith(cur.getStatisticsFor(new ListRootItem()).global);
} }
return acc; return acc;
}, new GPXStatistics()); }, new GPXGlobalStatistics());
} }
return { return {
time: statistics.global.time.total === 0, time: statistics.time.total === 0,
hr: statistics.global.hr.count === 0, hr: statistics.hr.count === 0,
cad: statistics.global.cad.count === 0, cad: statistics.cad.count === 0,
atemp: statistics.global.atemp.count === 0, atemp: statistics.atemp.count === 0,
power: statistics.global.power.count === 0, power: statistics.power.count === 0,
extensions: Object.keys(statistics.global.extensions).length === 0, extensions: Object.keys(statistics.extensions).length === 0,
}; };
} }
}); });

View File

@@ -72,17 +72,15 @@
} }
let style = node.getStyle(defaultColor); let style = node.getStyle(defaultColor);
style.color.forEach((c) => { colors = style.color;
if (!colors.includes(c)) {
colors.push(c);
}
});
} else if (node instanceof Track) { } else if (node instanceof Track) {
let style = node.getStyle(); let style = node.getStyle();
if (style) { if (
if (style['gpx_style:color'] && !colors.includes(style['gpx_style:color'])) { style &&
colors.push(style['gpx_style:color']); style['gpx_style:color'] &&
} !colors.includes(style['gpx_style:color'])
) {
colors.push(style['gpx_style:color']);
} }
if (colors.length === 0) { if (colors.length === 0) {
let layer = gpxLayers.getLayer(item.getFileId()); let layer = gpxLayers.getLayer(item.getFileId());

View File

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

View File

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

View File

@@ -13,6 +13,7 @@
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';
@@ -31,6 +32,7 @@
currentOverpassQueries, currentOverpassQueries,
customLayers, customLayers,
opacities, opacities,
terrainSource,
} = settings; } = settings;
const { isLayerFromExtension, getLayerName } = extensionAPI; const { isLayerFromExtension, getLayerName } = extensionAPI;
@@ -233,6 +235,23 @@
</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

@@ -3,8 +3,16 @@ 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 { treeFileView, elevationProfile, bottomPanelSize, rightPanelSize, distanceUnits } = settings; const {
treeFileView,
elevationProfile,
bottomPanelSize,
rightPanelSize,
distanceUnits,
terrainSource,
} = settings;
let fitBoundsOptions: mapboxgl.MapOptions['fitBoundsOptions'] = { let fitBoundsOptions: mapboxgl.MapOptions['fitBoundsOptions'] = {
maxZoom: 15, maxZoom: 15,
@@ -123,34 +131,14 @@ 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', () => { map.on('pitch', this.setTerrain.bind(this));
if (map.getPitch() > 0) { this.setTerrain();
map.setTerrain({
source: 'mapbox-dem',
exaggeration: 1,
});
} else {
map.setTerrain(null);
}
});
}); });
map.on('style.import.load', () => { map.on('style.import.load', () => {
const basemap = map.getStyle().imports?.find((imprt) => imprt.id === 'basemap'); const basemap = map.getStyle().imports?.find((imprt) => imprt.id === 'basemap');
@@ -162,6 +150,7 @@ export class MapboxGLMap {
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));
@@ -177,6 +166,7 @@ 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) {
@@ -217,6 +207,24 @@ export class MapboxGLMap {
} }
} }
} }
setTerrain() {
const map = get(this._map);
if (map) {
const source = get(terrainSource);
if (!map.getSource(source)) {
map.addSource(source, terrainSources[source]);
}
if (map.getPitch() > 0) {
map.setTerrain({
source: source,
exaggeration: 1,
});
} else {
map.setTerrain(null);
}
}
}
} }
export const map = new MapboxGLMap(); export const map = new MapboxGLMap();

View File

@@ -28,17 +28,15 @@ export class ReducedGPXLayer {
update() { update() {
const file = this._fileState.file; const file = this._fileState.file;
const stats = this._fileState.statistics; if (!file) {
if (!file || !stats) {
return; return;
} }
file.forEachSegment((segment, trackIndex, segmentIndex) => { file.forEachSegment((segment, trackIndex, segmentIndex) => {
let segmentItem = new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex); let segmentItem = new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex);
let statistics = stats.getStatisticsFor(segmentItem);
this._updateSimplified(segmentItem.getFullId(), [ this._updateSimplified(segmentItem.getFullId(), [
segmentItem, segmentItem,
statistics.local.points.length, segment.trkpt.length,
ramerDouglasPeucker(statistics.local.points, minTolerance), ramerDouglasPeucker(segment.trkpt, minTolerance),
]); ]);
}); });
} }

View File

@@ -21,7 +21,7 @@
SquareArrowUpLeft, SquareArrowUpLeft,
SquareArrowOutDownRight, SquareArrowOutDownRight,
} from '@lucide/svelte'; } from '@lucide/svelte';
import { brouterProfiles } from '$lib/components/toolbar/tools/routing/routing'; import { routingProfiles } from '$lib/components/toolbar/tools/routing/routing';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { import {
@@ -167,7 +167,7 @@
{i18n._(`toolbar.routing.activities.${$routingProfile}`)} {i18n._(`toolbar.routing.activities.${$routingProfile}`)}
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
{#each Object.keys(brouterProfiles) as profile} {#each Object.keys(routingProfiles) as profile}
<Select.Item value={profile} <Select.Item value={profile}
>{i18n._( >{i18n._(
`toolbar.routing.activities.${profile}` `toolbar.routing.activities.${profile}`

View File

@@ -793,24 +793,25 @@ export class RoutingControls {
replacingDistance += replacingDistance +=
distance(response[i - 1].getCoordinates(), response[i].getCoordinates()) / 1000; distance(response[i - 1].getCoordinates(), response[i].getCoordinates()) / 1000;
} }
let startAnchorStats = stats.getTrackPoint(anchors[0].point._data.index)!;
let endAnchorStats = stats.getTrackPoint(
anchors[anchors.length - 1].point._data.index
)!;
let replacedDistance = let replacedDistance =
stats.local.distance.moving[anchors[anchors.length - 1].point._data.index] - endAnchorStats.distance.moving - startAnchorStats.distance.moving;
stats.local.distance.moving[anchors[0].point._data.index];
let newDistance = stats.global.distance.moving + replacingDistance - replacedDistance; let newDistance = stats.global.distance.moving + replacingDistance - replacedDistance;
let newTime = (newDistance / stats.global.speed.moving) * 3600; let newTime = (newDistance / stats.global.speed.moving) * 3600;
let remainingTime = let remainingTime =
stats.global.time.moving - stats.global.time.moving -
(stats.local.time.moving[anchors[anchors.length - 1].point._data.index] - (endAnchorStats.time.moving - startAnchorStats.time.moving);
stats.local.time.moving[anchors[0].point._data.index]);
let replacingTime = newTime - remainingTime; let replacingTime = newTime - remainingTime;
if (replacingTime <= 0) { if (replacingTime <= 0) {
// Fallback to simple time difference // Fallback to simple time difference
replacingTime = replacingTime = endAnchorStats.time.total - startAnchorStats.time.total;
stats.local.time.total[anchors[anchors.length - 1].point._data.index] -
stats.local.time.total[anchors[0].point._data.index];
} }
speed = (replacingDistance / replacingTime) * 3600; speed = (replacingDistance / replacingTime) * 3600;
@@ -820,9 +821,7 @@ export class RoutingControls {
let endIndex = anchors[anchors.length - 1].point._data.index; let endIndex = anchors[anchors.length - 1].point._data.index;
startTime = new Date( startTime = new Date(
(segment.trkpt[endIndex].time?.getTime() ?? 0) - (segment.trkpt[endIndex].time?.getTime() ?? 0) -
(replacingTime + (replacingTime + endAnchorStats.time.total - endAnchorStats.time.moving) *
stats.local.time.total[endIndex] -
stats.local.time.moving[endIndex]) *
1000 1000
); );
} }

View File

@@ -6,35 +6,185 @@ import { get } from 'svelte/store';
const { routing, routingProfile, privateRoads } = settings; const { routing, routingProfile, privateRoads } = settings;
export const brouterProfiles: { [key: string]: string } = { export type RoutingProfile = {
bike: 'Trekking-dry', engine: 'graphhopper' | 'brouter';
racing_bike: 'fastbike', profile: string;
gravel_bike: 'gravel', };
mountain_bike: 'MTB',
foot: 'Hiking-Alpine-SAC6', export const routingProfiles: { [key: string]: RoutingProfile } = {
motorcycle: 'Car-FastEco', bike: { engine: 'graphhopper', profile: 'bike' },
water: 'river', racing_bike: { engine: 'graphhopper', profile: 'racingbike' },
railway: 'rail', gravel_bike: { engine: 'graphhopper', profile: 'gravelbike' },
mountain_bike: { engine: 'graphhopper', profile: 'mtb' },
foot: { engine: 'graphhopper', profile: 'foot' },
motorcycle: { engine: 'graphhopper', profile: 'motorcycle' },
water: { engine: 'brouter', profile: 'river' },
railway: { engine: 'brouter', profile: 'rail' },
}; };
export function route(points: Coordinates[]): Promise<TrackPoint[]> { export function route(points: Coordinates[]): Promise<TrackPoint[]> {
if (get(routing)) { if (get(routing)) {
return getRoute(points, brouterProfiles[get(routingProfile)], get(privateRoads)); const profile = routingProfiles[get(routingProfile)];
if (profile.engine === 'graphhopper') {
return getGraphHopperRoute(points, profile.profile, get(privateRoads));
} else {
return getBRouterRoute(points, profile.profile);
}
} else { } else {
return getIntermediatePoints(points); return getIntermediatePoints(points);
} }
} }
async function getRoute( const graphhopperDetails = ['road_class', 'surface', 'hike_rating', 'mtb_rating'];
const hikeRatingToSACScale: { [key: string]: string } = {
'1': 'hiking',
'2': 'mountain_hiking',
'3': 'demanding_mountain_hiking',
'4': 'alpine_hiking',
'5': 'demanding_alpine_hiking',
'6': 'difficult_alpine_hiking',
};
const mtbRatingToScale: { [key: string]: string } = {
'1': '0',
'2': '1',
'3': '2',
'4': '3',
'5': '4',
'6': '5',
'7': '6',
};
const graphhopperBlockPrivateCustomModels: { [key: string]: any } = {
bike: {
priority: [
{
if: 'bike_road_access == PRIVATE',
multiply_by: '0.0',
},
],
},
racingbike: {
priority: [
{
if: 'bike_road_access == PRIVATE',
multiply_by: '0.0',
},
],
},
gravelbike: {
priority: [
{
if: 'bike_road_access == PRIVATE',
multiply_by: '0.0',
},
],
},
mtb: {
priority: [
{
if: 'bike_road_access == PRIVATE',
multiply_by: '0.0',
},
],
},
foot: {
priority: [
{
if: 'foot_road_access == PRIVATE',
multiply_by: '0.0',
},
],
},
motorcycle: {
priority: [
{
if: 'road_access == PRIVATE',
multiply_by: '0.0',
},
],
},
};
async function getGraphHopperRoute(
points: Coordinates[], points: Coordinates[],
brouterProfile: string, graphHopperProfile: string,
privateRoads: boolean privateRoads: boolean
): Promise<TrackPoint[]> { ): Promise<TrackPoint[]> {
let url = `https://brouter.gpx.studio?lonlats=${points.map((point) => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile + (privateRoads ? '-private' : '')}&format=geojson&alternativeidx=0`; let response = await fetch('https://graphhopper-a.gpx.studio/route', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
points: points.map((point) => [point.lon, point.lat]),
profile: graphHopperProfile,
elevation: true,
points_encoded: false,
details: graphhopperDetails,
custom_model: privateRoads
? {}
: graphhopperBlockPrivateCustomModels[graphHopperProfile] || {},
}),
});
if (!response.ok) {
throw new Error(`${await response.text()}`);
}
let json = await response.json();
let route: TrackPoint[] = [];
let coordinates = json.paths[0].points.coordinates;
let details = json.paths[0].details;
for (let i = 0; i < coordinates.length; i++) {
route.push(
new TrackPoint({
attributes: {
lat: coordinates[i][1],
lon: coordinates[i][0],
},
ele: coordinates[i][2] ?? (i > 0 ? route[i - 1].ele : 0),
extensions: {},
})
);
}
for (let key of graphhopperDetails) {
let detail = details[key];
for (let i = 0; i < detail.length; i++) {
for (let j = detail[i][0]; j < detail[i][1] + (i == detail.length - 1); j++) {
if (detail[i][2] !== undefined && detail[i][2] !== 'missing') {
if (key === 'road_class') {
route[j].setExtension('highway', detail[i][2]);
} else if (key === 'hike_rating') {
const sacScale = hikeRatingToSACScale[detail[i][2]];
if (sacScale) {
route[j].setExtension('sac_scale', sacScale);
}
} else if (key === 'mtb_rating') {
const mtbScale = mtbRatingToScale[detail[i][2]];
if (mtbScale) {
route[j].setExtension('mtb_scale', mtbScale);
}
} else if (key === 'surface' && detail[i][2] !== 'other') {
route[j].setExtension('surface', detail[i][2]);
}
}
}
}
}
return route;
}
async function getBRouterRoute(
points: Coordinates[],
brouterProfile: string
): Promise<TrackPoint[]> {
let url = `https://brouter.de/brouter?lonlats=${points.map((point) => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile}&format=geojson&alternativeidx=0`;
let response = await fetch(url); let response = await fetch(url);
// Check if the response is ok
if (!response.ok) { if (!response.ok) {
throw new Error(`${await response.text()}`); throw new Error(`${await response.text()}`);
} }
@@ -52,14 +202,13 @@ async function getRoute(
let tags = messageIdx < messages.length ? getTags(messages[messageIdx][tagIdx]) : {}; let tags = messageIdx < messages.length ? getTags(messages[messageIdx][tagIdx]) : {};
for (let i = 0; i < coordinates.length; i++) { for (let i = 0; i < coordinates.length; i++) {
let coord = coordinates[i];
route.push( route.push(
new TrackPoint({ new TrackPoint({
attributes: { attributes: {
lat: coord[1], lat: coordinates[i][1],
lon: coord[0], lon: coordinates[i][0],
}, },
ele: coord[2] ?? (i > 0 ? route[i - 1].ele : 0), ele: coordinates[i][2] ?? (i > 0 ? route[i - 1].ele : 0),
}) })
); );

View File

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

View File

@@ -1,5 +1,5 @@
Mapbox ist das Unternehmen, das einige der schönen Karten auf dieser Website zur Verfügung stellt. 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> welche **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 ä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. Wir sind äußerst 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

@@ -215,7 +215,7 @@ export const fileActions = {
reverseSelection: () => { reverseSelection: () => {
if ( if (
!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) || !get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) ||
get(gpxStatistics).local.points?.length <= 1 get(gpxStatistics).global.length <= 1
) { ) {
return; return;
} }
@@ -345,19 +345,20 @@ export const fileActions = {
let startTime: Date | undefined = undefined; let startTime: Date | undefined = undefined;
if (speed !== undefined) { if (speed !== undefined) {
if ( if (
statistics.local.points.length > 0 && statistics.global.length > 0 &&
statistics.local.points[0].time !== undefined statistics.getTrackPoint(0)!.trkpt.time !== undefined
) { ) {
startTime = statistics.local.points[0].time; startTime = statistics.getTrackPoint(0)!.trkpt.time;
} else { } else {
let index = statistics.local.points.findIndex( for (let i = 0; i < statistics.global.length; i++) {
(point) => point.time !== undefined const point = statistics.getTrackPoint(i)!;
); if (point.trkpt.time !== undefined) {
if (index !== -1 && statistics.local.points[index].time) { startTime = new Date(
startTime = new Date( point.trkpt.time.getTime() -
statistics.local.points[index].time.getTime() - (1000 * 3600 * point.distance.total) / speed
(1000 * 3600 * statistics.local.distance.total[index]) / speed );
); break;
}
} }
} }
} }

View File

@@ -8,6 +8,7 @@ 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';
@@ -154,6 +155,7 @@ 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, type Track } from 'gpx'; import { GPXFile, GPXStatistics, GPXStatisticsGroup, type Track } from 'gpx';
export class GPXStatisticsTree { export class GPXStatisticsTree {
level: ListLevel; level: ListLevel;
@@ -21,35 +21,26 @@ export class GPXStatisticsTree {
} }
} }
getStatisticsFor(item: ListItem): GPXStatistics { getStatisticsFor(item: ListItem): GPXStatisticsGroup {
let statistics = []; let statistics = new GPXStatisticsGroup();
let id = item.getIdAtLevel(this.level); let id = item.getIdAtLevel(this.level);
if (id === undefined || id === 'waypoints') { if (id === undefined || id === 'waypoints') {
Object.keys(this.statistics).forEach((key) => { Object.keys(this.statistics).forEach((key) => {
if (this.statistics[key] instanceof GPXStatistics) { if (this.statistics[key] instanceof GPXStatistics) {
statistics.push(this.statistics[key]); statistics.add(this.statistics[key]);
} else { } else {
statistics.push(this.statistics[key].getStatisticsFor(item)); statistics.add(this.statistics[key].getStatisticsFor(item));
} }
}); });
} else { } else {
let child = this.statistics[id]; let child = this.statistics[id];
if (child instanceof GPXStatistics) { if (child instanceof GPXStatistics) {
statistics.push(child); statistics.add(child);
} else if (child !== undefined) { } else if (child !== undefined) {
statistics.push(child.getStatisticsFor(item)); statistics.add(child.getStatisticsFor(item));
} }
} }
if (statistics.length === 0) { return statistics;
return new GPXStatistics();
} else if (statistics.length === 1) {
return statistics[0];
} else {
return statistics.reduce((acc, curr) => {
acc.mergeWith(curr);
return acc;
}, new GPXStatistics());
}
} }
} }
export type GPXFileWithStatistics = { file: GPXFile; statistics: GPXStatisticsTree }; export type GPXFileWithStatistics = { file: GPXFile; statistics: GPXStatisticsTree };

View File

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

View File

@@ -282,6 +282,7 @@
"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",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"update": "Actualitza la capa" "update": "Actualitza la capa"
}, },
"opacity": "Opacitat de la superposició", "opacity": "Opacitat de la superposició",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Mapes base", "basemaps": "Mapes base",
"overlays": "Capes", "overlays": "Capes",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"update": "Aktualizovat vrstvu" "update": "Aktualizovat vrstvu"
}, },
"opacity": "Průhlednost překryvu", "opacity": "Průhlednost překryvu",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Základní mapy", "basemaps": "Základní mapy",
"overlays": "Překrytí", "overlays": "Překrytí",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"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",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"update": "Layer aktualisieren" "update": "Layer aktualisieren"
}, },
"opacity": "Deckkraft der Überlagerung", "opacity": "Deckkraft der Überlagerung",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Basiskarte", "basemaps": "Basiskarte",
"overlays": "Ebenen", "overlays": "Ebenen",
@@ -325,6 +326,8 @@
"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 Neigung", "swisstopoSlope": "swisstopo Neigung",
"swisstopoHiking": "swisstopo Wandern", "swisstopoHiking": "swisstopo Wandern",
"swisstopoHikingClosures": "swisstopo Wanderungen Schließungen", "swisstopoHikingClosures": "swisstopo Wanderungen Schließungen",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"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",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"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",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"update": "Actualizar capa" "update": "Actualizar capa"
}, },
"opacity": "Opacidad de la capa superpuesta", "opacity": "Opacidad de la capa superpuesta",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Mapas base", "basemaps": "Mapas base",
"overlays": "Capas", "overlays": "Capas",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"update": "Eguneratu geruza" "update": "Eguneratu geruza"
}, },
"opacity": "Geruzaren opakutasuna", "opacity": "Geruzaren opakutasuna",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Oinarrizko mapak", "basemaps": "Oinarrizko mapak",
"overlays": "Geruzak", "overlays": "Geruzak",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"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",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"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",
@@ -325,6 +326,8 @@
"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",
@@ -350,7 +353,7 @@
"eat-and-drink": "Nourriture et boissons", "eat-and-drink": "Nourriture et boissons",
"amenities": "Commodités", "amenities": "Commodités",
"toilets": "Toilettes", "toilets": "Toilettes",
"water": "Cours d'eau", "water": "Eau potable",
"shower": "Douche", "shower": "Douche",
"shelter": "Abri", "shelter": "Abri",
"cemetery": "Cimetière", "cemetery": "Cimetière",
@@ -377,7 +380,9 @@
"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": {
@@ -443,7 +448,7 @@
"convenience_store": "Épicerie", "convenience_store": "Épicerie",
"crossing": "Croisement", "crossing": "Croisement",
"department_store": "Grand magasin", "department_store": "Grand magasin",
"drinking_water": "Cours d'eau", "drinking_water": "Eau potable",
"exit": "Sortie", "exit": "Sortie",
"lodge": "Refuge", "lodge": "Refuge",
"lodging": "Hébergement", "lodging": "Hébergement",
@@ -468,7 +473,7 @@
"summit": "Sommet", "summit": "Sommet",
"telephone": "Téléphone", "telephone": "Téléphone",
"tunnel": "Tunnel", "tunnel": "Tunnel",
"water_source": "Source d'eau" "water_source": "Point d'eau"
} }
}, },
"homepage": { "homepage": {

View File

@@ -282,6 +282,7 @@
"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",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"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",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"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",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"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",
@@ -325,6 +326,8 @@
"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 Pendenza", "swisstopoSlope": "swisstopo Pendenza",
"swisstopoHiking": "swisstopo Escursione", "swisstopoHiking": "swisstopo Escursione",
"swisstopoHikingClosures": "swisstopo Fine escursione", "swisstopoHikingClosures": "swisstopo Fine escursione",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"update": "레이어 갱신" "update": "레이어 갱신"
}, },
"opacity": "오버레이 투명도", "opacity": "오버레이 투명도",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "배경 지도", "basemaps": "배경 지도",
"overlays": "오버레이", "overlays": "오버레이",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"railway-station": "철도역", "railway-station": "철도역",
"tram-stop": "트램 정류장", "tram-stop": "트램 정류장",
"bus-stop": "버스 정류장", "bus-stop": "버스 정류장",
"ferry": "페리" "ferry": "페리",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,6 +282,7 @@
"update": "Naujinti sluoksnį" "update": "Naujinti sluoksnį"
}, },
"opacity": "Sluoksnio skaidrumas", "opacity": "Sluoksnio skaidrumas",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Pagrindo žemėlapiai", "basemaps": "Pagrindo žemėlapiai",
"overlays": "Sluoksniai", "overlays": "Sluoksniai",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"railway-station": "Geležinkelio stotis", "railway-station": "Geležinkelio stotis",
"tram-stop": "Tramvajaus stotelė", "tram-stop": "Tramvajaus stotelė",
"bus-stop": "Autobusų stotelė", "bus-stop": "Autobusų stotelė",
"ferry": "Keltas" "ferry": "Keltas",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,6 +282,7 @@
"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",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"update": "Update laag" "update": "Update laag"
}, },
"opacity": "Laag Transparantie", "opacity": "Laag Transparantie",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Basis kaarten", "basemaps": "Basis kaarten",
"overlays": "Lagen", "overlays": "Lagen",
@@ -325,6 +326,8 @@
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Grind", "bikerouterGravel": "bikerouter.de Grind",
"cyclOSMlite": "CyclOSM Lite", "cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Helling", "swisstopoSlope": "swisstopo Helling",
"swisstopoHiking": "swisstopo Wandelen", "swisstopoHiking": "swisstopo Wandelen",
"swisstopoHikingClosures": "swisstopo Hiking Sluiting", "swisstopoHikingClosures": "swisstopo Hiking Sluiting",
@@ -377,7 +380,9 @@
"railway-station": "Treinstation", "railway-station": "Treinstation",
"tram-stop": "Tramhalte", "tram-stop": "Tramhalte",
"bus-stop": "Bushalte", "bus-stop": "Bushalte",
"ferry": "Veerboot" "ferry": "Veerboot",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,6 +282,7 @@
"update": "Oppdater lag" "update": "Oppdater lag"
}, },
"opacity": "Gjennomsiktighet for overlegg", "opacity": "Gjennomsiktighet for overlegg",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Basiskart", "basemaps": "Basiskart",
"overlays": "Overlag", "overlays": "Overlag",
@@ -325,6 +326,8 @@
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "sykkelrute Grus", "bikerouterGravel": "sykkelrute Grus",
"cyclOSMlite": "SyklOSM Lite", "cyclOSMlite": "SyklOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopografisk helningskart", "swisstopoSlope": "swisstopografisk helningskart",
"swisstopoHiking": "swisstopografisk Fottur", "swisstopoHiking": "swisstopografisk Fottur",
"swisstopoHikingClosures": "swisstopografi Stengte turstier", "swisstopoHikingClosures": "swisstopografi Stengte turstier",
@@ -377,7 +380,9 @@
"railway-station": "Jernbanestasjon", "railway-station": "Jernbanestasjon",
"tram-stop": "Trikkestopp", "tram-stop": "Trikkestopp",
"bus-stop": "Bussholdeplass", "bus-stop": "Bussholdeplass",
"ferry": "Ferge" "ferry": "Ferge",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,6 +282,7 @@
"update": "Zaktualizuj warstwę" "update": "Zaktualizuj warstwę"
}, },
"opacity": "Przezroczystość nakładki", "opacity": "Przezroczystość nakładki",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Mapy bazowe", "basemaps": "Mapy bazowe",
"overlays": "Nakładki", "overlays": "Nakładki",
@@ -325,6 +326,8 @@
"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 Stoki", "swisstopoSlope": "swisstopo Stoki",
"swisstopoHiking": "swisstopo Szlaki Turystyczne", "swisstopoHiking": "swisstopo Szlaki Turystyczne",
"swisstopoHikingClosures": "swisstopo Zamknięcia Szlaków", "swisstopoHikingClosures": "swisstopo Zamknięcia Szlaków",
@@ -377,7 +380,9 @@
"railway-station": "Stacja kolejowa", "railway-station": "Stacja kolejowa",
"tram-stop": "Przystanek tramwajowy", "tram-stop": "Przystanek tramwajowy",
"bus-stop": "Przystanek autobusowy", "bus-stop": "Przystanek autobusowy",
"ferry": "Prom" "ferry": "Prom",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,6 +282,7 @@
"update": "Atualizar camada" "update": "Atualizar camada"
}, },
"opacity": "Opacidade de sobreposição", "opacity": "Opacidade de sobreposição",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Mapa base", "basemaps": "Mapa base",
"overlays": "Sobreposições", "overlays": "Sobreposições",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"railway-station": "Estações ferroviárias", "railway-station": "Estações ferroviárias",
"tram-stop": "Parada de bonde", "tram-stop": "Parada de bonde",
"bus-stop": "Parada de Ônibus", "bus-stop": "Parada de Ônibus",
"ferry": "Balsa" "ferry": "Balsa",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,6 +282,7 @@
"update": "Atualizar camada" "update": "Atualizar camada"
}, },
"opacity": "Opacidade da sobreposição", "opacity": "Opacidade da sobreposição",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Mapas base", "basemaps": "Mapas base",
"overlays": "Sobreposições", "overlays": "Sobreposições",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"railway-station": "Estações ferroviárias", "railway-station": "Estações ferroviárias",
"tram-stop": "Parada de bonde", "tram-stop": "Parada de bonde",
"bus-stop": "Parada de Ônibus", "bus-stop": "Parada de Ônibus",
"ferry": "Balsa" "ferry": "Balsa",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,6 +282,7 @@
"update": "Actualizează stratul" "update": "Actualizează stratul"
}, },
"opacity": "Opacitatea overlay-ului", "opacity": "Opacitatea overlay-ului",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Hărți de bază", "basemaps": "Hărți de bază",
"overlays": "Suprapuneri", "overlays": "Suprapuneri",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"railway-station": "Gară", "railway-station": "Gară",
"tram-stop": "Tram Stop", "tram-stop": "Tram Stop",
"bus-stop": "Stație de autobuz", "bus-stop": "Stație de autobuz",
"ferry": "Feribot" "ferry": "Feribot",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,6 +282,7 @@
"update": "Обновить слой" "update": "Обновить слой"
}, },
"opacity": "Прозрачность наложения", "opacity": "Прозрачность наложения",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Основные карты", "basemaps": "Основные карты",
"overlays": "Наложения", "overlays": "Наложения",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"railway-station": "Железнодорожная станция", "railway-station": "Железнодорожная станция",
"tram-stop": "Трамвайная остановка", "tram-stop": "Трамвайная остановка",
"bus-stop": "Автобусная остановка", "bus-stop": "Автобусная остановка",
"ferry": "Паром" "ferry": "Паром",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,6 +282,7 @@
"update": "Ažurirajte sloj" "update": "Ažurirajte sloj"
}, },
"opacity": "Providnost preklapanja", "opacity": "Providnost preklapanja",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Osnovne mape", "basemaps": "Osnovne mape",
"overlays": "Preklapanja", "overlays": "Preklapanja",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"railway-station": "Železnička stanica", "railway-station": "Železnička stanica",
"tram-stop": "Tramvajsko stajalište", "tram-stop": "Tramvajsko stajalište",
"bus-stop": "Autobusko stajalište", "bus-stop": "Autobusko stajalište",
"ferry": "Trajekt" "ferry": "Trajekt",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,6 +282,7 @@
"update": "Update layer" "update": "Update layer"
}, },
"opacity": "Overlay opacity", "opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Baskartor", "basemaps": "Baskartor",
"overlays": "Lager", "overlays": "Lager",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"railway-station": "Järnvägsstation", "railway-station": "Järnvägsstation",
"tram-stop": "Spårvagnshållplats", "tram-stop": "Spårvagnshållplats",
"bus-stop": "Busshållplats", "bus-stop": "Busshållplats",
"ferry": "Ferry" "ferry": "Ferry",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,6 +282,7 @@
"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",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"update": "Katman güncelle" "update": "Katman güncelle"
}, },
"opacity": "Katman şeffaflığı", "opacity": "Katman şeffaflığı",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Temel haritalar", "basemaps": "Temel haritalar",
"overlays": "Katmanlar", "overlays": "Katmanlar",
@@ -325,6 +326,8 @@
"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 Eğim", "swisstopoSlope": "swisstopo Eğim",
"swisstopoHiking": "swisstopo Yürüyüş", "swisstopoHiking": "swisstopo Yürüyüş",
"swisstopoHikingClosures": "swisstopo Yürüyüş Sonu", "swisstopoHikingClosures": "swisstopo Yürüyüş Sonu",
@@ -377,7 +380,9 @@
"railway-station": "Tren istasyonu", "railway-station": "Tren istasyonu",
"tram-stop": "Tramvay Durağı", "tram-stop": "Tramvay Durağı",
"bus-stop": "Otobüs Durağı", "bus-stop": "Otobüs Durağı",
"ferry": "Feribot" "ferry": "Feribot",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,6 +282,7 @@
"update": "Оновити шар" "update": "Оновити шар"
}, },
"opacity": "Непрозорість накладання", "opacity": "Непрозорість накладання",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Базові карти", "basemaps": "Базові карти",
"overlays": "Накладання", "overlays": "Накладання",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"railway-station": "Залізнична Станція", "railway-station": "Залізнична Станція",
"tram-stop": "Трамвайна Зупинка", "tram-stop": "Трамвайна Зупинка",
"bus-stop": "Автобусна Зупинка", "bus-stop": "Автобусна Зупинка",
"ferry": "Пором" "ferry": "Пором",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,6 +282,7 @@
"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",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"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",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"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": {

View File

@@ -282,6 +282,7 @@
"update": "更新图层" "update": "更新图层"
}, },
"opacity": "图层透明度", "opacity": "图层透明度",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "底图", "basemaps": "底图",
"overlays": "叠加层", "overlays": "叠加层",
@@ -325,6 +326,8 @@
"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",
@@ -377,7 +380,9 @@
"railway-station": "火车站", "railway-station": "火车站",
"tram-stop": "有轨电车站", "tram-stop": "有轨电车站",
"bus-stop": "小型公交站台", "bus-stop": "小型公交站台",
"ferry": "渡口" "ferry": "渡口",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {