Initial check-in.

This commit is contained in:
Ray Miller 2020-04-15 09:46:58 +01:00
commit f41b1a01e3
5 changed files with 324 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/data/

134
cmd/analyze-gpx/main.go Normal file
View 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
View 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
View 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
View 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
}