small ui improvements

This commit is contained in:
vcoppe
2024-09-06 13:36:36 +02:00
parent 25a3df5756
commit 71c88b15c6
6 changed files with 496 additions and 486 deletions

View File

@@ -64,11 +64,11 @@
<span class="mx-1">/</span> <span class="mx-1">/</span>
<WithUnits value={statistics.global.speed.total} type="speed" /> <WithUnits value={statistics.global.speed.total} type="speed" />
</span> </span>
<span slot="tooltip" <span slot="tooltip">
>{$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_( {$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_(
'quantities.moving' 'quantities.moving'
)} / {$_('quantities.total')})</span )} / {$_('quantities.total')})
> </span>
</Tooltip> </Tooltip>
{/if} {/if}
{#if panelSize > 160 || orientation === 'horizontal'} {#if panelSize > 160 || orientation === 'horizontal'}

View File

@@ -13,6 +13,7 @@
<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"
disabled={!validSelection} disabled={!validSelection}
on:click={async () => { on:click={async () => {
if ($map) { if ($map) {
@@ -20,7 +21,7 @@
} }
}} }}
> >
<MountainSnow size="16" class="mr-1" /> <MountainSnow size="16" class="mr-1 shrink-0" />
{$_('toolbar.elevation.button')} {$_('toolbar.elevation.button')}
</Button> </Button>
<Help link="./help/toolbar/elevation"> <Help link="./help/toolbar/elevation">

View File

@@ -1,88 +1,89 @@
<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 { _ } 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';
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"
disabled={(mergeType === MergeType.TRACES && !canMergeTraces) || class="whitespace-normal h-fit"
(mergeType === MergeType.CONTENTS && !canMergeContents)} disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
on:click={() => { (mergeType === MergeType.CONTENTS && !canMergeContents)}
dbUtils.mergeSelection(mergeType === MergeType.TRACES); on:click={() => {
}} dbUtils.mergeSelection(mergeType === MergeType.TRACES);
> }}
<Group size="16" class="mr-1" /> >
{$_('toolbar.merge.merge_selection')} <Group size="16" class="mr-1 shrink-0" />
</Button> {$_('toolbar.merge.merge_selection')}
<Help link="./help/toolbar/merge"> </Button>
{#if mergeType === MergeType.TRACES && canMergeTraces} <Help link="./help/toolbar/merge">
{$_('toolbar.merge.help_merge_traces')} {#if mergeType === MergeType.TRACES && canMergeTraces}
{:else if mergeType === MergeType.TRACES && !canMergeTraces} {$_('toolbar.merge.help_merge_traces')}
{$_('toolbar.merge.help_cannot_merge_traces')} {:else if mergeType === MergeType.TRACES && !canMergeTraces}
{:else if mergeType === MergeType.CONTENTS && canMergeContents} {$_('toolbar.merge.help_cannot_merge_traces')}
{$_('toolbar.merge.help_merge_contents')} {:else if mergeType === MergeType.CONTENTS && canMergeContents}
{:else if mergeType === MergeType.CONTENTS && !canMergeContents} {$_('toolbar.merge.help_merge_contents')}
{$_('toolbar.merge.help_cannot_merge_contents')} {:else if mergeType === MergeType.CONTENTS && !canMergeContents}
{/if} {$_('toolbar.merge.help_cannot_merge_contents')}
</Help> {/if}
</Help>
</div> </div>

View File

@@ -1,174 +1,183 @@
<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 { ListItem, ListRootItem, ListTrackSegmentItem } from '$lib/components/file-list/FileList'; import {
import Help from '$lib/components/Help.svelte'; ListItem,
import { Filter } from 'lucide-svelte'; ListRootItem,
import { _ } from 'svelte-i18n'; ListTrackSegmentItem
import WithUnits from '$lib/components/WithUnits.svelte'; } from '$lib/components/file-list/FileList';
import { dbUtils, fileObservers } from '$lib/db'; import Help from '$lib/components/Help.svelte';
import { map } from '$lib/stores'; import { Filter } from 'lucide-svelte';
import { onDestroy } from 'svelte'; import { _ } from 'svelte-i18n';
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx'; import WithUnits from '$lib/components/WithUnits.svelte';
import { derived } from 'svelte/store'; import { dbUtils, fileObservers } from '$lib/db';
import { map } from '$lib/stores';
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]) => [fs, sel]).subscribe( let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [
([fs, sel]) => { fs,
if (fs) { sel
fs.file.forEachSegment((segment, trackIndex, segmentIndex) => { ]).subscribe(([fs, sel]) => {
let segmentItem = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex); if (fs) {
if (sel.hasAnyParent(segmentItem)) { fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
let statistics = fs.statistics.getStatisticsFor(segmentItem); let segmentItem = new ListTrackSegmentItem(
simplified.set(segmentItem.getFullId(), [ fileId,
segmentItem, trackIndex,
statistics.local.points.length, segmentIndex
ramerDouglasPeucker(statistics.local.points, 1) );
]); if (sel.hasAnyParent(segmentItem)) {
update(); let statistics = fs.statistics.getStatisticsFor(segmentItem);
} else if (simplified.has(segmentItem.getFullId())) { simplified.set(segmentItem.getFullId(), [
simplified.delete(segmentItem.getFullId()); segmentItem,
update(); statistics.local.points.length,
} ramerDouglasPeucker(statistics.local.points, 1)
}); ]);
} update();
} } else if (simplified.has(segmentItem.getFullId())) {
); simplified.delete(segmentItem.getFullId());
unsubscribes.set(fileId, unsubscribe); update();
} }
}); });
} }
});
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} /> <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>{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="./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

@@ -293,11 +293,11 @@
</div> </div>
{/if} {/if}
</fieldset> </fieldset>
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2 items-center">
<Button <Button
variant="outline" variant="outline"
disabled={!canUpdate} disabled={!canUpdate}
class="grow" class="grow whitespace-normal h-fit"
on:click={() => { on:click={() => {
let effectiveSpeed = getSpeed(); let effectiveSpeed = getSpeed();
if ( if (
@@ -372,7 +372,7 @@
}); });
}} }}
> >
<CalendarClock size="16" class="mr-1" /> <CalendarClock size="16" class="mr-1 shrink-0" />
{$_('toolbar.time.update')} {$_('toolbar.time.update')}
</Button> </Button>
<Button variant="outline" on:click={setGPXData}> <Button variant="outline" on:click={setGPXData}>

View File

@@ -1,273 +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 { 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> <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> <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 flex-wrap gap-2"> <div class="flex flex-row gap-2 items-center">
<Button <Button
variant="outline" variant="outline"
disabled={!canCreate && !$selectedWaypoint} disabled={!canCreate && !$selectedWaypoint}
class="grow" class="grow whitespace-normal h-fit"
on:click={createOrUpdateWaypoint} on:click={createOrUpdateWaypoint}
> >
{#if $selectedWaypoint} {#if $selectedWaypoint}
<Save size="16" class="mr-1" /> <Save size="16" class="mr-1 shrink-0" />
{$_('menu.metadata.save')} {$_('menu.metadata.save')}
{:else} {:else}
<CirclePlus size="16" class="mr-1" /> <CirclePlus size="16" class="mr-1 shrink-0" />
{$_('toolbar.waypoint.create')} {$_('toolbar.waypoint.create')}
{/if} {/if}
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
class="ml-auto" 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="./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>