diff --git a/website/package-lock.json b/website/package-lock.json index 354865e8..e31229d2 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -13,6 +13,7 @@ "chart.js": "^4.4.2", "clsx": "^2.1.0", "gpx": "file:../gpx", + "kdbush": "^4.0.2", "lucide-svelte": "^0.365.0", "mapbox-gl": "^3.2.0", "sortablejs": "^1.15.2", diff --git a/website/package.json b/website/package.json index f0b20529..6993f8a0 100644 --- a/website/package.json +++ b/website/package.json @@ -46,6 +46,7 @@ "chart.js": "^4.4.2", "clsx": "^2.1.0", "gpx": "file:../gpx", + "kdbush": "^4.0.2", "lucide-svelte": "^0.365.0", "mapbox-gl": "^3.2.0", "sortablejs": "^1.15.2", diff --git a/website/src/lib/components/GPX.svelte b/website/src/lib/components/GPX.svelte index 432d556b..dd8c6280 100644 --- a/website/src/lib/components/GPX.svelte +++ b/website/src/lib/components/GPX.svelte @@ -49,6 +49,11 @@ let layerId = getLayerId(); let layerColor = getColor(); + Object.defineProperty(file, 'layerId', { + value: layerId, + writable: false + }); + function selectOnClick(e: any) { if (e.originalEvent.shiftKey) { get(selectFiles).addSelect(file); diff --git a/website/src/lib/components/toolbar/routing/Routing.svelte b/website/src/lib/components/toolbar/routing/Routing.svelte index a41e67bf..91fd901e 100644 --- a/website/src/lib/components/toolbar/routing/Routing.svelte +++ b/website/src/lib/components/toolbar/routing/Routing.svelte @@ -8,8 +8,12 @@ import { CircleHelp } from 'lucide-svelte'; import { map, selectedFiles } from '$lib/stores'; - import { AnchorPointHierarchy } from './routing'; + import { AnchorPointHierarchy, getMarker } from './routing'; import { onDestroy } from 'svelte'; + import mapboxgl from 'mapbox-gl'; + import KDBush from 'kdbush'; + + import type { GPXFile } from 'gpx'; let routingProfile = { value: 'bike', @@ -28,6 +32,8 @@ let privateRoads = false; let markers: mapboxgl.Marker[] = []; + let file: GPXFile | null = null; + let kdbush: KDBush | null = null; function addMarkersForZoomLevel() { if ($map) { @@ -42,6 +48,38 @@ } } + function extendFile(e: mapboxgl.MapMouseEvent) { + console.log(e.lngLat); + } + + let insertableMarker: mapboxgl.Marker | null = null; + function moveInsertableMarker(e: mapboxgl.MapMouseEvent) { + if (insertableMarker && kdbush && $map) { + let bounds = $map.getBounds(); + let latLngDistance = Math.max( + Math.abs(bounds.getNorth() - bounds.getSouth()), + Math.abs(bounds.getEast() - bounds.getWest()) + ); + if (kdbush.within(e.lngLat.lng, e.lngLat.lat, latLngDistance / 200).length > 0) { + insertableMarker.setLngLat(e.lngLat); + } else { + insertableMarker.remove(); + insertableMarker = null; + $map.off('mousemove', moveInsertableMarker); + } + } + } + function showInsertableMarker(e: mapboxgl.MapMouseEvent) { + if ($map && !insertableMarker) { + insertableMarker = getMarker({ + lon: e.lngLat.lng, + lat: e.lngLat.lat + }); + insertableMarker.addTo($map); + $map.on('mousemove', moveInsertableMarker); + } + } + function clean() { markers.forEach((marker) => { marker.remove(); @@ -49,16 +87,45 @@ markers = []; if ($map) { $map.off('zoom', addMarkersForZoomLevel); + $map.off('click', extendFile); + if (file) { + $map.off('mouseover', file.layerId, showInsertableMarker); + } + if (insertableMarker) { + insertableMarker.remove(); + } } + kdbush = null; } $: if ($selectedFiles.size == 1 && $map) { - let file = $selectedFiles.values().next().value; + clean(); + + file = $selectedFiles.values().next().value; + // record time + let start = performance.now(); let anchorPoints = AnchorPointHierarchy.create(file); + // record time + let end = performance.now(); + console.log('Time to create anchor points: ' + (end - start) + 'ms'); + markers = anchorPoints.getMarkers($map); addMarkersForZoomLevel(); $map.on('zoom', addMarkersForZoomLevel); + $map.on('click', extendFile); + $map.on('mouseover', file.layerId, showInsertableMarker); + + let points = file.getTrackPointsAndStatistics().points; + + start = performance.now(); + kdbush = new KDBush(points.length); + for (let i = 0; i < points.length; i++) { + kdbush.add(points[i].getLongitude(), points[i].getLatitude()); + } + kdbush.finish(); + end = performance.now(); + console.log('Time to create kdbush: ' + (end - start) + 'ms'); } else { clean(); } diff --git a/website/src/lib/components/toolbar/routing/routing.ts b/website/src/lib/components/toolbar/routing/routing.ts index 3a17dd28..ce47420b 100644 --- a/website/src/lib/components/toolbar/routing/routing.ts +++ b/website/src/lib/components/toolbar/routing/routing.ts @@ -1,6 +1,15 @@ import type { Coordinates, GPXFile, TrackPoint } from "gpx"; import mapboxgl from "mapbox-gl"; +export function getMarker(coordinates: Coordinates, draggable: boolean = false, hidden: boolean = false): mapboxgl.Marker { + let element = document.createElement('div'); + element.className = `${hidden ? 'hidden' : ''} h-3 w-3 rounded-full bg-background border-2 border-black cursor-pointer`; + return new mapboxgl.Marker({ + draggable, + element + }).setLngLat(coordinates); +} + export type TrackPointWithIndex = { point: TrackPoint, index: number }; export class AnchorPointHierarchy { @@ -20,13 +29,7 @@ export class AnchorPointHierarchy { getMarkers(map: mapboxgl.Map, last: boolean = true, markers: mapboxgl.Marker[] = []): mapboxgl.Marker[] { if (this.left == null && this.right == null && this.point) { - let element = document.createElement('div'); - element.className = 'hidden h-3 w-3 rounded-full bg-background border-2 border-black'; - let marker = new mapboxgl.Marker({ - draggable: true, - element - }); - marker.setLngLat(this.point.point.getCoordinates()); + let marker = getMarker(this.point.point.getCoordinates()); marker.addTo(map); Object.defineProperty(marker, '_hierarchy', { value: this }); markers.push(marker); @@ -181,3 +184,19 @@ function bearing(latA: number, lonA: number, latB: number, lonB: number): number Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA)); } +export function route(points: TrackPoint[], brouterProfile: string, privateRoads: boolean, routing: boolean) { + if (routing) { + getRoute(points, brouterProfile, privateRoads).then(response => { + return response.json(); + }); + } else { + return new Promise((resolve) => { + resolve(points); + }); + } +} + +function getRoute(points: TrackPoint[], brouterProfile: string, privateRoads: boolean): Promise { + let url = `https://routing.gpx.studio?profile=${brouterProfile + privateRoads ? '-private' : ''}&lonlats=${points.map(point => `${point.getLongitude()},${point.getLatitude()}`).join('|')}&format=geojson&alternativeidx=0`; + return fetch(url); +} \ No newline at end of file