This commit is contained in:
vcoppe
2024-05-24 20:23:49 +02:00
parent 8e085a718f
commit fb21347e63
13 changed files with 203 additions and 120 deletions

View File

@@ -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;
});
}
});
</script>
<ScrollArea

View File

@@ -49,7 +49,7 @@
}}
on:mouseenter={() => {
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();
}

View File

@@ -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);

View File

@@ -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() {

View File

@@ -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<GPXFileWithStatistics | undefined>;
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;

View File

@@ -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));
}
});
}

View File

@@ -1,7 +1,5 @@
<script lang="ts">
import { Tool } from '$lib/stores';
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
import ToolbarItem from './ToolbarItem.svelte';
import {
Group,
@@ -11,11 +9,11 @@
Ungroup,
MapPin,
Palette,
FolderTree,
Filter
} from 'lucide-svelte';
import { _ } from 'svelte-i18n';
import ToolbarItemMenu from './ToolbarItemMenu.svelte';
</script>
<div class="absolute top-0 bottom-0 left-0 z-20 flex flex-col justify-center pointer-events-none">
@@ -27,6 +25,10 @@
<Pencil slot="icon" size="18" />
<span slot="tooltip">{$_('toolbar.routing.tooltip')}</span>
</ToolbarItem>
<ToolbarItem tool={Tool.WAYPOINT}>
<MapPin slot="icon" size="18" />
<span slot="tooltip">{$_('toolbar.waypoint_tooltip')}</span>
</ToolbarItem>
<ToolbarItem tool={Tool.TIME}>
<CalendarClock slot="icon" size="18" />
<span slot="tooltip">{$_('toolbar.time_tooltip')}</span>
@@ -39,10 +41,6 @@
<Ungroup slot="icon" size="18" />
<span slot="tooltip">{$_('toolbar.extract_tooltip')}</span>
</ToolbarItem>
<ToolbarItem tool={Tool.WAYPOINT}>
<MapPin slot="icon" size="18" />
<span slot="tooltip">{$_('toolbar.waypoint_tooltip')}</span>
</ToolbarItem>
<ToolbarItem tool={Tool.REDUCE}>
<Filter slot="icon" size="18" />
<span slot="tooltip">{$_('toolbar.reduce_tooltip')}</span>
@@ -56,7 +54,6 @@
<span slot="tooltip">{$_('toolbar.style_tooltip')}</span>
</ToolbarItem>
</div>
<Routing />
<Waypoint />
<ToolbarItemMenu />
</div>
</div>

View File

