This commit is contained in:
vcoppe
2024-09-20 10:15:28 +02:00
parent 930b4b84ed
commit f94edf3e3a
10 changed files with 884 additions and 898 deletions

View File

@@ -11,9 +11,9 @@
import * as RadioGroup from '$lib/components/ui/radio-group'; import * as RadioGroup from '$lib/components/ui/radio-group';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { _ } from 'svelte-i18n'; import { _, locale } from 'svelte-i18n';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { resetCursor, setCrosshairCursor } from '$lib/utils'; import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
import { Trash2 } from 'lucide-svelte'; import { Trash2 } from 'lucide-svelte';
import { map } from '$lib/stores'; import { map } from '$lib/stores';
import { selection } from '$lib/components/file-list/Selection'; import { selection } from '$lib/components/file-list/Selection';
@@ -178,7 +178,7 @@
<Trash2 size="16" class="mr-1" /> <Trash2 size="16" class="mr-1" />
{$_('toolbar.clean.button')} {$_('toolbar.clean.button')}
</Button> </Button>
<Help link="./help/toolbar/clean"> <Help link={getURLForLanguage($locale, '/help/toolbar/clean')}>
{#if validSelection} {#if validSelection}
{$_('toolbar.clean.help')} {$_('toolbar.clean.help')}
{:else} {:else}

View File

@@ -1,34 +1,35 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { selection } from '$lib/components/file-list/Selection'; import { selection } from '$lib/components/file-list/Selection';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { MountainSnow } from 'lucide-svelte'; import { MountainSnow } from 'lucide-svelte';
import { dbUtils } from '$lib/db'; import { dbUtils } from '$lib/db';
import { map } from '$lib/stores'; import { map } from '$lib/stores';
import { _ } from 'svelte-i18n'; import { _, locale } from 'svelte-i18n';
import { getURLForLanguage } from '$lib/utils';
$: validSelection = $selection.size > 0; $: validSelection = $selection.size > 0;
</script> </script>
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
<Button <Button
variant="outline" variant="outline"
class="whitespace-normal h-fit" class="whitespace-normal h-fit"
disabled={!validSelection} disabled={!validSelection}
on:click={async () => { on:click={async () => {
if ($map) { if ($map) {
dbUtils.addElevationToSelection($map); dbUtils.addElevationToSelection($map);
} }
}} }}
> >
<MountainSnow size="16" class="mr-1 shrink-0" /> <MountainSnow size="16" class="mr-1 shrink-0" />
{$_('toolbar.elevation.button')} {$_('toolbar.elevation.button')}
</Button> </Button>
<Help link="./help/toolbar/elevation"> <Help link={getURLForLanguage($locale, '/help/toolbar/elevation')}>
{#if validSelection} {#if validSelection}
{$_('toolbar.elevation.help')} {$_('toolbar.elevation.help')}
{:else} {:else}
{$_('toolbar.elevation.help_no_selection')} {$_('toolbar.elevation.help_no_selection')}
{/if} {/if}
</Help> </Help>
</div> </div>

View File

@@ -11,7 +11,8 @@
} from '$lib/components/file-list/FileList'; } from '$lib/components/file-list/FileList';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { dbUtils, getFile } from '$lib/db'; import { dbUtils, getFile } from '$lib/db';
import { _ } from 'svelte-i18n'; import { _, locale } from 'svelte-i18n';
import { getURLForLanguage } from '$lib/utils';
$: validSelection = $: validSelection =
$selection.size > 0 && $selection.size > 0 &&
@@ -42,7 +43,7 @@
<Ungroup size="16" class="mr-1" /> <Ungroup size="16" class="mr-1" />
{$_('toolbar.extract.button')} {$_('toolbar.extract.button')}
</Button> </Button>
<Help link="./help/toolbar/extract"> <Help link={getURLForLanguage($locale, '/help/toolbar/extract')}>
{#if validSelection} {#if validSelection}
{$_('toolbar.extract.help')} {$_('toolbar.extract.help')}
{:else} {:else}

View File

@@ -1,89 +1,90 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
enum MergeType { enum MergeType {
TRACES = 'traces', TRACES = 'traces',
CONTENTS = 'contents' CONTENTS = 'contents'
} }
</script> </script>
<script lang="ts"> <script lang="ts">
import { ListFileItem, ListTrackItem } from '$lib/components/file-list/FileList'; import { ListFileItem, ListTrackItem } from '$lib/components/file-list/FileList';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { selection } from '$lib/components/file-list/Selection'; import { selection } from '$lib/components/file-list/Selection';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label/index.js'; import { Label } from '$lib/components/ui/label/index.js';
import * as RadioGroup from '$lib/components/ui/radio-group'; import * as RadioGroup from '$lib/components/ui/radio-group';
import { _ } from 'svelte-i18n'; import { _, locale } from 'svelte-i18n';
import { dbUtils, getFile } from '$lib/db'; import { dbUtils, getFile } from '$lib/db';
import { Group } from 'lucide-svelte'; import { Group } from 'lucide-svelte';
import { getURLForLanguage } from '$lib/utils';
let canMergeTraces = false; let canMergeTraces = false;
let canMergeContents = false; let canMergeContents = false;
$: if ($selection.size > 1) { $: if ($selection.size > 1) {
canMergeTraces = true; canMergeTraces = true;
} else if ($selection.size === 1) { } else if ($selection.size === 1) {
let selected = $selection.getSelected()[0]; let selected = $selection.getSelected()[0];
if (selected instanceof ListFileItem) { if (selected instanceof ListFileItem) {
let file = getFile(selected.getFileId()); let file = getFile(selected.getFileId());
if (file) { if (file) {
canMergeTraces = file.getSegments().length > 1; canMergeTraces = file.getSegments().length > 1;
} else { } else {
canMergeTraces = false; canMergeTraces = false;
} }
} else if (selected instanceof ListTrackItem) { } else if (selected instanceof ListTrackItem) {
let trackIndex = selected.getTrackIndex(); let trackIndex = selected.getTrackIndex();
let file = getFile(selected.getFileId()); let file = getFile(selected.getFileId());
if (file && trackIndex < file.trk.length) { if (file && trackIndex < file.trk.length) {
canMergeTraces = file.trk[trackIndex].getSegments().length > 1; canMergeTraces = file.trk[trackIndex].getSegments().length > 1;
} else { } else {
canMergeTraces = false; canMergeTraces = false;
} }
} else { } else {
canMergeContents = false; canMergeContents = false;
} }
} }
$: canMergeContents = $: canMergeContents =
$selection.size > 1 && $selection.size > 1 &&
$selection $selection
.getSelected() .getSelected()
.some((item) => item instanceof ListFileItem || item instanceof ListTrackItem); .some((item) => item instanceof ListFileItem || item instanceof ListTrackItem);
let mergeType = MergeType.TRACES; let mergeType = MergeType.TRACES;
</script> </script>
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
<RadioGroup.Root bind:value={mergeType}> <RadioGroup.Root bind:value={mergeType}>
<Label class="flex flex-row items-center gap-2 leading-5"> <Label class="flex flex-row items-center gap-2 leading-5">
<RadioGroup.Item value={MergeType.TRACES} /> <RadioGroup.Item value={MergeType.TRACES} />
{$_('toolbar.merge.merge_traces')} {$_('toolbar.merge.merge_traces')}
</Label> </Label>
<Label class="flex flex-row items-center gap-2 leading-5"> <Label class="flex flex-row items-center gap-2 leading-5">
<RadioGroup.Item value={MergeType.CONTENTS} /> <RadioGroup.Item value={MergeType.CONTENTS} />
{$_('toolbar.merge.merge_contents')} {$_('toolbar.merge.merge_contents')}
</Label> </Label>
</RadioGroup.Root> </RadioGroup.Root>
<Button <Button
variant="outline" variant="outline"
class="whitespace-normal h-fit" class="whitespace-normal h-fit"
disabled={(mergeType === MergeType.TRACES && !canMergeTraces) || disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
(mergeType === MergeType.CONTENTS && !canMergeContents)} (mergeType === MergeType.CONTENTS && !canMergeContents)}
on:click={() => { on:click={() => {
dbUtils.mergeSelection(mergeType === MergeType.TRACES); dbUtils.mergeSelection(mergeType === MergeType.TRACES);
}} }}
> >
<Group size="16" class="mr-1 shrink-0" /> <Group size="16" class="mr-1 shrink-0" />
{$_('toolbar.merge.merge_selection')} {$_('toolbar.merge.merge_selection')}
</Button> </Button>
<Help link="./help/toolbar/merge"> <Help link={getURLForLanguage($locale, '/help/toolbar/merge')}>
{#if mergeType === MergeType.TRACES && canMergeTraces} {#if mergeType === MergeType.TRACES && canMergeTraces}
{$_('toolbar.merge.help_merge_traces')} {$_('toolbar.merge.help_merge_traces')}
{:else if mergeType === MergeType.TRACES && !canMergeTraces} {:else if mergeType === MergeType.TRACES && !canMergeTraces}
{$_('toolbar.merge.help_cannot_merge_traces')} {$_('toolbar.merge.help_cannot_merge_traces')}
{:else if mergeType === MergeType.CONTENTS && canMergeContents} {:else if mergeType === MergeType.CONTENTS && canMergeContents}
{$_('toolbar.merge.help_merge_contents')} {$_('toolbar.merge.help_merge_contents')}
{:else if mergeType === MergeType.CONTENTS && !canMergeContents} {:else if mergeType === MergeType.CONTENTS && !canMergeContents}
{$_('toolbar.merge.help_cannot_merge_contents')} {$_('toolbar.merge.help_cannot_merge_contents')}
{/if} {/if}
</Help> </Help>
</div> </div>

View File

@@ -1,183 +1,175 @@
<script lang="ts"> <script lang="ts">
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 { Slider } from '$lib/components/ui/slider'; import { Slider } from '$lib/components/ui/slider';
import { selection } from '$lib/components/file-list/Selection'; import { selection } from '$lib/components/file-list/Selection';
import { import { ListItem, ListRootItem, ListTrackSegmentItem } from '$lib/components/file-list/FileList';
ListItem, import Help from '$lib/components/Help.svelte';
ListRootItem, import { Filter } from 'lucide-svelte';
ListTrackSegmentItem import { _, locale } from 'svelte-i18n';
} from '$lib/components/file-list/FileList'; import WithUnits from '$lib/components/WithUnits.svelte';
import Help from '$lib/components/Help.svelte'; import { dbUtils, fileObservers } from '$lib/db';
import { Filter } from 'lucide-svelte'; import { map } from '$lib/stores';
import { _ } from 'svelte-i18n'; import { onDestroy } from 'svelte';
import WithUnits from '$lib/components/WithUnits.svelte'; import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
import { dbUtils, fileObservers } from '$lib/db'; import { derived } from 'svelte/store';
import { map } from '$lib/stores'; import { getURLForLanguage } from '$lib/utils';
import { onDestroy } from 'svelte';
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
import { derived } from 'svelte/store';
let sliderValue = [50]; let sliderValue = [50];
let maxPoints = 0; let maxPoints = 0;
let currentPoints = 0; let currentPoints = 0;
$: validSelection = $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']); $: validSelection = $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
$: tolerance = 2 ** (sliderValue[0] / (100 / Math.log2(10000))); $: tolerance = 2 ** (sliderValue[0] / (100 / Math.log2(10000)));
let simplified = new Map<string, [ListItem, number, SimplifiedTrackPoint[]]>(); let simplified = new Map<string, [ListItem, number, SimplifiedTrackPoint[]]>();
let unsubscribes = new Map<string, () => void>(); let unsubscribes = new Map<string, () => void>();
function update() { function update() {
maxPoints = 0; maxPoints = 0;
currentPoints = 0; currentPoints = 0;
let data: GeoJSON.FeatureCollection = { let data: GeoJSON.FeatureCollection = {
type: 'FeatureCollection', type: 'FeatureCollection',
features: [] features: []
}; };
simplified.forEach(([item, maxPts, points], itemFullId) => { simplified.forEach(([item, maxPts, points], itemFullId) => {
maxPoints += maxPts; maxPoints += maxPts;
let current = points.filter( let current = points.filter(
(point) => point.distance === undefined || point.distance >= tolerance (point) => point.distance === undefined || point.distance >= tolerance
); );
currentPoints += current.length; currentPoints += current.length;
data.features.push({ data.features.push({
type: 'Feature', type: 'Feature',
geometry: { geometry: {
type: 'LineString', type: 'LineString',
coordinates: current.map((point) => [ coordinates: current.map((point) => [
point.point.getLongitude(), point.point.getLongitude(),
point.point.getLatitude() point.point.getLatitude()
]) ])
}, },
properties: {} properties: {}
}); });
}); });
if ($map) { if ($map) {
let source = $map.getSource('simplified'); let source = $map.getSource('simplified');
if (source) { if (source) {
source.setData(data); source.setData(data);
} else { } else {
$map.addSource('simplified', { $map.addSource('simplified', {
type: 'geojson', type: 'geojson',
data: data data: data
}); });
} }
if (!$map.getLayer('simplified')) { if (!$map.getLayer('simplified')) {
$map.addLayer({ $map.addLayer({
id: 'simplified', id: 'simplified',
type: 'line', type: 'line',
source: 'simplified', source: 'simplified',
paint: { paint: {
'line-color': 'white', 'line-color': 'white',
'line-width': 3 'line-width': 3
} }
}); });
} else { } else {
$map.moveLayer('simplified'); $map.moveLayer('simplified');
} }
} }
} }
$: if ($fileObservers) { $: if ($fileObservers) {
unsubscribes.forEach((unsubscribe, fileId) => { unsubscribes.forEach((unsubscribe, fileId) => {
if (!$fileObservers.has(fileId)) { if (!$fileObservers.has(fileId)) {
unsubscribe(); unsubscribe();
unsubscribes.delete(fileId); unsubscribes.delete(fileId);
} }
}); });
$fileObservers.forEach((fileStore, fileId) => { $fileObservers.forEach((fileStore, fileId) => {
if (!unsubscribes.has(fileId)) { if (!unsubscribes.has(fileId)) {
let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [ let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [fs, sel]).subscribe(
fs, ([fs, sel]) => {
sel if (fs) {
]).subscribe(([fs, sel]) => { fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
if (fs) { let segmentItem = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex);
fs.file.forEachSegment((segment, trackIndex, segmentIndex) => { if (sel.hasAnyParent(segmentItem)) {
let segmentItem = new ListTrackSegmentItem( let statistics = fs.statistics.getStatisticsFor(segmentItem);
fileId, simplified.set(segmentItem.getFullId(), [
trackIndex, segmentItem,
segmentIndex statistics.local.points.length,
); ramerDouglasPeucker(statistics.local.points, 1)
if (sel.hasAnyParent(segmentItem)) { ]);
let statistics = fs.statistics.getStatisticsFor(segmentItem); update();
simplified.set(segmentItem.getFullId(), [ } else if (simplified.has(segmentItem.getFullId())) {
segmentItem, simplified.delete(segmentItem.getFullId());
statistics.local.points.length, update();
ramerDouglasPeucker(statistics.local.points, 1) }
]); });
update(); }
} else if (simplified.has(segmentItem.getFullId())) { }
simplified.delete(segmentItem.getFullId()); );
update(); unsubscribes.set(fileId, unsubscribe);
} }
}); });
} }
});
unsubscribes.set(fileId, unsubscribe);
}
});
}
$: if (tolerance) { $: if (tolerance) {
update(); update();
} }
onDestroy(() => { onDestroy(() => {
if ($map) { if ($map) {
if ($map.getLayer('simplified')) { if ($map.getLayer('simplified')) {
$map.removeLayer('simplified'); $map.removeLayer('simplified');
} }
if ($map.getSource('simplified')) { if ($map.getSource('simplified')) {
$map.removeSource('simplified'); $map.removeSource('simplified');
} }
} }
unsubscribes.forEach((unsubscribe) => unsubscribe()); unsubscribes.forEach((unsubscribe) => unsubscribe());
simplified.clear(); simplified.clear();
}); });
function reduce() { function reduce() {
let itemsAndPoints = new Map<ListItem, TrackPoint[]>(); let itemsAndPoints = new Map<ListItem, TrackPoint[]>();
simplified.forEach(([item, maxPts, points], itemFullId) => { simplified.forEach(([item, maxPts, points], itemFullId) => {
itemsAndPoints.set( itemsAndPoints.set(
item, item,
points points
.filter((point) => point.distance === undefined || point.distance >= tolerance) .filter((point) => point.distance === undefined || point.distance >= tolerance)
.map((point) => point.point) .map((point) => point.point)
); );
}); });
dbUtils.reduce(itemsAndPoints); dbUtils.reduce(itemsAndPoints);
} }
</script> </script>
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
<div class="p-2"> <div class="p-2">
<Slider bind:value={sliderValue} min={0} max={100} step={1} /> <Slider bind:value={sliderValue} min={0} max={100} step={1} />
</div> </div>
<Label class="flex flex-row justify-between"> <Label class="flex flex-row justify-between">
<span>{$_('toolbar.reduce.tolerance')}</span> <span>{$_('toolbar.reduce.tolerance')}</span>
<WithUnits value={tolerance / 1000} type="distance" decimals={3} class="font-normal" /> <WithUnits value={tolerance / 1000} type="distance" decimals={3} class="font-normal" />
</Label> </Label>
<Label class="flex flex-row justify-between"> <Label class="flex flex-row justify-between">
<span>{$_('toolbar.reduce.number_of_points')}</span> <span>{$_('toolbar.reduce.number_of_points')}</span>
<span class="font-normal">{currentPoints}/{maxPoints}</span> <span class="font-normal">{currentPoints}/{maxPoints}</span>
</Label> </Label>
<Button variant="outline" disabled={!validSelection} on:click={reduce}> <Button variant="outline" disabled={!validSelection} on:click={reduce}>
<Filter size="16" class="mr-1" /> <Filter size="16" class="mr-1" />
{$_('toolbar.reduce.button')} {$_('toolbar.reduce.button')}
</Button> </Button>
<Help link="./help/toolbar/minify"> <Help link={getURLForLanguage($locale, '/help/toolbar/minify')}>
{#if validSelection} {#if validSelection}
{$_('toolbar.reduce.help')} {$_('toolbar.reduce.help')}
{:else} {:else}
{$_('toolbar.reduce.help_no_selection')} {$_('toolbar.reduce.help_no_selection')}
{/if} {/if}
</Help> </Help>
</div> </div>

View File

@@ -1,403 +1,393 @@
<script lang="ts"> <script lang="ts">
import DatePicker from '$lib/components/ui/date-picker/DatePicker.svelte'; import DatePicker from '$lib/components/ui/date-picker/DatePicker.svelte';
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
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 { Checkbox } from '$lib/components/ui/checkbox'; import { Checkbox } from '$lib/components/ui/checkbox';
import TimePicker from '$lib/components/ui/time-picker/TimePicker.svelte'; import TimePicker from '$lib/components/ui/time-picker/TimePicker.svelte';
import { dbUtils, settings } from '$lib/db'; import { dbUtils, settings } from '$lib/db';
import { gpxStatistics } from '$lib/stores'; import { gpxStatistics } from '$lib/stores';
import { import {
distancePerHourToSecondsPerDistance, distancePerHourToSecondsPerDistance,
getConvertedVelocity, getConvertedVelocity,
milesToKilometers, milesToKilometers,
nauticalMilesToKilometers nauticalMilesToKilometers
} from '$lib/units'; } from '$lib/units';
import { CalendarDate, type DateValue } from '@internationalized/date'; import { CalendarDate, type DateValue } from '@internationalized/date';
import { CalendarClock, CirclePlay, CircleStop, CircleX, Timer, Zap } from 'lucide-svelte'; import { CalendarClock, CirclePlay, CircleStop, CircleX, Timer, Zap } from 'lucide-svelte';
import { tick } from 'svelte'; import { tick } from 'svelte';
import { _, locale } from 'svelte-i18n'; import { _, locale } from 'svelte-i18n';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { selection } from '$lib/components/file-list/Selection'; import { selection } from '$lib/components/file-list/Selection';
import { import {
ListFileItem, ListFileItem,
ListRootItem, ListRootItem,
ListTrackItem, ListTrackItem,
ListTrackSegmentItem ListTrackSegmentItem
} from '$lib/components/file-list/FileList'; } from '$lib/components/file-list/FileList';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { getURLForLanguage } from '$lib/utils';
let startDate: DateValue | undefined = undefined; let startDate: DateValue | undefined = undefined;
let startTime: string | undefined = undefined; let startTime: string | undefined = undefined;
let endDate: DateValue | undefined = undefined; let endDate: DateValue | undefined = undefined;
let endTime: string | undefined = undefined; let endTime: string | undefined = undefined;
let movingTime: number | undefined = undefined; let movingTime: number | undefined = undefined;
let speed: number | undefined = undefined; let speed: number | undefined = undefined;
let artificial = false; let artificial = false;
function toCalendarDate(date: Date): CalendarDate { function toCalendarDate(date: Date): CalendarDate {
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate()); return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
} }
function toTimeString(date: Date): string { function toTimeString(date: Date): string {
return date.toTimeString().split(' ')[0]; return date.toTimeString().split(' ')[0];
} }
const { velocityUnits, distanceUnits } = settings; const { velocityUnits, distanceUnits } = settings;
function setSpeed(value: number) { function setSpeed(value: number) {
let speedValue = getConvertedVelocity(value); let speedValue = getConvertedVelocity(value);
if ($velocityUnits === 'speed') { if ($velocityUnits === 'speed') {
speedValue = parseFloat(speedValue.toFixed(2)); speedValue = parseFloat(speedValue.toFixed(2));
} }
speed = speedValue; speed = speedValue;
} }
function setGPXData() { function setGPXData() {
if ($gpxStatistics.global.time.start) { if ($gpxStatistics.global.time.start) {
startDate = toCalendarDate($gpxStatistics.global.time.start); startDate = toCalendarDate($gpxStatistics.global.time.start);
startTime = toTimeString($gpxStatistics.global.time.start); startTime = toTimeString($gpxStatistics.global.time.start);
} else { } else {
startDate = undefined; startDate = undefined;
startTime = undefined; startTime = undefined;
} }
if ($gpxStatistics.global.time.end) { if ($gpxStatistics.global.time.end) {
endDate = toCalendarDate($gpxStatistics.global.time.end); endDate = toCalendarDate($gpxStatistics.global.time.end);
endTime = toTimeString($gpxStatistics.global.time.end); endTime = toTimeString($gpxStatistics.global.time.end);
} else { } else {
endDate = undefined; endDate = undefined;
endTime = undefined; endTime = undefined;
} }
if ($gpxStatistics.global.time.moving) { if ($gpxStatistics.global.time.moving) {
movingTime = $gpxStatistics.global.time.moving; movingTime = $gpxStatistics.global.time.moving;
} else { } else {
movingTime = undefined; movingTime = undefined;
} }
if ($gpxStatistics.global.speed.moving) { if ($gpxStatistics.global.speed.moving) {
setSpeed($gpxStatistics.global.speed.moving); setSpeed($gpxStatistics.global.speed.moving);
} else { } else {
speed = undefined; speed = undefined;
} }
} }
$: if ($gpxStatistics && $velocityUnits && $distanceUnits) { $: if ($gpxStatistics && $velocityUnits && $distanceUnits) {
setGPXData(); setGPXData();
} }
function getDate(date: DateValue, time: string): Date { function getDate(date: DateValue, time: string): Date {
if (date === undefined) { if (date === undefined) {
return new Date(); return new Date();
} }
let [hours, minutes, seconds] = time.split(':').map((x) => parseInt(x)); let [hours, minutes, seconds] = time.split(':').map((x) => parseInt(x));
if (seconds === undefined) { if (seconds === undefined) {
seconds = 0; seconds = 0;
} }
return new Date(date.year, date.month - 1, date.day, hours, minutes, seconds); return new Date(date.year, date.month - 1, date.day, hours, minutes, seconds);
} }
function updateEnd() { function updateEnd() {
if (startDate && movingTime !== undefined) { if (startDate && movingTime !== undefined) {
if (startTime === undefined) { if (startTime === undefined) {
startTime = '00:00:00'; startTime = '00:00:00';
} }
let start = getDate(startDate, startTime); let start = getDate(startDate, startTime);
let ratio = let ratio =
$gpxStatistics.global.time.moving > 0 $gpxStatistics.global.time.moving > 0
? $gpxStatistics.global.time.total / $gpxStatistics.global.time.moving ? $gpxStatistics.global.time.total / $gpxStatistics.global.time.moving
: 1; : 1;
let end = new Date(start.getTime() + ratio * movingTime * 1000); let end = new Date(start.getTime() + ratio * movingTime * 1000);
endDate = toCalendarDate(end); endDate = toCalendarDate(end);
endTime = toTimeString(end); endTime = toTimeString(end);
} }
} }
function updateStart() { function updateStart() {
if (endDate && movingTime !== undefined) { if (endDate && movingTime !== undefined) {
if (endTime === undefined) { if (endTime === undefined) {
endTime = '00:00:00'; endTime = '00:00:00';
} }
let end = getDate(endDate, endTime); let end = getDate(endDate, endTime);
let ratio = let ratio =
$gpxStatistics.global.time.moving > 0 $gpxStatistics.global.time.moving > 0
? $gpxStatistics.global.time.total / $gpxStatistics.global.time.moving ? $gpxStatistics.global.time.total / $gpxStatistics.global.time.moving
: 1; : 1;
let start = new Date(end.getTime() - ratio * movingTime * 1000); let start = new Date(end.getTime() - ratio * movingTime * 1000);
startDate = toCalendarDate(start); startDate = toCalendarDate(start);
startTime = toTimeString(start); startTime = toTimeString(start);
} }
} }
function getSpeed() { function getSpeed() {
if (speed === undefined) { if (speed === undefined) {
return undefined; return undefined;
} }
let speedValue = speed; let speedValue = speed;
if ($velocityUnits === 'pace') { if ($velocityUnits === 'pace') {
speedValue = distancePerHourToSecondsPerDistance(speed); speedValue = distancePerHourToSecondsPerDistance(speed);
} }
if ($distanceUnits === 'imperial') { if ($distanceUnits === 'imperial') {
speedValue = milesToKilometers(speedValue); speedValue = milesToKilometers(speedValue);
} else if ($distanceUnits === 'nautical') { } else if ($distanceUnits === 'nautical') {
speedValue = nauticalMilesToKilometers(speedValue); speedValue = nauticalMilesToKilometers(speedValue);
} }
return speedValue; return speedValue;
} }
function updateDataFromSpeed() { function updateDataFromSpeed() {
let speedValue = getSpeed(); let speedValue = getSpeed();
if (speedValue === undefined) { if (speedValue === undefined) {
return; return;
} }
let distance = let distance =
$gpxStatistics.global.distance.moving > 0 $gpxStatistics.global.distance.moving > 0
? $gpxStatistics.global.distance.moving ? $gpxStatistics.global.distance.moving
: $gpxStatistics.global.distance.total; : $gpxStatistics.global.distance.total;
movingTime = (distance / speedValue) * 3600; movingTime = (distance / speedValue) * 3600;
updateEnd(); updateEnd();
} }
function updateDataFromTotalTime() { function updateDataFromTotalTime() {
if (movingTime === undefined) { if (movingTime === undefined) {
return; return;
} }
let distance = let distance =
$gpxStatistics.global.distance.moving > 0 $gpxStatistics.global.distance.moving > 0
? $gpxStatistics.global.distance.moving ? $gpxStatistics.global.distance.moving
: $gpxStatistics.global.distance.total; : $gpxStatistics.global.distance.total;
setSpeed(distance / (movingTime / 3600)); setSpeed(distance / (movingTime / 3600));
updateEnd(); updateEnd();
} }
$: canUpdate = $: canUpdate =
$selection.size === 1 && $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']); $selection.size === 1 && $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
</script> </script>
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
<fieldset class="flex flex-col gap-2"> <fieldset class="flex flex-col gap-2">
<div class="flex flex-row gap-2 justify-center"> <div class="flex flex-row gap-2 justify-center">
<div class="flex flex-col gap-2 grow"> <div class="flex flex-col gap-2 grow">
<Label for="speed" class="flex flex-row"> <Label for="speed" class="flex flex-row">
<Zap size="16" class="mr-1" /> <Zap size="16" class="mr-1" />
{#if $velocityUnits === 'speed'} {#if $velocityUnits === 'speed'}
{$_('quantities.speed')} {$_('quantities.speed')}
{:else} {:else}
{$_('quantities.pace')} {$_('quantities.pace')}
{/if} {/if}
</Label> </Label>
<div class="flex flex-row gap-1 items-center"> <div class="flex flex-row gap-1 items-center">
{#if $velocityUnits === 'speed'} {#if $velocityUnits === 'speed'}
<Input <Input
id="speed" id="speed"
type="number" type="number"
step={0.01} step={0.01}
min={0.01} min={0.01}
disabled={!canUpdate} disabled={!canUpdate}
bind:value={speed} bind:value={speed}
on:change={updateDataFromSpeed} on:change={updateDataFromSpeed}
/> />
<span class="text-sm shrink-0"> <span class="text-sm shrink-0">
{#if $distanceUnits === 'imperial'} {#if $distanceUnits === 'imperial'}
{$_('units.miles_per_hour')} {$_('units.miles_per_hour')}
{:else if $distanceUnits === 'metric'} {:else if $distanceUnits === 'metric'}
{$_('units.kilometers_per_hour')} {$_('units.kilometers_per_hour')}
{:else if $distanceUnits === 'nautical'} {:else if $distanceUnits === 'nautical'}
{$_('units.knots')} {$_('units.knots')}
{/if} {/if}
</span> </span>
{:else} {:else}
<TimePicker <TimePicker
bind:value={speed} bind:value={speed}
showHours={false} showHours={false}
disabled={!canUpdate} disabled={!canUpdate}
onChange={updateDataFromSpeed} onChange={updateDataFromSpeed}
/> />
<span class="text-sm shrink-0"> <span class="text-sm shrink-0">
{#if $distanceUnits === 'imperial'} {#if $distanceUnits === 'imperial'}
{$_('units.minutes_per_mile')} {$_('units.minutes_per_mile')}
{:else if $distanceUnits === 'metric'} {:else if $distanceUnits === 'metric'}
{$_('units.minutes_per_kilometer')} {$_('units.minutes_per_kilometer')}
{:else if $distanceUnits === 'nautical'} {:else if $distanceUnits === 'nautical'}
{$_('units.minutes_per_nautical_mile')} {$_('units.minutes_per_nautical_mile')}
{/if} {/if}
</span> </span>
{/if} {/if}
</div> </div>
</div> </div>
<div class="flex flex-col gap-2 grow"> <div class="flex flex-col gap-2 grow">
<Label for="duration" class="flex flex-row"> <Label for="duration" class="flex flex-row">
<Timer size="16" class="mr-1" /> <Timer size="16" class="mr-1" />
{$_('toolbar.time.total_time')} {$_('toolbar.time.total_time')}
</Label> </Label>
<TimePicker <TimePicker
bind:value={movingTime} bind:value={movingTime}
disabled={!canUpdate} disabled={!canUpdate}
onChange={updateDataFromTotalTime} onChange={updateDataFromTotalTime}
/> />
</div> </div>
</div> </div>
<Label class="flex flex-row"> <Label class="flex flex-row">
<CirclePlay size="16" class="mr-1" /> <CirclePlay size="16" class="mr-1" />
{$_('toolbar.time.start')} {$_('toolbar.time.start')}
</Label> </Label>
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<DatePicker <DatePicker
bind:value={startDate} bind:value={startDate}
disabled={!canUpdate} disabled={!canUpdate}
locale={get(locale) ?? 'en'} locale={get(locale) ?? 'en'}
placeholder={$_('toolbar.time.pick_date')} placeholder={$_('toolbar.time.pick_date')}
class="w-fit grow" class="w-fit grow"
onValueChange={async () => { onValueChange={async () => {
await tick(); await tick();
updateEnd(); updateEnd();
}} }}
/> />
<input <input
type="time" type="time"
step={1} step={1}
disabled={!canUpdate} disabled={!canUpdate}
bind:value={startTime} bind:value={startTime}
class="w-fit" class="w-fit"
on:change={updateEnd} on:change={updateEnd}
/> />
</div> </div>
<Label class="flex flex-row"> <Label class="flex flex-row">
<CircleStop size="16" class="mr-1" /> <CircleStop size="16" class="mr-1" />
{$_('toolbar.time.end')} {$_('toolbar.time.end')}
</Label> </Label>
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<DatePicker <DatePicker
bind:value={endDate} bind:value={endDate}
disabled={!canUpdate} disabled={!canUpdate}
locale={get(locale) ?? 'en'} locale={get(locale) ?? 'en'}
placeholder={$_('toolbar.time.pick_date')} placeholder={$_('toolbar.time.pick_date')}
class="w-fit grow" class="w-fit grow"
onValueChange={async () => { onValueChange={async () => {
await tick(); await tick();
updateStart(); updateStart();
}} }}
/> />
<input <input
type="time" type="time"
step={1} step={1}
disabled={!canUpdate} disabled={!canUpdate}
bind:value={endTime} bind:value={endTime}
class="w-fit" class="w-fit"
on:change={updateStart} on:change={updateStart}
/> />
</div> </div>
{#if $gpxStatistics.global.time.moving === 0 || $gpxStatistics.global.time.moving === undefined} {#if $gpxStatistics.global.time.moving === 0 || $gpxStatistics.global.time.moving === undefined}
<div class="mt-0.5 flex flex-row gap-1 items-center"> <div class="mt-0.5 flex flex-row gap-1 items-center">
<Checkbox id="artificial-time" bind:checked={artificial} disabled={!canUpdate} /> <Checkbox id="artificial-time" bind:checked={artificial} disabled={!canUpdate} />
<Label for="artificial-time"> <Label for="artificial-time">
{$_('toolbar.time.artificial')} {$_('toolbar.time.artificial')}
</Label> </Label>
</div> </div>
{/if} {/if}
</fieldset> </fieldset>
<div class="flex flex-row gap-2 items-center"> <div class="flex flex-row gap-2 items-center">
<Button <Button
variant="outline" variant="outline"
disabled={!canUpdate} disabled={!canUpdate}
class="grow whitespace-normal h-fit" class="grow whitespace-normal h-fit"
on:click={() => { on:click={() => {
let effectiveSpeed = getSpeed(); let effectiveSpeed = getSpeed();
if ( if (startDate === undefined || startTime === undefined || effectiveSpeed === undefined) {
startDate === undefined || return;
startTime === undefined || }
effectiveSpeed === undefined
) {
return;
}
if (Math.abs(effectiveSpeed - $gpxStatistics.global.speed.moving) < 0.01) { if (Math.abs(effectiveSpeed - $gpxStatistics.global.speed.moving) < 0.01) {
effectiveSpeed = $gpxStatistics.global.speed.moving; effectiveSpeed = $gpxStatistics.global.speed.moving;
} }
let ratio = 1; let ratio = 1;
if ( if (
$gpxStatistics.global.speed.moving > 0 && $gpxStatistics.global.speed.moving > 0 &&
$gpxStatistics.global.speed.moving !== effectiveSpeed $gpxStatistics.global.speed.moving !== effectiveSpeed
) { ) {
ratio = $gpxStatistics.global.speed.moving / effectiveSpeed; ratio = $gpxStatistics.global.speed.moving / effectiveSpeed;
} }
let item = $selection.getSelected()[0]; let item = $selection.getSelected()[0];
let fileId = item.getFileId(); let fileId = item.getFileId();
dbUtils.applyToFile(fileId, (file) => { dbUtils.applyToFile(fileId, (file) => {
if (item instanceof ListFileItem) { if (item instanceof ListFileItem) {
if (artificial) { if (artificial) {
file.createArtificialTimestamps( file.createArtificialTimestamps(getDate(startDate, startTime), movingTime);
getDate(startDate, startTime), } else {
movingTime file.changeTimestamps(getDate(startDate, startTime), effectiveSpeed, ratio);
); }
} else { } else if (item instanceof ListTrackItem) {
file.changeTimestamps( if (artificial) {
getDate(startDate, startTime), file.createArtificialTimestamps(
effectiveSpeed, getDate(startDate, startTime),
ratio movingTime,
); item.getTrackIndex()
} );
} else if (item instanceof ListTrackItem) { } else {
if (artificial) { file.changeTimestamps(
file.createArtificialTimestamps( getDate(startDate, startTime),
getDate(startDate, startTime), effectiveSpeed,
movingTime, ratio,
item.getTrackIndex() item.getTrackIndex()
); );
} else { }
file.changeTimestamps( } else if (item instanceof ListTrackSegmentItem) {
getDate(startDate, startTime), if (artificial) {
effectiveSpeed, file.createArtificialTimestamps(
ratio, getDate(startDate, startTime),
item.getTrackIndex() movingTime,
); item.getTrackIndex(),
} item.getSegmentIndex()
} else if (item instanceof ListTrackSegmentItem) { );
if (artificial) { } else {
file.createArtificialTimestamps( file.changeTimestamps(
getDate(startDate, startTime), getDate(startDate, startTime),
movingTime, effectiveSpeed,
item.getTrackIndex(), ratio,
item.getSegmentIndex() item.getTrackIndex(),
); item.getSegmentIndex()
} else { );
file.changeTimestamps( }
getDate(startDate, startTime), }
effectiveSpeed, });
ratio, }}
item.getTrackIndex(), >
item.getSegmentIndex() <CalendarClock size="16" class="mr-1 shrink-0" />
); {$_('toolbar.time.update')}
} </Button>
} <Button variant="outline" on:click={setGPXData}>
}); <CircleX size="16" />
}} </Button>
> </div>
<CalendarClock size="16" class="mr-1 shrink-0" /> <Help link={getURLForLanguage($locale, '/help/toolbar/time')}>
{$_('toolbar.time.update')} {#if canUpdate}
</Button> {$_('toolbar.time.help')}
<Button variant="outline" on:click={setGPXData}> {:else}
<CircleX size="16" /> {$_('toolbar.time.help_invalid_selection')}
</Button> {/if}
</div> </Help>
<Help link="./help/toolbar/time">
{#if canUpdate}
{$_('toolbar.time.help')}
{:else}
{$_('toolbar.time.help_invalid_selection')}
{/if}
</Help>
</div> </div>
<style lang="postcss"> <style lang="postcss">
div :global(input[type='time']) { div :global(input[type='time']) {
/* /*
Style copy-pasted from shadcn-svelte Input. Style copy-pasted from shadcn-svelte Input.
Needed to use native time input to avoid a bug with 2-level bind:value. Needed to use native time input to avoid a bug with 2-level bind:value.
*/ */
@apply flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50; @apply flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
} }
</style> </style>

View File

@@ -1,272 +1,272 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
export const selectedWaypoint = writable<[Waypoint, string] | undefined>(undefined); export const selectedWaypoint = writable<[Waypoint, string] | undefined>(undefined);
</script> </script>
<script lang="ts"> <script lang="ts">
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
import { Textarea } from '$lib/components/ui/textarea'; import { Textarea } from '$lib/components/ui/textarea';
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 Select from '$lib/components/ui/select'; import * as Select from '$lib/components/ui/select';
import { selection } from '$lib/components/file-list/Selection'; import { selection } from '$lib/components/file-list/Selection';
import { Waypoint } from 'gpx'; import { Waypoint } from 'gpx';
import { _, locale } from 'svelte-i18n'; import { _, locale } from 'svelte-i18n';
import { ListWaypointItem } from '$lib/components/file-list/FileList'; import { ListWaypointItem } from '$lib/components/file-list/FileList';
import { dbUtils, fileObservers, getFile, settings, type GPXFileWithStatistics } from '$lib/db'; import { dbUtils, fileObservers, getFile, settings, type GPXFileWithStatistics } from '$lib/db';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { map } from '$lib/stores'; import { map } from '$lib/stores';
import { resetCursor, setCrosshairCursor } from '$lib/utils'; import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
import { CirclePlus, CircleX, Save } from 'lucide-svelte'; import { CirclePlus, CircleX, Save } from 'lucide-svelte';
import { getSymbolKey, symbols } from '$lib/assets/symbols'; import { getSymbolKey, symbols } from '$lib/assets/symbols';
let name: string; let name: string;
let description: string; let description: string;
let link: string; let link: string;
let longitude: number; let longitude: number;
let latitude: number; let latitude: number;
let selectedSymbol = { let selectedSymbol = {
value: '', value: '',
label: '' label: ''
}; };
const { verticalFileView } = settings; const { verticalFileView } = settings;
$: canCreate = $selection.size > 0; $: canCreate = $selection.size > 0;
$: if ($verticalFileView && $selection) { $: if ($verticalFileView && $selection) {
selectedWaypoint.update(() => { selectedWaypoint.update(() => {
if ($selection.size === 1) { if ($selection.size === 1) {
let item = $selection.getSelected()[0]; let item = $selection.getSelected()[0];
if (item instanceof ListWaypointItem) { if (item instanceof ListWaypointItem) {
let file = getFile(item.getFileId()); let file = getFile(item.getFileId());
let waypoint = file?.wpt[item.getWaypointIndex()]; let waypoint = file?.wpt[item.getWaypointIndex()];
if (waypoint) { if (waypoint) {
return [waypoint, item.getFileId()]; return [waypoint, item.getFileId()];
} }
} }
} }
return undefined; return undefined;
}); });
} }
let unsubscribe: (() => void) | undefined = undefined; let unsubscribe: (() => void) | undefined = undefined;
function updateWaypointData(fileStore: GPXFileWithStatistics | undefined) { function updateWaypointData(fileStore: GPXFileWithStatistics | undefined) {
if ($selectedWaypoint) { if ($selectedWaypoint) {
if (fileStore) { if (fileStore) {
if ($selectedWaypoint[0]._data.index < fileStore.file.wpt.length) { if ($selectedWaypoint[0]._data.index < fileStore.file.wpt.length) {
$selectedWaypoint[0] = fileStore.file.wpt[$selectedWaypoint[0]._data.index]; $selectedWaypoint[0] = fileStore.file.wpt[$selectedWaypoint[0]._data.index];
name = $selectedWaypoint[0].name ?? ''; name = $selectedWaypoint[0].name ?? '';
description = $selectedWaypoint[0].desc ?? ''; description = $selectedWaypoint[0].desc ?? '';
if ( if (
$selectedWaypoint[0].cmt !== undefined && $selectedWaypoint[0].cmt !== undefined &&
$selectedWaypoint[0].cmt !== $selectedWaypoint[0].desc $selectedWaypoint[0].cmt !== $selectedWaypoint[0].desc
) { ) {
description += '\n\n' + $selectedWaypoint[0].cmt; description += '\n\n' + $selectedWaypoint[0].cmt;
} }
link = $selectedWaypoint[0].link?.attributes?.href ?? ''; link = $selectedWaypoint[0].link?.attributes?.href ?? '';
let symbol = $selectedWaypoint[0].sym ?? ''; let symbol = $selectedWaypoint[0].sym ?? '';
let symbolKey = getSymbolKey(symbol); let symbolKey = getSymbolKey(symbol);
if (symbolKey) { if (symbolKey) {
selectedSymbol = { selectedSymbol = {
value: symbol, value: symbol,
label: $_(`gpx.symbol.${symbolKey}`) label: $_(`gpx.symbol.${symbolKey}`)
}; };
} else { } else {
selectedSymbol = { selectedSymbol = {
value: symbol, value: symbol,
label: '' label: ''
}; };
} }
longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6)); longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
latitude = parseFloat($selectedWaypoint[0].getLatitude().toFixed(6)); latitude = parseFloat($selectedWaypoint[0].getLatitude().toFixed(6));
} else { } else {
selectedWaypoint.set(undefined); selectedWaypoint.set(undefined);
} }
} else { } else {
selectedWaypoint.set(undefined); selectedWaypoint.set(undefined);
} }
} }
} }
function resetWaypointData() { function resetWaypointData() {
name = ''; name = '';
description = ''; description = '';
link = ''; link = '';
selectedSymbol = { selectedSymbol = {
value: '', value: '',
label: '' label: ''
}; };
longitude = 0; longitude = 0;
latitude = 0; latitude = 0;
} }
$: { $: {
if (unsubscribe) { if (unsubscribe) {
unsubscribe(); unsubscribe();
unsubscribe = undefined; unsubscribe = undefined;
} }
if ($selectedWaypoint) { if ($selectedWaypoint) {
let fileStore = get(fileObservers).get($selectedWaypoint[1]); let fileStore = get(fileObservers).get($selectedWaypoint[1]);
if (fileStore) { if (fileStore) {
unsubscribe = fileStore.subscribe(updateWaypointData); unsubscribe = fileStore.subscribe(updateWaypointData);
} }
} else { } else {
resetWaypointData(); resetWaypointData();
} }
} }
function createOrUpdateWaypoint() { function createOrUpdateWaypoint() {
if (typeof latitude === 'string') { if (typeof latitude === 'string') {
latitude = parseFloat(latitude); latitude = parseFloat(latitude);
} }
if (typeof longitude === 'string') { if (typeof longitude === 'string') {
longitude = parseFloat(longitude); longitude = parseFloat(longitude);
} }
latitude = parseFloat(latitude.toFixed(6)); latitude = parseFloat(latitude.toFixed(6));
longitude = parseFloat(longitude.toFixed(6)); longitude = parseFloat(longitude.toFixed(6));
dbUtils.addOrUpdateWaypoint( dbUtils.addOrUpdateWaypoint(
{ {
attributes: { attributes: {
lat: latitude, lat: latitude,
lon: longitude lon: longitude
}, },
name: name.length > 0 ? name : undefined, name: name.length > 0 ? name : undefined,
desc: description.length > 0 ? description : undefined, desc: description.length > 0 ? description : undefined,
cmt: description.length > 0 ? description : undefined, cmt: description.length > 0 ? description : undefined,
link: link.length > 0 ? { attributes: { href: link } } : undefined, link: link.length > 0 ? { attributes: { href: link } } : undefined,
sym: selectedSymbol.value.length > 0 ? selectedSymbol.value : undefined sym: selectedSymbol.value.length > 0 ? selectedSymbol.value : undefined
}, },
$selectedWaypoint $selectedWaypoint
? new ListWaypointItem($selectedWaypoint[1], $selectedWaypoint[0]._data.index) ? new ListWaypointItem($selectedWaypoint[1], $selectedWaypoint[0]._data.index)
: undefined : undefined
); );
selectedWaypoint.set(undefined); selectedWaypoint.set(undefined);
resetWaypointData(); resetWaypointData();
} }
function setCoordinates(e: any) { function setCoordinates(e: any) {
latitude = e.lngLat.lat.toFixed(6); latitude = e.lngLat.lat.toFixed(6);
longitude = e.lngLat.lng.toFixed(6); longitude = e.lngLat.lng.toFixed(6);
} }
$: sortedSymbols = Object.entries(symbols).sort((a, b) => { $: sortedSymbols = Object.entries(symbols).sort((a, b) => {
return $_(`gpx.symbol.${a[0]}`).localeCompare($_(`gpx.symbol.${b[0]}`), $locale ?? 'en'); return $_(`gpx.symbol.${a[0]}`).localeCompare($_(`gpx.symbol.${b[0]}`), $locale ?? 'en');
}); });
onMount(() => { onMount(() => {
let m = get(map); let m = get(map);
m?.on('click', setCoordinates); m?.on('click', setCoordinates);
setCrosshairCursor(); setCrosshairCursor();
}); });
onDestroy(() => { onDestroy(() => {
let m = get(map); let m = get(map);
m?.off('click', setCoordinates); m?.off('click', setCoordinates);
resetCursor(); resetCursor();
if (unsubscribe) { if (unsubscribe) {
unsubscribe(); unsubscribe();
unsubscribe = undefined; unsubscribe = undefined;
} }
}); });
</script> </script>
<div class="flex flex-col gap-3 w-full max-w-96 {$$props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-96 {$$props.class ?? ''}">
<fieldset class="flex flex-col gap-2"> <fieldset class="flex flex-col gap-2">
<Label for="name">{$_('menu.metadata.name')}</Label> <Label for="name">{$_('menu.metadata.name')}</Label>
<Input bind:value={name} id="name" class="font-semibold h-8" /> <Input bind:value={name} id="name" class="font-semibold h-8" />
<Label for="description">{$_('menu.metadata.description')}</Label> <Label for="description">{$_('menu.metadata.description')}</Label>
<Textarea bind:value={description} id="description" /> <Textarea bind:value={description} id="description" />
<Label for="symbol">{$_('toolbar.waypoint.icon')}</Label> <Label for="symbol">{$_('toolbar.waypoint.icon')}</Label>
<Select.Root bind:selected={selectedSymbol}> <Select.Root bind:selected={selectedSymbol}>
<Select.Trigger id="symbol" class="w-full h-8"> <Select.Trigger id="symbol" class="w-full h-8">
<Select.Value /> <Select.Value />
</Select.Trigger> </Select.Trigger>
<Select.Content class="max-h-60 overflow-y-scroll"> <Select.Content class="max-h-60 overflow-y-scroll">
{#each sortedSymbols as [key, symbol]} {#each sortedSymbols as [key, symbol]}
<Select.Item value={symbol.value}> <Select.Item value={symbol.value}>
<span> <span>
{#if symbol.icon} {#if symbol.icon}
<svelte:component <svelte:component
this={symbol.icon} this={symbol.icon}
size="14" size="14"
class="inline-block align-sub mr-0.5" class="inline-block align-sub mr-0.5"
/> />
{:else} {:else}
<span class="w-4 inline-block" /> <span class="w-4 inline-block" />
{/if} {/if}
{$_(`gpx.symbol.${key}`)} {$_(`gpx.symbol.${key}`)}
</span> </span>
</Select.Item> </Select.Item>
{/each} {/each}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
<Label for="link">{$_('toolbar.waypoint.link')}</Label> <Label for="link">{$_('toolbar.waypoint.link')}</Label>
<Input bind:value={link} id="link" class="h-8" /> <Input bind:value={link} id="link" class="h-8" />
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<div class="grow"> <div class="grow">
<Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label> <Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label>
<Input <Input
bind:value={latitude} bind:value={latitude}
type="number" type="number"
id="latitude" id="latitude"
step={1e-6} step={1e-6}
min={-90} min={-90}
max={90} max={90}
class="text-xs h-8 " class="text-xs h-8 "
/> />
</div> </div>
<div class="grow"> <div class="grow">
<Label for="longitude">{$_('toolbar.waypoint.longitude')}</Label> <Label for="longitude">{$_('toolbar.waypoint.longitude')}</Label>
<Input <Input
bind:value={longitude} bind:value={longitude}
type="number" type="number"
id="longitude" id="longitude"
step={1e-6} step={1e-6}
min={-180} min={-180}
max={180} max={180}
class="text-xs h-8" class="text-xs h-8"
/> />
</div> </div>
</div> </div>
</fieldset> </fieldset>
<div class="flex flex-row gap-2 items-center"> <div class="flex flex-row gap-2 items-center">
<Button <Button
variant="outline" variant="outline"
disabled={!canCreate && !$selectedWaypoint} disabled={!canCreate && !$selectedWaypoint}
class="grow whitespace-normal h-fit" class="grow whitespace-normal h-fit"
on:click={createOrUpdateWaypoint} on:click={createOrUpdateWaypoint}
> >
{#if $selectedWaypoint} {#if $selectedWaypoint}
<Save size="16" class="mr-1 shrink-0" /> <Save size="16" class="mr-1 shrink-0" />
{$_('menu.metadata.save')} {$_('menu.metadata.save')}
{:else} {:else}
<CirclePlus size="16" class="mr-1 shrink-0" /> <CirclePlus size="16" class="mr-1 shrink-0" />
{$_('toolbar.waypoint.create')} {$_('toolbar.waypoint.create')}
{/if} {/if}
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
on:click={() => { on:click={() => {
selectedWaypoint.set(undefined); selectedWaypoint.set(undefined);
resetWaypointData(); resetWaypointData();
}} }}
> >
<CircleX size="16" /> <CircleX size="16" />
</Button> </Button>
</div> </div>
<Help link="./help/toolbar/poi"> <Help link={getURLForLanguage($locale, '/help/toolbar/poi')}>
{#if $selectedWaypoint || canCreate} {#if $selectedWaypoint || canCreate}
{$_('toolbar.waypoint.help')} {$_('toolbar.waypoint.help')}
{:else} {:else}
{$_('toolbar.waypoint.help_no_selection')} {$_('toolbar.waypoint.help_no_selection')}
{/if} {/if}
</Help> </Help>
</div> </div>

View File

@@ -25,7 +25,7 @@
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 { _ } from 'svelte-i18n'; import { _, locale } from 'svelte-i18n';
import { RoutingControls } from './RoutingControls'; import { RoutingControls } from './RoutingControls';
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import { fileObservers } from '$lib/db'; import { fileObservers } from '$lib/db';
@@ -38,7 +38,7 @@
ListTrackSegmentItem, ListTrackSegmentItem,
type ListItem type ListItem
} from '$lib/components/file-list/FileList'; } from '$lib/components/file-list/FileList';
import { flyAndScale, resetCursor, setCrosshairCursor } from '$lib/utils'; import { flyAndScale, getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { TrackPoint } from 'gpx'; import { TrackPoint } from 'gpx';
@@ -236,7 +236,7 @@
</Tooltip> </Tooltip>
</div> </div>
<div class="w-full flex flex-row gap-2 items-end justify-between"> <div class="w-full flex flex-row gap-2 items-end justify-between">
<Help link="./help/toolbar/routing"> <Help link={getURLForLanguage($locale, '/help/toolbar/routing')}>
{#if !validSelection} {#if !validSelection}
{$_('toolbar.routing.help_no_file')} {$_('toolbar.routing.help_no_file')}
{:else} {:else}

View File

@@ -17,11 +17,12 @@
import { Separator } from '$lib/components/ui/separator'; import { Separator } from '$lib/components/ui/separator';
import { gpxStatistics, map, slicedGPXStatistics, splitAs } from '$lib/stores'; import { gpxStatistics, map, slicedGPXStatistics, splitAs } from '$lib/stores';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { _ } from 'svelte-i18n'; import { _, locale } from 'svelte-i18n';
import { onDestroy, tick } from 'svelte'; import { onDestroy, tick } from 'svelte';
import { Crop } from 'lucide-svelte'; import { Crop } from 'lucide-svelte';
import { dbUtils } from '$lib/db'; import { dbUtils } from '$lib/db';
import { SplitControls } from './SplitControls'; import { SplitControls } from './SplitControls';
import { getURLForLanguage } from '$lib/utils';
let splitControls: SplitControls | undefined = undefined; let splitControls: SplitControls | undefined = undefined;
let canCrop = false; let canCrop = false;
@@ -135,7 +136,7 @@
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
</Label> </Label>
<Help link="./help/toolbar/scissors"> <Help link={getURLForLanguage($locale, '/help/toolbar/scissors')}>
{#if validSelection} {#if validSelection}
{$_('toolbar.scissors.help')} {$_('toolbar.scissors.help')}
{:else} {:else}

View File

@@ -40,7 +40,7 @@ By right-clicking on a file tab, you can access the same actions as in the [edit
### Vertical layout ### Vertical layout
As mentioned in the [view options section](./menu/view), you can switch between a horizontal and a vertical layout for the file list. As mentioned in the [view options section](./menu/view), you can switch between a horizontal and a vertical layout for the file list.
The vertical file list is useful when you have many files open, or files with multiple [tracks, segments, or points of interest](../gpx). The vertical file list is useful when you have many files open, or files with multiple [tracks, segments, or points of interest](./gpx).
Indeed, this layout allows you to inspect the content of the files through collapsible sections. Indeed, this layout allows you to inspect the content of the files through collapsible sections.
You can also apply [edit actions](./menu/edit) and [tools](./toolbar/) to internal file items. You can also apply [edit actions](./menu/edit) and [tools](./toolbar/) to internal file items.