Initial check-in.
This commit is contained in:
commit
f41b1a01e3
5 changed files with 324 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/data/
|
134
cmd/analyze-gpx/main.go
Normal file
134
cmd/analyze-gpx/main.go
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/dhconnelly/rtreego"
|
||||||
|
"github.com/fofanov/go-osgb"
|
||||||
|
"github.com/twpayne/go-gpx"
|
||||||
|
|
||||||
|
"github.com/ray1729/gpx-utils/pkg/openname"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
openNames := flag.String("open-names", "", "Path to Ordnance Server Open Names zip archive")
|
||||||
|
gpxFile := flag.String("gpx", "", "Path to GPX file")
|
||||||
|
flag.Parse()
|
||||||
|
if *openNames == "" {
|
||||||
|
log.Fatal("--open-names is required")
|
||||||
|
}
|
||||||
|
if *gpxFile == "" {
|
||||||
|
log.Fatal("--gpx is required")
|
||||||
|
}
|
||||||
|
rt, err := buildIndex(*openNames)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
points, err := readGPX(*gpxFile)
|
||||||
|
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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
prevPoint = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readGPX(filename string) ([]rtreego.Point, error) {
|
||||||
|
r, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
g, err := gpx.Read(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
trans, err := osgb.NewOSTN15Transformer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var points []rtreego.Point
|
||||||
|
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
|
||||||
|
}
|
||||||
|
points = append(points, rtreego.Point{ngCoord.Easting, ngCoord.Northing})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return points, 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
|
||||||
|
}
|
9
go.mod
Normal file
9
go.mod
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
module github.com/ray1729/gpx-utils
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dhconnelly/rtreego v1.0.0
|
||||||
|
github.com/fofanov/go-osgb v0.0.0-20170711141822-6893d1f95cd9
|
||||||
|
github.com/twpayne/go-gpx v1.1.1
|
||||||
|
)
|
16
go.sum
Normal file
16
go.sum
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dhconnelly/rtreego v1.0.0 h1:1+V1STGw+zwx7jpvH/fwbeC5w5gZfn+XinARU45oRek=
|
||||||
|
github.com/dhconnelly/rtreego v1.0.0/go.mod h1:SDozu0Fjy17XH1svEXJgdYq8Tah6Zjfa/4Q33Z80+KM=
|
||||||
|
github.com/fofanov/go-osgb v0.0.0-20170711141822-6893d1f95cd9 h1:thlfI7kPFFxkrzB6HUgSgAkfj3YuY5PBXETFl1EAs10=
|
||||||
|
github.com/fofanov/go-osgb v0.0.0-20170711141822-6893d1f95cd9/go.mod h1:eEWh/C5XNJx6u/8Bi+hN16JEJk0P4g494xKPdHrooZk=
|
||||||
|
github.com/twpayne/go-geom v1.0.0 h1:ARrRnN4+rBX3LZFZQy9NFeXXlgQqVL4OOvAcnLdgy2g=
|
||||||
|
github.com/twpayne/go-geom v1.0.0/go.mod h1:RWsl+e3XSahOul/KH2BHCfF0QxSL4RMnMlFw/TNmET0=
|
||||||
|
github.com/twpayne/go-gpx v1.1.1 h1:vbg0lRc/ZKSu8ev84/hJWZtplKJdBbucNmks4TNzSqQ=
|
||||||
|
github.com/twpayne/go-gpx v1.1.1/go.mod h1:fQ+EsiFNgDuErUYyI0ZOgZPB+ACxW58L16oormty798=
|
||||||
|
github.com/twpayne/go-kml v1.0.0/go.mod h1:LlvLIQSfMqYk2O7Nx8vYAbSLv4K9rjMvLlEdUKWdjq0=
|
||||||
|
github.com/twpayne/go-polyline v1.0.0/go.mod h1:ICh24bcLYBX8CknfvNPKqoTbe+eg+MX1NPyJmSBo7pU=
|
||||||
|
golang.org/x/net v0.0.0-20180824152047-4bcd98cce591 h1:4S2XUgvg3hUNTvxI307qkFPb9zKHG3Nf9TXFzX/DZZI=
|
||||||
|
golang.org/x/net v0.0.0-20180824152047-4bcd98cce591/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
164
pkg/openname/parse.go
Normal file
164
pkg/openname/parse.go
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
package openname
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/csv"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/dhconnelly/rtreego"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Record struct {
|
||||||
|
ID string
|
||||||
|
NamesUri string
|
||||||
|
Name string
|
||||||
|
NameLang string
|
||||||
|
AltName string
|
||||||
|
AltNameLang string
|
||||||
|
Type string
|
||||||
|
LocalType string
|
||||||
|
GeomX float64
|
||||||
|
GeomY float64
|
||||||
|
MostDetailViewRes float64
|
||||||
|
LeastDetailViewRes float64
|
||||||
|
MbrXMin float64
|
||||||
|
MbrYMin float64
|
||||||
|
MbrXMax float64
|
||||||
|
MbrYMax float64
|
||||||
|
PostcodeDistrict string
|
||||||
|
PostcodeDistrictUri string
|
||||||
|
PopulatedPlace string
|
||||||
|
PopulatedPlaceUri string
|
||||||
|
PopulatedPlaceType string
|
||||||
|
DistrictBorough string
|
||||||
|
DistrictBoroughUri string
|
||||||
|
DistrictBoroughType string
|
||||||
|
CountyUnitary string
|
||||||
|
ConutyUnitaryUri string
|
||||||
|
CountyUnitaryType string
|
||||||
|
Region string
|
||||||
|
RegionUri string
|
||||||
|
Country string
|
||||||
|
CountryUri string
|
||||||
|
RelativeSpatialObject string
|
||||||
|
SameAsDbpedia string
|
||||||
|
SameAsGeonames string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Record) Bounds() *rtreego.Rect {
|
||||||
|
p := rtreego.Point{r.MbrXMin, r.MbrYMin}
|
||||||
|
rect, err := rtreego.NewRect(p, []float64{r.MbrXMax - r.MbrXMin, r.MbrYMax - r.MbrYMin})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return rect
|
||||||
|
}
|
||||||
|
|
||||||
|
type Scanner struct {
|
||||||
|
csvReader *csv.Reader
|
||||||
|
nextRecord *Record
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScanner(r io.Reader) (*Scanner, error) {
|
||||||
|
br := bufio.NewReader(r)
|
||||||
|
err := skipBOM(br)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Scanner{csvReader: csv.NewReader(br)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var BOM = [3]byte{0xef, 0xbb, 0xbf}
|
||||||
|
|
||||||
|
func skipBOM(br *bufio.Reader) error {
|
||||||
|
xs, err := br.Peek(3)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if xs[0] == BOM[0] && xs[1] == BOM[1] && xs[2] == BOM[2] {
|
||||||
|
br.Discard(3)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) Scan() bool {
|
||||||
|
rawRecord, err := s.csvReader.Read()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
s.err = err
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
s.nextRecord, err = parseRecord(rawRecord)
|
||||||
|
if err != nil {
|
||||||
|
s.err = err
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) Err() error {
|
||||||
|
return s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) Record() *Record {
|
||||||
|
return s.nextRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRecord(xs []string) (*Record, error) {
|
||||||
|
if len(xs) != 34 {
|
||||||
|
return nil, csv.ErrFieldCount
|
||||||
|
}
|
||||||
|
record := Record{
|
||||||
|
ID: xs[0],
|
||||||
|
NamesUri: xs[1],
|
||||||
|
Name: xs[2],
|
||||||
|
NameLang: xs[3],
|
||||||
|
AltName: xs[4],
|
||||||
|
AltNameLang: xs[5],
|
||||||
|
Type: xs[6],
|
||||||
|
LocalType: xs[7],
|
||||||
|
GeomX: 0,
|
||||||
|
GeomY: 0,
|
||||||
|
MostDetailViewRes: 0,
|
||||||
|
LeastDetailViewRes: 0,
|
||||||
|
MbrXMin: 0,
|
||||||
|
MbrYMin: 0,
|
||||||
|
MbrXMax: 0,
|
||||||
|
MbrYMax: 0,
|
||||||
|
PostcodeDistrict: xs[16],
|
||||||
|
PostcodeDistrictUri: xs[17],
|
||||||
|
PopulatedPlace: xs[18],
|
||||||
|
PopulatedPlaceUri: xs[19],
|
||||||
|
PopulatedPlaceType: xs[20],
|
||||||
|
DistrictBorough: xs[21],
|
||||||
|
DistrictBoroughUri: xs[22],
|
||||||
|
DistrictBoroughType: xs[23],
|
||||||
|
CountyUnitary: xs[24],
|
||||||
|
ConutyUnitaryUri: xs[25],
|
||||||
|
CountyUnitaryType: xs[26],
|
||||||
|
Region: xs[27],
|
||||||
|
RegionUri: xs[28],
|
||||||
|
Country: xs[29],
|
||||||
|
CountryUri: xs[30],
|
||||||
|
RelativeSpatialObject: xs[31],
|
||||||
|
SameAsDbpedia: xs[32],
|
||||||
|
SameAsGeonames: xs[33],
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, p := range []*float64{&record.GeomX, &record.GeomY, &record.MostDetailViewRes, &record.LeastDetailViewRes, &record.MbrXMin, &record.MbrYMin, &record.MbrXMax, &record.MbrYMax} {
|
||||||
|
s := xs[i+8]
|
||||||
|
if s != "" {
|
||||||
|
var err error
|
||||||
|
*p, err = strconv.ParseFloat(s, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &record, nil
|
||||||
|
}
|
Loading…
Reference in a new issue