Compare commits
10 commits
f38029736c
...
f09fd9a418
Author | SHA1 | Date | |
---|---|---|---|
|
f09fd9a418 | ||
|
4ef3eca5a4 | ||
|
bd7eb246d5 | ||
|
4de001c867 | ||
|
9ec3b2e980 | ||
|
f740686d2f | ||
|
34bdaeab71 | ||
|
bc6affbce1 | ||
|
08f37e8592 | ||
|
a3e30a5a3c |
10 changed files with 343 additions and 50 deletions
25
.gitlab-ci.yml
Normal file
25
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,25 @@
|
|||
stages:
|
||||
- build
|
||||
- deploy
|
||||
|
||||
build-gpx-anomalies:
|
||||
stage: build
|
||||
image: golang:1.15-buster
|
||||
before_script:
|
||||
- apt-get -qq update && apt-get --yes install zip
|
||||
script:
|
||||
- env GOOS=windows go build -o gpx-anomalies.exe ./cmd/gpx-anomalies
|
||||
- zip gpx-anomalies.zip gpx-anomalies.exe
|
||||
artifacts:
|
||||
paths:
|
||||
- gpx-anomalies.zip
|
||||
|
||||
publish-gpx-anomalies:
|
||||
stage: deploy
|
||||
image: curlimages/curl:latest
|
||||
variables:
|
||||
GIT_STRATEGY: none
|
||||
script:
|
||||
- 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file gpx-anomalies.zip "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/gpx-anomalies/latest/gpx-anomalies.zip"'
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
|
@ -19,8 +19,11 @@ import (
|
|||
func main() {
|
||||
log.SetFlags(0)
|
||||
stopNames := flag.String("stops", "", "Source for refreshment stops")
|
||||
minDist := flag.Float64("min-dist", 0.2, "Minimum distance (km) between points of interest")
|
||||
minSettlement := flag.String("min-settlement", "Other Settlement", "Exclude populated places smaller than this (City, Town, Village, Hamlet, Other Settlement)")
|
||||
stopRect := flag.Float64("sr", placenames.DefaultGPXSummarizerConfig.CoffeeStopSearchRectangleSize, "Size (m) of the rectangle we search for coffee stops near the route")
|
||||
stopDupDist := flag.Float64("sdd", placenames.DefaultGPXSummarizerConfig.CoffeeStopDuplicateDistance, "Suppress recurrences of coffee stops within this distance (km)")
|
||||
dupDist := flag.Float64("dd", placenames.DefaultGPXSummarizerConfig.PointOfInterestDuplicateDistance, "Suppress recurrences of points of interest within this distance (km)")
|
||||
minDist := flag.Float64("md", placenames.DefaultGPXSummarizerConfig.PointOfInterestMinimumDistance, "Minimum distance (km) between points of interest")
|
||||
minSettlement := flag.String("ms", "Other Settlement", "Exclude populated places smaller than this (City, Town, Village, Hamlet, Other Settlement)")
|
||||
flag.Parse()
|
||||
if flag.NArg() != 1 {
|
||||
log.Fatal("Usage: %s [--stops=ctccambridge|cyclingmaps] [--min-dist X] [--min-settlement S] GPX_FILE_OR_DIRECTORY")
|
||||
|
@ -38,12 +41,16 @@ func main() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
gs, err := placenames.NewGPXSummarizer()
|
||||
gs, err := placenames.NewGPXSummarizer(
|
||||
placenames.WithMinimumSettlement(*minSettlement),
|
||||
placenames.WithPointOfInterestMinimumDistance(*minDist),
|
||||
placenames.WithPointOfInterestDuplicateDistance(*dupDist),
|
||||
placenames.WithCoffeeStopSearchRectangleSize(*stopRect),
|
||||
placenames.WithCoffeeStopDuplicateDistance(*stopDupDist),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
gs.SetMinDistance(*minDist)
|
||||
gs.SetMinSettlement(*minSettlement)
|
||||
if info.IsDir() {
|
||||
err = summarizeDirectory(gs, stops, inFile)
|
||||
} else {
|
||||
|
|
135
cmd/gpx-anomalies/main.go
Normal file
135
cmd/gpx-anomalies/main.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
"github.com/fofanov/go-osgb"
|
||||
"github.com/twpayne/go-gpx"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
app := &cli.App{
|
||||
Name: "gpx-anomalies",
|
||||
Usage: "Find repeated points in a GPX track",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "gpx-file",
|
||||
Aliases: []string{"g"},
|
||||
Usage: "Name of GPX file to process",
|
||||
Required: true,
|
||||
},
|
||||
&cli.Float64Flag{
|
||||
Name: "fuzz",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "Consider two points coincident if they are within FUZZ kilometres of each other",
|
||||
Value: 0.005,
|
||||
},
|
||||
&cli.Float64Flag{
|
||||
Name: "min-distance",
|
||||
Aliases: []string{"min"},
|
||||
Usage: "Only show repeats that appear at least MIN kilometers apart",
|
||||
Value: 0.1,
|
||||
},
|
||||
&cli.Float64Flag{
|
||||
Name: "max-distance",
|
||||
Aliases: []string{"max"},
|
||||
Usage: "Do not show repeats that appear more than MAX kilometers apart",
|
||||
Value: 5.0,
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
points, err := readGPXTrack(c.String("gpx-file"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
findDuplicates(
|
||||
points,
|
||||
c.Float64("fuzz")*1000.0,
|
||||
c.Float64("min-distance")*1000.0,
|
||||
c.Float64("max-distance")*1000.0,
|
||||
)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func findDuplicates(points []RoutePoint, fuzz, minDist, maxDist float64) {
|
||||
var lastError *RoutePoint
|
||||
for i := range points {
|
||||
p := points[i]
|
||||
for j := i + 1; j < len(points); j++ {
|
||||
q := points[j]
|
||||
if p.Distance == q.Distance {
|
||||
continue
|
||||
}
|
||||
d := euclideanDistance(p.Coordinate, q.Coordinate)
|
||||
D := q.Distance - p.Distance
|
||||
if d < fuzz && D > minDist && D < maxDist {
|
||||
if lastError == nil || p.Distance-lastError.Distance > 500 {
|
||||
fmt.Printf("Point (%0.f, %0.f) revisited at %0.2f km and %0.2f km\n",
|
||||
p.Coordinate.Easting, p.Coordinate.Northing, p.Distance/1000.0, q.Distance/1000.0)
|
||||
}
|
||||
lastError = &p
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func euclideanDistance(p, q *osgb.OSGB36Coordinate) float64 {
|
||||
x := p.Easting - q.Easting
|
||||
y := p.Northing - q.Northing
|
||||
return math.Sqrt(x*x + y*y)
|
||||
}
|
||||
|
||||
func readGPXTrack(filename string) ([]RoutePoint, error) {
|
||||
r, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening %s for reading: %v", filename, err)
|
||||
}
|
||||
defer r.Close()
|
||||
g, err := gpx.Read(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading GPS track %s: %v", filename, err)
|
||||
}
|
||||
trans, err := osgb.NewOSTN15Transformer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error constructing coordinate transformer: %v", err)
|
||||
}
|
||||
distance := 0.0
|
||||
var prevPoint *osgb.OSGB36Coordinate
|
||||
var points []RoutePoint
|
||||
for _, trk := range g.Trk {
|
||||
for _, seg := range trk.TrkSeg {
|
||||
for _, trkPt := range seg.TrkPt {
|
||||
gpsCoord := osgb.NewETRS89Coord(trkPt.Lon, trkPt.Lat, trkPt.Ele)
|
||||
p, err := trans.ToNationalGrid(gpsCoord)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting coordinates to National Grid: %v", err)
|
||||
}
|
||||
if prevPoint != nil {
|
||||
distance += euclideanDistance(prevPoint, p)
|
||||
}
|
||||
points = append(points, RoutePoint{
|
||||
Coordinate: p,
|
||||
Distance: distance,
|
||||
})
|
||||
prevPoint = p
|
||||
}
|
||||
}
|
||||
}
|
||||
return points, nil
|
||||
}
|
||||
|
||||
type RoutePoint struct {
|
||||
Coordinate *osgb.OSGB36Coordinate
|
||||
Distance float64
|
||||
}
|
10
go.mod
10
go.mod
|
@ -3,12 +3,14 @@ module github.com/ray1729/gpx-utils
|
|||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/d4l3k/messagediff v1.2.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/dhconnelly/rtreego v1.0.0
|
||||
github.com/fofanov/go-osgb v0.0.0-20170711141822-6893d1f95cd9
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/twpayne/go-geom v1.3.6 // indirect
|
||||
github.com/twpayne/go-gpx v1.2.0
|
||||
github.com/wlbr/mule v0.0.0-20200329114911-0724e1639b62 // indirect
|
||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7 // indirect
|
||||
golang.org/x/text v0.3.4 // indirect
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
github.com/wlbr/mule v0.0.0-20200517121540-6f9faa2e2d0b // indirect
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 // indirect
|
||||
)
|
||||
|
|
36
go.sum
36
go.sum
|
@ -1,4 +1,5 @@
|
|||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.2/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
|
@ -7,8 +8,12 @@ github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jB
|
|||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
|
||||
github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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=
|
||||
|
@ -32,7 +37,14 @@ github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zM
|
|||
github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/ory/dockertest/v3 v3.6.0/go.mod h1:4ZOpj8qBUmh8fcBSVzkH2bws2s91JdGvHUqan4GHEuQ=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
|
@ -41,31 +53,32 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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-geom v1.3.6 h1:O27mIXZnMYiZi0ZD8ewjs/IT/ZOFVbZHBzPjA9skdmg=
|
||||
github.com/twpayne/go-geom v1.3.6/go.mod h1:XTyWHR6+l9TUYONbbK4ImUTYbWDCu2ySSPrZmmiA0Pg=
|
||||
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-gpx v1.2.0 h1:Jjq0NKXgHmEXXhmQue4KWtAVG5gxkAYY+FvsM1AliLQ=
|
||||
github.com/twpayne/go-gpx v1.2.0/go.mod h1:70xTQn0dGph3dgKIPxfl0K3XMVNpulC70/e383iHouA=
|
||||
github.com/twpayne/go-kml v1.0.0/go.mod h1:LlvLIQSfMqYk2O7Nx8vYAbSLv4K9rjMvLlEdUKWdjq0=
|
||||
github.com/twpayne/go-kml v1.5.1/go.mod h1:kz8jAiIz6FIdU2Zjce9qGlVtgFYES9vt7BTPBHf5jl4=
|
||||
github.com/twpayne/go-polyline v1.0.0/go.mod h1:ICh24bcLYBX8CknfvNPKqoTbe+eg+MX1NPyJmSBo7pU=
|
||||
github.com/twpayne/go-waypoint v0.0.0-20200706203930-b263a7f6e4e8/go.mod h1:qj5pHncxKhu9gxtZEYWypA/z097sxhFlbTyOyt9gcnU=
|
||||
github.com/wlbr/mule v0.0.0-20200329114911-0724e1639b62 h1:vHDdpwOGHzfFKbMLEnnM0s1jnGLjsQ9EPWtCFWMJs8o=
|
||||
github.com/wlbr/mule v0.0.0-20200329114911-0724e1639b62/go.mod h1:uDXgZTfL0uJWiY/MQKcqI5VPQV8PCooNsWXozHf7CJ8=
|
||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/wlbr/mule v0.0.0-20200517121540-6f9faa2e2d0b h1:wZDyxL+jeSaBLmUFM+k/P97BbS8z3QYKcfbvWPnKq9Q=
|
||||
github.com/wlbr/mule v0.0.0-20200517121540-6f9faa2e2d0b/go.mod h1:uDXgZTfL0uJWiY/MQKcqI5VPQV8PCooNsWXozHf7CJ8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
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/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7 h1:3uJsdck53FDIpWwLeAXlia9p4C8j0BO2xZrqzKpL0D8=
|
||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 h1:4qWs8cYYH6PoEFy4dfhDFgoMGkwAcETd+MmPdCPMzUc=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -74,16 +87,23 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200121082415-34d275377bf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||
|
|
|
@ -9,6 +9,9 @@ import (
|
|||
"github.com/dhconnelly/rtreego"
|
||||
)
|
||||
|
||||
// Size (in metres) of the bounding box around a stop
|
||||
const stopRectangleSize = 50
|
||||
|
||||
type RefreshmentStop struct {
|
||||
Name string
|
||||
Url string
|
||||
|
@ -18,7 +21,7 @@ type RefreshmentStop struct {
|
|||
|
||||
func (s *RefreshmentStop) Bounds() *rtreego.Rect {
|
||||
p := rtreego.Point{s.Easting, s.Northing}
|
||||
return p.ToRect(100)
|
||||
return p.ToRect(stopRectangleSize)
|
||||
}
|
||||
|
||||
func (s *RefreshmentStop) Contains(p rtreego.Point) bool {
|
||||
|
|
|
@ -54,7 +54,12 @@ func BuildCtcCamIndex(r io.Reader) (*rtreego.Rtree, error) {
|
|||
|
||||
func FetchCtcCamIndex() (*rtreego.Rtree, error) {
|
||||
log.Printf("Fetching %s", ctcCamWaypointsUrl)
|
||||
res, err := http.Get(ctcCamWaypointsUrl)
|
||||
req, err := http.NewRequest(http.MethodGet, ctcCamWaypointsUrl, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error constructing waypoints request: %v", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "gpx-utils")
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting %s: %v", ctcCamWaypointsUrl, err)
|
||||
}
|
||||
|
|
|
@ -9,9 +9,8 @@ import (
|
|||
|
||||
"github.com/dhconnelly/rtreego"
|
||||
"github.com/fofanov/go-osgb"
|
||||
"github.com/twpayne/go-gpx"
|
||||
|
||||
"github.com/ray1729/gpx-utils/pkg/cafes"
|
||||
"github.com/twpayne/go-gpx"
|
||||
)
|
||||
|
||||
var populatedPlaceRank = map[string]int{
|
||||
|
@ -22,14 +21,80 @@ var populatedPlaceRank = map[string]int{
|
|||
"Other Settlement": 1,
|
||||
}
|
||||
|
||||
// GPXSummarizerConfig allows override of defaults used by the search algorithm.
|
||||
type GPXSummarizerConfig struct {
|
||||
CoffeeStopSearchRectangleSize float64
|
||||
CoffeeStopDuplicateDistance float64
|
||||
PointOfInterestDuplicateDistance float64
|
||||
PointOfInterestMinimumDistance float64
|
||||
MinimumSettlementRank int
|
||||
}
|
||||
|
||||
var DefaultGPXSummarizerConfig = GPXSummarizerConfig{
|
||||
CoffeeStopSearchRectangleSize: 500.0, // m
|
||||
CoffeeStopDuplicateDistance: 2.0, // km
|
||||
PointOfInterestDuplicateDistance: 1.0, // km
|
||||
PointOfInterestMinimumDistance: 0.0, // km
|
||||
MinimumSettlementRank: 1, // "Other Settlement"
|
||||
}
|
||||
|
||||
type Option func(*GPXSummarizerConfig)
|
||||
|
||||
// WithCoffeeStopSearchRectangleSize overrides the size (in metres) of the rectangle searched
|
||||
// for coffee stops near the route. Default 500m.
|
||||
func WithCoffeeStopSearchRectangleSize(d float64) Option {
|
||||
return func(c *GPXSummarizerConfig) {
|
||||
c.CoffeeStopSearchRectangleSize = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithCoffeeStopDuplicateDistance overrides the distance (in kilometers) we look back along the
|
||||
// route when suppressing duplicate coffee stop entries. This should be at least twice the
|
||||
// CoffeeStopSearchRectangleSize. Default 2km.
|
||||
func WithCoffeeStopDuplicateDistance(d float64) Option {
|
||||
return func(c *GPXSummarizerConfig) {
|
||||
c.CoffeeStopDuplicateDistance = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithPointOfInterestDuplicateDistance overrides the distance (in km) we look back along
|
||||
// the route when suppressing duplicate points of interest.
|
||||
func WithPointOfInterestDuplicateDistance(d float64) Option {
|
||||
return func(c *GPXSummarizerConfig) {
|
||||
c.PointOfInterestDuplicateDistance = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithPointOfInterestMinimumDistance overrides the minimum distance (in km) between points
|
||||
// of interest (if two POI appear within this distance, the second one is suppressed). Default
|
||||
// 0km (no suppression).
|
||||
func WithPointOfInterestMinimumDistance(d float64) Option {
|
||||
return func(c *GPXSummarizerConfig) {
|
||||
c.PointOfInterestMinimumDistance = d
|
||||
}
|
||||
}
|
||||
|
||||
func WithMinimumSettlement(s string) Option {
|
||||
rank, ok := populatedPlaceRank[s]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("invalid settlement type: %s", s))
|
||||
}
|
||||
return func(c *GPXSummarizerConfig) {
|
||||
c.MinimumSettlementRank = rank
|
||||
}
|
||||
}
|
||||
|
||||
type GPXSummarizer struct {
|
||||
poi *rtreego.Rtree
|
||||
trans osgb.CoordinateTransformer
|
||||
minDist float64
|
||||
minSettlementRank int
|
||||
conf GPXSummarizerConfig
|
||||
}
|
||||
|
||||
func NewGPXSummarizer() (*GPXSummarizer, error) {
|
||||
func NewGPXSummarizer(opts ...Option) (*GPXSummarizer, error) {
|
||||
conf := DefaultGPXSummarizerConfig
|
||||
for _, f := range opts {
|
||||
f(&conf)
|
||||
}
|
||||
trans, err := osgb.NewOSTN15Transformer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -38,15 +103,7 @@ func NewGPXSummarizer() (*GPXSummarizer, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &GPXSummarizer{poi: rt, trans: trans, minDist: 0.2, minSettlementRank: 1}, nil
|
||||
}
|
||||
|
||||
func (gs *GPXSummarizer) SetMinSettlement(t string) {
|
||||
gs.minSettlementRank = populatedPlaceRank[t]
|
||||
}
|
||||
|
||||
func (gs *GPXSummarizer) SetMinDistance(d float64) {
|
||||
gs.minDist = d
|
||||
return &GPXSummarizer{poi: rt, trans: trans, conf: conf}, nil
|
||||
}
|
||||
|
||||
func distance(p1, p2 rtreego.Point) float64 {
|
||||
|
@ -108,7 +165,6 @@ func (gs *GPXSummarizer) SummarizeTrack(r io.Reader, stops *rtreego.Rtree) (*Tra
|
|||
var prevPlace string
|
||||
var prevPlacePoint rtreego.Point
|
||||
var prevPoint rtreego.Point
|
||||
var prevStop *cafes.RefreshmentStop
|
||||
var start rtreego.Point
|
||||
var dN, dE float64
|
||||
|
||||
|
@ -141,25 +197,44 @@ func (gs *GPXSummarizer) SummarizeTrack(r io.Reader, stops *rtreego.Rtree) (*Tra
|
|||
s.Distance += distance(thisPoint, prevPoint)
|
||||
dE += thisPoint[0] - start[0]
|
||||
dN += thisPoint[1] - start[1]
|
||||
if nn.Contains(thisPoint) {
|
||||
if nn.Contains(thisPoint) && populatedPlaceRank[nn.Type] >= gs.conf.MinimumSettlementRank {
|
||||
s.Counties[nn.County]++
|
||||
if nn.Name != prevPlace &&
|
||||
distance(thisPoint, prevPlacePoint) > gs.minDist &&
|
||||
populatedPlaceRank[nn.Type] >= gs.minSettlementRank {
|
||||
seenRecently := false
|
||||
for i := len(s.PointsOfInterest) - 1; i >= 0; i-- {
|
||||
if i < len(s.PointsOfInterest)-1 && s.Distance-s.PointsOfInterest[i].Distance > gs.conf.PointOfInterestDuplicateDistance {
|
||||
break
|
||||
}
|
||||
if nn.Name == s.PointsOfInterest[i].Name {
|
||||
seenRecently = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !seenRecently && distance(thisPoint, prevPlacePoint) > gs.conf.PointOfInterestMinimumDistance {
|
||||
s.PointsOfInterest = append(s.PointsOfInterest, POI{Name: nn.Name, Type: nn.Type, Distance: s.Distance})
|
||||
prevPlace = nn.Name
|
||||
prevPlacePoint = thisPoint
|
||||
}
|
||||
}
|
||||
if stops != nil {
|
||||
stop, ok := stops.NearestNeighbor(thisPoint).(*cafes.RefreshmentStop)
|
||||
if ok && stop.Contains(thisPoint) && (prevStop == nil || stop.Name != prevStop.Name) {
|
||||
for _, nearbyStop := range stops.SearchIntersect(thisPoint.ToRect(gs.conf.CoffeeStopSearchRectangleSize)) {
|
||||
stop := nearbyStop.(*cafes.RefreshmentStop)
|
||||
seenRecently := false
|
||||
for i := len(s.RefreshmentStops) - 1; i >= 0; i-- {
|
||||
if i < len(s.RefreshmentStops)-1 && s.Distance-s.RefreshmentStops[i].Distance > gs.conf.CoffeeStopDuplicateDistance {
|
||||
break
|
||||
}
|
||||
if s.RefreshmentStops[i].Name == stop.Name {
|
||||
seenRecently = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !seenRecently {
|
||||
s.RefreshmentStops = append(s.RefreshmentStops, RefreshmentStop{
|
||||
Name: stop.Name,
|
||||
Url: stop.Url,
|
||||
Distance: s.Distance,
|
||||
})
|
||||
prevStop = stop
|
||||
}
|
||||
}
|
||||
}
|
||||
prevPoint = thisPoint
|
||||
|
|
|
@ -41,7 +41,7 @@ func (h *RWGPSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
routeId, err := strconv.Atoi(rawRouteId)
|
||||
if err != nil {
|
||||
log.Println("Error parsing route id '%s': %v", rawRouteId, err)
|
||||
log.Printf("Error parsing route id '%s': %v", rawRouteId, err)
|
||||
http.Error(w, fmt.Sprintf("Invalid routeId: %s", rawRouteId), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
|
21
scripts/summarizeRoutes.py
Normal file
21
scripts/summarizeRoutes.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import csv
|
||||
import sys
|
||||
|
||||
path = sys.argv[1]
|
||||
|
||||
with open(path) as f:
|
||||
r = csv.reader(f)
|
||||
skip = True
|
||||
for x in r:
|
||||
if skip:
|
||||
skip = False
|
||||
continue
|
||||
coffee = x[7].strip()
|
||||
lunch = x[8].strip()
|
||||
tea = x[9].strip()
|
||||
if coffee and tea:
|
||||
print(coffee + ", " + lunch + ", " + tea)
|
||||
|
||||
|
Loading…
Reference in a new issue