Files
gpx.studio/website/src/lib/components/toolbar/tools/Time.svelte
T

368 lines
13 KiB
Svelte
Raw Normal View History

2024-06-13 17:36:43 +02:00
<script lang="ts">
2025-02-02 11:17:22 +01:00
import DatePicker from '$lib/components/ui/date-picker/DatePicker.svelte';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label/index.js';
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 {
distancePerHourToSecondsPerDistance,
getConvertedVelocity,
milesToKilometers,
nauticalMilesToKilometers,
2026-10-11 11:18:36 +02:00
} from '$lib/units/units';
2025-02-02 11:17:22 +01:00
import { CalendarDate, type DateValue } from '@internationalized/date';
2025-06-21 21:07:36 +02:00
import { CalendarClock, CirclePlay, CircleStop, CircleX, Timer, Zap } from '@lucide/svelte';
2025-10-24 20:07:15 +02:00
import { untrack } from 'svelte';
2026-10-11 11:18:36 +02:00
import { i18n } from '$lib/i18n/i18n.svelte';
2025-02-02 11:17:22 +01:00
import Help from '$lib/components/Help.svelte';
import { getURLForLanguage } from '$lib/utils';
2026-10-11 11:18:36 +02:00
import { settings } from '$lib/state/settings';
2026-10-08 08:12:44 +02:00
import { engine } from '$lib/engine';
2025-10-05 19:34:05 +02:00
let props: {
class?: string;
} = $props();
2024-06-13 17:36:43 +02:00
2026-10-08 08:12:44 +02:00
const selection = engine.selection;
const statistics = engine.statistics;
let global = $derived($statistics.global);
2025-10-18 19:21:10 +02:00
let startDate: DateValue | undefined = $state(undefined);
let startTime: string | undefined = $state(undefined);
let endDate: DateValue | undefined = $state(undefined);
let endTime: string | undefined = $state(undefined);
let movingTime: number | undefined = $state(undefined);
let speed: number | undefined = $state(undefined);
2025-11-16 12:13:55 +01:00
let artificial = $state(true);
2024-06-13 17:36:43 +02:00
2025-02-02 11:17:22 +01:00
function toCalendarDate(date: Date): CalendarDate {
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
}
2024-06-13 17:36:43 +02:00
2025-02-02 11:17:22 +01:00
function toTimeString(date: Date): string {
return date.toTimeString().split(' ')[0];
}
2025-02-02 11:17:22 +01:00
const { velocityUnits, distanceUnits } = settings;
2024-06-13 17:36:43 +02:00
2025-02-02 11:17:22 +01:00
function setSpeed(value: number) {
let speedValue = getConvertedVelocity(value);
2025-10-18 19:21:10 +02:00
if ($velocityUnits === 'speed') {
2025-02-02 11:17:22 +01:00
speedValue = parseFloat(speedValue.toFixed(2));
}
speed = speedValue;
}
2024-06-13 17:36:43 +02:00
2025-02-02 11:17:22 +01:00
function setGPXData() {
2026-10-08 08:12:44 +02:00
if (global.startTime !== undefined) {
const start = new Date(global.startTime);
startDate = toCalendarDate(start);
startTime = toTimeString(start);
2025-02-02 11:17:22 +01:00
} else {
startDate = undefined;
startTime = undefined;
}
2026-10-08 08:12:44 +02:00
if (global.endTime !== undefined) {
const end = new Date(global.endTime);
endDate = toCalendarDate(end);
endTime = toTimeString(end);
2025-02-02 11:17:22 +01:00
} else {
endDate = undefined;
endTime = undefined;
}
2026-10-08 08:12:44 +02:00
if (global.movingTime && global.movingSpeed) {
movingTime = global.movingTime;
setSpeed(global.movingSpeed);
} else if (global.totalTime && global.totalSpeed) {
movingTime = global.totalTime;
setSpeed(global.totalSpeed);
2025-02-02 11:17:22 +01:00
} else {
movingTime = undefined;
speed = undefined;
}
}
2024-06-13 17:36:43 +02:00
2025-10-18 19:21:10 +02:00
$effect(() => {
2026-10-08 08:12:44 +02:00
if ($statistics && $velocityUnits && $distanceUnits) {
2025-10-24 20:07:15 +02:00
untrack(() => setGPXData());
2025-10-18 19:21:10 +02:00
}
});
2024-06-13 17:36:43 +02:00
2025-02-02 11:17:22 +01:00
function getDate(date: DateValue, time: string): Date {
if (date === undefined) {
return new Date();
}
let [hours, minutes, seconds] = time.split(':').map((x) => parseInt(x));
if (seconds === undefined) {
seconds = 0;
}
return new Date(date.year, date.month - 1, date.day, hours, minutes, seconds);
}
2024-06-13 17:36:43 +02:00
2026-10-08 08:12:44 +02:00
/** Total time over moving time. */
function timeRatio() {
return global.movingTime && global.totalTime ? global.totalTime / global.movingTime : 1;
}
/** The distance to cover at the speed: the moving one if there is one. */
function distance() {
return global.movingDistance ? global.movingDistance : global.totalDistance;
}
2025-02-02 11:17:22 +01:00
function updateEnd() {
if (startDate && movingTime !== undefined) {
if (startTime === undefined) {
startTime = '00:00:00';
}
let start = getDate(startDate, startTime);
2026-10-08 08:12:44 +02:00
let ratio = timeRatio();
2025-02-02 11:17:22 +01:00
let end = new Date(start.getTime() + ratio * movingTime * 1000);
endDate = toCalendarDate(end);
endTime = toTimeString(end);
}
}
2024-06-13 17:36:43 +02:00
2025-02-02 11:17:22 +01:00
function updateStart() {
if (endDate && movingTime !== undefined) {
if (endTime === undefined) {
endTime = '00:00:00';
}
let end = getDate(endDate, endTime);
2026-10-08 08:12:44 +02:00
let ratio = timeRatio();
2025-02-02 11:17:22 +01:00
let start = new Date(end.getTime() - ratio * movingTime * 1000);
startDate = toCalendarDate(start);
startTime = toTimeString(start);
}
}
2024-06-13 17:36:43 +02:00
2025-02-02 11:17:22 +01:00
function getSpeed() {
if (speed === undefined) {
return undefined;
}
2024-06-13 17:36:43 +02:00
2025-02-02 11:17:22 +01:00
let speedValue = speed;
2025-10-18 19:21:10 +02:00
if ($velocityUnits === 'pace') {
2025-02-02 11:17:22 +01:00
speedValue = distancePerHourToSecondsPerDistance(speed);
}
2025-10-18 19:21:10 +02:00
if ($distanceUnits === 'imperial') {
2025-02-02 11:17:22 +01:00
speedValue = milesToKilometers(speedValue);
2025-10-18 19:21:10 +02:00
} else if ($distanceUnits === 'nautical') {
2025-02-02 11:17:22 +01:00
speedValue = nauticalMilesToKilometers(speedValue);
}
return speedValue;
}
2024-06-13 17:36:43 +02:00
2025-02-02 11:17:22 +01:00
function updateDataFromSpeed() {
let speedValue = getSpeed();
if (speedValue === undefined) {
return;
}
2024-06-18 12:35:24 +02:00
2026-10-08 08:12:44 +02:00
movingTime = (distance() / speedValue) * 3600;
2024-06-13 17:36:43 +02:00
2025-02-02 11:17:22 +01:00
updateEnd();
}
2024-06-13 17:36:43 +02:00
2025-02-02 11:17:22 +01:00
function updateDataFromTotalTime() {
if (movingTime === undefined) {
return;
}
2026-10-08 08:12:44 +02:00
setSpeed(distance() / (movingTime / 3600));
2025-02-02 11:17:22 +01:00
updateEnd();
}
2024-06-13 17:36:43 +02:00
2026-10-26 18:51:56 +01:00
// a single file, route, track or segment
2025-10-05 19:34:05 +02:00
let canUpdate = $derived(
2026-10-08 08:12:44 +02:00
($selection.type === 'file' && $selection.fileIds.length === 1) ||
2026-10-26 18:51:56 +01:00
($selection.type === 'route' && $selection.routeIds.length === 1) ||
2026-10-08 08:12:44 +02:00
($selection.type === 'track' && $selection.trackIds.length === 1) ||
($selection.type === 'segment' && $selection.segmentIds.length === 1)
2025-10-05 19:34:05 +02:00
);
2024-06-13 17:36:43 +02:00
</script>
2025-10-05 19:34:05 +02:00
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
2025-02-02 11:17:22 +01:00
<fieldset class="flex flex-col gap-2">
2026-04-06 18:22:01 +02:00
<div class="flex flex-row gap-1.5 justify-center">
<div class="flex flex-col gap-1 grow">
2025-02-02 11:17:22 +01:00
<Label for="speed" class="flex flex-row">
2025-11-10 11:51:16 +01:00
<Zap size="16" />
2025-10-18 19:21:10 +02:00
{#if $velocityUnits === 'speed'}
2025-06-21 21:07:36 +02:00
{i18n._('quantities.speed')}
2025-02-02 11:17:22 +01:00
{:else}
2025-06-21 21:07:36 +02:00
{i18n._('quantities.pace')}
2025-02-02 11:17:22 +01:00
{/if}
</Label>
<div class="flex flex-row gap-1 items-center">
2025-10-18 19:21:10 +02:00
{#if $velocityUnits === 'speed'}
2025-02-02 11:17:22 +01:00
<Input
id="speed"
type="number"
step={0.01}
min={0.01}
disabled={!canUpdate}
bind:value={speed}
2025-10-24 20:07:15 +02:00
onchange={() => {
untrack(() => updateDataFromSpeed());
}}
2025-10-18 19:21:10 +02:00
class="text-sm"
2025-02-02 11:17:22 +01:00
/>
<span class="text-sm shrink-0">
2025-10-18 19:21:10 +02:00
{#if $distanceUnits === 'imperial'}
2025-06-21 21:07:36 +02:00
{i18n._('units.miles_per_hour')}
2025-10-18 19:21:10 +02:00
{:else if $distanceUnits === 'metric'}
2025-06-21 21:07:36 +02:00
{i18n._('units.kilometers_per_hour')}
2025-10-18 19:21:10 +02:00
{:else if $distanceUnits === 'nautical'}
2025-06-21 21:07:36 +02:00
{i18n._('units.knots')}
2025-02-02 11:17:22 +01:00
{/if}
</span>
{:else}
<TimePicker
bind:value={speed}
showHours={false}
disabled={!canUpdate}
2025-10-24 20:07:15 +02:00
onChange={() => {
untrack(() => updateDataFromSpeed());
}}
2025-02-02 11:17:22 +01:00
/>
<span class="text-sm shrink-0">
2025-10-18 19:21:10 +02:00
{#if $distanceUnits === 'imperial'}
2025-06-21 21:07:36 +02:00
{i18n._('units.minutes_per_mile')}
2025-10-18 19:21:10 +02:00
{:else if $distanceUnits === 'metric'}
2025-06-21 21:07:36 +02:00
{i18n._('units.minutes_per_kilometer')}
2025-10-18 19:21:10 +02:00
{:else if $distanceUnits === 'nautical'}
2025-06-21 21:07:36 +02:00
{i18n._('units.minutes_per_nautical_mile')}
2025-02-02 11:17:22 +01:00
{/if}
</span>
{/if}
</div>
</div>
2026-04-06 18:22:01 +02:00
<div class="flex flex-col gap-1 grow">
2025-02-02 11:17:22 +01:00
<Label for="duration" class="flex flex-row">
2025-11-10 11:51:16 +01:00
<Timer size="16" />
2025-06-21 21:07:36 +02:00
{i18n._('toolbar.time.total_time')}
2025-02-02 11:17:22 +01:00
</Label>
<TimePicker
bind:value={movingTime}
disabled={!canUpdate}
2025-10-24 20:07:15 +02:00
onChange={() => {
untrack(() => updateDataFromTotalTime());
}}
2025-02-02 11:17:22 +01:00
/>
</div>
</div>
2026-04-06 18:22:01 +02:00
<div class="flex flex-col gap-1">
<Label class="flex flex-row">
<CirclePlay size="16" />
{i18n._('toolbar.time.start')}
</Label>
<div class="flex flex-row gap-1.5">
<DatePicker
bind:value={startDate}
disabled={!canUpdate}
locale={i18n.lang}
placeholder={i18n._('toolbar.time.pick_date')}
class="w-fit grow"
onchange={() => {
untrack(() => updateEnd());
}}
/>
<Input
type="time"
step={1}
disabled={!canUpdate}
bind:value={startTime}
class="w-fit"
onchange={() => {
untrack(() => updateEnd());
}}
/>
</div>
2025-02-02 11:17:22 +01:00
</div>
2026-04-06 18:22:01 +02:00
<div class="flex flex-col gap-1">
<Label class="flex flex-row">
<CircleStop size="16" />
{i18n._('toolbar.time.end')}
</Label>
<div class="flex flex-row gap-1.5">
<DatePicker
bind:value={endDate}
disabled={!canUpdate}
locale={i18n.lang}
placeholder={i18n._('toolbar.time.pick_date')}
class="w-fit grow"
onchange={() => {
untrack(() => updateStart());
}}
/>
<Input
type="time"
step={1}
disabled={!canUpdate}
bind:value={endTime}
class="w-fit"
onchange={() => {
untrack(() => updateStart());
}}
/>
</div>
2025-02-02 11:17:22 +01:00
</div>
2026-10-08 08:12:44 +02:00
{#if !global.movingTime}
2025-02-02 11:17:22 +01:00
<div class="mt-0.5 flex flex-row gap-1 items-center">
<Checkbox id="artificial-time" bind:checked={artificial} disabled={!canUpdate} />
<Label for="artificial-time">
2025-06-21 21:07:36 +02:00
{i18n._('toolbar.time.artificial')}
2025-02-02 11:17:22 +01:00
</Label>
</div>
{/if}
</fieldset>
2026-04-06 18:22:01 +02:00
<div class="flex flex-row gap-1.5 items-center">
2025-02-02 11:17:22 +01:00
<Button
variant="outline"
disabled={!canUpdate}
2026-04-06 18:22:01 +02:00
class="grow shrink whitespace-normal h-fit min-h-8 py-1"
2025-06-21 21:07:36 +02:00
onclick={() => {
2026-10-08 08:12:44 +02:00
const speedValue = getSpeed();
2025-02-02 11:17:22 +01:00
if (
startDate === undefined ||
startTime === undefined ||
2026-10-08 08:12:44 +02:00
speedValue === undefined ||
2025-11-10 13:11:44 +01:00
movingTime === undefined
2025-02-02 11:17:22 +01:00
) {
return;
}
2024-06-18 12:35:24 +02:00
2026-10-08 08:12:44 +02:00
let effectiveSpeed: number = speedValue;
const movingSpeed = global.movingSpeed ?? 0;
if (Math.abs(effectiveSpeed - movingSpeed) < 0.01) {
effectiveSpeed = movingSpeed;
2025-02-02 11:17:22 +01:00
}
2024-06-18 12:35:24 +02:00
2025-02-02 11:17:22 +01:00
let ratio = 1;
2026-10-08 08:12:44 +02:00
if (movingSpeed > 0 && movingSpeed !== effectiveSpeed) {
ratio = movingSpeed / effectiveSpeed;
2025-02-02 11:17:22 +01:00
}
2024-06-18 12:35:24 +02:00
2026-10-08 08:12:44 +02:00
const start = getDate(startDate, startTime);
if (artificial && !global.movingTime) {
engine.createArtificialTimestamps(start, movingTime);
} else {
engine.changeTimestamps(start, effectiveSpeed, ratio);
}
2025-02-02 11:17:22 +01:00
}}
>
2025-11-10 11:51:16 +01:00
<CalendarClock size="16" class="shrink-0" />
2025-06-21 21:07:36 +02:00
{i18n._('toolbar.time.update')}
2025-02-02 11:17:22 +01:00
</Button>
2025-11-10 11:51:16 +01:00
<Button variant="outline" size="icon" onclick={setGPXData}>
2025-02-02 11:17:22 +01:00
<CircleX size="16" />
</Button>
</div>
2025-06-21 21:07:36 +02:00
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/time')}>
2025-02-02 11:17:22 +01:00
{#if canUpdate}
2025-06-21 21:07:36 +02:00
{i18n._('toolbar.time.help')}
2025-02-02 11:17:22 +01:00
{:else}
2025-06-21 21:07:36 +02:00
{i18n._('toolbar.time.help_invalid_selection')}
2025-02-02 11:17:22 +01:00
{/if}
</Help>
2024-06-13 17:36:43 +02:00
</div>