From fb816c31322343d9cad80645661e1c560a46a9fe Mon Sep 17 00:00:00 2001 From: Ray Miller Date: Wed, 15 Apr 2020 20:56:10 +0100 Subject: [PATCH] Analyze all files in a directory, output JSON. --- README.md | 3 + cmd/analyze-gpx/main.go | 151 ++++++++++++++++++++++++++++++++-------- 2 files changed, 126 insertions(+), 28 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..43335f2 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# GPX Utils + +Utilities for analyzing and indexing GPX routes. diff --git a/cmd/analyze-gpx/main.go b/cmd/analyze-gpx/main.go index 08eceee..f7ec2f8 100644 --- a/cmd/analyze-gpx/main.go +++ b/cmd/analyze-gpx/main.go @@ -2,12 +2,17 @@ package main import ( "archive/zip" + "encoding/json" "flag" "fmt" + "io" + "io/ioutil" "log" "math" "os" + "path" "strings" + "time" "github.com/dhconnelly/rtreego" "github.com/fofanov/go-osgb" @@ -17,42 +22,84 @@ import ( ) func main() { - openNames := flag.String("open-names", "", "Path to Ordnance Server Open Names zip archive") + openNames := flag.String("opname", "", "Path to Ordnance Server Open Names zip archive") gpxFile := flag.String("gpx", "", "Path to GPX file") + dirName := flag.String("dir", "", "Directory to scan for GPX files") flag.Parse() if *openNames == "" { - log.Fatal("--open-names is required") + log.Fatal("--opname is required") } - if *gpxFile == "" { - log.Fatal("--gpx is required") + if (*gpxFile == "" && *dirName == "") || (*gpxFile != "" && *dirName != "") { + log.Fatal("exactly one of --dir or --gpx is required") } rt, err := buildIndex(*openNames) if err != nil { log.Fatal(err) } - points, err := readGPX(*gpxFile) + trans, err := osgb.NewOSTN15Transformer() if err != nil { log.Fatal(err) } - var dist float64 - var prevPlace string - var prevPoint rtreego.Point - for i, p := range points { - nn := rt.NearestNeighbor(p) - loc, _ := nn.(*openname.Record) - if i == 0 { - fmt.Printf("%0.2f %s\n", dist, loc.Name) - prevPlace = loc.Name - prevPoint = p + if *gpxFile != "" { + err = summarizeSingleFile(rt, trans, *gpxFile) + } else { + err = summarizeDirectory(rt, trans, *dirName) + } + if err != nil { + log.Fatal(err) + } +} + +func summarizeDirectory(rt *rtreego.Rtree, trans osgb.CoordinateTransformer, dirName string) error { + files, err := ioutil.ReadDir(dirName) + if err != nil { + return err + } + for _, f := range files { + if f.IsDir() || path.Ext(f.Name()) != ".gpx" { continue } - dist += distance(prevPoint, p) - if insideLoc(p, loc) && loc.Name != prevPlace { - fmt.Printf("%0.2f %s\n", dist/1000, loc.Name) - prevPlace = loc.Name + filename := path.Join(dirName, f.Name()) + log.Printf("Analyzing %s", filename) + summary, err := summarizeGPXTrack(rt, trans, filename) + if err != nil { + return fmt.Errorf("error creating summary of GPX track %s: %v", filename, err) + } + outfile := filename[:len(filename)-4] + ".json" + wc, err := os.OpenFile(outfile, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0644) + if err != nil { + return fmt.Errorf("error creating output file %s: %v", outfile, err) + } + err = writeSummary(summary, wc) + if err != nil { + wc.Close() + return fmt.Errorf("error marshalling JSON to %s: %v", outfile, err) + } + if err = wc.Close(); err != nil { + return fmt.Errorf("error closing file %s: %v", outfile, err) } - prevPoint = p } + return nil +} + +func summarizeSingleFile(rt *rtreego.Rtree, trans osgb.CoordinateTransformer, filename string) error { + summary, err := summarizeGPXTrack(rt, trans, filename) + if err != nil { + return fmt.Errorf("error creating summary of GPX track %s: %v", filename, err) + } + if err = writeSummary(summary, os.Stdout); err != nil { + return fmt.Errorf("error marshalling summary for %s: %v", filename, err) + } + return nil +} + +func writeSummary(s *Summary, w io.Writer) error { + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + if err := enc.Encode(s); err != nil { + return err + } + return nil } func insideLoc(p rtreego.Point, loc *openname.Record) bool { @@ -68,10 +115,26 @@ func distance(p1, p2 rtreego.Point) float64 { d := p1[i] - p2[i] s += d * d } - return math.Sqrt(s) + return math.Sqrt(s) / 1000.0 } -func readGPX(filename string) ([]rtreego.Point, error) { +type POI struct { + Name string + Distance float64 +} + +type Summary struct { + Name string + Time time.Time + Link string + Start string + Finish string + Distance float64 + Ascent float64 + PointsOfInterest []POI +} + +func summarizeGPXTrack(rt *rtreego.Rtree, trans osgb.CoordinateTransformer, filename string) (*Summary, error) { r, err := os.Open(filename) if err != nil { return nil, err @@ -80,11 +143,21 @@ func readGPX(filename string) ([]rtreego.Point, error) { if err != nil { return nil, err } - trans, err := osgb.NewOSTN15Transformer() - if err != nil { - return nil, err + var s Summary + s.Name = g.Metadata.Name + s.Time = g.Metadata.Time + for _, l := range g.Metadata.Link { + if strings.HasPrefix(l.HREF, "http") { + s.Link = l.HREF + break + } } - var points []rtreego.Point + + var prevPlace string + var prevPoint rtreego.Point + var prevHeight float64 + + init := true for _, trk := range g.Trk { for _, seg := range trk.TrkSeg { for _, p := range seg.TrkPt { @@ -93,11 +166,33 @@ func readGPX(filename string) ([]rtreego.Point, error) { if err != nil { return nil, err } - points = append(points, rtreego.Point{ngCoord.Easting, ngCoord.Northing}) + thisPoint := rtreego.Point{ngCoord.Easting, ngCoord.Northing} + thisHeight := ngCoord.Height + nn, _ := rt.NearestNeighbor(thisPoint).(*openname.Record) + if init { + s.Start = nn.Name + prevPlace = nn.Name + prevPoint = thisPoint + prevHeight = thisHeight + s.PointsOfInterest = append(s.PointsOfInterest, POI{nn.Name, 0.0}) + init = false + continue + } + s.Distance += distance(thisPoint, prevPoint) + if ascent := thisHeight - prevHeight; ascent > 0 { + s.Ascent += ascent + } + if insideLoc(thisPoint, nn) && nn.Name != prevPlace { + s.PointsOfInterest = append(s.PointsOfInterest, POI{nn.Name, s.Distance}) + prevPlace = nn.Name + } + prevPoint = thisPoint + prevHeight = thisHeight } } } - return points, nil + s.Finish = prevPlace + return &s, nil } func buildIndex(filename string) (*rtreego.Rtree, error) {