mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-08-31 15:43:25 +00:00
time data management
This commit is contained in:
112
gpx/src/gpx.ts
112
gpx/src/gpx.ts
@@ -205,11 +205,11 @@ export class GPXFile extends GPXTreeNode<Track>{
|
||||
return [result, removed];
|
||||
}
|
||||
|
||||
replaceTrackPoints(trackIndex: number, segmentIndex: number, start: number, end: number, points: TrackPoint[]) {
|
||||
replaceTrackPoints(trackIndex: number, segmentIndex: number, start: number, end: number, points: TrackPoint[], speed?: number, startTime?: Date) {
|
||||
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();
|
||||
trk[trackIndex] = trk[trackIndex].replaceTrackPoints(segmentIndex, start, end, points);
|
||||
trk[trackIndex] = trk[trackIndex].replaceTrackPoints(segmentIndex, start, end, points, speed, startTime);
|
||||
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
|
||||
});
|
||||
}
|
||||
@@ -310,6 +310,21 @@ export class GPXFile extends GPXTreeNode<Track>{
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
changeTimestamps(startTime: Date, speed: number, ratio: number, trackIndex?: number, segmentIndex?: number) {
|
||||
let lastPoint = undefined;
|
||||
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.map((track, index) => {
|
||||
if (trackIndex === undefined || trackIndex === index) {
|
||||
return track.changeTimestamps(startTime, speed, ratio, lastPoint, segmentIndex);
|
||||
} else {
|
||||
return track;
|
||||
}
|
||||
});
|
||||
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// A class that represents a Track in a GPX file
|
||||
@@ -405,11 +420,11 @@ export class Track extends GPXTreeNode<TrackSegment> {
|
||||
return [result, removed];
|
||||
}
|
||||
|
||||
replaceTrackPoints(segmentIndex: number, start: number, end: number, points: TrackPoint[]) {
|
||||
replaceTrackPoints(segmentIndex: number, start: number, end: number, points: TrackPoint[], speed?: number, startTime?: Date) {
|
||||
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();
|
||||
trkseg[segmentIndex] = trkseg[segmentIndex].replaceTrackPoints(start, end, points);
|
||||
trkseg[segmentIndex] = trkseg[segmentIndex].replaceTrackPoints(start, end, points, speed, startTime);
|
||||
draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
|
||||
});
|
||||
}
|
||||
@@ -473,6 +488,25 @@ export class Track extends GPXTreeNode<TrackSegment> {
|
||||
draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
|
||||
});
|
||||
}
|
||||
|
||||
changeTimestamps(startTime: Date, speed: number, ratio: number, lastPoint?: TrackPoint, segmentIndex?: 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();
|
||||
trkseg = trkseg.map((segment, index) => {
|
||||
if (segmentIndex === undefined || segmentIndex === index) {
|
||||
let seg = segment.changeTimestamps(startTime, speed, ratio, lastPoint);
|
||||
if (seg.trkpt.length > 0) {
|
||||
lastPoint = seg.trkpt[seg.trkpt.length - 1];
|
||||
}
|
||||
return seg;
|
||||
} else {
|
||||
return segment;
|
||||
}
|
||||
});
|
||||
draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// A class that represents a TrackSegment in a GPX file
|
||||
@@ -634,10 +668,33 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
}
|
||||
|
||||
// Producers
|
||||
replaceTrackPoints(start: number, end: number, points: TrackPoint[]) {
|
||||
replaceTrackPoints(start: number, end: number, points: TrackPoint[], speed?: number, startTime?: Date) {
|
||||
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();
|
||||
|
||||
if (speed !== undefined || (trkpt.length > 0 && trkpt[0].time !== undefined)) {
|
||||
if (start > 0 && trkpt[0].time === undefined) {
|
||||
trkpt.splice(0, 0, withTimestamps(trkpt.splice(0, start), speed, undefined, startTime));
|
||||
}
|
||||
if (points.length > 0) {
|
||||
let last = start > 0 ? trkpt[start - 1] : undefined;
|
||||
if (points[0].time === undefined || (points.length > 1 && points[1].time === undefined)) {
|
||||
points = withTimestamps(points, speed, last, startTime);
|
||||
} else if (last !== undefined && points[0].time < last.time) {
|
||||
points = withShiftedAndCompressedTimestamps(points, speed, 1, last);
|
||||
}
|
||||
}
|
||||
if (end < trkpt.length - 1) {
|
||||
let last = points.length > 0 ? points[points.length - 1] : start > 0 ? trkpt[start - 1] : undefined;
|
||||
if (trkpt[end + 1].time === undefined) {
|
||||
trkpt.splice(end + 1, 0, withTimestamps(trkpt.splice(end + 1), speed, last, startTime));
|
||||
} else if (last !== undefined && trkpt[end + 1].time < last.time) {
|
||||
points = withShiftedAndCompressedTimestamps(points, speed, 1, last);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trkpt.splice(start, end - start + 1, ...points);
|
||||
draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
||||
});
|
||||
@@ -690,6 +747,23 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
||||
});
|
||||
}
|
||||
|
||||
changeTimestamps(startTime: Date, speed: number, ratio: number, lastPoint?: TrackPoint) {
|
||||
if (lastPoint === undefined && this.trkpt.length > 0) {
|
||||
lastPoint = this.trkpt[0].clone();
|
||||
lastPoint.time = startTime;
|
||||
}
|
||||
return produce(this, (draft) => {
|
||||
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
|
||||
if (og.trkpt.length > 0 && og.trkpt[0].time === undefined) {
|
||||
let trkpt = withTimestamps(og.trkpt, speed, lastPoint, startTime);
|
||||
draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
||||
} else {
|
||||
let trkpt = withShiftedAndCompressedTimestamps(og.trkpt, speed, ratio, lastPoint);
|
||||
draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export class TrackPoint {
|
||||
@@ -1039,6 +1113,34 @@ function distanceWindowSmoothingWithDistanceAccumulator(points: ReadonlyArray<Re
|
||||
return distanceWindowSmoothing(points, distanceWindow, (index) => index > 0 ? distance(points[index - 1].getCoordinates(), points[index].getCoordinates()) : 0, compute, (index) => distance(points[index].getCoordinates(), points[index + 1].getCoordinates()));
|
||||
}
|
||||
|
||||
function withTimestamps(points: TrackPoint[], speed: number, lastPoint: TrackPoint | undefined, startTime?: Date): TrackPoint[] {
|
||||
let last = lastPoint;
|
||||
if (last === undefined) {
|
||||
last = points[0].clone();
|
||||
last.time = startTime;
|
||||
}
|
||||
return points.map((point) => {
|
||||
let time = getTimestamp(last, point, speed);
|
||||
last = point.clone();
|
||||
last.time = time;
|
||||
return last;
|
||||
});
|
||||
}
|
||||
|
||||
function withShiftedAndCompressedTimestamps(points: TrackPoint[], speed: number, ratio: number, lastPoint: TrackPoint): TrackPoint[] {
|
||||
let start = getTimestamp(lastPoint, points[0], speed);
|
||||
return points.map((point) => {
|
||||
let pt = point.clone();
|
||||
pt.time = new Date(start.getTime() + ratio * (point.time.getTime() - points[0].time.getTime()));
|
||||
return pt;
|
||||
});
|
||||
}
|
||||
|
||||
function getTimestamp(a: TrackPoint, b: TrackPoint, speed: number): Date {
|
||||
let dist = distance(a.getCoordinates(), b.getCoordinates()) / 1000;
|
||||
return new Date(a.time.getTime() + 1000 * 3600 * dist / speed);
|
||||
}
|
||||
|
||||
function getOriginal(obj: any): any {
|
||||
while (isDraft(obj)) {
|
||||
obj = original(obj);
|
||||
|
@@ -205,6 +205,7 @@
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
reverse: () => id === 'speed' && $velocityUnits === 'pace',
|
||||
display: false
|
||||
};
|
||||
}
|
||||
@@ -256,17 +257,26 @@
|
||||
function getIndex(evt) {
|
||||
const points = chart.getElementsAtEventForMode(
|
||||
evt,
|
||||
'index',
|
||||
'x',
|
||||
{
|
||||
intersect: false
|
||||
},
|
||||
true
|
||||
);
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
return (
|
||||
points.find((point) => point.datasetIndex === 0)?.element.raw.index ??
|
||||
(evt.x - rect.left <= chart.chartArea.left ? 0 : get(gpxStatistics).local.points.length - 1)
|
||||
);
|
||||
|
||||
if (points.length === 0) {
|
||||
return evt.x - rect.left <= chart.chartArea.left
|
||||
? 0
|
||||
: get(gpxStatistics).local.points.length - 1;
|
||||
}
|
||||
let point = points.find((point) => point.element.raw);
|
||||
if (point) {
|
||||
return point.element.raw.index;
|
||||
} else {
|
||||
console.log(points);
|
||||
return points[0].index;
|
||||
}
|
||||
}
|
||||
canvas.addEventListener('pointerdown', (evt) => {
|
||||
dragging = true;
|
||||
@@ -323,7 +333,8 @@
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: getConvertedVelocity(data.local.speed[index])
|
||||
y: getConvertedVelocity(data.local.speed[index]),
|
||||
index: index
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
@@ -335,7 +346,8 @@
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getHeartRate()
|
||||
y: point.getHeartRate(),
|
||||
index: index
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
@@ -347,7 +359,8 @@
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getCadence()
|
||||
y: point.getCadence(),
|
||||
index: index
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
@@ -359,7 +372,8 @@
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: getConvertedTemperature(point.getTemperature())
|
||||
y: getConvertedTemperature(point.getTemperature()),
|
||||
index: index
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
@@ -371,7 +385,8 @@
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getPower()
|
||||
y: point.getPower(),
|
||||
index: index
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
|
@@ -3,7 +3,7 @@
|
||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||
import WithUnits from '$lib/components/WithUnits.svelte';
|
||||
|
||||
import { gpxStatistics, slicedGPXStatistics, currentTool, Tool } from '$lib/stores';
|
||||
import { gpxStatistics, slicedGPXStatistics } from '$lib/stores';
|
||||
import { settings } from '$lib/db';
|
||||
|
||||
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from 'lucide-svelte';
|
||||
@@ -25,7 +25,7 @@
|
||||
<Card.Root
|
||||
class="h-full {$elevationProfile
|
||||
? ''
|
||||
: 'w-full pr-4'} overflow-hidden border-none shadow-none min-w-48 pl-4"
|
||||
: 'w-full pr-4'} overflow-hidden border-none shadow-none min-w-52 pl-4"
|
||||
>
|
||||
<Card.Content
|
||||
class="h-full flex {$elevationProfile
|
||||
|
@@ -5,7 +5,7 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import TimePicker from '$lib/components/ui/time-picker/TimePicker.svelte';
|
||||
import { settings } from '$lib/db';
|
||||
import { dbUtils, settings } from '$lib/db';
|
||||
import { gpxStatistics } from '$lib/stores';
|
||||
import {
|
||||
distancePerHourToSecondsPerDistance,
|
||||
@@ -13,19 +13,24 @@
|
||||
kilometersToMiles
|
||||
} from '$lib/units';
|
||||
import { CalendarDate, type DateValue } from '@internationalized/date';
|
||||
import { CirclePlay, CircleStop, CircleX, RefreshCw, Timer, Zap } from 'lucide-svelte';
|
||||
import { CalendarClock, CirclePlay, CircleStop, CircleX, Timer, Zap } from 'lucide-svelte';
|
||||
import { tick } from 'svelte';
|
||||
import { _, locale } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { selection } from '$lib/components/file-list/Selection';
|
||||
import { ListRootItem } from '$lib/components/file-list/FileList';
|
||||
import {
|
||||
ListFileItem,
|
||||
ListRootItem,
|
||||
ListTrackItem,
|
||||
ListTrackSegmentItem
|
||||
} from '$lib/components/file-list/FileList';
|
||||
import Help from '$lib/components/Help.svelte';
|
||||
|
||||
let startDate: DateValue | undefined = undefined;
|
||||
let startTime: string | undefined = undefined;
|
||||
let endDate: DateValue | undefined = undefined;
|
||||
let endTime: string | undefined = undefined;
|
||||
let totalTime: number | undefined = undefined;
|
||||
let movingTime: number | undefined = undefined;
|
||||
let speed: number | undefined = undefined;
|
||||
|
||||
function toCalendarDate(date: Date): CalendarDate {
|
||||
@@ -57,13 +62,13 @@
|
||||
endDate = undefined;
|
||||
endTime = undefined;
|
||||
}
|
||||
if ($gpxStatistics.global.time.total) {
|
||||
totalTime = $gpxStatistics.global.time.total;
|
||||
if ($gpxStatistics.global.time.moving) {
|
||||
movingTime = $gpxStatistics.global.time.moving;
|
||||
} else {
|
||||
totalTime = undefined;
|
||||
movingTime = undefined;
|
||||
}
|
||||
if ($gpxStatistics.global.speed.total) {
|
||||
setSpeed($gpxStatistics.global.speed.total);
|
||||
if ($gpxStatistics.global.speed.moving) {
|
||||
setSpeed($gpxStatistics.global.speed.moving);
|
||||
} else {
|
||||
speed = undefined;
|
||||
}
|
||||
@@ -82,32 +87,40 @@
|
||||
}
|
||||
|
||||
function updateEnd() {
|
||||
if (startDate && totalTime !== undefined) {
|
||||
if (startDate && movingTime !== undefined) {
|
||||
if (startTime === undefined) {
|
||||
startTime = '00:00:00';
|
||||
}
|
||||
let start = getDate(startDate, startTime);
|
||||
let end = new Date(start.getTime() + totalTime * 1000);
|
||||
let ratio =
|
||||
$gpxStatistics.global.time.moving > 0
|
||||
? $gpxStatistics.global.time.total / $gpxStatistics.global.time.moving
|
||||
: 1;
|
||||
let end = new Date(start.getTime() + ratio * movingTime * 1000);
|
||||
endDate = toCalendarDate(end);
|
||||
endTime = end.toLocaleTimeString();
|
||||
}
|
||||
}
|
||||
|
||||
function updateStart() {
|
||||
if (endDate && totalTime !== undefined) {
|
||||
if (endDate && movingTime !== undefined) {
|
||||
if (endTime === undefined) {
|
||||
endTime = '00:00:00';
|
||||
}
|
||||
let end = getDate(endDate, endTime);
|
||||
let start = new Date(end.getTime() - totalTime * 1000);
|
||||
let ratio =
|
||||
$gpxStatistics.global.time.moving > 0
|
||||
? $gpxStatistics.global.time.total / $gpxStatistics.global.time.moving
|
||||
: 1;
|
||||
let start = new Date(end.getTime() - ratio * movingTime * 1000);
|
||||
startDate = toCalendarDate(start);
|
||||
startTime = start.toLocaleTimeString();
|
||||
}
|
||||
}
|
||||
|
||||
function updateDataFromSpeed() {
|
||||
function getSpeed() {
|
||||
if (speed === undefined) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let speedValue = speed;
|
||||
@@ -117,17 +130,29 @@
|
||||
if ($distanceUnits === 'imperial') {
|
||||
speedValue = kilometersToMiles(speedValue);
|
||||
}
|
||||
return speedValue;
|
||||
}
|
||||
|
||||
totalTime = ($gpxStatistics.global.distance.total / speedValue) * 3600;
|
||||
function updateDataFromSpeed() {
|
||||
let speedValue = getSpeed();
|
||||
if (speedValue === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let distance =
|
||||
$gpxStatistics.global.distance.moving > 0
|
||||
? $gpxStatistics.global.distance.moving
|
||||
: $gpxStatistics.global.distance.total;
|
||||
movingTime = (distance / speedValue) * 3600;
|
||||
|
||||
updateEnd();
|
||||
}
|
||||
|
||||
function updateDataFromTotalTime() {
|
||||
if (totalTime === undefined) {
|
||||
if (movingTime === undefined) {
|
||||
return;
|
||||
}
|
||||
setSpeed($gpxStatistics.global.distance.total / (totalTime / 3600));
|
||||
setSpeed($gpxStatistics.global.distance.moving / (movingTime / 3600));
|
||||
updateEnd();
|
||||
}
|
||||
|
||||
@@ -153,7 +178,7 @@
|
||||
id="speed"
|
||||
type="number"
|
||||
step={0.01}
|
||||
min={0}
|
||||
min={0.01}
|
||||
disabled={!canUpdate}
|
||||
bind:value={speed}
|
||||
on:change={updateDataFromSpeed}
|
||||
@@ -188,7 +213,7 @@
|
||||
{$_('toolbar.time.total_time')}
|
||||
</Label>
|
||||
<TimePicker
|
||||
bind:value={totalTime}
|
||||
bind:value={movingTime}
|
||||
disabled={!canUpdate}
|
||||
on:change={updateDataFromTotalTime}
|
||||
/>
|
||||
@@ -244,7 +269,7 @@
|
||||
on:change={updateStart}
|
||||
/>
|
||||
</div>
|
||||
{#if $gpxStatistics.global.time.total === 0 || $gpxStatistics.global.time.total === undefined}
|
||||
{#if $gpxStatistics.global.time.moving === 0 || $gpxStatistics.global.time.moving === undefined}
|
||||
<Label class="mt-0.5 flex flex-row gap-1 items-center">
|
||||
<Checkbox disabled={!canUpdate} />
|
||||
{$_('toolbar.time.artificial')}
|
||||
@@ -252,8 +277,53 @@
|
||||
{/if}
|
||||
</fieldset>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button variant="outline" disabled={!canUpdate} class="grow" on:click={() => {}}>
|
||||
<RefreshCw size="16" class="mr-1" />
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={!canUpdate}
|
||||
class="grow"
|
||||
on:click={() => {
|
||||
let effectiveSpeed = getSpeed();
|
||||
if (startDate === undefined || startTime === undefined || effectiveSpeed === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs(effectiveSpeed - $gpxStatistics.global.speed.moving) < 0.01) {
|
||||
effectiveSpeed = $gpxStatistics.global.speed.moving;
|
||||
}
|
||||
|
||||
let ratio = 1;
|
||||
if (
|
||||
$gpxStatistics.global.speed.moving > 0 &&
|
||||
$gpxStatistics.global.speed.moving !== effectiveSpeed
|
||||
) {
|
||||
ratio = $gpxStatistics.global.speed.moving / effectiveSpeed;
|
||||
}
|
||||
|
||||
let item = $selection.getSelected()[0];
|
||||
let fileId = item.getFileId();
|
||||
dbUtils.applyToFile(fileId, (file) => {
|
||||
if (item instanceof ListFileItem) {
|
||||
return file.changeTimestamps(getDate(startDate, startTime), effectiveSpeed, ratio);
|
||||
} else if (item instanceof ListTrackItem) {
|
||||
return file.changeTimestamps(
|
||||
getDate(startDate, startTime),
|
||||
effectiveSpeed,
|
||||
ratio,
|
||||
item.getTrackIndex()
|
||||
);
|
||||
} else if (item instanceof ListTrackSegmentItem) {
|
||||
return file.changeTimestamps(
|
||||
getDate(startDate, startTime),
|
||||
effectiveSpeed,
|
||||
ratio,
|
||||
item.getTrackIndex(),
|
||||
item.getSegmentIndex()
|
||||
);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<CalendarClock size="16" class="mr-1" />
|
||||
{$_('toolbar.time.update')}
|
||||
</Button>
|
||||
<Button variant="outline" on:click={setGPXData}>
|
||||
|
@@ -361,14 +361,16 @@ export class RoutingControls {
|
||||
startLoopAtAnchor(anchor: Anchor) {
|
||||
this.popup.remove();
|
||||
|
||||
let file = get(this.file)?.file;
|
||||
if (!file) {
|
||||
let fileWithStats = get(this.file);
|
||||
if (!fileWithStats) {
|
||||
return;
|
||||
}
|
||||
|
||||
let speed = fileWithStats.statistics.getStatisticsFor(new ListTrackSegmentItem(this.fileId, anchor.trackIndex, anchor.segmentIndex)).global.speed.moving;
|
||||
|
||||
let segment = anchor.segment;
|
||||
dbUtils.applyToFile(this.fileId, (file) => {
|
||||
let newFile = file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, segment.trkpt.length, segment.trkpt.length - 1, segment.trkpt.slice(0, anchor.point._data.index));
|
||||
let newFile = file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, segment.trkpt.length, segment.trkpt.length - 1, segment.trkpt.slice(0, anchor.point._data.index), speed > 0 ? speed : undefined);
|
||||
return newFile.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, anchor.point._data.index - 1, []);
|
||||
});
|
||||
}
|
||||
@@ -480,6 +482,11 @@ export class RoutingControls {
|
||||
async routeBetweenAnchors(anchors: Anchor[], targetCoordinates: Coordinates[]): Promise<boolean> {
|
||||
let segment = anchors[0].segment;
|
||||
|
||||
let fileWithStats = get(this.file);
|
||||
if (!fileWithStats) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (anchors.length === 1) { // Only one anchor, update the point in the segment
|
||||
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(anchors[0].trackIndex, anchors[0].segmentIndex, 0, 0, [new TrackPoint({
|
||||
attributes: targetCoordinates[0],
|
||||
@@ -541,7 +548,27 @@ export class RoutingControls {
|
||||
anchor.point._data.zoom = 0; // Make these anchors permanent
|
||||
});
|
||||
|
||||
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(anchors[0].trackIndex, anchors[0].segmentIndex, anchors[0].point._data.index, anchors[anchors.length - 1].point._data.index, response));
|
||||
let stats = fileWithStats.statistics.getStatisticsFor(new ListTrackSegmentItem(this.fileId, anchors[0].trackIndex, anchors[0].segmentIndex));
|
||||
let speed: number | undefined = undefined;
|
||||
let startTime = anchors[0].point.time;
|
||||
|
||||
if (stats.global.speed.moving > 0) {
|
||||
let replacingDistance = 0;
|
||||
for (let i = 1; i < response.length; i++) {
|
||||
replacingDistance += distance(response[i - 1].getCoordinates(), response[i].getCoordinates()) / 1000;
|
||||
}
|
||||
let replacedDistance = stats.local.distance.moving[anchors[anchors.length - 1].point._data.index] - stats.local.distance.moving[anchors[0].point._data.index];
|
||||
|
||||
let newDistance = stats.global.distance.moving + replacingDistance - replacedDistance;
|
||||
let newTime = newDistance / stats.global.speed.moving * 3600;
|
||||
|
||||
let remainingTime = stats.global.time.moving - (stats.local.time.moving[anchors[anchors.length - 1].point._data.index] - stats.local.time.moving[anchors[0].point._data.index]);
|
||||
let replacingTime = newTime - remainingTime;
|
||||
|
||||
speed = replacingDistance / replacingTime * 3600;
|
||||
}
|
||||
|
||||
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(anchors[0].trackIndex, anchors[0].segmentIndex, anchors[0].point._data.index, anchors[anchors.length - 1].point._data.index, response, speed, startTime));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@@ -17,7 +17,10 @@
|
||||
}
|
||||
|
||||
function computeValue() {
|
||||
return maybeParseInt(hours) * 3600 + maybeParseInt(minutes) * 60 + maybeParseInt(seconds);
|
||||
return Math.max(
|
||||
maybeParseInt(hours) * 3600 + maybeParseInt(minutes) * 60 + maybeParseInt(seconds),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
function updateValue() {
|
||||
@@ -31,7 +34,7 @@
|
||||
minutes = '--';
|
||||
seconds = '--';
|
||||
} else if (value !== computeValue()) {
|
||||
let rounded = Math.round(value);
|
||||
let rounded = Math.max(Math.round(value), 1);
|
||||
if (showHours) {
|
||||
hours = Math.floor(rounded / 3600);
|
||||
minutes = Math.floor((rounded % 3600) / 60)
|
||||
|
@@ -2,7 +2,7 @@ import Dexie, { liveQuery } from 'dexie';
|
||||
import { GPXFile, GPXStatistics, Track, TrackSegment, Waypoint, TrackPoint, type Coordinates, distance } from 'gpx';
|
||||
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 { initTargetMapBounds, splitAs, updateTargetMapBounds } from './stores';
|
||||
import { gpxStatistics, initTargetMapBounds, splitAs, updateTargetMapBounds } from './stores';
|
||||
import { mode } from 'mode-watcher';
|
||||
import { defaultBasemap, defaultBasemapTree, defaultOverlayTree, defaultOverlays } from './assets/layers';
|
||||
import { applyToOrderedItemsFromFile, applyToOrderedSelectedItemsFromFile, selection } from '$lib/components/file-list/Selection';
|
||||
@@ -546,11 +546,25 @@ export const dbUtils = {
|
||||
});
|
||||
|
||||
if (mergeTraces) {
|
||||
let statistics = get(gpxStatistics);
|
||||
let speed = statistics.global.speed.moving > 0 ? statistics.global.speed.moving : undefined;
|
||||
let startTime: Date | undefined = undefined;
|
||||
if (speed !== undefined) {
|
||||
if (statistics.local.points.length > 0 && statistics.local.points[0].time !== undefined) {
|
||||
startTime = statistics.local.points[0].time;
|
||||
} else {
|
||||
let index = statistics.local.points.findIndex((point) => point.time !== undefined);
|
||||
if (index !== -1) {
|
||||
startTime = new Date(statistics.local.points[index].time.getTime() - 1000 * 3600 * statistics.local.distance.total[index] / speed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toMerge.trk.length > 0) {
|
||||
let s = new TrackSegment();
|
||||
toMerge.trk.map((track) => {
|
||||
track.trkseg.forEach((segment) => {
|
||||
s = s.replaceTrackPoints(s.trkpt.length, s.trkpt.length, segment.trkpt.slice());
|
||||
s = s.replaceTrackPoints(s.trkpt.length, s.trkpt.length, segment.trkpt.slice(), speed, startTime);
|
||||
});
|
||||
});
|
||||
toMerge.trk = [toMerge.trk[0].replaceTrackSegments(0, toMerge.trk[0].trkseg.length - 1, [s])[0]];
|
||||
@@ -558,7 +572,7 @@ export const dbUtils = {
|
||||
if (toMerge.trkseg.length > 0) {
|
||||
let s = new TrackSegment();
|
||||
toMerge.trkseg.forEach((segment) => {
|
||||
s = s.replaceTrackPoints(s.trkpt.length, s.trkpt.length, segment.trkpt.slice());
|
||||
s = s.replaceTrackPoints(s.trkpt.length, s.trkpt.length, segment.trkpt.slice(), speed, startTime);
|
||||
});
|
||||
toMerge.trkseg = [s];
|
||||
}
|
||||
|
@@ -60,6 +60,10 @@ selection.subscribe(($selection) => { // Maintain up-to-date statistics for the
|
||||
});
|
||||
});
|
||||
|
||||
gpxStatistics.subscribe(() => {
|
||||
slicedGPXStatistics.set(undefined);
|
||||
});
|
||||
|
||||
const targetMapBounds = writable({
|
||||
bounds: new mapboxgl.LngLatBounds([180, 90, -180, -90]),
|
||||
initial: true
|
||||
|
@@ -30,7 +30,7 @@ export function distancePerHourToSecondsPerDistance(value: number) {
|
||||
export function secondsToHHMMSS(value: number) {
|
||||
var hours = Math.floor(value / 3600);
|
||||
var minutes = Math.floor(value / 60) % 60;
|
||||
var seconds = Math.round(value % 60);
|
||||
var seconds = Math.min(59, Math.round(value % 60));
|
||||
|
||||
return [hours, minutes, seconds]
|
||||
.map((v) => (v < 10 ? '0' + v : v))
|
||||
|
@@ -124,11 +124,11 @@
|
||||
"tooltip": "Manage time data",
|
||||
"start": "Start",
|
||||
"end": "End",
|
||||
"total_time": "Total time",
|
||||
"total_time": "Moving time",
|
||||
"pick_date": "Pick a date",
|
||||
"artificial": "Create realistic time data",
|
||||
"update": "Update time data",
|
||||
"help": "",
|
||||
"help": "Use the form to set new time data",
|
||||
"help_invalid_selection": "Select a single file item to manage its time data"
|
||||
},
|
||||
"merge": {
|
||||
|
Reference in New Issue
Block a user