mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-01-15 14:18:41 +00:00
Compare commits
8 Commits
e7a1d0488b
...
d3e733aa3e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3e733aa3e | ||
|
|
a011768d2d | ||
|
|
4b45b5d716 | ||
|
|
ebe9681c12 | ||
|
|
51c85e4cd5 | ||
|
|
2e171dfbee | ||
|
|
a6a3917986 | ||
|
|
21f2448213 |
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
8
website/package-lock.json
generated
8
website/package-lock.json
generated
@@ -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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -428,57 +428,72 @@ 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',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -702,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}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -723,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 = () => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user