mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-09-01 08:12:32 +00:00
scissor tool
This commit is contained in:
148
gpx/src/gpx.ts
148
gpx/src/gpx.ts
@@ -15,6 +15,7 @@ export abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
|
|||||||
abstract isLeaf(): boolean;
|
abstract isLeaf(): boolean;
|
||||||
abstract get children(): ReadonlyArray<T>;
|
abstract get children(): ReadonlyArray<T>;
|
||||||
|
|
||||||
|
abstract getNumberOfTrackPoints(): number;
|
||||||
abstract getStartTimestamp(): Date;
|
abstract getStartTimestamp(): Date;
|
||||||
abstract getEndTimestamp(): Date;
|
abstract getEndTimestamp(): Date;
|
||||||
abstract getStatistics(): GPXStatistics;
|
abstract getStatistics(): GPXStatistics;
|
||||||
@@ -34,6 +35,10 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNumberOfTrackPoints(): number {
|
||||||
|
return this.children.reduce((acc, child) => acc + child.getNumberOfTrackPoints(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
getStartTimestamp(): Date {
|
getStartTimestamp(): Date {
|
||||||
return this.children[0].getStartTimestamp();
|
return this.children[0].getStartTimestamp();
|
||||||
}
|
}
|
||||||
@@ -241,6 +246,34 @@ export class GPXFile extends GPXTreeNode<Track>{
|
|||||||
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
|
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
crop(start: number, end: number, trackIndices?: number[], segmentIndices?: number[]) {
|
||||||
|
return produce(this, (draft) => {
|
||||||
|
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
|
||||||
|
let trk = og.trk.slice();
|
||||||
|
let i = 0;
|
||||||
|
let trackIndex = 0;
|
||||||
|
while (i < trk.length) {
|
||||||
|
let length = trk[i].getNumberOfTrackPoints();
|
||||||
|
if (trackIndices === undefined || trackIndices.includes(trackIndex)) {
|
||||||
|
if (start >= length || end < 0) {
|
||||||
|
trk.splice(i, 1);
|
||||||
|
} else {
|
||||||
|
if (start > 0 || end < length - 1) {
|
||||||
|
trk[i] = trk[i].crop(Math.max(0, start), Math.min(length - 1, end), segmentIndices);
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
start -= length;
|
||||||
|
end -= length;
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
trackIndex++;
|
||||||
|
}
|
||||||
|
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// A class that represents a Track in a GPX file
|
// A class that represents a Track in a GPX file
|
||||||
@@ -353,6 +386,34 @@ export class Track extends GPXTreeNode<TrackSegment> {
|
|||||||
draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
|
draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
crop(start: number, end: number, segmentIndices?: number[]) {
|
||||||
|
return produce(this, (draft) => {
|
||||||
|
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
|
||||||
|
let trkseg = og.trkseg.slice();
|
||||||
|
let i = 0;
|
||||||
|
let segmentIndex = 0;
|
||||||
|
while (i < trkseg.length) {
|
||||||
|
let length = trkseg[i].getNumberOfTrackPoints();
|
||||||
|
if (segmentIndices === undefined || segmentIndices.includes(segmentIndex)) {
|
||||||
|
if (start >= length || end < 0) {
|
||||||
|
trkseg.splice(i, 1);
|
||||||
|
} else {
|
||||||
|
if (start > 0 || end < length - 1) {
|
||||||
|
trkseg[i] = trkseg[i].crop(Math.max(0, start), Math.min(length - 1, end));
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
start -= length;
|
||||||
|
end -= length;
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
segmentIndex++;
|
||||||
|
}
|
||||||
|
draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// A class that represents a TrackSegment in a GPX file
|
// A class that represents a TrackSegment in a GPX file
|
||||||
@@ -393,14 +454,14 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
statistics.global.distance.total += dist;
|
statistics.global.distance.total += dist;
|
||||||
}
|
}
|
||||||
|
|
||||||
statistics.local.distance.push(statistics.global.distance.total);
|
statistics.local.distance.total.push(statistics.global.distance.total);
|
||||||
|
|
||||||
// elevation
|
// elevation
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
const ele = statistics.local.elevation.smoothed[i] - statistics.local.elevation.smoothed[i - 1];
|
const ele = statistics.local.elevation.smoothed[i] - statistics.local.elevation.smoothed[i - 1];
|
||||||
if (ele > 0) {
|
if (ele > 0) {
|
||||||
statistics.global.elevation.gain += ele;
|
statistics.global.elevation.gain += ele;
|
||||||
} else {
|
} else if (ele < 0) {
|
||||||
statistics.global.elevation.loss -= ele;
|
statistics.global.elevation.loss -= ele;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,9 +471,9 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
|
|
||||||
// time
|
// time
|
||||||
if (points[0].time !== undefined && points[i].time !== undefined) {
|
if (points[0].time !== undefined && points[i].time !== undefined) {
|
||||||
const time = (points[i].time.getTime() - points[0].time.getTime()) / 1000;
|
statistics.local.time.total.push((points[i].time.getTime() - points[0].time.getTime()) / 1000);
|
||||||
|
} else {
|
||||||
statistics.local.time.push(time);
|
statistics.local.time.total.push(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
// speed
|
// speed
|
||||||
@@ -427,6 +488,9 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statistics.local.distance.moving.push(statistics.global.distance.moving);
|
||||||
|
statistics.local.time.moving.push(statistics.global.time.moving);
|
||||||
|
|
||||||
// bounds
|
// bounds
|
||||||
statistics.global.bounds.southWest.lat = Math.min(statistics.global.bounds.southWest.lat, points[i].attributes.lat);
|
statistics.global.bounds.southWest.lat = Math.min(statistics.global.bounds.southWest.lat, points[i].attributes.lat);
|
||||||
statistics.global.bounds.southWest.lon = Math.min(statistics.global.bounds.southWest.lon, points[i].attributes.lon);
|
statistics.global.bounds.southWest.lon = Math.min(statistics.global.bounds.southWest.lon, points[i].attributes.lon);
|
||||||
@@ -434,9 +498,9 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
statistics.global.bounds.northEast.lon = Math.max(statistics.global.bounds.northEast.lon, points[i].attributes.lon);
|
statistics.global.bounds.northEast.lon = Math.max(statistics.global.bounds.northEast.lon, points[i].attributes.lon);
|
||||||
}
|
}
|
||||||
|
|
||||||
statistics.global.time.total = statistics.local.time[statistics.local.time.length - 1];
|
statistics.global.time.total = statistics.local.time.total[statistics.local.time.total.length - 1] ?? 0;
|
||||||
statistics.global.speed.total = statistics.global.distance.total / (statistics.global.time.total / 3600);
|
statistics.global.speed.total = statistics.global.time.total > 0 ? statistics.global.distance.total / (statistics.global.time.total / 3600) : 0;
|
||||||
statistics.global.speed.moving = statistics.global.distance.moving / (statistics.global.time.moving / 3600);
|
statistics.global.speed.moving = statistics.global.time.moving > 0 ? statistics.global.distance.moving / (statistics.global.time.moving / 3600) : 0;
|
||||||
|
|
||||||
statistics.local.speed = distanceWindowSmoothingWithDistanceAccumulator(points, 200, (accumulated, start, end) => (points[start].time && points[end].time) ? 3600 * accumulated / (points[end].time.getTime() - points[start].time.getTime()) : undefined);
|
statistics.local.speed = distanceWindowSmoothingWithDistanceAccumulator(points, 200, (accumulated, start, end) => (points[start].time && points[end].time) ? 3600 * accumulated / (points[end].time.getTime() - points[start].time.getTime()) : undefined);
|
||||||
|
|
||||||
@@ -462,6 +526,10 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
return distanceWindowSmoothingWithDistanceAccumulator(points, 50, (accumulated, start, end) => 100 * (points[end].ele - points[start].ele) / accumulated);
|
return distanceWindowSmoothingWithDistanceAccumulator(points, 50, (accumulated, start, end) => 100 * (points[end].ele - points[start].ele) / accumulated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNumberOfTrackPoints(): number {
|
||||||
|
return this.trkpt.length;
|
||||||
|
}
|
||||||
|
|
||||||
getStartTimestamp(): Date {
|
getStartTimestamp(): Date {
|
||||||
return this.trkpt[0].time;
|
return this.trkpt[0].time;
|
||||||
}
|
}
|
||||||
@@ -540,6 +608,14 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
crop(start: number, end: number) {
|
||||||
|
return produce(this, (draft) => {
|
||||||
|
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
|
||||||
|
let trkpt = og.trkpt.slice(start, end + 1);
|
||||||
|
draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export class TrackPoint {
|
export class TrackPoint {
|
||||||
@@ -718,8 +794,14 @@ export class GPXStatistics {
|
|||||||
};
|
};
|
||||||
local: {
|
local: {
|
||||||
points: TrackPoint[],
|
points: TrackPoint[],
|
||||||
distance: number[],
|
distance: {
|
||||||
time: number[],
|
moving: number[],
|
||||||
|
total: number[],
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
moving: number[],
|
||||||
|
total: number[],
|
||||||
|
},
|
||||||
speed: number[],
|
speed: number[],
|
||||||
elevation: {
|
elevation: {
|
||||||
smoothed: number[],
|
smoothed: number[],
|
||||||
@@ -760,8 +842,14 @@ export class GPXStatistics {
|
|||||||
};
|
};
|
||||||
this.local = {
|
this.local = {
|
||||||
points: [],
|
points: [],
|
||||||
distance: [],
|
distance: {
|
||||||
time: [],
|
moving: [],
|
||||||
|
total: [],
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
moving: [],
|
||||||
|
total: [],
|
||||||
|
},
|
||||||
speed: [],
|
speed: [],
|
||||||
elevation: {
|
elevation: {
|
||||||
smoothed: [],
|
smoothed: [],
|
||||||
@@ -773,11 +861,12 @@ export class GPXStatistics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mergeWith(other: GPXStatistics): void {
|
mergeWith(other: GPXStatistics): void {
|
||||||
|
|
||||||
this.local.points = this.local.points.concat(other.local.points);
|
this.local.points = this.local.points.concat(other.local.points);
|
||||||
|
|
||||||
this.local.distance = this.local.distance.concat(other.local.distance.map((distance) => distance + this.global.distance.total));
|
this.local.distance.total = this.local.distance.total.concat(other.local.distance.total.map((distance) => distance + this.global.distance.total));
|
||||||
this.local.time = this.local.time.concat(other.local.time.map((time) => time + this.global.time.total));
|
this.local.distance.moving = this.local.distance.moving.concat(other.local.distance.moving.map((distance) => distance + this.global.distance.moving));
|
||||||
|
this.local.time.total = this.local.time.total.concat(other.local.time.total.map((time) => time + this.global.time.total));
|
||||||
|
this.local.time.moving = this.local.time.moving.concat(other.local.time.moving.map((time) => time + this.global.time.moving));
|
||||||
this.local.elevation.gain = this.local.elevation.gain.concat(other.local.elevation.gain.map((gain) => gain + this.global.elevation.gain));
|
this.local.elevation.gain = this.local.elevation.gain.concat(other.local.elevation.gain.map((gain) => gain + this.global.elevation.gain));
|
||||||
this.local.elevation.loss = this.local.elevation.loss.concat(other.local.elevation.loss.map((loss) => loss + this.global.elevation.loss));
|
this.local.elevation.loss = this.local.elevation.loss.concat(other.local.elevation.loss.map((loss) => loss + this.global.elevation.loss));
|
||||||
|
|
||||||
@@ -791,8 +880,8 @@ export class GPXStatistics {
|
|||||||
this.global.time.total += other.global.time.total;
|
this.global.time.total += other.global.time.total;
|
||||||
this.global.time.moving += other.global.time.moving;
|
this.global.time.moving += other.global.time.moving;
|
||||||
|
|
||||||
this.global.speed.moving = this.global.distance.moving / (this.global.time.moving / 3600);
|
this.global.speed.moving = this.global.time.moving > 0 ? this.global.distance.moving / (this.global.time.moving / 3600) : 0;
|
||||||
this.global.speed.total = this.global.distance.total / (this.global.time.total / 3600);
|
this.global.speed.total = this.global.time.total > 0 ? this.global.distance.total / (this.global.time.total / 3600) : 0;
|
||||||
|
|
||||||
this.global.elevation.gain += other.global.elevation.gain;
|
this.global.elevation.gain += other.global.elevation.gain;
|
||||||
this.global.elevation.loss += other.global.elevation.loss;
|
this.global.elevation.loss += other.global.elevation.loss;
|
||||||
@@ -802,6 +891,31 @@ export class GPXStatistics {
|
|||||||
this.global.bounds.northEast.lat = Math.max(this.global.bounds.northEast.lat, other.global.bounds.northEast.lat);
|
this.global.bounds.northEast.lat = Math.max(this.global.bounds.northEast.lat, other.global.bounds.northEast.lat);
|
||||||
this.global.bounds.northEast.lon = Math.max(this.global.bounds.northEast.lon, other.global.bounds.northEast.lon);
|
this.global.bounds.northEast.lon = Math.max(this.global.bounds.northEast.lon, other.global.bounds.northEast.lon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slice(start: number, end: number): GPXStatistics {
|
||||||
|
let statistics = new GPXStatistics();
|
||||||
|
|
||||||
|
statistics.local.points = this.local.points.slice(start, end);
|
||||||
|
|
||||||
|
statistics.global.distance.total = this.local.distance.total[end - 1] - this.local.distance.total[start];
|
||||||
|
statistics.global.distance.moving = this.local.distance.moving[end - 1] - this.local.distance.moving[start];
|
||||||
|
|
||||||
|
statistics.global.time.total = this.local.time.total[end - 1] - this.local.time.total[start];
|
||||||
|
statistics.global.time.moving = this.local.time.moving[end - 1] - this.local.time.moving[start];
|
||||||
|
|
||||||
|
statistics.global.speed.moving = statistics.global.time.moving > 0 ? statistics.global.distance.moving / (statistics.global.time.moving / 3600) : 0;
|
||||||
|
statistics.global.speed.total = statistics.global.time.total > 0 ? statistics.global.distance.total / (statistics.global.time.total / 3600) : 0;
|
||||||
|
|
||||||
|
statistics.global.elevation.gain = this.local.elevation.gain[end - 1] - this.local.elevation.gain[start];
|
||||||
|
statistics.global.elevation.loss = this.local.elevation.loss[end - 1] - this.local.elevation.loss[start];
|
||||||
|
|
||||||
|
statistics.global.bounds.southWest.lat = this.global.bounds.southWest.lat;
|
||||||
|
statistics.global.bounds.southWest.lon = this.global.bounds.southWest.lon;
|
||||||
|
statistics.global.bounds.northEast.lat = this.global.bounds.northEast.lat;
|
||||||
|
statistics.global.bounds.northEast.lon = this.global.bounds.northEast.lon;
|
||||||
|
|
||||||
|
return statistics;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const earthRadius = 6371008.8;
|
const earthRadius = 6371008.8;
|
||||||
|
8
website/package-lock.json
generated
8
website/package-lock.json
generated
@@ -9,7 +9,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/mapbox-gl-geocoder": "^5.0.2",
|
"@mapbox/mapbox-gl-geocoder": "^5.0.2",
|
||||||
"bits-ui": "^0.21.5",
|
"bits-ui": "^0.21.10",
|
||||||
"chart.js": "^4.4.2",
|
"chart.js": "^4.4.2",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"dexie": "^4.0.4",
|
"dexie": "^4.0.4",
|
||||||
@@ -1882,9 +1882,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bits-ui": {
|
"node_modules/bits-ui": {
|
||||||
"version": "0.21.7",
|
"version": "0.21.10",
|
||||||
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-0.21.7.tgz",
|
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-0.21.10.tgz",
|
||||||
"integrity": "sha512-1PKp90ly1R6jexIiAUj1Dk4u2pln7ok+L8Vc0rHMY7pi7YZvadFNZvkp1G5BtmL8qh2xsn4MVNgKjPAQMCxW0A==",
|
"integrity": "sha512-KuweEOKO0Rr8XX87dQh46G9mG0bZSmTqNxj5qBazz4OTQC+oPKui04/wP/ISsCOSGFomaRydTULqh4p+nsyc2g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@internationalized/date": "^3.5.1",
|
"@internationalized/date": "^3.5.1",
|
||||||
"@melt-ui/svelte": "0.76.2",
|
"@melt-ui/svelte": "0.76.2",
|
||||||
|
@@ -42,7 +42,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/mapbox-gl-geocoder": "^5.0.2",
|
"@mapbox/mapbox-gl-geocoder": "^5.0.2",
|
||||||
"bits-ui": "^0.21.5",
|
"bits-ui": "^0.21.10",
|
||||||
"chart.js": "^4.4.2",
|
"chart.js": "^4.4.2",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"dexie": "^4.0.4",
|
"dexie": "^4.0.4",
|
||||||
|
@@ -242,7 +242,7 @@
|
|||||||
label: $_('quantities.elevation'),
|
label: $_('quantities.elevation'),
|
||||||
data: data.local.points.map((point, index) => {
|
data: data.local.points.map((point, index) => {
|
||||||
return {
|
return {
|
||||||
x: getConvertedDistance(data.local.distance[index]),
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
y: point.ele ? getConvertedElevation(point.ele) : 0,
|
y: point.ele ? getConvertedElevation(point.ele) : 0,
|
||||||
slope: data.local.slope[index],
|
slope: data.local.slope[index],
|
||||||
surface: point.getSurface(),
|
surface: point.getSurface(),
|
||||||
@@ -257,7 +257,7 @@
|
|||||||
label: datasets.speed.getLabel(),
|
label: datasets.speed.getLabel(),
|
||||||
data: data.local.points.map((point, index) => {
|
data: data.local.points.map((point, index) => {
|
||||||
return {
|
return {
|
||||||
x: getConvertedDistance(data.local.distance[index]),
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
y: getConvertedVelocity(data.local.speed[index])
|
y: getConvertedVelocity(data.local.speed[index])
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -269,7 +269,7 @@
|
|||||||
label: datasets.hr.getLabel(),
|
label: datasets.hr.getLabel(),
|
||||||
data: data.local.points.map((point, index) => {
|
data: data.local.points.map((point, index) => {
|
||||||
return {
|
return {
|
||||||
x: getConvertedDistance(data.local.distance[index]),
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
y: point.getHeartRate()
|
y: point.getHeartRate()
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -281,7 +281,7 @@
|
|||||||
label: datasets.cad.getLabel(),
|
label: datasets.cad.getLabel(),
|
||||||
data: data.local.points.map((point, index) => {
|
data: data.local.points.map((point, index) => {
|
||||||
return {
|
return {
|
||||||
x: getConvertedDistance(data.local.distance[index]),
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
y: point.getCadence()
|
y: point.getCadence()
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -293,7 +293,7 @@
|
|||||||
label: datasets.atemp.getLabel(),
|
label: datasets.atemp.getLabel(),
|
||||||
data: data.local.points.map((point, index) => {
|
data: data.local.points.map((point, index) => {
|
||||||
return {
|
return {
|
||||||
x: getConvertedDistance(data.local.distance[index]),
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
y: getConvertedTemperature(point.getTemperature())
|
y: getConvertedTemperature(point.getTemperature())
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -305,7 +305,7 @@
|
|||||||
label: datasets.power.getLabel(),
|
label: datasets.power.getLabel(),
|
||||||
data: data.local.points.map((point, index) => {
|
data: data.local.points.map((point, index) => {
|
||||||
return {
|
return {
|
||||||
x: getConvertedDistance(data.local.distance[index]),
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
y: point.getPower()
|
y: point.getPower()
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
@@ -3,14 +3,23 @@
|
|||||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
import WithUnits from '$lib/components/WithUnits.svelte';
|
import WithUnits from '$lib/components/WithUnits.svelte';
|
||||||
|
|
||||||
import { gpxStatistics } from '$lib/stores';
|
import { gpxStatistics, slicedGPXStatistics, currentTool, Tool } from '$lib/stores';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
|
|
||||||
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from 'lucide-svelte';
|
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from 'lucide-svelte';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
|
import type { GPXStatistics } from 'gpx';
|
||||||
|
|
||||||
const { velocityUnits, elevationProfile } = settings;
|
const { velocityUnits, elevationProfile } = settings;
|
||||||
|
|
||||||
|
let statistics: GPXStatistics;
|
||||||
|
|
||||||
|
$: if ($currentTool === Tool.SCISSORS) {
|
||||||
|
statistics = $slicedGPXStatistics;
|
||||||
|
} else {
|
||||||
|
statistics = $gpxStatistics;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card.Root
|
<Card.Root
|
||||||
@@ -26,25 +35,25 @@
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<span slot="data" class="flex flex-row items-center">
|
<span slot="data" class="flex flex-row items-center">
|
||||||
<Ruler size="18" class="mr-1" />
|
<Ruler size="18" class="mr-1" />
|
||||||
<WithUnits value={$gpxStatistics.global.distance.total} type="distance" />
|
<WithUnits value={statistics.global.distance.total} type="distance" />
|
||||||
</span>
|
</span>
|
||||||
<span slot="tooltip">{$_('quantities.distance')}</span>
|
<span slot="tooltip">{$_('quantities.distance')}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<span slot="data" class="flex flex-row items-center">
|
<span slot="data" class="flex flex-row items-center">
|
||||||
<MoveUpRight size="18" class="mr-1" />
|
<MoveUpRight size="18" class="mr-1" />
|
||||||
<WithUnits value={$gpxStatistics.global.elevation.gain} type="elevation" />
|
<WithUnits value={statistics.global.elevation.gain} type="elevation" />
|
||||||
<MoveDownRight size="18" class="mx-1" />
|
<MoveDownRight size="18" class="mx-1" />
|
||||||
<WithUnits value={$gpxStatistics.global.elevation.loss} type="elevation" />
|
<WithUnits value={statistics.global.elevation.loss} type="elevation" />
|
||||||
</span>
|
</span>
|
||||||
<span slot="tooltip">{$_('quantities.elevation')}</span>
|
<span slot="tooltip">{$_('quantities.elevation')}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<span slot="data" class="flex flex-row items-center">
|
<span slot="data" class="flex flex-row items-center">
|
||||||
<Zap size="18" class="mr-1" />
|
<Zap size="18" class="mr-1" />
|
||||||
<WithUnits value={$gpxStatistics.global.speed.total} type="speed" showUnits={false} />
|
<WithUnits value={statistics.global.speed.total} type="speed" showUnits={false} />
|
||||||
<span class="mx-1">/</span>
|
<span class="mx-1">/</span>
|
||||||
<WithUnits value={$gpxStatistics.global.speed.moving} type="speed" />
|
<WithUnits value={statistics.global.speed.moving} type="speed" />
|
||||||
</span>
|
</span>
|
||||||
<span slot="tooltip"
|
<span slot="tooltip"
|
||||||
>{$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_(
|
>{$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_(
|
||||||
@@ -55,9 +64,9 @@
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<span slot="data" class="flex flex-row items-center">
|
<span slot="data" class="flex flex-row items-center">
|
||||||
<Timer size="18" class="mr-1" />
|
<Timer size="18" class="mr-1" />
|
||||||
<WithUnits value={$gpxStatistics.global.time.total} type="time" />
|
<WithUnits value={statistics.global.time.total} type="time" />
|
||||||
<span class="mx-1">/</span>
|
<span class="mx-1">/</span>
|
||||||
<WithUnits value={$gpxStatistics.global.time.moving} type="time" />
|
<WithUnits value={statistics.global.time.moving} type="time" />
|
||||||
</span>
|
</span>
|
||||||
<span slot="tooltip"
|
<span slot="tooltip"
|
||||||
>{$_('quantities.time')} ({$_('quantities.total')} / {$_('quantities.moving')})</span
|
>{$_('quantities.time')} ({$_('quantities.total')} / {$_('quantities.moving')})</span
|
||||||
|
@@ -102,7 +102,7 @@
|
|||||||
<Logo class="h-5 mt-0.5 mx-2" />
|
<Logo class="h-5 mt-0.5 mx-2" />
|
||||||
<Menubar.Root class="border-none h-fit p-0">
|
<Menubar.Root class="border-none h-fit p-0">
|
||||||
<Menubar.Menu>
|
<Menubar.Menu>
|
||||||
<Menubar.Trigger>{$_('menu.file')}</Menubar.Trigger>
|
<Menubar.Trigger>{$_('gpx.file')}</Menubar.Trigger>
|
||||||
<Menubar.Content class="border-none">
|
<Menubar.Content class="border-none">
|
||||||
<Menubar.Item on:click={createFile}>
|
<Menubar.Item on:click={createFile}>
|
||||||
<Plus size="16" class="mr-1" />
|
<Plus size="16" class="mr-1" />
|
||||||
|
@@ -129,8 +129,10 @@ export class SelectionTreeType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteChild(id: string | number) {
|
deleteChild(id: string | number) {
|
||||||
this.size -= this.children[id].size;
|
if (this.children.hasOwnProperty(id)) {
|
||||||
delete this.children[id];
|
this.size -= this.children[id].size;
|
||||||
|
delete this.children[id];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@ import { addSelectItem, selectItem, selection } from "$lib/components/file-list/
|
|||||||
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList";
|
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList";
|
||||||
import type { Waypoint } from "gpx";
|
import type { Waypoint } from "gpx";
|
||||||
import { produce } from "immer";
|
import { produce } from "immer";
|
||||||
import { resetCursor, setGrabbingCursor, setPointerCursor } from "$lib/utils";
|
import { resetCursor, setCursor, setGrabbingCursor, setPointerCursor } from "$lib/utils";
|
||||||
import { font } from "$lib/assets/layers";
|
import { font } from "$lib/assets/layers";
|
||||||
|
|
||||||
let defaultWeight = 5;
|
let defaultWeight = 5;
|
||||||
@@ -56,7 +56,9 @@ export class GPXLayer {
|
|||||||
unsubscribe: Function[] = [];
|
unsubscribe: Function[] = [];
|
||||||
|
|
||||||
updateBinded: () => void = this.update.bind(this);
|
updateBinded: () => void = this.update.bind(this);
|
||||||
selectOnClickBinded: (e: any) => void = this.selectOnClick.bind(this);
|
layerOnMouseEnterBinded: () => void = this.layerOnMouseEnter.bind(this);
|
||||||
|
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
|
||||||
|
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
||||||
|
|
||||||
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
|
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
@@ -122,9 +124,9 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.map.on('click', this.fileId, this.selectOnClickBinded);
|
this.map.on('click', this.fileId, this.layerOnClickBinded);
|
||||||
this.map.on('mouseenter', this.fileId, setPointerCursor);
|
this.map.on('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
||||||
this.map.on('mouseleave', this.fileId, resetCursor);
|
this.map.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get(directionMarkers)) {
|
if (get(directionMarkers)) {
|
||||||
@@ -232,9 +234,9 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.map.off('click', this.fileId, this.selectOnClickBinded);
|
this.map.off('click', this.fileId, this.layerOnClickBinded);
|
||||||
this.map.off('mouseenter', this.fileId, setPointerCursor);
|
this.map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
||||||
this.map.off('mouseleave', this.fileId, resetCursor);
|
this.map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
||||||
this.map.off('style.load', this.updateBinded);
|
this.map.off('style.load', this.updateBinded);
|
||||||
|
|
||||||
if (this.map.getLayer(this.fileId + '-direction')) {
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
@@ -265,11 +267,34 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectOnClick(e: any) {
|
layerOnMouseEnter(e: any) {
|
||||||
|
let trackIndex = e.features[0].properties.trackIndex;
|
||||||
|
let segmentIndex = e.features[0].properties.segmentIndex;
|
||||||
|
|
||||||
|
if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
|
||||||
|
setCursor(`url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" version="1.1"><path d="M 3.200 3.200 C 0.441 5.959, 2.384 9.516, 7 10.154 C 10.466 10.634, 10.187 13.359, 6.607 13.990 C 2.934 14.637, 1.078 17.314, 2.612 19.750 C 4.899 23.380, 10 21.935, 10 17.657 C 10 16.445, 12.405 13.128, 15.693 9.805 C 18.824 6.641, 21.066 3.732, 20.674 3.341 C 20.283 2.950, 18.212 4.340, 16.072 6.430 C 12.019 10.388, 10 10.458, 10 6.641 C 10 2.602, 5.882 0.518, 3.200 3.200 M 4.446 5.087 C 3.416 6.755, 5.733 8.667, 7.113 7.287 C 8.267 6.133, 7.545 4, 6 4 C 5.515 4, 4.816 4.489, 4.446 5.087 M 14 14.813 C 14 16.187, 19.935 21.398, 20.667 20.667 C 21.045 20.289, 20.065 18.634, 18.490 16.990 C 15.661 14.036, 14 13.231, 14 14.813 M 4.446 17.087 C 3.416 18.755, 5.733 20.667, 7.113 19.287 C 8.267 18.133, 7.545 16, 6 16 C 5.515 16, 4.816 16.489, 4.446 17.087" stroke="black" stroke-width="1.2" fill="white" fill-rule="evenodd"/></svg>') 12 12, auto`);
|
||||||
|
} else {
|
||||||
|
setPointerCursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layerOnMouseLeave() {
|
||||||
|
resetCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
layerOnClick(e: any) {
|
||||||
if (get(currentTool) === Tool.ROUTING && get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])) {
|
if (get(currentTool) === Tool.ROUTING && get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let trackIndex = e.features[0].properties.trackIndex;
|
||||||
|
let segmentIndex = e.features[0].properties.segmentIndex;
|
||||||
|
|
||||||
|
if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
|
||||||
|
dbUtils.split(this.fileId, trackIndex, segmentIndex, { lat: e.lngLat.lat, lon: e.lngLat.lng });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let file = get(this.file)?.file;
|
let file = get(this.file)?.file;
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
@@ -277,8 +302,6 @@ export class GPXLayer {
|
|||||||
|
|
||||||
let item = undefined;
|
let item = undefined;
|
||||||
if (get(verticalFileView) && file.getSegments().length > 1) { // Select inner item
|
if (get(verticalFileView) && file.getSegments().length > 1) { // Select inner item
|
||||||
let trackIndex = e.features[0].properties.trackIndex;
|
|
||||||
let segmentIndex = e.features[0].properties.segmentIndex;
|
|
||||||
item = file.children[trackIndex].children.length > 1 ? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) : new ListTrackItem(this.fileId, trackIndex);
|
item = file.children[trackIndex].children.length > 1 ? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) : new ListTrackItem(this.fileId, trackIndex);
|
||||||
} else {
|
} else {
|
||||||
item = new ListFileItem(this.fileId);
|
item = new ListFileItem(this.fileId);
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { gpxStatistics, currentTool, Tool } from "$lib/stores";
|
import { gpxStatistics, slicedGPXStatistics, currentTool, Tool } from "$lib/stores";
|
||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from "mapbox-gl";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
@@ -21,12 +21,14 @@ export class StartEndMarkers {
|
|||||||
this.end = new mapboxgl.Marker({ element: endElement });
|
this.end = new mapboxgl.Marker({ element: endElement });
|
||||||
|
|
||||||
gpxStatistics.subscribe(this.updateBinded);
|
gpxStatistics.subscribe(this.updateBinded);
|
||||||
|
slicedGPXStatistics.subscribe(this.updateBinded);
|
||||||
currentTool.subscribe(this.updateBinded);
|
currentTool.subscribe(this.updateBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
let statistics = get(gpxStatistics);
|
let tool = get(currentTool);
|
||||||
if (statistics.local.points.length > 0 && get(currentTool) !== Tool.ROUTING) {
|
let statistics = tool === Tool.SCISSORS ? get(slicedGPXStatistics) : get(gpxStatistics);
|
||||||
|
if (statistics.local.points.length > 0 && tool !== Tool.ROUTING) {
|
||||||
this.start.setLngLat(statistics.local.points[0].getCoordinates()).addTo(this.map);
|
this.start.setLngLat(statistics.local.points[0].getCoordinates()).addTo(this.map);
|
||||||
this.end.setLngLat(statistics.local.points[statistics.local.points.length - 1].getCoordinates()).addTo(this.map);
|
this.end.setLngLat(statistics.local.points[statistics.local.points.length - 1].getCoordinates()).addTo(this.map);
|
||||||
} else {
|
} else {
|
||||||
|
@@ -1 +1,100 @@
|
|||||||
<div>- Start/end sliders - Cut by clicking on a route</div>
|
<script lang="ts" context="module">
|
||||||
|
export enum SplitType {
|
||||||
|
FILES = 'files',
|
||||||
|
TRACKS = 'tracks',
|
||||||
|
SEGMENTS = 'segments'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Help from '$lib/components/Help.svelte';
|
||||||
|
import { ListRootItem } from '$lib/components/file-list/FileList';
|
||||||
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Slider } from '$lib/components/ui/slider';
|
||||||
|
import * as Select from '$lib/components/ui/select';
|
||||||
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
|
import { gpxStatistics, slicedGPXStatistics, splitAs } from '$lib/stores';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
import { tick } from 'svelte';
|
||||||
|
import { Crop } from 'lucide-svelte';
|
||||||
|
import { dbUtils } from '$lib/db';
|
||||||
|
|
||||||
|
$: validSelection = $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
|
||||||
|
|
||||||
|
let maxSliderValue = 100;
|
||||||
|
let sliderValues = [0, 100];
|
||||||
|
|
||||||
|
function updateSlicedGPXStatistics() {
|
||||||
|
if (validSelection) {
|
||||||
|
slicedGPXStatistics.set(get(gpxStatistics).slice(sliderValues[0], sliderValues[1]));
|
||||||
|
} else {
|
||||||
|
slicedGPXStatistics.set(get(gpxStatistics));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSliderLimits() {
|
||||||
|
if (validSelection) {
|
||||||
|
maxSliderValue = $gpxStatistics.local.points.length;
|
||||||
|
} else {
|
||||||
|
maxSliderValue = 100;
|
||||||
|
}
|
||||||
|
await tick();
|
||||||
|
sliderValues = [0, maxSliderValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if ($gpxStatistics.local.points.length != maxSliderValue) {
|
||||||
|
updateSliderLimits();
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (sliderValues) {
|
||||||
|
updateSlicedGPXStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitTypes = [
|
||||||
|
{ value: SplitType.FILES, label: $_('gpx.files') },
|
||||||
|
{ value: SplitType.TRACKS, label: $_('gpx.tracks') },
|
||||||
|
{ value: SplitType.SEGMENTS, label: $_('gpx.segments') }
|
||||||
|
];
|
||||||
|
|
||||||
|
let splitType = splitTypes[0];
|
||||||
|
|
||||||
|
$: splitAs.set(splitType.value);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3 max-w-80">
|
||||||
|
<div class="p-2">
|
||||||
|
<Slider bind:value={sliderValues} max={maxSliderValue} step={1} disabled={!validSelection} />
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
disabled={!validSelection}
|
||||||
|
on:click={() => dbUtils.cropSelection(sliderValues[0], sliderValues[1])}
|
||||||
|
><Crop size="16" class="mr-1" />{$_('toolbar.scissors.crop')}</Button
|
||||||
|
>
|
||||||
|
<Separator />
|
||||||
|
<Label class="flex flex-row gap-3 items-center">
|
||||||
|
<span class="shrink-0">
|
||||||
|
{$_('toolbar.scissors.split_as')}
|
||||||
|
</span>
|
||||||
|
<Select.Root bind:selected={splitType}>
|
||||||
|
<Select.Trigger class="h-8">
|
||||||
|
<Select.Value />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
{#each splitTypes as { value, label }}
|
||||||
|
<Select.Item {value}>{label}</Select.Item>
|
||||||
|
{/each}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
</Label>
|
||||||
|
<Help>
|
||||||
|
{#if validSelection}
|
||||||
|
{$_('toolbar.scissors.help')}
|
||||||
|
{:else}
|
||||||
|
{$_('toolbar.scissors.help_invalid_selection')}
|
||||||
|
{/if}
|
||||||
|
</Help>
|
||||||
|
</div>
|
||||||
|
@@ -291,7 +291,7 @@ export class RoutingControls {
|
|||||||
getPermanentAnchor(): Anchor {
|
getPermanentAnchor(): Anchor {
|
||||||
let file = get(this.file)?.file;
|
let file = get(this.file)?.file;
|
||||||
|
|
||||||
// Find the closest point closest to the temporary anchor
|
// Find the point closest to the temporary anchor
|
||||||
let minDistance = Number.MAX_VALUE;
|
let minDistance = Number.MAX_VALUE;
|
||||||
let minAnchor = this.temporaryAnchor as Anchor;
|
let minAnchor = this.temporaryAnchor as Anchor;
|
||||||
file?.forEachSegment((segment, trackIndex, segmentIndex) => {
|
file?.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||||
|
7
website/src/lib/components/ui/slider/index.ts
Normal file
7
website/src/lib/components/ui/slider/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./slider.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Slider,
|
||||||
|
};
|
27
website/src/lib/components/ui/slider/slider.svelte
Normal file
27
website/src/lib/components/ui/slider/slider.svelte
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Slider as SliderPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = SliderPrimitive.Props;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let value: $$Props["value"] = [0];
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
bind:value
|
||||||
|
class={cn("relative flex w-full touch-none select-none items-center", className)}
|
||||||
|
{...$$restProps}
|
||||||
|
let:thumbs
|
||||||
|
>
|
||||||
|
<span class="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||||
|
<SliderPrimitive.Range class="absolute h-full bg-primary" />
|
||||||
|
</span>
|
||||||
|
{#each thumbs as thumb}
|
||||||
|
<SliderPrimitive.Thumb
|
||||||
|
{thumb}
|
||||||
|
class="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</SliderPrimitive.Root>
|
@@ -1,13 +1,14 @@
|
|||||||
import Dexie, { liveQuery } from 'dexie';
|
import Dexie, { liveQuery } from 'dexie';
|
||||||
import { GPXFile, GPXStatistics, Track, type AnyGPXTreeElement, TrackSegment, Waypoint, TrackPoint } from 'gpx';
|
import { GPXFile, GPXStatistics, Track, type AnyGPXTreeElement, TrackSegment, Waypoint, TrackPoint, type Coordinates, distance } from 'gpx';
|
||||||
import { enableMapSet, enablePatches, applyPatches, type Patch, type WritableDraft, castDraft, freeze, produceWithPatches, original, produce } from 'immer';
|
import { enableMapSet, enablePatches, applyPatches, type Patch, type WritableDraft, castDraft, freeze, produceWithPatches, original, produce } from 'immer';
|
||||||
import { writable, get, derived, type Readable, type Writable } from 'svelte/store';
|
import { writable, get, derived, type Readable, type Writable } from 'svelte/store';
|
||||||
import { initTargetMapBounds, updateTargetMapBounds } from './stores';
|
import { initTargetMapBounds, splitAs, updateTargetMapBounds } from './stores';
|
||||||
import { mode } from 'mode-watcher';
|
import { mode } from 'mode-watcher';
|
||||||
import { defaultBasemap, defaultBasemapTree, defaultOverlayTree, defaultOverlays } from './assets/layers';
|
import { defaultBasemap, defaultBasemapTree, defaultOverlayTree, defaultOverlays } from './assets/layers';
|
||||||
import { applyToOrderedSelectedItemsFromFile, selection } from '$lib/components/file-list/Selection';
|
import { applyToOrderedSelectedItemsFromFile, selection } from '$lib/components/file-list/Selection';
|
||||||
import { ListFileItem, ListItem, ListTrackItem, ListLevel, ListTrackSegmentItem, ListWaypointItem, ListRootItem } from '$lib/components/file-list/FileList';
|
import { ListFileItem, ListItem, ListTrackItem, ListLevel, ListTrackSegmentItem, ListWaypointItem, ListRootItem } from '$lib/components/file-list/FileList';
|
||||||
import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/Simplify';
|
import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/Simplify';
|
||||||
|
import { SplitType } from '$lib/components/toolbar/tools/Scissors.svelte';
|
||||||
|
|
||||||
enableMapSet();
|
enableMapSet();
|
||||||
enablePatches();
|
enablePatches();
|
||||||
@@ -393,35 +394,6 @@ export const dbUtils = {
|
|||||||
applyEachToFilesAndGlobal: (ids: string[], callbacks: ((file: WritableDraft<GPXFile>, context?: any) => GPXFile)[], globalCallback: (files: Map<string, GPXFile>, context?: any) => void, context?: any) => {
|
applyEachToFilesAndGlobal: (ids: string[], callbacks: ((file: WritableDraft<GPXFile>, context?: any) => GPXFile)[], globalCallback: (files: Map<string, GPXFile>, context?: any) => void, context?: any) => {
|
||||||
applyEachToFilesAndGlobal(ids, callbacks, globalCallback, context);
|
applyEachToFilesAndGlobal(ids, callbacks, globalCallback, context);
|
||||||
},
|
},
|
||||||
applyToSelection: (callback: (file: WritableDraft<AnyGPXTreeElement>) => AnyGPXTreeElement) => {
|
|
||||||
if (get(selection).size === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applyGlobal((draft) => {
|
|
||||||
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
|
||||||
let file = draft.get(fileId);
|
|
||||||
if (file) {
|
|
||||||
for (let item of items) {
|
|
||||||
if (item instanceof ListFileItem) {
|
|
||||||
callback(castDraft(file));
|
|
||||||
} else if (item instanceof ListTrackItem) {
|
|
||||||
let trackIndex = item.getTrackIndex();
|
|
||||||
file = produce(file, (fileDraft) => {
|
|
||||||
callback(fileDraft.trk[trackIndex]);
|
|
||||||
});
|
|
||||||
} else if (item instanceof ListTrackSegmentItem) {
|
|
||||||
let trackIndex = item.getTrackIndex();
|
|
||||||
let segmentIndex = item.getSegmentIndex();
|
|
||||||
file = produce(file, (fileDraft) => {
|
|
||||||
callback(fileDraft.trk[trackIndex].trkseg[segmentIndex]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
draft.set(fileId, freeze(file));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
duplicateSelection: () => {
|
duplicateSelection: () => {
|
||||||
if (get(selection).size === 0) {
|
if (get(selection).size === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -601,6 +573,80 @@ export const dbUtils = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
cropSelection: (start: number, end: number) => {
|
||||||
|
if (get(selection).size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyGlobal((draft) => {
|
||||||
|
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||||
|
let file = original(draft)?.get(fileId);
|
||||||
|
if (file) {
|
||||||
|
if (level === ListLevel.FILE) {
|
||||||
|
let length = file.getNumberOfTrackPoints();
|
||||||
|
if (start >= length || end < 0) {
|
||||||
|
draft.delete(fileId);
|
||||||
|
} else if (start > 0 || end < length - 1) {
|
||||||
|
let newFile = file.crop(Math.max(0, start), Math.min(length - 1, end));
|
||||||
|
draft.set(newFile._data.id, freeze(newFile));
|
||||||
|
}
|
||||||
|
start -= length;
|
||||||
|
end -= length;
|
||||||
|
} else if (level === ListLevel.TRACK) {
|
||||||
|
let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex());
|
||||||
|
let newFile = file.crop(start, end, trackIndices);
|
||||||
|
draft.set(newFile._data.id, freeze(newFile));
|
||||||
|
} else if (level === ListLevel.SEGMENT) {
|
||||||
|
let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()];
|
||||||
|
let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex());
|
||||||
|
let newFile = file.crop(start, end, trackIndices, segmentIndices);
|
||||||
|
draft.set(newFile._data.id, freeze(newFile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
split(fileId: string, trackIndex: number, segmentIndex: number, coordinates: Coordinates) {
|
||||||
|
let splitType = get(splitAs);
|
||||||
|
return applyGlobal((draft) => {
|
||||||
|
let file = original(draft)?.get(fileId);
|
||||||
|
if (file) {
|
||||||
|
let segment = file.trk[trackIndex].trkseg[segmentIndex];
|
||||||
|
|
||||||
|
// Find the point closest to split
|
||||||
|
let minDistance = Number.MAX_VALUE;
|
||||||
|
let minIndex = 0;
|
||||||
|
for (let i = 0; i < segment.trkpt.length; i++) {
|
||||||
|
let dist = distance(segment.trkpt[i].getCoordinates(), coordinates);
|
||||||
|
if (dist < minDistance) {
|
||||||
|
minDistance = dist;
|
||||||
|
minIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let absoluteIndex = minIndex;
|
||||||
|
file.forEachSegment((seg, trkIndex, segIndex) => {
|
||||||
|
if ((trkIndex < trackIndex && splitType === SplitType.FILES) || (trkIndex === trackIndex && segIndex < segmentIndex)) {
|
||||||
|
absoluteIndex += seg.trkpt.length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (splitType === SplitType.FILES) {
|
||||||
|
let newFile = file.crop(0, absoluteIndex);
|
||||||
|
draft.set(newFile._data.id, freeze(newFile));
|
||||||
|
let newFile2 = file.clone();
|
||||||
|
newFile2._data.id = getFileIds(1)[0];
|
||||||
|
newFile2 = newFile2.crop(absoluteIndex, file.getNumberOfTrackPoints() - 1);
|
||||||
|
draft.set(newFile2._data.id, freeze(newFile2));
|
||||||
|
} else if (splitType === SplitType.TRACKS) {
|
||||||
|
let newFile = file.replaceTracks(trackIndex, trackIndex, [file.trk[trackIndex].crop(0, absoluteIndex), file.trk[trackIndex].crop(absoluteIndex, file.trk[trackIndex].getNumberOfTrackPoints() - 1)])[0];
|
||||||
|
draft.set(newFile._data.id, freeze(newFile));
|
||||||
|
} else if (splitType === SplitType.SEGMENTS) {
|
||||||
|
let newFile = file.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex, [segment.crop(0, minIndex), segment.crop(minIndex, segment.trkpt.length - 1)])[0];
|
||||||
|
draft.set(newFile._data.id, freeze(newFile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
deleteSelection: () => {
|
deleteSelection: () => {
|
||||||
if (get(selection).size === 0) {
|
if (get(selection).size === 0) {
|
||||||
return;
|
return;
|
||||||
|
@@ -10,11 +10,13 @@ import { applyToOrderedSelectedItemsFromFile, selectFile, selection } from '$lib
|
|||||||
import { ListFileItem, ListWaypointItem, ListWaypointsItem } from '$lib/components/file-list/FileList';
|
import { ListFileItem, ListWaypointItem, ListWaypointsItem } from '$lib/components/file-list/FileList';
|
||||||
import type { RoutingControls } from '$lib/components/toolbar/tools/routing/RoutingControls';
|
import type { RoutingControls } from '$lib/components/toolbar/tools/routing/RoutingControls';
|
||||||
import { overlayTree, overlays, stravaHeatmapActivityIds, stravaHeatmapServers } from '$lib/assets/layers';
|
import { overlayTree, overlays, stravaHeatmapActivityIds, stravaHeatmapServers } from '$lib/assets/layers';
|
||||||
|
import { SplitType } from '$lib/components/toolbar/tools/Scissors.svelte';
|
||||||
|
|
||||||
export const map = writable<mapboxgl.Map | null>(null);
|
export const map = writable<mapboxgl.Map | null>(null);
|
||||||
export const selectFiles = writable<{ [key: string]: (fileId?: string) => void }>({});
|
export const selectFiles = writable<{ [key: string]: (fileId?: string) => void }>({});
|
||||||
|
|
||||||
export const gpxStatistics: Writable<GPXStatistics> = writable(new GPXStatistics());
|
export const gpxStatistics: Writable<GPXStatistics> = writable(new GPXStatistics());
|
||||||
|
export const slicedGPXStatistics: Writable<GPXStatistics> = writable(new GPXStatistics());
|
||||||
|
|
||||||
function updateGPXData() {
|
function updateGPXData() {
|
||||||
let statistics = new GPXStatistics();
|
let statistics = new GPXStatistics();
|
||||||
@@ -128,6 +130,7 @@ export enum Tool {
|
|||||||
STYLE
|
STYLE
|
||||||
}
|
}
|
||||||
export const currentTool = writable<Tool | null>(null);
|
export const currentTool = writable<Tool | null>(null);
|
||||||
|
export const splitAs = writable(SplitType.FILES);
|
||||||
|
|
||||||
export function newGPXFile() {
|
export function newGPXFile() {
|
||||||
let file = new GPXFile();
|
let file = new GPXFile();
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"menu": {
|
"menu": {
|
||||||
"file": "File",
|
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"new_filename": "new",
|
"new_filename": "new",
|
||||||
"load_desktop": "Load...",
|
"load_desktop": "Load...",
|
||||||
@@ -112,7 +111,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scissors": {
|
"scissors": {
|
||||||
"tooltip": "Trim or split routes"
|
"tooltip": "Crop or split traces",
|
||||||
|
"crop": "Crop",
|
||||||
|
"split_as": "Split the trace into",
|
||||||
|
"help_invalid_selection": "Select a file element to crop or split",
|
||||||
|
"help": "Use the slider to crop the trace, or click on the map to split it at the selected point"
|
||||||
},
|
},
|
||||||
"time_tooltip": "Manage time and speed data",
|
"time_tooltip": "Manage time and speed data",
|
||||||
"merge": {
|
"merge": {
|
||||||
@@ -129,7 +132,7 @@
|
|||||||
"waypoint_tooltip": "Create and edit points of interest",
|
"waypoint_tooltip": "Create and edit points of interest",
|
||||||
"reduce_tooltip": "Reduce the number of GPS points",
|
"reduce_tooltip": "Reduce the number of GPS points",
|
||||||
"clean_tooltip": "Clean GPS points and points of interest with a rectangle selection",
|
"clean_tooltip": "Clean GPS points and points of interest with a rectangle selection",
|
||||||
"style_tooltip": "Change the style of the route"
|
"style_tooltip": "Change the style of the trace"
|
||||||
},
|
},
|
||||||
"layers": {
|
"layers": {
|
||||||
"settings": "Layer settings",
|
"settings": "Layer settings",
|
||||||
@@ -250,8 +253,12 @@
|
|||||||
"power": "W"
|
"power": "W"
|
||||||
},
|
},
|
||||||
"gpx": {
|
"gpx": {
|
||||||
|
"file": "File",
|
||||||
|
"files": "Files",
|
||||||
"track": "Track",
|
"track": "Track",
|
||||||
|
"tracks": "Tracks",
|
||||||
"segment": "Segment",
|
"segment": "Segment",
|
||||||
|
"segments": "Segments",
|
||||||
"waypoint": "Point of interest",
|
"waypoint": "Point of interest",
|
||||||
"waypoints": "Points of interest"
|
"waypoints": "Points of interest"
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user