mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-08-30 15:20:01 +00:00
preserve timestamps option
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||
import Help from '$lib/components/Help.svelte';
|
||||
import ButtonWithTooltip from '$lib/components/ButtonWithTooltip.svelte';
|
||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||
@@ -19,16 +20,22 @@
|
||||
RouteOff,
|
||||
Repeat,
|
||||
SquareArrowUpLeft,
|
||||
SquareArrowOutDownRight
|
||||
SquareArrowOutDownRight,
|
||||
Timer
|
||||
} from 'lucide-svelte';
|
||||
|
||||
import { map, newGPXFile, routingControls, selectFileWhenLoaded } from '$lib/stores';
|
||||
import {
|
||||
gpxStatistics,
|
||||
map,
|
||||
newGPXFile,
|
||||
routingControls,
|
||||
selectFileWhenLoaded
|
||||
} from '$lib/stores';
|
||||
import { dbUtils, getFile, getFileIds, settings } from '$lib/db';
|
||||
import { brouterProfiles, routingProfileSelectItem } from './Routing';
|
||||
|
||||
import { _, locale } from 'svelte-i18n';
|
||||
import { RoutingControls } from './RoutingControls';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { fileObservers } from '$lib/db';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { getOrderedSelection, selection } from '$lib/components/file-list/Selection';
|
||||
@@ -40,6 +47,7 @@
|
||||
type ListItem
|
||||
} from '$lib/components/file-list/FileList';
|
||||
import { flyAndScale, getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||
import { TimestampsMode } from '$lib/types';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { TrackPoint } from 'gpx';
|
||||
|
||||
@@ -49,7 +57,7 @@
|
||||
export let popupElement: HTMLElement | undefined = undefined;
|
||||
let selectedItem: ListItem | null = null;
|
||||
|
||||
const { privateRoads, routing } = settings;
|
||||
const { privateRoads, routing, timestampsMode } = settings;
|
||||
|
||||
$: if ($map && popup && popupElement) {
|
||||
// remove controls for deleted files
|
||||
@@ -161,7 +169,7 @@
|
||||
</Select.Root>
|
||||
</Label>
|
||||
<Label class="flex flex-row justify-between items-center gap-2">
|
||||
<span class="flex flex-row gap-1">
|
||||
<span class="flex flex-row items-center gap-1">
|
||||
<TriangleAlert size="16" />
|
||||
{$_('toolbar.routing.allow_private')}
|
||||
</span>
|
||||
@@ -169,6 +177,28 @@
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $gpxStatistics.global.time.total > 0}
|
||||
<RadioGroup.Root bind:value={$timestampsMode}>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<RadioGroup.Item
|
||||
value={TimestampsMode.PRESERVE_AVERAGE_SPEED}
|
||||
id={TimestampsMode.PRESERVE_AVERAGE_SPEED}
|
||||
/>
|
||||
<Label for={TimestampsMode.PRESERVE_AVERAGE_SPEED}>
|
||||
{$_('toolbar.routing.preserve_average_speed')}
|
||||
</Label>
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<RadioGroup.Item
|
||||
value={TimestampsMode.PRESERVE_TIMESTAMPS}
|
||||
id={TimestampsMode.PRESERVE_TIMESTAMPS}
|
||||
/>
|
||||
<Label for={TimestampsMode.PRESERVE_TIMESTAMPS}>
|
||||
{$_('toolbar.routing.preserve_timestamps')}
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap justify-center gap-1">
|
||||
<ButtonWithTooltip
|
||||
|
@@ -9,8 +9,10 @@ import { getOrderedSelection, selection } from "$lib/components/file-list/Select
|
||||
import { ListFileItem, ListTrackItem, ListTrackSegmentItem } from "$lib/components/file-list/FileList";
|
||||
import { currentTool, streetViewEnabled, Tool } from "$lib/stores";
|
||||
import { getClosestLinePoint, resetCursor, setGrabbingCursor } from "$lib/utils";
|
||||
import { TimestampsMode } from "$lib/types";
|
||||
|
||||
const { streetViewSource, timestampsMode } = settings;
|
||||
|
||||
const { streetViewSource } = settings;
|
||||
export const canChangeStart = writable(false);
|
||||
|
||||
function stopPropagation(e: any) {
|
||||
@@ -573,9 +575,11 @@ export class RoutingControls {
|
||||
}
|
||||
|
||||
if (anchors[0].point._data.index === 0) { // First anchor is the first point of the segment
|
||||
response[0].time = anchors[0].point.time;
|
||||
anchors[0].point = response[0]; // replace the first anchor
|
||||
anchors[0].point._data.index = 0;
|
||||
} else if (anchors[0].point._data.index === segment.trkpt.length - 1 && distance(anchors[0].point.getCoordinates(), response[0].getCoordinates()) < 1) { // First anchor is the last point of the segment, and the new point is close enough
|
||||
response[0].time = anchors[0].point.time;
|
||||
anchors[0].point = response[0]; // replace the first anchor
|
||||
anchors[0].point._data.index = segment.trkpt.length - 1;
|
||||
} else {
|
||||
@@ -584,6 +588,7 @@ export class RoutingControls {
|
||||
}
|
||||
|
||||
if (anchors[anchors.length - 1].point._data.index === segment.trkpt.length - 1) { // Last anchor is the last point of the segment
|
||||
response[response.length - 1].time = anchors[anchors.length - 1].point.time;
|
||||
anchors[anchors.length - 1].point = response[response.length - 1]; // replace the last anchor
|
||||
anchors[anchors.length - 1].point._data.index = segment.trkpt.length - 1;
|
||||
} else {
|
||||
@@ -607,20 +612,40 @@ export class RoutingControls {
|
||||
let startTime = anchors[0].point.time;
|
||||
|
||||
if (stats.global.speed.moving > 0) {
|
||||
let replacingTime = 0;
|
||||
|
||||
if (get(timestampsMode) === TimestampsMode.PRESERVE_TIMESTAMPS) {
|
||||
this.extendResponseToContiguousAdaptedTimePoints(segment, anchors, response);
|
||||
|
||||
response.forEach((point) => point.time = undefined);
|
||||
startTime = anchors[0].point.time;
|
||||
|
||||
replacingTime = stats.local.time.total[anchors[anchors.length - 1].point._data.index] - stats.local.time.total[anchors[0].point._data.index];
|
||||
|
||||
if (replacingTime > 0) {
|
||||
for (let i = 1; i < anchors.length - 1; i++) {
|
||||
anchors[i].point._data['adapted_time'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if (get(timestampsMode) === TimestampsMode.PRESERVE_AVERAGE_SPEED || replacingTime === 0) {
|
||||
let replacedDistance = stats.local.distance.moving[anchors[anchors.length - 1].point._data.index] - stats.local.distance.moving[anchors[0].point._data.index];
|
||||
|
||||
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;
|
||||
let newDistance = stats.global.distance.moving + replacingDistance - replacedDistance;
|
||||
let newTime = newDistance / stats.global.speed.moving * 3600;
|
||||
|
||||
if (replacingTime <= 0) { // Fallback to simple time difference
|
||||
replacingTime = stats.local.time.total[anchors[anchors.length - 1].point._data.index] - stats.local.time.total[anchors[0].point._data.index];
|
||||
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]);
|
||||
replacingTime = newTime - remainingTime;
|
||||
|
||||
if (replacingTime <= 0) { // Fallback to simple time difference
|
||||
replacingTime = stats.local.time.total[anchors[anchors.length - 1].point._data.index] - stats.local.time.total[anchors[0].point._data.index];
|
||||
}
|
||||
}
|
||||
|
||||
speed = replacingDistance / replacingTime * 3600;
|
||||
@@ -636,6 +661,41 @@ export class RoutingControls {
|
||||
return true;
|
||||
}
|
||||
|
||||
extendResponseToContiguousAdaptedTimePoints(segment: TrackSegment, anchors: Anchor[], response: TrackPoint[]) {
|
||||
while (anchors[0].point._data.adapted_time) {
|
||||
let previousAnchor = null;
|
||||
for (let i = 0; i < this.anchors.length; i++) {
|
||||
if (this.anchors[i].point._data.index < anchors[0].point._data.index) {
|
||||
previousAnchor = this.anchors[i];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (previousAnchor === null) {
|
||||
break;
|
||||
} else {
|
||||
response.splice(0, 0, ...segment.trkpt.slice(previousAnchor.point._data.index, anchors[0].point._data.index).map((point) => point.clone()));
|
||||
anchors.splice(0, 0, previousAnchor);
|
||||
}
|
||||
}
|
||||
while (anchors[anchors.length - 1].point._data.adapted_time) {
|
||||
let nextAnchor = null;
|
||||
for (let i = this.anchors.length - 1; i >= 0; i--) {
|
||||
if (this.anchors[i].point._data.index > anchors[anchors.length - 1].point._data.index) {
|
||||
nextAnchor = this.anchors[i];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (nextAnchor === null) {
|
||||
break;
|
||||
} else {
|
||||
response.push(...segment.trkpt.slice(anchors[anchors.length - 1].point._data.index + 1, nextAnchor.point._data.index + 1).map((point) => point.clone()));
|
||||
anchors.push(nextAnchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.remove();
|
||||
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
|
@@ -9,6 +9,7 @@ import { ListFileItem, ListItem, ListTrackItem, ListLevel, ListTrackSegmentItem,
|
||||
import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/Simplify';
|
||||
import { SplitType } from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
|
||||
import { getClosestLinePoint, getElevation } from '$lib/utils';
|
||||
import { TimestampsMode } from '$lib/types';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
enableMapSet();
|
||||
@@ -90,6 +91,7 @@ export const settings = {
|
||||
routing: dexieSettingStore('routing', true),
|
||||
routingProfile: dexieSettingStore('routingProfile', 'bike'),
|
||||
privateRoads: dexieSettingStore('privateRoads', false),
|
||||
timestampsMode: dexieSettingStore('timestampsMode', TimestampsMode.PRESERVE_AVERAGE_SPEED),
|
||||
currentBasemap: dexieSettingStore('currentBasemap', defaultBasemap),
|
||||
previousBasemap: dexieSettingStore('previousBasemap', defaultBasemap),
|
||||
selectedBasemapTree: dexieSettingStore('selectedBasemapTree', defaultBasemapTree),
|
||||
|
4
website/src/lib/types.ts
Normal file
4
website/src/lib/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum TimestampsMode {
|
||||
PRESERVE_TIMESTAMPS = 'preserve_timestamps',
|
||||
PRESERVE_AVERAGE_SPEED = 'preserve_average_speed',
|
||||
};
|
@@ -88,6 +88,8 @@
|
||||
"use_routing": "Routing",
|
||||
"use_routing_tooltip": "Connect anchor points via road network, or in a straight line if disabled",
|
||||
"allow_private": "Allow private roads",
|
||||
"preserve_average_speed": "Maintain average speed",
|
||||
"preserve_timestamps": "Adjust timestamps to fill gaps",
|
||||
"reverse": {
|
||||
"button": "Reverse",
|
||||
"tooltip": "Reverse the direction of the route"
|
||||
|
Reference in New Issue
Block a user