Refactor to pull useful functionailty into a package.

This commit is contained in:
Ray Miller 2020-04-16 08:52:37 +01:00
parent fb816c3132
commit e4e4ac260f
4 changed files with 172 additions and 143 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
/data/ /data/
/bin/

View file

@ -1,22 +1,14 @@
package main package main
import ( import (
"archive/zip"
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"math"
"os" "os"
"path" "path"
"strings"
"time"
"github.com/dhconnelly/rtreego"
"github.com/fofanov/go-osgb"
"github.com/twpayne/go-gpx"
"github.com/ray1729/gpx-utils/pkg/openname" "github.com/ray1729/gpx-utils/pkg/openname"
) )
@ -32,25 +24,25 @@ func main() {
if (*gpxFile == "" && *dirName == "") || (*gpxFile != "" && *dirName != "") { if (*gpxFile == "" && *dirName == "") || (*gpxFile != "" && *dirName != "") {
log.Fatal("exactly one of --dir or --gpx is required") log.Fatal("exactly one of --dir or --gpx is required")
} }
rt, err := buildIndex(*openNames) rt, err := openname.BuildIndex(*openNames)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
trans, err := osgb.NewOSTN15Transformer() gs, err := openname.NewGPXSummarizer(rt)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if *gpxFile != "" { if *gpxFile != "" {
err = summarizeSingleFile(rt, trans, *gpxFile) err = summarizeSingleFile(gs, *gpxFile)
} else { } else {
err = summarizeDirectory(rt, trans, *dirName) err = summarizeDirectory(gs, *dirName)
} }
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
func summarizeDirectory(rt *rtreego.Rtree, trans osgb.CoordinateTransformer, dirName string) error { func summarizeDirectory(gs *openname.GPXSummarizer, dirName string) error {
files, err := ioutil.ReadDir(dirName) files, err := ioutil.ReadDir(dirName)
if err != nil { if err != nil {
return err return err
@ -61,7 +53,7 @@ func summarizeDirectory(rt *rtreego.Rtree, trans osgb.CoordinateTransformer, dir
} }
filename := path.Join(dirName, f.Name()) filename := path.Join(dirName, f.Name())
log.Printf("Analyzing %s", filename) log.Printf("Analyzing %s", filename)
summary, err := summarizeGPXTrack(rt, trans, filename) summary, err := gs.SummarizeTrack(filename)
if err != nil { if err != nil {
return fmt.Errorf("error creating summary of GPX track %s: %v", filename, err) return fmt.Errorf("error creating summary of GPX track %s: %v", filename, err)
} }
@ -82,8 +74,8 @@ func summarizeDirectory(rt *rtreego.Rtree, trans osgb.CoordinateTransformer, dir
return nil return nil
} }
func summarizeSingleFile(rt *rtreego.Rtree, trans osgb.CoordinateTransformer, filename string) error { func summarizeSingleFile(gs *openname.GPXSummarizer, filename string) error {
summary, err := summarizeGPXTrack(rt, trans, filename) summary, err := gs.SummarizeTrack(filename)
if err != nil { if err != nil {
return fmt.Errorf("error creating summary of GPX track %s: %v", filename, err) return fmt.Errorf("error creating summary of GPX track %s: %v", filename, err)
} }
@ -93,7 +85,7 @@ func summarizeSingleFile(rt *rtreego.Rtree, trans osgb.CoordinateTransformer, fi
return nil return nil
} }
func writeSummary(s *Summary, w io.Writer) error { func writeSummary(s *openname.TrackSummary, w io.Writer) error {
enc := json.NewEncoder(w) enc := json.NewEncoder(w)
enc.SetIndent("", " ") enc.SetIndent("", " ")
if err := enc.Encode(s); err != nil { if err := enc.Encode(s); err != nil {
@ -101,129 +93,3 @@ func writeSummary(s *Summary, w io.Writer) error {
} }
return nil return nil
} }
func insideLoc(p rtreego.Point, loc *openname.Record) bool {
return p[0] >= loc.MbrXMin && p[0] <= loc.MbrXMax && p[1] >= loc.MbrYMin && p[1] <= loc.MbrYMax
}
func distance(p1, p2 rtreego.Point) float64 {
if len(p1) != len(p2) {
panic("Length mismatch")
}
var s float64
for i := range p1 {
d := p1[i] - p2[i]
s += d * d
}
return math.Sqrt(s) / 1000.0
}
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
}
g, err := gpx.Read(r)
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 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 {
gpsCoord := osgb.NewETRS89Coord(p.Lon, p.Lat, p.Ele)
ngCoord, err := trans.ToNationalGrid(gpsCoord)
if err != nil {
return nil, err
}
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
}
}
}
s.Finish = prevPlace
return &s, nil
}
func buildIndex(filename string) (*rtreego.Rtree, error) {
r, err := zip.OpenReader(filename)
if err != nil {
log.Fatal(err)
}
defer r.Close()
rt := rtreego.NewTree(2, 25, 50)
for _, f := range r.File {
if !(strings.HasPrefix(f.Name, "DATA/") && strings.HasSuffix(f.Name, ".csv")) {
continue
}
rc, err := f.Open()
if err != nil {
log.Fatal(err)
}
defer rc.Close()
s, err := openname.NewScanner(rc)
if err != nil {
log.Fatalf("Error reading %s: %v", f.Name, err)
}
for s.Scan() {
r := s.Record()
if r.Type == "populatedPlace" && r.MbrXMax != r.MbrXMin && r.MbrYMax != r.MbrYMin {
rt.Insert(r)
}
}
if err = s.Err(); err != nil {
log.Fatalf("Error parsing %s: %v", f.Name, err)
}
}
return rt, nil
}

42
pkg/openname/index.go Normal file
View file

@ -0,0 +1,42 @@
package openname
import (
"archive/zip"
"log"
"strings"
"github.com/dhconnelly/rtreego"
)
func BuildIndex(filename string) (*rtreego.Rtree, error) {
r, err := zip.OpenReader(filename)
if err != nil {
log.Fatal(err)
}
defer r.Close()
rt := rtreego.NewTree(2, 25, 50)
for _, f := range r.File {
if !(strings.HasPrefix(f.Name, "DATA/") && strings.HasSuffix(f.Name, ".csv")) {
continue
}
rc, err := f.Open()
if err != nil {
log.Fatal(err)
}
defer rc.Close()
s, err := NewScanner(rc)
if err != nil {
log.Fatalf("Error reading %s: %v", f.Name, err)
}
for s.Scan() {
r := s.Record()
if r.Type == "populatedPlace" && r.MbrXMax != r.MbrXMin && r.MbrYMax != r.MbrYMin {
rt.Insert(r)
}
}
if err = s.Err(); err != nil {
log.Fatalf("Error parsing %s: %v", f.Name, err)
}
}
return rt, nil
}

120
pkg/openname/summarize.go Normal file
View file

@ -0,0 +1,120 @@
package openname
import (
"math"
"os"
"strings"
"time"
"github.com/dhconnelly/rtreego"
"github.com/fofanov/go-osgb"
"github.com/twpayne/go-gpx"
)
type GPXSummarizer struct {
rt *rtreego.Rtree
trans osgb.CoordinateTransformer
}
func NewGPXSummarizer(rt *rtreego.Rtree) (*GPXSummarizer, error) {
trans, err := osgb.NewOSTN15Transformer()
if err != nil {
if err != nil {
return nil, err
}
}
return &GPXSummarizer{rt: rt, trans: trans}, nil
}
func insideLoc(p rtreego.Point, loc *Record) bool {
return p[0] >= loc.MbrXMin && p[0] <= loc.MbrXMax && p[1] >= loc.MbrYMin && p[1] <= loc.MbrYMax
}
func distance(p1, p2 rtreego.Point) float64 {
if len(p1) != len(p2) {
panic("Length mismatch")
}
var s float64
for i := range p1 {
d := p1[i] - p2[i]
s += d * d
}
return math.Sqrt(s) / 1000.0
}
type POI struct {
Name string
Distance float64
}
type TrackSummary struct {
Name string
Time time.Time
Link string
Start string
Finish string
Distance float64
Ascent float64
PointsOfInterest []POI
}
func (gs *GPXSummarizer) SummarizeTrack(filename string) (*TrackSummary, error) {
r, err := os.Open(filename)
if err != nil {
return nil, err
}
g, err := gpx.Read(r)
if err != nil {
return nil, err
}
var s TrackSummary
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 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 {
gpsCoord := osgb.NewETRS89Coord(p.Lon, p.Lat, p.Ele)
ngCoord, err := gs.trans.ToNationalGrid(gpsCoord)
if err != nil {
return nil, err
}
thisPoint := rtreego.Point{ngCoord.Easting, ngCoord.Northing}
thisHeight := ngCoord.Height
nn, _ := gs.rt.NearestNeighbor(thisPoint).(*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
}
}
}
s.Finish = prevPlace
return &s, nil
}