@@ -1,20 +1,39 @@
<script lang="ts">
import { type Tool, currentTool } from '$lib/stores';
import { Tool, currentTool } from '$lib/stores';
import { flyAndScale } from '$lib/utils';
import * as Card from '$lib/components/ui/card';
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
import { onMount } from 'svelte';
import mapboxgl from 'mapbox-gl';
export let tool: Tool;
export let active = false;
let popupElement: HTMLElement;
let popup: mapboxgl.Popup;
$: active = $currentTool === tool;
onMount(() => {
popup = new mapboxgl.Popup({
closeButton: false,
maxWidth: undefined
});
popup.setDOMContent(popupElement);
popupElement.classList.remove('hidden');
});
</script>
{#if active}
<div in:flyAndScale={{ x: -2, y: 0, duration: 100 }} class="translate-x-1 h-full">
{#if $currentTool !== null}
<div
in:flyAndScale={{ x: -2, y: 0, duration: 100 }}
class="translate-x-1 h-full {$$props.class ?? ''}"
>
<div class="rounded-md shadow-md pointer-events-auto">
<Card.Root class="border-none">
<Card.Content class="p-3 flex flex-col gap-3">
<slot />
<Card.Content class="p-3">
{#if $currentTool === Tool.ROUTING}
<Routing {popup} {popupElement} />
{:else if $currentTool === Tool.WAYPOINT}
<Waypoint />
{/if}
</Card.Content>
</Card.Root>
</div>
@@ -23,8 +42,10 @@
<svelte:window
on:keydown={(e) => {
if (active && e.key === 'Escape') {
if ($currentTool && e.key === 'Escape') {
currentTool.set(null);
}
}}
/>
<RoutingControlPopup bind:element={popupElement} />

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import ToolbarItemMenu from '$lib/components/toolbar/ToolbarItemMenu.svelte';
import * as Select from '$lib/components/ui/select';
import { Switch } from '$lib/components/ui/switch';
import { Label } from '$lib/components/ui/label/index.js';
@@ -18,26 +17,22 @@
RouteOff
} from 'lucide-svelte';
import { map, Tool } from '$lib/stores';
import { map, routingControls } from '$lib/stores';
import { dbUtils, settings } from '$lib/db';
import { brouterProfiles, routingProfileSelectItem } from './Routing';
import { _ } from 'svelte-i18n';
import { get } from 'svelte/store';
import { RoutingControls } from './RoutingControls';
import RoutingControlPopup from './RoutingControlPopup.svelte';
import { onMount } from 'svelte';
import mapboxgl from 'mapbox-gl';
import { fileObservers } from '$lib/db';
import { slide } from 'svelte/transition';
import { selection } from '$lib/components/file-list/Selection';
import { ListRootItem, type ListItem } from '$lib/components/file-list/FileList';
let routingControls: Map<string, RoutingControls> = new Map();
let popupElement: HTMLElement;
let popup: mapboxgl.Popup | null = null;
export let popup: mapboxgl.Popup;
export let popupElement: HTMLElement;
let selectedItem: ListItem | null = null;
let active = false;
const { privateRoads, routing } = settings;
@@ -65,18 +60,9 @@
}
$: validSelection = $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
onMount(() => {
popup = new mapboxgl.Popup({
closeButton: false,
maxWidth: undefined
});
popup.setDOMContent(popupElement);
popupElement.classList.remove('hidden');
});
</script>
<ToolbarItemMenu tool={Tool.ROUTING} bind:active>
<div class=" flex flex-col gap-3">
<Tooltip>
<div slot="data" class="w-full flex flex-row justify-between items-center gap-2">
<Label for="routing" class="flex flex-row gap-1">
@@ -91,6 +77,7 @@
</div>
<span slot="tooltip">{$_('toolbar.routing.use_routing_tooltip')}</span>
</Tooltip>
{#if $routing}
<div class="flex flex-col gap-3" in:slide>
<div class="w-full flex flex-row justify-between items-center gap-2">
@@ -177,6 +164,4 @@
<div>{$_('toolbar.routing.help')}</div>
{/if}
</Help>
</ToolbarItemMenu>
<RoutingControlPopup bind:element={popupElement} />
</div>

View File

@@ -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;

View File

@@ -1,19 +1,35 @@
<script lang="ts">
import ToolbarItemMenu from '$lib/components/toolbar/ToolbarItemMenu.svelte';
import * as Alert from '$lib/components/ui/alert';
import { CircleHelp } from 'lucide-svelte';
import { selection } from '$lib/components/file-list/Selection';
import { Tool } from '$lib/stores';
import type { Waypoint } from 'gpx';
import { _ } from 'svelte-i18n';
import { ListWaypointItem } from '$lib/components/file-list/FileList';
import { fileObservers } from '$lib/db';
import { get } from 'svelte/store';
$: $selection.forEach((item) => {
// todo
});
let waypoint: Waypoint | undefined = undefined;
$: if ($selection) {
waypoint = undefined;
$selection.forEach((item) => {
if (item instanceof ListWaypointItem) {
if (waypoint) return;
let fileStore = get(fileObservers).get(item.getFileId());
if (fileStore) {
waypoint = get(fileStore)?.file.wpt[item.getWaypointIndex()];
}
}
});
}
</script>
<ToolbarItemMenu tool={Tool.WAYPOINT}>
<div class="w-full flex flex-row justify-between items-center gap-2">todo</div>
<div class="flex flex-col gap-3 max-w-96">
{#if waypoint}
<span>{waypoint.name}</span>
<span>{waypoint.desc ?? ''}</span>
<span>{waypoint.cmt ?? ''}</span>
{/if}
<Alert.Root class="max-w-64">
<CircleHelp size="16" />
@@ -21,4 +37,4 @@
<div>{$_('toolbar.waypoint.help')}</div>
</Alert.Description>
</Alert.Root>
</ToolbarItemMenu>
</div>

View File

@@ -488,10 +488,6 @@ export const dbUtils = {
}
});
});
selection.update(($selection) => {
$selection.clear();
return $selection;
});
},
deleteAllFiles: () => {
applyGlobal((draft) => {

View File

@@ -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<mapboxgl.Map | null>(null);
export const selectFiles = writable<{ [key: string]: (fileId?: string) => void }>({});
@@ -113,15 +114,15 @@ export function updateTargetMapBounds(bounds: {
});
}
export const gpxLayers: Writable<Map<string, GPXLayer>> = writable(new Map());
export const gpxLayers: Map<string, GPXLayer> = new Map();
export const routingControls: Map<string, RoutingControls> = 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();
}