fix tools

This commit is contained in:
vcoppe
2025-10-18 16:10:08 +02:00
parent 9fa8fe5767
commit c59cd66141
60 changed files with 1289 additions and 1161 deletions

View File

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

View File

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

View File

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

View File

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