mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-01-01 15:54:44 +00:00
Compare commits
24 Commits
867b6a6ac7
...
l10n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca464d16c7 | ||
|
|
e0b7dd881b | ||
|
|
f62916b19d | ||
|
|
c6dfefad45 | ||
|
|
09d307e6a9 | ||
|
|
6688dba9d1 | ||
|
|
fa459e0063 | ||
|
|
f54de4eb3e | ||
|
|
d3e733aa3e | ||
|
|
a011768d2d | ||
|
|
4b45b5d716 | ||
|
|
ebe9681c12 | ||
|
|
51c85e4cd5 | ||
|
|
2e171dfbee | ||
|
|
a6a3917986 | ||
|
|
21f2448213 | ||
|
|
e7a1d0488b | ||
|
|
22b8e0edb4 | ||
|
|
d062a38e8f | ||
|
|
affa59130f | ||
|
|
3c816567bc | ||
|
|
e92e48ffde | ||
|
|
4ce7777b86 | ||
|
|
bc130ad867 |
@@ -1,6 +0,0 @@
|
|||||||
# Ignore files for PNPM, NPM and YARN
|
|
||||||
pnpm-lock.yaml
|
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
||||||
src/lib/components/ui
|
|
||||||
*.mdx
|
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
3
website/.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
src/lib/components/ui
|
||||||
|
src/lib/docs/**/*.mdx
|
||||||
|
**/*.webmanifest
|
||||||
35
website/package-lock.json
generated
35
website/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import 'tailwindcss';
|
||||||
@import "tw-animate-css";
|
@import 'tw-animate-css';
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
|||||||
@@ -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' ||
|
||||||
|
|||||||
@@ -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:
|
||||||
|
data.global.time.total > 0
|
||||||
|
? data.local.points.map((point, index) => {
|
||||||
return {
|
return {
|
||||||
x: getConvertedDistance(data.local.distance.total[index]),
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
y: getConvertedVelocity(data.local.speed[index]),
|
y: getConvertedVelocity(data.local.speed[index]),
|
||||||
index: 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:
|
||||||
|
data.global.hr.count > 0
|
||||||
|
? data.local.points.map((point, index) => {
|
||||||
return {
|
return {
|
||||||
x: getConvertedDistance(data.local.distance.total[index]),
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
y: point.getHeartRate(),
|
y: point.getHeartRate(),
|
||||||
index: index,
|
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:
|
||||||
|
data.global.cad.count > 0
|
||||||
|
? data.local.points.map((point, index) => {
|
||||||
return {
|
return {
|
||||||
x: getConvertedDistance(data.local.distance.total[index]),
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
y: point.getCadence(),
|
y: point.getCadence(),
|
||||||
index: index,
|
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:
|
||||||
|
data.global.atemp.count > 0
|
||||||
|
? data.local.points.map((point, index) => {
|
||||||
return {
|
return {
|
||||||
x: getConvertedDistance(data.local.distance.total[index]),
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
y: getConvertedTemperature(point.getTemperature()),
|
y: getConvertedTemperature(point.getTemperature()),
|
||||||
index: index,
|
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:
|
||||||
|
data.global.power.count > 0
|
||||||
|
? data.local.points.map((point, index) => {
|
||||||
return {
|
return {
|
||||||
x: getConvertedDistance(data.local.distance.total[index]),
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
y: point.getPower(),
|
y: point.getPower(),
|
||||||
index: index,
|
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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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...
|
||||||
|
|
||||||
|
|||||||
@@ -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...
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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ể.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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" /> 导出...
|
||||||
|
|
||||||
|
|||||||
@@ -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})`;
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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": "停车场",
|
||||||
|
|||||||
Reference in New Issue
Block a user