2024-04-26 14:30:08 +02:00
import { distance , type Coordinates , type GPXFile , type TrackSegment , TrackPoint } from "gpx" ;
2024-04-25 16:41:06 +02:00
import { get , type Writable } from "svelte/store" ;
2024-04-25 19:02:34 +02:00
import { computeAnchorPoints , type SimplifiedTrackPoint } from "./Simplify" ;
2024-04-25 16:41:06 +02:00
import mapboxgl from "mapbox-gl" ;
import { route } from "./Routing" ;
2024-04-25 19:02:34 +02:00
import { applyToFileElement , applyToFileStore } from "$lib/stores" ;
2024-04-25 16:41:06 +02:00
export class RoutingControls {
map : mapboxgl.Map ;
file : Writable < GPXFile > ;
markers : mapboxgl.Marker [ ] = [ ] ;
2024-04-26 13:33:17 +02:00
popup : mapboxgl.Popup ;
popupElement : HTMLElement ;
2024-04-26 19:34:46 +02:00
temporaryAnchor : SimplifiedTrackPoint ;
2024-04-25 16:41:06 +02:00
unsubscribe : ( ) = > void = ( ) = > { } ;
toggleMarkersForZoomLevelAndBoundsBinded : ( ) = > void = this . toggleMarkersForZoomLevelAndBounds . bind ( this ) ;
2024-04-26 19:34:46 +02:00
showTemporaryAnchorBinded : ( e : any ) = > void = this . showTemporaryAnchor . bind ( this ) ;
hideTemporaryAnchorBinded : ( e : any ) = > void = this . hideTemporaryAnchor . bind ( this ) ;
2024-04-26 14:37:05 +02:00
appendAnchorBinded : ( e : mapboxgl.MapMouseEvent ) = > void = this . appendAnchor . bind ( this ) ;
2024-04-25 16:41:06 +02:00
2024-04-26 13:33:17 +02:00
constructor ( map : mapboxgl.Map , file : Writable < GPXFile > , popup : mapboxgl.Popup , popupElement : HTMLElement ) {
2024-04-25 16:41:06 +02:00
this . map = map ;
this . file = file ;
2024-04-26 13:33:17 +02:00
this . popup = popup ;
this . popupElement = popupElement ;
2024-04-26 19:34:46 +02:00
this . temporaryAnchor = {
point : new TrackPoint ( {
attributes : {
lon : 0 ,
lat : 0
}
} ) ,
zoom : 0
} ;
this . createMarker ( this . temporaryAnchor ) ;
let marker = this . markers . pop ( ) ; // Remove the temporary anchor from the markers list
marker . getElement ( ) . classList . add ( 'z-0' ) ; // Show below the other markers
Object . defineProperty ( marker , '_temporary' , {
value : true
} ) ;
2024-04-25 16:41:06 +02:00
this . add ( ) ;
}
add() {
this . map . on ( 'zoom' , this . toggleMarkersForZoomLevelAndBoundsBinded ) ;
this . map . on ( 'move' , this . toggleMarkersForZoomLevelAndBoundsBinded ) ;
2024-04-26 14:37:05 +02:00
this . map . on ( 'click' , this . appendAnchorBinded ) ;
2024-04-26 19:34:46 +02:00
this . map . on ( 'mousemove' , get ( this . file ) . _data . layerId , this . showTemporaryAnchorBinded ) ;
this . map . on ( 'mousemove' , this . hideTemporaryAnchorBinded ) ;
2024-04-25 16:41:06 +02:00
this . unsubscribe = this . file . subscribe ( this . updateControls . bind ( this ) ) ;
}
2024-04-26 14:37:05 +02:00
updateControls() { // Update the markers when the file changes
2024-04-25 19:02:34 +02:00
for ( let segment of get ( this . file ) . getSegments ( ) ) {
2024-04-26 14:37:05 +02:00
if ( ! segment . _data . anchors ) { // New segment, create anchors for it
2024-04-25 19:02:34 +02:00
computeAnchorPoints ( segment ) ;
this . createMarkers ( segment ) ;
continue ;
}
let anchors = segment . _data . anchors ;
for ( let i = 0 ; i < anchors . length ; ) {
let anchor = anchors [ i ] ;
2024-04-26 14:37:05 +02:00
if ( anchor . point . _data . index >= segment . trkpt . length || anchor . point !== segment . trkpt [ anchor . point . _data . index ] ) { // Point does not exist anymore, remove the anchor
2024-04-25 19:02:34 +02:00
anchors . splice ( i , 1 ) ;
2024-04-26 10:18:08 +02:00
this . markers [ i ] . remove ( ) ;
this . markers . splice ( i , 1 ) ;
2024-04-25 19:02:34 +02:00
continue ;
}
i ++ ;
}
}
this . toggleMarkersForZoomLevelAndBounds ( ) ;
2024-04-25 16:41:06 +02:00
}
remove() {
for ( let marker of this . markers ) {
marker . remove ( ) ;
}
this . map . off ( 'zoom' , this . toggleMarkersForZoomLevelAndBoundsBinded ) ;
this . map . off ( 'move' , this . toggleMarkersForZoomLevelAndBoundsBinded ) ;
2024-04-26 14:37:05 +02:00
this . map . off ( 'click' , this . appendAnchorBinded ) ;
2024-04-26 19:34:46 +02:00
this . map . off ( 'mousemove' , get ( this . file ) . _data . layerId , this . showTemporaryAnchorBinded ) ;
this . map . off ( 'mousemove' , this . hideTemporaryAnchorBinded ) ;
2024-04-25 16:41:06 +02:00
this . unsubscribe ( ) ;
}
2024-04-25 19:02:34 +02:00
createMarkers ( segment : TrackSegment ) {
for ( let anchor of segment . _data . anchors ) {
this . createMarker ( anchor ) ;
2024-04-25 16:41:06 +02:00
}
}
2024-04-25 19:02:34 +02:00
createMarker ( anchor : SimplifiedTrackPoint ) {
let element = document . createElement ( 'div' ) ;
element . className = ` h-3 w-3 rounded-full bg-background border-2 border-black cursor-pointer ` ;
let marker = new mapboxgl . Marker ( {
draggable : true ,
2024-04-26 19:34:46 +02:00
className : 'z-10' ,
2024-04-25 19:02:34 +02:00
element
} ) . setLngLat ( anchor . point . getCoordinates ( ) ) ;
Object . defineProperty ( marker , '_simplified' , {
value : anchor
} ) ;
anchor . marker = marker ;
2024-04-26 14:16:59 +02:00
let lastDragEvent = 0 ;
marker . on ( 'dragstart' , ( e ) = > {
lastDragEvent = Date . now ( ) ;
2024-04-26 13:33:17 +02:00
this . map . getCanvas ( ) . style . cursor = 'grabbing' ;
element . classList . add ( 'cursor-grabbing' ) ;
} ) ;
marker . on ( 'dragend' , ( ) = > {
2024-04-26 14:16:59 +02:00
lastDragEvent = Date . now ( ) ;
2024-04-26 13:33:17 +02:00
this . map . getCanvas ( ) . style . cursor = '' ;
element . classList . remove ( 'cursor-grabbing' ) ;
} ) ;
2024-04-26 14:37:05 +02:00
marker . on ( 'dragend' , this . moveAnchor . bind ( this ) ) ;
2024-04-26 13:33:17 +02:00
marker . getElement ( ) . addEventListener ( 'click' , ( e ) = > {
2024-04-26 14:37:05 +02:00
if ( Date . now ( ) - lastDragEvent < 100 ) { // Prevent click event during drag
2024-04-26 14:16:59 +02:00
return ;
}
2024-04-26 13:33:17 +02:00
marker . setPopup ( this . popup ) ;
marker . togglePopup ( ) ;
e . stopPropagation ( ) ;
let deleteThisAnchor = this . getDeleteAnchor ( anchor ) ;
2024-04-26 14:37:05 +02:00
this . popupElement . addEventListener ( 'delete' , deleteThisAnchor ) ; // Register the delete event for this anchor
2024-04-26 13:33:17 +02:00
this . popup . once ( 'close' , ( ) = > {
this . popupElement . removeEventListener ( 'delete' , deleteThisAnchor ) ;
} ) ;
} ) ;
2024-04-25 19:02:34 +02:00
this . markers . push ( marker ) ;
}
2024-04-26 14:37:05 +02:00
toggleMarkersForZoomLevelAndBounds() { // Show markers only if they are in the current zoom level and bounds
2024-04-25 16:41:06 +02:00
let zoom = this . map . getZoom ( ) ;
this . markers . forEach ( ( marker ) = > {
2024-04-26 10:18:08 +02:00
Object . defineProperty ( marker , '_inZoom' , {
value : marker._simplified.zoom <= zoom ,
writable : true
} ) ;
if ( marker . _inZoom && this . map . getBounds ( ) . contains ( marker . getLngLat ( ) ) ) {
2024-04-25 16:41:06 +02:00
marker . addTo ( this . map ) ;
} else {
marker . remove ( ) ;
}
} ) ;
}
2024-04-26 19:34:46 +02:00
showTemporaryAnchor ( e : any ) {
this . temporaryAnchor . point . setCoordinates ( {
lat : e.lngLat.lat ,
lon : e.lngLat.lng
} ) ;
this . temporaryAnchor . marker . setLngLat ( this . temporaryAnchor . point . getCoordinates ( ) ) . addTo ( this . map ) ;
}
hideTemporaryAnchor ( e : any ) {
if ( this . temporaryAnchor . marker ? . getElement ( ) . classList . contains ( 'cursor-grabbing' ) ) {
return ;
}
if ( e . point . dist ( this . map . project ( this . temporaryAnchor . marker ? . getLngLat ( ) ) ) < 20 ) {
return ;
}
this . temporaryAnchor . marker . remove ( ) ;
}
2024-04-26 14:37:05 +02:00
async moveAnchor ( e : any ) { // Move the anchor and update the route from and to the neighbouring anchors
2024-04-25 19:02:34 +02:00
let marker = e . target ;
let anchor = marker . _simplified ;
2024-04-26 19:34:46 +02:00
if ( marker . _temporary ) { // Temporary anchor, need to find the closest point of the segment and create an anchor for it
anchor = this . getPermanentAnchor ( anchor ) ;
marker = anchor . marker ;
}
2024-04-25 19:02:34 +02:00
let latlng = marker . getLngLat ( ) ;
let coordinates = {
lat : latlng.lat ,
lon : latlng.lng
} ;
2024-04-26 14:16:59 +02:00
let [ previousAnchor , nextAnchor ] = this . getNeighbouringAnchors ( anchor ) ;
2024-04-25 19:02:34 +02:00
2024-04-26 14:16:59 +02:00
let anchors = [ ] ;
let targetCoordinates = [ ] ;
2024-04-25 19:02:34 +02:00
2024-04-26 14:16:59 +02:00
if ( previousAnchor !== null ) {
anchors . push ( previousAnchor ) ;
targetCoordinates . push ( previousAnchor . point . getCoordinates ( ) ) ;
2024-04-25 19:02:34 +02:00
}
2024-04-26 14:16:59 +02:00
anchors . push ( anchor ) ;
targetCoordinates . push ( coordinates ) ;
2024-04-25 19:02:34 +02:00
2024-04-26 14:16:59 +02:00
if ( nextAnchor !== null ) {
anchors . push ( nextAnchor ) ;
targetCoordinates . push ( nextAnchor . point . getCoordinates ( ) ) ;
2024-04-25 19:02:34 +02:00
}
2024-04-26 10:18:08 +02:00
2024-04-26 14:16:59 +02:00
await this . routeBetweenAnchors ( anchors , targetCoordinates ) ;
2024-04-25 19:02:34 +02:00
}
2024-04-26 19:34:46 +02:00
getPermanentAnchor ( anchor : SimplifiedTrackPoint ) {
// Find the closest point closest to the temporary anchor
let minDistance = Number . MAX_VALUE ;
let minPoint : TrackPoint | null = null ;
for ( let segment of get ( this . file ) . getSegments ( ) ) {
for ( let point of segment . trkpt ) {
let dist = distance ( point . getCoordinates ( ) , anchor . point . getCoordinates ( ) ) ;
if ( dist < minDistance ) {
minDistance = dist ;
minPoint = point ;
}
}
}
if ( ! minPoint ) {
return anchor ;
}
let segment = minPoint . _data . segment ;
let newAnchorIndex = segment . _data . anchors . findIndex ( ( a ) = > a . point . _data . index >= minPoint . _data . index ) ;
if ( segment . _data . anchors [ newAnchorIndex ] . point . _data . index === minPoint . _data . index ) { // Anchor already exists for this point
return segment . _data . anchors [ newAnchorIndex ] ;
}
let newAnchor = {
point : minPoint ,
zoom : 0
} ;
this . createMarker ( newAnchor ) ;
let marker = this . markers . pop ( ) ;
marker ? . setLngLat ( anchor . marker . getLngLat ( ) ) . addTo ( this . map ) ;
segment . _data . anchors . splice ( newAnchorIndex , 0 , newAnchor ) ;
this . markers . splice ( newAnchorIndex , 0 , marker ) ;
return newAnchor ;
}
2024-04-26 13:33:17 +02:00
getDeleteAnchor ( anchor : SimplifiedTrackPoint ) {
return ( ) = > this . deleteAnchor ( anchor ) ;
}
2024-04-26 14:37:05 +02:00
async deleteAnchor ( anchor : SimplifiedTrackPoint ) { // Remove the anchor and route between the neighbouring anchors if they exist
2024-04-26 14:16:59 +02:00
let [ previousAnchor , nextAnchor ] = this . getNeighbouringAnchors ( anchor ) ;
2024-04-26 13:33:17 +02:00
2024-04-26 14:16:59 +02:00
if ( previousAnchor === null ) {
// remove trackpoints until nextAnchor
} else if ( nextAnchor === null ) {
// remove trackpoints from previousAnchor
} else {
// route between previousAnchor and nextAnchor
this . routeBetweenAnchors ( [ previousAnchor , nextAnchor ] , [ previousAnchor . point . getCoordinates ( ) , nextAnchor . point . getCoordinates ( ) ] ) ;
2024-04-26 10:18:08 +02:00
}
2024-04-26 14:16:59 +02:00
}
2024-04-26 10:18:08 +02:00
2024-04-26 14:37:05 +02:00
async appendAnchor ( e : mapboxgl.MapMouseEvent ) { // Add a new anchor to the end of the last segment
2024-04-25 16:41:06 +02:00
let segments = get ( this . file ) . getSegments ( ) ;
if ( segments . length === 0 ) {
return ;
}
2024-04-25 19:02:34 +02:00
let segment = segments [ segments . length - 1 ] ;
let anchors = segment . _data . anchors ;
2024-04-25 16:41:06 +02:00
let lastAnchor = anchors [ anchors . length - 1 ] ;
2024-04-26 14:30:08 +02:00
let newPoint = new TrackPoint ( {
attributes : {
lon : e.lngLat.lng ,
lat : e.lngLat.lat
}
} ) ;
newPoint . _data . index = segment . trkpt . length - 1 ; // Do as if the point was the last point in the segment
newPoint . _data . segment = segment ;
let newAnchor = {
point : newPoint ,
2024-04-25 19:02:34 +02:00
zoom : 0
} ;
2024-04-26 14:30:08 +02:00
this . createMarker ( newAnchor ) ;
segment . _data . anchors . push ( newAnchor ) ;
2024-04-25 19:02:34 +02:00
2024-04-26 14:30:08 +02:00
this . routeBetweenAnchors ( [ lastAnchor , newAnchor ] , [ lastAnchor . point . getCoordinates ( ) , newAnchor . point . getCoordinates ( ) ] ) ;
2024-04-26 14:16:59 +02:00
}
getNeighbouringAnchors ( anchor : SimplifiedTrackPoint ) : [ SimplifiedTrackPoint | null , SimplifiedTrackPoint | null ] {
let previousAnchor : SimplifiedTrackPoint | null = null ;
let nextAnchor : SimplifiedTrackPoint | null = null ;
let segment = anchor . point . _data . segment ;
let anchors = segment . _data . anchors ;
for ( let i = 0 ; i < anchors . length ; i ++ ) {
if ( anchors [ i ] . point . _data . index < anchor . point . _data . index &&
anchors [ i ] . point . _data . segment === anchor . point . _data . segment &&
anchors [ i ] . marker . _inZoom ) {
if ( ! previousAnchor || anchors [ i ] . point . _data . index > previousAnchor . point . _data . index ) {
previousAnchor = anchors [ i ] ;
}
} else if ( anchors [ i ] . point . _data . index > anchor . point . _data . index &&
anchors [ i ] . point . _data . segment === anchor . point . _data . segment &&
anchors [ i ] . marker . _inZoom ) {
if ( ! nextAnchor || anchors [ i ] . point . _data . index < nextAnchor . point . _data . index ) {
nextAnchor = anchors [ i ] ;
}
}
}
return [ previousAnchor , nextAnchor ] ;
}
async routeBetweenAnchors ( anchors : SimplifiedTrackPoint [ ] , targetCoordinates : Coordinates [ ] ) {
if ( anchors . length === 1 ) {
anchors [ 0 ] . point . setCoordinates ( targetCoordinates [ 0 ] ) ;
return ;
}
let segment = anchors [ 0 ] . point . _data . segment ;
let response = await route ( targetCoordinates ) ;
let start = anchors [ 0 ] . point . _data . index + 1 ;
let end = anchors [ anchors . length - 1 ] . point . _data . index - 1 ;
if ( anchors [ 0 ] . point . _data . index === 0 ) { // First anchor is the first point of the segment
anchors [ 0 ] . point = response [ 0 ] ; // Update the first anchor in case it was not on a road
start -- ; // Remove the original first point
}
if ( anchors [ anchors . length - 1 ] . point . _data . index === anchors [ anchors . length - 1 ] . point . _data . segment . trkpt . length - 1 ) { // Last anchor is the last point of the segment
anchors [ anchors . length - 1 ] . point = response [ response . length - 1 ] ; // Update the last anchor in case it was not on a road
end ++ ; // Remove the original last point
}
for ( let i = 1 ; i < anchors . length - 1 ; i ++ ) {
// Find the closest point to the intermediate anchor
// and transfer the marker to that point
let minDistance = Number . MAX_VALUE ;
let minIndex = 0 ;
for ( let j = 1 ; j < response . length - 1 ; j ++ ) {
let dist = distance ( response [ j ] . getCoordinates ( ) , targetCoordinates [ i ] ) ;
if ( dist < minDistance ) {
minDistance = dist ;
minIndex = j ;
}
}
anchors [ i ] . point = response [ minIndex ] ;
}
anchors . forEach ( ( anchor ) = > {
anchor . zoom = 0 ; // Make these anchors permanent
anchor . marker . setLngLat ( anchor . point . getCoordinates ( ) ) ; // Update the marker position if needed
} ) ;
2024-04-26 10:18:08 +02:00
2024-04-26 14:16:59 +02:00
applyToFileElement ( this . file , segment , ( segment ) = > {
segment . replace ( start , end , response ) ;
} , true ) ;
2024-04-25 16:41:06 +02:00
}
}