mirror of
				https://github.com/gpxstudio/gpx.studio.git
				synced 2025-11-04 05:21:09 +00:00 
			
		
		
		
	fix tools
This commit is contained in:
		@@ -544,15 +544,17 @@
 | 
			
		||||
<svelte:window
 | 
			
		||||
    on:keydown={(e) => {
 | 
			
		||||
        let targetInput =
 | 
			
		||||
            e.target.tagName === 'INPUT' ||
 | 
			
		||||
            e.target.tagName === 'TEXTAREA' ||
 | 
			
		||||
            e.target.tagName === 'SELECT' ||
 | 
			
		||||
            e.target.role === 'combobox' ||
 | 
			
		||||
            e.target.role === 'radio' ||
 | 
			
		||||
            e.target.role === 'menu' ||
 | 
			
		||||
            e.target.role === 'menuitem' ||
 | 
			
		||||
            e.target.role === 'menuitemradio' ||
 | 
			
		||||
            e.target.role === 'menuitemcheckbox';
 | 
			
		||||
            e &&
 | 
			
		||||
            e.target &&
 | 
			
		||||
            (e.target.tagName === 'INPUT' ||
 | 
			
		||||
                e.target.tagName === 'TEXTAREA' ||
 | 
			
		||||
                e.target.tagName === 'SELECT' ||
 | 
			
		||||
                e.target.role === 'combobox' ||
 | 
			
		||||
                e.target.role === 'radio' ||
 | 
			
		||||
                e.target.role === 'menu' ||
 | 
			
		||||
                e.target.role === 'menuitem' ||
 | 
			
		||||
                e.target.role === 'menuitemradio' ||
 | 
			
		||||
                e.target.role === 'menuitemcheckbox');
 | 
			
		||||
 | 
			
		||||
        if (e.key === '+' && (e.metaKey || e.ctrlKey)) {
 | 
			
		||||
            createFile();
 | 
			
		||||
 
 | 
			
		||||
@@ -10,14 +10,7 @@ import {
 | 
			
		||||
    ListFileItem,
 | 
			
		||||
    ListRootItem,
 | 
			
		||||
} from '$lib/components/file-list/file-list';
 | 
			
		||||
import {
 | 
			
		||||
    getClosestLinePoint,
 | 
			
		||||
    getElevation,
 | 
			
		||||
    resetCursor,
 | 
			
		||||
    setGrabbingCursor,
 | 
			
		||||
    setPointerCursor,
 | 
			
		||||
    setScissorsCursor,
 | 
			
		||||
} from '$lib/utils';
 | 
			
		||||
import { getClosestLinePoint, getElevation } from '$lib/utils';
 | 
			
		||||
import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/waypoint';
 | 
			
		||||
import { MapPin, Square } from 'lucide-static';
 | 
			
		||||
import { getSymbolKey, symbols } from '$lib/assets/symbols';
 | 
			
		||||
@@ -28,6 +21,7 @@ import { currentTool, Tool } from '$lib/components/toolbar/tools';
 | 
			
		||||
import { fileActionManager } from '$lib/logic/file-action-manager';
 | 
			
		||||
import { fileActions } from '$lib/logic/file-actions';
 | 
			
		||||
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
 | 
			
		||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
 | 
			
		||||
 | 
			
		||||
const colors = [
 | 
			
		||||
    '#ff0000',
 | 
			
		||||
@@ -335,12 +329,12 @@ export class GPXLayer {
 | 
			
		||||
                        e.stopPropagation();
 | 
			
		||||
                    });
 | 
			
		||||
                    marker.on('dragstart', () => {
 | 
			
		||||
                        setGrabbingCursor();
 | 
			
		||||
                        mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, true);
 | 
			
		||||
                        marker.getElement().style.cursor = 'grabbing';
 | 
			
		||||
                        waypointPopup?.hide();
 | 
			
		||||
                    });
 | 
			
		||||
                    marker.on('dragend', (e) => {
 | 
			
		||||
                        resetCursor();
 | 
			
		||||
                        mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
 | 
			
		||||
                        marker.getElement().style.cursor = '';
 | 
			
		||||
                        getElevation([marker._waypoint]).then((ele) => {
 | 
			
		||||
                            fileActionManager.applyToFile(this.fileId, (file) => {
 | 
			
		||||
@@ -431,14 +425,15 @@ export class GPXLayer {
 | 
			
		||||
                new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
 | 
			
		||||
            )
 | 
			
		||||
        ) {
 | 
			
		||||
            setScissorsCursor();
 | 
			
		||||
            mapCursor.notify(MapCursorState.SCISSORS, true);
 | 
			
		||||
        } else {
 | 
			
		||||
            setPointerCursor();
 | 
			
		||||
            mapCursor.notify(MapCursorState.LAYER_HOVER, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    layerOnMouseLeave() {
 | 
			
		||||
        resetCursor();
 | 
			
		||||
        mapCursor.notify(MapCursorState.SCISSORS, false);
 | 
			
		||||
        mapCursor.notify(MapCursorState.LAYER_HOVER, false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    layerOnMouseMove(e: any) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { resetCursor, setCrosshairCursor } from '$lib/utils';
 | 
			
		||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
 | 
			
		||||
import type mapboxgl from 'mapbox-gl';
 | 
			
		||||
 | 
			
		||||
export class GoogleRedirect {
 | 
			
		||||
@@ -13,7 +13,7 @@ export class GoogleRedirect {
 | 
			
		||||
        if (this.enabled) return;
 | 
			
		||||
 | 
			
		||||
        this.enabled = true;
 | 
			
		||||
        setCrosshairCursor();
 | 
			
		||||
        mapCursor.notify(MapCursorState.STREET_VIEW_CROSSHAIR, true);
 | 
			
		||||
        this.map.on('click', this.openStreetView);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -21,11 +21,11 @@ export class GoogleRedirect {
 | 
			
		||||
        if (!this.enabled) return;
 | 
			
		||||
 | 
			
		||||
        this.enabled = false;
 | 
			
		||||
        resetCursor();
 | 
			
		||||
        mapCursor.notify(MapCursorState.STREET_VIEW_CROSSHAIR, false);
 | 
			
		||||
        this.map.off('click', this.openStreetView);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    openStreetView(e) {
 | 
			
		||||
    openStreetView(e: mapboxgl.MapMouseEvent) {
 | 
			
		||||
        window.open(
 | 
			
		||||
            `https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${e.lngLat.lat},${e.lngLat.lng}`
 | 
			
		||||
        );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } from 'mapbox-gl';
 | 
			
		||||
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
 | 
			
		||||
import 'mapillary-js/dist/mapillary.css';
 | 
			
		||||
import { resetCursor, setPointerCursor } from '$lib/utils';
 | 
			
		||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
 | 
			
		||||
 | 
			
		||||
const mapillarySource: VectorSourceSpecification = {
 | 
			
		||||
    type: 'vector',
 | 
			
		||||
@@ -140,10 +140,10 @@ export class MapillaryLayer {
 | 
			
		||||
        this.viewer.resize();
 | 
			
		||||
        this.viewer.moveTo(e.features[0].properties.id);
 | 
			
		||||
 | 
			
		||||
        setPointerCursor();
 | 
			
		||||
        mapCursor.notify(MapCursorState.MAPILLARY_HOVER, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onMouseLeave() {
 | 
			
		||||
        resetCursor();
 | 
			
		||||
        mapCursor.notify(MapCursorState.MAPILLARY_HOVER, false);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import { Button } from '$lib/components/ui/button';
 | 
			
		||||
    import * as Tooltip from '$lib/components/ui/tooltip/index.js';
 | 
			
		||||
    import { tool, Tool } from '$lib/components/toolbar/tools';
 | 
			
		||||
    import { currentTool, Tool } from '$lib/components/toolbar/tools';
 | 
			
		||||
    import type { Snippet } from 'svelte';
 | 
			
		||||
 | 
			
		||||
    let {
 | 
			
		||||
@@ -15,10 +15,10 @@
 | 
			
		||||
    } = $props();
 | 
			
		||||
 | 
			
		||||
    function toggleTool() {
 | 
			
		||||
        if (tool.current === itemTool) {
 | 
			
		||||
            tool.current = null;
 | 
			
		||||
        if ($currentTool === itemTool) {
 | 
			
		||||
            $currentTool = null;
 | 
			
		||||
        } else {
 | 
			
		||||
            tool.current = itemTool;
 | 
			
		||||
            $currentTool = itemTool;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
@@ -30,7 +30,7 @@
 | 
			
		||||
                <Button
 | 
			
		||||
                    {...props}
 | 
			
		||||
                    variant="ghost"
 | 
			
		||||
                    class="h-[26px] px-1 py-1.5 {tool.current === itemTool ? 'bg-accent' : ''}"
 | 
			
		||||
                    class="h-[26px] px-1 py-1.5 {$currentTool === itemTool ? 'bg-accent' : ''}"
 | 
			
		||||
                    onclick={toggleTool}
 | 
			
		||||
                    aria-label={label}
 | 
			
		||||
                >
 | 
			
		||||
 
 | 
			
		||||
@@ -1,68 +1,64 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import { Tool, tool } from '$lib/components/toolbar/tools';
 | 
			
		||||
    import { Tool, currentTool } from '$lib/components/toolbar/tools';
 | 
			
		||||
    import * as Card from '$lib/components/ui/card';
 | 
			
		||||
    import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
 | 
			
		||||
    import Scissors from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
 | 
			
		||||
    import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
 | 
			
		||||
    import Time from '$lib/components/toolbar/tools/Time.svelte';
 | 
			
		||||
    // import Time from '$lib/components/toolbar/tools/Time.svelte';
 | 
			
		||||
    import Merge from '$lib/components/toolbar/tools/Merge.svelte';
 | 
			
		||||
    import Extract from '$lib/components/toolbar/tools/Extract.svelte';
 | 
			
		||||
    import Elevation from '$lib/components/toolbar/tools/Elevation.svelte';
 | 
			
		||||
    import Extract from '$lib/components/toolbar/tools/Extract.svelte';
 | 
			
		||||
    import Clean from '$lib/components/toolbar/tools/Clean.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
 | 
			
		||||
    import { onMount } from 'svelte';
 | 
			
		||||
    import mapboxgl from 'mapbox-gl';
 | 
			
		||||
    import { settings } from '$lib/logic/settings';
 | 
			
		||||
 | 
			
		||||
    let {
 | 
			
		||||
        popupElement,
 | 
			
		||||
        popup,
 | 
			
		||||
        class: className = '',
 | 
			
		||||
    }: {
 | 
			
		||||
        popupElement: HTMLDivElement;
 | 
			
		||||
        popup: mapboxgl.Popup;
 | 
			
		||||
        class: string;
 | 
			
		||||
    } = $props();
 | 
			
		||||
 | 
			
		||||
    const { minimizeRoutingMenu } = settings;
 | 
			
		||||
 | 
			
		||||
    onMount(() => {
 | 
			
		||||
        popup = new mapboxgl.Popup({
 | 
			
		||||
    let popupElement: HTMLDivElement | undefined = $state(undefined);
 | 
			
		||||
    let popup: mapboxgl.Popup | undefined = $derived.by(() => {
 | 
			
		||||
        if (!popupElement) {
 | 
			
		||||
            return undefined;
 | 
			
		||||
        }
 | 
			
		||||
        let popup = new mapboxgl.Popup({
 | 
			
		||||
            closeButton: false,
 | 
			
		||||
            maxWidth: undefined,
 | 
			
		||||
        });
 | 
			
		||||
        popup.setDOMContent(popupElement);
 | 
			
		||||
        popupElement.classList.remove('hidden');
 | 
			
		||||
        return popup;
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if tool.current !== null}
 | 
			
		||||
{#if $currentTool !== null}
 | 
			
		||||
    <div class="translate-x-1 h-full animate-in animate-out {className}">
 | 
			
		||||
        <div class="rounded-md shadow-md pointer-events-auto">
 | 
			
		||||
            <Card.Root class="rounded-md border-none">
 | 
			
		||||
                <Card.Content class="p-2.5">
 | 
			
		||||
                    {#if tool.current === Tool.ROUTING}
 | 
			
		||||
                        <Routing
 | 
			
		||||
                            {popup}
 | 
			
		||||
                            {popupElement}
 | 
			
		||||
                            bind:minimized={minimizeRoutingMenu.value}
 | 
			
		||||
                        />
 | 
			
		||||
                    {:else if tool.current === Tool.SCISSORS}
 | 
			
		||||
                    {#if $currentTool === Tool.ROUTING}
 | 
			
		||||
                        <Routing {popup} {popupElement} bind:minimized={$minimizeRoutingMenu} />
 | 
			
		||||
                    {:else if $currentTool === Tool.SCISSORS}
 | 
			
		||||
                        <Scissors />
 | 
			
		||||
                    {:else if tool.current === Tool.WAYPOINT}
 | 
			
		||||
                    {:else if $currentTool === Tool.WAYPOINT}
 | 
			
		||||
                        <Waypoint />
 | 
			
		||||
                    {:else if tool.current === Tool.TIME}
 | 
			
		||||
                        <Time />
 | 
			
		||||
                    {:else if tool.current === Tool.MERGE}
 | 
			
		||||
                        <!-- {:else if $currentTool === Tool.TIME}
 | 
			
		||||
                        <Time /> -->
 | 
			
		||||
                    {:else if $currentTool === Tool.MERGE}
 | 
			
		||||
                        <Merge />
 | 
			
		||||
                    {:else if tool.current === Tool.ELEVATION}
 | 
			
		||||
                    {:else if $currentTool === Tool.ELEVATION}
 | 
			
		||||
                        <Elevation />
 | 
			
		||||
                    {:else if tool.current === Tool.EXTRACT}
 | 
			
		||||
                    {:else if $currentTool === Tool.EXTRACT}
 | 
			
		||||
                        <Extract />
 | 
			
		||||
                    {:else if tool.current === Tool.CLEAN}
 | 
			
		||||
                    {:else if $currentTool === Tool.CLEAN}
 | 
			
		||||
                        <Clean />
 | 
			
		||||
                    {:else if tool.current === Tool.REDUCE}
 | 
			
		||||
                    {:else if $currentTool === Tool.REDUCE}
 | 
			
		||||
                        <Reduce />
 | 
			
		||||
                    {/if}
 | 
			
		||||
                </Card.Content>
 | 
			
		||||
@@ -73,8 +69,8 @@
 | 
			
		||||
 | 
			
		||||
<svelte:window
 | 
			
		||||
    on:keydown={(e) => {
 | 
			
		||||
        if (tool.current !== null && e.key === 'Escape') {
 | 
			
		||||
            tool.current = null;
 | 
			
		||||
        if ($currentTool !== null && e.key === 'Escape') {
 | 
			
		||||
            $currentTool = null;
 | 
			
		||||
        }
 | 
			
		||||
    }}
 | 
			
		||||
/>
 | 
			
		||||
 
 | 
			
		||||
@@ -13,12 +13,13 @@
 | 
			
		||||
    import Help from '$lib/components/Help.svelte';
 | 
			
		||||
    import { i18n } from '$lib/i18n.svelte';
 | 
			
		||||
    import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
    import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
 | 
			
		||||
    import { getURLForLanguage } from '$lib/utils';
 | 
			
		||||
    import { Trash2 } from '@lucide/svelte';
 | 
			
		||||
    import { map } from '$lib/components/map/map';
 | 
			
		||||
    import type { GeoJSONSource } from 'mapbox-gl';
 | 
			
		||||
    import { selection } from '$lib/logic/selection';
 | 
			
		||||
    import { fileActions } from '$lib/logic/file-actions';
 | 
			
		||||
    import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
 | 
			
		||||
 | 
			
		||||
    let props: {
 | 
			
		||||
        class?: string;
 | 
			
		||||
@@ -30,10 +31,10 @@
 | 
			
		||||
    let rectangleCoordinates: mapboxgl.LngLat[] = $state([]);
 | 
			
		||||
 | 
			
		||||
    $effect(() => {
 | 
			
		||||
        if (map.value) {
 | 
			
		||||
        if ($map) {
 | 
			
		||||
            if (rectangleCoordinates.length != 2) {
 | 
			
		||||
                if (map.value.getLayer('rectangle')) {
 | 
			
		||||
                    map.value.removeLayer('rectangle');
 | 
			
		||||
                if ($map.getLayer('rectangle')) {
 | 
			
		||||
                    $map.removeLayer('rectangle');
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                let data: GeoJSON.Feature = {
 | 
			
		||||
@@ -52,17 +53,17 @@
 | 
			
		||||
                    },
 | 
			
		||||
                    properties: {},
 | 
			
		||||
                };
 | 
			
		||||
                let source: GeoJSONSource | undefined = map.value.getSource('rectangle');
 | 
			
		||||
                let source: GeoJSONSource | undefined = $map.getSource('rectangle');
 | 
			
		||||
                if (source) {
 | 
			
		||||
                    source.setData(data);
 | 
			
		||||
                } else {
 | 
			
		||||
                    map.value.addSource('rectangle', {
 | 
			
		||||
                    $map.addSource('rectangle', {
 | 
			
		||||
                        type: 'geojson',
 | 
			
		||||
                        data: data,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
                if (!map.value.getLayer('rectangle')) {
 | 
			
		||||
                    map.value.addLayer({
 | 
			
		||||
                if (!$map.getLayer('rectangle')) {
 | 
			
		||||
                    $map.addLayer({
 | 
			
		||||
                        id: 'rectangle',
 | 
			
		||||
                        type: 'fill',
 | 
			
		||||
                        source: 'rectangle',
 | 
			
		||||
@@ -93,39 +94,39 @@
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onMount(() => {
 | 
			
		||||
        if (map.value) {
 | 
			
		||||
            setCrosshairCursor(map.value.getCanvas());
 | 
			
		||||
            map.value.on('mousedown', onMouseDown);
 | 
			
		||||
            map.value.on('mousemove', onMouseMove);
 | 
			
		||||
            map.value.on('mouseup', onMouseUp);
 | 
			
		||||
            map.value.on('touchstart', onMouseDown);
 | 
			
		||||
            map.value.on('touchmove', onMouseMove);
 | 
			
		||||
            map.value.on('touchend', onMouseUp);
 | 
			
		||||
            map.value.dragPan.disable();
 | 
			
		||||
        if ($map) {
 | 
			
		||||
            mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, true);
 | 
			
		||||
            $map.on('mousedown', onMouseDown);
 | 
			
		||||
            $map.on('mousemove', onMouseMove);
 | 
			
		||||
            $map.on('mouseup', onMouseUp);
 | 
			
		||||
            $map.on('touchstart', onMouseDown);
 | 
			
		||||
            $map.on('touchmove', onMouseMove);
 | 
			
		||||
            $map.on('touchend', onMouseUp);
 | 
			
		||||
            $map.dragPan.disable();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    onDestroy(() => {
 | 
			
		||||
        if (map.value) {
 | 
			
		||||
            resetCursor(map.value.getCanvas());
 | 
			
		||||
            map.value.off('mousedown', onMouseDown);
 | 
			
		||||
            map.value.off('mousemove', onMouseMove);
 | 
			
		||||
            map.value.off('mouseup', onMouseUp);
 | 
			
		||||
            map.value.off('touchstart', onMouseDown);
 | 
			
		||||
            map.value.off('touchmove', onMouseMove);
 | 
			
		||||
            map.value.off('touchend', onMouseUp);
 | 
			
		||||
            map.value.dragPan.enable();
 | 
			
		||||
        if ($map) {
 | 
			
		||||
            mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
 | 
			
		||||
            $map.off('mousedown', onMouseDown);
 | 
			
		||||
            $map.off('mousemove', onMouseMove);
 | 
			
		||||
            $map.off('mouseup', onMouseUp);
 | 
			
		||||
            $map.off('touchstart', onMouseDown);
 | 
			
		||||
            $map.off('touchmove', onMouseMove);
 | 
			
		||||
            $map.off('touchend', onMouseUp);
 | 
			
		||||
            $map.dragPan.enable();
 | 
			
		||||
 | 
			
		||||
            if (map.value.getLayer('rectangle')) {
 | 
			
		||||
                map.value.removeLayer('rectangle');
 | 
			
		||||
            if ($map.getLayer('rectangle')) {
 | 
			
		||||
                $map.removeLayer('rectangle');
 | 
			
		||||
            }
 | 
			
		||||
            if (map.value.getSource('rectangle')) {
 | 
			
		||||
                map.value.removeSource('rectangle');
 | 
			
		||||
            if ($map.getSource('rectangle')) {
 | 
			
		||||
                $map.removeSource('rectangle');
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    let validSelection = $derived(selection.value.size > 0);
 | 
			
		||||
    let validSelection = $derived($selection.size > 0);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-col gap-3 w-full max-w-80 items-center {props.class ?? ''}">
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@
 | 
			
		||||
        class?: string;
 | 
			
		||||
    } = $props();
 | 
			
		||||
 | 
			
		||||
    let validSelection = $derived(selection.value.size > 0);
 | 
			
		||||
    let validSelection = $derived($selection.size > 0);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
 | 
			
		||||
@@ -21,8 +21,8 @@
 | 
			
		||||
        class="whitespace-normal h-fit"
 | 
			
		||||
        disabled={!validSelection}
 | 
			
		||||
        onclick={async () => {
 | 
			
		||||
            if (map.value) {
 | 
			
		||||
                fileActions.addElevationToSelection(map.value);
 | 
			
		||||
            if ($map) {
 | 
			
		||||
                fileActions.addElevationToSelection($map);
 | 
			
		||||
            }
 | 
			
		||||
        }}
 | 
			
		||||
    >
 | 
			
		||||
 
 | 
			
		||||
@@ -20,8 +20,8 @@
 | 
			
		||||
    } = $props();
 | 
			
		||||
 | 
			
		||||
    let validSelection = $derived(
 | 
			
		||||
        selection.value.size > 0 &&
 | 
			
		||||
            selection.value.getSelected().every((item) => {
 | 
			
		||||
        $selection.size > 0 &&
 | 
			
		||||
            $selection.getSelected().every((item) => {
 | 
			
		||||
                if (
 | 
			
		||||
                    item instanceof ListWaypointsItem ||
 | 
			
		||||
                    item instanceof ListWaypointItem ||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,20 +16,20 @@
 | 
			
		||||
    import { Group } from '@lucide/svelte';
 | 
			
		||||
    import { getURLForLanguage } from '$lib/utils';
 | 
			
		||||
    import Shortcut from '$lib/components/Shortcut.svelte';
 | 
			
		||||
    import { gpxStatistics } from '$lib/stores';
 | 
			
		||||
    import { selection } from '$lib/logic/selection';
 | 
			
		||||
    import { fileStateCollection } from '$lib/logic/file-state';
 | 
			
		||||
    import { fileActions } from '$lib/logic/file-actions';
 | 
			
		||||
    import { gpxStatistics } from '$lib/logic/statistics';
 | 
			
		||||
 | 
			
		||||
    let props: {
 | 
			
		||||
        class?: string;
 | 
			
		||||
    } = $props();
 | 
			
		||||
 | 
			
		||||
    let canMergeTraces = $derived.by(() => {
 | 
			
		||||
        if (selection.value.size > 1) {
 | 
			
		||||
        if ($selection.size > 1) {
 | 
			
		||||
            return true;
 | 
			
		||||
        } else if (selection.value.size === 1) {
 | 
			
		||||
            let selected = selection.value.getSelected()[0];
 | 
			
		||||
        } else if ($selection.size === 1) {
 | 
			
		||||
            let selected = $selection.getSelected()[0];
 | 
			
		||||
            if (selected instanceof ListFileItem) {
 | 
			
		||||
                let file = fileStateCollection.getFile(selected.getFileId());
 | 
			
		||||
                if (file) {
 | 
			
		||||
@@ -47,8 +47,8 @@
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    let canMergeContents = $derived(
 | 
			
		||||
        selection.value.size > 1 &&
 | 
			
		||||
            selection.value
 | 
			
		||||
        $selection.size > 1 &&
 | 
			
		||||
            $selection
 | 
			
		||||
                .getSelected()
 | 
			
		||||
                .some((item) => item instanceof ListFileItem || item instanceof ListTrackItem)
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,195 +0,0 @@
 | 
			
		||||
<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 {
 | 
			
		||||
        ListItem,
 | 
			
		||||
        ListRootItem,
 | 
			
		||||
        ListTrackSegmentItem,
 | 
			
		||||
    } from '$lib/components/file-list/file-list';
 | 
			
		||||
    import Help from '$lib/components/Help.svelte';
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import { i18n } from '$lib/i18n.svelte';
 | 
			
		||||
    import WithUnits from '$lib/components/WithUnits.svelte';
 | 
			
		||||
    import { map } from '$lib/components/map/map';
 | 
			
		||||
    import { onDestroy } from 'svelte';
 | 
			
		||||
    import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
 | 
			
		||||
    import { getURLForLanguage } from '$lib/utils';
 | 
			
		||||
    import type { GeoJSONSource } from 'mapbox-gl';
 | 
			
		||||
    import { selection } from '$lib/logic/selection';
 | 
			
		||||
    import { fileActions } from '$lib/logic/file-actions';
 | 
			
		||||
 | 
			
		||||
    let props: { class?: string } = $props();
 | 
			
		||||
 | 
			
		||||
    let sliderValue = $state([50]);
 | 
			
		||||
    let maxPoints = $state(0);
 | 
			
		||||
    let currentPoints = $state(0);
 | 
			
		||||
    const minTolerance = 0.1;
 | 
			
		||||
    const maxTolerance = 10000;
 | 
			
		||||
 | 
			
		||||
    let validSelection = $derived(
 | 
			
		||||
        selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
 | 
			
		||||
    );
 | 
			
		||||
    let tolerance = $derived(
 | 
			
		||||
        minTolerance * 2 ** (sliderValue[0] / (100 / Math.log2(maxTolerance / minTolerance)))
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let simplified = new Map<string, [ListItem, number, SimplifiedTrackPoint[]]>();
 | 
			
		||||
    let unsubscribes = new Map<string, () => void>();
 | 
			
		||||
 | 
			
		||||
    function update() {
 | 
			
		||||
        maxPoints = 0;
 | 
			
		||||
        currentPoints = 0;
 | 
			
		||||
 | 
			
		||||
        let data: GeoJSON.FeatureCollection = {
 | 
			
		||||
            type: 'FeatureCollection',
 | 
			
		||||
            features: [],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        simplified.forEach(([item, maxPts, points], itemFullId) => {
 | 
			
		||||
            maxPoints += maxPts;
 | 
			
		||||
 | 
			
		||||
            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: {},
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (map.value) {
 | 
			
		||||
            let source: GeoJSONSource | undefined = map.value.getSource('simplified');
 | 
			
		||||
            if (source) {
 | 
			
		||||
                source.setData(data);
 | 
			
		||||
            } else {
 | 
			
		||||
                map.value.addSource('simplified', {
 | 
			
		||||
                    type: 'geojson',
 | 
			
		||||
                    data: data,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            if (!map.value.getLayer('simplified')) {
 | 
			
		||||
                map.value.addLayer({
 | 
			
		||||
                    id: 'simplified',
 | 
			
		||||
                    type: 'line',
 | 
			
		||||
                    source: 'simplified',
 | 
			
		||||
                    paint: {
 | 
			
		||||
                        'line-color': 'white',
 | 
			
		||||
                        'line-width': 3,
 | 
			
		||||
                    },
 | 
			
		||||
                });
 | 
			
		||||
            } else {
 | 
			
		||||
                map.value.moveLayer('simplified');
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // $effect(() => {
 | 
			
		||||
    //     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, minTolerance),
 | 
			
		||||
    //                                 ]);
 | 
			
		||||
    //                                 update();
 | 
			
		||||
    //                             } else if (simplified.has(segmentItem.getFullId())) {
 | 
			
		||||
    //                                 simplified.delete(segmentItem.getFullId());
 | 
			
		||||
    //                                 update();
 | 
			
		||||
    //                             }
 | 
			
		||||
    //                         });
 | 
			
		||||
    //                     }
 | 
			
		||||
    //                 });
 | 
			
		||||
    //                 unsubscribes.set(fileId, unsubscribe);
 | 
			
		||||
    //             }
 | 
			
		||||
    //         });
 | 
			
		||||
    //     }
 | 
			
		||||
    // });
 | 
			
		||||
 | 
			
		||||
    $effect(() => {
 | 
			
		||||
        if (tolerance) {
 | 
			
		||||
            update();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    onDestroy(() => {
 | 
			
		||||
        if (map.value) {
 | 
			
		||||
            if (map.value.getLayer('simplified')) {
 | 
			
		||||
                map.value.removeLayer('simplified');
 | 
			
		||||
            }
 | 
			
		||||
            if (map.value.getSource('simplified')) {
 | 
			
		||||
                map.value.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)
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
        fileActions.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} type="multiple" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <Label class="flex flex-row justify-between">
 | 
			
		||||
        <span>{i18n._('toolbar.reduce.tolerance')}</span>
 | 
			
		||||
        <WithUnits value={tolerance / 1000} type="distance" decimals={4} class="font-normal" />
 | 
			
		||||
    </Label>
 | 
			
		||||
    <Label class="flex flex-row justify-between">
 | 
			
		||||
        <span>{i18n._('toolbar.reduce.number_of_points')}</span>
 | 
			
		||||
        <span class="font-normal">{currentPoints}/{maxPoints}</span>
 | 
			
		||||
    </Label>
 | 
			
		||||
    <Button variant="outline" disabled={!validSelection} onclick={reduce}>
 | 
			
		||||
        <Funnel size="16" class="mr-1" />
 | 
			
		||||
        {i18n._('toolbar.reduce.button')}
 | 
			
		||||
    </Button>
 | 
			
		||||
 | 
			
		||||
    <Help link={getURLForLanguage(i18n.lang, '/help/toolbar/minify')}>
 | 
			
		||||
        {#if validSelection}
 | 
			
		||||
            {i18n._('toolbar.reduce.help')}
 | 
			
		||||
        {:else}
 | 
			
		||||
            {i18n._('toolbar.reduce.help_no_selection')}
 | 
			
		||||
        {/if}
 | 
			
		||||
    </Help>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -0,0 +1,63 @@
 | 
			
		||||
<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 { ListItem, ListRootItem } from '$lib/components/file-list/file-list';
 | 
			
		||||
    import Help from '$lib/components/Help.svelte';
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import { i18n } from '$lib/i18n.svelte';
 | 
			
		||||
    import WithUnits from '$lib/components/WithUnits.svelte';
 | 
			
		||||
    import { onDestroy } from 'svelte';
 | 
			
		||||
    import { getURLForLanguage } from '$lib/utils';
 | 
			
		||||
    import { selection } from '$lib/logic/selection';
 | 
			
		||||
    import { minTolerance, ReducedGPXLayerCollection, tolerance } from './reduce';
 | 
			
		||||
 | 
			
		||||
    let props: { class?: string } = $props();
 | 
			
		||||
 | 
			
		||||
    let sliderValue = $state([50]);
 | 
			
		||||
    let maxPoints = $state(0);
 | 
			
		||||
    let currentPoints = $state(0);
 | 
			
		||||
    const maxTolerance = 10000;
 | 
			
		||||
 | 
			
		||||
    let validSelection = $derived(
 | 
			
		||||
        $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let reducedLayers = new ReducedGPXLayerCollection();
 | 
			
		||||
 | 
			
		||||
    $effect(() => {
 | 
			
		||||
        tolerance.set(
 | 
			
		||||
            minTolerance * 2 ** (sliderValue[0] / (100 / Math.log2(maxTolerance / minTolerance)))
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    onDestroy(() => {
 | 
			
		||||
        reducedLayers.destroy();
 | 
			
		||||
    });
 | 
			
		||||
</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} type="multiple" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <Label class="flex flex-row justify-between">
 | 
			
		||||
        <span>{i18n._('toolbar.reduce.tolerance')}</span>
 | 
			
		||||
        <WithUnits value={$tolerance / 1000} type="distance" decimals={4} class="font-normal" />
 | 
			
		||||
    </Label>
 | 
			
		||||
    <Label class="flex flex-row justify-between">
 | 
			
		||||
        <span>{i18n._('toolbar.reduce.number_of_points')}</span>
 | 
			
		||||
        <span class="font-normal">{currentPoints}/{maxPoints}</span>
 | 
			
		||||
    </Label>
 | 
			
		||||
    <Button variant="outline" disabled={!validSelection} onclick={() => reducedLayers.reduce()}>
 | 
			
		||||
        <Funnel size="16" class="mr-1" />
 | 
			
		||||
        {i18n._('toolbar.reduce.button')}
 | 
			
		||||
    </Button>
 | 
			
		||||
 | 
			
		||||
    <Help link={getURLForLanguage(i18n.lang, '/help/toolbar/minify')}>
 | 
			
		||||
        {#if validSelection}
 | 
			
		||||
            {i18n._('toolbar.reduce.help')}
 | 
			
		||||
        {:else}
 | 
			
		||||
            {i18n._('toolbar.reduce.help_no_selection')}
 | 
			
		||||
        {/if}
 | 
			
		||||
    </Help>
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										187
									
								
								website/src/lib/components/toolbar/tools/reduce/reduce.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								website/src/lib/components/toolbar/tools/reduce/reduce.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,187 @@
 | 
			
		||||
import { ListItem, ListTrackSegmentItem } from '$lib/components/file-list/file-list';
 | 
			
		||||
import { map } from '$lib/components/map/map';
 | 
			
		||||
import { fileActions } from '$lib/logic/file-actions';
 | 
			
		||||
import { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/file-state';
 | 
			
		||||
import { selection } from '$lib/logic/selection';
 | 
			
		||||
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
 | 
			
		||||
import type { GeoJSONSource } from 'mapbox-gl';
 | 
			
		||||
import { get, writable } from 'svelte/store';
 | 
			
		||||
 | 
			
		||||
export const minTolerance = 0.1;
 | 
			
		||||
 | 
			
		||||
export class ReducedGPXLayer {
 | 
			
		||||
    private _fileState: GPXFileState;
 | 
			
		||||
    private _updateSimplified: (
 | 
			
		||||
        itemId: string,
 | 
			
		||||
        data: [ListItem, number, SimplifiedTrackPoint[]]
 | 
			
		||||
    ) => void;
 | 
			
		||||
    private _unsubscribes: (() => void)[] = [];
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        fileState: GPXFileState,
 | 
			
		||||
        updateSimplified: (itemId: string, data: [ListItem, number, SimplifiedTrackPoint[]]) => void
 | 
			
		||||
    ) {
 | 
			
		||||
        this._fileState = fileState;
 | 
			
		||||
        this._updateSimplified = updateSimplified;
 | 
			
		||||
        this._unsubscribes.push(this._fileState.subscribe(() => this.update()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    update() {
 | 
			
		||||
        const file = this._fileState.file;
 | 
			
		||||
        const stats = this._fileState.statistics;
 | 
			
		||||
        if (!file || !stats) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        file.forEachSegment((segment, trackIndex, segmentIndex) => {
 | 
			
		||||
            let segmentItem = new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex);
 | 
			
		||||
            let statistics = stats.getStatisticsFor(segmentItem);
 | 
			
		||||
            this._updateSimplified(segmentItem.getFullId(), [
 | 
			
		||||
                segmentItem,
 | 
			
		||||
                statistics.local.points.length,
 | 
			
		||||
                ramerDouglasPeucker(statistics.local.points, minTolerance),
 | 
			
		||||
            ]);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroy() {
 | 
			
		||||
        this._unsubscribes.forEach((unsubscribe) => unsubscribe());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const tolerance = writable<number>(0);
 | 
			
		||||
 | 
			
		||||
export class ReducedGPXLayerCollection {
 | 
			
		||||
    private _layers: Map<string, ReducedGPXLayer> = new Map();
 | 
			
		||||
    private _simplified: Map<string, [ListItem, number, SimplifiedTrackPoint[]]>;
 | 
			
		||||
    private _fileStateCollectionOberver: GPXFileStateCollectionObserver;
 | 
			
		||||
    private _updateSimplified = this.updateSimplified.bind(this);
 | 
			
		||||
    private _unsubscribes: (() => void)[] = [];
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this._layers = new Map();
 | 
			
		||||
        this._simplified = new Map();
 | 
			
		||||
        this._fileStateCollectionOberver = new GPXFileStateCollectionObserver(
 | 
			
		||||
            (fileId, fileState) => {
 | 
			
		||||
                this._layers.set(fileId, new ReducedGPXLayer(fileState, this._updateSimplified));
 | 
			
		||||
            },
 | 
			
		||||
            (fileId) => {
 | 
			
		||||
                this._layers.get(fileId)?.destroy();
 | 
			
		||||
                this._layers.delete(fileId);
 | 
			
		||||
            },
 | 
			
		||||
            () => {
 | 
			
		||||
                this._layers.forEach((layer) => layer.destroy());
 | 
			
		||||
                this._layers.clear();
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
        this._unsubscribes.push(selection.subscribe(() => this.update()));
 | 
			
		||||
        this._unsubscribes.push(tolerance.subscribe(() => this.update()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateSimplified(itemId: string, data: [ListItem, number, SimplifiedTrackPoint[]]) {
 | 
			
		||||
        this._simplified.set(itemId, data);
 | 
			
		||||
        if (get(selection).hasAnyParent(data[0])) {
 | 
			
		||||
            this.update();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    removeSimplified(itemId: string) {
 | 
			
		||||
        if (this._simplified.delete(itemId)) {
 | 
			
		||||
            this.update();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    update() {
 | 
			
		||||
        let maxPoints = 0;
 | 
			
		||||
        let currentPoints = 0;
 | 
			
		||||
 | 
			
		||||
        let data: GeoJSON.FeatureCollection = {
 | 
			
		||||
            type: 'FeatureCollection',
 | 
			
		||||
            features: [],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this._simplified.forEach(([item, maxPts, points], itemFullId) => {
 | 
			
		||||
            if (!get(selection).hasAnyParent(item)) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            maxPoints += maxPts;
 | 
			
		||||
 | 
			
		||||
            let current = points.filter(
 | 
			
		||||
                (point) => point.distance === undefined || point.distance >= get(tolerance)
 | 
			
		||||
            );
 | 
			
		||||
            currentPoints += current.length;
 | 
			
		||||
 | 
			
		||||
            data.features.push({
 | 
			
		||||
                type: 'Feature',
 | 
			
		||||
                geometry: {
 | 
			
		||||
                    type: 'LineString',
 | 
			
		||||
                    coordinates: current.map((point) => [
 | 
			
		||||
                        point.point.getLongitude(),
 | 
			
		||||
                        point.point.getLatitude(),
 | 
			
		||||
                    ]),
 | 
			
		||||
                },
 | 
			
		||||
                properties: {},
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const map_ = get(map);
 | 
			
		||||
        if (!map_) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let source: GeoJSONSource | undefined = 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');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    reduce() {
 | 
			
		||||
        let itemsAndPoints = new Map<ListItem, TrackPoint[]>();
 | 
			
		||||
        this._simplified.forEach(([item, maxPts, points], itemFullId) => {
 | 
			
		||||
            itemsAndPoints.set(
 | 
			
		||||
                item,
 | 
			
		||||
                points
 | 
			
		||||
                    .filter(
 | 
			
		||||
                        (point) => point.distance === undefined || point.distance >= get(tolerance)
 | 
			
		||||
                    )
 | 
			
		||||
                    .map((point) => point.point)
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
        fileActions.reduce(itemsAndPoints);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroy() {
 | 
			
		||||
        this._fileStateCollectionOberver.destroy();
 | 
			
		||||
        this._unsubscribes.forEach((unsubscribe) => unsubscribe());
 | 
			
		||||
 | 
			
		||||
        const map_ = get(map);
 | 
			
		||||
        if (!map_) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (map_.getLayer('simplified')) {
 | 
			
		||||
            map_.removeLayer('simplified');
 | 
			
		||||
        }
 | 
			
		||||
        if (map_.getSource('simplified')) {
 | 
			
		||||
            map_.removeSource('simplified');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -21,9 +21,8 @@
 | 
			
		||||
        SquareArrowUpLeft,
 | 
			
		||||
        SquareArrowOutDownRight,
 | 
			
		||||
    } from '@lucide/svelte';
 | 
			
		||||
    import { brouterProfiles } from '$lib/components/toolbar/tools/routing/utils.svelte';
 | 
			
		||||
    import { brouterProfiles } from '$lib/components/toolbar/tools/routing/routing';
 | 
			
		||||
    import { i18n } from '$lib/i18n.svelte';
 | 
			
		||||
    // import { RoutingControls } from './RoutingControls';
 | 
			
		||||
    import { slide } from 'svelte/transition';
 | 
			
		||||
    import {
 | 
			
		||||
        ListFileItem,
 | 
			
		||||
@@ -32,14 +31,16 @@
 | 
			
		||||
        ListTrackSegmentItem,
 | 
			
		||||
        type ListItem,
 | 
			
		||||
    } from '$lib/components/file-list/file-list';
 | 
			
		||||
    import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
 | 
			
		||||
    import { getURLForLanguage } from '$lib/utils';
 | 
			
		||||
    import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
    import { TrackPoint } from 'gpx';
 | 
			
		||||
    import { settings } from '$lib/logic/settings';
 | 
			
		||||
    import { map } from '$lib/components/map/map';
 | 
			
		||||
    import { fileStateCollection } from '$lib/logic/file-state';
 | 
			
		||||
    import { fileStateCollection, GPXFileStateCollectionObserver } from '$lib/logic/file-state';
 | 
			
		||||
    import { selection } from '$lib/logic/selection';
 | 
			
		||||
    import { fileActions, getFileIds, newGPXFile } from '$lib/logic/file-actions';
 | 
			
		||||
    import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
 | 
			
		||||
    import { RoutingControls, routingControls } from './RoutingControls';
 | 
			
		||||
 | 
			
		||||
    let {
 | 
			
		||||
        minimized = $bindable(false),
 | 
			
		||||
@@ -55,34 +56,9 @@
 | 
			
		||||
        class?: string;
 | 
			
		||||
    } = $props();
 | 
			
		||||
 | 
			
		||||
    let selectedItem: ListItem | null = null;
 | 
			
		||||
 | 
			
		||||
    const { privateRoads, routing, routingProfile } = settings;
 | 
			
		||||
 | 
			
		||||
    // $: if (map && popup && popupElement) {
 | 
			
		||||
    //     // remove controls for deleted files
 | 
			
		||||
    //     routingControls.forEach((controls, fileId) => {
 | 
			
		||||
    //         if (!$fileObservers.has(fileId)) {
 | 
			
		||||
    //             controls.destroy();
 | 
			
		||||
    //             routingControls.delete(fileId);
 | 
			
		||||
 | 
			
		||||
    //             if (selectedItem && selectedItem.getFileId() === fileId) {
 | 
			
		||||
    //                 selectedItem = null;
 | 
			
		||||
    //             }
 | 
			
		||||
    //         } else if ($map !== controls.map) {
 | 
			
		||||
    //             controls.updateMap($map);
 | 
			
		||||
    //         }
 | 
			
		||||
    //     });
 | 
			
		||||
    //     // add controls for new files
 | 
			
		||||
    //     fileStateCollection.files.forEach((file, fileId) => {
 | 
			
		||||
    //         if (!routingControls.has(fileId)) {
 | 
			
		||||
    //             routingControls.set(
 | 
			
		||||
    //                 fileId,
 | 
			
		||||
    //                 new RoutingControls($map, fileId, file, popup, popupElement)
 | 
			
		||||
    //             );
 | 
			
		||||
    //         }
 | 
			
		||||
    //     });
 | 
			
		||||
    // }
 | 
			
		||||
    let fileStateCollectionObserver: GPXFileStateCollectionObserver;
 | 
			
		||||
 | 
			
		||||
    let validSelection = $derived(
 | 
			
		||||
        $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
 | 
			
		||||
@@ -101,21 +77,44 @@
 | 
			
		||||
            ]);
 | 
			
		||||
            file._data.id = getFileIds(1)[0];
 | 
			
		||||
            fileActions.add(file);
 | 
			
		||||
            // selectFileWhenLoaded(file._data.id);
 | 
			
		||||
            selection.selectFileWhenLoaded(file._data.id);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onMount(() => {
 | 
			
		||||
        // setCrosshairCursor();
 | 
			
		||||
        $map?.on('click', createFileWithPoint);
 | 
			
		||||
        if ($map && popup && popupElement) {
 | 
			
		||||
            fileStateCollectionObserver = new GPXFileStateCollectionObserver(
 | 
			
		||||
                (fileId, fileState) => {
 | 
			
		||||
                    routingControls.set(
 | 
			
		||||
                        fileId,
 | 
			
		||||
                        new RoutingControls(fileId, fileState, popup, popupElement)
 | 
			
		||||
                    );
 | 
			
		||||
                },
 | 
			
		||||
                (fileId) => {
 | 
			
		||||
                    const controls = routingControls.get(fileId);
 | 
			
		||||
                    if (controls) {
 | 
			
		||||
                        controls.destroy();
 | 
			
		||||
                        routingControls.delete(fileId);
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                () => {
 | 
			
		||||
                    routingControls.forEach((controls) => controls.destroy());
 | 
			
		||||
                    routingControls.clear();
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, true);
 | 
			
		||||
            $map.on('click', createFileWithPoint);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    onDestroy(() => {
 | 
			
		||||
        // resetCursor();
 | 
			
		||||
        $map?.off('click', createFileWithPoint);
 | 
			
		||||
        if ($map) {
 | 
			
		||||
            fileStateCollectionObserver.destroy();
 | 
			
		||||
 | 
			
		||||
        // routingControls.forEach((controls) => controls.destroy());
 | 
			
		||||
        // routingControls.clear();
 | 
			
		||||
            mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
 | 
			
		||||
            $map.off('click', createFileWithPoint);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@@ -130,7 +129,7 @@
 | 
			
		||||
        <div class="flex flex-col gap-3">
 | 
			
		||||
            <Label class="flex flex-row justify-between items-center gap-2">
 | 
			
		||||
                <span class="flex flex-row items-center gap-1">
 | 
			
		||||
                    {#if routing.value}
 | 
			
		||||
                    {#if $routing}
 | 
			
		||||
                        <Route size="16" />
 | 
			
		||||
                    {:else}
 | 
			
		||||
                        <RouteOff size="16" />
 | 
			
		||||
@@ -138,28 +137,28 @@
 | 
			
		||||
                    {i18n._('toolbar.routing.use_routing')}
 | 
			
		||||
                </span>
 | 
			
		||||
                <Tooltip label={i18n._('toolbar.routing.use_routing_tooltip')}>
 | 
			
		||||
                    <Switch class="scale-90" bind:checked={routing.value} />
 | 
			
		||||
                    <Switch class="scale-90" bind:checked={$routing} />
 | 
			
		||||
                    <Shortcut slot="extra" key="F5" />
 | 
			
		||||
                </Tooltip>
 | 
			
		||||
            </Label>
 | 
			
		||||
            {#if routing.value}
 | 
			
		||||
            {#if $routing}
 | 
			
		||||
                <div class="flex flex-col gap-3" in:slide>
 | 
			
		||||
                    <Label class="flex flex-row justify-between items-center gap-2">
 | 
			
		||||
                        <span class="shrink-0 flex flex-row items-center gap-1">
 | 
			
		||||
                            {#if routingProfile.value.includes('bike') || routingProfile.value.includes('motorcycle')}
 | 
			
		||||
                            {#if $routingProfile.includes('bike') || $routingProfile.includes('motorcycle')}
 | 
			
		||||
                                <Bike size="16" />
 | 
			
		||||
                            {:else if routingProfile.value.includes('foot')}
 | 
			
		||||
                            {:else if $routingProfile.includes('foot')}
 | 
			
		||||
                                <Footprints size="16" />
 | 
			
		||||
                            {:else if routingProfile.value.includes('water')}
 | 
			
		||||
                            {:else if $routingProfile.includes('water')}
 | 
			
		||||
                                <Waves size="16" />
 | 
			
		||||
                            {:else if routingProfile.value.includes('railway')}
 | 
			
		||||
                            {:else if $routingProfile.includes('railway')}
 | 
			
		||||
                                <TrainFront size="16" />
 | 
			
		||||
                            {/if}
 | 
			
		||||
                            {i18n._('toolbar.routing.activity')}
 | 
			
		||||
                        </span>
 | 
			
		||||
                        <Select.Root type="single" bind:value={routingProfile.value}>
 | 
			
		||||
                        <Select.Root type="single" bind:value={$routingProfile}>
 | 
			
		||||
                            <Select.Trigger class="h-8 grow">
 | 
			
		||||
                                {i18n._(`toolbar.routing.activities.${routingProfile.value}`)}
 | 
			
		||||
                                {i18n._(`toolbar.routing.activities.${$routingProfile}`)}
 | 
			
		||||
                            </Select.Trigger>
 | 
			
		||||
                            <Select.Content>
 | 
			
		||||
                                {#each Object.keys(brouterProfiles) as profile}
 | 
			
		||||
@@ -177,7 +176,7 @@
 | 
			
		||||
                            <TriangleAlert size="16" />
 | 
			
		||||
                            {i18n._('toolbar.routing.allow_private')}
 | 
			
		||||
                        </span>
 | 
			
		||||
                        <Switch class="scale-90" bind:checked={privateRoads.value} />
 | 
			
		||||
                        <Switch class="scale-90" bind:checked={$privateRoads} />
 | 
			
		||||
                    </Label>
 | 
			
		||||
                </div>
 | 
			
		||||
            {/if}
 | 
			
		||||
@@ -218,9 +217,9 @@
 | 
			
		||||
 | 
			
		||||
                            if (start !== undefined) {
 | 
			
		||||
                                const lastFileId = selected[selected.length - 1].getFileId();
 | 
			
		||||
                                // routingControls
 | 
			
		||||
                                //     .get(lastFileId)
 | 
			
		||||
                                //     ?.appendAnchorWithCoordinates(start.getCoordinates());
 | 
			
		||||
                                routingControls
 | 
			
		||||
                                    .get(lastFileId)
 | 
			
		||||
                                    ?.appendAnchorWithCoordinates(start.getCoordinates());
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,11 @@
 | 
			
		||||
 | 
			
		||||
    import { i18n } from '$lib/i18n.svelte';
 | 
			
		||||
 | 
			
		||||
    export let element: HTMLElement;
 | 
			
		||||
    let {
 | 
			
		||||
        element = $bindable(),
 | 
			
		||||
    }: {
 | 
			
		||||
        element: HTMLElement | undefined;
 | 
			
		||||
    } = $props();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div bind:this={element} class="hidden">
 | 
			
		||||
@@ -17,7 +21,7 @@
 | 
			
		||||
                <Button
 | 
			
		||||
                    class="w-full px-2 py-1 h-6 justify-start"
 | 
			
		||||
                    variant="ghost"
 | 
			
		||||
                    onclick={() => element.dispatchEvent(new CustomEvent('change-start'))}
 | 
			
		||||
                    onclick={() => element?.dispatchEvent(new CustomEvent('change-start'))}
 | 
			
		||||
                >
 | 
			
		||||
                    <CirclePlay size="16" class="mr-1" />
 | 
			
		||||
                    {i18n._('toolbar.routing.start_loop_here')}
 | 
			
		||||
@@ -26,7 +30,7 @@
 | 
			
		||||
            <Button
 | 
			
		||||
                class="w-full px-2 py-1 h-6 justify-start"
 | 
			
		||||
                variant="ghost"
 | 
			
		||||
                onclick={() => element.dispatchEvent(new CustomEvent('delete'))}
 | 
			
		||||
                onclick={() => element?.dispatchEvent(new CustomEvent('delete'))}
 | 
			
		||||
            >
 | 
			
		||||
                <Trash2 size="16" class="mr-1" />
 | 
			
		||||
                {i18n._('menu.delete')}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,25 @@
 | 
			
		||||
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from 'gpx';
 | 
			
		||||
import { get, writable, type Readable } from 'svelte/store';
 | 
			
		||||
import mapboxgl from 'mapbox-gl';
 | 
			
		||||
import { route } from './utils.svelte';
 | 
			
		||||
import { route } from './routing';
 | 
			
		||||
import { toast } from 'svelte-sonner';
 | 
			
		||||
import {
 | 
			
		||||
    ListFileItem,
 | 
			
		||||
    ListTrackItem,
 | 
			
		||||
    ListTrackSegmentItem,
 | 
			
		||||
} from '$lib/components/file-list/file-list';
 | 
			
		||||
import { getClosestLinePoint, resetCursor, setGrabbingCursor } from '$lib/utils';
 | 
			
		||||
import { getClosestLinePoint } from '$lib/utils';
 | 
			
		||||
import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree';
 | 
			
		||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
 | 
			
		||||
import { settings } from '$lib/logic/settings';
 | 
			
		||||
import { selection } from '$lib/logic/selection';
 | 
			
		||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
 | 
			
		||||
import { streetViewEnabled } from '$lib/components/map/street-view-control/utils';
 | 
			
		||||
import { fileActionManager } from '$lib/logic/file-action-manager';
 | 
			
		||||
import { i18n } from '$lib/i18n.svelte';
 | 
			
		||||
import { map } from '$lib/components/map/map';
 | 
			
		||||
 | 
			
		||||
// const { streetViewSource } = settings;
 | 
			
		||||
const { streetViewSource } = settings;
 | 
			
		||||
export const canChangeStart = writable(false);
 | 
			
		||||
 | 
			
		||||
function stopPropagation(e: any) {
 | 
			
		||||
@@ -20,7 +28,6 @@ function stopPropagation(e: any) {
 | 
			
		||||
 | 
			
		||||
export class RoutingControls {
 | 
			
		||||
    active: boolean = false;
 | 
			
		||||
    map: mapboxgl.Map;
 | 
			
		||||
    fileId: string = '';
 | 
			
		||||
    file: Readable<GPXFileWithStatistics | undefined>;
 | 
			
		||||
    anchors: AnchorWithMarker[] = [];
 | 
			
		||||
@@ -39,13 +46,11 @@ export class RoutingControls {
 | 
			
		||||
    appendAnchorBinded: (e: mapboxgl.MapMouseEvent) => void = this.appendAnchor.bind(this);
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        map: mapboxgl.Map,
 | 
			
		||||
        fileId: string,
 | 
			
		||||
        file: Readable<GPXFileWithStatistics | undefined>,
 | 
			
		||||
        popup: mapboxgl.Popup,
 | 
			
		||||
        popupElement: HTMLElement
 | 
			
		||||
    ) {
 | 
			
		||||
        this.map = map;
 | 
			
		||||
        this.fileId = fileId;
 | 
			
		||||
        this.file = file;
 | 
			
		||||
        this.popup = popup;
 | 
			
		||||
@@ -88,12 +93,17 @@ export class RoutingControls {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    add() {
 | 
			
		||||
        const map_ = get(map);
 | 
			
		||||
        if (!map_) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.active = true;
 | 
			
		||||
 | 
			
		||||
        this.map.on('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
 | 
			
		||||
        this.map.on('click', this.appendAnchorBinded);
 | 
			
		||||
        this.map.on('mousemove', this.fileId, this.showTemporaryAnchorBinded);
 | 
			
		||||
        this.map.on('click', this.fileId, stopPropagation);
 | 
			
		||||
        map_.on('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
 | 
			
		||||
        map_.on('click', this.appendAnchorBinded);
 | 
			
		||||
        map_.on('mousemove', this.fileId, this.showTemporaryAnchorBinded);
 | 
			
		||||
        map_.on('click', this.fileId, stopPropagation);
 | 
			
		||||
 | 
			
		||||
        this.fileUnsubscribe = this.file.subscribe(this.updateControls.bind(this));
 | 
			
		||||
    }
 | 
			
		||||
@@ -141,25 +151,26 @@ export class RoutingControls {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    remove() {
 | 
			
		||||
        const map_ = get(map);
 | 
			
		||||
        if (!map_) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.active = false;
 | 
			
		||||
 | 
			
		||||
        for (let anchor of this.anchors) {
 | 
			
		||||
            anchor.marker.remove();
 | 
			
		||||
        }
 | 
			
		||||
        this.map.off('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
 | 
			
		||||
        this.map.off('click', this.appendAnchorBinded);
 | 
			
		||||
        this.map.off('mousemove', this.fileId, this.showTemporaryAnchorBinded);
 | 
			
		||||
        this.map.off('click', this.fileId, stopPropagation);
 | 
			
		||||
        this.map.off('mousemove', this.updateTemporaryAnchorBinded);
 | 
			
		||||
        map_.off('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
 | 
			
		||||
        map_.off('click', this.appendAnchorBinded);
 | 
			
		||||
        map_.off('mousemove', this.fileId, this.showTemporaryAnchorBinded);
 | 
			
		||||
        map_.off('click', this.fileId, stopPropagation);
 | 
			
		||||
        map_.off('mousemove', this.updateTemporaryAnchorBinded);
 | 
			
		||||
        this.temporaryAnchor.marker.remove();
 | 
			
		||||
 | 
			
		||||
        this.fileUnsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateMap(map: mapboxgl.Map) {
 | 
			
		||||
        this.map = map;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    createAnchor(
 | 
			
		||||
        point: TrackPoint,
 | 
			
		||||
        segment: TrackSegment,
 | 
			
		||||
@@ -186,13 +197,13 @@ export class RoutingControls {
 | 
			
		||||
 | 
			
		||||
        marker.on('dragstart', (e) => {
 | 
			
		||||
            this.lastDragEvent = Date.now();
 | 
			
		||||
            setGrabbingCursor();
 | 
			
		||||
            mapCursor.notify(MapCursorState.TRACKPOINT_DRAGGING, true);
 | 
			
		||||
            element.classList.remove('cursor-pointer');
 | 
			
		||||
            element.classList.add('cursor-grabbing');
 | 
			
		||||
        });
 | 
			
		||||
        marker.on('dragend', (e) => {
 | 
			
		||||
            this.lastDragEvent = Date.now();
 | 
			
		||||
            resetCursor();
 | 
			
		||||
            mapCursor.notify(MapCursorState.TRACKPOINT_DRAGGING, false);
 | 
			
		||||
            element.classList.remove('cursor-grabbing');
 | 
			
		||||
            element.classList.add('cursor-pointer');
 | 
			
		||||
            this.moveAnchor(anchor);
 | 
			
		||||
@@ -255,19 +266,24 @@ export class RoutingControls {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toggleAnchorsForZoomLevelAndBounds() {
 | 
			
		||||
        const map_ = get(map);
 | 
			
		||||
        if (!map_) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Show markers only if they are in the current zoom level and bounds
 | 
			
		||||
        this.shownAnchors.splice(0, this.shownAnchors.length);
 | 
			
		||||
 | 
			
		||||
        let center = this.map.getCenter();
 | 
			
		||||
        let bottomLeft = this.map.unproject([0, this.map.getCanvas().height]);
 | 
			
		||||
        let topRight = this.map.unproject([this.map.getCanvas().width, 0]);
 | 
			
		||||
        let center = map_.getCenter();
 | 
			
		||||
        let bottomLeft = map_.unproject([0, map_.getCanvas().height]);
 | 
			
		||||
        let topRight = map_.unproject([map_.getCanvas().width, 0]);
 | 
			
		||||
        let diagonal = bottomLeft.distanceTo(topRight);
 | 
			
		||||
 | 
			
		||||
        let zoom = this.map.getZoom();
 | 
			
		||||
        let zoom = map_.getZoom();
 | 
			
		||||
        this.anchors.forEach((anchor) => {
 | 
			
		||||
            anchor.inZoom = anchor.point._data.zoom <= zoom;
 | 
			
		||||
            if (anchor.inZoom && center.distanceTo(anchor.marker.getLngLat()) < diagonal) {
 | 
			
		||||
                anchor.marker.addTo(this.map);
 | 
			
		||||
                anchor.marker.addTo(map_);
 | 
			
		||||
                this.shownAnchors.push(anchor);
 | 
			
		||||
            } else {
 | 
			
		||||
                anchor.marker.remove();
 | 
			
		||||
@@ -276,6 +292,11 @@ export class RoutingControls {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showTemporaryAnchor(e: any) {
 | 
			
		||||
        const map_ = get(map);
 | 
			
		||||
        if (!map_) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) {
 | 
			
		||||
            // Do not not change the source point if it is already being dragged
 | 
			
		||||
            return;
 | 
			
		||||
@@ -305,25 +326,30 @@ export class RoutingControls {
 | 
			
		||||
            lat: e.lngLat.lat,
 | 
			
		||||
            lon: e.lngLat.lng,
 | 
			
		||||
        });
 | 
			
		||||
        this.temporaryAnchor.marker.setLngLat(e.lngLat).addTo(this.map);
 | 
			
		||||
        this.temporaryAnchor.marker.setLngLat(e.lngLat).addTo(map_);
 | 
			
		||||
 | 
			
		||||
        this.map.on('mousemove', this.updateTemporaryAnchorBinded);
 | 
			
		||||
        map_.on('mousemove', this.updateTemporaryAnchorBinded);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateTemporaryAnchor(e: any) {
 | 
			
		||||
        const map_ = get(map);
 | 
			
		||||
        if (!map_) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) {
 | 
			
		||||
            // Do not hide if it is being dragged, and stop listening for mousemove
 | 
			
		||||
            this.map.off('mousemove', this.updateTemporaryAnchorBinded);
 | 
			
		||||
            map_.off('mousemove', this.updateTemporaryAnchorBinded);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
            e.point.dist(this.map.project(this.temporaryAnchor.point.getCoordinates())) > 20 ||
 | 
			
		||||
            e.point.dist(map_.project(this.temporaryAnchor.point.getCoordinates())) > 20 ||
 | 
			
		||||
            this.temporaryAnchorCloseToOtherAnchor(e)
 | 
			
		||||
        ) {
 | 
			
		||||
            // Hide if too far from the layer
 | 
			
		||||
            this.temporaryAnchor.marker.remove();
 | 
			
		||||
            this.map.off('mousemove', this.updateTemporaryAnchorBinded);
 | 
			
		||||
            map_.off('mousemove', this.updateTemporaryAnchorBinded);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -331,8 +357,13 @@ export class RoutingControls {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    temporaryAnchorCloseToOtherAnchor(e: any) {
 | 
			
		||||
        const map_ = get(map);
 | 
			
		||||
        if (!map_) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (let anchor of this.shownAnchors) {
 | 
			
		||||
            if (e.point.dist(this.map.project(anchor.marker.getLngLat())) < 10) {
 | 
			
		||||
            if (e.point.dist(map_.project(anchor.marker.getLngLat())) < 10) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -482,7 +513,7 @@ export class RoutingControls {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (minInfo.trackIndex !== -1) {
 | 
			
		||||
            dbUtils.applyToFile(this.fileId, (file) =>
 | 
			
		||||
            fileActionManager.applyToFile(this.fileId, (file) =>
 | 
			
		||||
                file.replaceTrackPoints(
 | 
			
		||||
                    minInfo.trackIndex,
 | 
			
		||||
                    minInfo.segmentIndex,
 | 
			
		||||
@@ -506,12 +537,12 @@ export class RoutingControls {
 | 
			
		||||
 | 
			
		||||
        if (previousAnchor === null && nextAnchor === null) {
 | 
			
		||||
            // Only one point, remove it
 | 
			
		||||
            dbUtils.applyToFile(this.fileId, (file) =>
 | 
			
		||||
            fileActionManager.applyToFile(this.fileId, (file) =>
 | 
			
		||||
                file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, 0, [])
 | 
			
		||||
            );
 | 
			
		||||
        } else if (previousAnchor === null) {
 | 
			
		||||
            // First point, remove trackpoints until nextAnchor
 | 
			
		||||
            dbUtils.applyToFile(this.fileId, (file) =>
 | 
			
		||||
            fileActionManager.applyToFile(this.fileId, (file) =>
 | 
			
		||||
                file.replaceTrackPoints(
 | 
			
		||||
                    anchor.trackIndex,
 | 
			
		||||
                    anchor.segmentIndex,
 | 
			
		||||
@@ -522,7 +553,7 @@ export class RoutingControls {
 | 
			
		||||
            );
 | 
			
		||||
        } else if (nextAnchor === null) {
 | 
			
		||||
            // Last point, remove trackpoints from previousAnchor
 | 
			
		||||
            dbUtils.applyToFile(this.fileId, (file) => {
 | 
			
		||||
            fileActionManager.applyToFile(this.fileId, (file) => {
 | 
			
		||||
                let segment = file.getSegment(anchor.trackIndex, anchor.segmentIndex);
 | 
			
		||||
                file.replaceTrackPoints(
 | 
			
		||||
                    anchor.trackIndex,
 | 
			
		||||
@@ -558,7 +589,7 @@ export class RoutingControls {
 | 
			
		||||
        ).global.speed.moving;
 | 
			
		||||
 | 
			
		||||
        let segment = anchor.segment;
 | 
			
		||||
        dbUtils.applyToFile(this.fileId, (file) => {
 | 
			
		||||
        fileActionManager.applyToFile(this.fileId, (file) => {
 | 
			
		||||
            file.replaceTrackPoints(
 | 
			
		||||
                anchor.trackIndex,
 | 
			
		||||
                anchor.segmentIndex,
 | 
			
		||||
@@ -590,7 +621,7 @@ export class RoutingControls {
 | 
			
		||||
 | 
			
		||||
    async appendAnchorWithCoordinates(coordinates: Coordinates) {
 | 
			
		||||
        // Add a new anchor to the end of the last segment
 | 
			
		||||
        let selected = getOrderedSelection();
 | 
			
		||||
        let selected = selection.getOrderedSelection();
 | 
			
		||||
        if (selected.length === 0 || selected[selected.length - 1].getFileId() !== this.fileId) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -605,7 +636,7 @@ export class RoutingControls {
 | 
			
		||||
        newPoint._data.zoom = 0;
 | 
			
		||||
 | 
			
		||||
        if (!lastAnchor) {
 | 
			
		||||
            dbUtils.applyToFile(this.fileId, (file) => {
 | 
			
		||||
            fileActionManager.applyToFile(this.fileId, (file) => {
 | 
			
		||||
                let trackIndex = file.trk.length > 0 ? file.trk.length - 1 : 0;
 | 
			
		||||
                if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
 | 
			
		||||
                    trackIndex = item.getTrackIndex();
 | 
			
		||||
@@ -686,7 +717,7 @@ export class RoutingControls {
 | 
			
		||||
 | 
			
		||||
        if (anchors.length === 1) {
 | 
			
		||||
            // Only one anchor, update the point in the segment
 | 
			
		||||
            dbUtils.applyToFile(this.fileId, (file) =>
 | 
			
		||||
            fileActionManager.applyToFile(this.fileId, (file) =>
 | 
			
		||||
                file.replaceTrackPoints(anchors[0].trackIndex, anchors[0].segmentIndex, 0, 0, [
 | 
			
		||||
                    new TrackPoint({
 | 
			
		||||
                        attributes: targetCoordinates[0],
 | 
			
		||||
@@ -701,13 +732,13 @@ export class RoutingControls {
 | 
			
		||||
            response = await route(targetCoordinates);
 | 
			
		||||
        } catch (e: any) {
 | 
			
		||||
            if (e.message.includes('from-position not mapped in existing datafile')) {
 | 
			
		||||
                toast.error(get(_)('toolbar.routing.error.from'));
 | 
			
		||||
                toast.error(i18n._('toolbar.routing.error.from'));
 | 
			
		||||
            } else if (e.message.includes('via1-position not mapped in existing datafile')) {
 | 
			
		||||
                toast.error(get(_)('toolbar.routing.error.via'));
 | 
			
		||||
                toast.error(i18n._('toolbar.routing.error.via'));
 | 
			
		||||
            } else if (e.message.includes('to-position not mapped in existing datafile')) {
 | 
			
		||||
                toast.error(get(_)('toolbar.routing.error.to'));
 | 
			
		||||
                toast.error(i18n._('toolbar.routing.error.to'));
 | 
			
		||||
            } else if (e.message.includes('Time-out')) {
 | 
			
		||||
                toast.error(get(_)('toolbar.routing.error.timeout'));
 | 
			
		||||
                toast.error(i18n._('toolbar.routing.error.timeout'));
 | 
			
		||||
            } else {
 | 
			
		||||
                toast.error(e.message);
 | 
			
		||||
            }
 | 
			
		||||
@@ -797,7 +828,7 @@ export class RoutingControls {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        dbUtils.applyToFile(this.fileId, (file) =>
 | 
			
		||||
        fileActionManager.applyToFile(this.fileId, (file) =>
 | 
			
		||||
            file.replaceTrackPoints(
 | 
			
		||||
                anchors[0].trackIndex,
 | 
			
		||||
                anchors[0].segmentIndex,
 | 
			
		||||
@@ -818,6 +849,8 @@ export class RoutingControls {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const routingControls: Map<string, RoutingControls> = new Map();
 | 
			
		||||
 | 
			
		||||
type Anchor = {
 | 
			
		||||
    segment: TrackSegment;
 | 
			
		||||
    trackIndex: number;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import type { Coordinates } from 'gpx';
 | 
			
		||||
import { TrackPoint, distance } from 'gpx';
 | 
			
		||||
import { settings } from '$lib/logic/settings';
 | 
			
		||||
import { getElevation } from '$lib/utils';
 | 
			
		||||
import { get } from 'svelte/store';
 | 
			
		||||
 | 
			
		||||
const { routing, routingProfile, privateRoads } = settings;
 | 
			
		||||
 | 
			
		||||
@@ -17,8 +18,8 @@ export const brouterProfiles: { [key: string]: string } = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function route(points: Coordinates[]): Promise<TrackPoint[]> {
 | 
			
		||||
    if (routing.value) {
 | 
			
		||||
        return getRoute(points, brouterProfiles[routingProfile.value], privateRoads.value);
 | 
			
		||||
    if (get(routing)) {
 | 
			
		||||
        return getRoute(points, brouterProfiles[get(routingProfile)], get(privateRoads));
 | 
			
		||||
    } else {
 | 
			
		||||
        return getIntermediatePoints(points);
 | 
			
		||||
    }
 | 
			
		||||
@@ -7,16 +7,16 @@
 | 
			
		||||
    import { Slider } from '$lib/components/ui/slider';
 | 
			
		||||
    import * as Select from '$lib/components/ui/select';
 | 
			
		||||
    import { Separator } from '$lib/components/ui/separator';
 | 
			
		||||
    import { gpxStatistics, slicedGPXStatistics } from '$lib/stores';
 | 
			
		||||
    import { map } from '$lib/components/map/map';
 | 
			
		||||
    import { get } from 'svelte/store';
 | 
			
		||||
    import { i18n } from '$lib/i18n.svelte';
 | 
			
		||||
    import { onDestroy, tick } from 'svelte';
 | 
			
		||||
    import { Crop } from '@lucide/svelte';
 | 
			
		||||
    import { dbUtils } from '$lib/db';
 | 
			
		||||
    import { SplitControls } from './split-controls';
 | 
			
		||||
    import { getURLForLanguage } from '$lib/utils';
 | 
			
		||||
    import { selection } from '$lib/logic/selection';
 | 
			
		||||
    import { fileActions } from '$lib/logic/file-actions';
 | 
			
		||||
    import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
 | 
			
		||||
 | 
			
		||||
    let props: {
 | 
			
		||||
        class?: string;
 | 
			
		||||
@@ -26,16 +26,16 @@
 | 
			
		||||
    let canCrop = $state(false);
 | 
			
		||||
 | 
			
		||||
    $effect(() => {
 | 
			
		||||
        if (map.current) {
 | 
			
		||||
        if ($map) {
 | 
			
		||||
            if (splitControls) {
 | 
			
		||||
                splitControls.destroy();
 | 
			
		||||
            }
 | 
			
		||||
            splitControls = new SplitControls(map.current);
 | 
			
		||||
            splitControls = new SplitControls($map);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    let validSelection = $derived(
 | 
			
		||||
        selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
 | 
			
		||||
        $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
 | 
			
		||||
            $gpxStatistics.local.points.length > 0
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@@ -120,7 +120,7 @@
 | 
			
		||||
    <Button
 | 
			
		||||
        variant="outline"
 | 
			
		||||
        disabled={!validSelection || !canCrop}
 | 
			
		||||
        onclick={() => dbUtils.cropSelection(sliderValues[0], sliderValues[1])}
 | 
			
		||||
        onclick={() => fileActions.cropSelection(sliderValues[0], sliderValues[1])}
 | 
			
		||||
    >
 | 
			
		||||
        <Crop size="16" class="mr-1" />{i18n._('toolbar.scissors.crop')}
 | 
			
		||||
    </Button>
 | 
			
		||||
@@ -129,9 +129,9 @@
 | 
			
		||||
        <span class="shrink-0">
 | 
			
		||||
            {i18n._('toolbar.scissors.split_as')}
 | 
			
		||||
        </span>
 | 
			
		||||
        <Select.Root bind:value={splitAs.current} type="single">
 | 
			
		||||
        <Select.Root bind:value={$splitAs} type="single">
 | 
			
		||||
            <Select.Trigger class="h-8 w-fit grow">
 | 
			
		||||
                {i18n._('gpx.' + splitAs)}
 | 
			
		||||
                {i18n._('gpx.' + $splitAs)}
 | 
			
		||||
            </Select.Trigger>
 | 
			
		||||
            <Select.Content>
 | 
			
		||||
                {#each Object.values(SplitType) as splitType}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,14 @@
 | 
			
		||||
import { TrackPoint, TrackSegment } from 'gpx';
 | 
			
		||||
import mapboxgl from 'mapbox-gl';
 | 
			
		||||
import { dbUtils, getFile } from '$lib/db';
 | 
			
		||||
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
 | 
			
		||||
import { gpxStatistics } from '$lib/stores';
 | 
			
		||||
import { tool, Tool } from '$lib/components/toolbar/tools';
 | 
			
		||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
 | 
			
		||||
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
 | 
			
		||||
import { Scissors } from 'lucide-static';
 | 
			
		||||
import { selection } from '$lib/logic/selection';
 | 
			
		||||
import { gpxStatistics } from '$lib/logic/statistics';
 | 
			
		||||
import { get } from 'svelte/store';
 | 
			
		||||
import { fileStateCollection } from '$lib/logic/file-state';
 | 
			
		||||
import { fileActions } from '$lib/logic/file-actions';
 | 
			
		||||
 | 
			
		||||
export class SplitControls {
 | 
			
		||||
    active: boolean = false;
 | 
			
		||||
@@ -22,13 +24,12 @@ export class SplitControls {
 | 
			
		||||
        this.map = map;
 | 
			
		||||
 | 
			
		||||
        this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
 | 
			
		||||
        $effect(() => {
 | 
			
		||||
            tool.current, selection.value, this.addIfNeeded.bind(this);
 | 
			
		||||
        });
 | 
			
		||||
        this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
 | 
			
		||||
        this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addIfNeeded() {
 | 
			
		||||
        let scissors = tool.current === Tool.SCISSORS;
 | 
			
		||||
        let scissors = get(currentTool) === Tool.SCISSORS;
 | 
			
		||||
        if (!scissors) {
 | 
			
		||||
            if (this.active) {
 | 
			
		||||
                this.remove();
 | 
			
		||||
@@ -54,12 +55,12 @@ export class SplitControls {
 | 
			
		||||
        // Update the markers when the files change
 | 
			
		||||
        let controlIndex = 0;
 | 
			
		||||
        selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
 | 
			
		||||
            let file = getFile(fileId);
 | 
			
		||||
            let file = fileStateCollection.getFile(fileId);
 | 
			
		||||
 | 
			
		||||
            if (file) {
 | 
			
		||||
                file.forEachSegment((segment, trackIndex, segmentIndex) => {
 | 
			
		||||
                    if (
 | 
			
		||||
                        selection.value.hasAnyParent(
 | 
			
		||||
                        get(selection).hasAnyParent(
 | 
			
		||||
                            new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
 | 
			
		||||
                        )
 | 
			
		||||
                    ) {
 | 
			
		||||
@@ -163,8 +164,8 @@ export class SplitControls {
 | 
			
		||||
 | 
			
		||||
        marker.getElement().addEventListener('click', (e) => {
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            dbUtils.split(
 | 
			
		||||
                splitAs.current,
 | 
			
		||||
            fileActions.split(
 | 
			
		||||
                get(splitAs),
 | 
			
		||||
                control.fileId,
 | 
			
		||||
                control.trackIndex,
 | 
			
		||||
                control.segmentIndex,
 | 
			
		||||
 
 | 
			
		||||
@@ -6,16 +6,16 @@
 | 
			
		||||
    import * as Select from '$lib/components/ui/select';
 | 
			
		||||
    import { i18n } from '$lib/i18n.svelte';
 | 
			
		||||
    import { ListWaypointItem } from '$lib/components/file-list/file-list';
 | 
			
		||||
    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 { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
 | 
			
		||||
    import { onDestroy, onMount, untrack } from 'svelte';
 | 
			
		||||
    import { getURLForLanguage } from '$lib/utils';
 | 
			
		||||
    import { MapPin, CircleX, Save } from '@lucide/svelte';
 | 
			
		||||
    import { getSymbolKey, symbols } from '$lib/assets/symbols';
 | 
			
		||||
    import { selection } from '$lib/logic/selection';
 | 
			
		||||
    import { selectedWaypoint } from './waypoint';
 | 
			
		||||
    import { fileActions } from '$lib/logic/file-actions';
 | 
			
		||||
    import { map } from '$lib/components/map/map';
 | 
			
		||||
    import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
 | 
			
		||||
 | 
			
		||||
    let props: {
 | 
			
		||||
        class?: string;
 | 
			
		||||
@@ -24,20 +24,46 @@
 | 
			
		||||
    let name = $state('');
 | 
			
		||||
    let description = $state('');
 | 
			
		||||
    let link = $state('');
 | 
			
		||||
    let symbolKey = $state('');
 | 
			
		||||
    let sym = $state('');
 | 
			
		||||
    let longitude = $state(0);
 | 
			
		||||
    let latitude = $state(0);
 | 
			
		||||
    let symbolKey = $derived(getSymbolKey(sym));
 | 
			
		||||
 | 
			
		||||
    let canCreate = $derived(selection.value.size > 0);
 | 
			
		||||
    let canCreate = $derived($selection.size > 0);
 | 
			
		||||
 | 
			
		||||
    function resetWaypointData() {
 | 
			
		||||
        name = '';
 | 
			
		||||
        description = '';
 | 
			
		||||
        link = '';
 | 
			
		||||
        symbolKey = '';
 | 
			
		||||
        longitude = 0;
 | 
			
		||||
        latitude = 0;
 | 
			
		||||
    }
 | 
			
		||||
    let sortedSymbols = $derived(
 | 
			
		||||
        Object.entries(symbols).sort((a, b) => {
 | 
			
		||||
            return i18n
 | 
			
		||||
                ._(`gpx.symbol.${a[0]}`)
 | 
			
		||||
                .localeCompare(i18n._(`gpx.symbol.${b[0]}`), i18n.lang);
 | 
			
		||||
        })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    $effect(() => {
 | 
			
		||||
        if ($selectedWaypoint) {
 | 
			
		||||
            const wpt = $selectedWaypoint[0];
 | 
			
		||||
            untrack(() => {
 | 
			
		||||
                name = wpt.name ?? '';
 | 
			
		||||
                description = wpt.desc ?? '';
 | 
			
		||||
                if (wpt.cmt !== undefined && wpt.cmt !== wpt.desc) {
 | 
			
		||||
                    description += '\n\n' + wpt.cmt;
 | 
			
		||||
                }
 | 
			
		||||
                link = wpt.link?.attributes?.href ?? '';
 | 
			
		||||
                sym = wpt.sym ?? '';
 | 
			
		||||
                longitude = parseFloat(wpt.getLongitude().toFixed(6));
 | 
			
		||||
                latitude = parseFloat(wpt.getLatitude().toFixed(6));
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            untrack(() => {
 | 
			
		||||
                name = '';
 | 
			
		||||
                description = '';
 | 
			
		||||
                link = '';
 | 
			
		||||
                sym = '';
 | 
			
		||||
                longitude = 0;
 | 
			
		||||
                latitude = 0;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function createOrUpdateWaypoint() {
 | 
			
		||||
        if (typeof latitude === 'string') {
 | 
			
		||||
@@ -49,7 +75,7 @@
 | 
			
		||||
        latitude = parseFloat(latitude.toFixed(6));
 | 
			
		||||
        longitude = parseFloat(longitude.toFixed(6));
 | 
			
		||||
 | 
			
		||||
        dbUtils.addOrUpdateWaypoint(
 | 
			
		||||
        fileActions.addOrUpdateWaypoint(
 | 
			
		||||
            {
 | 
			
		||||
                attributes: {
 | 
			
		||||
                    lat: latitude,
 | 
			
		||||
@@ -59,7 +85,7 @@
 | 
			
		||||
                desc: description.length > 0 ? description : undefined,
 | 
			
		||||
                cmt: description.length > 0 ? description : undefined,
 | 
			
		||||
                link: link.length > 0 ? { attributes: { href: link } } : undefined,
 | 
			
		||||
                sym: symbols[symbolKey]?.value ?? '',
 | 
			
		||||
                sym: sym,
 | 
			
		||||
            },
 | 
			
		||||
            selectedWaypoint.wpt && selectedWaypoint.fileId
 | 
			
		||||
                ? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index)
 | 
			
		||||
@@ -67,7 +93,6 @@
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        selectedWaypoint.reset();
 | 
			
		||||
        resetWaypointData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setCoordinates(e: any) {
 | 
			
		||||
@@ -75,22 +100,18 @@
 | 
			
		||||
        longitude = e.lngLat.lng.toFixed(6);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let sortedSymbols = $derived(
 | 
			
		||||
        Object.entries(symbols).sort((a, b) => {
 | 
			
		||||
            return i18n
 | 
			
		||||
                ._(`gpx.symbol.${a[0]}`)
 | 
			
		||||
                .localeCompare(i18n._(`gpx.symbol.${b[0]}`), i18n.lang);
 | 
			
		||||
        })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    onMount(() => {
 | 
			
		||||
        map.value?.on('click', setCoordinates);
 | 
			
		||||
        // setCrosshairCursor();
 | 
			
		||||
        if ($map) {
 | 
			
		||||
            $map.on('click', setCoordinates);
 | 
			
		||||
            mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, true);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    onDestroy(() => {
 | 
			
		||||
        map.value?.off('click', setCoordinates);
 | 
			
		||||
        // resetCursor();
 | 
			
		||||
        if ($map) {
 | 
			
		||||
            $map.off('click', setCoordinates);
 | 
			
		||||
            mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@@ -101,25 +122,25 @@
 | 
			
		||||
            bind:value={name}
 | 
			
		||||
            id="name"
 | 
			
		||||
            class="font-semibold h-8"
 | 
			
		||||
            disabled={!canCreate && !selectedWaypoint.wpt}
 | 
			
		||||
            disabled={!canCreate && !$selectedWaypoint}
 | 
			
		||||
        />
 | 
			
		||||
        <Label for="description">{i18n._('menu.metadata.description')}</Label>
 | 
			
		||||
        <Textarea
 | 
			
		||||
            bind:value={description}
 | 
			
		||||
            id="description"
 | 
			
		||||
            disabled={!canCreate && !selectedWaypoint.wpt}
 | 
			
		||||
            disabled={!canCreate && !$selectedWaypoint}
 | 
			
		||||
        />
 | 
			
		||||
        <Label for="symbol">{i18n._('toolbar.waypoint.icon')}</Label>
 | 
			
		||||
        <Select.Root bind:value={symbolKey} type="single">
 | 
			
		||||
        <Select.Root bind:value={sym} type="single">
 | 
			
		||||
            <Select.Trigger
 | 
			
		||||
                id="symbol"
 | 
			
		||||
                class="w-full h-8"
 | 
			
		||||
                disabled={!canCreate && !selectedWaypoint.wpt}
 | 
			
		||||
                disabled={!canCreate && !$selectedWaypoint}
 | 
			
		||||
            >
 | 
			
		||||
                {#if symbolKey in symbols}
 | 
			
		||||
                {#if symbolKey}
 | 
			
		||||
                    {i18n._(`gpx.symbol.${symbolKey}`)}
 | 
			
		||||
                {:else}
 | 
			
		||||
                    {symbolKey}
 | 
			
		||||
                    {sym}
 | 
			
		||||
                {/if}
 | 
			
		||||
            </Select.Trigger>
 | 
			
		||||
            <Select.Content class="max-h-60 overflow-y-scroll">
 | 
			
		||||
@@ -127,11 +148,8 @@
 | 
			
		||||
                    <Select.Item value={symbol.value}>
 | 
			
		||||
                        <span>
 | 
			
		||||
                            {#if symbol.icon}
 | 
			
		||||
                                <svelte:component
 | 
			
		||||
                                    this={symbol.icon}
 | 
			
		||||
                                    size="14"
 | 
			
		||||
                                    class="inline-block align-sub mr-0.5"
 | 
			
		||||
                                />
 | 
			
		||||
                                {@const Component = symbol.icon}
 | 
			
		||||
                                <Component size="14" class="inline-block align-sub mr-0.5" />
 | 
			
		||||
                            {:else}
 | 
			
		||||
                                <span class="w-4 inline-block"></span>
 | 
			
		||||
                            {/if}
 | 
			
		||||
@@ -146,7 +164,7 @@
 | 
			
		||||
            bind:value={link}
 | 
			
		||||
            id="link"
 | 
			
		||||
            class="h-8"
 | 
			
		||||
            disabled={!canCreate && !selectedWaypoint.wpt}
 | 
			
		||||
            disabled={!canCreate && !$selectedWaypoint}
 | 
			
		||||
        />
 | 
			
		||||
        <div class="flex flex-row gap-2">
 | 
			
		||||
            <div class="grow">
 | 
			
		||||
@@ -159,7 +177,7 @@
 | 
			
		||||
                    min={-90}
 | 
			
		||||
                    max={90}
 | 
			
		||||
                    class="text-xs h-8"
 | 
			
		||||
                    disabled={!canCreate && !selectedWaypoint.wpt}
 | 
			
		||||
                    disabled={!canCreate && !$selectedWaypoint}
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="grow">
 | 
			
		||||
@@ -172,7 +190,7 @@
 | 
			
		||||
                    min={-180}
 | 
			
		||||
                    max={180}
 | 
			
		||||
                    class="text-xs h-8"
 | 
			
		||||
                    disabled={!canCreate && !selectedWaypoint.wpt}
 | 
			
		||||
                    disabled={!canCreate && !$selectedWaypoint}
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -180,11 +198,11 @@
 | 
			
		||||
    <div class="flex flex-row gap-2 items-center">
 | 
			
		||||
        <Button
 | 
			
		||||
            variant="outline"
 | 
			
		||||
            disabled={!canCreate && !selectedWaypoint.wpt}
 | 
			
		||||
            disabled={!canCreate && !$selectedWaypoint}
 | 
			
		||||
            class="grow whitespace-normal h-fit"
 | 
			
		||||
            onclick={createOrUpdateWaypoint}
 | 
			
		||||
        >
 | 
			
		||||
            {#if selectedWaypoint.wpt}
 | 
			
		||||
            {#if $selectedWaypoint}
 | 
			
		||||
                <Save size="16" class="mr-1 shrink-0" />
 | 
			
		||||
                {i18n._('menu.metadata.save')}
 | 
			
		||||
            {:else}
 | 
			
		||||
@@ -192,18 +210,12 @@
 | 
			
		||||
                {i18n._('toolbar.waypoint.create')}
 | 
			
		||||
            {/if}
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button
 | 
			
		||||
            variant="outline"
 | 
			
		||||
            onclick={() => {
 | 
			
		||||
                selectedWaypoint.reset();
 | 
			
		||||
                resetWaypointData();
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
        <Button variant="outline" onclick={() => selectedWaypoint.reset()}>
 | 
			
		||||
            <CircleX size="16" />
 | 
			
		||||
        </Button>
 | 
			
		||||
    </div>
 | 
			
		||||
    <Help link={getURLForLanguage(i18n.lang, '/help/toolbar/poi')}>
 | 
			
		||||
        {#if selectedWaypoint.wpt || canCreate}
 | 
			
		||||
        {#if $selectedWaypoint || canCreate}
 | 
			
		||||
            {i18n._('toolbar.waypoint.help')}
 | 
			
		||||
        {:else}
 | 
			
		||||
            {i18n._('toolbar.waypoint.help_no_selection')}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import { get, writable, type Writable } from 'svelte/store';
 | 
			
		||||
 | 
			
		||||
export class WaypointSelection {
 | 
			
		||||
    private _selection: Writable<[Waypoint, string] | undefined>;
 | 
			
		||||
    private _fileUnsubscribe: (() => void) | undefined;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this._selection = writable(undefined);
 | 
			
		||||
@@ -18,15 +19,37 @@ export class WaypointSelection {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    subscribe(
 | 
			
		||||
        run: (value: [Waypoint, string] | undefined) => void,
 | 
			
		||||
        invalidate?: (value?: [Waypoint, string] | undefined) => void
 | 
			
		||||
    ) {
 | 
			
		||||
        return this._selection.subscribe(run, invalidate);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    set(value: [Waypoint, string] | undefined) {
 | 
			
		||||
        this._selection.set(value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    update() {
 | 
			
		||||
        if (this._fileUnsubscribe) {
 | 
			
		||||
            this._fileUnsubscribe();
 | 
			
		||||
            this._fileUnsubscribe = undefined;
 | 
			
		||||
        }
 | 
			
		||||
        this._selection.update(() => {
 | 
			
		||||
            if (get(settings.treeFileView) && get(selection).size === 1) {
 | 
			
		||||
                let item = get(selection).getSelected()[0];
 | 
			
		||||
                if (item instanceof ListWaypointItem) {
 | 
			
		||||
                    let file = fileStateCollection.getFile(item.getFileId());
 | 
			
		||||
                    let waypoint = file?.wpt[item.getWaypointIndex()];
 | 
			
		||||
                    if (waypoint) {
 | 
			
		||||
                        return [waypoint, item.getFileId()];
 | 
			
		||||
                    let fileState = fileStateCollection.getFileState(item.getFileId());
 | 
			
		||||
                    if (fileState) {
 | 
			
		||||
                        let first = true;
 | 
			
		||||
                        this._fileUnsubscribe = fileState.subscribe(() => {
 | 
			
		||||
                            if (first) first = false;
 | 
			
		||||
                            else this.update();
 | 
			
		||||
                        });
 | 
			
		||||
                        let waypoint = fileState.file?.wpt[item.getWaypointIndex()];
 | 
			
		||||
                        if (waypoint) {
 | 
			
		||||
                            return [waypoint, item.getFileId()];
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@@ -47,34 +70,6 @@ export class WaypointSelection {
 | 
			
		||||
        const selection = get(this._selection);
 | 
			
		||||
        return selection ? selection[1] : undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO update the waypoint data if the file changes
 | 
			
		||||
    // function updateWaypointData(fileStore: GPXFileWithStatistics | undefined) {
 | 
			
		||||
    //     if (selectedWaypoint.wpt) {
 | 
			
		||||
    //         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 ?? '';
 | 
			
		||||
    //                 symbolKey = getSymbolKey(symbol) ?? symbol ?? '';
 | 
			
		||||
    //                 longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
 | 
			
		||||
    //                 latitude = parseFloat($selectedWaypoint[0].getLatitude().toFixed(6));
 | 
			
		||||
    //             } else {
 | 
			
		||||
    //                 selectedWaypoint.reset();
 | 
			
		||||
    //             }
 | 
			
		||||
    //         } else {
 | 
			
		||||
    //             selectedWaypoint.reset();
 | 
			
		||||
    //         }
 | 
			
		||||
    //     }
 | 
			
		||||
    // }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const selectedWaypoint = new WaypointSelection();
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minify
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minimitzar
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Zjednodušit
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minify
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minimieren
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minify
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minify
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minimizar
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Txikiagotu
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minify
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minifier
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minify
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minimalizálás
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minimizza
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minify
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minify
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minify
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Verkleinen
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minify
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minify
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minificar
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minify
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minify
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minify
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minify
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minimera
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Küçült
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minify
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: Minify
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ title: 精简 GPS 点数量
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { Funnel } from '@lucide/svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
 | 
			
		||||
    import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -151,6 +151,7 @@ export class GPXFileStateCollectionObserver {
 | 
			
		||||
    private _onFileAdded: GPXFileStateCallback;
 | 
			
		||||
    private _onFileRemoved: (fileId: string) => void;
 | 
			
		||||
    private _onDestroy: () => void;
 | 
			
		||||
    private _unsubscribe: () => void;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        onFileAdded: GPXFileStateCallback,
 | 
			
		||||
@@ -162,7 +163,7 @@ export class GPXFileStateCollectionObserver {
 | 
			
		||||
        this._onFileRemoved = onFileRemoved;
 | 
			
		||||
        this._onDestroy = onDestroy;
 | 
			
		||||
 | 
			
		||||
        fileStateCollection.subscribe((files) => {
 | 
			
		||||
        this._unsubscribe = fileStateCollection.subscribe((files) => {
 | 
			
		||||
            this._fileIds.forEach((fileId) => {
 | 
			
		||||
                if (!files.has(fileId)) {
 | 
			
		||||
                    this._onFileRemoved(fileId);
 | 
			
		||||
@@ -180,5 +181,6 @@ export class GPXFileStateCollectionObserver {
 | 
			
		||||
 | 
			
		||||
    destroy() {
 | 
			
		||||
        this._onDestroy();
 | 
			
		||||
        this._unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										55
									
								
								website/src/lib/logic/map-cursor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								website/src/lib/logic/map-cursor.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
import { map } from '$lib/components/map/map';
 | 
			
		||||
import { get, writable, type Writable } from 'svelte/store';
 | 
			
		||||
 | 
			
		||||
export enum MapCursorState {
 | 
			
		||||
    DEFAULT,
 | 
			
		||||
    LAYER_HOVER,
 | 
			
		||||
    WAYPOINT_DRAGGING,
 | 
			
		||||
    TRACKPOINT_DRAGGING,
 | 
			
		||||
    TOOL_WITH_CROSSHAIR,
 | 
			
		||||
    SCISSORS,
 | 
			
		||||
    MAPILLARY_HOVER,
 | 
			
		||||
    STREET_VIEW_CROSSHAIR,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const scissorsCursor = `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" version="1.1"><path d="M 3.200 3.200 C 0.441 5.959, 2.384 9.516, 7 10.154 C 10.466 10.634, 10.187 13.359, 6.607 13.990 C 2.934 14.637, 1.078 17.314, 2.612 19.750 C 4.899 23.380, 10 21.935, 10 17.657 C 10 16.445, 12.405 13.128, 15.693 9.805 C 18.824 6.641, 21.066 3.732, 20.674 3.341 C 20.283 2.950, 18.212 4.340, 16.072 6.430 C 12.019 10.388, 10 10.458, 10 6.641 C 10 2.602, 5.882 0.518, 3.200 3.200 M 4.446 5.087 C 3.416 6.755, 5.733 8.667, 7.113 7.287 C 8.267 6.133, 7.545 4, 6 4 C 5.515 4, 4.816 4.489, 4.446 5.087 M 14 14.813 C 14 16.187, 19.935 21.398, 20.667 20.667 C 21.045 20.289, 20.065 18.634, 18.490 16.990 C 15.661 14.036, 14 13.231, 14 14.813 M 4.446 17.087 C 3.416 18.755, 5.733 20.667, 7.113 19.287 C 8.267 18.133, 7.545 16, 6 16 C 5.515 16, 4.816 16.489, 4.446 17.087" stroke="black" stroke-width="1.2" fill="white" fill-rule="evenodd"/></svg>') 12 12, auto`;
 | 
			
		||||
const cursorStyles = {
 | 
			
		||||
    [MapCursorState.DEFAULT]: 'default',
 | 
			
		||||
    [MapCursorState.LAYER_HOVER]: 'pointer',
 | 
			
		||||
    [MapCursorState.WAYPOINT_DRAGGING]: 'grabbing',
 | 
			
		||||
    [MapCursorState.TRACKPOINT_DRAGGING]: 'grabbing',
 | 
			
		||||
    [MapCursorState.TOOL_WITH_CROSSHAIR]: 'crosshair',
 | 
			
		||||
    [MapCursorState.SCISSORS]: scissorsCursor,
 | 
			
		||||
    [MapCursorState.MAPILLARY_HOVER]: 'pointer',
 | 
			
		||||
    [MapCursorState.STREET_VIEW_CROSSHAIR]: 'crosshair',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export class MapCursor {
 | 
			
		||||
    private _states: Writable<Set<MapCursorState>>;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this._states = writable(new Set());
 | 
			
		||||
        this._states.subscribe((states) => {
 | 
			
		||||
            let state = states.entries().reduce((max, entry) => {
 | 
			
		||||
                return entry[0] > max ? entry[0] : max;
 | 
			
		||||
            }, MapCursorState.DEFAULT);
 | 
			
		||||
            let canvas = get(map)?.getCanvas();
 | 
			
		||||
            if (canvas) {
 | 
			
		||||
                canvas.style.cursor = cursorStyles[state];
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    notify(cursorState: MapCursorState, isActive: boolean) {
 | 
			
		||||
        this._states.update((states) => {
 | 
			
		||||
            if (isActive) {
 | 
			
		||||
                states.add(cursorState);
 | 
			
		||||
            } else {
 | 
			
		||||
                states.delete(cursorState);
 | 
			
		||||
            }
 | 
			
		||||
            return states;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const mapCursor = new MapCursor();
 | 
			
		||||
@@ -14,6 +14,7 @@ import { settings } from '$lib/logic/settings';
 | 
			
		||||
import type { GPXFile } from 'gpx';
 | 
			
		||||
import { get, writable, type Readable, type Writable } from 'svelte/store';
 | 
			
		||||
import { SelectionTreeType } from '$lib/logic/selection-tree';
 | 
			
		||||
import { tick } from 'svelte';
 | 
			
		||||
 | 
			
		||||
export class Selection {
 | 
			
		||||
    private _selection: Writable<SelectionTreeType>;
 | 
			
		||||
@@ -100,6 +101,15 @@ export class Selection {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    selectFileWhenLoaded(fileId: string) {
 | 
			
		||||
        const unsubscribe = fileStateCollection.subscribe((files) => {
 | 
			
		||||
            if (files.has(fileId)) {
 | 
			
		||||
                this.selectFile(fileId);
 | 
			
		||||
                unsubscribe();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    set(items: ListItem[]) {
 | 
			
		||||
        this._selection.update(($selection) => {
 | 
			
		||||
            $selection.clear();
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,9 @@ import {
 | 
			
		||||
    ListWaypointsItem,
 | 
			
		||||
} from '$lib/components/file-list/file-list';
 | 
			
		||||
import { get, writable, type Writable } from 'svelte/store';
 | 
			
		||||
import { settings } from '$lib/logic/settings';
 | 
			
		||||
 | 
			
		||||
const { fileOrder } = settings;
 | 
			
		||||
 | 
			
		||||
export class SelectedGPXStatistics {
 | 
			
		||||
    private _statistics: Writable<GPXStatistics>;
 | 
			
		||||
@@ -22,6 +25,7 @@ export class SelectedGPXStatistics {
 | 
			
		||||
        this._statistics = writable(new GPXStatistics());
 | 
			
		||||
        this._files = new Map();
 | 
			
		||||
        selection.subscribe(() => this.update());
 | 
			
		||||
        fileOrder.subscribe(() => this.update());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    subscribe(run: (value: GPXStatistics) => void, invalidate?: (value?: GPXStatistics) => void) {
 | 
			
		||||
 
 | 
			
		||||
@@ -24,17 +24,6 @@
 | 
			
		||||
 | 
			
		||||
// export const routingControls: Map<string, RoutingControls> = new Map();
 | 
			
		||||
 | 
			
		||||
// export function selectFileWhenLoaded(fileId: string) {
 | 
			
		||||
//     const unsubscribe = fileObservers.subscribe((files) => {
 | 
			
		||||
//         if (files.has(fileId)) {
 | 
			
		||||
//             tick().then(() => {
 | 
			
		||||
//                 selectFile(fileId);
 | 
			
		||||
//             });
 | 
			
		||||
//             unsubscribe();
 | 
			
		||||
//         }
 | 
			
		||||
//     });
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
// export const allHidden = writable(false);
 | 
			
		||||
 | 
			
		||||
// export function updateAllHidden() {
 | 
			
		||||
 
 | 
			
		||||
@@ -126,34 +126,6 @@ export function getElevation(
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let previousCursors: string[] = [];
 | 
			
		||||
export function setCursor(canvas: HTMLCanvasElement, cursor: string) {
 | 
			
		||||
    previousCursors.push(canvas.style.cursor);
 | 
			
		||||
    canvas.style.cursor = cursor;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function resetCursor(canvas: HTMLCanvasElement) {
 | 
			
		||||
    canvas.style.cursor = previousCursors.pop() ?? '';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function setPointerCursor(canvas: HTMLCanvasElement) {
 | 
			
		||||
    setCursor(canvas, 'pointer');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function setGrabbingCursor(canvas: HTMLCanvasElement) {
 | 
			
		||||
    setCursor(canvas, 'grabbing');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function setCrosshairCursor(canvas: HTMLCanvasElement) {
 | 
			
		||||
    setCursor(canvas, 'crosshair');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const scissorsCursor = `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" version="1.1"><path d="M 3.200 3.200 C 0.441 5.959, 2.384 9.516, 7 10.154 C 10.466 10.634, 10.187 13.359, 6.607 13.990 C 2.934 14.637, 1.078 17.314, 2.612 19.750 C 4.899 23.380, 10 21.935, 10 17.657 C 10 16.445, 12.405 13.128, 15.693 9.805 C 18.824 6.641, 21.066 3.732, 20.674 3.341 C 20.283 2.950, 18.212 4.340, 16.072 6.430 C 12.019 10.388, 10 10.458, 10 6.641 C 10 2.602, 5.882 0.518, 3.200 3.200 M 4.446 5.087 C 3.416 6.755, 5.733 8.667, 7.113 7.287 C 8.267 6.133, 7.545 4, 6 4 C 5.515 4, 4.816 4.489, 4.446 5.087 M 14 14.813 C 14 16.187, 19.935 21.398, 20.667 20.667 C 21.045 20.289, 20.065 18.634, 18.490 16.990 C 15.661 14.036, 14 13.231, 14 14.813 M 4.446 17.087 C 3.416 18.755, 5.733 20.667, 7.113 19.287 C 8.267 18.133, 7.545 16, 6 16 C 5.515 16, 4.816 16.489, 4.446 17.087" stroke="black" stroke-width="1.2" fill="white" fill-rule="evenodd"/></svg>') 12 12, auto`;
 | 
			
		||||
 | 
			
		||||
export function setScissorsCursor(canvas: HTMLCanvasElement) {
 | 
			
		||||
    setCursor(canvas, scissorsCursor);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isMac() {
 | 
			
		||||
    return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
    import DocsContainer from '$lib/components/docs/DocsContainer.svelte';
 | 
			
		||||
    import Logo from '$lib/components/Logo.svelte';
 | 
			
		||||
    // import ElevationProfile from '$lib/components/ElevationProfile.svelte';
 | 
			
		||||
    // import GPXStatistics from '$lib/components/GPXStatistics.svelte';
 | 
			
		||||
    import GPXStatistics from '$lib/components/GPXStatistics.svelte';
 | 
			
		||||
    // import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
 | 
			
		||||
    import {
 | 
			
		||||
        BookOpenText,
 | 
			
		||||
@@ -20,7 +20,7 @@
 | 
			
		||||
    import { exampleGPXFile } from '$lib/assets/example';
 | 
			
		||||
    import { writable } from 'svelte/store';
 | 
			
		||||
    // import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
 | 
			
		||||
    // import { tool, Tool } from '$lib/components/toolbar/utils.svelte';
 | 
			
		||||
    import { currentTool, Tool } from '$lib/components/toolbar/tools';
 | 
			
		||||
    import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
 | 
			
		||||
    let {
 | 
			
		||||
@@ -38,19 +38,19 @@
 | 
			
		||||
    let additionalDatasets = writable(['speed', 'atemp']);
 | 
			
		||||
    let elevationFill = writable<'slope' | 'surface' | undefined>(undefined);
 | 
			
		||||
 | 
			
		||||
    // onMount(() => {
 | 
			
		||||
    //     tool.current = Tool.SCISSORS;
 | 
			
		||||
    // });
 | 
			
		||||
    onMount(() => {
 | 
			
		||||
        $currentTool = Tool.SCISSORS;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // $effect(() => {
 | 
			
		||||
    //     if (tool.current !== Tool.SCISSORS) {
 | 
			
		||||
    //         tool.current = Tool.SCISSORS;
 | 
			
		||||
    //     }
 | 
			
		||||
    // });
 | 
			
		||||
    $effect(() => {
 | 
			
		||||
        if ($currentTool !== Tool.SCISSORS) {
 | 
			
		||||
            $currentTool = Tool.SCISSORS;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // onDestroy(() => {
 | 
			
		||||
    //     tool.current = null;
 | 
			
		||||
    // });
 | 
			
		||||
    onDestroy(() => {
 | 
			
		||||
        $currentTool = null;
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="space-y-24 my-24">
 | 
			
		||||
@@ -199,12 +199,12 @@
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="flex flex-col items-center">
 | 
			
		||||
            <div class="h-10 w-fit">
 | 
			
		||||
                <!-- <GPXStatistics
 | 
			
		||||
                <GPXStatistics
 | 
			
		||||
                    {gpxStatistics}
 | 
			
		||||
                    {slicedGPXStatistics}
 | 
			
		||||
                    panelSize={192}
 | 
			
		||||
                    orientation={'horizontal'}
 | 
			
		||||
                /> -->
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
    import GPXStatistics from '$lib/components/GPXStatistics.svelte';
 | 
			
		||||
    import Map from '$lib/components/map/Map.svelte';
 | 
			
		||||
    import Menu from '$lib/components/Menu.svelte';
 | 
			
		||||
    // import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
 | 
			
		||||
    import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
 | 
			
		||||
    import StreetViewControl from '$lib/components/map/street-view-control/StreetViewControl.svelte';
 | 
			
		||||
    import LayerControl from '$lib/components/map/layer-control/LayerControl.svelte';
 | 
			
		||||
    // import CoordinatesPopup from '$lib/components/map/CoordinatesPopup.svelte';
 | 
			
		||||
@@ -101,7 +101,7 @@
 | 
			
		||||
            <div
 | 
			
		||||
                class="absolute top-0 bottom-0 left-0 z-20 flex flex-col justify-center pointer-events-none"
 | 
			
		||||
            >
 | 
			
		||||
                <!-- <Toolbar /> -->
 | 
			
		||||
                <Toolbar />
 | 
			
		||||
            </div>
 | 
			
		||||
            <Map class="h-full {$treeFileView ? '' : 'horizontal'}" />
 | 
			
		||||
            <StreetViewControl />
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user