2024-05-02 19:51:08 +02:00
import { writable , get , type Writable } from 'svelte/store' ;
2024-04-17 11:44:37 +02:00
import mapboxgl from 'mapbox-gl' ;
2024-05-03 22:15:47 +02:00
import { GPXFile , buildGPX , parseGPX , GPXStatistics } from 'gpx' ;
2024-04-27 12:18:40 +02:00
import { tick } from 'svelte' ;
import { _ } from 'svelte-i18n' ;
2024-04-28 18:59:31 +02:00
import type { GPXLayer } from '$lib/components/gpx-layer/GPXLayer' ;
2024-05-03 15:59:34 +02:00
import { dbUtils , fileObservers } from './db' ;
2024-04-17 11:44:37 +02:00
2024-04-17 16:46:51 +02:00
export const map = writable < mapboxgl.Map | null > ( null ) ;
2024-04-30 20:55:47 +02:00
export const fileOrder = writable < string [ ] > ( [ ] ) ;
export const selectedFiles = writable < Set < string > > ( new Set ( ) ) ;
export const selectFiles = writable < { [ key : string ] : ( fileId? : string ) = > void } > ( { } ) ;
2024-05-02 19:51:08 +02:00
fileObservers . subscribe ( ( files ) = > { // Update selectedFiles automatically when files are deleted (either by action or by undo-redo)
2024-04-30 22:35:54 +02:00
let deletedFileIds : string [ ] = [ ] ;
get ( selectedFiles ) . forEach ( ( fileId ) = > {
2024-05-02 19:51:08 +02:00
if ( ! files . has ( fileId ) ) {
2024-04-30 22:35:54 +02:00
deletedFileIds . push ( fileId ) ;
}
} ) ;
if ( deletedFileIds . length > 0 ) {
selectedFiles . update ( ( selectedFiles ) = > {
deletedFileIds . forEach ( ( fileId ) = > selectedFiles . delete ( fileId ) ) ;
return selectedFiles ;
} ) ;
}
} ) ;
2024-05-07 15:16:32 +02:00
const targetMapBounds = writable ( {
bounds : new mapboxgl . LngLatBounds ( [ 180 , 90 , - 180 , - 90 ] ) ,
initial : true
} ) ;
2024-05-07 10:37:24 +02:00
const fileStatistics : Map < string , Writable < GPXStatistics > > = new Map ( ) ;
const fileUnsubscribe : Map < string , Function > = new Map ( ) ;
2024-05-03 22:15:47 +02:00
export const gpxStatistics : Writable < GPXStatistics > = writable ( new GPXStatistics ( ) ) ;
2024-04-30 20:55:47 +02:00
function updateGPXData() {
2024-05-03 22:15:47 +02:00
let fileIds : string [ ] = get ( fileOrder ) . filter ( ( f ) = > fileStatistics . has ( f ) && get ( selectedFiles ) . has ( f ) ) ;
gpxStatistics . set ( fileIds . reduce ( ( stats : GPXStatistics , fileId : string ) = > {
2024-05-07 10:37:24 +02:00
let statisticsStore = fileStatistics . get ( fileId ) ;
if ( statisticsStore ) {
stats . mergeWith ( get ( statisticsStore ) ) ;
}
2024-05-03 22:15:47 +02:00
return stats ;
} , new GPXStatistics ( ) ) ) ;
2024-04-30 20:55:47 +02:00
}
2024-05-07 10:37:24 +02:00
fileObservers . subscribe ( ( files ) = > { // Maintain up-to-date statistics
2024-05-07 12:37:21 +02:00
if ( files . size > fileStatistics . size ) { // Files are added
let bounds = new mapboxgl . LngLatBounds ( [ 180 , 90 , - 180 , - 90 ] ) ;
let mapBounds = new mapboxgl . LngLatBounds ( [ 180 , 90 , - 180 , - 90 ] ) ;
if ( fileStatistics . size > 0 ) { // Some files are already loaded
mapBounds = get ( map ) ? . getBounds ( ) ? ? mapBounds ;
bounds . extend ( mapBounds ) ;
}
2024-05-07 15:16:32 +02:00
targetMapBounds . set ( {
bounds : bounds ,
initial : true
} ) ;
2024-05-07 12:37:21 +02:00
}
2024-05-07 10:37:24 +02:00
fileStatistics . forEach ( ( stats , fileId ) = > {
if ( ! files . has ( fileId ) ) {
fileStatistics . delete ( fileId ) ;
let unsubscribe = fileUnsubscribe . get ( fileId ) ;
if ( unsubscribe ) unsubscribe ( ) ;
fileUnsubscribe . delete ( fileId ) ;
}
} ) ;
files . forEach ( ( fileObserver , fileId ) = > {
if ( ! fileStatistics . has ( fileId ) ) {
2024-05-07 12:16:30 +02:00
let statisticsStore = writable ( new GPXStatistics ( ) ) ;
fileStatistics . set ( fileId , statisticsStore ) ;
2024-05-03 22:15:47 +02:00
let unsubscribe = fileObserver . subscribe ( ( file ) = > {
if ( file ) {
2024-05-07 12:16:30 +02:00
statisticsStore . set ( file . getStatistics ( ) ) ;
2024-05-07 10:37:24 +02:00
if ( get ( selectedFiles ) . has ( fileId ) ) {
updateGPXData ( ) ;
}
2024-05-03 22:15:47 +02:00
}
2024-04-30 20:55:47 +02:00
} ) ;
2024-05-07 10:37:24 +02:00
fileUnsubscribe . set ( fileId , unsubscribe ) ;
2024-05-07 12:16:30 +02:00
let boundsUnsubscribe = statisticsStore . subscribe ( ( stats ) = > {
let fileBounds = stats . global . bounds ;
2024-05-07 15:16:32 +02:00
if ( fileBounds . southWest . lat == 90 && fileBounds . southWest . lon == 180 && fileBounds . northEast . lat == - 90 && fileBounds . northEast . lon == - 180 ) { // Stats are not yet calculated
2024-05-07 12:16:30 +02:00
return ;
}
2024-05-07 15:16:32 +02:00
if ( fileBounds . southWest . lat != fileBounds . northEast . lat || fileBounds . southWest . lon != fileBounds . northEast . lon ) { // Avoid update for new files
targetMapBounds . update ( ( target ) = > {
target . bounds . extend ( fileBounds . southWest ) ;
target . bounds . extend ( fileBounds . northEast ) ;
target . bounds . extend ( [ fileBounds . southWest . lon , fileBounds . northEast . lat ] ) ;
target . bounds . extend ( [ fileBounds . northEast . lon , fileBounds . southWest . lat ] ) ;
target . initial = false ;
return target ;
} ) ;
}
2024-05-07 12:16:30 +02:00
boundsUnsubscribe ( ) ;
} )
2024-04-30 20:55:47 +02:00
}
} ) ;
2024-05-07 10:37:24 +02:00
} ) ;
selectedFiles . subscribe ( ( selectedFiles ) = > { // Maintain up-to-date statistics for the current selection
2024-04-30 20:55:47 +02:00
updateGPXData ( ) ;
} ) ;
2024-05-07 12:16:30 +02:00
targetMapBounds . subscribe ( ( bounds ) = > {
2024-05-07 15:16:32 +02:00
if ( bounds . initial ) {
2024-05-07 12:16:30 +02:00
return ;
}
2024-05-07 15:16:32 +02:00
get ( map ) ? . fitBounds ( bounds . bounds , {
2024-05-07 12:16:30 +02:00
padding : 80 ,
linear : true ,
easing : ( ) = > 1
} ) ;
} ) ;
2024-04-30 20:55:47 +02:00
export const gpxLayers : Writable < Map < string , GPXLayer > > = writable ( new Map ( ) ) ;
2024-04-28 18:59:31 +02:00
2024-04-25 13:56:07 +02:00
export enum Tool {
2024-04-28 18:59:31 +02:00
ROUTING ,
TIME ,
REVERSE ,
MERGE ,
EXTRACT ,
WAYPOINT ,
REDUCE ,
CLEAN ,
STYLE ,
STRUCTURE
2024-04-25 13:56:07 +02:00
}
export const currentTool = writable < Tool | null > ( null ) ;
2024-04-18 10:55:55 +02:00
2024-04-30 20:55:47 +02:00
export function createFile() {
2024-04-27 12:18:40 +02:00
let file = new GPXFile ( ) ;
file . metadata . name = get ( _ ) ( "menu.new_filename" ) ;
2024-04-30 20:55:47 +02:00
2024-05-02 19:51:08 +02:00
dbUtils . add ( file ) ;
2024-04-30 20:55:47 +02:00
2024-05-03 15:59:34 +02:00
selectFileWhenLoaded ( file . _data . id ) ;
2024-05-06 17:56:07 +02:00
currentTool . set ( Tool . ROUTING ) ;
2024-04-27 12:18:40 +02:00
}
2024-04-18 10:55:55 +02:00
export function triggerFileInput() {
const input = document . createElement ( 'input' ) ;
input . type = 'file' ;
input . accept = '.gpx' ;
input . multiple = true ;
input . className = 'hidden' ;
input . onchange = ( ) = > {
if ( input . files ) {
loadFiles ( input . files ) ;
}
} ;
input . click ( ) ;
}
2024-04-20 18:47:16 +02:00
export async function loadFiles ( list : FileList ) {
2024-04-30 20:55:47 +02:00
let files = [ ] ;
2024-04-20 18:47:16 +02:00
for ( let i = 0 ; i < list . length ; i ++ ) {
2024-04-22 17:22:21 +02:00
let file = await loadFile ( list [ i ] ) ;
2024-04-25 13:48:31 +02:00
if ( file ) {
2024-04-30 20:55:47 +02:00
files . push ( file ) ;
2024-04-20 18:47:16 +02:00
}
2024-04-18 10:55:55 +02:00
}
2024-04-30 20:55:47 +02:00
2024-05-07 12:16:30 +02:00
dbUtils . addMultiple ( files ) ;
2024-04-30 20:55:47 +02:00
2024-05-03 15:59:34 +02:00
selectFileWhenLoaded ( files [ 0 ] . _data . id ) ;
2024-04-18 10:55:55 +02:00
}
2024-04-30 20:55:47 +02:00
export async function loadFile ( file : File ) : Promise < GPXFile | null > {
let result = await new Promise < GPXFile | null > ( ( resolve ) = > {
2024-04-20 18:47:16 +02:00
const reader = new FileReader ( ) ;
reader . onload = ( ) = > {
let data = reader . result ? . toString ( ) ? ? null ;
if ( data ) {
let gpx = parseGPX ( data ) ;
if ( gpx . metadata . name === undefined ) {
gpx . metadata [ 'name' ] = file . name . split ( '.' ) . slice ( 0 , - 1 ) . join ( '.' ) ;
}
2024-04-30 20:55:47 +02:00
resolve ( gpx ) ;
2024-04-22 17:22:21 +02:00
} else {
resolve ( null ) ;
2024-04-18 10:55:55 +02:00
}
2024-04-20 18:47:16 +02:00
} ;
reader . readAsText ( file ) ;
} ) ;
return result ;
2024-04-18 15:30:19 +02:00
}
2024-05-03 15:59:34 +02:00
function selectFileWhenLoaded ( fileId : string ) {
const unsubscribe = fileObservers . subscribe ( ( files ) = > {
if ( files . has ( fileId ) ) {
tick ( ) . then ( ( ) = > {
get ( selectFiles ) . select ( fileId ) ;
} ) ;
unsubscribe ( ) ;
2024-04-18 15:30:19 +02:00
}
2024-05-03 15:59:34 +02:00
} ) ;
2024-04-18 15:30:19 +02:00
}
2024-05-03 15:59:34 +02:00
export function exportSelectedFiles() {
get ( fileObservers ) . forEach ( async ( file , fileId ) = > {
if ( get ( selectedFiles ) . has ( fileId ) ) {
let f = get ( file ) ;
if ( f ) {
exportFile ( f ) ;
await new Promise ( resolve = > setTimeout ( resolve , 200 ) ) ;
}
}
} ) ;
}
export function exportAllFiles() {
get ( fileObservers ) . forEach ( async ( file ) = > {
let f = get ( file ) ;
if ( f ) {
exportFile ( f ) ;
await new Promise ( resolve = > setTimeout ( resolve , 200 ) ) ;
}
} ) ;
2024-04-18 15:30:19 +02:00
}
2024-05-03 22:15:47 +02:00
export function exportFile ( file : GPXFile ) {
2024-04-18 15:30:19 +02:00
let blob = new Blob ( [ buildGPX ( file ) ] , { type : 'application/gpx+xml' } ) ;
let url = URL . createObjectURL ( blob ) ;
let a = document . createElement ( 'a' ) ;
a . href = url ;
a . download = file . metadata . name + '.gpx' ;
a . click ( ) ;
URL . revokeObjectURL ( url ) ;
2024-04-18 10:55:55 +02:00
}