24 Commits

Author SHA1 Message Date
vcoppe
ca464d16c7 New translations en.json (Polish) 2025-12-31 11:39:54 +01:00
vcoppe
e0b7dd881b New translations en.json (Chinese Simplified) 2025-12-28 12:02:45 +01:00
vcoppe
f62916b19d New translations file.mdx (Chinese Simplified) 2025-12-27 03:51:27 +01:00
vcoppe
c6dfefad45 New translations edit.mdx (Vietnamese) 2025-12-24 19:40:22 +01:00
vcoppe
09d307e6a9 New translations mapbox.mdx (Vietnamese) 2025-12-24 19:40:21 +01:00
vcoppe
6688dba9d1 New translations funding.mdx (Vietnamese) 2025-12-24 19:40:20 +01:00
vcoppe
fa459e0063 New translations en.json (Vietnamese) 2025-12-24 19:40:19 +01:00
vcoppe
f54de4eb3e New translations en.json (Vietnamese) 2025-12-24 18:36:45 +01:00
vcoppe
d3e733aa3e fix wpt colors 2025-12-24 16:34:40 +01:00
vcoppe
a011768d2d computeStatistics compatible with read-only segment 2025-12-24 16:12:44 +01:00
vcoppe
4b45b5d716 update bits-ui 2025-12-24 15:27:44 +01:00
vcoppe
ebe9681c12 avoid creating useless data 2025-12-24 13:31:04 +01:00
vcoppe
51c85e4cd5 fix wpt to segment matching 2025-12-24 13:07:22 +01:00
vcoppe
2e171dfbee speed up wpt to segment matching 2025-12-24 12:43:24 +01:00
vcoppe
a6a3917986 improve statistics tree performance 2025-12-24 12:21:27 +01:00
vcoppe
21f2448213 improve cloning performance 2025-12-24 12:03:36 +01:00
vcoppe
e7a1d0488b fix ts error 2025-12-24 10:23:15 +01:00
vcoppe
22b8e0edb4 update chartjs 2025-12-24 10:11:43 +01:00
vcoppe
d062a38e8f new mapbox version 2025-12-24 08:48:50 +01:00
vcoppe
affa59130f simplify filter for hiding layers 2025-12-24 08:48:21 +01:00
vcoppe
3c816567bc use explicit path for prettierrc file, closes #289 2025-12-23 17:34:34 +01:00
vcoppe
e92e48ffde New Crowdin updates (#290)
* New translations en.json (Spanish)

* New translations getting-started.mdx (Dutch)

* New translations edit.mdx (Dutch)

* New translations en.json (Basque)

* New translations file.mdx (Basque)

* New translations en.json (Chinese Simplified)

* New translations file.mdx (Chinese Simplified)
2025-12-18 17:41:09 +01:00
vcoppe
4ce7777b86 New Crowdin updates (#286)
* New translations en.json (Italian)

* New translations file.mdx (Italian)
2025-12-08 20:18:25 +01:00
vcoppe
bc130ad867 use current zoom for osm edit link 2025-12-08 20:17:57 +01:00
30 changed files with 490 additions and 445 deletions

View File

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

View File

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

View File

@@ -16,16 +16,6 @@ import {
} from './types'; } from './types';
import { immerable, isDraft, original, freeze } from 'immer'; import { immerable, isDraft, original, freeze } from 'immer';
function cloneJSON<T>(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 // An abstract class that groups functions that need to be computed recursively in the GPX file hierarchy
export abstract class GPXTreeElement<T extends GPXTreeElement<any>> { export abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
_data: { [key: string]: any } = {}; _data: { [key: string]: any } = {};
@@ -148,7 +138,9 @@ export class GPXFile extends GPXTreeNode<Track> {
}, },
}, },
}; };
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)) : []; this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : [];
if (gpx.rte && gpx.rte.length > 0) { if (gpx.rte && gpx.rte.length > 0) {
this.trk = this.trk.concat(gpx.rte.map((route) => convertRouteToTrack(route))); this.trk = this.trk.concat(gpx.rte.map((route) => convertRouteToTrack(route)));
@@ -186,9 +178,6 @@ export class GPXFile extends GPXTreeNode<Track> {
segment._data['segmentIndex'] = segmentIndex; segment._data['segmentIndex'] = segmentIndex;
}); });
}); });
this.wpt.forEach((waypoint, waypointIndex) => {
waypoint._data['index'] = waypointIndex;
});
} }
get children(): Array<Track> { get children(): Array<Track> {
@@ -249,12 +238,12 @@ export class GPXFile extends GPXTreeNode<Track> {
clone(): GPXFile { clone(): GPXFile {
return new GPXFile({ return new GPXFile({
attributes: cloneJSON(this.attributes), attributes: structuredClone(this.attributes),
metadata: cloneJSON(this.metadata), metadata: structuredClone(this.metadata),
wpt: this.wpt.map((waypoint) => waypoint.clone()), wpt: this.wpt.map((waypoint) => waypoint.clone()),
trk: this.trk.map((track) => track.clone()), trk: this.trk.map((track) => track.clone()),
rte: [], rte: [],
_data: cloneJSON(this._data), _data: structuredClone(this._data),
}); });
} }
@@ -267,7 +256,7 @@ export class GPXFile extends GPXTreeNode<Track> {
toGPXFileType(exclude: string[] = []): GPXFileType { toGPXFileType(exclude: string[] = []): GPXFileType {
let file: GPXFileType = { let file: GPXFileType = {
attributes: cloneJSON(this.attributes), attributes: structuredClone(this.attributes),
metadata: {}, metadata: {},
wpt: this.wpt.map((wpt) => wpt.toWaypointType(exclude)), wpt: this.wpt.map((wpt) => wpt.toWaypointType(exclude)),
trk: this.trk.map((track) => track.toTrackType(exclude)), trk: this.trk.map((track) => track.toTrackType(exclude)),
@@ -281,10 +270,10 @@ export class GPXFile extends GPXTreeNode<Track> {
file.metadata.desc = this.metadata.desc; file.metadata.desc = this.metadata.desc;
} }
if (this.metadata.author) { if (this.metadata.author) {
file.metadata.author = cloneJSON(this.metadata.author); file.metadata.author = structuredClone(this.metadata.author);
} }
if (this.metadata.link) { 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')) { if (this.metadata.time && !exclude.includes('time')) {
file.metadata.time = this.metadata.time; file.metadata.time = this.metadata.time;
@@ -577,11 +566,11 @@ export class Track extends GPXTreeNode<TrackSegment> {
cmt: this.cmt, cmt: this.cmt,
desc: this.desc, desc: this.desc,
src: this.src, src: this.src,
link: cloneJSON(this.link), link: structuredClone(this.link),
type: this.type, type: this.type,
extensions: cloneJSON(this.extensions), extensions: structuredClone(this.extensions),
trkseg: this.trkseg.map((seg) => seg.clone()), trkseg: this.trkseg.map((seg) => seg.clone()),
_data: cloneJSON(this._data), _data: structuredClone(this._data),
}); });
} }
@@ -807,7 +796,7 @@ export class TrackSegment extends GPXTreeLeaf {
constructor(segment?: (TrackSegmentType & { _data?: any }) | TrackSegment) { constructor(segment?: (TrackSegmentType & { _data?: any }) | TrackSegment) {
super(); super();
if (segment) { if (segment) {
this.trkpt = segment.trkpt.map((point) => new TrackPoint(point)); this.trkpt = segment.trkpt.map((point, index) => new TrackPoint(point, index));
if (segment.hasOwnProperty('_data')) { if (segment.hasOwnProperty('_data')) {
this._data = segment._data; this._data = segment._data;
} }
@@ -819,12 +808,10 @@ export class TrackSegment extends GPXTreeLeaf {
_computeStatistics(): GPXStatistics { _computeStatistics(): GPXStatistics {
let statistics = new GPXStatistics(); let statistics = new GPXStatistics();
statistics.local.points = this.trkpt.map((point) => point); statistics.local.points = this.trkpt.slice(0);
const points = this.trkpt; const points = this.trkpt;
for (let i = 0; i < points.length; i++) { for (let i = 0; i < points.length; i++) {
points[i]._data['index'] = i;
// distance // distance
let dist = 0; let dist = 0;
if (i > 0) { if (i > 0) {
@@ -1100,7 +1087,7 @@ export class TrackSegment extends GPXTreeLeaf {
clone(): TrackSegment { clone(): TrackSegment {
return new TrackSegment({ return new TrackSegment({
trkpt: this.trkpt.map((point) => point.clone()), trkpt: this.trkpt.map((point) => point.clone()),
_data: cloneJSON(this._data), _data: structuredClone(this._data),
}); });
} }
@@ -1226,14 +1213,14 @@ export class TrackSegment extends GPXTreeLeaf {
let trkpt = og.trkpt.map( let trkpt = og.trkpt.map(
(point, i) => (point, i) =>
new TrackPoint({ new TrackPoint({
attributes: cloneJSON(point.attributes), attributes: structuredClone(point.attributes),
ele: point.ele, ele: point.ele,
time: new Date( time: new Date(
newStartTimestamp.getTime() + newStartTimestamp.getTime() +
(originalEndTimestamp.getTime() - og.trkpt[i].time.getTime()) (originalEndTimestamp.getTime() - og.trkpt[i].time.getTime())
), ),
extensions: cloneJSON(point.extensions), extensions: structuredClone(point.extensions),
_data: cloneJSON(point._data), _data: structuredClone(point._data),
}) })
); );
@@ -1317,7 +1304,7 @@ export class TrackPoint {
_data: { [key: string]: any } = {}; _data: { [key: string]: any } = {};
constructor(point: (TrackPointType & { _data?: any }) | TrackPoint) { constructor(point: (TrackPointType & { _data?: any }) | TrackPoint, index?: number) {
this.attributes = point.attributes; this.attributes = point.attributes;
this.ele = point.ele; this.ele = point.ele;
this.time = point.time; this.time = point.time;
@@ -1325,6 +1312,9 @@ export class TrackPoint {
if (point.hasOwnProperty('_data')) { if (point.hasOwnProperty('_data')) {
this._data = point._data; this._data = point._data;
} }
if (index !== undefined) {
this._data.index = index;
}
} }
getCoordinates(): Coordinates { getCoordinates(): Coordinates {
@@ -1468,11 +1458,18 @@ export class TrackPoint {
clone(): TrackPoint { clone(): TrackPoint {
return new TrackPoint({ return new TrackPoint({
attributes: cloneJSON(this.attributes), attributes: {
lat: this.attributes.lat,
lon: this.attributes.lon,
},
ele: this.ele, ele: this.ele,
time: this.time ? new Date(this.time.getTime()) : undefined, time: this.time ? new Date(this.time.getTime()) : undefined,
extensions: cloneJSON(this.extensions), extensions: this.extensions ? structuredClone(this.extensions) : undefined,
_data: cloneJSON(this._data), _data: {
index: this._data?.index,
anchor: this._data?.anchor,
zoom: this._data?.zoom,
},
}); });
} }
} }
@@ -1491,7 +1488,7 @@ export class Waypoint {
type?: string; type?: string;
_data: { [key: string]: any } = {}; _data: { [key: string]: any } = {};
constructor(waypoint: (WaypointType & { _data?: any }) | Waypoint) { constructor(waypoint: (WaypointType & { _data?: any }) | Waypoint, index?: number) {
this.attributes = waypoint.attributes; this.attributes = waypoint.attributes;
this.ele = waypoint.ele; this.ele = waypoint.ele;
this.time = waypoint.time; this.time = waypoint.time;
@@ -1510,6 +1507,9 @@ export class Waypoint {
if (waypoint.hasOwnProperty('_data')) { if (waypoint.hasOwnProperty('_data')) {
this._data = waypoint._data; this._data = waypoint._data;
} }
if (index !== undefined) {
this._data.index = index;
}
} }
getCoordinates(): Coordinates { getCoordinates(): Coordinates {
@@ -1557,13 +1557,16 @@ export class Waypoint {
clone(): Waypoint { clone(): Waypoint {
return new Waypoint({ return new Waypoint({
attributes: cloneJSON(this.attributes), attributes: {
lat: this.attributes.lat,
lon: this.attributes.lon,
},
ele: this.ele, ele: this.ele,
time: this.time ? new Date(this.time.getTime()) : undefined, time: this.time ? new Date(this.time.getTime()) : undefined,
name: this.name, name: this.name,
cmt: this.cmt, cmt: this.cmt,
desc: this.desc, desc: this.desc,
link: cloneJSON(this.link), link: structuredClone(this.link),
sym: this.sym, sym: this.sym,
type: this.type, type: this.type,
}); });

View File

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

3
website/.prettierignore Normal file
View File

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

View File

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

View File

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

View File

@@ -10,8 +10,8 @@
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .", "lint": "prettier --check . --config ../.prettierrc && eslint .",
"format": "prettier --write ." "format": "prettier --write . --config ../.prettierrc"
}, },
"devDependencies": { "devDependencies": {
"@lucide/svelte": "^0.544.0", "@lucide/svelte": "^0.544.0",
@@ -31,7 +31,7 @@
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^8.33.1", "@typescript-eslint/eslint-plugin": "^8.33.1",
"@typescript-eslint/parser": "^8.33.1", "@typescript-eslint/parser": "^8.33.1",
"bits-ui": "^2.12.0", "bits-ui": "^2.14.4",
"eslint": "^9.28.0", "eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^3.9.1", "eslint-plugin-svelte": "^3.9.1",
@@ -66,7 +66,7 @@
"@mapbox/sphericalmercator": "^2.0.1", "@mapbox/sphericalmercator": "^2.0.1",
"@mapbox/tilebelt": "^2.0.2", "@mapbox/tilebelt": "^2.0.2",
"@types/mapbox__sphericalmercator": "^1.2.3", "@types/mapbox__sphericalmercator": "^1.2.3",
"chart.js": "^4.4.9", "chart.js": "^4.5.1",
"chartjs-plugin-zoom": "^2.2.0", "chartjs-plugin-zoom": "^2.2.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dexie": "^4.0.11", "dexie": "^4.0.11",
@@ -74,7 +74,7 @@
"gpx": "file:../gpx", "gpx": "file:../gpx",
"immer": "^10.1.1", "immer": "^10.1.1",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"mapbox-gl": "^3.16.0", "mapbox-gl": "^3.17.0",
"mapillary-js": "^4.1.2", "mapillary-js": "^4.1.2",
"png.js": "^0.2.1", "png.js": "^0.2.1",
"sanitize-html": "^2.17.0", "sanitize-html": "^2.17.0",

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@
import { Compass, Earth, Mountain, Timer } from '@lucide/svelte'; import { Compass, Earth, Mountain, Timer } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import type { PopupItem } from '$lib/components/map/map-popup'; import type { PopupItem } from '$lib/components/map/map-popup';
import { map } from '$lib/components/map/map';
let { trackpoint }: { trackpoint: PopupItem<TrackPoint> } = $props(); let { trackpoint }: { trackpoint: PopupItem<TrackPoint> } = $props();
</script> </script>
@@ -40,7 +41,7 @@
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
href={`https://www.openstreetmap.org/edit?#map=18/${trackpoint.item.getLatitude().toFixed(5)}/${trackpoint.item.getLongitude().toFixed(5)}`} href={`https://www.openstreetmap.org/edit?#map=${(($map?.getZoom() ?? 17) + 1).toFixed(0)}/${trackpoint.item.getLatitude().toFixed(5)}/${trackpoint.item.getLongitude().toFixed(5)}`}
target="_blank" target="_blank"
> >
<Earth size="14" /> <Earth size="14" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
import { HeartHandshake } from '@lucide/svelte'; import { HeartHandshake } from '@lucide/svelte';
</script> </script>
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free) ## <HeartHandshake size="18" class="inline-block align-baseline" /> Hãy giúp duy trì trang web miễn phí (và không có quảng cáo)
Khi bạn thêm hoặc di chuyển các điểm định vị, máy chủ của chúng tôi sẽ tính toán đoạn đường tốt nhất trên mạng lưới giao thông. Khi bạn thêm hoặc di chuyển các điểm định vị, máy chủ của chúng tôi sẽ tính toán đoạn đường tốt nhất trên mạng lưới giao thông.
Chúng tôi cũng sử dụng các API từ <a href="https://mapbox.com" target="_blank">Mapbox</a> để hiển thị đa dạng các bản đồ, lưu trữ các dữ liệu độ cao cũng như giúp bạn có thể tìm kiếm các địa điểm khác nhau. Chúng tôi cũng sử dụng các API từ <a href="https://mapbox.com" target="_blank">Mapbox</a> để hiển thị đa dạng các bản đồ, lưu trữ các dữ liệu độ cao cũng như giúp bạn có thể tìm kiếm các địa điểm khác nhau.

View File

@@ -1,5 +1,5 @@
Mapbox is the company that provides some of the beautiful maps on this website. Mapbox là công ty cung cấp một số bản đồ đẹp trên trang web này.
They also develop the <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">map engine</a> which powers **gpx.studio**. Họ cũng phát triển <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">công cụ bản đồ</a> cung cấp sức mạnh cho **gpx.studio**.
We are incredibly fortunate and grateful to be part of their <a href="https://mapbox.com/community" target="_blank">Community</a> program, which supports nonprofits, educational institutions, and positive impact organizations. Chúng tôi vô cùng may mắn và biết ơn khi được tham gia chương trình <a href="https://mapbox.com/community" target="_blank">Cộng đồng</a> của họ, chương trình hỗ trợ các tổ chức phi lợi nhuận, các tổ chức giáo dục và các tổ chức tạo ra tác động tích cực.
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. 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ể.

View File

@@ -9,8 +9,8 @@ title: Edit actions
# { title } # { title }
Unlike the file actions, the edit actions can potentially modify the content of the currently selected files. 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.
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). 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_. Therefore, we will refer to the elements that can be modified by these actions as _file items_.
Note that except for the undo and redo actions, the edit actions are also accessible through the context menu (right-click) of the file items. Note that except for the undo and redo actions, the edit actions are also accessible through the context menu (right-click) of the file items.

View File

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

View File

@@ -17,7 +17,6 @@ import {
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { freeze, type WritableDraft } from 'immer'; import { freeze, type WritableDraft } from 'immer';
import { import {
distance,
GPXFile, GPXFile,
parseGPX, parseGPX,
Track, Track,
@@ -30,7 +29,7 @@ import {
} from 'gpx'; } from 'gpx';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { getClosestLinePoint, getElevation } from '$lib/utils'; import { getClosestLinePoint, getClosestTrackSegments, getElevation } from '$lib/utils';
import { gpxStatistics } from '$lib/logic/statistics'; import { gpxStatistics } from '$lib/logic/statistics';
import { boundsManager } from './bounds'; import { boundsManager } from './bounds';
@@ -453,34 +452,13 @@ export const fileActions = {
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
if (level === ListLevel.FILE) { if (level === ListLevel.FILE) {
let file = fileStateCollection.getFile(fileId); let file = fileStateCollection.getFile(fileId);
if (file) { let statistics = fileStateCollection.getStatistics(fileId);
if (file && statistics) {
if (file.trk.length > 1) { if (file.trk.length > 1) {
let fileIds = getFileIds(file.trk.length); let fileIds = getFileIds(file.trk.length);
let closest = file.wpt.map((wpt, wptIndex) => { let closest = file.wpt.map((wpt) =>
return { getClosestTrackSegments(file, statistics, wpt.getCoordinates())
wptIndex: wptIndex, );
index: [0],
distance: Number.MAX_VALUE,
};
});
file.trk.forEach((track, index) => {
track.getSegments().forEach((segment) => {
segment.trkpt.forEach((point) => {
file.wpt.forEach((wpt, wptIndex) => {
let dist = distance(
point.getCoordinates(),
wpt.getCoordinates()
);
if (dist < closest[wptIndex].distance) {
closest[wptIndex].distance = dist;
closest[wptIndex].index = [index];
} else if (dist === closest[wptIndex].distance) {
closest[wptIndex].index.push(index);
}
});
});
});
});
file.trk.forEach((track, index) => { file.trk.forEach((track, index) => {
let newFile = file.clone(); let newFile = file.clone();
let tracks = track.trkseg.map((segment, segmentIndex) => { let tracks = track.trkseg.map((segment, segmentIndex) => {
@@ -495,9 +473,11 @@ export const fileActions = {
newFile.replaceWaypoints( newFile.replaceWaypoints(
0, 0,
file.wpt.length - 1, file.wpt.length - 1,
closest file.wpt.filter((wpt, wptIndex) =>
.filter((c) => c.index.includes(index)) closest[wptIndex].some(
.map((c) => file.wpt[c.wptIndex]) ([trackIndex, segmentIndex]) => trackIndex === index
)
)
); );
newFile._data.id = fileIds[index]; newFile._data.id = fileIds[index];
newFile.metadata.name = newFile.metadata.name =
@@ -506,29 +486,9 @@ export const fileActions = {
}); });
} else if (file.trk.length === 1) { } else if (file.trk.length === 1) {
let fileIds = getFileIds(file.trk[0].trkseg.length); let fileIds = getFileIds(file.trk[0].trkseg.length);
let closest = file.wpt.map((wpt, wptIndex) => { let closest = file.wpt.map((wpt) =>
return { getClosestTrackSegments(file, statistics, wpt.getCoordinates())
wptIndex: wptIndex, );
index: [0],
distance: Number.MAX_VALUE,
};
});
file.trk[0].trkseg.forEach((segment, index) => {
segment.trkpt.forEach((point) => {
file.wpt.forEach((wpt, wptIndex) => {
let dist = distance(
point.getCoordinates(),
wpt.getCoordinates()
);
if (dist < closest[wptIndex].distance) {
closest[wptIndex].distance = dist;
closest[wptIndex].index = [index];
} else if (dist === closest[wptIndex].distance) {
closest[wptIndex].index.push(index);
}
});
});
});
file.trk[0].trkseg.forEach((segment, index) => { file.trk[0].trkseg.forEach((segment, index) => {
let newFile = file.clone(); let newFile = file.clone();
newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [ newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [
@@ -537,9 +497,11 @@ export const fileActions = {
newFile.replaceWaypoints( newFile.replaceWaypoints(
0, 0,
file.wpt.length - 1, file.wpt.length - 1,
closest file.wpt.filter((wpt, wptIndex) =>
.filter((c) => c.index.includes(index)) closest[wptIndex].some(
.map((c) => file.wpt[c.wptIndex]) ([trackIndex, segmentIndex]) => segmentIndex === index
)
)
); );
newFile._data.id = fileIds[index]; newFile._data.id = fileIds[index];
newFile.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`; newFile.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`;

View File

@@ -22,25 +22,34 @@ export class GPXStatisticsTree {
} }
getStatisticsFor(item: ListItem): GPXStatistics { getStatisticsFor(item: ListItem): GPXStatistics {
let statistics = new GPXStatistics(); let statistics = [];
let id = item.getIdAtLevel(this.level); let id = item.getIdAtLevel(this.level);
if (id === undefined || id === 'waypoints') { if (id === undefined || id === 'waypoints') {
Object.keys(this.statistics).forEach((key) => { Object.keys(this.statistics).forEach((key) => {
if (this.statistics[key] instanceof GPXStatistics) { if (this.statistics[key] instanceof GPXStatistics) {
statistics.mergeWith(this.statistics[key]); statistics.push(this.statistics[key]);
} else { } else {
statistics.mergeWith(this.statistics[key].getStatisticsFor(item)); statistics.push(this.statistics[key].getStatisticsFor(item));
} }
}); });
} else { } else {
let child = this.statistics[id]; let child = this.statistics[id];
if (child instanceof GPXStatistics) { if (child instanceof GPXStatistics) {
statistics.mergeWith(child); statistics.push(child);
} else if (child !== undefined) { } 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 }; export type GPXFileWithStatistics = { file: GPXFile; statistics: GPXStatisticsTree };

View File

@@ -2,11 +2,13 @@ import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { base } from '$app/paths'; import { base } from '$app/paths';
import { languages } from '$lib/languages'; import { languages } from '$lib/languages';
import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance } from 'gpx'; import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance, GPXFile } from 'gpx';
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt'; import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt';
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public'; import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import PNGReader from 'png.js'; import PNGReader from 'png.js';
import type { GPXStatisticsTree } from '$lib/logic/statistics-tree';
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
@@ -47,6 +49,59 @@ export function getClosestLinePoint(
return closest; return closest;
} }
export function getClosestTrackSegments(
file: GPXFile,
statistics: GPXStatisticsTree,
point: Coordinates
): [number, number][] {
let segmentBoundsDistances: [number, number, number][] = [];
file.forEachSegment((segment, trackIndex, segmentIndex) => {
let segmentStatistics = statistics.getStatisticsFor(
new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex)
);
let segmentBounds = segmentStatistics.global.bounds;
let northEast = segmentBounds.northEast;
let southWest = segmentBounds.southWest;
let bounds = new mapboxgl.LngLatBounds(southWest, northEast);
if (bounds.contains(point)) {
segmentBoundsDistances.push([0, trackIndex, segmentIndex]);
} else {
let northWest: Coordinates = { lat: northEast.lat, lon: southWest.lon };
let southEast: Coordinates = { lat: southWest.lat, lon: northEast.lon };
let distanceToBounds = Math.min(
crossarcDistance(northWest, northEast, point),
crossarcDistance(northEast, southEast, point),
crossarcDistance(southEast, southWest, point),
crossarcDistance(southWest, northWest, point)
);
segmentBoundsDistances.push([distanceToBounds, trackIndex, segmentIndex]);
}
});
segmentBoundsDistances.sort((a, b) => a[0] - b[0]);
let closest: { distance: number; indices: [number, number][] } = {
distance: Number.MAX_VALUE,
indices: [],
};
for (let s = 0; s < segmentBoundsDistances.length; s++) {
if (segmentBoundsDistances[s][0] > closest.distance) {
break;
}
const segment = file.getSegment(segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]);
segment.trkpt.forEach((pt) => {
let dist = distance(pt.getCoordinates(), point);
if (dist < closest.distance) {
closest.distance = dist;
closest.indices = [[segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]]];
} else if (dist === closest.distance) {
closest.indices.push([segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]]);
}
});
}
return closest.indices;
}
export function getElevation( export function getElevation(
points: (TrackPoint | Waypoint | Coordinates)[], points: (TrackPoint | Waypoint | Coordinates)[],
ELEVATION_ZOOM: number = 13, ELEVATION_ZOOM: number = 13,

View File

@@ -80,7 +80,7 @@
"center": "Centrar", "center": "Centrar",
"open_in": "Abrir en", "open_in": "Abrir en",
"copy_coordinates": "Copiar coordenadas", "copy_coordinates": "Copiar coordenadas",
"edit_osm": "Edit in OpenStreetMap" "edit_osm": "Editar en OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -353,7 +353,7 @@
"water": "Agua", "water": "Agua",
"shower": "Ducha", "shower": "Ducha",
"shelter": "Refugio", "shelter": "Refugio",
"cemetery": "Cemetery", "cemetery": "Cementerio",
"motorized": "Coches y motos", "motorized": "Coches y motos",
"fuel-station": "Gasolinera", "fuel-station": "Gasolinera",
"parking": "Aparcamiento", "parking": "Aparcamiento",

View File

@@ -28,7 +28,7 @@
"undo": "Desegin", "undo": "Desegin",
"redo": "Berregin", "redo": "Berregin",
"delete": "Ezabatu", "delete": "Ezabatu",
"delete_all": "Delete all", "delete_all": "Ezabatu guztiak",
"select_all": "Hautatu dena", "select_all": "Hautatu dena",
"view": "Ikusi", "view": "Ikusi",
"elevation_profile": "Altuera profila", "elevation_profile": "Altuera profila",
@@ -80,7 +80,7 @@
"center": "Erdiratu", "center": "Erdiratu",
"open_in": "Ireki hemen", "open_in": "Ireki hemen",
"copy_coordinates": "Kopiatu koordenatuak", "copy_coordinates": "Kopiatu koordenatuak",
"edit_osm": "Edit in OpenStreetMap" "edit_osm": "Editatu OpenStreeMapen"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -353,7 +353,7 @@
"water": "Ura", "water": "Ura",
"shower": "Dutxa", "shower": "Dutxa",
"shelter": "Babeslekua", "shelter": "Babeslekua",
"cemetery": "Cemetery", "cemetery": "Hilerria",
"motorized": "Kotxeak eta motorrak", "motorized": "Kotxeak eta motorrak",
"fuel-station": "Gasolindegia", "fuel-station": "Gasolindegia",
"parking": "Aparkalekua", "parking": "Aparkalekua",

View File

@@ -28,7 +28,7 @@
"undo": "Annulla", "undo": "Annulla",
"redo": "Ripeti", "redo": "Ripeti",
"delete": "Elimina", "delete": "Elimina",
"delete_all": "Delete all", "delete_all": "Cancella tutto",
"select_all": "Seleziona tutto", "select_all": "Seleziona tutto",
"view": "Visualizza", "view": "Visualizza",
"elevation_profile": "Profilo altimetrico", "elevation_profile": "Profilo altimetrico",
@@ -80,7 +80,7 @@
"center": "Centra", "center": "Centra",
"open_in": "Apri con", "open_in": "Apri con",
"copy_coordinates": "Copia le coordinate", "copy_coordinates": "Copia le coordinate",
"edit_osm": "Edit in OpenStreetMap" "edit_osm": "Modifica in OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -353,7 +353,7 @@
"water": "Acqua", "water": "Acqua",
"shower": "Doccia", "shower": "Doccia",
"shelter": "Riparo", "shelter": "Riparo",
"cemetery": "Cemetery", "cemetery": "Cimitero",
"motorized": "Auto e Motocicli", "motorized": "Auto e Motocicli",
"fuel-station": "Stazione di Rifornimento", "fuel-station": "Stazione di Rifornimento",
"parking": "Parcheggio", "parking": "Parcheggio",

View File

@@ -2,7 +2,7 @@
"metadata": { "metadata": {
"home_title": "edytor online plików GPX", "home_title": "edytor online plików GPX",
"app_title": "Aplikacja", "app_title": "Aplikacja",
"embed_title": "Online'owy edytor plików GPX", "embed_title": "Edytor plików GPX online",
"help_title": "pomoc", "help_title": "pomoc",
"404_title": "nie odnaleziono strony", "404_title": "nie odnaleziono strony",
"description": "Przeglądaj, edytuj i twórz pliki GPX online z zaawansowanymi możliwościami planowania trasy i narzędziami do przetwarzania plików, pięknymi mapami i szczegółowymi wizualizacjami danych." "description": "Przeglądaj, edytuj i twórz pliki GPX online z zaawansowanymi możliwościami planowania trasy i narzędziami do przetwarzania plików, pięknymi mapami i szczegółowymi wizualizacjami danych."
@@ -28,7 +28,7 @@
"undo": "Cofnij", "undo": "Cofnij",
"redo": "Ponów", "redo": "Ponów",
"delete": "Usuń", "delete": "Usuń",
"delete_all": "Delete all", "delete_all": "Usuń wszystko",
"select_all": "Zaznacz wszystko", "select_all": "Zaznacz wszystko",
"view": "Widok", "view": "Widok",
"elevation_profile": "Profil wysokości", "elevation_profile": "Profil wysokości",
@@ -80,7 +80,7 @@
"center": "Wyśrodkuj", "center": "Wyśrodkuj",
"open_in": "Otwórz w", "open_in": "Otwórz w",
"copy_coordinates": "Kopiuj współrzędne", "copy_coordinates": "Kopiuj współrzędne",
"edit_osm": "Edit in OpenStreetMap" "edit_osm": "Edytuj w OpenStreetMap"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -353,7 +353,7 @@
"water": "Woda", "water": "Woda",
"shower": "Prysznic", "shower": "Prysznic",
"shelter": "Schronienie", "shelter": "Schronienie",
"cemetery": "Cemetery", "cemetery": "Cmentarz",
"motorized": "Samochody i motocykle", "motorized": "Samochody i motocykle",
"fuel-station": "Stacja paliw", "fuel-station": "Stacja paliw",
"parking": "Parking", "parking": "Parking",

View File

@@ -21,18 +21,18 @@
"export_all": "Xuất tất cả...", "export_all": "Xuất tất cả...",
"export_options": "Tùy chọn xuất", "export_options": "Tùy chọn xuất",
"support_message": "Công cụ này miễn phí, nhưng nó tốn phí để duy trì hoạt động. Nếu bạn dùng công cụ này thường xuyên, bạn có thể xem xét đóng góp để hỗ trợ chúng tôi. Chúng tôi rất biết ơn vì điều đó!", "support_message": "Công cụ này miễn phí, nhưng nó tốn phí để duy trì hoạt động. Nếu bạn dùng công cụ này thường xuyên, bạn có thể xem xét đóng góp để hỗ trợ chúng tôi. Chúng tôi rất biết ơn vì điều đó!",
"support_button": "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_file": "Tải tệp xuống",
"download_files": "Tải xuống tất cả", "download_files": "Tải xuống tất cả",
"edit": "Chỉnh sửa", "edit": "Chỉnh sửa",
"undo": "Hoàn tác", "undo": "Hoàn tác",
"redo": "Khôi phục", "redo": "Khôi phục",
"delete": "Xóa", "delete": "Xóa",
"delete_all": "Delete all", "delete_all": "Xóa tất cả",
"select_all": "Chọn tất cả", "select_all": "Chọn tất cả",
"view": "Xem", "view": "Xem",
"elevation_profile": "Thông tin độ cao", "elevation_profile": "Thông tin độ cao",
"tree_file_view": "File tree", "tree_file_view": "Cây thư mục",
"switch_basemap": "Quay lại bản đồ trước đó", "switch_basemap": "Quay lại bản đồ trước đó",
"toggle_overlays": "Thay đổi lớp phủ", "toggle_overlays": "Thay đổi lớp phủ",
"toggle_3d": "Chuyển đổi 3D", "toggle_3d": "Chuyển đổi 3D",
@@ -57,35 +57,35 @@
"layers": "Lớp bản đồ...", "layers": "Lớp bản đồ...",
"distance_markers": "Đánh dấu khoảng cách", "distance_markers": "Đánh dấu khoảng cách",
"direction_markers": "Mũi tên định hướng", "direction_markers": "Mũi tên định hướng",
"help": "Help", "help": "Trợ giúp",
"more": "More...", "more": "Chi tiết...",
"donate": "Donate", "donate": "Ủng hộ",
"ctrl": "Ctrl", "ctrl": "Ctrl",
"click": "Click", "click": "Nhấp chuột",
"drag": "Drag", "drag": "Kéo",
"metadata": { "metadata": {
"button": "Info...", "button": "Thông tin...",
"name": "Name", "name": "Tên",
"description": "Description", "description": "Mô tả",
"save": "Save" "save": "Lưu"
}, },
"style": { "style": {
"button": "Appearance...", "button": "Diện mạo...",
"color": "Color", "color": "Màu sắc",
"opacity": "Opacity", "opacity": "Độ trong suốt",
"width": "Width" "width": "Độ rộng"
}, },
"hide": "Hide", "hide": "Ẩn",
"unhide": "Unhide", "unhide": "Bỏ ẩn",
"center": "Center", "center": "Giữa",
"open_in": "Open in", "open_in": "Mở ra",
"copy_coordinates": "Copy coordinates", "copy_coordinates": "Sao chép tọa độ",
"edit_osm": "Edit in OpenStreetMap" "edit_osm": ""
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
"tooltip": "Plan or edit a route", "tooltip": "Plan or edit a route",
"activity": "Activity", "activity": "Hoạt động",
"use_routing": "Routing", "use_routing": "Routing",
"use_routing_tooltip": "Connect anchor points via road network, or in a straight line if disabled", "use_routing_tooltip": "Connect anchor points via road network, or in a straight line if disabled",
"allow_private": "Allow private roads", "allow_private": "Allow private roads",
@@ -94,14 +94,14 @@
"tooltip": "Reverse the direction of the route" "tooltip": "Reverse the direction of the route"
}, },
"route_back_to_start": { "route_back_to_start": {
"button": "Back to start", "button": "Quay về Bắt đầu",
"tooltip": "Connect the last point of the route with the starting point" "tooltip": "Connect the last point of the route with the starting point"
}, },
"round_trip": { "round_trip": {
"button": "Round trip", "button": "Round trip",
"tooltip": "Return to the starting point by the same route" "tooltip": "Return to the starting point by the same route"
}, },
"start_loop_here": "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_no_file": "Select a trace to use the routing tool, or click on the map to start creating a new route.",
"help": "Click on the map to add a new anchor point, or drag existing ones to change the route.", "help": "Click on the map to add a new anchor point, or drag existing ones to change the route.",
"activities": { "activities": {
@@ -127,15 +127,15 @@
"wood": "Wood", "wood": "Wood",
"compacted": "Compacted gravel", "compacted": "Compacted gravel",
"fine_gravel": "Fine gravel", "fine_gravel": "Fine gravel",
"gravel": "Gravel", "gravel": "Sỏi",
"pebblestone": "Pebblestone", "pebblestone": "Pebblestone",
"rock": "Rock", "rock": "Đá",
"dirt": "Dirt", "dirt": "Đất",
"ground": "Ground", "ground": "Ground",
"earth": "Earth", "earth": "Earth",
"mud": "Mud", "mud": "Mud",
"sand": "Sand", "sand": "Sand",
"grass": "Grass", "grass": "Cỏ",
"grass_paver": "Grass paver", "grass_paver": "Grass paver",
"clay": "Clay", "clay": "Clay",
"stone": "Stone" "stone": "Stone"

View File

@@ -28,7 +28,7 @@
"undo": "撤销", "undo": "撤销",
"redo": "恢复", "redo": "恢复",
"delete": "删除", "delete": "删除",
"delete_all": "", "delete_all": "全部删除",
"select_all": "全选", "select_all": "全选",
"view": "显示", "view": "显示",
"elevation_profile": "海拔剖面图", "elevation_profile": "海拔剖面图",
@@ -80,7 +80,7 @@
"center": "居中", "center": "居中",
"open_in": "打开于", "open_in": "打开于",
"copy_coordinates": "复制坐标", "copy_coordinates": "复制坐标",
"edit_osm": "Edit in OpenStreetMap" "edit_osm": " OpenStreetMap 中编辑"
}, },
"toolbar": { "toolbar": {
"routing": { "routing": {
@@ -230,8 +230,8 @@
"help_invalid_selection": "须先选择包含多个轨迹的文件以提取。" "help_invalid_selection": "须先选择包含多个轨迹的文件以提取。"
}, },
"elevation": { "elevation": {
"button": "请求数据", "button": "请求海拔数据",
"help": "请求成功后将使用 Mapbox 海拔数据替换原有数据。", "help": "请求成功后将移除原有的海拔数据,并使用 Mapbox 海拔数据替换原有数据。",
"help_no_selection": "选择要请求海拔数据的文件。" "help_no_selection": "选择要请求海拔数据的文件。"
}, },
"waypoint": { "waypoint": {
@@ -353,7 +353,7 @@
"water": "饮用水", "water": "饮用水",
"shower": "淋浴", "shower": "淋浴",
"shelter": "庇护所", "shelter": "庇护所",
"cemetery": "Cemetery", "cemetery": "墓地",
"motorized": "汽车和摩托车", "motorized": "汽车和摩托车",
"fuel-station": "加油站", "fuel-station": "加油站",
"parking": "停车场", "parking": "停车场",