Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 147 additions & 61 deletions gpxutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
"encoding/xml"
"flag"
"fmt"
"io/ioutil"
"io"

"os"
"path"
Expand All @@ -35,69 +35,127 @@ import (
"github.com/briandowns/spinner"
)

const version = "1.1.0"

var (
readFile = ""
ls, split, tm bool
showVersion bool
outDir = ""
timezone = "+02:00"
begin, end time.Time
)

func main() {

// command line parsing
inPtr := flag.String("in", "none", "relative path to GPX file")
outPtr := flag.String("out", "out", "relative path to output directory")

flag.BoolVar(&split, "split", false, "write single files for tracks")
flag.BoolVar(&ls, "ls", false, "list tracks ")
flag.BoolVar(&tm, "time", false, "add time")
func printUsage() {
fmt.Fprintf(os.Stderr, "gpxutils v%s - GPS Track Utilities\n\n", version)
fmt.Fprintf(os.Stderr, "USAGE:\n")
fmt.Fprintf(os.Stderr, " gpxutils [OPTIONS]\n\n")
fmt.Fprintf(os.Stderr, "DESCRIPTION:\n")
fmt.Fprintf(os.Stderr, " Extract tracks from multi-track GPX files, list tracks, or add timestamps.\n\n")
fmt.Fprintf(os.Stderr, "OPTIONS:\n")
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, "\nEXAMPLES:\n")
fmt.Fprintf(os.Stderr, " List all tracks in a GPX file:\n")
fmt.Fprintf(os.Stderr, " gpxutils -in track.gpx -ls\n\n")
fmt.Fprintf(os.Stderr, " Extract all tracks to individual files:\n")
fmt.Fprintf(os.Stderr, " gpxutils -in track.gpx -out ./tracks -split\n\n")
fmt.Fprintf(os.Stderr, " Add timestamps to waypoints:\n")
fmt.Fprintf(os.Stderr, " gpxutils -in track.gpx -time -begin 09:00 -end 17:00\n\n")
}

