commit f41b1a01e367da3296726f5891478ab9384ae81a Author: Ray Miller Date: Wed Apr 15 09:46:58 2020 +0100 Initial check-in. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82f0c3a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/data/ diff --git a/cmd/analyze-gpx/main.go b/cmd/analyze-gpx/main.go new file mode 100644 index 0000000..08eceee --- /dev/null +++ b/cmd/analyze-gpx/main.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..355c189 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b49d757 --- /dev/null +++ b/go.sum @@ -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= diff --git a/pkg/openname/parse.go b/pkg/openname/parse.go new file mode 100644 index 0000000..b0e4461 --- /dev/null +++ b/pkg/openname/parse.go @@ -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 +}