diff --git a/website/src/lib/components/file-list/FileList.svelte b/website/src/lib/components/file-list/FileList.svelte index 973d14d6..92433407 100644 --- a/website/src/lib/components/file-list/FileList.svelte +++ b/website/src/lib/components/file-list/FileList.svelte @@ -2,15 +2,41 @@ import { ScrollArea } from '$lib/components/ui/scroll-area/index'; import FileListNode from './FileListNode.svelte'; - import { fileObservers } from '$lib/db'; + import { fileObservers, settings } from '$lib/db'; import { setContext } from 'svelte'; - import { ListRootItem } from './FileList'; + import { ListFileItem, ListRootItem } from './FileList'; + import { selection } from './Selection'; export let orientation: 'vertical' | 'horizontal'; export let recursive = false; setContext('orientation', orientation); setContext('recursive', recursive); + + const { verticalFileView } = settings; + + verticalFileView.subscribe(($vertical) => { + if ($vertical) { + selection.update(($selection) => { + $selection.forEach((item) => { + if ($selection.hasAnyChildren(item, false)) { + $selection.toggle(item); + } + }); + return $selection; + }); + } else { + selection.update(($selection) => { + $selection.forEach((item) => { + if (!(item instanceof ListFileItem)) { + $selection.toggle(item); + $selection.set(new ListFileItem(item.getFileId()), true); + } + }); + return $selection; + }); + } + }); { if (item instanceof ListWaypointItem) { - let layer = get(gpxLayers).get(item.getFileId()); + let layer = gpxLayers.get(item.getFileId()); let fileStore = get(fileObservers).get(item.getFileId()); if (layer && fileStore) { let waypoint = get(fileStore)?.file.wpt[item.getWaypointIndex()]; @@ -61,7 +61,7 @@ }} on:mouseleave={() => { if (item instanceof ListWaypointItem) { - let layer = get(gpxLayers).get(item.getFileId()); + let layer = gpxLayers.get(item.getFileId()); if (layer) { layer.hideWaypointPopup(); } diff --git a/website/src/lib/components/file-list/Selection.ts b/website/src/lib/components/file-list/Selection.ts index 320fd436..546203cd 100644 --- a/website/src/lib/components/file-list/Selection.ts +++ b/website/src/lib/components/file-list/Selection.ts @@ -87,7 +87,7 @@ export function applyToOrderedSelectedItemsFromFile(callback: (fileId: string, l } else if (a instanceof ListWaypointItem && b instanceof ListWaypointItem) { return b.getWaypointIndex() - a.getWaypointIndex(); } - return 0; + return b.level - a.level; }); callback(fileId, level, items); diff --git a/website/src/lib/components/gpx-layer/DistanceMarkers.ts b/website/src/lib/components/gpx-layer/DistanceMarkers.ts index a60baef1..2a350ea1 100644 --- a/website/src/lib/components/gpx-layer/DistanceMarkers.ts +++ b/website/src/lib/components/gpx-layer/DistanceMarkers.ts @@ -14,11 +14,7 @@ export class DistanceMarkers { gpxStatistics.subscribe(this.updateBinded); distanceMarkers.subscribe(this.updateBinded); - distanceUnits.subscribe(() => { - if (get(distanceMarkers)) { - this.update(); - } - }); + distanceUnits.subscribe(this.updateBinded); } update() { diff --git a/website/src/lib/components/gpx-layer/GPXLayer.ts b/website/src/lib/components/gpx-layer/GPXLayer.ts index 6111d71a..e6ca0faa 100644 --- a/website/src/lib/components/gpx-layer/GPXLayer.ts +++ b/website/src/lib/components/gpx-layer/GPXLayer.ts @@ -1,14 +1,15 @@ import { map, currentTool, Tool } from "$lib/stores"; -import { settings, type GPXFileWithStatistics } from "$lib/db"; +import { settings, type GPXFileWithStatistics, dbUtils } from "$lib/db"; import { get, type Readable } from "svelte/store"; import mapboxgl from "mapbox-gl"; import { currentWaypoint, waypointPopup } from "./WaypointPopup"; import { addSelectItem, selectItem, selection } from "$lib/components/file-list/Selection"; import { ListTrackSegmentItem, type ListItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList"; import type { Waypoint } from "gpx"; +import { produce } from "immer"; let defaultWeight = 5; -let defaultOpacity = 0.7; +let defaultOpacity = 0.6; const colors = [ '#ff0000', @@ -48,7 +49,8 @@ export class GPXLayer { file: Readable; layerColor: string; markers: mapboxgl.Marker[] = []; - selected: ListItem[] = []; + selected: boolean = false; + draggable: boolean; unsubscribe: Function[] = []; updateBinded: () => void = this.update.bind(this); @@ -61,16 +63,26 @@ export class GPXLayer { this.layerColor = getColor(); this.unsubscribe.push(file.subscribe(this.updateBinded)); this.unsubscribe.push(selection.subscribe($selection => { - let selected = $selection.getChild(fileId)?.getSelected() || []; - if (selected.length !== this.selected.length || selected.some((item, index) => item !== this.selected[index])) { - this.selected = selected; + let newSelected = $selection.hasAnyChildren(new ListFileItem(this.fileId)); + if (this.selected || newSelected) { + this.selected = newSelected; this.update(); - if (this.selected.length > 0) { - this.moveToFront(); - } + } + if (newSelected) { + this.moveToFront(); } })); this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded)); + this.unsubscribe.push(currentTool.subscribe(tool => { + if (tool === Tool.WAYPOINT && !this.draggable) { + this.draggable = true; + this.markers.forEach(marker => marker.setDraggable(true)); + } else if (tool !== Tool.WAYPOINT && this.draggable) { + this.draggable = false; + this.markers.forEach(marker => marker.setDraggable(false)); + } + })); + this.draggable = get(currentTool) === Tool.WAYPOINT; this.map.on('style.load', this.updateBinded); } @@ -144,30 +156,67 @@ export class GPXLayer { } let markerIndex = 0; - file.wpt.forEach((waypoint) => { // Update markers - if (markerIndex < this.markers.length) { - this.markers[markerIndex].setLngLat(waypoint.getCoordinates()); - Object.defineProperty(this.markers[markerIndex], '_waypoint', { value: waypoint, writable: true }); - } else { - let marker = new mapboxgl.Marker().setLngLat(waypoint.getCoordinates()); - Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true }); - marker.getElement().addEventListener('mouseover', (e) => { - this.showWaypointPopup(marker._waypoint); - e.stopPropagation(); - }); - marker.getElement().addEventListener('mouseout', () => { - this.hideWaypointPopup(); - }); - marker.getElement().addEventListener('click', (e) => { - if (get(verticalFileView)) { - selectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index)); + + if (get(selection).hasAnyChildren(new ListFileItem(this.fileId))) { + file.wpt.forEach((waypoint) => { // Update markers + if (markerIndex < this.markers.length) { + this.markers[markerIndex].setLngLat(waypoint.getCoordinates()); + Object.defineProperty(this.markers[markerIndex], '_waypoint', { value: waypoint, writable: true }); + } else { + let marker = new mapboxgl.Marker({ + draggable: this.draggable + }).setLngLat(waypoint.getCoordinates()); + Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true }); + let dragEndTimestamp = 0; + marker.getElement().addEventListener('mouseover', (e) => { + if (marker._isDragging) { + return; + } + this.showWaypointPopup(marker._waypoint); e.stopPropagation(); - } - }); - this.markers.push(marker); - } - markerIndex++; - }); + }); + marker.getElement().addEventListener('mouseout', () => { + this.hideWaypointPopup(); + }); + marker.getElement().addEventListener('click', (e) => { + if (dragEndTimestamp && Date.now() - dragEndTimestamp < 1000) { + return; + } + + if ((e.shiftKey || e.ctrlKey || e.metaKey) && get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), false)) { + addSelectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index)); + } else { + selectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index)); + } + if (!get(verticalFileView) && !get(selection).has(new ListFileItem(this.fileId))) { + addSelectItem(new ListFileItem(this.fileId)); + } + e.stopPropagation(); + }); + marker.on('dragstart', () => { + this.map.getCanvas().style.cursor = 'grabbing'; + marker.getElement().style.cursor = 'grabbing'; + this.hideWaypointPopup(); + }); + marker.on('dragend', (e) => { + this.map.getCanvas().style.cursor = ''; + marker.getElement().style.cursor = ''; + dbUtils.applyToFile(this.fileId, (file) => { + return produce(file, (draft) => { + let latLng = marker.getLngLat(); + draft.wpt[marker._waypoint._data.index].setCoordinates({ + lat: latLng.lat, + lon: latLng.lng + }); + }); + }); + dragEndTimestamp = Date.now() + }); + this.markers.push(marker); + } + markerIndex++; + }); + } while (markerIndex < this.markers.length) { // Remove extra markers this.markers.pop()?.remove(); @@ -249,7 +298,7 @@ export class GPXLayer { let waypoint = get(currentWaypoint); if (waypoint) { let marker = this.markers[waypoint._data.index]; - marker.togglePopup(); + marker.getPopup()?.remove(); } } @@ -281,7 +330,7 @@ export class GPXLayer { } if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)) || get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), true)) { feature.properties.weight = feature.properties.weight + 2; - feature.properties.opacity = (feature.properties.opacity + 1) / 2; + feature.properties.opacity = (feature.properties.opacity + 2) / 3; } feature.properties.trackIndex = trackIndex; feature.properties.segmentIndex = segmentIndex; diff --git a/website/src/lib/components/gpx-layer/GPXLayers.svelte b/website/src/lib/components/gpx-layer/GPXLayers.svelte index d98d9923..2d3c1802 100644 --- a/website/src/lib/components/gpx-layer/GPXLayers.svelte +++ b/website/src/lib/components/gpx-layer/GPXLayers.svelte @@ -9,21 +9,18 @@ let distanceMarkers: DistanceMarkers; $: if ($map && $fileObservers) { - gpxLayers.update(($layers) => { - // remove layers for deleted files - $layers.forEach((layer, fileId) => { - if (!$fileObservers.has(fileId)) { - layer.remove(); - $layers.delete(fileId); - } - }); - // add layers for new files - $fileObservers.forEach((file, fileId) => { - if (!$layers.has(fileId)) { - $layers.set(fileId, new GPXLayer(get(map), fileId, file)); - } - }); - return $layers; + // remove layers for deleted files + gpxLayers.forEach((layer, fileId) => { + if (!$fileObservers.has(fileId)) { + layer.remove(); + gpxLayers.delete(fileId); + } + }); + // add layers for new files + $fileObservers.forEach((file, fileId) => { + if (!gpxLayers.has(fileId)) { + gpxLayers.set(fileId, new GPXLayer(get(map), fileId, file)); + } }); } diff --git a/website/src/lib/components/toolbar/Toolbar.svelte b/website/src/lib/components/toolbar/Toolbar.svelte index 4d48f021..bca0da0b 100644 --- a/website/src/lib/components/toolbar/Toolbar.svelte +++ b/website/src/lib/components/toolbar/Toolbar.svelte @@ -1,7 +1,5 @@
@@ -27,6 +25,10 @@ {$_('toolbar.routing.tooltip')} + + + {$_('toolbar.waypoint_tooltip')} + {$_('toolbar.time_tooltip')} @@ -39,10 +41,6 @@ {$_('toolbar.extract_tooltip')} - - - {$_('toolbar.waypoint_tooltip')} - {$_('toolbar.reduce_tooltip')} @@ -56,7 +54,6 @@ {$_('toolbar.style_tooltip')}
- - + diff --git a/website/src/lib/components/toolbar/ToolbarItemMenu.svelte b/website/src/lib/components/toolbar/ToolbarItemMenu.svelte index d45501e7..eb574b39 100644 --- a/website/src/lib/components/toolbar/ToolbarItemMenu.svelte +++ b/website/src/lib/components/toolbar/ToolbarItemMenu.svelte @@ -1,20 +1,39 @@ -{#if active} -
+{#if $currentTool !== null} +
- - + + {#if $currentTool === Tool.ROUTING} + + {:else if $currentTool === Tool.WAYPOINT} + + {/if}
@@ -23,8 +42,10 @@ { - if (active && e.key === 'Escape') { + if ($currentTool && e.key === 'Escape') { currentTool.set(null); } }} /> + + diff --git a/website/src/lib/components/toolbar/tools/routing/Routing.svelte b/website/src/lib/components/toolbar/tools/routing/Routing.svelte index 5e8cba7f..5f3e814c 100644 --- a/website/src/lib/components/toolbar/tools/routing/Routing.svelte +++ b/website/src/lib/components/toolbar/tools/routing/Routing.svelte @@ -1,5 +1,4 @@ - +
{$_('toolbar.routing.use_routing_tooltip')}
+ {#if $routing}
@@ -177,6 +164,4 @@
{$_('toolbar.routing.help')}
{/if} - - - +
diff --git a/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts b/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts index cf77f450..cfbb7129 100644 --- a/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts +++ b/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts @@ -165,6 +165,9 @@ export class RoutingControls { }); marker.getElement().addEventListener('click', (e) => { e.stopPropagation(); + if (marker === this.temporaryAnchor.marker) { + return; + } if (Date.now() - lastDragEvent < 100) { // Prevent click event during drag return; diff --git a/website/src/lib/components/toolbar/tools/waypoint/Waypoint.svelte b/website/src/lib/components/toolbar/tools/waypoint/Waypoint.svelte index 93c21263..70693af2 100644 --- a/website/src/lib/components/toolbar/tools/waypoint/Waypoint.svelte +++ b/website/src/lib/components/toolbar/tools/waypoint/Waypoint.svelte @@ -1,19 +1,35 @@ - -
todo
+
+ {#if waypoint} + {waypoint.name} + {waypoint.desc ?? ''} + {waypoint.cmt ?? ''} + {/if} @@ -21,4 +37,4 @@
{$_('toolbar.waypoint.help')}
- +
diff --git a/website/src/lib/db.ts b/website/src/lib/db.ts index 5a8f92e6..eafbb275 100644 --- a/website/src/lib/db.ts +++ b/website/src/lib/db.ts @@ -488,10 +488,6 @@ export const dbUtils = { } }); }); - selection.update(($selection) => { - $selection.clear(); - return $selection; - }); }, deleteAllFiles: () => { applyGlobal((draft) => { diff --git a/website/src/lib/stores.ts b/website/src/lib/stores.ts index bfc6dece..e1ba32ee 100644 --- a/website/src/lib/stores.ts +++ b/website/src/lib/stores.ts @@ -6,8 +6,9 @@ import { tick } from 'svelte'; import { _ } from 'svelte-i18n'; import type { GPXLayer } from '$lib/components/gpx-layer/GPXLayer'; import { settings, dbUtils, fileObservers } from './db'; -import { selection } from '$lib/components/file-list/Selection'; +import { selectFile, selection } from '$lib/components/file-list/Selection'; import { ListFileItem, ListWaypointItem } from '$lib/components/file-list/FileList'; +import type { RoutingControls } from '$lib/components/toolbar/tools/routing/RoutingControls'; export const map = writable(null); export const selectFiles = writable<{ [key: string]: (fileId?: string) => void }>({}); @@ -113,15 +114,15 @@ export function updateTargetMapBounds(bounds: { }); } -export const gpxLayers: Writable> = writable(new Map()); +export const gpxLayers: Map = new Map(); +export const routingControls: Map = new Map(); export enum Tool { ROUTING, + WAYPOINT, TIME, - REVERSE, MERGE, EXTRACT, - WAYPOINT, REDUCE, CLEAN, STYLE @@ -192,11 +193,7 @@ function selectFileWhenLoaded(fileId: string) { const unsubscribe = fileObservers.subscribe((files) => { if (files.has(fileId)) { tick().then(() => { - selection.update(($selection) => { - $selection.clear(); - $selection.toggle(new ListFileItem(fileId)); - return $selection; - }); + selectFile(fileId); }); unsubscribe(); }