if inPtr == nil || *inPtr == "" {
panic("input file is missing")
func validateInputFile(filepath string) error {
if filepath == "" || filepath == "none" {
return fmt.Errorf("input file is required (use -in flag)")
}

if outPtr == nil || *outPtr == "" {
outDir = "out"
} else {
outDir = *outPtr

if _, err := os.Stat(filepath); os.IsNotExist(err) {
return fmt.Errorf("input file '%s' does not exist", filepath)
}

file, err := os.Open(filepath)
if err != nil {
return fmt.Errorf("cannot open input file '%s': %v", filepath, err)
}
file.Close()

return nil
}

func main() {
// Configure flag usage function
flag.Usage = printUsage

// Define command line flags with proper descriptions
inPtr := flag.String("in", "", "Path to input GPX file (required)")
outPtr := flag.String("out", "out", "Output directory for generated files")

flag.BoolVar(&split, "split", false, "Extract each track to a separate GPX file")
flag.BoolVar(&ls, "ls", false, "List all tracks found in the GPX file")
flag.BoolVar(&tm, "time", false, "Add timestamps to waypoints (requires -begin and -end)")
flag.BoolVar(&showVersion, "version", false, "Show version information")
flag.StringVar(&timezone, "tz", "+02:00", "Timezone offset for timestamp operations (e.g. +02:00, -05:00)")

flag.Func("begin", "`start time`", func(s string) error {
// var begin time.Time
flag.Func("begin", "Start time for timestamp operation (format: HH:MM)", func(s string) error {
var err error
ds := time.Now().Format("2006-01-02") + "T" + s + ":00+02:00"
ds := time.Now().Format("2006-01-02") + "T" + s + ":00" + timezone
begin, err = time.Parse(time.RFC3339, ds)
if err != nil {
fmt.Println("Begin time not valid")
return err
return fmt.Errorf("invalid start time format '%s': expected HH:MM (e.g. 09:30)", s)
}
return nil
})

flag.Func("end", "`end time`", func(s string) error {
flag.Func("end", "End time for timestamp operation (format: HH:MM)", func(s string) error {
var err error
ds := time.Now().Format("2006-01-02") + "T" + s + ":00+02:00"
ds := time.Now().Format("2006-01-02") + "T" + s + ":00" + timezone
end, err = time.Parse(time.RFC3339, ds)
if err != nil {
fmt.Println("Begin time not valid")
return err
return fmt.Errorf("invalid end time format '%s': expected HH:MM (e.g. 17:30)", s)
}
return nil
})

// Parse command line arguments
flag.Parse()
// Open the file given at commandline

// Handle version flag
if showVersion {
fmt.Printf("gpxutils version %s\n", version)
return
}

// Validate input file
readFile = *inPtr
xmlFile, err := os.Open(readFile)
if err := validateInputFile(readFile); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n\n", err)
flag.Usage()
os.Exit(1)
}

// Set output directory
outDir = *outPtr

// Validate that at least one operation is specified
if !ls && !split && !tm {
fmt.Fprintf(os.Stderr, "Error: No operation specified. Use -ls, -split, or -time\n\n")
flag.Usage()
os.Exit(1)
}

// Open the input file
xmlFile, err := os.Open(readFile)
if err != nil {
fmt.Println("Error opening file:", err)
return
fmt.Fprintf(os.Stderr, "Error: Failed to open input file '%s': %v\n", readFile, err)
os.Exit(1)
}

defer func() { _ = xmlFile.Close() }()

b, _ := ioutil.ReadAll(xmlFile)
b, err := io.ReadAll(xmlFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: Failed to read input file '%s': %v\n", readFile, err)
os.Exit(1)
}

// Progress spinner
s := spinner.New(spinner.CharSets[38], 200*time.Millisecond)
Expand All @@ -107,15 +165,20 @@ func main() {
// Unmarshall gpx file
s.Start() // Start the spinner
var q Query
_ = xml.Unmarshal(b, &q)
if err := xml.Unmarshal(b, &q); err != nil {
fmt.Fprintf(os.Stderr, "Error: Failed to parse GPX file '%s': %v\n", readFile, err)
os.Exit(1)
}
s.Stop() // Stop Spinner

// Write each track to a single file
// Write each track to a single file
if split {

// Create out directory
if _, err = os.Stat(outDir); os.IsNotExist(err) {
_ = os.Mkdir(outDir, 0711)
// Create output directory
if _, err := os.Stat(outDir); os.IsNotExist(err) {
if err := os.Mkdir(outDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error: Failed to create output directory '%s': %v\n", outDir, err)
os.Exit(1)
}
}
s.Prefix = "Creating single files: "
s.Start() // Start the spinner
Expand All @@ -129,13 +192,20 @@ func main() {
name := basename[0 : len(basename)-len(extension)]
outfile = name + fmt.Sprintf("-%02d", i)
}
f, err := os.Create(outDir + "/" + outfile + ".gpx")
outPath := outDir + "/" + outfile + ".gpx"
f, err := os.Create(outPath)
if err != nil {
fmt.Println(err)
fmt.Fprintf(os.Stderr, "Warning: Failed to create file '%s': %v\n", outPath, err)
continue
}
f.WriteString(track.gpx())
_ = f.Close()
if _, err := f.WriteString(track.gpx()); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to write to file '%s': %v\n", outPath, err)
f.Close()
continue
}
if err := f.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to close file '%s': %v\n", outPath, err)
}
}
s.Stop()
return
Expand All @@ -150,38 +220,54 @@ func main() {
}

if tm {
outfile := ""
switch {
case begin.IsZero():
fmt.Println("Valid start time must be given")
return

case end.IsZero():
fmt.Println("Valid end time must be given")
return
case end.Before(begin):
fmt.Println("End time must be after start time")
return
case end.Equal(begin):
fmt.Println("End time must be after start time")
return
// Validate time parameters for timestamp operation
if begin.IsZero() {
fmt.Fprintf(os.Stderr, "Error: Start time is required for timestamp operation (use -begin HH:MM)\n")
os.Exit(1)
}

if end.IsZero() {
fmt.Fprintf(os.Stderr, "Error: End time is required for timestamp operation (use -end HH:MM)\n")
os.Exit(1)
}

if end.Before(begin) || end.Equal(begin) {
fmt.Fprintf(os.Stderr, "Error: End time (%s) must be after start time (%s)\n",
end.Format("15:04"), begin.Format("15:04"))
os.Exit(1)
}

// Create output directory if it doesn't exist
if _, err := os.Stat(outDir); os.IsNotExist(err) {
if err := os.Mkdir(outDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error: Failed to create output directory '%s': %v\n", outDir, err)
os.Exit(1)
}
}

outfile := ""

for i := range q.Tracks {

basename := path.Base(*inPtr)
extension := path.Ext(basename)
name := basename[0 : len(basename)-len(extension)]
outfile = name + fmt.Sprintf("-%02d", i)

f, err := os.Create(outDir + "/" + outfile + ".gpx")
outPath := outDir + "/" + outfile + ".gpx"
f, err := os.Create(outPath)
if err != nil {
fmt.Println(err)
fmt.Fprintf(os.Stderr, "Warning: Failed to create file '%s': %v\n", outPath, err)
continue
}
f.WriteString(q.Tracks[0].setTimeStamps(begin, end))

_ = f.Close()
if _, err := f.WriteString(q.Tracks[i].setTimeStamps(begin, end)); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to write to file '%s': %v\n", outPath, err)
f.Close()
continue
}
if err := f.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to close file '%s': %v\n", outPath, err)
}
}
return
}
Expand Down
Loading