Refactor to pull useful functionailty into a package.
This commit is contained in:
parent
fb816c3132
commit
e4e4ac260f
4 changed files with 172 additions and 143 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
/data/
|
/data/
|
||||||
|
/bin/
|
||||||
|
|
|
@ -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
42
pkg/openname/index.go
Normal 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
120
pkg/openname/summarize.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue