From 7ef19adf53397def57e148f0dab237f6cdcc51e4 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Thu, 25 Apr 2024 16:41:06 +0200 Subject: [PATCH] routing controls class --- gpx/src/gpx.ts | 10 + .../src/lib/components/gpx-layer/GPXLayer.ts | 11 +- .../lib/components/gpx-layer/GPXLayers.svelte | 2 + .../src/lib/components/routing/Routing.svelte | 216 +++++++----------- website/src/lib/components/routing/Routing.ts | 177 ++------------ .../lib/components/routing/RoutingControls.ts | 100 ++++++++ .../src/lib/components/routing/Simplify.ts | 116 ++++++++++ .../src/lib/components/toolbar/Toolbar.svelte | 5 +- website/src/lib/stores.ts | 4 + 9 files changed, 343 insertions(+), 298 deletions(-) create mode 100644 website/src/lib/components/routing/RoutingControls.ts create mode 100644 website/src/lib/components/routing/Simplify.ts diff --git a/gpx/src/gpx.ts b/gpx/src/gpx.ts index b615c6de..35823706 100644 --- a/gpx/src/gpx.ts +++ b/gpx/src/gpx.ts @@ -22,6 +22,7 @@ abstract class GPXTreeElement> { abstract getStatistics(): GPXStatistics; abstract getTrackPoints(): TrackPoint[]; abstract getTrackPointsAndStatistics(): { points: TrackPoint[], point_statistics: TrackPointStatistics, statistics: GPXStatistics }; + abstract getSegments(): TrackSegment[]; abstract toGeoJSON(): GeoJSON.Feature | GeoJSON.Feature[] | GeoJSON.FeatureCollection | GeoJSON.FeatureCollection[]; } @@ -115,6 +116,10 @@ abstract class GPXTreeNode> extends GPXTreeElement return { points, point_statistics, statistics }; } + + getSegments(): TrackSegment[] { + return this.getChildren().flatMap((child) => child.getSegments()); + } } // An abstract class that TrackSegment extends to implement the GPXTreeElement interface @@ -280,6 +285,7 @@ export class TrackSegment extends GPXTreeLeaf { const points = this.trkpt; for (let i = 0; i < points.length; i++) { + points[i]._data['index'] = i; // distance let dist = 0; @@ -408,6 +414,10 @@ export class TrackSegment extends GPXTreeLeaf { }; } + getSegments(): TrackSegment[] { + return [this]; + } + toGeoJSON(): GeoJSON.Feature { return { type: "Feature", diff --git a/website/src/lib/components/gpx-layer/GPXLayer.ts b/website/src/lib/components/gpx-layer/GPXLayer.ts index fcb0634c..94f0776e 100644 --- a/website/src/lib/components/gpx-layer/GPXLayer.ts +++ b/website/src/lib/components/gpx-layer/GPXLayer.ts @@ -48,6 +48,9 @@ export class GPXLayer { layerColor: string; unsubscribe: () => void; + addBinded: () => void = this.add.bind(this); + selectOnClickBinded: (e: any) => void = this.selectOnClick.bind(this); + constructor(map: mapboxgl.Map, file: Writable) { this.map = map; this.file = file; @@ -61,7 +64,7 @@ export class GPXLayer { }; this.add(); - this.map.on('style.load', this.add.bind(this)); + this.map.on('style.load', this.addBinded); } add() { @@ -90,7 +93,7 @@ export class GPXLayer { } }); - this.map.on('click', this.layerId, this.selectOnClick.bind(this)); + this.map.on('click', this.layerId, this.selectOnClickBinded); this.map.on('mouseenter', this.layerId, toPointerCursor); this.map.on('mouseleave', this.layerId, toDefaultCursor); } @@ -104,10 +107,10 @@ export class GPXLayer { } remove() { - this.map.off('click', this.layerId, this.selectOnClick.bind(this)); + this.map.off('click', this.layerId, this.selectOnClickBinded); this.map.off('mouseenter', this.layerId, toPointerCursor); this.map.off('mouseleave', this.layerId, toDefaultCursor); - this.map.off('style.load', this.add.bind(this)); + this.map.off('style.load', this.addBinded); this.map.removeLayer(this.layerId); this.map.removeSource(this.layerId); diff --git a/website/src/lib/components/gpx-layer/GPXLayers.svelte b/website/src/lib/components/gpx-layer/GPXLayers.svelte index f006f133..568d90d1 100644 --- a/website/src/lib/components/gpx-layer/GPXLayers.svelte +++ b/website/src/lib/components/gpx-layer/GPXLayers.svelte @@ -7,12 +7,14 @@ let gpxLayers: Map, GPXLayer> = new Map(); $: if ($map) { + // remove layers for deleted files gpxLayers.forEach((layer, file) => { if (!get(files).includes(file)) { layer.remove(); gpxLayers.delete(file); } }); + // add layers for new files $files.forEach((file) => { if (!gpxLayers.has(file)) { gpxLayers.set(file, new GPXLayer(get(map), file)); diff --git a/website/src/lib/components/routing/Routing.svelte b/website/src/lib/components/routing/Routing.svelte index a15b5cc0..b5adec77 100644 --- a/website/src/lib/components/routing/Routing.svelte +++ b/website/src/lib/components/routing/Routing.svelte @@ -7,153 +7,99 @@ import * as Alert from '$lib/components/ui/alert'; import { CircleHelp } from 'lucide-svelte'; - import { map, selectedFiles, applyToFile } from '$lib/stores'; - import { AnchorPointHierarchy, route } from './Routing'; - import { onDestroy } from 'svelte'; - import mapboxgl from 'mapbox-gl'; - - import type { GPXFile } from 'gpx'; + import { currentTool, files, getFileStore, map, selectedFiles, Tool } from '$lib/stores'; + import { brouterProfiles, privateRoads, routing, routingProfile } from './Routing'; import { _ } from 'svelte-i18n'; + import { get, type Writable } from 'svelte/store'; + import type { GPXFile } from 'gpx'; + import { RoutingControls } from './RoutingControls'; - let brouterProfiles: { [key: string]: string } = { - bike: 'Trekking-dry', - racing_bike: 'fastbike', - mountain_bike: 'MTB', - foot: 'Hiking-Alpine-SAC6', - motorcycle: 'Car-FastEco', - water: 'river', - railway: 'rail' - }; - let routingProfile = { - value: 'bike', - label: $_('toolbar.routing.activities.bike') - }; - let routing = true; - let privateRoads = false; + let routingControls: Map, RoutingControls> = new Map(); + let selectedFile: Writable | null = null; + let active = false; - let anchorPointHierarchy: AnchorPointHierarchy | null = null; - let markers: mapboxgl.Marker[] = []; - let file: GPXFile | null = null; - - function toggleMarkersForZoomLevelAndBounds() { - if ($map) { - let zoom = $map.getZoom(); - markers.forEach((marker) => { - if (marker._simplified.zoom <= zoom && $map.getBounds().contains(marker.getLngLat())) { - marker.addTo($map); - } else { - marker.remove(); - } - }); - } - } - - async function extendFile(e: mapboxgl.MapMouseEvent) { - if (file && anchorPointHierarchy && anchorPointHierarchy.points.length > 0) { - let lastPoint = anchorPointHierarchy.points[anchorPointHierarchy.points.length - 1]; - let newPoint = { - lon: e.lngLat.lng, - lat: e.lngLat.lat - }; - let response = await route( - [lastPoint.point.getCoordinates(), newPoint], - brouterProfiles[routingProfile.value], - privateRoads, - routing - ); - applyToFile(file, (f) => f.append(response), true); - } - } - - function clean() { - markers.forEach((marker) => { - marker.remove(); + $: if ($map && $files) { + // remove controls for deleted files + routingControls.forEach((controls, file) => { + if (!get(files).includes(file)) { + controls.remove(); + routingControls.delete(file); + } }); - markers = []; - if ($map) { - $map.off('zoom', toggleMarkersForZoomLevelAndBounds); - $map.off('move', toggleMarkersForZoomLevelAndBounds); - $map.off('click', extendFile); - } } - $: if ($selectedFiles.size == 1) { - let selectedFile = $selectedFiles.values().next().value; + $: active = $currentTool === Tool.ROUTING; - if (selectedFile !== file) { - clean(); - file = selectedFile; + $: if ($map && $selectedFiles) { + // update selected file + if ($selectedFiles.size == 0 || $selectedFiles.size > 1 || !active) { + if (selectedFile) { + routingControls.get(selectedFile)?.remove(); + } + selectedFile = null; } else { - // update markers + let newSelectedFile = get(selectedFiles).values().next().value; + let newSelectedFileStore = getFileStore(newSelectedFile); + if (selectedFile !== newSelectedFileStore) { + if (selectedFile) { + routingControls.get(selectedFile)?.remove(); + } + selectedFile = newSelectedFileStore; + } } - } else { - clean(); - file = null; } - $: if ($map && file) { - // record time - let start = performance.now(); - anchorPointHierarchy = AnchorPointHierarchy.create(file); - // record time - let end = performance.now(); - console.log('Time to create anchor points: ' + (end - start) + 'ms'); - - markers = anchorPointHierarchy.getMarkers(); - - toggleMarkersForZoomLevelAndBounds(); - $map.on('zoom', toggleMarkersForZoomLevelAndBounds); - $map.on('move', toggleMarkersForZoomLevelAndBounds); - $map.on('click', extendFile); - - let points = file.getTrackPoints(); + $: if ($map && selectedFile) { + if (!routingControls.has(selectedFile)) { + routingControls.set(selectedFile, new RoutingControls(get(map), selectedFile)); + } else { + routingControls.get(selectedFile)?.add(); + } } - - onDestroy(() => { - clean(); - }); - - - -
- - - - - - - {#each Object.keys(brouterProfiles) as profile} - {$_(`toolbar.routing.activities.${profile}`)} - {/each} - - -
-
- - -
-
- - -
- - - - - {#if $selectedFiles.size > 1} -
{$_('toolbar.routing.help_multiple_files')}
- {:else if $selectedFiles.size == 0} -
{$_('toolbar.routing.help_no_file')}
- {:else} -
{$_('toolbar.routing.help')}
- {/if} -
-
-
-
-
+{#if active} + + + +
+ + + + + + + {#each Object.keys(brouterProfiles) as profile} + {$_(`toolbar.routing.activities.${profile}`)} + {/each} + + +
+
+ + +
+
+ + +
+ + + + + {#if $selectedFiles.size > 1} +
{$_('toolbar.routing.help_multiple_files')}
+ {:else if $selectedFiles.size == 0} +
{$_('toolbar.routing.help_no_file')}
+ {:else} +
{$_('toolbar.routing.help')}
+ {/if} +
+
+
+
+
+{/if} diff --git a/website/src/lib/components/routing/Routing.ts b/website/src/lib/components/routing/Routing.ts index ad965de2..137b9f56 100644 --- a/website/src/lib/components/routing/Routing.ts +++ b/website/src/lib/components/routing/Routing.ts @@ -1,162 +1,27 @@ -import type { Coordinates, GPXFile } from "gpx"; +import type { Coordinates } from "gpx"; import { TrackPoint } from "gpx"; -import mapboxgl from "mapbox-gl"; +import { get, writable } from "svelte/store"; +import { _ } from "svelte-i18n"; -export function getMarker(coordinates: Coordinates, draggable: boolean = false): mapboxgl.Marker { - let element = document.createElement('div'); - element.className = `h-3 w-3 rounded-full bg-background border-2 border-black cursor-pointer`; - return new mapboxgl.Marker({ - draggable, - element - }).setLngLat(coordinates); -} +export const brouterProfiles: { [key: string]: string } = { + bike: 'Trekking-dry', + racing_bike: 'fastbike', + mountain_bike: 'MTB', + foot: 'Hiking-Alpine-SAC6', + motorcycle: 'Car-FastEco', + water: 'river', + railway: 'rail' +}; +export const routingProfile = writable({ + value: 'bike', + label: get(_)('toolbar.routing.activities.bike') +}); +export const routing = writable(true); +export const privateRoads = writable(false); -export type SimplifiedTrackPoint = { point: TrackPoint, index: number, distance?: number, segment?: number, zoom?: number }; - -export class AnchorPointHierarchy { - points: SimplifiedTrackPoint[]; - - constructor() { - this.points = []; - } - - getMarkers(): mapboxgl.Marker[] { - let markers = []; - for (let point of this.points) { - let marker = getMarker(point.point.getCoordinates(), true); - Object.defineProperty(marker, '_simplified', { value: point }); - markers.push(marker); - } - return markers; - } - - static create(file: GPXFile, epsilon: number = 50): AnchorPointHierarchy { - let hierarchy = new AnchorPointHierarchy(); - - let s = 0; - for (let track of file.getChildren()) { - for (let segment of track.getChildren()) { - let points = segment.trkpt; - let simplified = ramerDouglasPeucker(points, epsilon); - // Assign segment number to each point - simplified.forEach((point) => { - point.segment = s; - point.zoom = getZoomLevelForDistance(point.point.getLatitude(), point.distance); - hierarchy.points.push(point); - }); - s++; - } - } - - return hierarchy; - } -} - -function getZoomLevelForDistance(latitude: number, distance?: number): number { - if (distance === undefined) { - return 0; - } - - const rad = Math.PI / 180; - const lat = latitude * rad; - - return Math.min(20, Math.max(0, Math.floor(Math.log2((earthRadius * Math.cos(lat)) / distance)))); -} - -function ramerDouglasPeucker(points: TrackPoint[], epsilon: number, start: number = 0, end: number = points.length - 1): SimplifiedTrackPoint[] { - let simplified = [{ - point: points[start], - index: start, - - }]; - ramerDouglasPeuckerRecursive(points, epsilon, start, end, simplified); - simplified.push({ - point: points[end], - index: end - }); - return simplified; -} - -function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, start: number, end: number, simplified: SimplifiedTrackPoint[]) { - let largest = { - index: 0, - distance: 0 - }; - - for (let i = start + 1; i < end; i++) { - let distance = crossarc(points[start].getCoordinates(), points[end].getCoordinates(), points[i].getCoordinates()); - if (distance > largest.distance) { - largest.index = i; - largest.distance = distance; - } - } - - if (largest.distance > epsilon) { - ramerDouglasPeuckerRecursive(points, epsilon, start, largest.index, simplified); - simplified.push({ point: points[largest.index], index: largest.index, distance: largest.distance }); - ramerDouglasPeuckerRecursive(points, epsilon, largest.index, end, simplified); - } -} - -const earthRadius = 6371008.8; - -function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number { - // Calculates the shortest distance in meters - // between an arc (defined by p1 and p2) and a third point, p3. - // Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees. - - const rad = Math.PI / 180; - const lat1 = coord1.lat * rad; - const lat2 = coord2.lat * rad; - const lat3 = coord3.lat * rad; - - const lon1 = coord1.lon * rad; - const lon2 = coord2.lon * rad; - const lon3 = coord3.lon * rad; - - // Prerequisites for the formulas - const bear12 = bearing(lat1, lon1, lat2, lon2); - const bear13 = bearing(lat1, lon1, lat3, lon3); - let dis13 = distance(lat1, lon1, lat3, lon3); - - let diff = Math.abs(bear13 - bear12); - if (diff > Math.PI) { - diff = 2 * Math.PI - diff; - } - - // Is relative bearing obtuse? - if (diff > (Math.PI / 2)) { - return dis13; - } - - // Find the cross-track distance. - let dxt = Math.asin(Math.sin(dis13 / earthRadius) * Math.sin(bear13 - bear12)) * earthRadius; - - // Is p4 beyond the arc? - let dis12 = distance(lat1, lon1, lat2, lon2); - let dis14 = Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius; - if (dis14 > dis12) { - return distance(lat2, lon2, lat3, lon3); - } else { - return Math.abs(dxt); - } -} - -function distance(latA: number, lonA: number, latB: number, lonB: number): number { - // Finds the distance between two lat / lon points. - return Math.acos(Math.sin(latA) * Math.sin(latB) + Math.cos(latA) * Math.cos(latB) * Math.cos(lonB - lonA)) * earthRadius; -} - - -function bearing(latA: number, lonA: number, latB: number, lonB: number): number { - // Finds the bearing from one lat / lon point to another. - return Math.atan2(Math.sin(lonB - lonA) * Math.cos(latB), - Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA)); -} - -export function route(points: Coordinates[], brouterProfile: string, privateRoads: boolean, routing: boolean): Promise { - if (routing) { - return getRoute(points, brouterProfile, privateRoads); +export function route(points: Coordinates[]): Promise { + if (get(routing)) { + return getRoute(points, brouterProfiles[get(routingProfile).value], get(privateRoads)); } else { return new Promise((resolve) => { resolve(points.map(point => new TrackPoint({ diff --git a/website/src/lib/components/routing/RoutingControls.ts b/website/src/lib/components/routing/RoutingControls.ts new file mode 100644 index 00000000..dba227bd --- /dev/null +++ b/website/src/lib/components/routing/RoutingControls.ts @@ -0,0 +1,100 @@ +import type { Coordinates, GPXFile } from "gpx"; +import { get, type Writable } from "svelte/store"; +import { computeAnchorPoints } from "./Simplify"; +import mapboxgl from "mapbox-gl"; +import { route } from "./Routing"; +import { applyToFileStore } from "$lib/stores"; + +export class RoutingControls { + map: mapboxgl.Map; + file: Writable; + markers: mapboxgl.Marker[] = []; + unsubscribe: () => void = () => { }; + + toggleMarkersForZoomLevelAndBoundsBinded: () => void = this.toggleMarkersForZoomLevelAndBounds.bind(this); + extendFileBinded: (e: mapboxgl.MapMouseEvent) => void = this.extendFile.bind(this); + + constructor(map: mapboxgl.Map, file: Writable) { + this.map = map; + this.file = file; + + computeAnchorPoints(get(file)); + this.createMarkers(); + this.add(); + } + + add() { + this.toggleMarkersForZoomLevelAndBounds(); + this.map.on('zoom', this.toggleMarkersForZoomLevelAndBoundsBinded); + this.map.on('move', this.toggleMarkersForZoomLevelAndBoundsBinded); + this.map.on('click', this.extendFileBinded); + + this.unsubscribe = this.file.subscribe(this.updateControls.bind(this)); + } + + updateControls() { + // Update controls + console.log('updateControls'); + } + + remove() { + for (let marker of this.markers) { + marker.remove(); + } + this.map.off('zoom', this.toggleMarkersForZoomLevelAndBoundsBinded); + this.map.off('move', this.toggleMarkersForZoomLevelAndBoundsBinded); + this.map.off('click', this.extendFileBinded); + + this.unsubscribe(); + } + + createMarkers() { + for (let segment of get(this.file).getSegments()) { + for (let anchor of segment._data.anchors) { + let marker = getMarker(anchor.point.getCoordinates(), true); + Object.defineProperty(marker, '_simplified', { + value: anchor + }); + this.markers.push(marker); + } + } + } + + toggleMarkersForZoomLevelAndBounds() { + let zoom = this.map.getZoom(); + this.markers.forEach((marker) => { + if (marker._simplified.zoom <= zoom && this.map.getBounds().contains(marker.getLngLat())) { + marker.addTo(this.map); + } else { + marker.remove(); + } + }); + } + + async extendFile(e: mapboxgl.MapMouseEvent) { + let segments = get(this.file).getSegments(); + if (segments.length === 0) { + return; + } + let anchors = segments[segments.length - 1]._data.anchors; + let lastAnchor = anchors[anchors.length - 1]; + + let newPoint = { + lon: e.lngLat.lng, + lat: e.lngLat.lat + }; + + let response = await route([lastAnchor.point.getCoordinates(), newPoint]); + + applyToFileStore(this.file, (f) => f.append(response), true); + } +} + +export function getMarker(coordinates: Coordinates, draggable: boolean = false): mapboxgl.Marker { + let element = document.createElement('div'); + element.className = `h-3 w-3 rounded-full bg-background border-2 border-black cursor-pointer`; + return new mapboxgl.Marker({ + draggable, + element + }).setLngLat(coordinates); +} \ No newline at end of file diff --git a/website/src/lib/components/routing/Simplify.ts b/website/src/lib/components/routing/Simplify.ts new file mode 100644 index 00000000..15c79d90 --- /dev/null +++ b/website/src/lib/components/routing/Simplify.ts @@ -0,0 +1,116 @@ +import type { Coordinates, GPXFile, TrackPoint } from "gpx"; + +export type SimplifiedTrackPoint = { point: TrackPoint, distance?: number, zoom?: number }; + +const earthRadius = 6371008.8; + +export function getZoomLevelForDistance(latitude: number, distance?: number): number { + if (distance === undefined) { + return 0; + } + + const rad = Math.PI / 180; + const lat = latitude * rad; + + return Math.min(20, Math.max(0, Math.floor(Math.log2((earthRadius * Math.cos(lat)) / distance)))); +} + +export function computeAnchorPoints(file: GPXFile) { + for (let segment of file.getSegments()) { + let points = segment.trkpt; + let anchors = ramerDouglasPeucker(points); + anchors.forEach((point) => { + point.zoom = getZoomLevelForDistance(point.point.getLatitude(), point.distance); + }); + segment._data['anchors'] = anchors; + } +} + +export function ramerDouglasPeucker(points: TrackPoint[], epsilon: number = 50, start: number = 0, end: number = points.length - 1): SimplifiedTrackPoint[] { + let simplified = [{ + point: points[start], + index: start, + + }]; + ramerDouglasPeuckerRecursive(points, epsilon, start, end, simplified); + simplified.push({ + point: points[end], + index: end + }); + return simplified; +} + +function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, start: number, end: number, simplified: SimplifiedTrackPoint[]) { + let largest = { + index: 0, + distance: 0 + }; + + for (let i = start + 1; i < end; i++) { + let distance = crossarc(points[start].getCoordinates(), points[end].getCoordinates(), points[i].getCoordinates()); + if (distance > largest.distance) { + largest.index = i; + largest.distance = distance; + } + } + + if (largest.distance > epsilon) { + ramerDouglasPeuckerRecursive(points, epsilon, start, largest.index, simplified); + simplified.push({ point: points[largest.index], distance: largest.distance }); + ramerDouglasPeuckerRecursive(points, epsilon, largest.index, end, simplified); + } +} + +function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number { + // Calculates the shortest distance in meters + // between an arc (defined by p1 and p2) and a third point, p3. + // Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees. + + const rad = Math.PI / 180; + const lat1 = coord1.lat * rad; + const lat2 = coord2.lat * rad; + const lat3 = coord3.lat * rad; + + const lon1 = coord1.lon * rad; + const lon2 = coord2.lon * rad; + const lon3 = coord3.lon * rad; + + // Prerequisites for the formulas + const bear12 = bearing(lat1, lon1, lat2, lon2); + const bear13 = bearing(lat1, lon1, lat3, lon3); + let dis13 = distance(lat1, lon1, lat3, lon3); + + let diff = Math.abs(bear13 - bear12); + if (diff > Math.PI) { + diff = 2 * Math.PI - diff; + } + + // Is relative bearing obtuse? + if (diff > (Math.PI / 2)) { + return dis13; + } + + // Find the cross-track distance. + let dxt = Math.asin(Math.sin(dis13 / earthRadius) * Math.sin(bear13 - bear12)) * earthRadius; + + // Is p4 beyond the arc? + let dis12 = distance(lat1, lon1, lat2, lon2); + let dis14 = Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius; + if (dis14 > dis12) { + return distance(lat2, lon2, lat3, lon3); + } else { + return Math.abs(dxt); + } +} + +function distance(latA: number, lonA: number, latB: number, lonB: number): number { + // Finds the distance between two lat / lon points. + return Math.acos(Math.sin(latA) * Math.sin(latB) + Math.cos(latA) * Math.cos(latB) * Math.cos(lonB - lonA)) * earthRadius; +} + + +function bearing(latA: number, lonA: number, latB: number, lonB: number): number { + // Finds the bearing from one lat / lon point to another. + return Math.atan2(Math.sin(lonB - lonA) * Math.cos(latB), + Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA)); +} \ No newline at end of file diff --git a/website/src/lib/components/toolbar/Toolbar.svelte b/website/src/lib/components/toolbar/Toolbar.svelte index 87db6289..ce7d6c31 100644 --- a/website/src/lib/components/toolbar/Toolbar.svelte +++ b/website/src/lib/components/toolbar/Toolbar.svelte @@ -16,6 +16,7 @@ } from 'lucide-svelte'; import { _ } from 'svelte-i18n'; + import { derived } from 'svelte/store'; function getToggleTool(tool: Tool) { return () => toggleTool(tool); @@ -72,8 +73,6 @@ {$_('toolbar.structure_tooltip')} - {#if $currentTool === Tool.ROUTING} - - {/if} + diff --git a/website/src/lib/stores.ts b/website/src/lib/stores.ts index 2d43a9d8..3bc3b140 100644 --- a/website/src/lib/stores.ts +++ b/website/src/lib/stores.ts @@ -28,6 +28,10 @@ export function getFileIndex(file: GPXFile): number { export function applyToFile(file: GPXFile, callback: (file: GPXFile) => void, updateSelected: boolean) { let store = getFileStore(file); + applyToFileStore(store, callback, updateSelected); +} + +export function applyToFileStore(store: Writable, callback: (file: GPXFile) => void, updateSelected: boolean) { store.update($file => { callback($file) return $file;