From c59cd66141f8e970b50f9de70b7712b071722639 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Sat, 18 Oct 2025 16:10:08 +0200 Subject: [PATCH] fix tools --- website/src/lib/components/Menu.svelte | 20 +- .../lib/components/map/gpx-layer/gpx-layer.ts | 21 +- .../map/street-view-control/Google.ts | 8 +- .../map/street-view-control/Mapillary.ts | 6 +- .../lib/components/toolbar/ToolbarItem.svelte | 10 +- .../components/toolbar/ToolbarItemMenu.svelte | 54 +- .../lib/components/toolbar/tools/Clean.svelte | 63 +- .../components/toolbar/tools/Elevation.svelte | 6 +- .../components/toolbar/tools/Extract.svelte | 4 +- .../lib/components/toolbar/tools/Merge.svelte | 12 +- .../components/toolbar/tools/Reduce.svelte | 195 --- .../toolbar/tools/reduce/Reduce.svelte | 63 + .../components/toolbar/tools/reduce/reduce.ts | 187 +++ .../toolbar/tools/routing/Routing.svelte | 99 +- .../tools/routing/RoutingControlPopup.svelte | 10 +- .../toolbar/tools/routing/RoutingControls.ts | 123 +- .../routing/{utils.svelte.ts => routing.ts} | 5 +- .../toolbar/tools/scissors/Scissors.svelte | 16 +- .../toolbar/tools/scissors/split-controls.ts | 23 +- .../toolbar/tools/waypoint/Waypoint.svelte | 120 +- .../toolbar/tools/waypoint/waypoint.ts | 59 +- website/src/lib/docs/be/toolbar/minify.mdx | 2 +- website/src/lib/docs/ca/toolbar/minify.mdx | 2 +- website/src/lib/docs/cs/toolbar/minify.mdx | 2 +- website/src/lib/docs/da/toolbar/minify.mdx | 2 +- website/src/lib/docs/de/toolbar/minify.mdx | 2 +- website/src/lib/docs/el/toolbar/minify.mdx | 2 +- website/src/lib/docs/en/toolbar/minify.mdx | 2 +- website/src/lib/docs/es/toolbar/minify.mdx | 2 +- website/src/lib/docs/eu/toolbar/minify.mdx | 2 +- website/src/lib/docs/fi/toolbar/minify.mdx | 2 +- website/src/lib/docs/fr/toolbar/minify.mdx | 2 +- website/src/lib/docs/he/toolbar/minify.mdx | 2 +- website/src/lib/docs/hu/toolbar/minify.mdx | 2 +- website/src/lib/docs/it/toolbar/minify.mdx | 2 +- website/src/lib/docs/ko/toolbar/minify.mdx | 2 +- website/src/lib/docs/lt/toolbar/minify.mdx | 2 +- website/src/lib/docs/lv/toolbar/minify.mdx | 2 +- website/src/lib/docs/nl/toolbar/minify.mdx | 2 +- website/src/lib/docs/no/toolbar/minify.mdx | 2 +- website/src/lib/docs/pl/toolbar/minify.mdx | 2 +- website/src/lib/docs/pt-BR/toolbar/minify.mdx | 2 +- website/src/lib/docs/pt/toolbar/minify.mdx | 2 +- website/src/lib/docs/ro/toolbar/minify.mdx | 2 +- website/src/lib/docs/ru/toolbar/minify.mdx | 2 +- website/src/lib/docs/sr/toolbar/minify.mdx | 2 +- website/src/lib/docs/sv/toolbar/minify.mdx | 2 +- website/src/lib/docs/tr/toolbar/minify.mdx | 2 +- website/src/lib/docs/uk/toolbar/minify.mdx | 2 +- website/src/lib/docs/vi/toolbar/minify.mdx | 2 +- website/src/lib/docs/zh/toolbar/minify.mdx | 2 +- website/src/lib/logic/file-actions.ts | 1140 +++++++++-------- website/src/lib/logic/file-state.ts | 4 +- website/src/lib/logic/map-cursor.ts | 55 + website/src/lib/logic/selection.ts | 10 + website/src/lib/logic/statistics.ts | 4 + website/src/lib/stores.ts | 11 - website/src/lib/utils.ts | 28 - website/src/routes/[[language]]/+page.svelte | 30 +- .../src/routes/[[language]]/app/+page.svelte | 4 +- 60 files changed, 1289 insertions(+), 1161 deletions(-) delete mode 100644 website/src/lib/components/toolbar/tools/Reduce.svelte create mode 100644 website/src/lib/components/toolbar/tools/reduce/Reduce.svelte create mode 100644 website/src/lib/components/toolbar/tools/reduce/reduce.ts rename website/src/lib/components/toolbar/tools/routing/{utils.svelte.ts => routing.ts} (96%) create mode 100644 website/src/lib/logic/map-cursor.ts diff --git a/website/src/lib/components/Menu.svelte b/website/src/lib/components/Menu.svelte index 85834adc..cd05e748 100644 --- a/website/src/lib/components/Menu.svelte +++ b/website/src/lib/components/Menu.svelte @@ -544,15 +544,17 @@ { 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(); diff --git a/website/src/lib/components/map/gpx-layer/gpx-layer.ts b/website/src/lib/components/map/gpx-layer/gpx-layer.ts index 3853ac30..1b144879 100644 --- a/website/src/lib/components/map/gpx-layer/gpx-layer.ts +++ b/website/src/lib/components/map/gpx-layer/gpx-layer.ts @@ -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) { diff --git a/website/src/lib/components/map/street-view-control/Google.ts b/website/src/lib/components/map/street-view-control/Google.ts index a70add0b..149011a8 100644 --- a/website/src/lib/components/map/street-view-control/Google.ts +++ b/website/src/lib/components/map/street-view-control/Google.ts @@ -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}` ); diff --git a/website/src/lib/components/map/street-view-control/Mapillary.ts b/website/src/lib/components/map/street-view-control/Mapillary.ts index b2b478f9..4456c8ee 100644 --- a/website/src/lib/components/map/street-view-control/Mapillary.ts +++ b/website/src/lib/components/map/street-view-control/Mapillary.ts @@ -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); } } diff --git a/website/src/lib/components/toolbar/ToolbarItem.svelte b/website/src/lib/components/toolbar/ToolbarItem.svelte index 1ce25a33..cc8be95a 100644 --- a/website/src/lib/components/toolbar/ToolbarItem.svelte +++ b/website/src/lib/components/toolbar/ToolbarItem.svelte @@ -1,7 +1,7 @@ @@ -30,7 +30,7 @@ - - - {#if validSelection} - {i18n._('toolbar.reduce.help')} - {:else} - {i18n._('toolbar.reduce.help_no_selection')} - {/if} - - diff --git a/website/src/lib/components/toolbar/tools/reduce/Reduce.svelte b/website/src/lib/components/toolbar/tools/reduce/Reduce.svelte new file mode 100644 index 00000000..8e9f32a1 --- /dev/null +++ b/website/src/lib/components/toolbar/tools/reduce/Reduce.svelte @@ -0,0 +1,63 @@ + + +
+
+ +
+ + + + + + {#if validSelection} + {i18n._('toolbar.reduce.help')} + {:else} + {i18n._('toolbar.reduce.help_no_selection')} + {/if} + +
diff --git a/website/src/lib/components/toolbar/tools/reduce/reduce.ts b/website/src/lib/components/toolbar/tools/reduce/reduce.ts new file mode 100644 index 00000000..da4a7158 --- /dev/null +++ b/website/src/lib/components/toolbar/tools/reduce/reduce.ts @@ -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(0); + +export class ReducedGPXLayerCollection { + private _layers: Map = new Map(); + private _simplified: Map; + 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(); + 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'); + } + } +} diff --git a/website/src/lib/components/toolbar/tools/routing/Routing.svelte b/website/src/lib/components/toolbar/tools/routing/Routing.svelte index c91a0f1f..ee09a890 100644 --- a/website/src/lib/components/toolbar/tools/routing/Routing.svelte +++ b/website/src/lib/components/toolbar/tools/routing/Routing.svelte @@ -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); + } }); @@ -130,7 +129,7 @@
- {#if routing.value} + {#if $routing}
{/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()); } } } diff --git a/website/src/lib/components/toolbar/tools/routing/RoutingControlPopup.svelte b/website/src/lib/components/toolbar/tools/routing/RoutingControlPopup.svelte index 051277fe..de06589f 100644 --- a/website/src/lib/components/toolbar/tools/routing/RoutingControlPopup.svelte +++ b/website/src/lib/components/toolbar/tools/routing/RoutingControlPopup.svelte @@ -7,7 +7,11 @@ import { i18n } from '$lib/i18n.svelte'; - export let element: HTMLElement; + let { + element = $bindable(), + }: { + element: HTMLElement | undefined; + } = $props();