8 Commits

Author SHA1 Message Date
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
59 changed files with 990 additions and 662 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
Copyright (c) 2025 gpx.studio
Copyright (c) 2026 gpx.studio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

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

View File

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

391
gpx/src/statistics.ts Normal file
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",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . --config ../.prettierrc && eslint .",
"format": "prettier --write . --config ../.prettierrc"
"lint": "prettier --check . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore && eslint .",
"format": "prettier --write . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore"
},
"devDependencies": {
"@lucide/svelte": "^0.544.0",

View File

@@ -22,7 +22,7 @@ import {
Binoculars,
Toilet,
} 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 ignFrPlan from './custom/ign-fr-plan.json';
import ignFrSatellite from './custom/ign-fr-satellite.json';
@@ -368,6 +368,42 @@ export const overlays: { [key: string]: string | StyleSpecification } = {
],
},
bikerouterGravel: bikerouterGravel as StyleSpecification,
openRailwayMap: {
version: 8,
sources: {
openRailwayMap: {
type: 'raster',
tiles: ['https://tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 19,
attribution:
'Data <a href="https://www.openstreetmap.org/copyright">&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: {
version: 8,
sources: {
@@ -799,8 +835,10 @@ export const overlayTree: LayerTreeType = {
waymarkedTrailsHorseRiding: true,
waymarkedTrailsWinter: true,
},
cyclOSMlite: true,
bikerouterGravel: true,
cyclOSMlite: true,
mapterhornHillshade: true,
openRailwayMap: true,
},
countries: {
france: {
@@ -883,8 +921,10 @@ export const defaultOverlays: LayerTreeType = {
waymarkedTrailsHorseRiding: false,
waymarkedTrailsWinter: false,
},
cyclOSMlite: false,
bikerouterGravel: false,
cyclOSMlite: false,
mapterhornHillshade: false,
openRailwayMap: false,
},
countries: {
france: {
@@ -1018,8 +1058,10 @@ export const defaultOverlayTree: LayerTreeType = {
waymarkedTrailsHorseRiding: false,
waymarkedTrailsWinter: false,
},
cyclOSMlite: false,
bikerouterGravel: false,
cyclOSMlite: false,
mapterhornHillshade: false,
openRailwayMap: false,
},
countries: {
france: {
@@ -1411,3 +1453,18 @@ export const overpassQueryData: Record<string, OverpassQueryData> = {
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"
target="_blank"
>
MIT © 2025 gpx.studio
MIT © 2026 gpx.studio
</Button>
<LanguageSelect class="w-40 mt-3" />
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,6 +13,7 @@
overlays,
overlayTree,
overpassTree,
terrainSources,
} from '$lib/assets/layers';
import { getLayers, isSelected, toggle } from '$lib/components/map/layer-control/utils';
import { i18n } from '$lib/i18n.svelte';
@@ -31,6 +32,7 @@
currentOverpassQueries,
customLayers,
opacities,
terrainSource,
} = settings;
const { isLayerFromExtension, getLayerName } = extensionAPI;
@@ -233,6 +235,23 @@
</ScrollArea>
</Accordion.Content>
</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>
</ScrollArea>
</Sheet.Header>

View File

@@ -3,8 +3,16 @@ import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
import { get, writable, type Writable } from 'svelte/store';
import { settings } from '$lib/logic/settings';
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'] = {
maxZoom: 15,
@@ -123,34 +131,14 @@ export class MapboxGLMap {
});
map.addControl(scaleControl);
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({
color: 'rgb(186, 210, 235)',
'high-color': 'rgb(36, 92, 223)',
'horizon-blend': 0.1,
'space-color': 'rgb(156, 240, 255)',
});
map.on('pitch', () => {
if (map.getPitch() > 0) {
map.setTerrain({
source: 'mapbox-dem',
exaggeration: 1,
});
} else {
map.setTerrain(null);
}
});
map.on('pitch', this.setTerrain.bind(this));
this.setTerrain();
});
map.on('style.import.load', () => {
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
window._map = map; // entry point for extensions
this.resize();
this.setTerrain();
scaleControl.setUnit(get(distanceUnits));
this._onLoadCallbacks.forEach((callback) => callback(map));
@@ -177,6 +166,7 @@ export class MapboxGLMap {
scaleControl.setUnit(units);
})
);
this._unsubscribes.push(terrainSource.subscribe(() => this.setTerrain()));
}
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();

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
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.
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.

View File

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

View File

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

View File

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

View File

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

View File

@@ -282,6 +282,7 @@
"update": "Update layer"
},
"opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": {
"basemaps": "Basemaps",
"overlays": "Overlays",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Railway Station",
"tram-stop": "Tram Stop",
"bus-stop": "Bus Stop",
"ferry": "Ferry"
"ferry": "Ferry",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Actualitza la capa"
},
"opacity": "Opacitat de la superposició",
"terrain": "Terrain source",
"label": {
"basemaps": "Mapes base",
"overlays": "Capes",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Estació de tren",
"tram-stop": "Parada de tramvia",
"bus-stop": "Parada d'autobús",
"ferry": "Ferri"
"ferry": "Ferri",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Aktualizovat vrstvu"
},
"opacity": "Průhlednost překryvu",
"terrain": "Terrain source",
"label": {
"basemaps": "Základní mapy",
"overlays": "Překrytí",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Vrstevnice",
"swisstopoHiking": "swisstopo Turistická",
"swisstopoHikingClosures": "swisstopo Turistické uzávěry",
@@ -377,7 +380,9 @@
"railway-station": "Železniční stanice",
"tram-stop": "Zastávka tramvaje",
"bus-stop": "Autobusová zastávka",
"ferry": "Trajekt"
"ferry": "Trajekt",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Update layer"
},
"opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": {
"basemaps": "Basemaps",
"overlays": "Overlays",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Railway Station",
"tram-stop": "Tram Stop",
"bus-stop": "Bus Stop",
"ferry": "Ferry"
"ferry": "Ferry",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Layer aktualisieren"
},
"opacity": "Deckkraft der Überlagerung",
"terrain": "Terrain source",
"label": {
"basemaps": "Basiskarte",
"overlays": "Ebenen",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Neigung",
"swisstopoHiking": "swisstopo Wandern",
"swisstopoHikingClosures": "swisstopo Wanderungen Schließungen",
@@ -377,7 +380,9 @@
"railway-station": "Bahnhof",
"tram-stop": "Straßenbahnhaltestelle",
"bus-stop": "Bushaltestelle",
"ferry": "Fähre"
"ferry": "Fähre",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Update layer"
},
"opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": {
"basemaps": "Basemaps",
"overlays": "Overlays",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Railway Station",
"tram-stop": "Tram Stop",
"bus-stop": "Bus Stop",
"ferry": "Ferry"
"ferry": "Ferry",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Update layer"
},
"opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": {
"basemaps": "Basemaps",
"overlays": "Overlays",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Railway Station",
"tram-stop": "Tram Stop",
"bus-stop": "Bus Stop",
"ferry": "Ferry"
"ferry": "Ferry",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Actualizar capa"
},
"opacity": "Opacidad de la capa superpuesta",
"terrain": "Terrain source",
"label": {
"basemaps": "Mapas base",
"overlays": "Capas",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "Gravel bikerouter.de",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Senderismo",
"swisstopoHikingClosures": "swisstopo Rutas Senderismo",
@@ -377,7 +380,9 @@
"railway-station": "Estación de tren",
"tram-stop": "Parada de tranvía",
"bus-stop": "Parada de autobús",
"ferry": "Ferri"
"ferry": "Ferri",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Eguneratu geruza"
},
"opacity": "Geruzaren opakutasuna",
"terrain": "Terrain source",
"label": {
"basemaps": "Oinarrizko mapak",
"overlays": "Geruzak",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Malda",
"swisstopoHiking": "swisstopo Mendi ibilaldiak",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Tren geltokia",
"tram-stop": "Tranbia geltokia",
"bus-stop": "Autobus geltokia",
"ferry": "Ferria"
"ferry": "Ferria",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Päivitä karttataso"
},
"opacity": "Peitetason läpinäkyvyys",
"terrain": "Terrain source",
"label": {
"basemaps": "Taustakartat",
"overlays": "Peitetasot",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Rinnekaltevuus",
"swisstopoHiking": "swisstopo Retkeilyreitit",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Rautatieasemat",
"tram-stop": "Raitiovaunupysäkit",
"bus-stop": "Linja-autopysäkit",
"ferry": "Lautat"
"ferry": "Lautat",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Mettre à jour la couche"
},
"opacity": "Opacité de la surcouche",
"terrain": "Source du relief",
"label": {
"basemaps": "Fonds de carte",
"overlays": "Surcouches",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Relief",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Pente",
"swisstopoHiking": "swisstopo Randonnée",
"swisstopoHikingClosures": "swisstopo Fermetures de randonnée",
@@ -350,7 +353,7 @@
"eat-and-drink": "Nourriture et boissons",
"amenities": "Commodités",
"toilets": "Toilettes",
"water": "Cours d'eau",
"water": "Eau potable",
"shower": "Douche",
"shelter": "Abri",
"cemetery": "Cimetière",
@@ -377,7 +380,9 @@
"railway-station": "Gare",
"tram-stop": "Arrêt de tram",
"bus-stop": "Arrêt de bus",
"ferry": "Ferry"
"ferry": "Ferry",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {
@@ -443,7 +448,7 @@
"convenience_store": "Épicerie",
"crossing": "Croisement",
"department_store": "Grand magasin",
"drinking_water": "Cours d'eau",
"drinking_water": "Eau potable",
"exit": "Sortie",
"lodge": "Refuge",
"lodging": "Hébergement",
@@ -468,7 +473,7 @@
"summit": "Sommet",
"telephone": "Téléphone",
"tunnel": "Tunnel",
"water_source": "Source d'eau"
"water_source": "Point d'eau"
}
},
"homepage": {

View File

@@ -282,6 +282,7 @@
"update": "Update layer"
},
"opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": {
"basemaps": "Basemaps",
"overlays": "Overlays",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Railway Station",
"tram-stop": "Tram Stop",
"bus-stop": "Bus Stop",
"ferry": "Ferry"
"ferry": "Ferry",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Réteg feltöltése"
},
"opacity": "Átfedés átlátszósága",
"terrain": "Terrain source",
"label": {
"basemaps": "Alaptérkép",
"overlays": "Térkép rétegek",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "kerékpár és terepkerékpár út",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Lejtő",
"swisstopoHiking": "swisstopo Túra",
"swisstopoHikingClosures": "swisstopo túralezárások",
@@ -377,7 +380,9 @@
"railway-station": "Vasútállomás",
"tram-stop": "Villamos megálló",
"bus-stop": "Buszmegálló",
"ferry": "Komp"
"ferry": "Komp",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Perbarui lapisan"
},
"opacity": "Opasitas Overlay",
"terrain": "Terrain source",
"label": {
"basemaps": "Peta dasar",
"overlays": "Overlay",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Kemiringan",
"swisstopoHiking": "swisstopo Pendakian",
"swisstopoHikingClosures": "Penutupan Jalur Pendakian swisstopo",
@@ -377,7 +380,9 @@
"railway-station": "Stasiun kereta api",
"tram-stop": "Halt trem",
"bus-stop": "Pemberhentian Bus",
"ferry": "Feri"
"ferry": "Feri",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Aggiorna livello"
},
"opacity": "Opacità di sovrapposizione",
"terrain": "Terrain source",
"label": {
"basemaps": "Mappe di base",
"overlays": "Sovrapposizioni",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Pendenza",
"swisstopoHiking": "swisstopo Escursione",
"swisstopoHikingClosures": "swisstopo Fine escursione",
@@ -377,7 +380,9 @@
"railway-station": "Stazione ferroviaria",
"tram-stop": "Fermata del tram",
"bus-stop": "Fermata dell'autobus",
"ferry": "Traghetto"
"ferry": "Traghetto",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "레이어 갱신"
},
"opacity": "오버레이 투명도",
"terrain": "Terrain source",
"label": {
"basemaps": "배경 지도",
"overlays": "오버레이",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "철도역",
"tram-stop": "트램 정류장",
"bus-stop": "버스 정류장",
"ferry": "페리"
"ferry": "페리",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Naujinti sluoksnį"
},
"opacity": "Sluoksnio skaidrumas",
"terrain": "Terrain source",
"label": {
"basemaps": "Pagrindo žemėlapiai",
"overlays": "Sluoksniai",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Geležinkelio stotis",
"tram-stop": "Tramvajaus stotelė",
"bus-stop": "Autobusų stotelė",
"ferry": "Keltas"
"ferry": "Keltas",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Update layer"
},
"opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": {
"basemaps": "Basemaps",
"overlays": "Overlays",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Railway Station",
"tram-stop": "Tram Stop",
"bus-stop": "Bus Stop",
"ferry": "Ferry"
"ferry": "Ferry",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

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

View File

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

View File

@@ -282,6 +282,7 @@
"update": "Zaktualizuj warstwę"
},
"opacity": "Przezroczystość nakładki",
"terrain": "Terrain source",
"label": {
"basemaps": "Mapy bazowe",
"overlays": "Nakładki",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Stoki",
"swisstopoHiking": "swisstopo Szlaki Turystyczne",
"swisstopoHikingClosures": "swisstopo Zamknięcia Szlaków",
@@ -377,7 +380,9 @@
"railway-station": "Stacja kolejowa",
"tram-stop": "Przystanek tramwajowy",
"bus-stop": "Przystanek autobusowy",
"ferry": "Prom"
"ferry": "Prom",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Atualizar camada"
},
"opacity": "Opacidade de sobreposição",
"terrain": "Terrain source",
"label": {
"basemaps": "Mapa base",
"overlays": "Sobreposições",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Estações ferroviárias",
"tram-stop": "Parada de bonde",
"bus-stop": "Parada de Ônibus",
"ferry": "Balsa"
"ferry": "Balsa",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Atualizar camada"
},
"opacity": "Opacidade da sobreposição",
"terrain": "Terrain source",
"label": {
"basemaps": "Mapas base",
"overlays": "Sobreposições",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Estações ferroviárias",
"tram-stop": "Parada de bonde",
"bus-stop": "Parada de Ônibus",
"ferry": "Balsa"
"ferry": "Balsa",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Actualizează stratul"
},
"opacity": "Opacitatea overlay-ului",
"terrain": "Terrain source",
"label": {
"basemaps": "Hărți de bază",
"overlays": "Suprapuneri",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Gară",
"tram-stop": "Tram Stop",
"bus-stop": "Stație de autobuz",
"ferry": "Feribot"
"ferry": "Feribot",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Обновить слой"
},
"opacity": "Прозрачность наложения",
"terrain": "Terrain source",
"label": {
"basemaps": "Основные карты",
"overlays": "Наложения",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Железнодорожная станция",
"tram-stop": "Трамвайная остановка",
"bus-stop": "Автобусная остановка",
"ferry": "Паром"
"ferry": "Паром",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Ažurirajte sloj"
},
"opacity": "Providnost preklapanja",
"terrain": "Terrain source",
"label": {
"basemaps": "Osnovne mape",
"overlays": "Preklapanja",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Železnička stanica",
"tram-stop": "Tramvajsko stajalište",
"bus-stop": "Autobusko stajalište",
"ferry": "Trajekt"
"ferry": "Trajekt",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Update layer"
},
"opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": {
"basemaps": "Baskartor",
"overlays": "Lager",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Järnvägsstation",
"tram-stop": "Spårvagnshållplats",
"bus-stop": "Busshållplats",
"ferry": "Ferry"
"ferry": "Ferry",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Update layer"
},
"opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": {
"basemaps": "Basemaps",
"overlays": "Overlays",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Railway Station",
"tram-stop": "Tram Stop",
"bus-stop": "Bus Stop",
"ferry": "Ferry"
"ferry": "Ferry",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Katman güncelle"
},
"opacity": "Katman şeffaflığı",
"terrain": "Terrain source",
"label": {
"basemaps": "Temel haritalar",
"overlays": "Katmanlar",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Eğim",
"swisstopoHiking": "swisstopo Yürüyüş",
"swisstopoHikingClosures": "swisstopo Yürüyüş Sonu",
@@ -377,7 +380,9 @@
"railway-station": "Tren istasyonu",
"tram-stop": "Tramvay Durağı",
"bus-stop": "Otobüs Durağı",
"ferry": "Feribot"
"ferry": "Feribot",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Оновити шар"
},
"opacity": "Непрозорість накладання",
"terrain": "Terrain source",
"label": {
"basemaps": "Базові карти",
"overlays": "Накладання",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Залізнична Станція",
"tram-stop": "Трамвайна Зупинка",
"bus-stop": "Автобусна Зупинка",
"ferry": "Пором"
"ferry": "Пором",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Update layer"
},
"opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": {
"basemaps": "Basemaps",
"overlays": "Overlays",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Railway Station",
"tram-stop": "Tram Stop",
"bus-stop": "Bus Stop",
"ferry": "Ferry"
"ferry": "Ferry",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "Update layer"
},
"opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": {
"basemaps": "Basemaps",
"overlays": "Overlays",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "Railway Station",
"tram-stop": "Tram Stop",
"bus-stop": "Bus Stop",
"ferry": "Ferry"
"ferry": "Ferry",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {

View File

@@ -282,6 +282,7 @@
"update": "更新图层"
},
"opacity": "图层透明度",
"terrain": "Terrain source",
"label": {
"basemaps": "底图",
"overlays": "叠加层",
@@ -325,6 +326,8 @@
"usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap",
"swisstopoSlope": "Swisstopo Slope",
"swisstopoHiking": "Swisstopo Hiking",
"swisstopoHikingClosures": "Swisstopo Hiking Closures",
@@ -377,7 +380,9 @@
"railway-station": "火车站",
"tram-stop": "有轨电车站",
"bus-stop": "小型公交站台",
"ferry": "渡口"
"ferry": "渡口",
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
}
},
"chart": {