From 3c816567bcc579010f7752885fe6ee22f31814c3 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Tue, 23 Dec 2025 17:34:34 +0100 Subject: [PATCH 01/26] use explicit path for prettierrc file, closes #289 --- .prettierignore | 6 -- gpx/package.json | 4 +- website/.prettierignore | 3 + website/components.json | 30 +++--- website/package.json | 4 +- website/src/app.css | 230 ++++++++++++++++++++-------------------- 6 files changed, 137 insertions(+), 140 deletions(-) delete mode 100644 .prettierignore create mode 100644 website/.prettierignore diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index d59d9d574..000000000 --- a/.prettierignore +++ /dev/null @@ -1,6 +0,0 @@ -# Ignore files for PNPM, NPM and YARN -pnpm-lock.yaml -package-lock.json -yarn.lock -src/lib/components/ui -*.mdx \ No newline at end of file diff --git a/gpx/package.json b/gpx/package.json index 07b54c037..3f6318aa1 100644 --- a/gpx/package.json +++ b/gpx/package.json @@ -25,7 +25,7 @@ "scripts": { "build": "tsc", "postinstall": "npm run build", - "lint": "prettier --check . && eslint .", - "format": "prettier --write ." + "lint": "prettier --check . --config ../.prettierrc && eslint .", + "format": "prettier --write . --config ../.prettierrc" } } diff --git a/website/.prettierignore b/website/.prettierignore new file mode 100644 index 000000000..fc85c07bc --- /dev/null +++ b/website/.prettierignore @@ -0,0 +1,3 @@ +src/lib/components/ui +src/lib/docs/**/*.mdx +**/*.webmanifest \ No newline at end of file diff --git a/website/components.json b/website/components.json index 89bed7d71..f5e3a96a1 100644 --- a/website/components.json +++ b/website/components.json @@ -1,17 +1,17 @@ { - "$schema": "https://shadcn-svelte.com/schema.json", - "style": "default", - "tailwind": { - "css": "src/app.css", - "baseColor": "slate" - }, - "aliases": { - "components": "$lib/components", - "utils": "$lib/utils", - "ui": "$lib/components/ui", - "hooks": "$lib/hooks", - "lib": "$lib" - }, - "typescript": true, - "registry": "https://shadcn-svelte.com/registry" + "$schema": "https://shadcn-svelte.com/schema.json", + "style": "default", + "tailwind": { + "css": "src/app.css", + "baseColor": "slate" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry" } diff --git a/website/package.json b/website/package.json index 6457d8896..cb26d4430 100644 --- a/website/package.json +++ b/website/package.json @@ -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 . && eslint .", - "format": "prettier --write ." + "lint": "prettier --check . --config ../.prettierrc && eslint .", + "format": "prettier --write . --config ../.prettierrc" }, "devDependencies": { "@lucide/svelte": "^0.544.0", diff --git a/website/src/app.css b/website/src/app.css index d93b269f9..a3300b90e 100644 --- a/website/src/app.css +++ b/website/src/app.css @@ -1,126 +1,126 @@ -@import "tailwindcss"; -@import "tw-animate-css"; - +@import 'tailwindcss'; +@import 'tw-animate-css'; + @custom-variant dark (&:is(.dark *)); - + :root { - --background: hsl(0 0% 100%) /* <- Wrap in HSL */; - --foreground: hsl(240 10% 3.9%); - --muted: hsl(240 4.8% 95.9%); - --muted-foreground: hsl(240 3.8% 46.1%); - --popover: hsl(0 0% 100%); - --popover-foreground: hsl(240 10% 3.9%); - --card: hsl(0 0% 100%); - --card-foreground: hsl(240 10% 3.9%); - --border: hsl(240 5.9% 90%); - --input: hsl(240 5.9% 90%); - --primary: hsl(240 5.9% 10%); - --primary-foreground: hsl(0 0% 98%); - --secondary: hsl(240 4.8% 95.9%); - --secondary-foreground: hsl(240 5.9% 10%); - --accent: hsl(240 4.8% 95.9%); - --accent-foreground: hsl(240 5.9% 10%); - --destructive: hsl(0 72.2% 50.6%); - --destructive-foreground: hsl(0 0% 98%); - --ring: hsl(240 10% 3.9%); - --sidebar: hsl(0 0% 98%); - --sidebar-foreground: hsl(240 5.3% 26.1%); - --sidebar-primary: hsl(240 5.9% 10%); - --sidebar-primary-foreground: hsl(0 0% 98%); - --sidebar-accent: hsl(240 4.8% 95.9%); - --sidebar-accent-foreground: hsl(240 5.9% 10%); - --sidebar-border: hsl(220 13% 91%); - --sidebar-ring: hsl(217.2 91.2% 59.8%); + --background: hsl(0 0% 100%) /* <- Wrap in HSL */; + --foreground: hsl(240 10% 3.9%); + --muted: hsl(240 4.8% 95.9%); + --muted-foreground: hsl(240 3.8% 46.1%); + --popover: hsl(0 0% 100%); + --popover-foreground: hsl(240 10% 3.9%); + --card: hsl(0 0% 100%); + --card-foreground: hsl(240 10% 3.9%); + --border: hsl(240 5.9% 90%); + --input: hsl(240 5.9% 90%); + --primary: hsl(240 5.9% 10%); + --primary-foreground: hsl(0 0% 98%); + --secondary: hsl(240 4.8% 95.9%); + --secondary-foreground: hsl(240 5.9% 10%); + --accent: hsl(240 4.8% 95.9%); + --accent-foreground: hsl(240 5.9% 10%); + --destructive: hsl(0 72.2% 50.6%); + --destructive-foreground: hsl(0 0% 98%); + --ring: hsl(240 10% 3.9%); + --sidebar: hsl(0 0% 98%); + --sidebar-foreground: hsl(240 5.3% 26.1%); + --sidebar-primary: hsl(240 5.9% 10%); + --sidebar-primary-foreground: hsl(0 0% 98%); + --sidebar-accent: hsl(240 4.8% 95.9%); + --sidebar-accent-foreground: hsl(240 5.9% 10%); + --sidebar-border: hsl(220 13% 91%); + --sidebar-ring: hsl(217.2 91.2% 59.8%); - --support: rgb(220 15 130); - --link: rgb(0 110 180); - --selection: hsl(240 4.8% 93%); - - --radius: 0.5rem; + --support: rgb(220 15 130); + --link: rgb(0 110 180); + --selection: hsl(240 4.8% 93%); + + --radius: 0.5rem; } - + .dark { - --background: hsl(240 10% 3.9%); - --foreground: hsl(0 0% 98%); - --muted: hsl(240 3.7% 15.9%); - --muted-foreground: hsl(240 5% 64.9%); - --popover: hsl(240 10% 3.9%); - --popover-foreground: hsl(0 0% 98%); - --card: hsl(240 10% 3.9%); - --card-foreground: hsl(0 0% 98%); - --border: hsl(240 3.7% 15.9%); - --input: hsl(240 3.7% 15.9%); - --primary: hsl(0 0% 98%); - --primary-foreground: hsl(240 5.9% 10%); - --secondary: hsl(240 3.7% 15.9%); - --secondary-foreground: hsl(0 0% 98%); - --accent: hsl(240 3.7% 15.9%); - --accent-foreground: hsl(0 0% 98%); - --destructive: hsl(0 62.8% 30.6%); - --destructive-foreground: hsl(0 0% 98%); - --ring: hsl(240 4.9% 83.9%); - --sidebar: hsl(240 5.9% 10%); - --sidebar-foreground: hsl(240 4.8% 95.9%); - --sidebar-primary: hsl(224.3 76.3% 48%); - --sidebar-primary-foreground: hsl(0 0% 100%); - --sidebar-accent: hsl(240 3.7% 15.9%); - --sidebar-accent-foreground: hsl(240 4.8% 95.9%); - --sidebar-border: hsl(240 3.7% 15.9%); - --sidebar-ring: hsl(217.2 91.2% 59.8%); + --background: hsl(240 10% 3.9%); + --foreground: hsl(0 0% 98%); + --muted: hsl(240 3.7% 15.9%); + --muted-foreground: hsl(240 5% 64.9%); + --popover: hsl(240 10% 3.9%); + --popover-foreground: hsl(0 0% 98%); + --card: hsl(240 10% 3.9%); + --card-foreground: hsl(0 0% 98%); + --border: hsl(240 3.7% 15.9%); + --input: hsl(240 3.7% 15.9%); + --primary: hsl(0 0% 98%); + --primary-foreground: hsl(240 5.9% 10%); + --secondary: hsl(240 3.7% 15.9%); + --secondary-foreground: hsl(0 0% 98%); + --accent: hsl(240 3.7% 15.9%); + --accent-foreground: hsl(0 0% 98%); + --destructive: hsl(0 62.8% 30.6%); + --destructive-foreground: hsl(0 0% 98%); + --ring: hsl(240 4.9% 83.9%); + --sidebar: hsl(240 5.9% 10%); + --sidebar-foreground: hsl(240 4.8% 95.9%); + --sidebar-primary: hsl(224.3 76.3% 48%); + --sidebar-primary-foreground: hsl(0 0% 100%); + --sidebar-accent: hsl(240 3.7% 15.9%); + --sidebar-accent-foreground: hsl(240 4.8% 95.9%); + --sidebar-border: hsl(240 3.7% 15.9%); + --sidebar-ring: hsl(217.2 91.2% 59.8%); - --support: rgb(255 110 190); - --link: rgb(80 190 255); - --selection: hsl(240 3.7% 22%); + --support: rgb(255 110 190); + --link: rgb(80 190 255); + --selection: hsl(240 3.7% 22%); } - + @theme inline { - /* Radius (for rounded-*) */ - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - - /* Colors */ - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-ring: var(--ring); - --color-radius: var(--radius); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); - --color-support: var(--support); - --color-link: var(--link); + /* Radius (for rounded-*) */ + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); - --breakpoint-xs: 540px; + /* Colors */ + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-ring: var(--ring); + --color-radius: var(--radius); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + --color-support: var(--support); + --color-link: var(--link); + + --breakpoint-xs: 540px; } - + @layer base { - * { - @apply border-border; - } - - body { - @apply bg-background text-foreground; - } -} \ No newline at end of file + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + } +} From affa59130f357185a596d689a681f246e800344c Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 24 Dec 2025 08:48:21 +0100 Subject: [PATCH 02/26] simplify filter for hiding layers --- .../lib/components/map/gpx-layer/gpx-layer.ts | 44 ++++++------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/website/src/lib/components/map/gpx-layer/gpx-layer.ts b/website/src/lib/components/map/gpx-layer/gpx-layer.ts index 2a1aae332..a85bd7e94 100644 --- a/website/src/lib/components/map/gpx-layer/gpx-layer.ts +++ b/website/src/lib/components/map/gpx-layer/gpx-layer.ts @@ -1,5 +1,5 @@ import { get, type Readable } from 'svelte/store'; -import mapboxgl from 'mapbox-gl'; +import mapboxgl, { type FilterSpecification } from 'mapbox-gl'; import { map } from '$lib/components/map/map'; import { waypointPopup, trackpointPopup } from './gpx-layer-popup'; import { @@ -281,25 +281,23 @@ export class GPXLayer { } } - let visibleSegments: [number, number][] = []; + let visibleTrackSegmentIds: string[] = []; file.forEachSegment((segment, trackIndex, segmentIndex) => { if (!segment._data.hidden) { - visibleSegments.push([trackIndex, segmentIndex]); + visibleTrackSegmentIds.push(`${trackIndex}-${segmentIndex}`); } }); + const segmentFilter: FilterSpecification = [ + 'in', + ['get', 'trackSegmentId'], + ['literal', visibleTrackSegmentIds], + ]; - _map.setFilter( - this.fileId, - [ - 'any', - ...visibleSegments.map(([trackIndex, segmentIndex]) => [ - 'all', - ['==', 'trackIndex', trackIndex], - ['==', 'segmentIndex', segmentIndex], - ]), - ], - { validate: false } - ); + _map.setFilter(this.fileId, segmentFilter, { validate: false }); + + if (_map.getLayer(this.fileId + '-direction')) { + _map.setFilter(this.fileId + '-direction', segmentFilter, { validate: false }); + } let visibleWaypoints: number[] = []; file.wpt.forEach((waypoint, waypointIndex) => { @@ -313,21 +311,6 @@ export class GPXLayer { ['in', ['get', 'waypointIndex'], ['literal', visibleWaypoints]], { validate: false } ); - - if (_map.getLayer(this.fileId + '-direction')) { - _map.setFilter( - this.fileId + '-direction', - [ - 'any', - ...visibleSegments.map(([trackIndex, segmentIndex]) => [ - 'all', - ['==', 'trackIndex', trackIndex], - ['==', 'segmentIndex', segmentIndex], - ]), - ], - { validate: false } - ); - } } catch (e) { // No reliable way to check if the map is ready to add sources and layers return; @@ -686,6 +669,7 @@ export class GPXLayer { } feature.properties.trackIndex = trackIndex; feature.properties.segmentIndex = segmentIndex; + feature.properties.trackSegmentId = `${trackIndex}-${segmentIndex}`; segmentIndex++; if (segmentIndex >= file.trk[trackIndex].trkseg.length) { From d062a38e8f6add3e98faa7621d4f74df7323b31e Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 24 Dec 2025 08:48:50 +0100 Subject: [PATCH 03/26] new mapbox version --- website/package-lock.json | 19 ++++++------------- website/package.json | 2 +- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/website/package-lock.json b/website/package-lock.json index 4c243283e..5bc5e1139 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -22,7 +22,7 @@ "gpx": "file:../gpx", "immer": "^10.1.1", "jszip": "^3.10.1", - "mapbox-gl": "^3.16.0", + "mapbox-gl": "^3.17.0", "mapillary-js": "^4.1.2", "png.js": "^0.2.1", "sanitize-html": "^2.17.0", @@ -6069,12 +6069,14 @@ } }, "node_modules/mapbox-gl": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.16.0.tgz", - "integrity": "sha512-rluV1Zp/0oHf1Y9BV+nePRNnKyTdljko3E19CzO5rBqtQaNUYS0ePCMPRtxOuWRwSdKp3f9NWJkOCjemM8nmjw==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.17.0.tgz", + "integrity": "sha512-nCrDKRlr5di6xUksUDslNWwxroJ5yv1hT8pyVFtcpWJOOKsYQxF/wOFTMie8oxMnXeFkrz1Tl1TwA1XN1yX0KA==", "license": "SEE LICENSE IN LICENSE.txt", "workspaces": [ "src/style-spec", + "test/build/vite", + "test/build/webpack", "test/build/typings" ], "dependencies": { @@ -6102,7 +6104,6 @@ "pbf": "^4.0.1", "potpack": "^2.0.0", "quickselect": "^3.0.0", - "serialize-to-js": "^3.1.2", "supercluster": "^8.0.1", "tinyqueue": "^3.0.0" } @@ -7634,14 +7635,6 @@ "node": ">=10" } }, - "node_modules/serialize-to-js": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/serialize-to-js/-/serialize-to-js-3.1.2.tgz", - "integrity": "sha512-owllqNuDDEimQat7EPG0tH7JjO090xKNzUtYz6X+Sk2BXDnOCilDdNLwjWeFywG9xkJul1ULvtUQa9O4pUaY0w==", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/set-cookie-parser": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz", diff --git a/website/package.json b/website/package.json index cb26d4430..9b290a885 100644 --- a/website/package.json +++ b/website/package.json @@ -74,7 +74,7 @@ "gpx": "file:../gpx", "immer": "^10.1.1", "jszip": "^3.10.1", - "mapbox-gl": "^3.16.0", + "mapbox-gl": "^3.17.0", "mapillary-js": "^4.1.2", "png.js": "^0.2.1", "sanitize-html": "^2.17.0", From 22b8e0edb42f9ecb951103cb0f6ad959fa8d0fa8 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 24 Dec 2025 10:11:43 +0100 Subject: [PATCH 04/26] update chartjs --- website/package-lock.json | 8 +- website/package.json | 2 +- .../elevation-profile/elevation-profile.ts | 101 +++++++++++------- 3 files changed, 68 insertions(+), 43 deletions(-) diff --git a/website/package-lock.json b/website/package-lock.json index 5bc5e1139..f86181ce0 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -14,7 +14,7 @@ "@mapbox/sphericalmercator": "^2.0.1", "@mapbox/tilebelt": "^2.0.2", "@types/mapbox__sphericalmercator": "^1.2.3", - "chart.js": "^4.4.9", + "chart.js": "^4.5.1", "chartjs-plugin-zoom": "^2.2.0", "clsx": "^2.1.1", "dexie": "^4.0.11", @@ -3664,9 +3664,9 @@ } }, "node_modules/chart.js": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", - "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", "dependencies": { "@kurkle/color": "^0.3.0" diff --git a/website/package.json b/website/package.json index 9b290a885..090e91a03 100644 --- a/website/package.json +++ b/website/package.json @@ -66,7 +66,7 @@ "@mapbox/sphericalmercator": "^2.0.1", "@mapbox/tilebelt": "^2.0.2", "@types/mapbox__sphericalmercator": "^1.2.3", - "chart.js": "^4.4.9", + "chart.js": "^4.5.1", "chartjs-plugin-zoom": "^2.2.0", "clsx": "^2.1.1", "dexie": "^4.0.11", diff --git a/website/src/lib/components/elevation-profile/elevation-profile.ts b/website/src/lib/components/elevation-profile/elevation-profile.ts index 7690e72f9..08102fbbf 100644 --- a/website/src/lib/components/elevation-profile/elevation-profile.ts +++ b/website/src/lib/components/elevation-profile/elevation-profile.ts @@ -14,7 +14,12 @@ import { getTemperatureWithUnits, getVelocityWithUnits, } from '$lib/units'; -import Chart from 'chart.js/auto'; +import Chart, { + type ChartEvent, + type ChartOptions, + type ScriptableLineSegmentContext, + type TooltipItem, +} from 'chart.js/auto'; import mapboxgl from 'mapbox-gl'; import { get, type Readable, type Writable } from 'svelte/store'; import { map } from '$lib/components/map/map'; @@ -27,6 +32,20 @@ const { distanceUnits, velocityUnits, temperatureUnits } = settings; Chart.defaults.font.family = 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; // Tailwind CSS font +interface ElevationProfilePoint { + x: number; + y: number; + time?: Date; + slope: { + at: number; + segment: number; + length: number; + }; + extensions: Record; + coordinates: [number, number]; + index: number; +} + export class ElevationProfile { private _chart: Chart | null = null; private _canvas: HTMLCanvasElement; @@ -90,7 +109,7 @@ export class ElevationProfile { } initialize() { - let options = { + let options: ChartOptions<'line'> = { animation: false, parsing: false, maintainAspectRatio: false, @@ -98,8 +117,8 @@ export class ElevationProfile { x: { type: 'linear', ticks: { - callback: function (value: number) { - return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`; + callback: function (value: number | string) { + return `${(value as number).toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`; }, align: 'inner', maxRotation: 0, @@ -108,8 +127,8 @@ export class ElevationProfile { y: { type: 'linear', ticks: { - callback: function (value: number) { - return getElevationWithUnits(value, false); + callback: function (value: number | string) { + return getElevationWithUnits(value as number, false); }, }, }, @@ -140,8 +159,8 @@ export class ElevationProfile { title: () => { return ''; }, - label: (context: Chart.TooltipContext) => { - let point = context.raw; + label: (context: TooltipItem<'line'>) => { + let point = context.raw as ElevationProfilePoint; if (context.datasetIndex === 0) { const map_ = get(map); if (map_ && this._marker) { @@ -165,10 +184,10 @@ export class ElevationProfile { return `${i18n._('quantities.power')}: ${getPowerWithUnits(point.y)}`; } }, - afterBody: (contexts: Chart.TooltipContext[]) => { + afterBody: (contexts: TooltipItem<'line'>[]) => { let context = contexts.filter((context) => context.datasetIndex === 0); if (context.length === 0) return; - let point = context[0].raw; + let point = context[0].raw as ElevationProfilePoint; let slope = { at: point.slope.at.toFixed(1), segment: point.slope.segment.toFixed(1), @@ -227,6 +246,7 @@ export class ElevationProfile { onPanStart: () => { this._panning = true; this._slicedGPXStatistics.set(undefined); + return true; }, onPanComplete: () => { this._panning = false; @@ -238,13 +258,13 @@ export class ElevationProfile { }, mode: 'x', onZoomStart: ({ chart, event }: { chart: Chart; event: any }) => { + if (!this._chart) { + return false; + } + const maxZoom = this._chart.getInitialScaleBounds()?.x?.max ?? 0; if ( event.deltaY < 0 && - Math.abs( - this._chart.getInitialScaleBounds().x.max / - this._chart.options.plugins.zoom.limits.x.minRange - - this._chart.getZoomLevel() - ) < 0.01 + Math.abs(maxZoom / this._chart.getZoomLevel()) < 0.01 ) { // Disable wheel pan if zoomed in to the max, and zooming in return false; @@ -262,7 +282,6 @@ export class ElevationProfile { }, }, }, - stacked: false, onResize: () => { this.updateOverlay(); }, @@ -270,7 +289,7 @@ export class ElevationProfile { let datasets: string[] = ['speed', 'hr', 'cad', 'atemp', 'power']; datasets.forEach((id) => { - options.scales[`y${id}`] = { + options.scales![`y${id}`] = { type: 'linear', position: 'right', grid: { @@ -291,7 +310,7 @@ export class ElevationProfile { { id: 'toggleMarker', events: ['mouseout'], - afterEvent: (chart: Chart, args: { event: Chart.ChartEvent }) => { + afterEvent: (chart: Chart, args: { event: ChartEvent }) => { if (args.event.type === 'mouseout') { const map_ = get(map); if (map_ && this._marker) { @@ -305,7 +324,7 @@ export class ElevationProfile { let startIndex = 0; let endIndex = 0; - const getIndex = (evt) => { + const getIndex = (evt: PointerEvent) => { if (!this._chart) { return undefined; } @@ -329,16 +348,16 @@ export class ElevationProfile { } } - let point = points.find((point) => point.element.raw); + const point = points.find((point) => (point.element as any).raw); if (point) { - return point.element.raw.index; + return (point.element as any).raw.index; } else { return points[0].index; } }; let dragStarted = false; - const onMouseDown = (evt) => { + const onMouseDown = (evt: PointerEvent) => { if (evt.shiftKey) { // Panning interaction return; @@ -347,7 +366,7 @@ export class ElevationProfile { this._canvas.style.cursor = 'col-resize'; startIndex = getIndex(evt); }; - const onMouseMove = (evt) => { + const onMouseMove = (evt: PointerEvent) => { if (dragStarted) { this._dragging = true; endIndex = getIndex(evt); @@ -367,7 +386,7 @@ export class ElevationProfile { } } }; - const onMouseUp = (evt) => { + const onMouseUp = (evt: PointerEvent) => { dragStarted = false; this._dragging = false; this._canvas.style.cursor = ''; @@ -463,8 +482,8 @@ export class ElevationProfile { normalized: true, yAxisID: 'ypower', }; - this._chart.options.scales.x['min'] = 0; - this._chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total); + this._chart.options.scales!.x!['min'] = 0; + this._chart.options.scales!.x!['max'] = getConvertedDistance(data.global.distance.total); this.setVisibility(); this.setFill(); @@ -513,21 +532,24 @@ export class ElevationProfile { return; } const elevationFill = get(this._elevationFill); + const dataset = this._chart.data.datasets[0]; + let segment: any = {}; if (elevationFill === 'slope') { - this._chart.data.datasets[0]['segment'] = { + segment = { backgroundColor: this.slopeFillCallback, }; } else if (elevationFill === 'surface') { - this._chart.data.datasets[0]['segment'] = { + segment = { backgroundColor: this.surfaceFillCallback, }; } else if (elevationFill === 'highway') { - this._chart.data.datasets[0]['segment'] = { + segment = { backgroundColor: this.highwayFillCallback, }; } else { - this._chart.data.datasets[0]['segment'] = {}; + segment = {}; } + Object.assign(dataset, { segment }); } updateOverlay() { @@ -575,19 +597,22 @@ export class ElevationProfile { } } - slopeFillCallback(context) { - return getSlopeColor(context.p0.raw.slope.segment); + slopeFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) { + const point = context.p0.raw as ElevationProfilePoint; + return getSlopeColor(point.slope.segment); } - surfaceFillCallback(context) { - return getSurfaceColor(context.p0.raw.extensions.surface); + surfaceFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) { + const point = context.p0.raw as ElevationProfilePoint; + return getSurfaceColor(point.extensions.surface); } - highwayFillCallback(context) { + highwayFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) { + const point = context.p0.raw as ElevationProfilePoint; return getHighwayColor( - context.p0.raw.extensions.highway, - context.p0.raw.extensions.sac_scale, - context.p0.raw.extensions.mtb_scale + point.extensions.highway, + point.extensions.sac_scale, + point.extensions.mtb_scale ); } From e7a1d0488b2d3eb7f39e56eb778c6bbcb1f1c24c Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 24 Dec 2025 10:23:15 +0100 Subject: [PATCH 05/26] fix ts error --- website/src/lib/components/Menu.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/website/src/lib/components/Menu.svelte b/website/src/lib/components/Menu.svelte index 518867158..23cd8f630 100644 --- a/website/src/lib/components/Menu.svelte +++ b/website/src/lib/components/Menu.svelte @@ -538,6 +538,7 @@ let targetInput = e && e.target && + e.target instanceof HTMLElement && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT' || From 21f2448213fe4a12b182b487c826968ba7143371 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 24 Dec 2025 12:03:36 +0100 Subject: [PATCH 06/26] improve cloning performance --- gpx/src/gpx.ts | 56 +++++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/gpx/src/gpx.ts b/gpx/src/gpx.ts index 94c210603..063c04bdf 100644 --- a/gpx/src/gpx.ts +++ b/gpx/src/gpx.ts @@ -16,16 +16,6 @@ import { } from './types'; import { immerable, isDraft, original, freeze } from 'immer'; -function cloneJSON(obj: T): T { - if (obj === undefined) { - return undefined; - } - if (obj === null || typeof obj !== 'object') { - return null; - } - return JSON.parse(JSON.stringify(obj)); -} - // An abstract class that groups functions that need to be computed recursively in the GPX file hierarchy export abstract class GPXTreeElement> { _data: { [key: string]: any } = {}; @@ -249,12 +239,12 @@ export class GPXFile extends GPXTreeNode { clone(): GPXFile { return new GPXFile({ - attributes: cloneJSON(this.attributes), - metadata: cloneJSON(this.metadata), + attributes: structuredClone(this.attributes), + metadata: structuredClone(this.metadata), wpt: this.wpt.map((waypoint) => waypoint.clone()), trk: this.trk.map((track) => track.clone()), rte: [], - _data: cloneJSON(this._data), + _data: structuredClone(this._data), }); } @@ -267,7 +257,7 @@ export class GPXFile extends GPXTreeNode { toGPXFileType(exclude: string[] = []): GPXFileType { let file: GPXFileType = { - attributes: cloneJSON(this.attributes), + attributes: structuredClone(this.attributes), metadata: {}, wpt: this.wpt.map((wpt) => wpt.toWaypointType(exclude)), trk: this.trk.map((track) => track.toTrackType(exclude)), @@ -281,10 +271,10 @@ export class GPXFile extends GPXTreeNode { file.metadata.desc = this.metadata.desc; } if (this.metadata.author) { - file.metadata.author = cloneJSON(this.metadata.author); + file.metadata.author = structuredClone(this.metadata.author); } if (this.metadata.link) { - file.metadata.link = cloneJSON(this.metadata.link); + file.metadata.link = structuredClone(this.metadata.link); } if (this.metadata.time && !exclude.includes('time')) { file.metadata.time = this.metadata.time; @@ -577,11 +567,11 @@ export class Track extends GPXTreeNode { cmt: this.cmt, desc: this.desc, src: this.src, - link: cloneJSON(this.link), + link: structuredClone(this.link), type: this.type, - extensions: cloneJSON(this.extensions), + extensions: structuredClone(this.extensions), trkseg: this.trkseg.map((seg) => seg.clone()), - _data: cloneJSON(this._data), + _data: structuredClone(this._data), }); } @@ -1100,7 +1090,7 @@ export class TrackSegment extends GPXTreeLeaf { clone(): TrackSegment { return new TrackSegment({ trkpt: this.trkpt.map((point) => point.clone()), - _data: cloneJSON(this._data), + _data: structuredClone(this._data), }); } @@ -1226,14 +1216,14 @@ export class TrackSegment extends GPXTreeLeaf { let trkpt = og.trkpt.map( (point, i) => new TrackPoint({ - attributes: cloneJSON(point.attributes), + attributes: structuredClone(point.attributes), ele: point.ele, time: new Date( newStartTimestamp.getTime() + (originalEndTimestamp.getTime() - og.trkpt[i].time.getTime()) ), - extensions: cloneJSON(point.extensions), - _data: cloneJSON(point._data), + extensions: structuredClone(point.extensions), + _data: structuredClone(point._data), }) ); @@ -1468,11 +1458,18 @@ export class TrackPoint { clone(): TrackPoint { return new TrackPoint({ - attributes: cloneJSON(this.attributes), + attributes: { + lat: this.attributes.lat, + lon: this.attributes.lon, + }, ele: this.ele, time: this.time ? new Date(this.time.getTime()) : undefined, - extensions: cloneJSON(this.extensions), - _data: cloneJSON(this._data), + extensions: this.extensions ? structuredClone(this.extensions) : undefined, + _data: { + index: this._data?.index, + anchor: this._data?.anchor, + zoom: this._data?.zoom, + }, }); } } @@ -1557,13 +1554,16 @@ export class Waypoint { clone(): Waypoint { return new Waypoint({ - attributes: cloneJSON(this.attributes), + attributes: { + lat: this.attributes.lat, + lon: this.attributes.lon, + }, ele: this.ele, time: this.time ? new Date(this.time.getTime()) : undefined, name: this.name, cmt: this.cmt, desc: this.desc, - link: cloneJSON(this.link), + link: structuredClone(this.link), sym: this.sym, type: this.type, }); From a6a3917986805592d99b0937b41e7a1be42f9d28 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 24 Dec 2025 12:21:27 +0100 Subject: [PATCH 07/26] improve statistics tree performance --- website/src/lib/logic/statistics-tree.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/website/src/lib/logic/statistics-tree.ts b/website/src/lib/logic/statistics-tree.ts index 5a114d944..a1ee5869d 100644 --- a/website/src/lib/logic/statistics-tree.ts +++ b/website/src/lib/logic/statistics-tree.ts @@ -22,25 +22,34 @@ export class GPXStatisticsTree { } getStatisticsFor(item: ListItem): GPXStatistics { - let statistics = new GPXStatistics(); + let statistics = []; let id = item.getIdAtLevel(this.level); if (id === undefined || id === 'waypoints') { Object.keys(this.statistics).forEach((key) => { if (this.statistics[key] instanceof GPXStatistics) { - statistics.mergeWith(this.statistics[key]); + statistics.push(this.statistics[key]); } else { - statistics.mergeWith(this.statistics[key].getStatisticsFor(item)); + statistics.push(this.statistics[key].getStatisticsFor(item)); } }); } else { let child = this.statistics[id]; if (child instanceof GPXStatistics) { - statistics.mergeWith(child); + statistics.push(child); } else if (child !== undefined) { - statistics.mergeWith(child.getStatisticsFor(item)); + statistics.push(child.getStatisticsFor(item)); } } - return statistics; + 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()); + } } } export type GPXFileWithStatistics = { file: GPXFile; statistics: GPXStatisticsTree }; From 2e171dfbee3fc95dca6bbf192152053d5ef1f59d Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 24 Dec 2025 12:43:24 +0100 Subject: [PATCH 08/26] speed up wpt to segment matching --- gpx/src/simplify.ts | 8 +-- website/src/lib/logic/file-actions.ts | 77 ++++++++------------------- website/src/lib/utils.ts | 52 +++++++++++++++++- 3 files changed, 77 insertions(+), 60 deletions(-) diff --git a/gpx/src/simplify.ts b/gpx/src/simplify.ts index d55e5c231..7669b3cd3 100644 --- a/gpx/src/simplify.ts +++ b/gpx/src/simplify.ts @@ -59,13 +59,13 @@ function ramerDouglasPeuckerRecursive( } export function crossarcDistance( - point1: TrackPoint, - point2: TrackPoint, + point1: TrackPoint | Coordinates, + point2: TrackPoint | Coordinates, point3: TrackPoint | Coordinates ): number { return crossarc( - point1.getCoordinates(), - point2.getCoordinates(), + point1 instanceof TrackPoint ? point1.getCoordinates() : point1, + point2 instanceof TrackPoint ? point2.getCoordinates() : point2, point3 instanceof TrackPoint ? point3.getCoordinates() : point3 ); } diff --git a/website/src/lib/logic/file-actions.ts b/website/src/lib/logic/file-actions.ts index f8771e727..2fb9c8a0c 100644 --- a/website/src/lib/logic/file-actions.ts +++ b/website/src/lib/logic/file-actions.ts @@ -17,7 +17,6 @@ import { import { i18n } from '$lib/i18n.svelte'; import { freeze, type WritableDraft } from 'immer'; import { - distance, GPXFile, parseGPX, Track, @@ -30,7 +29,7 @@ import { } from 'gpx'; import { get } from 'svelte/store'; import { settings } from '$lib/logic/settings'; -import { getClosestLinePoint, getElevation } from '$lib/utils'; +import { getClosestLinePoint, getClosestTrackSegments, getElevation } from '$lib/utils'; import { gpxStatistics } from '$lib/logic/statistics'; import { boundsManager } from './bounds'; @@ -453,34 +452,13 @@ export const fileActions = { selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { if (level === ListLevel.FILE) { let file = fileStateCollection.getFile(fileId); - if (file) { + let statistics = fileStateCollection.getStatistics(fileId); + if (file && statistics) { if (file.trk.length > 1) { let fileIds = getFileIds(file.trk.length); - let closest = file.wpt.map((wpt, wptIndex) => { - return { - wptIndex: wptIndex, - index: [0], - distance: Number.MAX_VALUE, - }; - }); - file.trk.forEach((track, index) => { - track.getSegments().forEach((segment) => { - segment.trkpt.forEach((point) => { - file.wpt.forEach((wpt, wptIndex) => { - let dist = distance( - point.getCoordinates(), - wpt.getCoordinates() - ); - if (dist < closest[wptIndex].distance) { - closest[wptIndex].distance = dist; - closest[wptIndex].index = [index]; - } else if (dist === closest[wptIndex].distance) { - closest[wptIndex].index.push(index); - } - }); - }); - }); - }); + let closest = file.wpt.map((wpt) => + getClosestTrackSegments(file, statistics, wpt.getCoordinates()) + ); file.trk.forEach((track, index) => { let newFile = file.clone(); let tracks = track.trkseg.map((segment, segmentIndex) => { @@ -496,8 +474,12 @@ export const fileActions = { 0, file.wpt.length - 1, closest - .filter((c) => c.index.includes(index)) - .map((c) => file.wpt[c.wptIndex]) + .filter((c) => + c.some( + ([trackIndex, segmentIndex]) => trackIndex === index + ) + ) + .map((c, wptIndex) => file.wpt[wptIndex]) ); newFile._data.id = fileIds[index]; newFile.metadata.name = @@ -506,29 +488,9 @@ export const fileActions = { }); } else if (file.trk.length === 1) { let fileIds = getFileIds(file.trk[0].trkseg.length); - let closest = file.wpt.map((wpt, wptIndex) => { - return { - wptIndex: wptIndex, - index: [0], - distance: Number.MAX_VALUE, - }; - }); - file.trk[0].trkseg.forEach((segment, index) => { - segment.trkpt.forEach((point) => { - file.wpt.forEach((wpt, wptIndex) => { - let dist = distance( - point.getCoordinates(), - wpt.getCoordinates() - ); - if (dist < closest[wptIndex].distance) { - closest[wptIndex].distance = dist; - closest[wptIndex].index = [index]; - } else if (dist === closest[wptIndex].distance) { - closest[wptIndex].index.push(index); - } - }); - }); - }); + let closest = file.wpt.map((wpt) => + getClosestTrackSegments(file, statistics, wpt.getCoordinates()) + ); file.trk[0].trkseg.forEach((segment, index) => { let newFile = file.clone(); newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [ @@ -538,8 +500,13 @@ export const fileActions = { 0, file.wpt.length - 1, closest - .filter((c) => c.index.includes(index)) - .map((c) => file.wpt[c.wptIndex]) + .filter((c) => + c.some( + ([trackIndex, segmentIndex]) => + segmentIndex === index + ) + ) + .map((c, wptIndex) => file.wpt[wptIndex]) ); newFile._data.id = fileIds[index]; newFile.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`; diff --git a/website/src/lib/utils.ts b/website/src/lib/utils.ts index 098aab0b4..7725075cd 100644 --- a/website/src/lib/utils.ts +++ b/website/src/lib/utils.ts @@ -2,11 +2,13 @@ import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; import { base } from '$app/paths'; import { languages } from '$lib/languages'; -import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance } from 'gpx'; +import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance, GPXFile } from 'gpx'; import mapboxgl from 'mapbox-gl'; import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt'; import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public'; import PNGReader from 'png.js'; +import type { GPXStatisticsTree } from '$lib/logic/statistics-tree'; +import { ListTrackSegmentItem } from '$lib/components/file-list/file-list'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -47,6 +49,54 @@ export function getClosestLinePoint( return closest; } +export function getClosestTrackSegments( + file: GPXFile, + statistics: GPXStatisticsTree, + point: Coordinates +): [number, number][] { + let segmentBoundsDistances: [number, number, number][] = []; + file.forEachSegment((segment, trackIndex, segmentIndex) => { + let segmentStatistics = statistics.getStatisticsFor( + new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex) + ); + let segmentBounds = segmentStatistics.global.bounds; + let northEast = segmentBounds.northEast; + let southWest = segmentBounds.southWest; + let northWest: Coordinates = { lat: northEast.lat, lon: southWest.lon }; + let southEast: Coordinates = { lat: southWest.lat, lon: northEast.lon }; + let distanceToSegment = Math.min( + crossarcDistance(northWest, northEast, point), + crossarcDistance(northEast, southEast, point), + crossarcDistance(southEast, southWest, point), + crossarcDistance(southWest, northWest, point) + ); + segmentBoundsDistances.push([distanceToSegment, trackIndex, segmentIndex]); + }); + segmentBoundsDistances.sort((a, b) => a[0] - b[0]); + + let closest: { distance: number; indices: [number, number][] } = { + distance: Number.MAX_VALUE, + indices: [], + }; + for (let s = 0; s < segmentBoundsDistances.length; s++) { + if (segmentBoundsDistances[s][0] > closest.distance) { + break; + } + const segment = file.getSegment(segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]); + segment.trkpt.forEach((pt) => { + let dist = distance(pt.getCoordinates(), point); + if (dist < closest.distance) { + closest.distance = dist; + closest.indices = [[segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]]]; + } else if (dist === closest.distance) { + closest.indices.push([segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]]); + } + }); + } + + return closest.indices; +} + export function getElevation( points: (TrackPoint | Waypoint | Coordinates)[], ELEVATION_ZOOM: number = 13, From 51c85e4cd5474a52a542ab67cb0511d53342169a Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 24 Dec 2025 13:07:22 +0100 Subject: [PATCH 09/26] fix wpt to segment matching --- website/src/lib/logic/file-actions.ts | 21 ++++++++------------- website/src/lib/utils.ts | 23 ++++++++++++++--------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/website/src/lib/logic/file-actions.ts b/website/src/lib/logic/file-actions.ts index 2fb9c8a0c..0798e333a 100644 --- a/website/src/lib/logic/file-actions.ts +++ b/website/src/lib/logic/file-actions.ts @@ -473,13 +473,11 @@ export const fileActions = { newFile.replaceWaypoints( 0, file.wpt.length - 1, - closest - .filter((c) => - c.some( - ([trackIndex, segmentIndex]) => trackIndex === index - ) + file.wpt.filter((wpt, wptIndex) => + closest[wptIndex].some( + ([trackIndex, segmentIndex]) => trackIndex === index ) - .map((c, wptIndex) => file.wpt[wptIndex]) + ) ); newFile._data.id = fileIds[index]; newFile.metadata.name = @@ -499,14 +497,11 @@ export const fileActions = { newFile.replaceWaypoints( 0, file.wpt.length - 1, - closest - .filter((c) => - c.some( - ([trackIndex, segmentIndex]) => - segmentIndex === index - ) + file.wpt.filter((wpt, wptIndex) => + closest[wptIndex].some( + ([trackIndex, segmentIndex]) => segmentIndex === index ) - .map((c, wptIndex) => file.wpt[wptIndex]) + ) ); newFile._data.id = fileIds[index]; newFile.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`; diff --git a/website/src/lib/utils.ts b/website/src/lib/utils.ts index 7725075cd..035de5e97 100644 --- a/website/src/lib/utils.ts +++ b/website/src/lib/utils.ts @@ -62,15 +62,20 @@ export function getClosestTrackSegments( let segmentBounds = segmentStatistics.global.bounds; let northEast = segmentBounds.northEast; let southWest = segmentBounds.southWest; - let northWest: Coordinates = { lat: northEast.lat, lon: southWest.lon }; - let southEast: Coordinates = { lat: southWest.lat, lon: northEast.lon }; - let distanceToSegment = Math.min( - crossarcDistance(northWest, northEast, point), - crossarcDistance(northEast, southEast, point), - crossarcDistance(southEast, southWest, point), - crossarcDistance(southWest, northWest, point) - ); - segmentBoundsDistances.push([distanceToSegment, trackIndex, segmentIndex]); + let bounds = new mapboxgl.LngLatBounds(southWest, northEast); + if (bounds.contains(point)) { + segmentBoundsDistances.push([0, trackIndex, segmentIndex]); + } else { + let northWest: Coordinates = { lat: northEast.lat, lon: southWest.lon }; + let southEast: Coordinates = { lat: southWest.lat, lon: northEast.lon }; + let distanceToBounds = Math.min( + crossarcDistance(northWest, northEast, point), + crossarcDistance(northEast, southEast, point), + crossarcDistance(southEast, southWest, point), + crossarcDistance(southWest, northWest, point) + ); + segmentBoundsDistances.push([distanceToBounds, trackIndex, segmentIndex]); + } }); segmentBoundsDistances.sort((a, b) => a[0] - b[0]); From ebe9681c12bba9f4b201e9f934352540c2eb80c7 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 24 Dec 2025 13:31:04 +0100 Subject: [PATCH 10/26] avoid creating useless data --- .../elevation-profile/elevation-profile.ts | 85 +++++++++++-------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/website/src/lib/components/elevation-profile/elevation-profile.ts b/website/src/lib/components/elevation-profile/elevation-profile.ts index 08102fbbf..623ebd12e 100644 --- a/website/src/lib/components/elevation-profile/elevation-profile.ts +++ b/website/src/lib/components/elevation-profile/elevation-profile.ts @@ -428,57 +428,72 @@ export class ElevationProfile { segment: {}, }; this._chart.data.datasets[1] = { - data: data.local.points.map((point, index) => { - return { - x: getConvertedDistance(data.local.distance.total[index]), - y: getConvertedVelocity(data.local.speed[index]), - index: index, - }; - }), + data: + data.global.time.total > 0 + ? data.local.points.map((point, index) => { + return { + x: getConvertedDistance(data.local.distance.total[index]), + y: getConvertedVelocity(data.local.speed[index]), + index: index, + }; + }) + : [], normalized: true, yAxisID: 'yspeed', }; this._chart.data.datasets[2] = { - data: data.local.points.map((point, index) => { - return { - x: getConvertedDistance(data.local.distance.total[index]), - y: point.getHeartRate(), - index: index, - }; - }), + data: + data.global.hr.count > 0 + ? data.local.points.map((point, index) => { + return { + x: getConvertedDistance(data.local.distance.total[index]), + y: point.getHeartRate(), + index: index, + }; + }) + : [], normalized: true, yAxisID: 'yhr', }; this._chart.data.datasets[3] = { - data: data.local.points.map((point, index) => { - return { - x: getConvertedDistance(data.local.distance.total[index]), - y: point.getCadence(), - index: index, - }; - }), + data: + data.global.cad.count > 0 + ? data.local.points.map((point, index) => { + return { + x: getConvertedDistance(data.local.distance.total[index]), + y: point.getCadence(), + index: index, + }; + }) + : [], normalized: true, yAxisID: 'ycad', }; this._chart.data.datasets[4] = { - data: data.local.points.map((point, index) => { - return { - x: getConvertedDistance(data.local.distance.total[index]), - y: getConvertedTemperature(point.getTemperature()), - index: index, - }; - }), + data: + data.global.atemp.count > 0 + ? data.local.points.map((point, index) => { + return { + x: getConvertedDistance(data.local.distance.total[index]), + y: getConvertedTemperature(point.getTemperature()), + index: index, + }; + }) + : [], normalized: true, yAxisID: 'yatemp', }; this._chart.data.datasets[5] = { - data: data.local.points.map((point, index) => { - return { - x: getConvertedDistance(data.local.distance.total[index]), - y: point.getPower(), - index: index, - }; - }), + data: + data.global.power.count > 0 + ? data.local.points.map((point, index) => { + return { + x: getConvertedDistance(data.local.distance.total[index]), + y: point.getPower(), + index: index, + }; + }) + : [], normalized: true, yAxisID: 'ypower', }; From 4b45b5d716b8f7cc8b35a0a1f9f68bffcce2e3fe Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 24 Dec 2025 15:27:44 +0100 Subject: [PATCH 11/26] update bits-ui --- website/package-lock.json | 8 ++++---- website/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/website/package-lock.json b/website/package-lock.json index f86181ce0..a4e6d3904 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -47,7 +47,7 @@ "@types/sortablejs": "^1.15.8", "@typescript-eslint/eslint-plugin": "^8.33.1", "@typescript-eslint/parser": "^8.33.1", - "bits-ui": "^2.12.0", + "bits-ui": "^2.14.4", "eslint": "^9.28.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-svelte": "^3.9.1", @@ -3241,9 +3241,9 @@ ] }, "node_modules/bits-ui": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.12.0.tgz", - "integrity": "sha512-8NF4ILNyAJlIxDXpl/akGXGBV5QmZAe+8gTfPttM5P6/+LrijumcSfFXY5cr4QkXwTmLA7H5stYpbgJf2XFJvg==", + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.14.4.tgz", + "integrity": "sha512-W6kenhnbd/YVvur+DKkaVJ6GldE53eLewur5AhUCqslYQ0vjZr8eWlOfwZnMiPB+PF5HMVqf61vXBvmyrAmPWg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/website/package.json b/website/package.json index 090e91a03..e74a9d66f 100644 --- a/website/package.json +++ b/website/package.json @@ -31,7 +31,7 @@ "@types/sortablejs": "^1.15.8", "@typescript-eslint/eslint-plugin": "^8.33.1", "@typescript-eslint/parser": "^8.33.1", - "bits-ui": "^2.12.0", + "bits-ui": "^2.14.4", "eslint": "^9.28.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-svelte": "^3.9.1", From a011768d2d9f64e6167acef68495292e7b53b720 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 24 Dec 2025 16:12:44 +0100 Subject: [PATCH 12/26] computeStatistics compatible with read-only segment --- gpx/src/gpx.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/gpx/src/gpx.ts b/gpx/src/gpx.ts index 063c04bdf..1395480b7 100644 --- a/gpx/src/gpx.ts +++ b/gpx/src/gpx.ts @@ -138,7 +138,9 @@ export class GPXFile extends GPXTreeNode { }, }, }; - this.wpt = gpx.wpt ? gpx.wpt.map((waypoint) => new Waypoint(waypoint)) : []; + this.wpt = gpx.wpt + ? gpx.wpt.map((waypoint, index) => new Waypoint(waypoint, index)) + : []; this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : []; if (gpx.rte && gpx.rte.length > 0) { this.trk = this.trk.concat(gpx.rte.map((route) => convertRouteToTrack(route))); @@ -176,9 +178,6 @@ export class GPXFile extends GPXTreeNode { segment._data['segmentIndex'] = segmentIndex; }); }); - this.wpt.forEach((waypoint, waypointIndex) => { - waypoint._data['index'] = waypointIndex; - }); } get children(): Array { @@ -797,7 +796,7 @@ export class TrackSegment extends GPXTreeLeaf { constructor(segment?: (TrackSegmentType & { _data?: any }) | TrackSegment) { super(); if (segment) { - this.trkpt = segment.trkpt.map((point) => new TrackPoint(point)); + this.trkpt = segment.trkpt.map((point, index) => new TrackPoint(point, index)); if (segment.hasOwnProperty('_data')) { this._data = segment._data; } @@ -809,12 +808,10 @@ export class TrackSegment extends GPXTreeLeaf { _computeStatistics(): GPXStatistics { let statistics = new GPXStatistics(); - statistics.local.points = this.trkpt.map((point) => point); + statistics.local.points = this.trkpt.slice(0); const points = this.trkpt; for (let i = 0; i < points.length; i++) { - points[i]._data['index'] = i; - // distance let dist = 0; if (i > 0) { @@ -1307,7 +1304,7 @@ export class TrackPoint { _data: { [key: string]: any } = {}; - constructor(point: (TrackPointType & { _data?: any }) | TrackPoint) { + constructor(point: (TrackPointType & { _data?: any }) | TrackPoint, index?: number) { this.attributes = point.attributes; this.ele = point.ele; this.time = point.time; @@ -1315,6 +1312,9 @@ export class TrackPoint { if (point.hasOwnProperty('_data')) { this._data = point._data; } + if (index !== undefined) { + this._data.index = index; + } } getCoordinates(): Coordinates { @@ -1488,7 +1488,7 @@ export class Waypoint { type?: string; _data: { [key: string]: any } = {}; - constructor(waypoint: (WaypointType & { _data?: any }) | Waypoint) { + constructor(waypoint: (WaypointType & { _data?: any }) | Waypoint, index?: number) { this.attributes = waypoint.attributes; this.ele = waypoint.ele; this.time = waypoint.time; @@ -1507,6 +1507,9 @@ export class Waypoint { if (waypoint.hasOwnProperty('_data')) { this._data = waypoint._data; } + if (index !== undefined) { + this._data.index = index; + } } getCoordinates(): Coordinates { From d3e733aa3ed3cdadbad9f87a22d3c3b5ba1ddf41 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 24 Dec 2025 16:34:40 +0100 Subject: [PATCH 13/26] fix wpt colors --- website/src/lib/components/map/gpx-layer/gpx-layer.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/src/lib/components/map/gpx-layer/gpx-layer.ts b/website/src/lib/components/map/gpx-layer/gpx-layer.ts index a85bd7e94..4439fd9a0 100644 --- a/website/src/lib/components/map/gpx-layer/gpx-layer.ts +++ b/website/src/lib/components/map/gpx-layer/gpx-layer.ts @@ -153,8 +153,6 @@ export class GPXLayer { return; } - this.loadIcons(); - if ( file._data.style && file._data.style.color && @@ -164,6 +162,8 @@ export class GPXLayer { this.layerColor = `#${file._data.style.color}`; } + this.loadIcons(); + try { let source = _map.getSource(this.fileId) as mapboxgl.GeoJSONSource | undefined; if (source) { @@ -702,7 +702,7 @@ export class GPXLayer { properties: { fileId: this.fileId, waypointIndex: index, - icon: `${this.fileId}-waypoint-${getSymbolKey(waypoint.sym) ?? 'default'}`, + icon: `waypoint-${getSymbolKey(waypoint.sym) ?? 'default'}-${this.layerColor}`, }, }); }); @@ -723,7 +723,7 @@ export class GPXLayer { }); symbols.forEach((symbol) => { - const iconId = `${this.fileId}-waypoint-${symbol ?? 'default'}`; + const iconId = `waypoint-${symbol ?? 'default'}-${this.layerColor}`; if (!_map.hasImage(iconId)) { let icon = new Image(100, 100); icon.onload = () => { From 595ea8e2d30f31da89d353c28151734b379681ae Mon Sep 17 00:00:00 2001 From: vcoppe Date: Fri, 26 Dec 2025 14:17:23 +0100 Subject: [PATCH 14/26] revert clone function switch --- gpx/src/gpx.ts | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/gpx/src/gpx.ts b/gpx/src/gpx.ts index 1395480b7..0a0aff9db 100644 --- a/gpx/src/gpx.ts +++ b/gpx/src/gpx.ts @@ -16,6 +16,16 @@ import { } from './types'; import { immerable, isDraft, original, freeze } from 'immer'; +function cloneJSON(obj: T): T { + if (obj === undefined) { + return undefined; + } + if (obj === null || typeof obj !== 'object') { + return null; + } + return JSON.parse(JSON.stringify(obj)); +} + // An abstract class that groups functions that need to be computed recursively in the GPX file hierarchy export abstract class GPXTreeElement> { _data: { [key: string]: any } = {}; @@ -238,12 +248,12 @@ export class GPXFile extends GPXTreeNode { clone(): GPXFile { return new GPXFile({ - attributes: structuredClone(this.attributes), - metadata: structuredClone(this.metadata), + attributes: cloneJSON(this.attributes), + metadata: cloneJSON(this.metadata), wpt: this.wpt.map((waypoint) => waypoint.clone()), trk: this.trk.map((track) => track.clone()), rte: [], - _data: structuredClone(this._data), + _data: cloneJSON(this._data), }); } @@ -256,7 +266,7 @@ export class GPXFile extends GPXTreeNode { toGPXFileType(exclude: string[] = []): GPXFileType { let file: GPXFileType = { - attributes: structuredClone(this.attributes), + attributes: cloneJSON(this.attributes), metadata: {}, wpt: this.wpt.map((wpt) => wpt.toWaypointType(exclude)), trk: this.trk.map((track) => track.toTrackType(exclude)), @@ -270,10 +280,10 @@ export class GPXFile extends GPXTreeNode { file.metadata.desc = this.metadata.desc; } if (this.metadata.author) { - file.metadata.author = structuredClone(this.metadata.author); + file.metadata.author = cloneJSON(this.metadata.author); } if (this.metadata.link) { - file.metadata.link = structuredClone(this.metadata.link); + file.metadata.link = cloneJSON(this.metadata.link); } if (this.metadata.time && !exclude.includes('time')) { file.metadata.time = this.metadata.time; @@ -566,11 +576,11 @@ export class Track extends GPXTreeNode { cmt: this.cmt, desc: this.desc, src: this.src, - link: structuredClone(this.link), + link: cloneJSON(this.link), type: this.type, - extensions: structuredClone(this.extensions), + extensions: cloneJSON(this.extensions), trkseg: this.trkseg.map((seg) => seg.clone()), - _data: structuredClone(this._data), + _data: cloneJSON(this._data), }); } @@ -1087,7 +1097,7 @@ export class TrackSegment extends GPXTreeLeaf { clone(): TrackSegment { return new TrackSegment({ trkpt: this.trkpt.map((point) => point.clone()), - _data: structuredClone(this._data), + _data: cloneJSON(this._data), }); } @@ -1213,14 +1223,14 @@ export class TrackSegment extends GPXTreeLeaf { let trkpt = og.trkpt.map( (point, i) => new TrackPoint({ - attributes: structuredClone(point.attributes), + attributes: cloneJSON(point.attributes), ele: point.ele, time: new Date( newStartTimestamp.getTime() + (originalEndTimestamp.getTime() - og.trkpt[i].time.getTime()) ), - extensions: structuredClone(point.extensions), - _data: structuredClone(point._data), + extensions: cloneJSON(point.extensions), + _data: cloneJSON(point._data), }) ); @@ -1464,7 +1474,7 @@ export class TrackPoint { }, ele: this.ele, time: this.time ? new Date(this.time.getTime()) : undefined, - extensions: this.extensions ? structuredClone(this.extensions) : undefined, + extensions: this.extensions ? cloneJSON(this.extensions) : undefined, _data: { index: this._data?.index, anchor: this._data?.anchor, @@ -1566,7 +1576,7 @@ export class Waypoint { name: this.name, cmt: this.cmt, desc: this.desc, - link: structuredClone(this.link), + link: cloneJSON(this.link), sym: this.sym, type: this.type, }); From 256d62b29be1922d72ec6744cc147a59d5de92f3 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Sun, 4 Jan 2026 19:00:13 +0100 Subject: [PATCH 15/26] only perform layer selection checks when settings are open --- .../map/layer-control/LayerControlSettings.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/lib/components/map/layer-control/LayerControlSettings.svelte b/website/src/lib/components/map/layer-control/LayerControlSettings.svelte index 7278371ae..27ace9277 100644 --- a/website/src/lib/components/map/layer-control/LayerControlSettings.svelte +++ b/website/src/lib/components/map/layer-control/LayerControlSettings.svelte @@ -54,7 +54,7 @@ } $effect(() => { - if ($selectedBasemapTree && $currentBasemap) { + if (open && $selectedBasemapTree && $currentBasemap) { if (!isSelected($selectedBasemapTree, $currentBasemap)) { if (!isSelected($selectedBasemapTree, defaultBasemap)) { $selectedBasemapTree = toggle($selectedBasemapTree, defaultBasemap); @@ -65,7 +65,7 @@ }); $effect(() => { - if ($selectedOverlayTree) { + if (open && $selectedOverlayTree) { untrack(() => { if ($currentOverlays) { let overlayLayers = getLayers($currentOverlays); @@ -86,7 +86,7 @@ }); $effect(() => { - if ($selectedOverpassTree) { + if (open && $selectedOverpassTree) { untrack(() => { if ($currentOverpassQueries) { let overlayLayers = getLayers($currentOverpassQueries); From 5dcb93ca5d20e20dd116a14fade10774397dc3e7 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Sun, 4 Jan 2026 19:29:46 +0100 Subject: [PATCH 16/26] fix loading style font --- .../map/layer-control/LayerControl.svelte | 4 +--- website/src/lib/components/map/map.ts | 17 ++++++----------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/website/src/lib/components/map/layer-control/LayerControl.svelte b/website/src/lib/components/map/layer-control/LayerControl.svelte index d8d279453..9f0b1a137 100644 --- a/website/src/lib/components/map/layer-control/LayerControl.svelte +++ b/website/src/lib/components/map/layer-control/LayerControl.svelte @@ -101,9 +101,7 @@ acc: Record, imprt: ImportSpecification ) => { - if ( - !['basemap', 'overlays', 'glyphs-and-sprite'].includes(imprt.id) - ) { + if (!['basemap', 'overlays'].includes(imprt.id)) { acc[imprt.id] = imprt; } return acc; diff --git a/website/src/lib/components/map/map.ts b/website/src/lib/components/map/map.ts index 5b31fe291..14e83462a 100644 --- a/website/src/lib/components/map/map.ts +++ b/website/src/lib/components/map/map.ts @@ -35,17 +35,6 @@ export class MapboxGLMap { sources: {}, layers: [], imports: [ - { - id: 'glyphs-and-sprite', // make Mapbox glyphs and sprite available to other styles - url: '', - data: { - version: 8, - sources: {}, - layers: [], - glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf', - sprite: 'mapbox://sprites/mapbox/outdoors-v12', - }, - }, { id: 'basemap', url: '', @@ -163,6 +152,12 @@ export class MapboxGLMap { } }); }); + map.on('style.import.load', () => { + const basemap = map.getStyle().imports?.find((imprt) => imprt.id === 'basemap'); + if (basemap && basemap.data && basemap.data.glyphs) { + map.setGlyphsUrl(basemap.data.glyphs); + } + }); map.on('load', () => { this._map.set(map); // only set the store after the map has loaded window._map = map; // entry point for extensions From 9cd87742f0e0ad09baca76ede97ec62bb2308da4 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Sun, 4 Jan 2026 19:52:25 +0100 Subject: [PATCH 17/26] avoid performing a get on unit stores for each point --- .../elevation-profile/elevation-profile.ts | 47 +++++++++++++++---- website/src/lib/units.ts | 7 ++- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/website/src/lib/components/elevation-profile/elevation-profile.ts b/website/src/lib/components/elevation-profile/elevation-profile.ts index 623ebd12e..f1ca8a6d8 100644 --- a/website/src/lib/components/elevation-profile/elevation-profile.ts +++ b/website/src/lib/components/elevation-profile/elevation-profile.ts @@ -405,12 +405,17 @@ export class ElevationProfile { return; } const data = get(this._gpxStatistics); + const units = { + distance: get(distanceUnits), + velocity: get(velocityUnits), + temperature: get(temperatureUnits), + }; this._chart.data.datasets[0] = { label: i18n._('quantities.elevation'), data: data.local.points.map((point, index) => { return { - x: getConvertedDistance(data.local.distance.total[index]), - y: point.ele ? getConvertedElevation(point.ele) : 0, + 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], @@ -432,8 +437,15 @@ export class ElevationProfile { data.global.time.total > 0 ? data.local.points.map((point, index) => { return { - x: getConvertedDistance(data.local.distance.total[index]), - y: getConvertedVelocity(data.local.speed[index]), + x: getConvertedDistance( + data.local.distance.total[index], + units.distance + ), + y: getConvertedVelocity( + data.local.speed[index], + units.velocity, + units.distance + ), index: index, }; }) @@ -446,7 +458,10 @@ export class ElevationProfile { data.global.hr.count > 0 ? data.local.points.map((point, index) => { return { - x: getConvertedDistance(data.local.distance.total[index]), + x: getConvertedDistance( + data.local.distance.total[index], + units.distance + ), y: point.getHeartRate(), index: index, }; @@ -460,7 +475,10 @@ export class ElevationProfile { data.global.cad.count > 0 ? data.local.points.map((point, index) => { return { - x: getConvertedDistance(data.local.distance.total[index]), + x: getConvertedDistance( + data.local.distance.total[index], + units.distance + ), y: point.getCadence(), index: index, }; @@ -474,8 +492,11 @@ export class ElevationProfile { data.global.atemp.count > 0 ? data.local.points.map((point, index) => { return { - x: getConvertedDistance(data.local.distance.total[index]), - y: getConvertedTemperature(point.getTemperature()), + x: getConvertedDistance( + data.local.distance.total[index], + units.distance + ), + y: getConvertedTemperature(point.getTemperature(), units.temperature), index: index, }; }) @@ -488,7 +509,10 @@ export class ElevationProfile { data.global.power.count > 0 ? data.local.points.map((point, index) => { return { - x: getConvertedDistance(data.local.distance.total[index]), + x: getConvertedDistance( + data.local.distance.total[index], + units.distance + ), y: point.getPower(), index: index, }; @@ -498,7 +522,10 @@ export class ElevationProfile { yAxisID: 'ypower', }; this._chart.options.scales!.x!['min'] = 0; - this._chart.options.scales!.x!['max'] = getConvertedDistance(data.global.distance.total); + this._chart.options.scales!.x!['max'] = getConvertedDistance( + data.global.distance.total, + units.distance + ); this.setVisibility(); this.setFill(); diff --git a/website/src/lib/units.ts b/website/src/lib/units.ts index dd0bee304..cfff44df2 100644 --- a/website/src/lib/units.ts +++ b/website/src/lib/units.ts @@ -229,6 +229,9 @@ export function getConvertedVelocity( } } -export function getConvertedTemperature(value: number) { - return get(temperatureUnits) === 'celsius' ? value : celsiusToFahrenheit(value); +export function getConvertedTemperature( + value: number, + targetTemperatureUnits = get(temperatureUnits) +) { + return targetTemperatureUnits === 'celsius' ? value : celsiusToFahrenheit(value); } From f70db42b915ac21c4d59fa8d8af6b5830e07b9c2 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Sun, 4 Jan 2026 21:39:32 +0100 Subject: [PATCH 18/26] New Crowdin updates (#294) * New translations en.json (Vietnamese) * New translations en.json (Vietnamese) * New translations funding.mdx (Vietnamese) * New translations mapbox.mdx (Vietnamese) * New translations edit.mdx (Vietnamese) * New translations file.mdx (Chinese Simplified) * New translations en.json (Chinese Simplified) * New translations en.json (Polish) --- website/src/lib/docs/vi/home/funding.mdx | 2 +- website/src/lib/docs/vi/home/mapbox.mdx | 8 ++-- website/src/lib/docs/vi/menu/edit.mdx | 4 +- website/src/lib/docs/zh/menu/file.mdx | 4 +- website/src/locales/pl.json | 8 ++-- website/src/locales/vi.json | 58 ++++++++++++------------ website/src/locales/zh.json | 4 +- 7 files changed, 44 insertions(+), 44 deletions(-) diff --git a/website/src/lib/docs/vi/home/funding.mdx b/website/src/lib/docs/vi/home/funding.mdx index ad60b05e8..db88fd7a4 100644 --- a/website/src/lib/docs/vi/home/funding.mdx +++ b/website/src/lib/docs/vi/home/funding.mdx @@ -2,7 +2,7 @@ import { HeartHandshake } from '@lucide/svelte'; -## Help keep the website free (and ad-free) +## Hãy giúp duy trì trang web miễn phí (và không có quảng cáo) Khi bạn thêm hoặc di chuyển các điểm định vị, máy chủ của chúng tôi sẽ tính toán đoạn đường tốt nhất trên mạng lưới giao thông. Chúng tôi cũng sử dụng các API từ Mapbox để hiển thị đa dạng các bản đồ, lưu trữ các dữ liệu độ cao cũng như giúp bạn có thể tìm kiếm các địa điểm khác nhau. diff --git a/website/src/lib/docs/vi/home/mapbox.mdx b/website/src/lib/docs/vi/home/mapbox.mdx index 3085ec537..86a0d1a9a 100644 --- a/website/src/lib/docs/vi/home/mapbox.mdx +++ b/website/src/lib/docs/vi/home/mapbox.mdx @@ -1,5 +1,5 @@ -Mapbox is the company that provides some of the beautiful maps on this website. -They also develop the map engine which powers **gpx.studio**. +Mapbox là công ty cung cấp một số bản đồ đẹp trên trang web này. +Họ cũng phát triển công cụ bản đồ cung cấp sức mạnh cho **gpx.studio**. -We are incredibly fortunate and grateful to be part of their Community program, which supports nonprofits, educational institutions, and positive impact organizations. -This partnership allows **gpx.studio** to benefit from Mapbox tools at discounted prices, greatly contributing to the financial viability of the project and enabling us to offer the best possible user experience. +Chúng tôi vô cùng may mắn và biết ơn khi được tham gia chương trình Cộng đồng của họ, chương trình hỗ trợ các tổ chức phi lợi nhuận, các tổ chức giáo dục và các tổ chức tạo ra tác động tích cực. +Sự hợp tác này cho phép **gpx.studio** được hưởng lợi từ các công cụ của Mapbox với giá ưu đãi, góp phần đáng kể vào tính khả thi về tài chính của dự án và giúp chúng tôi mang đến trải nghiệm người dùng tốt nhất có thể. diff --git a/website/src/lib/docs/vi/menu/edit.mdx b/website/src/lib/docs/vi/menu/edit.mdx index ac0b4e9ab..252488c82 100644 --- a/website/src/lib/docs/vi/menu/edit.mdx +++ b/website/src/lib/docs/vi/menu/edit.mdx @@ -9,8 +9,8 @@ title: Edit actions # { title } -Unlike the file actions, the edit actions can potentially modify the content of the currently selected files. -Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, and points of interest](../gpx). +Không giống như các thao tác trên tệp, các thao tác chỉnh sửa có thể thay đổi nội dung của các tệp hiện đang được chọn. +Hơn nữa, khi bố cục dạng cây của danh sách tệp được bật (xem [Tệp và thống kê](../files-and-stats)), chúng cũng có thể được áp dụng cho [đường đi, đoạn đường và điểm quan tâm](../gpx). Therefore, we will refer to the elements that can be modified by these actions as _file items_. Note that except for the undo and redo actions, the edit actions are also accessible through the context menu (right-click) of the file items. diff --git a/website/src/lib/docs/zh/menu/file.mdx b/website/src/lib/docs/zh/menu/file.mdx index 2e470ad00..d9c1ade51 100644 --- a/website/src/lib/docs/zh/menu/file.mdx +++ b/website/src/lib/docs/zh/menu/file.mdx @@ -29,9 +29,9 @@ title: 文件 创建当前选中文件的副本。 -### Delete +### 删除 -Delete the currently selected files. +删除当前选中的文件。 ### 删除全部 diff --git a/website/src/locales/pl.json b/website/src/locales/pl.json index 73a13dedf..e14f48659 100644 --- a/website/src/locales/pl.json +++ b/website/src/locales/pl.json @@ -2,7 +2,7 @@ "metadata": { "home_title": "edytor online plików GPX", "app_title": "Aplikacja", - "embed_title": "Online'owy edytor plików GPX", + "embed_title": "Edytor plików GPX online", "help_title": "pomoc", "404_title": "nie odnaleziono strony", "description": "Przeglądaj, edytuj i twórz pliki GPX online z zaawansowanymi możliwościami planowania trasy i narzędziami do przetwarzania plików, pięknymi mapami i szczegółowymi wizualizacjami danych." @@ -28,7 +28,7 @@ "undo": "Cofnij", "redo": "Ponów", "delete": "Usuń", - "delete_all": "Delete all", + "delete_all": "Usuń wszystko", "select_all": "Zaznacz wszystko", "view": "Widok", "elevation_profile": "Profil wysokości", @@ -80,7 +80,7 @@ "center": "Wyśrodkuj", "open_in": "Otwórz w", "copy_coordinates": "Kopiuj współrzędne", - "edit_osm": "Edit in OpenStreetMap" + "edit_osm": "Edytuj w OpenStreetMap" }, "toolbar": { "routing": { @@ -353,7 +353,7 @@ "water": "Woda", "shower": "Prysznic", "shelter": "Schronienie", - "cemetery": "Cemetery", + "cemetery": "Cmentarz", "motorized": "Samochody i motocykle", "fuel-station": "Stacja paliw", "parking": "Parking", diff --git a/website/src/locales/vi.json b/website/src/locales/vi.json index 63910c2e2..f93e94e0a 100644 --- a/website/src/locales/vi.json +++ b/website/src/locales/vi.json @@ -21,18 +21,18 @@ "export_all": "Xuất tất cả...", "export_options": "Tùy chọn xuất", "support_message": "Công cụ này miễn phí, nhưng nó tốn phí để duy trì hoạt động. Nếu bạn dùng công cụ này thường xuyên, bạn có thể xem xét đóng góp để hỗ trợ chúng tôi. Chúng tôi rất biết ơn vì điều đó!", - "support_button": "Help keep the website free", + "support_button": "Hãy giúp duy trì trang web miễn phí", "download_file": "Tải tệp xuống", "download_files": "Tải xuống tất cả", "edit": "Chỉnh sửa", "undo": "Hoàn tác", "redo": "Khôi phục", "delete": "Xóa", - "delete_all": "Delete all", + "delete_all": "Xóa tất cả", "select_all": "Chọn tất cả", "view": "Xem", "elevation_profile": "Thông tin độ cao", - "tree_file_view": "File tree", + "tree_file_view": "Cây thư mục", "switch_basemap": "Quay lại bản đồ trước đó", "toggle_overlays": "Thay đổi lớp phủ", "toggle_3d": "Chuyển đổi 3D", @@ -57,35 +57,35 @@ "layers": "Lớp bản đồ...", "distance_markers": "Đánh dấu khoảng cách", "direction_markers": "Mũi tên định hướng", - "help": "Help", - "more": "More...", - "donate": "Donate", + "help": "Trợ giúp", + "more": "Chi tiết...", + "donate": "Ủng hộ", "ctrl": "Ctrl", - "click": "Click", - "drag": "Drag", + "click": "Nhấp chuột", + "drag": "Kéo", "metadata": { - "button": "Info...", - "name": "Name", - "description": "Description", - "save": "Save" + "button": "Thông tin...", + "name": "Tên", + "description": "Mô tả", + "save": "Lưu" }, "style": { - "button": "Appearance...", - "color": "Color", - "opacity": "Opacity", - "width": "Width" + "button": "Diện mạo...", + "color": "Màu sắc", + "opacity": "Độ trong suốt", + "width": "Độ rộng" }, - "hide": "Hide", - "unhide": "Unhide", - "center": "Center", - "open_in": "Open in", - "copy_coordinates": "Copy coordinates", - "edit_osm": "Edit in OpenStreetMap" + "hide": "Ẩn", + "unhide": "Bỏ ẩn", + "center": "Giữa", + "open_in": "Mở ra", + "copy_coordinates": "Sao chép tọa độ", + "edit_osm": "" }, "toolbar": { "routing": { "tooltip": "Plan or edit a route", - "activity": "Activity", + "activity": "Hoạt động", "use_routing": "Routing", "use_routing_tooltip": "Connect anchor points via road network, or in a straight line if disabled", "allow_private": "Allow private roads", @@ -94,14 +94,14 @@ "tooltip": "Reverse the direction of the route" }, "route_back_to_start": { - "button": "Back to start", + "button": "Quay về Bắt đầu", "tooltip": "Connect the last point of the route with the starting point" }, "round_trip": { "button": "Round trip", "tooltip": "Return to the starting point by the same route" }, - "start_loop_here": "Start loop here", + "start_loop_here": "Bắt đầu lặp ở đây", "help_no_file": "Select a trace to use the routing tool, or click on the map to start creating a new route.", "help": "Click on the map to add a new anchor point, or drag existing ones to change the route.", "activities": { @@ -127,15 +127,15 @@ "wood": "Wood", "compacted": "Compacted gravel", "fine_gravel": "Fine gravel", - "gravel": "Gravel", + "gravel": "Sỏi", "pebblestone": "Pebblestone", - "rock": "Rock", - "dirt": "Dirt", + "rock": "Đá", + "dirt": "Đất", "ground": "Ground", "earth": "Earth", "mud": "Mud", "sand": "Sand", - "grass": "Grass", + "grass": "Cỏ", "grass_paver": "Grass paver", "clay": "Clay", "stone": "Stone" diff --git a/website/src/locales/zh.json b/website/src/locales/zh.json index e8b234c8e..23128e3b0 100644 --- a/website/src/locales/zh.json +++ b/website/src/locales/zh.json @@ -230,8 +230,8 @@ "help_invalid_selection": "须先选择包含多个轨迹的文件以提取。" }, "elevation": { - "button": "请求数据", - "help": "请求成功后将使用 Mapbox 海拔数据替换原有数据。", + "button": "请求海拔数据", + "help": "请求成功后将移除原有的海拔数据,并使用 Mapbox 的海拔数据替换原有数据。", "help_no_selection": "选择要请求海拔数据的文件。" }, "waypoint": { From 2a0227c1dea8e65b1c054546c84b8fd48c2a1cbc Mon Sep 17 00:00:00 2001 From: vcoppe Date: Sun, 11 Jan 2026 11:09:41 +0100 Subject: [PATCH 19/26] 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) --- website/src/lib/docs/it/files-and-stats.mdx | 4 +- website/src/lib/docs/it/gpx.mdx | 2 +- website/src/lib/docs/it/home/funding.mdx | 4 +- website/src/locales/de.json | 2 +- website/src/locales/es.json | 2 +- website/src/locales/fr.json | 6 +- website/src/locales/it.json | 70 ++++++++++----------- 7 files changed, 45 insertions(+), 45 deletions(-) diff --git a/website/src/lib/docs/it/files-and-stats.mdx b/website/src/lib/docs/it/files-and-stats.mdx index a72f05f97..9fc453316 100644 --- a/website/src/lib/docs/it/files-and-stats.mdx +++ b/website/src/lib/docs/it/files-and-stats.mdx @@ -50,7 +50,7 @@ Facendo clic destro su una scheda file, è possibile accedere alle stesse azioni Come accennato nella [sezione opzioni di visualizzazione](./menu/view), è possibile passare a un layout ad albero per l'elenco dei file. Questo layout è ideale per gestire un gran numero di file aperti, organizzandoli in una lista verticale sul lato destro della mappa. -Inoltre, la vista ad albero dei file consente di ispezionare [tracce, segmenti e punti di interesse](./gpx) all'interno dei file attraverso sezioni espandibili. +Inoltre, la vista ad albero dei file consente d'ispezionare [tracce, segmenti e punti di interesse](./gpx) all'interno dei file attraverso sezioni espandibili. Puoi anche applicare [modifiche](./menu/edit) e [strumenti](./toolbar) agli elementi interni del file. Inoltre, è possibile trascinare e rilasciare gli elementi per riordinarli, o spostarli nella gerarchia o anche in un altro file. @@ -78,7 +78,7 @@ Quando si passa sopra il profilo di elevazione, un suggerimento mostrerà le sta Per ottenere le statistiche per una sezione specifica del profilo di elevazione, è possibile trascinare un rettangolo di selezione sul profilo. Fare clic sul profilo per resettare la selezione. -È inoltre possibile utilizzare la rotellina del mouse per ingrandire e rimpicciolire sul profilo di elevazione, e spostarsi a sinistra e a destra trascinando il profilo tenendo premuto il tasto Maiusc. +È inoltre possibile utilizzare la rotellina del mouse per ingrandire e rimpicciolire sul profilo di elevazione, e spostarsi a sinistra e a destra trascinando il profilo tenendo premuto il tasto Maiuscolo.
Aiuta a mantenere il sito gratuito (e senza pubblicità) Ogni volta che aggiungi o sposti i punti GPS, i nostri server calcolano il percorso migliore sulla rete stradale. -Utilizziamo anche le API di Mapbox per visualizzare mappe gradevoli, recuperare i dati altimetrici e consentire la ricerca di luoghi. +Utilizziamo anche le API di Mapbox per visualizzare mappe stupende, recuperare i dati altimetrici e consentire la ricerca di luoghi. -Sfortunatamente, questo è costoso. +Sfortunatamente, fare tutto ciò è costoso. Se ti piace utilizzare questo strumento e lo trovi utile, per favore considera di fare una piccola donazione per aiutare a mantenere il sito web gratuito e senza pubblicità. Grazie mille per il vostro supporto! ❤️ diff --git a/website/src/locales/de.json b/website/src/locales/de.json index 6c663019a..294be3d8f 100644 --- a/website/src/locales/de.json +++ b/website/src/locales/de.json @@ -473,7 +473,7 @@ }, "homepage": { "website": "Webseite", - "home": "Zuhause", + "home": "Startseite", "app": "App", "contact": "Kontakt", "reddit": "Reddit", diff --git a/website/src/locales/es.json b/website/src/locales/es.json index 145566cc8..58d7cf70f 100644 --- a/website/src/locales/es.json +++ b/website/src/locales/es.json @@ -2,7 +2,7 @@ "metadata": { "home_title": "el editor online de archivos GPX", "app_title": "app", - "embed_title": "El editor online de archivos GPX", + "embed_title": " editor online de archivos GPX", "help_title": "ayuda", "404_title": "página no encontrada", "description": "Mira, edita y crea archivos GPX online con planificación avanzada de rutas y herramientas de procesamiento de archivos, bonitos mapas y visualizaciones detalladas de datos." diff --git a/website/src/locales/fr.json b/website/src/locales/fr.json index 44137b8ca..fd1d41275 100644 --- a/website/src/locales/fr.json +++ b/website/src/locales/fr.json @@ -350,7 +350,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", @@ -443,7 +443,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 +468,7 @@ "summit": "Sommet", "telephone": "Téléphone", "tunnel": "Tunnel", - "water_source": "Source d'eau" + "water_source": "Point d'eau" } }, "homepage": { diff --git a/website/src/locales/it.json b/website/src/locales/it.json index 7d3e2656b..91caca6e2 100644 --- a/website/src/locales/it.json +++ b/website/src/locales/it.json @@ -34,8 +34,8 @@ "elevation_profile": "Profilo altimetrico", "tree_file_view": "Vista ad albero", "switch_basemap": "Passa alla mappa di base precedente", - "toggle_overlays": "Attiva/disattiva le sovrapposizioni", - "toggle_3d": "Attiva/disattiva 3D", + "toggle_overlays": "Attiva / disattiva le sovrapposizioni", + "toggle_3d": "Attiva / disattiva 3D", "settings": "Impostazioni", "distance_units": "Unità distanza", "metric": "Metrico", @@ -53,7 +53,7 @@ "street_view_source": "Sorgente della vista stradale", "mapillary": "Mapillary", "google": "Google", - "toggle_street_view": "Street View", + "toggle_street_view": "Vista stradale", "layers": "Livelli della mappa...", "distance_markers": "Indicatori di distanza", "direction_markers": "Frecce direzionali", @@ -87,7 +87,7 @@ "tooltip": "Pianifica o modifica un percorsoo", "activity": "Attività", "use_routing": "Instradamento", - "use_routing_tooltip": "Collega i punti di ancoraggio tramite la rete stradale o in linea retta se disabilitato", + "use_routing_tooltip": "Collega i punti di ancoraggio tramite la rete stradale (o in linea retta se disabilitato)", "allow_private": "Consenti strade private", "reverse": { "button": "Inverti la traccia", @@ -235,18 +235,18 @@ "help_no_selection": "Seleziona un file per richiedere i dati di altitudine." }, "waypoint": { - "tooltip": "Creare e modificare punti di interesse", + "tooltip": "Creare e modificare punti d'interesse", "icon": "Icona", "link": "Collegamento", "longitude": "Longitudine", "latitude": "Latitudine", - "create": "Creare un punto di interesse", - "add": "Aggiungi punto di interesse al file", - "help": "Compila il modulo per creare un nuovo punto di interesse, oppure fai clic su uno esistente per modificarlo. Fare clic sulla mappa per inserire le coordinate o trascinare i punti di interesse per spostarli.", - "help_no_selection": "Selezionare un file per creare o modificare punti di interesse." + "create": "Creare un punto d'interesse", + "add": "Aggiungi punto d'interesse al file", + "help": "Compila il modulo per creare un nuovo punto d'interesse, oppure fai clic su uno esistente per modificarlo. Fare clic sulla mappa per inserire le coordinate o trascinare i punti d'interesse per spostarli.", + "help_no_selection": "Selezionare un file per creare o modificare punti d'interesse." }, "reduce": { - "tooltip": "Riduci il numero di punti della traccia", + "tooltip": "Riduci il numero di punti GPS", "tolerance": "Tolleranza", "number_of_points": "Numero di punti GPS", "button": "Minimizza", @@ -254,14 +254,14 @@ "help_no_selection": "Selezionare una traccia per ridurre il numero dei suoi punti GPS." }, "clean": { - "tooltip": "Pulire i punti GPS e i punti di interesse con una selezione rettangolare", + "tooltip": "Pulire i punti GPS e i punti d'interesse con una selezione rettangolare", "delete_trackpoints": "Eliminare punti GPS", "delete_waypoints": "Cancella punti d'interesse", "delete_inside": "Elimina all'interno della selezione", "delete_outside": "Elimina fuori dalla selezione", "button": "Elimina", - "help": "Selezionare un'area rettangolare sulla mappa per rimuovere i punti GPS e i punti di interesse.", - "help_no_selection": "Seleziona una traccia per pulire i punti GPS e i punti di interesse." + "help": "Selezionare un'area rettangolare sulla mappa per rimuovere i punti GPS e i punti d'interesse.", + "help_no_selection": "Seleziona una traccia per pulire i punti GPS e i punti d'interesse." } }, "layers": { @@ -273,7 +273,7 @@ "new": "Nuovo livello personalizzato", "edit": "Modifica livello personalizzato", "urls": "URL(s)", - "url_placeholder": "WMTS, WMS o Mapbox stile JSON", + "url_placeholder": "WMTS, WMS o JSON in stile Mapbox", "max_zoom": "Zoom massimo", "layer_type": "Tipo del layer", "basemap": "Mappa Base", @@ -309,7 +309,7 @@ "linz": "LINZ Topo", "linzTopo": "LINZ Topo50", "swisstopoRaster": "swisstopo Raster", - "swisstopoVector": "Swisstopo Vector", + "swisstopoVector": "swisstopo Vector", "swisstopoSatellite": "swisstopo Satellite", "ignBe": "IGN Topo", "ignFrPlan": "IGN Plan", @@ -318,25 +318,25 @@ "ignFrSatellite": "Satellitare IGN", "ignEs": "IGN Topo", "ignEsSatellite": "Satellitare IGN", - "ordnanceSurvey": "Sondaggio Ordnance", + "ordnanceSurvey": "Ordnance Survey", "norwayTopo": "Topografisk Norgeskart 4", - "finlandTopo": "Carta topografica del vecchio Catasto svedese", + "finlandTopo": "Lantmäteriverket Terrängkarta", "bgMountains": "BGMountains", "usgs": "USGS", "bikerouterGravel": "bikerouter.de Gravel", "cyclOSMlite": "CyclOSM Lite", - "swisstopoSlope": "Carta topografica Svizzera Pendenza", - "swisstopoHiking": "Carta topografica Svedese Escursione", - "swisstopoHikingClosures": "Carta topografica Svizzera Fine escursione", - "swisstopoCycling": "Carta topografica Svizzera Ciclabile", - "swisstopoCyclingClosures": "Carta topografica Svizzera fine ciclabile", - "swisstopoMountainBike": "Carta topografica Svizzera MTB", - "swisstopoMountainBikeClosures": "Carta topografica Svizzera fine MTB", - "swisstopoSkiTouring": "Carta topografica Svizzera pista sci", + "swisstopoSlope": "swisstopo Pendenza", + "swisstopoHiking": "swisstopo Escursione", + "swisstopoHikingClosures": "swisstopo Fine escursione", + "swisstopoCycling": "swisstopo Ciclabile", + "swisstopoCyclingClosures": "swisstopo Fine ciclabile", + "swisstopoMountainBike": "swisstopo MTB", + "swisstopoMountainBikeClosures": "swisstopo Fine MTB", + "swisstopoSkiTouring": "swisstopo Sci Alpinismo", "ignFrCadastre": "IGN Catasto", - "ignSlope": "Pendenza IGN", - "ignSkiTouring": "IGN Sciescursionismo", - "waymarked_trails": "Waymarked Trails", + "ignSlope": "IGN Pendenza", + "ignSkiTouring": "IGN Sci Alpinismo", + "waymarked_trails": "Sentieri Segnalati", "waymarkedTrailsHiking": "Escursionismo", "waymarkedTrailsCycling": "Ciclismo", "waymarkedTrailsMTB": "MTB", @@ -406,11 +406,11 @@ "feet": "ft", "kilometers": "km", "miles": "mi", - "nautical_miles": "nm", + "nautical_miles": "NM", "celsius": "°C", "fahrenheit": "°F", "kilometers_per_hour": "km/h", - "miles_per_hour": "mph", + "miles_per_hour": "mi/h", "minutes_per_kilometer": "min/km", "minutes_per_mile": "min/mi", "minutes_per_nautical_mile": "min/nm", @@ -426,8 +426,8 @@ "tracks": "Tracce", "segment": "Segmento", "segments": "Segmenti", - "waypoint": "Punto di interesse", - "waypoints": "Punti di interesse", + "waypoint": "Punto d'interesse", + "waypoints": "Punti d'interesse", "symbol": { "alert": "Avviso", "anchor": "Ancora", @@ -483,13 +483,13 @@ "email": "Email", "contribute": "Contribuire", "supported_by": "supportato da", - "support_button": "Supporto di gpx.studio su Ko-fi", + "support_button": "Supporta gpx.studio su Ko-fi", "route_planning": "Pianificazione del percorso", - "route_planning_description": "Un'interfaccia intuitiva per creare itinerari su misura per ogni sport, basati sui dati OpenStreetMap.", + "route_planning_description": "Un'interfaccia intuitiva per creare itinerari su misura per ogni sport, basata sui dati OpenStreetMap.", "file_processing": "Elaborazione avanzata dei file", "file_processing_description": "Una serie di strumenti per eseguire tutte le attività comuni di elaborazione dei file e che possono essere applicati a più file contemporaneamente.", "maps": "Mappe globali e locali", - "maps_description": "Una vasta collezione di mappe di base, sovrapposizioni e punti d'interesse per aiutarti a creare la tua prossima avventura all'aperto o visualizzare il tuo ultimo risultato.", + "maps_description": "Una vasta collezione di mappe di base, sovrapposizioni e punti d'interesse per aiutarti a creare la tua prossima avventura all'aperto o visualizzare la tua ultima impresa.", "data_visualization": "Visualizzazione dei dati", "data_visualization_description": "Un profilo di elevazione interattivo con statistiche dettagliate per analizzare attività registrate e obiettivi futuri.", "identity": "Gratuito, senza pubblicità e open source", From 9019317e5c9fc8b4a1555241ba1508f5979c2bd0 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Sun, 11 Jan 2026 19:06:54 +0100 Subject: [PATCH 20/26] fix prettier paths, continued --- .prettierignore | 3 +++ website/.prettierignore | 3 --- website/package.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 .prettierignore delete mode 100644 website/.prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..705b6c3cb --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +website/src/lib/components/ui +website/src/lib/docs/**/*.mdx +**/*.webmanifest \ No newline at end of file diff --git a/website/.prettierignore b/website/.prettierignore deleted file mode 100644 index fc85c07bc..000000000 --- a/website/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -src/lib/components/ui -src/lib/docs/**/*.mdx -**/*.webmanifest \ No newline at end of file diff --git a/website/package.json b/website/package.json index e74a9d66f..c03e5d40d 100644 --- a/website/package.json +++ b/website/package.json @@ -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", From f24956c58d9ea289e8fa1a5c4eb5eb6aebc6a938 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Sun, 11 Jan 2026 19:48:48 +0100 Subject: [PATCH 21/26] improve grouping statistics performance --- gpx/src/gpx.ts | 506 ++++-------------- gpx/src/index.ts | 1 + gpx/src/statistics.ts | 391 ++++++++++++++ .../src/lib/components/GPXStatistics.svelte | 26 +- .../elevation-profile/ElevationProfile.svelte | 6 +- .../elevation-profile/elevation-profile.ts | 160 +++--- .../src/lib/components/export/Export.svelte | 20 +- .../file-list/FileListNodeLabel.svelte | 16 +- .../map/gpx-layer/distance-markers.ts | 16 +- .../map/gpx-layer/start-end-markers.ts | 15 +- .../toolbar/tools/reduce/utils.svelte.ts | 8 +- .../toolbar/tools/routing/routing-controls.ts | 19 +- .../toolbar/tools/scissors/Scissors.svelte | 8 +- website/src/lib/logic/file-actions.ts | 25 +- website/src/lib/logic/statistics-tree.ts | 25 +- website/src/lib/logic/statistics.ts | 17 +- 16 files changed, 668 insertions(+), 591 deletions(-) create mode 100644 gpx/src/statistics.ts diff --git a/gpx/src/gpx.ts b/gpx/src/gpx.ts index 0a0aff9db..ef82f91e4 100644 --- a/gpx/src/gpx.ts +++ b/gpx/src/gpx.ts @@ -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> { 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> 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 { }); } + 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 { !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 { 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 = {}; 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>; - }; - 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; } diff --git a/gpx/src/index.ts b/gpx/src/index.ts index 1d79b1bc7..984e6893b 100644 --- a/gpx/src/index.ts +++ b/gpx/src/index.ts @@ -1,4 +1,5 @@ export * from './gpx'; +export * from './statistics'; export { Coordinates, LineStyleExtension, WaypointType } from './types'; export { parseGPX, buildGPX } from './io'; export * from './simplify'; diff --git a/gpx/src/statistics.ts b/gpx/src/statistics.ts new file mode 100644 index 000000000..f93d99188 --- /dev/null +++ b/gpx/src/statistics.ts @@ -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>; + + 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 + ) + ); + } + } +} diff --git a/website/src/lib/components/GPXStatistics.svelte b/website/src/lib/components/GPXStatistics.svelte index 301a70aaf..ecc38577b 100644 --- a/website/src/lib/components/GPXStatistics.svelte +++ b/website/src/lib/components/GPXStatistics.svelte @@ -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; - slicedGPXStatistics: Readable<[GPXStatistics, number, number] | undefined>; + gpxStatistics: Readable; + 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 ); @@ -42,15 +42,15 @@ - + - + - + {#if panelSize > 120 || orientation === 'horizontal'} @@ -64,13 +64,9 @@ > - + / - + {/if} @@ -83,9 +79,9 @@ > - + / - + {/if} diff --git a/website/src/lib/components/elevation-profile/ElevationProfile.svelte b/website/src/lib/components/elevation-profile/ElevationProfile.svelte index b9eb9dfe6..df469e107 100644 --- a/website/src/lib/components/elevation-profile/ElevationProfile.svelte +++ b/website/src/lib/components/elevation-profile/ElevationProfile.svelte @@ -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; - slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>; + gpxStatistics: Readable; + slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>; additionalDatasets: Writable; elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>; showControls?: boolean; diff --git a/website/src/lib/components/elevation-profile/elevation-profile.ts b/website/src/lib/components/elevation-profile/elevation-profile.ts index f1ca8a6d8..54b398139 100644 --- a/website/src/lib/components/elevation-profile/elevation-profile.ts +++ b/website/src/lib/components/elevation-profile/elevation-profile.ts @@ -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; - private _slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>; + private _gpxStatistics: Readable; + private _slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>; private _additionalDatasets: Readable; private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>; constructor( - gpxStatistics: Readable, - slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>, + gpxStatistics: Readable, + slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>, additionalDatasets: Readable, 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> = [[], [], [], [], [], []]; + 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( diff --git a/website/src/lib/components/export/Export.svelte b/website/src/lib/components/export/Export.svelte index 30d73822b..d2c7f8af6 100644 --- a/website/src/lib/components/export/Export.svelte +++ b/website/src/lib/components/export/Export.svelte @@ -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, }; } }); diff --git a/website/src/lib/components/file-list/FileListNodeLabel.svelte b/website/src/lib/components/file-list/FileListNodeLabel.svelte index d4fe4b2eb..78d970fe3 100644 --- a/website/src/lib/components/file-list/FileListNodeLabel.svelte +++ b/website/src/lib/components/file-list/FileListNodeLabel.svelte @@ -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()); diff --git a/website/src/lib/components/map/gpx-layer/distance-markers.ts b/website/src/lib/components/map/gpx-layer/distance-markers.ts index 99db27331..4a7f9d5c7 100644 --- a/website/src/lib/components/map/gpx-layer/distance-markers.ts +++ b/website/src/lib/components/map/gpx-layer/distance-markers.ts @@ -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', diff --git a/website/src/lib/components/map/gpx-layer/start-end-markers.ts b/website/src/lib/components/map/gpx-layer/start-end-markers.ts index a392685ad..cbf5fb217 100644 --- a/website/src/lib/components/map/gpx-layer/start-end-markers.ts +++ b/website/src/lib/components/map/gpx-layer/start-end-markers.ts @@ -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 { diff --git a/website/src/lib/components/toolbar/tools/reduce/utils.svelte.ts b/website/src/lib/components/toolbar/tools/reduce/utils.svelte.ts index ad496ce9c..af11ca8c4 100644 --- a/website/src/lib/components/toolbar/tools/reduce/utils.svelte.ts +++ b/website/src/lib/components/toolbar/tools/reduce/utils.svelte.ts @@ -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), ]); }); } diff --git a/website/src/lib/components/toolbar/tools/routing/routing-controls.ts b/website/src/lib/components/toolbar/tools/routing/routing-controls.ts index d58ba212a..57fb3be89 100644 --- a/website/src/lib/components/toolbar/tools/routing/routing-controls.ts +++ b/website/src/lib/components/toolbar/tools/routing/routing-controls.ts @@ -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 ); } diff --git a/website/src/lib/components/toolbar/tools/scissors/Scissors.svelte b/website/src/lib/components/toolbar/tools/scissors/Scissors.svelte index 2b4e4d833..6b9aef8e6 100644 --- a/website/src/lib/components/toolbar/tools/scissors/Scissors.svelte +++ b/website/src/lib/components/toolbar/tools/scissors/Scissors.svelte @@ -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], ]; diff --git a/website/src/lib/logic/file-actions.ts b/website/src/lib/logic/file-actions.ts index 0798e333a..8ffd4cbb1 100644 --- a/website/src/lib/logic/file-actions.ts +++ b/website/src/lib/logic/file-actions.ts @@ -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; + } } } } diff --git a/website/src/lib/logic/statistics-tree.ts b/website/src/lib/logic/statistics-tree.ts index a1ee5869d..6ef8041c3 100644 --- a/website/src/lib/logic/statistics-tree.ts +++ b/website/src/lib/logic/statistics-tree.ts @@ -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 }; diff --git a/website/src/lib/logic/statistics.ts b/website/src/lib/logic/statistics.ts index cf3a09f0d..1193c21a0 100644 --- a/website/src/lib/logic/statistics.ts +++ b/website/src/lib/logic/statistics.ts @@ -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; + private _statistics: Writable; 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(() => { From 59f31caf265d9731e08f586d3623931361918a91 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Sun, 11 Jan 2026 20:18:00 +0100 Subject: [PATCH 22/26] add openrailwaymap overlay, closes #298 --- website/src/lib/assets/layers.ts | 23 +++++++++++++++++++++++ website/src/locales/en.json | 1 + 2 files changed, 24 insertions(+) diff --git a/website/src/lib/assets/layers.ts b/website/src/lib/assets/layers.ts index ce31acf54..de993807e 100644 --- a/website/src/lib/assets/layers.ts +++ b/website/src/lib/assets/layers.ts @@ -368,6 +368,26 @@ 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 © OpenStreetMap contributors, Style: CC-BY-SA 2.0 OpenRailwayMap', + }, + }, + layers: [ + { + id: 'openRailwayMap', + type: 'raster', + source: 'openRailwayMap', + }, + ], + }, swisstopoSlope: { version: 8, sources: { @@ -801,6 +821,7 @@ export const overlayTree: LayerTreeType = { }, cyclOSMlite: true, bikerouterGravel: true, + openRailwayMap: true, }, countries: { france: { @@ -885,6 +906,7 @@ export const defaultOverlays: LayerTreeType = { }, cyclOSMlite: false, bikerouterGravel: false, + openRailwayMap: false, }, countries: { france: { @@ -1020,6 +1042,7 @@ export const defaultOverlayTree: LayerTreeType = { }, cyclOSMlite: false, bikerouterGravel: false, + openRailwayMap: false, }, countries: { france: { diff --git a/website/src/locales/en.json b/website/src/locales/en.json index 2350951d5..8e76edd35 100644 --- a/website/src/locales/en.json +++ b/website/src/locales/en.json @@ -324,6 +324,7 @@ "bgMountains": "BGMountains", "usgs": "USGS", "bikerouterGravel": "bikerouter.de Gravel", + "openRailwayMap": "OpenRailwayMap", "cyclOSMlite": "CyclOSM Lite", "swisstopoSlope": "swisstopo Slope", "swisstopoHiking": "swisstopo Hiking", From 4e18e3c8a0251229ac293e25c5cd3ad66982cd82 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Fri, 16 Jan 2026 18:25:27 +0100 Subject: [PATCH 23/26] update year --- LICENSE | 2 +- website/src/lib/components/Footer.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index c2ce4a3ae..846b46dd9 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/website/src/lib/components/Footer.svelte b/website/src/lib/components/Footer.svelte index 97f60ad4a..d6ae72222 100644 --- a/website/src/lib/components/Footer.svelte +++ b/website/src/lib/components/Footer.svelte @@ -18,7 +18,7 @@ href="https://github.com/gpxstudio/gpx.studio/blob/main/LICENSE" target="_blank" > - MIT © 2025 gpx.studio + MIT © 2026 gpx.studio
From f7c0805161e7235f28f686c15e2f69c68a842b71 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Fri, 16 Jan 2026 18:32:32 +0100 Subject: [PATCH 24/26] add mapterhorn hillshade overlay, closes #292 --- website/src/lib/assets/layers.ts | 25 ++++++++++++++++++++++--- website/src/locales/en.json | 3 ++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/website/src/lib/assets/layers.ts b/website/src/lib/assets/layers.ts index de993807e..099402cc1 100644 --- a/website/src/lib/assets/layers.ts +++ b/website/src/lib/assets/layers.ts @@ -388,6 +388,22 @@ export const overlays: { [key: string]: string | StyleSpecification } = { }, ], }, + 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: { @@ -819,8 +835,9 @@ export const overlayTree: LayerTreeType = { waymarkedTrailsHorseRiding: true, waymarkedTrailsWinter: true, }, - cyclOSMlite: true, bikerouterGravel: true, + cyclOSMlite: true, + mapterhornHillshade: true, openRailwayMap: true, }, countries: { @@ -904,8 +921,9 @@ export const defaultOverlays: LayerTreeType = { waymarkedTrailsHorseRiding: false, waymarkedTrailsWinter: false, }, - cyclOSMlite: false, bikerouterGravel: false, + cyclOSMlite: false, + mapterhornHillshade: false, openRailwayMap: false, }, countries: { @@ -1040,8 +1058,9 @@ export const defaultOverlayTree: LayerTreeType = { waymarkedTrailsHorseRiding: false, waymarkedTrailsWinter: false, }, - cyclOSMlite: false, bikerouterGravel: false, + cyclOSMlite: false, + mapterhornHillshade: false, openRailwayMap: false, }, countries: { diff --git a/website/src/locales/en.json b/website/src/locales/en.json index 8e76edd35..f3b65d535 100644 --- a/website/src/locales/en.json +++ b/website/src/locales/en.json @@ -324,8 +324,9 @@ "bgMountains": "BGMountains", "usgs": "USGS", "bikerouterGravel": "bikerouter.de Gravel", - "openRailwayMap": "OpenRailwayMap", "cyclOSMlite": "CyclOSM Lite", + "mapterhornHillshade": "Mapterhorn Hillshade", + "openRailwayMap": "OpenRailwayMap", "swisstopoSlope": "swisstopo Slope", "swisstopoHiking": "swisstopo Hiking", "swisstopoHikingClosures": "swisstopo Hiking Closures", From 2eb6ef6f032181d9286ec7ef774435378aa8aaeb Mon Sep 17 00:00:00 2001 From: vcoppe Date: Fri, 16 Jan 2026 19:16:28 +0100 Subject: [PATCH 25/26] new setting for selecting terrain source --- website/src/lib/assets/layers.ts | 17 +++++- .../layer-control/LayerControlSettings.svelte | 19 +++++++ website/src/lib/components/map/map.ts | 54 +++++++++++-------- website/src/lib/logic/settings.ts | 2 + website/src/locales/en.json | 5 +- 5 files changed, 72 insertions(+), 25 deletions(-) diff --git a/website/src/lib/assets/layers.ts b/website/src/lib/assets/layers.ts index 099402cc1..44b1cf72e 100644 --- a/website/src/lib/assets/layers.ts +++ b/website/src/lib/assets/layers.ts @@ -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'; @@ -1453,3 +1453,18 @@ export const overpassQueryData: Record = { 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'; diff --git a/website/src/lib/components/map/layer-control/LayerControlSettings.svelte b/website/src/lib/components/map/layer-control/LayerControlSettings.svelte index 27ace9277..5675bae8a 100644 --- a/website/src/lib/components/map/layer-control/LayerControlSettings.svelte +++ b/website/src/lib/components/map/layer-control/LayerControlSettings.svelte @@ -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 @@ + + {i18n._('layers.terrain')} + + + + {i18n._(`layers.label.${$terrainSource}`)} + + + {#each Object.keys(terrainSources) as id} + + {i18n._(`layers.label.${id}`)} + + {/each} + + + + diff --git a/website/src/lib/components/map/map.ts b/website/src/lib/components/map/map.ts index 14e83462a..2983f3f47 100644 --- a/website/src/lib/components/map/map.ts +++ b/website/src/lib/components/map/map.ts @@ -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(); diff --git a/website/src/lib/logic/settings.ts b/website/src/lib/logic/settings.ts index f4e6e2cf8..c86df6ac0 100644 --- a/website/src/lib/logic/settings.ts +++ b/website/src/lib/logic/settings.ts @@ -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>('customLayers', {}), customBasemapOrder: new Setting('customBasemapOrder', []), customOverlayOrder: new Setting('customOverlayOrder', []), + terrainSource: new Setting('terrainSource', defaultTerrainSource), directionMarkers: new Setting('directionMarkers', false), distanceMarkers: new Setting('distanceMarkers', false), streetViewSource: new Setting('streetViewSource', 'mapillary'), diff --git a/website/src/locales/en.json b/website/src/locales/en.json index f3b65d535..4bf445f04 100644 --- a/website/src/locales/en.json +++ b/website/src/locales/en.json @@ -282,6 +282,7 @@ "update": "Update layer" }, "opacity": "Overlay opacity", + "terrain": "Terrain source", "label": { "basemaps": "Basemaps", "overlays": "Overlays", @@ -379,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": { From f0f1ecb2df07616b7ebd810405fb7a00bc7125ce Mon Sep 17 00:00:00 2001 From: vcoppe Date: Fri, 16 Jan 2026 20:06:44 +0100 Subject: [PATCH 26/26] 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) --- website/src/lib/docs/de/home/mapbox.mdx | 2 +- website/src/locales/be.json | 7 ++++++- website/src/locales/ca.json | 7 ++++++- website/src/locales/cs.json | 7 ++++++- website/src/locales/da.json | 7 ++++++- website/src/locales/de.json | 7 ++++++- website/src/locales/el.json | 7 ++++++- website/src/locales/es.json | 7 ++++++- website/src/locales/eu.json | 7 ++++++- website/src/locales/fi.json | 7 ++++++- website/src/locales/fr.json | 7 ++++++- website/src/locales/he.json | 7 ++++++- website/src/locales/hu.json | 7 ++++++- website/src/locales/id.json | 7 ++++++- website/src/locales/it.json | 7 ++++++- website/src/locales/ko.json | 7 ++++++- website/src/locales/lt.json | 7 ++++++- website/src/locales/lv.json | 7 ++++++- website/src/locales/nl.json | 7 ++++++- website/src/locales/no.json | 7 ++++++- website/src/locales/pl.json | 7 ++++++- website/src/locales/pt-BR.json | 7 ++++++- website/src/locales/pt.json | 7 ++++++- website/src/locales/ro.json | 7 ++++++- website/src/locales/ru.json | 7 ++++++- website/src/locales/sr.json | 7 ++++++- website/src/locales/sv.json | 7 ++++++- website/src/locales/th.json | 7 ++++++- website/src/locales/tr.json | 7 ++++++- website/src/locales/uk.json | 7 ++++++- website/src/locales/vi.json | 7 ++++++- website/src/locales/zh-HK.json | 7 ++++++- website/src/locales/zh.json | 7 ++++++- 33 files changed, 193 insertions(+), 33 deletions(-) diff --git a/website/src/lib/docs/de/home/mapbox.mdx b/website/src/lib/docs/de/home/mapbox.mdx index d9d7c4126..6c4372a89 100644 --- a/website/src/lib/docs/de/home/mapbox.mdx +++ b/website/src/lib/docs/de/home/mapbox.mdx @@ -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 Karten-Engine welche **gpx.studio** unterstützt. -Wir sind äusserst glücklich und dankbar, Teil ihres Community 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 Community 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. diff --git a/website/src/locales/be.json b/website/src/locales/be.json index f94f456ea..441bec5a2 100644 --- a/website/src/locales/be.json +++ b/website/src/locales/be.json @@ -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": { diff --git a/website/src/locales/ca.json b/website/src/locales/ca.json index 3437c8574..a20e4a770 100644 --- a/website/src/locales/ca.json +++ b/website/src/locales/ca.json @@ -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": { diff --git a/website/src/locales/cs.json b/website/src/locales/cs.json index 7d283a256..42bc9ed9a 100644 --- a/website/src/locales/cs.json +++ b/website/src/locales/cs.json @@ -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": { diff --git a/website/src/locales/da.json b/website/src/locales/da.json index 9d0ae6623..282061a9a 100644 --- a/website/src/locales/da.json +++ b/website/src/locales/da.json @@ -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": { diff --git a/website/src/locales/de.json b/website/src/locales/de.json index 294be3d8f..f041ec3bc 100644 --- a/website/src/locales/de.json +++ b/website/src/locales/de.json @@ -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": { diff --git a/website/src/locales/el.json b/website/src/locales/el.json index 0137ac426..041f2726e 100644 --- a/website/src/locales/el.json +++ b/website/src/locales/el.json @@ -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": { diff --git a/website/src/locales/es.json b/website/src/locales/es.json index 58d7cf70f..5f2cbc8e4 100644 --- a/website/src/locales/es.json +++ b/website/src/locales/es.json @@ -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": { diff --git a/website/src/locales/eu.json b/website/src/locales/eu.json index 7f68f17d4..7aac98cf2 100644 --- a/website/src/locales/eu.json +++ b/website/src/locales/eu.json @@ -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": { diff --git a/website/src/locales/fi.json b/website/src/locales/fi.json index 6e46b70eb..754754d59 100644 --- a/website/src/locales/fi.json +++ b/website/src/locales/fi.json @@ -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": { diff --git a/website/src/locales/fr.json b/website/src/locales/fr.json index fd1d41275..033e2068f 100644 --- a/website/src/locales/fr.json +++ b/website/src/locales/fr.json @@ -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", @@ -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": { diff --git a/website/src/locales/he.json b/website/src/locales/he.json index a8143d282..2e2b27c2f 100644 --- a/website/src/locales/he.json +++ b/website/src/locales/he.json @@ -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": { diff --git a/website/src/locales/hu.json b/website/src/locales/hu.json index a187cfac6..0e9c76af8 100644 --- a/website/src/locales/hu.json +++ b/website/src/locales/hu.json @@ -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": { diff --git a/website/src/locales/id.json b/website/src/locales/id.json index 92f8c3fd1..6b63a575e 100644 --- a/website/src/locales/id.json +++ b/website/src/locales/id.json @@ -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": { diff --git a/website/src/locales/it.json b/website/src/locales/it.json index 91caca6e2..0c52c4584 100644 --- a/website/src/locales/it.json +++ b/website/src/locales/it.json @@ -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": { diff --git a/website/src/locales/ko.json b/website/src/locales/ko.json index 9fed8012f..918e9fcbe 100644 --- a/website/src/locales/ko.json +++ b/website/src/locales/ko.json @@ -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": { diff --git a/website/src/locales/lt.json b/website/src/locales/lt.json index 285823419..2ddc8046e 100644 --- a/website/src/locales/lt.json +++ b/website/src/locales/lt.json @@ -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": { diff --git a/website/src/locales/lv.json b/website/src/locales/lv.json index db6b57bb7..cbae3f1de 100644 --- a/website/src/locales/lv.json +++ b/website/src/locales/lv.json @@ -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": { diff --git a/website/src/locales/nl.json b/website/src/locales/nl.json index 69765238d..ad9332628 100644 --- a/website/src/locales/nl.json +++ b/website/src/locales/nl.json @@ -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": { diff --git a/website/src/locales/no.json b/website/src/locales/no.json index 79a862ecb..45baa5a48 100644 --- a/website/src/locales/no.json +++ b/website/src/locales/no.json @@ -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": { diff --git a/website/src/locales/pl.json b/website/src/locales/pl.json index e14f48659..2cd6a32a1 100644 --- a/website/src/locales/pl.json +++ b/website/src/locales/pl.json @@ -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": { diff --git a/website/src/locales/pt-BR.json b/website/src/locales/pt-BR.json index fc28dc3c4..bb9e3a59c 100644 --- a/website/src/locales/pt-BR.json +++ b/website/src/locales/pt-BR.json @@ -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": { diff --git a/website/src/locales/pt.json b/website/src/locales/pt.json index 56fc94b55..8dfbb1f3e 100644 --- a/website/src/locales/pt.json +++ b/website/src/locales/pt.json @@ -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": { diff --git a/website/src/locales/ro.json b/website/src/locales/ro.json index a3623708d..3853df374 100644 --- a/website/src/locales/ro.json +++ b/website/src/locales/ro.json @@ -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": { diff --git a/website/src/locales/ru.json b/website/src/locales/ru.json index 23064dde5..5b169ec3d 100644 --- a/website/src/locales/ru.json +++ b/website/src/locales/ru.json @@ -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": { diff --git a/website/src/locales/sr.json b/website/src/locales/sr.json index 8acc6bd3c..53e80c876 100644 --- a/website/src/locales/sr.json +++ b/website/src/locales/sr.json @@ -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": { diff --git a/website/src/locales/sv.json b/website/src/locales/sv.json index c8781b6fb..f6d259eda 100644 --- a/website/src/locales/sv.json +++ b/website/src/locales/sv.json @@ -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": { diff --git a/website/src/locales/th.json b/website/src/locales/th.json index aec6bb4b5..ff1210312 100644 --- a/website/src/locales/th.json +++ b/website/src/locales/th.json @@ -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": { diff --git a/website/src/locales/tr.json b/website/src/locales/tr.json index 63740c69f..1f27c242e 100644 --- a/website/src/locales/tr.json +++ b/website/src/locales/tr.json @@ -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": { diff --git a/website/src/locales/uk.json b/website/src/locales/uk.json index 4b508f4fc..2a8725e92 100644 --- a/website/src/locales/uk.json +++ b/website/src/locales/uk.json @@ -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": { diff --git a/website/src/locales/vi.json b/website/src/locales/vi.json index f93e94e0a..297b1bbb1 100644 --- a/website/src/locales/vi.json +++ b/website/src/locales/vi.json @@ -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": { diff --git a/website/src/locales/zh-HK.json b/website/src/locales/zh-HK.json index 54f37955c..13590168d 100644 --- a/website/src/locales/zh-HK.json +++ b/website/src/locales/zh-HK.json @@ -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": { diff --git a/website/src/locales/zh.json b/website/src/locales/zh.json index 23128e3b0..a18e741ea 100644 --- a/website/src/locales/zh.json +++ b/website/src/locales/zh.json @@ -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": {