@@ -10,6 +10,7 @@ import { unzip } from "fflate";
1010import type { FeatureCollection } from "geojson" ;
1111import shp from "shpjs" ;
1212import type { DuckDbVectorFile } from "./duckdb-vector-loader" ;
13+ import { parseGpxLayer } from "./gpx" ;
1314
1415export function isTauri ( ) : boolean {
1516 return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window ;
@@ -96,6 +97,12 @@ interface SaveBinaryFileOptions extends SaveTextFileOptions {}
9697
9798const SHAPEFILE_SIDECAR_EXTENSIONS = [ "dbf" , "shx" , "prj" , "cpg" ] ;
9899
100+ export interface LoadedVectorLayer {
101+ data : FeatureCollection ;
102+ name ?: string ;
103+ path : string ;
104+ }
105+
99106// Auxiliary files that accompany Shapefiles (spatial indexes, metadata, etc.)
100107// but are never standalone vector layers. Skipping them keeps a single such
101108// file from aborting an otherwise valid drag-and-drop import.
@@ -191,6 +198,31 @@ async function parseGeoJsonText(
191198 return assertFeatureCollection ( JSON . parse ( text ) ) ;
192199}
193200
201+ function parseGpxText ( text : string ) : FeatureCollection {
202+ const result = parseGpxLayer ( text ) ;
203+ return mergeFeatureCollections ( [
204+ result . waypoints ,
205+ result . tracks ,
206+ result . routes ,
207+ ] ) ;
208+ }
209+
210+ function parseGpxTextLayers ( text : string , path : string ) : LoadedVectorLayer [ ] {
211+ const result = parseGpxLayer ( text ) ;
212+ const baseName = pathWithoutExtension ( browserSafeFileName ( path ) ) || "GPX" ;
213+ return [
214+ { data : result . waypoints , label : "Waypoints" } ,
215+ { data : result . tracks , label : "Tracks" } ,
216+ { data : result . routes , label : "Routes" } ,
217+ ]
218+ . filter ( ( layer ) => layer . data . features . length > 0 )
219+ . map ( ( layer ) => ( {
220+ data : layer . data ,
221+ name : `${ baseName } ${ layer . label } ` ,
222+ path,
223+ } ) ) ;
224+ }
225+
194226async function parseShapefileZip (
195227 data : ArrayBuffer | Uint8Array ,
196228) : Promise < FeatureCollection > {
@@ -270,10 +302,7 @@ async function fileToDuckDbVectorFile(file: File): Promise<DuckDbVectorFile> {
270302async function loadBrowserVectorFile (
271303 file : File ,
272304 siblingFiles : DuckDbVectorFile [ ] = [ ] ,
273- ) : Promise < {
274- data : FeatureCollection ;
275- path : string ;
276- } > {
305+ ) : Promise < LoadedVectorLayer > {
277306 const extension = fileExtension ( file . name ) ;
278307 if ( extension === "geojson" || extension === "json" ) {
279308 try {
@@ -305,6 +334,13 @@ async function loadBrowserVectorFile(
305334 } ;
306335 }
307336
337+ if ( extension === "gpx" ) {
338+ return {
339+ data : parseGpxText ( await file . text ( ) ) ,
340+ path : file . name ,
341+ } ;
342+ }
343+
308344 return {
309345 data : await loadDuckDbVector ( {
310346 name : file . name ,
@@ -391,6 +427,18 @@ async function loadTauriVectorFile(path: string): Promise<{
391427 }
392428 }
393429
430+ if ( extension === "gpx" ) {
431+ try {
432+ return {
433+ data : parseGpxText ( await readTextFile ( path ) ) ,
434+ path,
435+ } ;
436+ } catch ( error ) {
437+ const detail = error instanceof Error ? error . message : "Unknown error" ;
438+ throw new Error ( `Could not read this GPX file. ${ detail } ` ) ;
439+ }
440+ }
441+
394442 try {
395443 const siblingFiles =
396444 extension === "shp" ? await readShapefileSiblings ( path ) : [ ] ;
@@ -884,7 +932,7 @@ export async function openVectorFileWithFallback(): Promise<{
884932
885933export async function loadDroppedVectorFiles (
886934 droppedFiles : FileList | File [ ] ,
887- ) : Promise < Array < { data : FeatureCollection ; path : string } > > {
935+ ) : Promise < LoadedVectorLayer [ ] > {
888936 const droppedFileArray = Array . from ( droppedFiles ) ;
889937 const files = droppedFileArray . filter ( ( file ) =>
890938 isVectorFileName ( file . name ) ,
@@ -900,11 +948,16 @@ export async function loadDroppedVectorFiles(
900948 ] ) ;
901949 }
902950
903- const layers : Array < { data : FeatureCollection ; path : string } > = [ ] ;
951+ const layers : LoadedVectorLayer [ ] = [ ] ;
904952 for ( const file of files ) {
905953 const extension = fileExtension ( file . name ) ;
906954 if ( SHAPEFILE_SIDECAR_EXTENSIONS . includes ( extension ) ) continue ;
907955
956+ if ( extension === "gpx" ) {
957+ layers . push ( ...parseGpxTextLayers ( await file . text ( ) , file . name ) ) ;
958+ continue ;
959+ }
960+
908961 const siblingFiles =
909962 extension === "shp"
910963 ? await Promise . all (
@@ -928,13 +981,23 @@ export async function loadDroppedVectorFiles(
928981
929982export async function loadDroppedVectorPaths (
930983 paths : string [ ] ,
931- ) : Promise < Array < { data : FeatureCollection ; path : string } > > {
984+ ) : Promise < LoadedVectorLayer [ ] > {
932985 const vectorPaths = paths . filter ( isVectorFileName ) ;
933986 if ( ! vectorPaths . length ) return [ ] ;
934987
935- const layers : Array < { data : FeatureCollection ; path : string } > = [ ] ;
988+ const layers : LoadedVectorLayer [ ] = [ ] ;
936989 for ( const path of vectorPaths ) {
937- if ( SHAPEFILE_SIDECAR_EXTENSIONS . includes ( fileExtension ( path ) ) ) continue ;
990+ const extension = fileExtension ( path ) ;
991+ if ( SHAPEFILE_SIDECAR_EXTENSIONS . includes ( extension ) ) continue ;
992+ if ( extension === "gpx" ) {
993+ try {
994+ layers . push ( ...parseGpxTextLayers ( await readTextFile ( path ) , path ) ) ;
995+ } catch ( error ) {
996+ const detail = error instanceof Error ? error . message : "Unknown error" ;
997+ throw new Error ( `Could not read this GPX file. ${ detail } ` ) ;
998+ }
999+ continue ;
1000+ }
9381001 layers . push ( await loadTauriVectorFile ( path ) ) ;
9391002 }
9401003
0 commit comments