mirror of
				https://github.com/gpxstudio/gpx.studio.git
				synced 2025-11-04 05:21:09 +00:00 
			
		
		
		
	small ui improvements
This commit is contained in:
		@@ -64,11 +64,11 @@
 | 
			
		||||
                    <span class="mx-1">/</span>
 | 
			
		||||
                    <WithUnits value={statistics.global.speed.total} type="speed" />
 | 
			
		||||
                </span>
 | 
			
		||||
                <span slot="tooltip"
 | 
			
		||||
                    >{$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_(
 | 
			
		||||
                <span slot="tooltip">
 | 
			
		||||
                    {$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_(
 | 
			
		||||
                        'quantities.moving'
 | 
			
		||||
                    )} / {$_('quantities.total')})</span
 | 
			
		||||
                >
 | 
			
		||||
                    )} / {$_('quantities.total')})
 | 
			
		||||
                </span>
 | 
			
		||||
            </Tooltip>
 | 
			
		||||
        {/if}
 | 
			
		||||
        {#if panelSize > 160 || orientation === 'horizontal'}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@
 | 
			
		||||
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
 | 
			
		||||
    <Button
 | 
			
		||||
        variant="outline"
 | 
			
		||||
        class="whitespace-normal h-fit"
 | 
			
		||||
        disabled={!validSelection}
 | 
			
		||||
        on:click={async () => {
 | 
			
		||||
            if ($map) {
 | 
			
		||||
@@ -20,7 +21,7 @@
 | 
			
		||||
            }
 | 
			
		||||
        }}
 | 
			
		||||
    >
 | 
			
		||||
        <MountainSnow size="16" class="mr-1" />
 | 
			
		||||
        <MountainSnow size="16" class="mr-1 shrink-0" />
 | 
			
		||||
        {$_('toolbar.elevation.button')}
 | 
			
		||||
    </Button>
 | 
			
		||||
    <Help link="./help/toolbar/elevation">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,88 +1,89 @@
 | 
			
		||||
<script lang="ts" context="module">
 | 
			
		||||
	enum MergeType {
 | 
			
		||||
		TRACES = 'traces',
 | 
			
		||||
		CONTENTS = 'contents'
 | 
			
		||||
	}
 | 
			
		||||
    enum MergeType {
 | 
			
		||||
        TRACES = 'traces',
 | 
			
		||||
        CONTENTS = 'contents'
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { ListFileItem, ListTrackItem } from '$lib/components/file-list/FileList';
 | 
			
		||||
	import Help from '$lib/components/Help.svelte';
 | 
			
		||||
	import { selection } from '$lib/components/file-list/Selection';
 | 
			
		||||
	import { Button } from '$lib/components/ui/button';
 | 
			
		||||
	import { Label } from '$lib/components/ui/label/index.js';
 | 
			
		||||
	import * as RadioGroup from '$lib/components/ui/radio-group';
 | 
			
		||||
	import { _ } from 'svelte-i18n';
 | 
			
		||||
	import { dbUtils, getFile } from '$lib/db';
 | 
			
		||||
	import { Group } from 'lucide-svelte';
 | 
			
		||||
    import { ListFileItem, ListTrackItem } from '$lib/components/file-list/FileList';
 | 
			
		||||
    import Help from '$lib/components/Help.svelte';
 | 
			
		||||
    import { selection } from '$lib/components/file-list/Selection';
 | 
			
		||||
    import { Button } from '$lib/components/ui/button';
 | 
			
		||||
    import { Label } from '$lib/components/ui/label/index.js';
 | 
			
		||||
    import * as RadioGroup from '$lib/components/ui/radio-group';
 | 
			
		||||
    import { _ } from 'svelte-i18n';
 | 
			
		||||
    import { dbUtils, getFile } from '$lib/db';
 | 
			
		||||
    import { Group } from 'lucide-svelte';
 | 
			
		||||
 | 
			
		||||
	let canMergeTraces = false;
 | 
			
		||||
	let canMergeContents = false;
 | 
			
		||||
    let canMergeTraces = false;
 | 
			
		||||
    let canMergeContents = false;
 | 
			
		||||
 | 
			
		||||
	$: if ($selection.size > 1) {
 | 
			
		||||
		canMergeTraces = true;
 | 
			
		||||
	} else if ($selection.size === 1) {
 | 
			
		||||
		let selected = $selection.getSelected()[0];
 | 
			
		||||
		if (selected instanceof ListFileItem) {
 | 
			
		||||
			let file = getFile(selected.getFileId());
 | 
			
		||||
			if (file) {
 | 
			
		||||
				canMergeTraces = file.getSegments().length > 1;
 | 
			
		||||
			} else {
 | 
			
		||||
				canMergeTraces = false;
 | 
			
		||||
			}
 | 
			
		||||
		} else if (selected instanceof ListTrackItem) {
 | 
			
		||||
			let trackIndex = selected.getTrackIndex();
 | 
			
		||||
			let file = getFile(selected.getFileId());
 | 
			
		||||
			if (file && trackIndex < file.trk.length) {
 | 
			
		||||
				canMergeTraces = file.trk[trackIndex].getSegments().length > 1;
 | 
			
		||||
			} else {
 | 
			
		||||
				canMergeTraces = false;
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			canMergeContents = false;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
    $: if ($selection.size > 1) {
 | 
			
		||||
        canMergeTraces = true;
 | 
			
		||||
    } else if ($selection.size === 1) {
 | 
			
		||||
        let selected = $selection.getSelected()[0];
 | 
			
		||||
        if (selected instanceof ListFileItem) {
 | 
			
		||||
            let file = getFile(selected.getFileId());
 | 
			
		||||
            if (file) {
 | 
			
		||||
                canMergeTraces = file.getSegments().length > 1;
 | 
			
		||||
            } else {
 | 
			
		||||
                canMergeTraces = false;
 | 
			
		||||
            }
 | 
			
		||||
        } else if (selected instanceof ListTrackItem) {
 | 
			
		||||
            let trackIndex = selected.getTrackIndex();
 | 
			
		||||
            let file = getFile(selected.getFileId());
 | 
			
		||||
            if (file && trackIndex < file.trk.length) {
 | 
			
		||||
                canMergeTraces = file.trk[trackIndex].getSegments().length > 1;
 | 
			
		||||
            } else {
 | 
			
		||||
                canMergeTraces = false;
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            canMergeContents = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	$: canMergeContents =
 | 
			
		||||
		$selection.size > 1 &&
 | 
			
		||||
		$selection
 | 
			
		||||
			.getSelected()
 | 
			
		||||
			.some((item) => item instanceof ListFileItem || item instanceof ListTrackItem);
 | 
			
		||||
    $: canMergeContents =
 | 
			
		||||
        $selection.size > 1 &&
 | 
			
		||||
        $selection
 | 
			
		||||
            .getSelected()
 | 
			
		||||
            .some((item) => item instanceof ListFileItem || item instanceof ListTrackItem);
 | 
			
		||||
 | 
			
		||||
	let mergeType = MergeType.TRACES;
 | 
			
		||||
    let mergeType = MergeType.TRACES;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
 | 
			
		||||
	<RadioGroup.Root bind:value={mergeType}>
 | 
			
		||||
		<Label class="flex flex-row items-center gap-2 leading-5">
 | 
			
		||||
			<RadioGroup.Item value={MergeType.TRACES} />
 | 
			
		||||
			{$_('toolbar.merge.merge_traces')}
 | 
			
		||||
		</Label>
 | 
			
		||||
		<Label class="flex flex-row items-center gap-2 leading-5">
 | 
			
		||||
			<RadioGroup.Item value={MergeType.CONTENTS} />
 | 
			
		||||
			{$_('toolbar.merge.merge_contents')}
 | 
			
		||||
		</Label>
 | 
			
		||||
	</RadioGroup.Root>
 | 
			
		||||
	<Button
 | 
			
		||||
		variant="outline"
 | 
			
		||||
		disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
 | 
			
		||||
			(mergeType === MergeType.CONTENTS && !canMergeContents)}
 | 
			
		||||
		on:click={() => {
 | 
			
		||||
			dbUtils.mergeSelection(mergeType === MergeType.TRACES);
 | 
			
		||||
		}}
 | 
			
		||||
	>
 | 
			
		||||
		<Group size="16" class="mr-1" />
 | 
			
		||||
		{$_('toolbar.merge.merge_selection')}
 | 
			
		||||
	</Button>
 | 
			
		||||
	<Help link="./help/toolbar/merge">
 | 
			
		||||
		{#if mergeType === MergeType.TRACES && canMergeTraces}
 | 
			
		||||
			{$_('toolbar.merge.help_merge_traces')}
 | 
			
		||||
		{:else if mergeType === MergeType.TRACES && !canMergeTraces}
 | 
			
		||||
			{$_('toolbar.merge.help_cannot_merge_traces')}
 | 
			
		||||
		{:else if mergeType === MergeType.CONTENTS && canMergeContents}
 | 
			
		||||
			{$_('toolbar.merge.help_merge_contents')}
 | 
			
		||||
		{:else if mergeType === MergeType.CONTENTS && !canMergeContents}
 | 
			
		||||
			{$_('toolbar.merge.help_cannot_merge_contents')}
 | 
			
		||||
		{/if}
 | 
			
		||||
	</Help>
 | 
			
		||||
    <RadioGroup.Root bind:value={mergeType}>
 | 
			
		||||
        <Label class="flex flex-row items-center gap-2 leading-5">
 | 
			
		||||
            <RadioGroup.Item value={MergeType.TRACES} />
 | 
			
		||||
            {$_('toolbar.merge.merge_traces')}
 | 
			
		||||
        </Label>
 | 
			
		||||
        <Label class="flex flex-row items-center gap-2 leading-5">
 | 
			
		||||
            <RadioGroup.Item value={MergeType.CONTENTS} />
 | 
			
		||||
            {$_('toolbar.merge.merge_contents')}
 | 
			
		||||
        </Label>
 | 
			
		||||
    </RadioGroup.Root>
 | 
			
		||||
    <Button
 | 
			
		||||
        variant="outline"
 | 
			
		||||
        class="whitespace-normal h-fit"
 | 
			
		||||
        disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
 | 
			
		||||
            (mergeType === MergeType.CONTENTS && !canMergeContents)}
 | 
			
		||||
        on:click={() => {
 | 
			
		||||
            dbUtils.mergeSelection(mergeType === MergeType.TRACES);
 | 
			
		||||
        }}
 | 
			
		||||
    >
 | 
			
		||||
        <Group size="16" class="mr-1 shrink-0" />
 | 
			
		||||
        {$_('toolbar.merge.merge_selection')}
 | 
			
		||||
    </Button>
 | 
			
		||||
    <Help link="./help/toolbar/merge">
 | 
			
		||||
        {#if mergeType === MergeType.TRACES && canMergeTraces}
 | 
			
		||||
            {$_('toolbar.merge.help_merge_traces')}
 | 
			
		||||
        {:else if mergeType === MergeType.TRACES && !canMergeTraces}
 | 
			
		||||
            {$_('toolbar.merge.help_cannot_merge_traces')}
 | 
			
		||||
        {:else if mergeType === MergeType.CONTENTS && canMergeContents}
 | 
			
		||||
            {$_('toolbar.merge.help_merge_contents')}
 | 
			
		||||
        {:else if mergeType === MergeType.CONTENTS && !canMergeContents}
 | 
			
		||||
            {$_('toolbar.merge.help_cannot_merge_contents')}
 | 
			
		||||
        {/if}
 | 
			
		||||
    </Help>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,174 +1,183 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { Label } from '$lib/components/ui/label/index.js';
 | 
			
		||||
	import { Button } from '$lib/components/ui/button';
 | 
			
		||||
	import { Slider } from '$lib/components/ui/slider';
 | 
			
		||||
	import { selection } from '$lib/components/file-list/Selection';
 | 
			
		||||
	import { ListItem, ListRootItem, ListTrackSegmentItem } from '$lib/components/file-list/FileList';
 | 
			
		||||
	import Help from '$lib/components/Help.svelte';
 | 
			
		||||
	import { Filter } from 'lucide-svelte';
 | 
			
		||||
	import { _ } from 'svelte-i18n';
 | 
			
		||||
	import WithUnits from '$lib/components/WithUnits.svelte';
 | 
			
		||||
	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';
 | 
			
		||||
    import { Label } from '$lib/components/ui/label/index.js';
 | 
			
		||||
    import { Button } from '$lib/components/ui/button';
 | 
			
		||||
    import { Slider } from '$lib/components/ui/slider';
 | 
			
		||||
    import { selection } from '$lib/components/file-list/Selection';
 | 
			
		||||
    import {
 | 
			
		||||
        ListItem,
 | 
			
		||||
        ListRootItem,
 | 
			
		||||
        ListTrackSegmentItem
 | 
			
		||||
    } from '$lib/components/file-list/FileList';
 | 
			
		||||
    import Help from '$lib/components/Help.svelte';
 | 
			
		||||
    import { Filter } from 'lucide-svelte';
 | 
			
		||||
    import { _ } from 'svelte-i18n';
 | 
			
		||||
    import WithUnits from '$lib/components/WithUnits.svelte';
 | 
			
		||||
    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 maxPoints = 0;
 | 
			
		||||
	let currentPoints = 0;
 | 
			
		||||
    let sliderValue = [50];
 | 
			
		||||
    let maxPoints = 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 unsubscribes = new Map<string, () => void>();
 | 
			
		||||
    let simplified = new Map<string, [ListItem, number, SimplifiedTrackPoint[]]>();
 | 
			
		||||
    let unsubscribes = new Map<string, () => void>();
 | 
			
		||||
 | 
			
		||||
	function update() {
 | 
			
		||||
		maxPoints = 0;
 | 
			
		||||
		currentPoints = 0;
 | 
			
		||||
    function update() {
 | 
			
		||||
        maxPoints = 0;
 | 
			
		||||
        currentPoints = 0;
 | 
			
		||||
 | 
			
		||||
		let data: GeoJSON.FeatureCollection = {
 | 
			
		||||
			type: 'FeatureCollection',
 | 
			
		||||
			features: []
 | 
			
		||||
		};
 | 
			
		||||
        let data: GeoJSON.FeatureCollection = {
 | 
			
		||||
            type: 'FeatureCollection',
 | 
			
		||||
            features: []
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
		simplified.forEach(([item, maxPts, points], itemFullId) => {
 | 
			
		||||
			maxPoints += maxPts;
 | 
			
		||||
        simplified.forEach(([item, maxPts, points], itemFullId) => {
 | 
			
		||||
            maxPoints += maxPts;
 | 
			
		||||
 | 
			
		||||
			let current = points.filter(
 | 
			
		||||
				(point) => point.distance === undefined || point.distance >= tolerance
 | 
			
		||||
			);
 | 
			
		||||
			currentPoints += current.length;
 | 
			
		||||
            let current = points.filter(
 | 
			
		||||
                (point) => point.distance === undefined || point.distance >= tolerance
 | 
			
		||||
            );
 | 
			
		||||
            currentPoints += current.length;
 | 
			
		||||
 | 
			
		||||
			data.features.push({
 | 
			
		||||
				type: 'Feature',
 | 
			
		||||
				geometry: {
 | 
			
		||||
					type: 'LineString',
 | 
			
		||||
					coordinates: current.map((point) => [
 | 
			
		||||
						point.point.getLongitude(),
 | 
			
		||||
						point.point.getLatitude()
 | 
			
		||||
					])
 | 
			
		||||
				},
 | 
			
		||||
				properties: {}
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
            data.features.push({
 | 
			
		||||
                type: 'Feature',
 | 
			
		||||
                geometry: {
 | 
			
		||||
                    type: 'LineString',
 | 
			
		||||
                    coordinates: current.map((point) => [
 | 
			
		||||
                        point.point.getLongitude(),
 | 
			
		||||
                        point.point.getLatitude()
 | 
			
		||||
                    ])
 | 
			
		||||
                },
 | 
			
		||||
                properties: {}
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
		if ($map) {
 | 
			
		||||
			let source = $map.getSource('simplified');
 | 
			
		||||
			if (source) {
 | 
			
		||||
				source.setData(data);
 | 
			
		||||
			} else {
 | 
			
		||||
				$map.addSource('simplified', {
 | 
			
		||||
					type: 'geojson',
 | 
			
		||||
					data: data
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
			if (!$map.getLayer('simplified')) {
 | 
			
		||||
				$map.addLayer({
 | 
			
		||||
					id: 'simplified',
 | 
			
		||||
					type: 'line',
 | 
			
		||||
					source: 'simplified',
 | 
			
		||||
					paint: {
 | 
			
		||||
						'line-color': 'white',
 | 
			
		||||
						'line-width': 3
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				$map.moveLayer('simplified');
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
        if ($map) {
 | 
			
		||||
            let source = $map.getSource('simplified');
 | 
			
		||||
            if (source) {
 | 
			
		||||
                source.setData(data);
 | 
			
		||||
            } else {
 | 
			
		||||
                $map.addSource('simplified', {
 | 
			
		||||
                    type: 'geojson',
 | 
			
		||||
                    data: data
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            if (!$map.getLayer('simplified')) {
 | 
			
		||||
                $map.addLayer({
 | 
			
		||||
                    id: 'simplified',
 | 
			
		||||
                    type: 'line',
 | 
			
		||||
                    source: 'simplified',
 | 
			
		||||
                    paint: {
 | 
			
		||||
                        'line-color': 'white',
 | 
			
		||||
                        'line-width': 3
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            } else {
 | 
			
		||||
                $map.moveLayer('simplified');
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	$: if ($fileObservers) {
 | 
			
		||||
		unsubscribes.forEach((unsubscribe, fileId) => {
 | 
			
		||||
			if (!$fileObservers.has(fileId)) {
 | 
			
		||||
				unsubscribe();
 | 
			
		||||
				unsubscribes.delete(fileId);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
		$fileObservers.forEach((fileStore, fileId) => {
 | 
			
		||||
			if (!unsubscribes.has(fileId)) {
 | 
			
		||||
				let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [fs, sel]).subscribe(
 | 
			
		||||
					([fs, sel]) => {
 | 
			
		||||
						if (fs) {
 | 
			
		||||
							fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
 | 
			
		||||
								let segmentItem = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex);
 | 
			
		||||
								if (sel.hasAnyParent(segmentItem)) {
 | 
			
		||||
									let statistics = fs.statistics.getStatisticsFor(segmentItem);
 | 
			
		||||
									simplified.set(segmentItem.getFullId(), [
 | 
			
		||||
										segmentItem,
 | 
			
		||||
										statistics.local.points.length,
 | 
			
		||||
										ramerDouglasPeucker(statistics.local.points, 1)
 | 
			
		||||
									]);
 | 
			
		||||
									update();
 | 
			
		||||
								} else if (simplified.has(segmentItem.getFullId())) {
 | 
			
		||||
									simplified.delete(segmentItem.getFullId());
 | 
			
		||||
									update();
 | 
			
		||||
								}
 | 
			
		||||
							});
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				);
 | 
			
		||||
				unsubscribes.set(fileId, unsubscribe);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
    $: if ($fileObservers) {
 | 
			
		||||
        unsubscribes.forEach((unsubscribe, fileId) => {
 | 
			
		||||
            if (!$fileObservers.has(fileId)) {
 | 
			
		||||
                unsubscribe();
 | 
			
		||||
                unsubscribes.delete(fileId);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        $fileObservers.forEach((fileStore, fileId) => {
 | 
			
		||||
            if (!unsubscribes.has(fileId)) {
 | 
			
		||||
                let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [
 | 
			
		||||
                    fs,
 | 
			
		||||
                    sel
 | 
			
		||||
                ]).subscribe(([fs, sel]) => {
 | 
			
		||||
                    if (fs) {
 | 
			
		||||
                        fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
 | 
			
		||||
                            let segmentItem = new ListTrackSegmentItem(
 | 
			
		||||
                                fileId,
 | 
			
		||||
                                trackIndex,
 | 
			
		||||
                                segmentIndex
 | 
			
		||||
                            );
 | 
			
		||||
                            if (sel.hasAnyParent(segmentItem)) {
 | 
			
		||||
                                let statistics = fs.statistics.getStatisticsFor(segmentItem);
 | 
			
		||||
                                simplified.set(segmentItem.getFullId(), [
 | 
			
		||||
                                    segmentItem,
 | 
			
		||||
                                    statistics.local.points.length,
 | 
			
		||||
                                    ramerDouglasPeucker(statistics.local.points, 1)
 | 
			
		||||
                                ]);
 | 
			
		||||
                                update();
 | 
			
		||||
                            } else if (simplified.has(segmentItem.getFullId())) {
 | 
			
		||||
                                simplified.delete(segmentItem.getFullId());
 | 
			
		||||
                                update();
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
                unsubscribes.set(fileId, unsubscribe);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	$: if (tolerance) {
 | 
			
		||||
		update();
 | 
			
		||||
	}
 | 
			
		||||
    $: if (tolerance) {
 | 
			
		||||
        update();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	onDestroy(() => {
 | 
			
		||||
		if ($map) {
 | 
			
		||||
			if ($map.getLayer('simplified')) {
 | 
			
		||||
				$map.removeLayer('simplified');
 | 
			
		||||
			}
 | 
			
		||||
			if ($map.getSource('simplified')) {
 | 
			
		||||
				$map.removeSource('simplified');
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		unsubscribes.forEach((unsubscribe) => unsubscribe());
 | 
			
		||||
		simplified.clear();
 | 
			
		||||
	});
 | 
			
		||||
    onDestroy(() => {
 | 
			
		||||
        if ($map) {
 | 
			
		||||
            if ($map.getLayer('simplified')) {
 | 
			
		||||
                $map.removeLayer('simplified');
 | 
			
		||||
            }
 | 
			
		||||
            if ($map.getSource('simplified')) {
 | 
			
		||||
                $map.removeSource('simplified');
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        unsubscribes.forEach((unsubscribe) => unsubscribe());
 | 
			
		||||
        simplified.clear();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
	function reduce() {
 | 
			
		||||
		let itemsAndPoints = new Map<ListItem, TrackPoint[]>();
 | 
			
		||||
		simplified.forEach(([item, maxPts, points], itemFullId) => {
 | 
			
		||||
			itemsAndPoints.set(
 | 
			
		||||
				item,
 | 
			
		||||
				points
 | 
			
		||||
					.filter((point) => point.distance === undefined || point.distance >= tolerance)
 | 
			
		||||
					.map((point) => point.point)
 | 
			
		||||
			);
 | 
			
		||||
		});
 | 
			
		||||
		dbUtils.reduce(itemsAndPoints);
 | 
			
		||||
	}
 | 
			
		||||
    function reduce() {
 | 
			
		||||
        let itemsAndPoints = new Map<ListItem, TrackPoint[]>();
 | 
			
		||||
        simplified.forEach(([item, maxPts, points], itemFullId) => {
 | 
			
		||||
            itemsAndPoints.set(
 | 
			
		||||
                item,
 | 
			
		||||
                points
 | 
			
		||||
                    .filter((point) => point.distance === undefined || point.distance >= tolerance)
 | 
			
		||||
                    .map((point) => point.point)
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
        dbUtils.reduce(itemsAndPoints);
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
 | 
			
		||||
	<div class="p-2">
 | 
			
		||||
		<Slider bind:value={sliderValue} min={0} max={100} step={1} />
 | 
			
		||||
	</div>
 | 
			
		||||
	<Label class="flex flex-row justify-between">
 | 
			
		||||
		<span>{$_('toolbar.reduce.tolerance')}</span>
 | 
			
		||||
		<WithUnits value={tolerance / 1000} type="distance" decimals={3} />
 | 
			
		||||
	</Label>
 | 
			
		||||
	<Label class="flex flex-row justify-between">
 | 
			
		||||
		<span>{$_('toolbar.reduce.number_of_points')}</span>
 | 
			
		||||
		<span>{currentPoints}/{maxPoints}</span>
 | 
			
		||||
	</Label>
 | 
			
		||||
	<Button variant="outline" disabled={!validSelection} on:click={reduce}>
 | 
			
		||||
		<Filter size="16" class="mr-1" />
 | 
			
		||||
		{$_('toolbar.reduce.button')}
 | 
			
		||||
	</Button>
 | 
			
		||||
    <div class="p-2">
 | 
			
		||||
        <Slider bind:value={sliderValue} min={0} max={100} step={1} />
 | 
			
		||||
    </div>
 | 
			
		||||
    <Label class="flex flex-row justify-between">
 | 
			
		||||
        <span>{$_('toolbar.reduce.tolerance')}</span>
 | 
			
		||||
        <WithUnits value={tolerance / 1000} type="distance" decimals={3} class="font-normal" />
 | 
			
		||||
    </Label>
 | 
			
		||||
    <Label class="flex flex-row justify-between">
 | 
			
		||||
        <span>{$_('toolbar.reduce.number_of_points')}</span>
 | 
			
		||||
        <span class="font-normal">{currentPoints}/{maxPoints}</span>
 | 
			
		||||
    </Label>
 | 
			
		||||
    <Button variant="outline" disabled={!validSelection} on:click={reduce}>
 | 
			
		||||
        <Filter size="16" class="mr-1" />
 | 
			
		||||
        {$_('toolbar.reduce.button')}
 | 
			
		||||
    </Button>
 | 
			
		||||
 | 
			
		||||
	<Help link="./help/toolbar/minify">
 | 
			
		||||
		{#if validSelection}
 | 
			
		||||
			{$_('toolbar.reduce.help')}
 | 
			
		||||
		{:else}
 | 
			
		||||
			{$_('toolbar.reduce.help_no_selection')}
 | 
			
		||||
		{/if}
 | 
			
		||||
	</Help>
 | 
			
		||||
    <Help link="./help/toolbar/minify">
 | 
			
		||||
        {#if validSelection}
 | 
			
		||||
            {$_('toolbar.reduce.help')}
 | 
			
		||||
        {:else}
 | 
			
		||||
            {$_('toolbar.reduce.help_no_selection')}
 | 
			
		||||
        {/if}
 | 
			
		||||
    </Help>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -293,11 +293,11 @@
 | 
			
		||||
            </div>
 | 
			
		||||
        {/if}
 | 
			
		||||
    </fieldset>
 | 
			
		||||
    <div class="flex flex-row gap-2">
 | 
			
		||||
    <div class="flex flex-row gap-2 items-center">
 | 
			
		||||
        <Button
 | 
			
		||||
            variant="outline"
 | 
			
		||||
            disabled={!canUpdate}
 | 
			
		||||
            class="grow"
 | 
			
		||||
            class="grow whitespace-normal h-fit"
 | 
			
		||||
            on:click={() => {
 | 
			
		||||
                let effectiveSpeed = getSpeed();
 | 
			
		||||
                if (
 | 
			
		||||
@@ -372,7 +372,7 @@
 | 
			
		||||
                });
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <CalendarClock size="16" class="mr-1" />
 | 
			
		||||
            <CalendarClock size="16" class="mr-1 shrink-0" />
 | 
			
		||||
            {$_('toolbar.time.update')}
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button variant="outline" on:click={setGPXData}>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,273 +1,272 @@
 | 
			
		||||
<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 lang="ts">
 | 
			
		||||
	import { Input } from '$lib/components/ui/input';
 | 
			
		||||
	import { Textarea } from '$lib/components/ui/textarea';
 | 
			
		||||
	import { Label } from '$lib/components/ui/label/index.js';
 | 
			
		||||
	import { Button } from '$lib/components/ui/button';
 | 
			
		||||
	import * as Select from '$lib/components/ui/select';
 | 
			
		||||
	import { selection } from '$lib/components/file-list/Selection';
 | 
			
		||||
	import { Waypoint } from 'gpx';
 | 
			
		||||
	import { _, locale } from 'svelte-i18n';
 | 
			
		||||
	import { ListWaypointItem } from '$lib/components/file-list/FileList';
 | 
			
		||||
	import { dbUtils, fileObservers, getFile, settings, type GPXFileWithStatistics } from '$lib/db';
 | 
			
		||||
	import { get } from 'svelte/store';
 | 
			
		||||
	import Help from '$lib/components/Help.svelte';
 | 
			
		||||
	import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
	import { map } from '$lib/stores';
 | 
			
		||||
	import { resetCursor, setCrosshairCursor } from '$lib/utils';
 | 
			
		||||
	import { CirclePlus, CircleX, Save } from 'lucide-svelte';
 | 
			
		||||
	import { getSymbolKey, symbols } from '$lib/assets/symbols';
 | 
			
		||||
    import { Input } from '$lib/components/ui/input';
 | 
			
		||||
    import { Textarea } from '$lib/components/ui/textarea';
 | 
			
		||||
    import { Label } from '$lib/components/ui/label/index.js';
 | 
			
		||||
    import { Button } from '$lib/components/ui/button';
 | 
			
		||||
    import * as Select from '$lib/components/ui/select';
 | 
			
		||||
    import { selection } from '$lib/components/file-list/Selection';
 | 
			
		||||
    import { Waypoint } from 'gpx';
 | 
			
		||||
    import { _, locale } from 'svelte-i18n';
 | 
			
		||||
    import { ListWaypointItem } from '$lib/components/file-list/FileList';
 | 
			
		||||
    import { dbUtils, fileObservers, getFile, settings, type GPXFileWithStatistics } from '$lib/db';
 | 
			
		||||
    import { get } from 'svelte/store';
 | 
			
		||||
    import Help from '$lib/components/Help.svelte';
 | 
			
		||||
    import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
    import { map } from '$lib/stores';
 | 
			
		||||
    import { resetCursor, setCrosshairCursor } from '$lib/utils';
 | 
			
		||||
    import { CirclePlus, CircleX, Save } from 'lucide-svelte';
 | 
			
		||||
    import { getSymbolKey, symbols } from '$lib/assets/symbols';
 | 
			
		||||
 | 
			
		||||
	let name: string;
 | 
			
		||||
	let description: string;
 | 
			
		||||
	let link: string;
 | 
			
		||||
	let longitude: number;
 | 
			
		||||
	let latitude: number;
 | 
			
		||||
    let name: string;
 | 
			
		||||
    let description: string;
 | 
			
		||||
    let link: string;
 | 
			
		||||
    let longitude: number;
 | 
			
		||||
    let latitude: number;
 | 
			
		||||
 | 
			
		||||
	let selectedSymbol = {
 | 
			
		||||
		value: '',
 | 
			
		||||
		label: ''
 | 
			
		||||
	};
 | 
			
		||||
    let selectedSymbol = {
 | 
			
		||||
        value: '',
 | 
			
		||||
        label: ''
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
	const { verticalFileView } = settings;
 | 
			
		||||
    const { verticalFileView } = settings;
 | 
			
		||||
 | 
			
		||||
	$: canCreate = $selection.size > 0;
 | 
			
		||||
    $: canCreate = $selection.size > 0;
 | 
			
		||||
 | 
			
		||||
	$: if ($verticalFileView && $selection) {
 | 
			
		||||
		selectedWaypoint.update(() => {
 | 
			
		||||
			if ($selection.size === 1) {
 | 
			
		||||
				let item = $selection.getSelected()[0];
 | 
			
		||||
				if (item instanceof ListWaypointItem) {
 | 
			
		||||
					let file = getFile(item.getFileId());
 | 
			
		||||
					let waypoint = file?.wpt[item.getWaypointIndex()];
 | 
			
		||||
					if (waypoint) {
 | 
			
		||||
						return [waypoint, item.getFileId()];
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			return undefined;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
    $: if ($verticalFileView && $selection) {
 | 
			
		||||
        selectedWaypoint.update(() => {
 | 
			
		||||
            if ($selection.size === 1) {
 | 
			
		||||
                let item = $selection.getSelected()[0];
 | 
			
		||||
                if (item instanceof ListWaypointItem) {
 | 
			
		||||
                    let file = getFile(item.getFileId());
 | 
			
		||||
                    let waypoint = file?.wpt[item.getWaypointIndex()];
 | 
			
		||||
                    if (waypoint) {
 | 
			
		||||
                        return [waypoint, item.getFileId()];
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return undefined;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	let unsubscribe: (() => void) | undefined = undefined;
 | 
			
		||||
	function updateWaypointData(fileStore: GPXFileWithStatistics | undefined) {
 | 
			
		||||
		if ($selectedWaypoint) {
 | 
			
		||||
			if (fileStore) {
 | 
			
		||||
				if ($selectedWaypoint[0]._data.index < fileStore.file.wpt.length) {
 | 
			
		||||
					$selectedWaypoint[0] = fileStore.file.wpt[$selectedWaypoint[0]._data.index];
 | 
			
		||||
					name = $selectedWaypoint[0].name ?? '';
 | 
			
		||||
					description = $selectedWaypoint[0].desc ?? '';
 | 
			
		||||
					if (
 | 
			
		||||
						$selectedWaypoint[0].cmt !== undefined &&
 | 
			
		||||
						$selectedWaypoint[0].cmt !== $selectedWaypoint[0].desc
 | 
			
		||||
					) {
 | 
			
		||||
						description += '\n\n' + $selectedWaypoint[0].cmt;
 | 
			
		||||
					}
 | 
			
		||||
					link = $selectedWaypoint[0].link?.attributes?.href ?? '';
 | 
			
		||||
					let symbol = $selectedWaypoint[0].sym ?? '';
 | 
			
		||||
					let symbolKey = getSymbolKey(symbol);
 | 
			
		||||
					if (symbolKey) {
 | 
			
		||||
						selectedSymbol = {
 | 
			
		||||
							value: symbol,
 | 
			
		||||
							label: $_(`gpx.symbol.${symbolKey}`)
 | 
			
		||||
						};
 | 
			
		||||
					} else {
 | 
			
		||||
						selectedSymbol = {
 | 
			
		||||
							value: symbol,
 | 
			
		||||
							label: ''
 | 
			
		||||
						};
 | 
			
		||||
					}
 | 
			
		||||
					longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
 | 
			
		||||
					latitude = parseFloat($selectedWaypoint[0].getLatitude().toFixed(6));
 | 
			
		||||
				} else {
 | 
			
		||||
					selectedWaypoint.set(undefined);
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				selectedWaypoint.set(undefined);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
    let unsubscribe: (() => void) | undefined = undefined;
 | 
			
		||||
    function updateWaypointData(fileStore: GPXFileWithStatistics | undefined) {
 | 
			
		||||
        if ($selectedWaypoint) {
 | 
			
		||||
            if (fileStore) {
 | 
			
		||||
                if ($selectedWaypoint[0]._data.index < fileStore.file.wpt.length) {
 | 
			
		||||
                    $selectedWaypoint[0] = fileStore.file.wpt[$selectedWaypoint[0]._data.index];
 | 
			
		||||
                    name = $selectedWaypoint[0].name ?? '';
 | 
			
		||||
                    description = $selectedWaypoint[0].desc ?? '';
 | 
			
		||||
                    if (
 | 
			
		||||
                        $selectedWaypoint[0].cmt !== undefined &&
 | 
			
		||||
                        $selectedWaypoint[0].cmt !== $selectedWaypoint[0].desc
 | 
			
		||||
                    ) {
 | 
			
		||||
                        description += '\n\n' + $selectedWaypoint[0].cmt;
 | 
			
		||||
                    }
 | 
			
		||||
                    link = $selectedWaypoint[0].link?.attributes?.href ?? '';
 | 
			
		||||
                    let symbol = $selectedWaypoint[0].sym ?? '';
 | 
			
		||||
                    let symbolKey = getSymbolKey(symbol);
 | 
			
		||||
                    if (symbolKey) {
 | 
			
		||||
                        selectedSymbol = {
 | 
			
		||||
                            value: symbol,
 | 
			
		||||
                            label: $_(`gpx.symbol.${symbolKey}`)
 | 
			
		||||
                        };
 | 
			
		||||
                    } else {
 | 
			
		||||
                        selectedSymbol = {
 | 
			
		||||
                            value: symbol,
 | 
			
		||||
                            label: ''
 | 
			
		||||
                        };
 | 
			
		||||
                    }
 | 
			
		||||
                    longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
 | 
			
		||||
                    latitude = parseFloat($selectedWaypoint[0].getLatitude().toFixed(6));
 | 
			
		||||
                } else {
 | 
			
		||||
                    selectedWaypoint.set(undefined);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                selectedWaypoint.set(undefined);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	function resetWaypointData() {
 | 
			
		||||
		name = '';
 | 
			
		||||
		description = '';
 | 
			
		||||
		link = '';
 | 
			
		||||
		selectedSymbol = {
 | 
			
		||||
			value: '',
 | 
			
		||||
			label: ''
 | 
			
		||||
		};
 | 
			
		||||
		longitude = 0;
 | 
			
		||||
		latitude = 0;
 | 
			
		||||
	}
 | 
			
		||||
    function resetWaypointData() {
 | 
			
		||||
        name = '';
 | 
			
		||||
        description = '';
 | 
			
		||||
        link = '';
 | 
			
		||||
        selectedSymbol = {
 | 
			
		||||
            value: '',
 | 
			
		||||
            label: ''
 | 
			
		||||
        };
 | 
			
		||||
        longitude = 0;
 | 
			
		||||
        latitude = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	$: {
 | 
			
		||||
		if (unsubscribe) {
 | 
			
		||||
			unsubscribe();
 | 
			
		||||
			unsubscribe = undefined;
 | 
			
		||||
		}
 | 
			
		||||
		if ($selectedWaypoint) {
 | 
			
		||||
			let fileStore = get(fileObservers).get($selectedWaypoint[1]);
 | 
			
		||||
			if (fileStore) {
 | 
			
		||||
				unsubscribe = fileStore.subscribe(updateWaypointData);
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			resetWaypointData();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
    $: {
 | 
			
		||||
        if (unsubscribe) {
 | 
			
		||||
            unsubscribe();
 | 
			
		||||
            unsubscribe = undefined;
 | 
			
		||||
        }
 | 
			
		||||
        if ($selectedWaypoint) {
 | 
			
		||||
            let fileStore = get(fileObservers).get($selectedWaypoint[1]);
 | 
			
		||||
            if (fileStore) {
 | 
			
		||||
                unsubscribe = fileStore.subscribe(updateWaypointData);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            resetWaypointData();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	function createOrUpdateWaypoint() {
 | 
			
		||||
		if (typeof latitude === 'string') {
 | 
			
		||||
			latitude = parseFloat(latitude);
 | 
			
		||||
		}
 | 
			
		||||
		if (typeof longitude === 'string') {
 | 
			
		||||
			longitude = parseFloat(longitude);
 | 
			
		||||
		}
 | 
			
		||||
		latitude = parseFloat(latitude.toFixed(6));
 | 
			
		||||
		longitude = parseFloat(longitude.toFixed(6));
 | 
			
		||||
    function createOrUpdateWaypoint() {
 | 
			
		||||
        if (typeof latitude === 'string') {
 | 
			
		||||
            latitude = parseFloat(latitude);
 | 
			
		||||
        }
 | 
			
		||||
        if (typeof longitude === 'string') {
 | 
			
		||||
            longitude = parseFloat(longitude);
 | 
			
		||||
        }
 | 
			
		||||
        latitude = parseFloat(latitude.toFixed(6));
 | 
			
		||||
        longitude = parseFloat(longitude.toFixed(6));
 | 
			
		||||
 | 
			
		||||
		dbUtils.addOrUpdateWaypoint(
 | 
			
		||||
			{
 | 
			
		||||
				attributes: {
 | 
			
		||||
					lat: latitude,
 | 
			
		||||
					lon: longitude
 | 
			
		||||
				},
 | 
			
		||||
				name: name.length > 0 ? name : undefined,
 | 
			
		||||
				desc: description.length > 0 ? description : undefined,
 | 
			
		||||
				cmt: description.length > 0 ? description : undefined,
 | 
			
		||||
				link: link.length > 0 ? { attributes: { href: link } } : undefined,
 | 
			
		||||
				sym: selectedSymbol.value.length > 0 ? selectedSymbol.value : undefined
 | 
			
		||||
			},
 | 
			
		||||
			$selectedWaypoint
 | 
			
		||||
				? new ListWaypointItem($selectedWaypoint[1], $selectedWaypoint[0]._data.index)
 | 
			
		||||
				: undefined
 | 
			
		||||
		);
 | 
			
		||||
        dbUtils.addOrUpdateWaypoint(
 | 
			
		||||
            {
 | 
			
		||||
                attributes: {
 | 
			
		||||
                    lat: latitude,
 | 
			
		||||
                    lon: longitude
 | 
			
		||||
                },
 | 
			
		||||
                name: name.length > 0 ? name : undefined,
 | 
			
		||||
                desc: description.length > 0 ? description : undefined,
 | 
			
		||||
                cmt: description.length > 0 ? description : undefined,
 | 
			
		||||
                link: link.length > 0 ? { attributes: { href: link } } : undefined,
 | 
			
		||||
                sym: selectedSymbol.value.length > 0 ? selectedSymbol.value : undefined
 | 
			
		||||
            },
 | 
			
		||||
            $selectedWaypoint
 | 
			
		||||
                ? new ListWaypointItem($selectedWaypoint[1], $selectedWaypoint[0]._data.index)
 | 
			
		||||
                : undefined
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
		selectedWaypoint.set(undefined);
 | 
			
		||||
		resetWaypointData();
 | 
			
		||||
	}
 | 
			
		||||
        selectedWaypoint.set(undefined);
 | 
			
		||||
        resetWaypointData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	function setCoordinates(e: any) {
 | 
			
		||||
		latitude = e.lngLat.lat.toFixed(6);
 | 
			
		||||
		longitude = e.lngLat.lng.toFixed(6);
 | 
			
		||||
	}
 | 
			
		||||
    function setCoordinates(e: any) {
 | 
			
		||||
        latitude = e.lngLat.lat.toFixed(6);
 | 
			
		||||
        longitude = e.lngLat.lng.toFixed(6);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	$: sortedSymbols = Object.entries(symbols).sort((a, b) => {
 | 
			
		||||
		return $_(`gpx.symbol.${a[0]}`).localeCompare($_(`gpx.symbol.${b[0]}`), $locale ?? 'en');
 | 
			
		||||
	});
 | 
			
		||||
    $: sortedSymbols = Object.entries(symbols).sort((a, b) => {
 | 
			
		||||
        return $_(`gpx.symbol.${a[0]}`).localeCompare($_(`gpx.symbol.${b[0]}`), $locale ?? 'en');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		let m = get(map);
 | 
			
		||||
		m?.on('click', setCoordinates);
 | 
			
		||||
		setCrosshairCursor();
 | 
			
		||||
	});
 | 
			
		||||
    onMount(() => {
 | 
			
		||||
        let m = get(map);
 | 
			
		||||
        m?.on('click', setCoordinates);
 | 
			
		||||
        setCrosshairCursor();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
	onDestroy(() => {
 | 
			
		||||
		let m = get(map);
 | 
			
		||||
		m?.off('click', setCoordinates);
 | 
			
		||||
		resetCursor();
 | 
			
		||||
    onDestroy(() => {
 | 
			
		||||
        let m = get(map);
 | 
			
		||||
        m?.off('click', setCoordinates);
 | 
			
		||||
        resetCursor();
 | 
			
		||||
 | 
			
		||||
		if (unsubscribe) {
 | 
			
		||||
			unsubscribe();
 | 
			
		||||
			unsubscribe = undefined;
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
        if (unsubscribe) {
 | 
			
		||||
            unsubscribe();
 | 
			
		||||
            unsubscribe = undefined;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-col gap-3 w-full max-w-96 {$$props.class ?? ''}">
 | 
			
		||||
	<fieldset class="flex flex-col gap-2">
 | 
			
		||||
		<Label for="name">{$_('menu.metadata.name')}</Label>
 | 
			
		||||
		<Input bind:value={name} id="name" class="font-semibold h-8" />
 | 
			
		||||
		<Label for="description">{$_('menu.metadata.description')}</Label>
 | 
			
		||||
		<Textarea bind:value={description} id="description" />
 | 
			
		||||
		<Label for="symbol">{$_('toolbar.waypoint.icon')}</Label>
 | 
			
		||||
		<Select.Root bind:selected={selectedSymbol}>
 | 
			
		||||
			<Select.Trigger id="symbol" class="w-full h-8">
 | 
			
		||||
				<Select.Value />
 | 
			
		||||
			</Select.Trigger>
 | 
			
		||||
			<Select.Content class="max-h-60 overflow-y-scroll">
 | 
			
		||||
				{#each sortedSymbols as [key, symbol]}
 | 
			
		||||
					<Select.Item value={symbol.value}>
 | 
			
		||||
						<span>
 | 
			
		||||
							{#if symbol.icon}
 | 
			
		||||
								<svelte:component
 | 
			
		||||
									this={symbol.icon}
 | 
			
		||||
									size="14"
 | 
			
		||||
									class="inline-block align-sub mr-0.5"
 | 
			
		||||
								/>
 | 
			
		||||
							{:else}
 | 
			
		||||
								<span class="w-4 inline-block" />
 | 
			
		||||
							{/if}
 | 
			
		||||
							{$_(`gpx.symbol.${key}`)}
 | 
			
		||||
						</span>
 | 
			
		||||
					</Select.Item>
 | 
			
		||||
				{/each}
 | 
			
		||||
			</Select.Content>
 | 
			
		||||
		</Select.Root>
 | 
			
		||||
		<Label for="link">{$_('toolbar.waypoint.link')}</Label>
 | 
			
		||||
		<Input bind:value={link} id="link" class="h-8" />
 | 
			
		||||
		<div class="flex flex-row gap-2">
 | 
			
		||||
			<div>
 | 
			
		||||
				<Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label>
 | 
			
		||||
				<Input
 | 
			
		||||
					bind:value={latitude}
 | 
			
		||||
					type="number"
 | 
			
		||||
					id="latitude"
 | 
			
		||||
					step={1e-6}
 | 
			
		||||
					min={-90}
 | 
			
		||||
					max={90}
 | 
			
		||||
					class="text-xs h-8"
 | 
			
		||||
				/>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div>
 | 
			
		||||
				<Label for="longitude">{$_('toolbar.waypoint.longitude')}</Label>
 | 
			
		||||
				<Input
 | 
			
		||||
					bind:value={longitude}
 | 
			
		||||
					type="number"
 | 
			
		||||
					id="longitude"
 | 
			
		||||
					step={1e-6}
 | 
			
		||||
					min={-180}
 | 
			
		||||
					max={180}
 | 
			
		||||
					class="text-xs h-8"
 | 
			
		||||
				/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</fieldset>
 | 
			
		||||
	<div class="flex flex-row flex-wrap gap-2">
 | 
			
		||||
		<Button
 | 
			
		||||
			variant="outline"
 | 
			
		||||
			disabled={!canCreate && !$selectedWaypoint}
 | 
			
		||||
			class="grow"
 | 
			
		||||
			on:click={createOrUpdateWaypoint}
 | 
			
		||||
		>
 | 
			
		||||
			{#if $selectedWaypoint}
 | 
			
		||||
				<Save size="16" class="mr-1" />
 | 
			
		||||
				{$_('menu.metadata.save')}
 | 
			
		||||
			{:else}
 | 
			
		||||
				<CirclePlus size="16" class="mr-1" />
 | 
			
		||||
				{$_('toolbar.waypoint.create')}
 | 
			
		||||
			{/if}
 | 
			
		||||
		</Button>
 | 
			
		||||
		<Button
 | 
			
		||||
			variant="outline"
 | 
			
		||||
			class="ml-auto"
 | 
			
		||||
			on:click={() => {
 | 
			
		||||
				selectedWaypoint.set(undefined);
 | 
			
		||||
				resetWaypointData();
 | 
			
		||||
			}}
 | 
			
		||||
		>
 | 
			
		||||
			<CircleX size="16" />
 | 
			
		||||
		</Button>
 | 
			
		||||
	</div>
 | 
			
		||||
	<Help link="./help/toolbar/poi">
 | 
			
		||||
		{#if $selectedWaypoint || canCreate}
 | 
			
		||||
			{$_('toolbar.waypoint.help')}
 | 
			
		||||
		{:else}
 | 
			
		||||
			{$_('toolbar.waypoint.help_no_selection')}
 | 
			
		||||
		{/if}
 | 
			
		||||
	</Help>
 | 
			
		||||
    <fieldset class="flex flex-col gap-2">
 | 
			
		||||
        <Label for="name">{$_('menu.metadata.name')}</Label>
 | 
			
		||||
        <Input bind:value={name} id="name" class="font-semibold h-8" />
 | 
			
		||||
        <Label for="description">{$_('menu.metadata.description')}</Label>
 | 
			
		||||
        <Textarea bind:value={description} id="description" />
 | 
			
		||||
        <Label for="symbol">{$_('toolbar.waypoint.icon')}</Label>
 | 
			
		||||
        <Select.Root bind:selected={selectedSymbol}>
 | 
			
		||||
            <Select.Trigger id="symbol" class="w-full h-8">
 | 
			
		||||
                <Select.Value />
 | 
			
		||||
            </Select.Trigger>
 | 
			
		||||
            <Select.Content class="max-h-60 overflow-y-scroll">
 | 
			
		||||
                {#each sortedSymbols as [key, symbol]}
 | 
			
		||||
                    <Select.Item value={symbol.value}>
 | 
			
		||||
                        <span>
 | 
			
		||||
                            {#if symbol.icon}
 | 
			
		||||
                                <svelte:component
 | 
			
		||||
                                    this={symbol.icon}
 | 
			
		||||
                                    size="14"
 | 
			
		||||
                                    class="inline-block align-sub mr-0.5"
 | 
			
		||||
                                />
 | 
			
		||||
                            {:else}
 | 
			
		||||
                                <span class="w-4 inline-block" />
 | 
			
		||||
                            {/if}
 | 
			
		||||
                            {$_(`gpx.symbol.${key}`)}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </Select.Item>
 | 
			
		||||
                {/each}
 | 
			
		||||
            </Select.Content>
 | 
			
		||||
        </Select.Root>
 | 
			
		||||
        <Label for="link">{$_('toolbar.waypoint.link')}</Label>
 | 
			
		||||
        <Input bind:value={link} id="link" class="h-8" />
 | 
			
		||||
        <div class="flex flex-row gap-2">
 | 
			
		||||
            <div class="grow">
 | 
			
		||||
                <Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label>
 | 
			
		||||
                <Input
 | 
			
		||||
                    bind:value={latitude}
 | 
			
		||||
                    type="number"
 | 
			
		||||
                    id="latitude"
 | 
			
		||||
                    step={1e-6}
 | 
			
		||||
                    min={-90}
 | 
			
		||||
                    max={90}
 | 
			
		||||
                    class="text-xs h-8 "
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="grow">
 | 
			
		||||
                <Label for="longitude">{$_('toolbar.waypoint.longitude')}</Label>
 | 
			
		||||
                <Input
 | 
			
		||||
                    bind:value={longitude}
 | 
			
		||||
                    type="number"
 | 
			
		||||
                    id="longitude"
 | 
			
		||||
                    step={1e-6}
 | 
			
		||||
                    min={-180}
 | 
			
		||||
                    max={180}
 | 
			
		||||
                    class="text-xs h-8"
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </fieldset>
 | 
			
		||||
    <div class="flex flex-row gap-2 items-center">
 | 
			
		||||
        <Button
 | 
			
		||||
            variant="outline"
 | 
			
		||||
            disabled={!canCreate && !$selectedWaypoint}
 | 
			
		||||
            class="grow whitespace-normal h-fit"
 | 
			
		||||
            on:click={createOrUpdateWaypoint}
 | 
			
		||||
        >
 | 
			
		||||
            {#if $selectedWaypoint}
 | 
			
		||||
                <Save size="16" class="mr-1 shrink-0" />
 | 
			
		||||
                {$_('menu.metadata.save')}
 | 
			
		||||
            {:else}
 | 
			
		||||
                <CirclePlus size="16" class="mr-1 shrink-0" />
 | 
			
		||||
                {$_('toolbar.waypoint.create')}
 | 
			
		||||
            {/if}
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button
 | 
			
		||||
            variant="outline"
 | 
			
		||||
            on:click={() => {
 | 
			
		||||
                selectedWaypoint.set(undefined);
 | 
			
		||||
                resetWaypointData();
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <CircleX size="16" />
 | 
			
		||||
        </Button>
 | 
			
		||||
    </div>
 | 
			
		||||
    <Help link="./help/toolbar/poi">
 | 
			
		||||
        {#if $selectedWaypoint || canCreate}
 | 
			
		||||
            {$_('toolbar.waypoint.help')}
 | 
			
		||||
        {:else}
 | 
			
		||||
            {$_('toolbar.waypoint.help_no_selection')}
 | 
			
		||||
        {/if}
 | 
			
		||||
    </Help>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user