time tool progress

This commit is contained in:
vcoppe
2024-06-13 17:36:43 +02:00
parent 9132a45798
commit b29500cfeb
27 changed files with 872 additions and 11 deletions

View File

@@ -529,10 +529,14 @@ export class TrackSegment extends GPXTreeLeaf {
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
// time
if (points[0].time !== undefined && points[i].time !== undefined) {
statistics.local.time.total.push((points[i].time.getTime() - points[0].time.getTime()) / 1000);
} else {
if (points[i].time === undefined) {
statistics.local.time.total.push(undefined);
} else {
if (statistics.global.time.start === undefined) {
statistics.global.time.start = points[i].time;
}
statistics.global.time.end = points[i].time;
statistics.local.time.total.push((points[i].time.getTime() - statistics.global.time.start.getTime()) / 1000);
}
// speed
@@ -557,7 +561,7 @@ export class TrackSegment extends GPXTreeLeaf {
statistics.global.bounds.northEast.lon = Math.max(statistics.global.bounds.northEast.lon, points[i].attributes.lon);
}
statistics.global.time.total = statistics.local.time.total[statistics.local.time.total.length - 1] ?? 0;
statistics.global.time.total = statistics.global.time.start && statistics.global.time.end ? (statistics.global.time.end.getTime() - statistics.global.time.start.getTime()) / 1000 : 0;
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.time.moving > 0 ? statistics.global.distance.moving / (statistics.global.time.moving / 3600) : 0;
@@ -846,6 +850,8 @@ export class GPXStatistics {
total: number,
},
time: {
start: Date | undefined,
end: Date | undefined,
moving: number,
total: number,
},
@@ -888,6 +894,8 @@ export class GPXStatistics {
total: 0,
},
time: {
start: undefined,
end: undefined,
moving: 0,
total: 0,
},
@@ -947,6 +955,9 @@ export class GPXStatistics {
this.global.distance.total += other.global.distance.total;
this.global.distance.moving += other.global.distance.moving;
this.global.time.start = this.global.time.start !== undefined && other.global.time.start !== undefined ? new Date(Math.min(this.global.time.start.getTime(), other.global.time.start.getTime())) : this.global.time.start ?? other.global.time.start;
this.global.time.end = this.global.time.end !== undefined && other.global.time.end !== undefined ? new Date(Math.max(this.global.time.end.getTime(), other.global.time.end.getTime())) : this.global.time.end ?? other.global.time.end;
this.global.time.total += other.global.time.total;
this.global.time.moving += other.global.time.moving;
@@ -970,6 +981,9 @@ export class GPXStatistics {
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.start = this.local.points[start].time;
statistics.global.time.end = this.local.points[end - 1].time;
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];

View File

@@ -8,6 +8,7 @@
"name": "website",
"version": "0.0.1",
"dependencies": {
"@internationalized/date": "^3.5.4",
"@mapbox/mapbox-gl-geocoder": "^5.0.2",
"bits-ui": "^0.21.10",
"chart.js": "^4.4.2",
@@ -777,9 +778,9 @@
"dev": true
},
"node_modules/@internationalized/date": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.2.tgz",
"integrity": "sha512-vo1yOMUt2hzp63IutEaTUxROdvQg1qlMRsbCvbay2AK2Gai7wIgCyK5weEX3nHkiLgo4qCXHijFNC/ILhlRpOQ==",
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.4.tgz",
"integrity": "sha512-qoVJVro+O0rBaw+8HPjUB1iH8Ihf8oziEnqMnvhJUSuVIrHOuZ6eNLHNvzXJKUvAtaDiqMnRlg8Z2mgh09BlUw==",
"dependencies": {
"@swc/helpers": "^0.5.0"
}

View File

@@ -41,6 +41,7 @@
},
"type": "module",
"dependencies": {
"@internationalized/date": "^3.5.4",
"@mapbox/mapbox-gl-geocoder": "^5.0.2",
"bits-ui": "^0.21.10",
"chart.js": "^4.4.2",

View File

@@ -35,7 +35,7 @@
</ToolbarItem>
<ToolbarItem tool={Tool.TIME}>
<CalendarClock slot="icon" size="18" />
<span slot="tooltip">{$_('toolbar.time_tooltip')}</span>
<span slot="tooltip">{$_('toolbar.time.tooltip')}</span>
</ToolbarItem>
<ToolbarItem tool={Tool.MERGE}>
<Group slot="icon" size="18" />

View File

@@ -5,6 +5,7 @@
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
import Scissors from '$lib/components/toolbar/tools/Scissors.svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte';
import Time from '$lib/components/toolbar/tools/Time.svelte';
import Merge from '$lib/components/toolbar/tools/Merge.svelte';
import Clean from '$lib/components/toolbar/tools/Clean.svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
@@ -39,6 +40,8 @@
<Scissors />
{:else if $currentTool === Tool.WAYPOINT}
<Waypoint />
{:else if $currentTool === Tool.TIME}
<Time />
{:else if $currentTool === Tool.MERGE}
<Merge />
{:else if $currentTool === Tool.CLEAN}

View File

@@ -0,0 +1,270 @@
<script lang="ts">
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 { settings } from '$lib/db';
import { gpxStatistics } from '$lib/stores';
import {
distancePerHourToSecondsPerDistance,
getConvertedVelocity,
kilometersToMiles
} from '$lib/units';
import { CalendarDate, type DateValue } from '@internationalized/date';
import { CirclePlay, CircleStop, CircleX, RefreshCw, 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 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 speed: number | undefined = undefined;
function toCalendarDate(date: Date): CalendarDate {
return new CalendarDate(date.getFullYear(), date.getMonth(), date.getDate());
}
const { velocityUnits, distanceUnits } = settings;
function setSpeed(value: number) {
let speedValue = getConvertedVelocity(value);
if ($velocityUnits === 'speed') {
speedValue = parseFloat(speedValue.toFixed(2));
}
speed = speedValue;
}
function setGPXData() {
if ($gpxStatistics.global.time.start) {
startDate = toCalendarDate($gpxStatistics.global.time.start);
startTime = $gpxStatistics.global.time.start.toLocaleTimeString();
} else {
startDate = undefined;
startTime = undefined;
}
if ($gpxStatistics.global.time.end) {
endDate = toCalendarDate($gpxStatistics.global.time.end);
endTime = $gpxStatistics.global.time.end.toLocaleTimeString();
} else {
endDate = undefined;
endTime = undefined;
}
if ($gpxStatistics.global.time.total) {
totalTime = $gpxStatistics.global.time.total;
} else {
totalTime = undefined;
}
if ($gpxStatistics.global.speed.total) {
setSpeed($gpxStatistics.global.speed.total);
} else {
speed = undefined;
}
}
$: if ($gpxStatistics && $velocityUnits && $distanceUnits) {
setGPXData();
}
function getDate(date: DateValue, time: string): Date {
if (date === undefined) {
return new Date();
}
let [hours, minutes, seconds] = time.split(':').map((x) => parseInt(x));
return new Date(date.year, date.month, date.day, hours, minutes, seconds);
}
function updateEnd() {
if (startDate && totalTime !== undefined) {
if (startTime === undefined) {
startTime = '00:00:00';
}
let start = getDate(startDate, startTime);
let end = new Date(start.getTime() + totalTime * 1000);
endDate = toCalendarDate(end);
endTime = end.toLocaleTimeString();
}
}
function updateStart() {
if (endDate && totalTime !== undefined) {
if (endTime === undefined) {
endTime = '00:00:00';
}
let end = getDate(endDate, endTime);
let start = new Date(end.getTime() - totalTime * 1000);
startDate = toCalendarDate(start);
startTime = start.toLocaleTimeString();
}
}
function updateDataFromSpeed() {
if (speed === undefined) {
return;
}
let speedValue = speed;
if ($velocityUnits === 'pace') {
speedValue = distancePerHourToSecondsPerDistance(speed);
}
if ($distanceUnits === 'imperial') {
speedValue = kilometersToMiles(speedValue);
}
totalTime = ($gpxStatistics.global.distance.total / speedValue) * 3600;
updateEnd();
}
function updateDataFromTotalTime() {
if (totalTime === undefined) {
return;
}
setSpeed($gpxStatistics.global.distance.total / (totalTime / 3600));
updateEnd();
}
$: canUpdate =
$selection.size === 1 && $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
</script>
<div class="flex flex-col gap-3 w-80">
<fieldset class="flex flex-col gap-2">
<div class="flex flex-row gap-2 justify-center">
<div class="flex flex-col gap-2">
<Label for="speed" class="flex flex-row">
<Zap size="16" class="mr-1" />
{#if $velocityUnits === 'speed'}
{$_('quantities.speed')}
{:else}
{$_('quantities.pace')}
{/if}
</Label>
<div class="flex flex-row gap-1 items-center">
{#if $velocityUnits === 'speed'}
<Input
id="speed"
type="number"
step={0.01}
min={0}
disabled={!canUpdate}
bind:value={speed}
on:change={updateDataFromSpeed}
/>
<span class="text-sm shrink-0">
{#if $distanceUnits === 'imperial'}
{$_('units.miles_per_hour')}
{:else}
{$_('units.kilometers_per_hour')}
{/if}
</span>
{:else}
<TimePicker
bind:value={speed}
showHours={false}
disabled={!canUpdate}
on:change={updateDataFromSpeed}
/>
<span class="text-sm shrink-0">
{#if $distanceUnits === 'imperial'}
{$_('units.minutes_per_mile')}
{:else}
{$_('units.minutes_per_kilometer')}
{/if}
</span>
{/if}
</div>
</div>
<div class="flex flex-col gap-2">
<Label for="duration" class="flex flex-row">
<Timer size="16" class="mr-1" />
{$_('toolbar.time.total_time')}
</Label>
<TimePicker
bind:value={totalTime}
disabled={!canUpdate}
on:change={updateDataFromTotalTime}
/>
</div>
</div>
<Label class="flex flex-row">
<CirclePlay size="16" class="mr-1" />
{$_('toolbar.time.start')}
</Label>
<div class="flex flex-row gap-2">
<DatePicker
bind:value={startDate}
disabled={!canUpdate}
locale={get(locale) ?? 'en'}
placeholder={$_('toolbar.time.pick_date')}
class="w-[211px]"
onValueChange={async () => {
await tick();
updateEnd();
}}
/>
<Input
type="time"
step={1}
disabled={!canUpdate}
bind:value={startTime}
class="w-[100px]"
on:input={updateEnd}
/>
</div>
<Label class="flex flex-row">
<CircleStop size="16" class="mr-1" />
{$_('toolbar.time.end')}
</Label>
<div class="flex flex-row gap-2">
<DatePicker
bind:value={endDate}
disabled={!canUpdate}
locale={get(locale) ?? 'en'}
placeholder={$_('toolbar.time.pick_date')}
class="w-[211px]"
onValueChange={async () => {
await tick();
updateStart();
}}
/>
<Input
type="time"
step={1}
disabled={!canUpdate}
bind:value={endTime}
class="w-[100px]"
on:change={updateStart}
/>
</div>
{#if $gpxStatistics.global.time.total === 0 || $gpxStatistics.global.time.total === undefined}
<Label class="mt-0.5 flex flex-row gap-1 items-center">
<Checkbox disabled={!canUpdate} />
{$_('toolbar.time.artificial')}
</Label>
{/if}
</fieldset>
<div class="flex flex-row gap-2">
<Button variant="outline" disabled={!canUpdate} class="grow" on:click={() => {}}>
<RefreshCw size="16" class="mr-1" />
{$_('toolbar.time.update')}
</Button>
<Button variant="outline" on:click={setGPXData}>
<CircleX size="16" />
</Button>
</div>
<Help>
{#if canUpdate}
{$_('toolbar.time.help')}
{:else}
{$_('toolbar.time.help_invalid_selection')}
{/if}
</Help>
</div>

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = CalendarPrimitive.CellProps;
export let date: $$Props["date"];
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<CalendarPrimitive.Cell
{date}
class={cn(
"relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-month])]:bg-accent/50",
className
)}
{...$$restProps}
>
<slot />
</CalendarPrimitive.Cell>

View File

@@ -0,0 +1,42 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
type $$Props = CalendarPrimitive.DayProps;
type $$Events = CalendarPrimitive.DayEvents;
export let date: $$Props["date"];
export let month: $$Props["month"];
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<CalendarPrimitive.Day
on:click
{date}
{month}
class={cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal ",
"[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground",
// Selected
"data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:opacity-100 data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground",
// Disabled
"data-[disabled]:text-muted-foreground data-[disabled]:opacity-50",
// Unavailable
"data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through",
// Outside months
"data-[outside-month]:pointer-events-none data-[outside-month]:text-muted-foreground data-[outside-month]:opacity-50 [&[data-outside-month][data-selected]]:bg-accent/50 [&[data-outside-month][data-selected]]:text-muted-foreground [&[data-outside-month][data-selected]]:opacity-30",
className
)}
{...$$restProps}
let:selected
let:disabled
let:unavailable
let:builder
>
<slot {selected} {disabled} {unavailable} {builder}>
{date.day}
</slot>
</CalendarPrimitive.Day>

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = CalendarPrimitive.GridBodyProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<CalendarPrimitive.GridBody class={cn(className)} {...$$restProps}>
<slot />
</CalendarPrimitive.GridBody>

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = CalendarPrimitive.GridHeadProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<CalendarPrimitive.GridHead class={cn(className)} {...$$restProps}>
<slot />
</CalendarPrimitive.GridHead>

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = CalendarPrimitive.GridRowProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<CalendarPrimitive.GridRow class={cn("flex", className)} {...$$restProps}>
<slot />
</CalendarPrimitive.GridRow>

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = CalendarPrimitive.GridProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<CalendarPrimitive.Grid class={cn("w-full border-collapse space-y-1", className)} {...$$restProps}>
<slot />
</CalendarPrimitive.Grid>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = CalendarPrimitive.HeadCellProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<CalendarPrimitive.HeadCell
class={cn("w-9 rounded-md text-[0.8rem] font-normal text-muted-foreground", className)}
{...$$restProps}
>
<slot />
</CalendarPrimitive.HeadCell>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = CalendarPrimitive.HeaderProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<CalendarPrimitive.Header
class={cn("relative flex w-full items-center justify-between pt-1", className)}
{...$$restProps}
>
<slot />
</CalendarPrimitive.Header>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = CalendarPrimitive.HeadingProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<CalendarPrimitive.Heading
let:headingValue
class={cn("text-sm font-medium", className)}
{...$$restProps}
>
<slot {headingValue}>
{headingValue}
</slot>
</CalendarPrimitive.Heading>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div
class={cn("mt-4 flex flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0", className)}
{...$$restProps}
>
<slot />
</div>

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
type $$Props = CalendarPrimitive.NextButtonProps;
type $$Events = CalendarPrimitive.NextButtonEvents;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<CalendarPrimitive.NextButton
on:click
class={cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
className
)}
{...$$restProps}
let:builder
>
<slot {builder}>
<ChevronRight class="h-4 w-4" />
</slot>
</CalendarPrimitive.NextButton>

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import ChevronLeft from "lucide-svelte/icons/chevron-left";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
type $$Props = CalendarPrimitive.PrevButtonProps;
type $$Events = CalendarPrimitive.PrevButtonEvents;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<CalendarPrimitive.PrevButton
on:click
class={cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
className
)}
{...$$restProps}
let:builder
>
<slot {builder}>
<ChevronLeft class="h-4 w-4" />
</slot>
</CalendarPrimitive.PrevButton>

View File

@@ -0,0 +1,59 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import * as Calendar from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = CalendarPrimitive.Props;
type $$Events = CalendarPrimitive.Events;
export let value: $$Props["value"] = undefined;
export let placeholder: $$Props["placeholder"] = undefined;
export let weekdayFormat: $$Props["weekdayFormat"] = "short";
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<CalendarPrimitive.Root
bind:value
bind:placeholder
{weekdayFormat}
class={cn("p-3", className)}
{...$$restProps}
on:keydown
let:months
let:weekdays
>
<Calendar.Header>
<Calendar.PrevButton />
<Calendar.Heading />
<Calendar.NextButton />
</Calendar.Header>
<Calendar.Months>
{#each months as month}
<Calendar.Grid>
<Calendar.GridHead>
<Calendar.GridRow class="flex">
{#each weekdays as weekday}
<Calendar.HeadCell>
{weekday.slice(0, 2)}
</Calendar.HeadCell>
{/each}
</Calendar.GridRow>
</Calendar.GridHead>
<Calendar.GridBody>
{#each month.weeks as weekDates}
<Calendar.GridRow class="mt-2 w-full">
{#each weekDates as date}
<Calendar.Cell {date}>
<Calendar.Day {date} month={month.value} />
</Calendar.Cell>
{/each}
</Calendar.GridRow>
{/each}
</Calendar.GridBody>
</Calendar.Grid>
{/each}
</Calendar.Months>
</CalendarPrimitive.Root>

View File

@@ -0,0 +1,30 @@
import Root from "./calendar.svelte";
import Cell from "./calendar-cell.svelte";
import Day from "./calendar-day.svelte";
import Grid from "./calendar-grid.svelte";
import Header from "./calendar-header.svelte";
import Months from "./calendar-months.svelte";
import GridRow from "./calendar-grid-row.svelte";
import Heading from "./calendar-heading.svelte";
import GridBody from "./calendar-grid-body.svelte";
import GridHead from "./calendar-grid-head.svelte";
import HeadCell from "./calendar-head-cell.svelte";
import NextButton from "./calendar-next-button.svelte";
import PrevButton from "./calendar-prev-button.svelte";
export {
Day,
Cell,
Grid,
Header,
Months,
GridRow,
Heading,
GridBody,
GridHead,
HeadCell,
NextButton,
PrevButton,
//
Root as Calendar,
};

View File

@@ -0,0 +1,39 @@
<script lang="ts">
import CalendarIcon from 'lucide-svelte/icons/calendar';
import { DateFormatter, type DateValue, getLocalTimeZone } from '@internationalized/date';
import { cn } from '$lib/utils.js';
import { Button } from '$lib/components/ui/button/index.js';
import { Calendar } from '$lib/components/ui/calendar/index.js';
import * as Popover from '$lib/components/ui/popover/index.js';
export let value: DateValue | undefined = undefined;
export let placeholder: string = 'Pick a date';
export let locale = 'en';
export let disabled: boolean = false;
export let onValueChange: any;
const df = new DateFormatter(locale, {
dateStyle: 'long'
});
</script>
<Popover.Root>
<Popover.Trigger asChild let:builder>
<Button
variant="outline"
class={cn(
'w-[280px] justify-start text-left font-normal',
!value && 'text-muted-foreground',
$$props.class
)}
{disabled}
builders={[builder]}
>
<CalendarIcon class="mr-2 h-4 w-4" />
{value ? df.format(value.toDate(getLocalTimeZone())) : placeholder}
</Button>
</Popover.Trigger>
<Popover.Content class="w-auto p-0">
<Calendar bind:value initialFocus {locale} {onValueChange} />
</Popover.Content>
</Popover.Root>

View File

@@ -0,0 +1,17 @@
import { Popover as PopoverPrimitive } from "bits-ui";
import Content from "./popover-content.svelte";
const Root = PopoverPrimitive.Root;
const Trigger = PopoverPrimitive.Trigger;
const Close = PopoverPrimitive.Close;
export {
Root,
Content,
Trigger,
Close,
//
Root as Popover,
Content as PopoverContent,
Trigger as PopoverTrigger,
Close as PopoverClose,
};

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = PopoverPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined;
export { className as class };
</script>
<PopoverPrimitive.Content
{transition}
{transitionConfig}
class={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none",
className
)}
{...$$restProps}
>
<slot />
</PopoverPrimitive.Content>

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
export let value: string | number;
</script>
<div>
<Input
type="text"
step={1}
bind:value
on:input
on:change
class="w-[22px] {$$props.class ?? ''}"
{...$$restProps}
/>
</div>
<style lang="postcss">
div :global(input) {
@apply px-0.5;
@apply text-right;
@apply border-none;
@apply focus-visible:ring-0;
@apply focus-visible:ring-offset-0;
}
</style>

View File

@@ -0,0 +1,125 @@
<script lang="ts">
import TimeComponentInput from './TimeComponentInput.svelte';
export let showHours = true;
export let value: number | undefined = undefined;
export let disabled: boolean = false;
let hours: string | number = '--';
let minutes: string | number = '--';
let seconds: string | number = '--';
function maybeParseInt(value: string | number): number {
if (value === '--') {
return 0;
}
return typeof value === 'string' ? parseInt(value) : value;
}
function computeValue() {
return maybeParseInt(hours) * 3600 + maybeParseInt(minutes) * 60 + maybeParseInt(seconds);
}
function updateValue() {
value = computeValue();
}
$: hours, minutes, seconds, updateValue();
$: if (value === undefined) {
hours = '--';
minutes = '--';
seconds = '--';
} else if (value !== computeValue()) {
let rounded = Math.round(value);
if (showHours) {
hours = Math.floor(rounded / 3600);
minutes = Math.floor((rounded % 3600) / 60)
.toString()
.padStart(2, '0');
} else {
minutes = Math.floor(rounded / 60).toString();
}
seconds = (rounded % 60).toString().padStart(2, '0');
}
</script>
<div
class="flex flex-row items-center w-fit border rounded-md px-3 {disabled
? 'opacity-50 cursor-not-allowed'
: ''}"
>
{#if showHours}
<TimeComponentInput
bind:value={hours}
{disabled}
class="w-[30px]"
on:input={() => {
if (typeof hours === 'string') {
hours = parseInt(hours);
}
if (hours >= 0) {
} else if (hours < 0) {
hours = 0;
} else {
hours = 0;
}
}}
on:change
/>
<span class="text-sm">:</span>
{/if}
<TimeComponentInput
bind:value={minutes}
{disabled}
on:input={() => {
if (typeof minutes === 'string') {
minutes = parseInt(minutes);
}
if (minutes >= 0 && (minutes <= 59 || !showHours)) {
} else if (minutes < 0) {
minutes = 0;
} else if (showHours && minutes > 59) {
minutes = 59;
} else {
minutes = 0;
}
minutes = minutes.toString().padStart(showHours ? 2 : 1, '0');
}}
on:change
/>
<span class="text-sm">:</span>
<TimeComponentInput
bind:value={seconds}
{disabled}
on:input={() => {
if (typeof seconds === 'string') {
seconds = parseInt(seconds);
}
if (seconds >= 0 && seconds <= 59) {
} else if (seconds < 0) {
seconds = 0;
} else if (seconds > 59) {
seconds = 59;
} else {
seconds = 0;
}
seconds = seconds.toString().padStart(2, '0');
}}
on:change
/>
</div>
<style>
div :global(input::-webkit-outer-spin-button) {
-webkit-appearance: none;
margin: 0;
}
div :global(input::-webkit-inner-spin-button) {
-webkit-appearance: none;
margin: 0;
}
div :global(input[type='number']) {
-moz-appearance: textfield;
}
</style>

View File

@@ -8,6 +8,10 @@ export function kilometersToMiles(value: number) {
return value * 0.621371;
}
export function milesToKilometers(value: number) {
return value * 1.60934;
}
export function metersToFeet(value: number) {
return value * 3.28084;
}
@@ -17,6 +21,9 @@ export function celsiusToFahrenheit(value: number) {
}
export function distancePerHourToSecondsPerDistance(value: number) {
if (value === 0) {
return 0;
}
return 3600 / value;
}

View File

@@ -118,7 +118,17 @@
"help_invalid_selection": "Select a file item 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 data",
"start": "Start",
"end": "End",
"total_time": "Total time",
"pick_date": "Pick a date",
"artificial": "Create realistic time data",
"update": "Update time data",
"help": "",
"help_invalid_selection": "Select a single file item to manage its time data"
},
"merge": {
"merge_traces": "Connect the traces",
"merge_contents": "Merge the contents and keep the traces disconnected",
@@ -137,8 +147,8 @@
"comment": "Comment",
"longitude": "Longitude",
"latitude": "Latitude",
"create": "Create",
"update": "Update",
"create": "Create point of interest",
"update": "Update point of interest",
"help": "Fill in the form to create a new point of interest, or click on an existing one to edit it. Click on the map to fill the coordinates, or drag points of interest to move them.",
"help_no_selection": "Select a file item to create or edit points of interest"
},