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