diff --git a/.gitignore b/.gitignore index a1338d6..6332060 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# JetBrains +.idea + # Binaries for programs and plugins *.exe *.dll diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e874638 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 openmarketplaceengine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 38a99e9..1a26adf 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ import ( "time" osrm "github.com/openmarketplaceengine/go-osrm" - geo "github.com/paulmach/go.geo" + geo "github.com/paulmach/orb" ) func main() { diff --git a/client.go b/client.go index de1d54d..0439609 100644 --- a/client.go +++ b/client.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/openmarketplaceengine/go-osrm/types" "io" "io/ioutil" "net/http" @@ -30,7 +31,7 @@ func newClient(serverURL string, c HTTPClient) client { } // doRequest makes GET request to OSRM server and decodes the given JSON -func (c client) doRequest(ctx context.Context, in *request, out interface{}) error { +func (c client) doRequest(ctx context.Context, in *types.Request, out interface{}) error { url, err := in.URL(c.serverURL) if err != nil { return err diff --git a/client_test.go b/client_test.go index 3cac90b..396c262 100644 --- a/client_test.go +++ b/client_test.go @@ -3,6 +3,7 @@ package osrm import ( "context" "fmt" + "github.com/openmarketplaceengine/go-osrm/types" "net/http" "net/http/httptest" "testing" @@ -25,10 +26,10 @@ func Test_doRequestWithBadHTTPCode(t *testing.T) { defer ts.Close() c := newClient(ts.URL, &http.Client{}) - req := request{ - profile: "something", - coords: geometry, - service: "foobar", + req := types.Request{ + Profile: "something", + Coords: geometry, + Service: "foobar", } err := c.doRequest(context.Background(), &req, nil) require.EqualError(t, err, "unexpected http status code 500 with body \"\"") @@ -41,10 +42,10 @@ func Test_doRequestWithBodyUnmarshalFailure(t *testing.T) { defer ts.Close() c := newClient(ts.URL, &http.Client{}) - req := request{ - profile: "something", - coords: geometry, - service: "foobar", + req := types.Request{ + Profile: "something", + Coords: geometry, + Service: "foobar", } err := c.doRequest(context.Background(), &req, nil) require.EqualError(t, err, "failed to unmarshal body \"\": unexpected end of JSON input") diff --git a/examples_test.go b/examples_test.go index 2a02d9a..da06375 100644 --- a/examples_test.go +++ b/examples_test.go @@ -3,27 +3,29 @@ package osrm_test import ( "context" "fmt" + "github.com/openmarketplaceengine/go-osrm/route" + "github.com/openmarketplaceengine/go-osrm/types" "log" osrm "github.com/openmarketplaceengine/go-osrm" - geo "github.com/paulmach/go.geo" + geo "github.com/paulmach/orb" ) func ExampleOSRM_Route() { client := osrm.NewFromURL("https://router.project-osrm.org") - resp, err := client.Route(context.Background(), osrm.RouteRequest{ + resp, err := client.Route(context.Background(), route.Request{ Profile: "car", - Coordinates: osrm.NewGeometryFromPointSet(geo.PointSet{ + Coordinates: types.NewGeometryFromMultiPoint(geo.MultiPoint{ {-73.87946, 40.75833}, {-73.87925, 40.75837}, {-73.87918, 40.75837}, {-73.87911, 40.75838}, }), - Steps: osrm.StepsTrue, - Annotations: osrm.AnnotationsTrue, - Overview: osrm.OverviewFalse, - Geometries: osrm.GeometriesPolyline6, + Steps: types.StepsTrue, + Annotations: types.AnnotationsTrue, + Overview: types.OverviewFalse, + Geometries: types.GeometriesPolyline6, }) if err != nil { log.Fatalf("route failed: %v", err) diff --git a/go.mod b/go.mod index 1e63e4c..32cd074 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,15 @@ module github.com/openmarketplaceengine/go-osrm +go 1.18 + +require ( + github.com/paulmach/orb v0.7.1 + github.com/stretchr/testify v1.7.2 + github.com/twpayne/go-polyline v1.1.1 +) + require ( - github.com/paulmach/go.geo v0.0.0-20180829195134-22b514266d33 - github.com/paulmach/go.geojson v1.4.0 // indirect - github.com/stretchr/testify v1.3.0 + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0cc69fb..b85ffde 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,57 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/paulmach/go.geo v0.0.0-20180829195134-22b514266d33 h1:doG/0aLlWE6E4ndyQlkAQrPwaojghwz1IlmH0kjTdyk= -github.com/paulmach/go.geo v0.0.0-20180829195134-22b514266d33/go.mod h1:btFYk/ltlMU7ZKguHS7zQrwHYCtLoXGTaa44OsPbEVw= -github.com/paulmach/go.geojson v1.4.0 h1:5x5moCkCtDo5x8af62P9IOAYGQcYHtxz2QJ3x1DoCgY= -github.com/paulmach/go.geojson v1.4.0/go.mod h1:YaKx1hKpWF+T2oj2lFJPsW/t1Q5e1jQI61eoQSTwpIs= +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/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/paulmach/orb v0.7.1 h1:Zha++Z5OX/l168sqHK3k4z18LDvr+YAO/VjK0ReQ9rU= +github.com/paulmach/orb v0.7.1/go.mod h1:FWRlTgl88VI1RBx/MkrwWDRhQ96ctqMCh8boXhmqB/A= +github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/twpayne/go-polyline v1.1.1 h1:/tSF1BR7rN4HWj4XKqvRUNrCiYVMCvywxTFVofvDV0w= +github.com/twpayne/go-polyline v1.1.1/go.mod h1:ybd9IWWivW/rlXPXuuckeKUyF3yrIim+iqA7kSl4NFY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/match.go b/match.go deleted file mode 100644 index 467f29b..0000000 --- a/match.go +++ /dev/null @@ -1,75 +0,0 @@ -package osrm - -import geo "github.com/paulmach/go.geo" - -// MatchRequest represents a request to the match method -type MatchRequest struct { - Profile string - Coordinates Geometry - Bearings []Bearing - Steps Steps - Annotations Annotations - Tidy Tidy - Timestamps []int64 - Radiuses []float64 - Hints []string - Overview Overview - Gaps Gaps - Geometries Geometries -} - -// MatchResponse represents a response from the match method -type MatchResponse struct { - ResponseStatus - Matchings []Matching `json:"matchings"` - Tracepoints []*Tracepoint `json:"tracepoints"` -} - -// Matching represents an array of Route objects that assemble the trace -type Matching struct { - Route - Confidence float64 `json:"confidence"` - Geometry Geometry `json:"geometry"` -} - -func (r MatchRequest) request() *request { - options := matcherOptions( - stepsOptions(r.Steps, r.Annotations, r.Overview, r.Geometries), - r.Tidy, - r.Gaps, - ) - if len(r.Timestamps) > 0 { - options.addInt64("timestamps", r.Timestamps...) - } - if len(r.Radiuses) > 0 { - options.addFloat("radiuses", r.Radiuses...) - } - if len(r.Hints) > 0 { - options.add("hints", r.Hints...) - } - if len(r.Bearings) > 0 { - options.set("bearings", bearings(r.Bearings)) - } - - return &request{ - profile: r.Profile, - coords: r.Coordinates, - service: "match", - options: options, - } -} - -// Tracepoint represents a matched point on a route -type Tracepoint struct { - Index int `json:"waypoint_index"` - Location geo.Point `json:"location"` - MatchingIndex int `json:"matchings_index"` - AlternativesCount int `json:"alternatives_count"` - Hint string `json:"hint"` -} - -func matcherOptions(options options, tidy Tidy, gaps Gaps) options { - return options. - setStringer("tidy", tidy). - setStringer("gaps", gaps) -} diff --git a/match/match.go b/match/match.go new file mode 100644 index 0000000..f35e121 --- /dev/null +++ b/match/match.go @@ -0,0 +1,131 @@ +package match + +import ( + "github.com/openmarketplaceengine/go-osrm/route" + "github.com/openmarketplaceengine/go-osrm/types" + "github.com/paulmach/orb" + "github.com/paulmach/orb/geojson" +) + +// Request represents a request to the match method. +// See https://github.com/Project-OSRM/osrm-backend/blob/master/docs/http.md#match-service +type Request struct { + // Mode of transportation, is determined statically by the Lua profile that + // is used to prepare the data using osrm-extract. Typically, car, bike or + // foot if using one of the supplied profiles. + Profile string + + // String of format + // {longitude},{latitude};{longitude},{latitude}[;{longitude},{latitude} ...] + // or polyline + // ({polyline}) or polyline6({polyline6}) + Coordinates types.Coordinates + + // Limits the search to segments with given bearing in degrees towards true + // north in clockwise direction. + Bearings []types.Bearing + + // Returned route steps for each route + Steps types.Steps + + // Returns additional metadata for each coordinate along the route geometry. + Annotations types.Annotations + + // Tidy allows the input track modification to obtain better matching + // quality for noisy tracks. + Tidy types.Tidy + + // Timestamps for the input locations in seconds since UNIX epoch. + // Timestamps need to be monotonically increasing. + Timestamps []int64 + + // Standard deviation of GPS precision used for map matching. If applicable + // use GPS accuracy. + Radii []float64 + + // Hint from previous request to derive position in street network. + Hints []string + + // Add overview geometry either full, simplified according to highest zoom + // level it could be display on, or not at all. + Overview types.Overview + + // Allows the input track splitting based on huge timestamp gaps between + // points. + Gaps types.Gaps + + // Returned route geometry format (influences overview and per step) + Geometries types.Geometries +} + +// Response represents a response from the match method +type Response struct { + types.ResponseStatus + + // An array of Route objects that assemble the trace + Matchings []Matching `json:"matchings"` + + // Array of Waypoint objects representing all points of the trace in order. + // If the trace point was ommited by map matching because it is an outlier, + // the entry will be null. Each Waypoint object has the following additional + // properties: + Tracepoints []*Tracepoint `json:"tracepoints"` +} + +// Matching represents an array of Route objects that assemble the trace +type Matching struct { + route.Route + // Confidence of the matching. float value between 0 and 1. 1 is very + // confident that the matching is correct. + Confidence float64 `json:"confidence"` + Geometry geojson.LineString `json:"geometry"` +} + +func (r Request) Request() *types.Request { + options := matcherOptions( + route.StepsOptions(r.Steps, r.Annotations, r.Overview, r.Geometries), + r.Tidy, + r.Gaps, + ) + if len(r.Timestamps) > 0 { + options.AddInt64("timestamps", r.Timestamps...) + } + if len(r.Radii) > 0 { + options.AddFloat("radiuses", r.Radii...) + } + if len(r.Hints) > 0 { + options.Add("hints", r.Hints...) + } + if len(r.Bearings) > 0 { + options.Set("bearings", types.Bearings(r.Bearings)) + } + + return &types.Request{ + Profile: r.Profile, + Coords: r.Coordinates, + Service: "match", + Options: options, + } +} + +// Tracepoint represents a matched point on a route +type Tracepoint struct { + // Index of the waypoint inside the matched route. + Index int `json:"waypoint_index"` + Location orb.Point `json:"location"` + + //Index to the Route object in matchings the sub-trace was matched to. + MatchingIndex int `json:"matchings_index"` + + // Number of probable alternative matchings for this trace point. A value of + // zero indicate that this point was matched unambiguously. Split the trace + // at these points for incremental map matching. + AlternativesCount int `json:"alternatives_count"` + Hint string `json:"hint"` +} + +func matcherOptions(options types.Options, tidy types.Tidy, gaps types.Gaps) types.Options { + return options. + SetStringer("tidy", tidy). + SetStringer("gaps", gaps) +} diff --git a/match_test.go b/match/match_test.go similarity index 63% rename from match_test.go rename to match/match_test.go index 379bd72..5c6a5d8 100644 --- a/match_test.go +++ b/match/match_test.go @@ -1,6 +1,7 @@ -package osrm +package match import ( + "github.com/openmarketplaceengine/go-osrm/types" "testing" "github.com/stretchr/testify/assert" @@ -9,7 +10,7 @@ import ( func TestEmptyMatchRequestOptions(t *testing.T) { cases := []struct { name string - request MatchRequest + request Request expectedURI string }{ { @@ -18,33 +19,33 @@ func TestEmptyMatchRequestOptions(t *testing.T) { }, { name: "with timestamps and radiuses", - request: MatchRequest{ + request: Request{ Timestamps: []int64{0, 1, 2}, - Radiuses: []float64{0.123123, 0.12312}, + Radii: []float64{0.123123, 0.12312}, }, expectedURI: "geometries=polyline6&radiuses=0.123123;0.12312×tamps=0;1;2", }, { name: "with gaps and tidy", - request: MatchRequest{ + request: Request{ Timestamps: []int64{0, 1, 2}, - Radiuses: []float64{0.123123, 0.12312}, - Gaps: GapsSplit, - Tidy: TidyTrue, + Radii: []float64{0.123123, 0.12312}, + Gaps: types.GapsSplit, + Tidy: types.TidyTrue, }, expectedURI: "gaps=split&geometries=polyline6&radiuses=0.123123;0.12312&tidy=true×tamps=0;1;2", }, { name: "with hints", - request: MatchRequest{ + request: Request{ Hints: []string{"a", "b", "c", "d"}, }, expectedURI: "geometries=polyline6&hints=a;b;c;d", }, { name: "with bearings", - request: MatchRequest{ - Bearings: []Bearing{ + request: Request{ + Bearings: []types.Bearing{ {0, 20}, {10, 20}, }, }, @@ -52,19 +53,19 @@ func TestEmptyMatchRequestOptions(t *testing.T) { }, { name: "custom overview option", - request: MatchRequest{ - Overview: OverviewSimplified, - Geometries: GeometriesGeojson, - Annotations: AnnotationsFalse, - Tidy: TidyFalse, - Steps: StepsFalse, + request: Request{ + Overview: types.OverviewSimplified, + Geometries: types.GeometriesGeojson, + Annotations: types.AnnotationsFalse, + Tidy: types.TidyFalse, + Steps: types.StepsFalse, }, expectedURI: "annotations=false&geometries=geojson&overview=simplified&steps=false&tidy=false", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { - assert.Equal(t, c.expectedURI, c.request.request().options.encode()) + assert.Equal(t, c.expectedURI, c.request.Request().Options.Encode()) }) } } diff --git a/nearest.go b/nearest.go deleted file mode 100644 index 06172ba..0000000 --- a/nearest.go +++ /dev/null @@ -1,44 +0,0 @@ -package osrm - -import geo "github.com/paulmach/go.geo" - -// NearestRequest represents a request to the nearest method -type NearestRequest struct { - Profile string - Coordinates Geometry - Bearings []Bearing - Number int -} - -// NearestResponse represents a response from the nearest method -type NearestResponse struct { - ResponseStatus - Waypoints []NearestWaypoint `json:"waypoints"` -} - -// NearestWaypoint represents a nearest point on a nearest query -type NearestWaypoint struct { - Location geo.Point `json:"location"` - Distance float64 `json:"distance"` - Name string `json:"name"` - Hint string `json:"hint"` - Nodes []uint64 `json:"nodes"` -} - -func (r NearestRequest) request() *request { - opts := options{} - if r.Number > 0 { - opts.addInt("number", r.Number) - } - - if len(r.Bearings) > 0 { - opts.set("bearings", bearings(r.Bearings)) - } - - return &request{ - profile: r.Profile, - service: "nearest", - coords: r.Coordinates, - options: opts, - } -} diff --git a/nearest/nearest.go b/nearest/nearest.go new file mode 100644 index 0000000..0b30a4d --- /dev/null +++ b/nearest/nearest.go @@ -0,0 +1,64 @@ +package nearest + +import ( + "github.com/openmarketplaceengine/go-osrm/types" + "github.com/paulmach/orb" +) + +// Request represents a request to the nearest method. +// See https://github.com/Project-OSRM/osrm-backend/blob/master/docs/http.md#nearest-service +type Request struct { + // Mode of transportation, is determined statically by the Lua profile that + // is used to prepare the data using osrm-extract. Typically, car, bike or + // foot if using one of the supplied profiles. + Profile string + + // String of format + // {longitude},{latitude};{longitude},{latitude}[;{longitude},{latitude} ...] + // or polyline + // ({polyline}) or polyline6({polyline6}) + // Length should be 1. + Coordinates types.Coordinates + + // Limits the search to segments with given bearing in degrees towards true + // north in clockwise direction. + Bearings []types.Bearing + + // Number of nearest matches (segments) that should be returned. + // Defaults to 1. Should be 1 or greater. + Number int +} + +// Response represents a response from the nearest method +type Response struct { + types.ResponseStatus + Waypoints []Waypoint `json:"waypoints"` +} + +// Waypoint represents a nearest point on a nearest query +type Waypoint struct { + Location orb.Point `json:"location"` + Distance float64 `json:"distance"` + Name string `json:"name"` + Hint string `json:"hint"` + // Array of OpenStreetMap node ids. + Nodes []uint64 `json:"nodes"` +} + +func (r Request) Request() *types.Request { + opts := types.Options{} + if r.Number > 0 { + opts.AddInt("number", r.Number) + } + + if len(r.Bearings) > 0 { + opts.Set("bearings", types.Bearings(r.Bearings)) + } + + return &types.Request{ + Profile: r.Profile, + Service: "nearest", + Coords: r.Coordinates, + Options: opts, + } +} diff --git a/nearest_test.go b/nearest/nearest_test.go similarity index 54% rename from nearest_test.go rename to nearest/nearest_test.go index b8c4005..1981adc 100644 --- a/nearest_test.go +++ b/nearest/nearest_test.go @@ -1,30 +1,31 @@ -package osrm +package nearest import ( + "github.com/openmarketplaceengine/go-osrm/types" "testing" "github.com/stretchr/testify/assert" ) func TestNearestRequestOverviewOption(t *testing.T) { - req := NearestRequest{ + req := Request{ Number: 2, - Bearings: []Bearing{ + Bearings: []types.Bearing{ {60, 380}, }, } assert.Equal( t, "bearings=60%2C380&number=2", - req.request().options.encode()) + req.Request().Options.Encode()) - req = NearestRequest{ - Bearings: []Bearing{ + req = Request{ + Bearings: []types.Bearing{ {60, 380}, }, } assert.Equal( t, "bearings=60%2C380", - req.request().options.encode()) + req.Request().Options.Encode()) } diff --git a/options_test.go b/options_test.go deleted file mode 100644 index b92ef61..0000000 --- a/options_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package osrm - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestOptionBlank(t *testing.T) { - var opts options - assert.Equal(t, "", opts.encode()) -} - -func TestOptionSetVal(t *testing.T) { - opts := options{} - opts.set("foo", "bar") - assert.Equal(t, "foo=bar", opts.encode()) -} - -func TestOptionSetTwoKeys(t *testing.T) { - opts := options{} - opts.set("foo", "bar") - opts.set("baz", "quux") - assert.Equal(t, "baz=quux&foo=bar", opts.encode()) -} - -func TestOptionSetBool(t *testing.T) { - opts := options{} - opts.setBool("foo", true) - assert.Equal(t, "foo=true", opts.encode()) -} - -func TestOptionReplaceVal(t *testing.T) { - opts := options{} - opts.set("foo", "bar") - opts.set("foo", "baz") - assert.Equal(t, "foo=baz", opts.encode()) -} - -func TestOptionAddVal(t *testing.T) { - opts := options{} - opts.add("foo", "bar") - assert.Equal(t, "foo=bar", opts.encode()) -} - -func TestOptionAddTwoVals(t *testing.T) { - opts := options{} - opts.add("foo", "bar") - opts.add("foo", "baz") - assert.Equal(t, "foo=bar;baz", opts.encode()) -} - -func TestOptionAddTwoValsAsVariadic(t *testing.T) { - opts := options{} - opts.add("foo", "bar", "baz") - assert.Equal(t, "foo=bar;baz", opts.encode()) -} - -func TestOptionSetKeyAndAddTwoVals(t *testing.T) { - opts := options{} - opts.set("foo", "bar") - opts.add("baz", "quux") - opts.add("baz", "zuko") - assert.Equal(t, "baz=quux;zuko&foo=bar", opts.encode()) -} - -func TestOptionAddTwoValsAndSetKey(t *testing.T) { - opts := options{} - opts.add("foo", "bar") - opts.add("foo", "baz") - opts.set("quux", "zuko") - assert.Equal(t, "foo=bar;baz&quux=zuko", opts.encode()) -} - -func TestOptionAddIntVal(t *testing.T) { - opts := options{} - opts.addInt("foo", 1) - assert.Equal(t, "foo=1", opts.encode()) -} - -func TestOptionAddIntValsAsVariadic(t *testing.T) { - opts := options{} - opts.addInt("foo", 1, 2) - assert.Equal(t, "foo=1;2", opts.encode()) -} - -func TestOptionsAddInt64Val(t *testing.T) { - opts := options{} - opts.addInt64("foo", int64(1)) - assert.Equal(t, "foo=1", opts.encode()) -} - -func TestOptionsAddFloatVal(t *testing.T) { - opts := options{} - opts.addFloat("foo", 0.1231) - assert.Equal(t, "foo=0.1231", opts.encode()) -} - -func TestOptionsAddFloatValsAsVariadic(t *testing.T) { - opts := options{} - opts.addFloat("foo", 1.1231312, 2.1233) - assert.Equal(t, "foo=1.1231312;2.1233", opts.encode()) -} diff --git a/osrm.go b/osrm.go index 8a7ebeb..0aaa000 100644 --- a/osrm.go +++ b/osrm.go @@ -2,6 +2,11 @@ package osrm import ( "context" + "github.com/openmarketplaceengine/go-osrm/match" + "github.com/openmarketplaceengine/go-osrm/nearest" + "github.com/openmarketplaceengine/go-osrm/route" + "github.com/openmarketplaceengine/go-osrm/table" + "github.com/openmarketplaceengine/go-osrm/types" "net/http" "time" ) @@ -9,8 +14,6 @@ import ( const ( defaultTimeout = time.Second defaultServerURL = "http://127.0.0.1:5000" - - version = "v1" ) // OSRM implements the common OSRM API v5. @@ -30,33 +33,6 @@ type Config struct { Client HTTPClient } -// ResponseStatus represent OSRM API response -type ResponseStatus struct { - Code string `json:"code"` - Message string `json:"message"` - DataVersion string `json:"data_version"` -} - -// ErrCode returns error code from OSRM response -func (r ResponseStatus) ErrCode() string { - return r.Code -} - -func (r ResponseStatus) Error() string { - return r.Code + " - " + r.Message -} - -func (r ResponseStatus) apiError() error { - if r.Code != errorCodeOK { - return r - } - return nil -} - -type response interface { - apiError() error -} - // New creates a client with default server url and default timeout func New() *OSRM { return NewWithConfig(Config{}) @@ -87,18 +63,22 @@ func NewWithConfig(cfg Config) *OSRM { return &OSRM{client: newClient(cfg.ServerURL, cfg.Client)} } -func (o OSRM) query(ctx context.Context, in *request, out response) error { +type response interface { + ApiError() error +} + +func (o OSRM) query(ctx context.Context, in *types.Request, out response) error { if err := o.client.doRequest(ctx, in, out); err != nil { return err } - return out.apiError() + return out.ApiError() } // Route searches the shortest path between given coordinates. // See https://github.com/Project-OSRM/osrm-backend/blob/master/docs/http.md#route-service for details. -func (o OSRM) Route(ctx context.Context, r RouteRequest) (*RouteResponse, error) { - var resp RouteResponse - if err := o.query(ctx, r.request(), &resp); err != nil { +func (o OSRM) Route(ctx context.Context, r route.Request) (*route.Response, error) { + var resp route.Response + if err := o.query(ctx, r.Request(), &resp); err != nil { return nil, err } return &resp, nil @@ -106,9 +86,9 @@ func (o OSRM) Route(ctx context.Context, r RouteRequest) (*RouteResponse, error) // Table computes duration tables for the given locations. // See https://github.com/Project-OSRM/osrm-backend/blob/master/docs/http.md#table-service for details. -func (o OSRM) Table(ctx context.Context, r TableRequest) (*TableResponse, error) { - var resp TableResponse - if err := o.query(ctx, r.request(), &resp); err != nil { +func (o OSRM) Table(ctx context.Context, r table.Request) (*table.Response, error) { + var resp table.Response + if err := o.query(ctx, r.Request(), &resp); err != nil { return nil, err } return &resp, nil @@ -116,9 +96,9 @@ func (o OSRM) Table(ctx context.Context, r TableRequest) (*TableResponse, error) // Match matches given GPS points to the road network in the most plausible way. // See https://github.com/Project-OSRM/osrm-backend/blob/master/docs/http.md#match-service for details. -func (o OSRM) Match(ctx context.Context, r MatchRequest) (*MatchResponse, error) { - var resp MatchResponse - if err := o.query(ctx, r.request(), &resp); err != nil { +func (o OSRM) Match(ctx context.Context, r match.Request) (*match.Response, error) { + var resp match.Response + if err := o.query(ctx, r.Request(), &resp); err != nil { return nil, err } return &resp, nil @@ -126,9 +106,9 @@ func (o OSRM) Match(ctx context.Context, r MatchRequest) (*MatchResponse, error) // Nearest matches given GPS point to the nearest road network. // See https://github.com/Project-OSRM/osrm-backend/blob/master/docs/http.md#nearest-service for details. -func (o OSRM) Nearest(ctx context.Context, r NearestRequest) (*NearestResponse, error) { - var resp NearestResponse - if err := o.query(ctx, r.request(), &resp); err != nil { +func (o OSRM) Nearest(ctx context.Context, r nearest.Request) (*nearest.Response, error) { + var resp nearest.Response + if err := o.query(ctx, r.Request(), &resp); err != nil { return nil, err } return &resp, nil diff --git a/osrm_test.go b/osrm_test.go index 70f32a8..74d289f 100644 --- a/osrm_test.go +++ b/osrm_test.go @@ -3,6 +3,12 @@ package osrm import ( "context" "fmt" + "github.com/openmarketplaceengine/go-osrm/match" + "github.com/openmarketplaceengine/go-osrm/nearest" + "github.com/openmarketplaceengine/go-osrm/route" + "github.com/openmarketplaceengine/go-osrm/table" + "github.com/openmarketplaceengine/go-osrm/types" + "github.com/paulmach/orb/geojson" "io/ioutil" "log" "net/http" @@ -10,18 +16,15 @@ import ( "testing" "time" - geo "github.com/paulmach/go.geo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -var geometry = NewGeometryFromPointSet( - geo.PointSet{ - {-73.990185, 40.714701}, - {-73.991801, 40.717571}, - {-73.985751, 40.715651}, - }, -) +var geometry = geojson.LineString{ + {-73.990185, 40.714701}, + {-73.991801, 40.717571}, + {-73.985751, 40.715651}, +} func fixturedJSON(name string) []byte { data, err := ioutil.ReadFile("testdata/" + name + ".json") @@ -49,10 +52,10 @@ func TestErrorWithTimeout(t *testing.T) { var nothing response - req := request{ - service: "nothing", - profile: "nothing", - coords: geometry, + req := types.Request{ + Service: "nothing", + Profile: "nothing", + Coords: geometry, } err := osrm.query(context.Background(), &req, nothing) @@ -65,16 +68,16 @@ func TestErrorOnRequest(t *testing.T) { osrm := NewFromURL(ts.URL) - geom := NewGeometryFromPointSet(geo.PointSet{{0.1, 0.1}}) + geom := geojson.LineString{{0.1, 0.1}} assert := func(t *testing.T, err error) { t.Helper() require.EqualError(t, err, "InvalidQuery - Query string malformed close to position 28") - assert.Equal(t, ErrorCodeInvalidQuery, err.(ResponseStatus).ErrCode()) + assert.Equal(t, types.ErrorCodeInvalidQuery, err.(types.ResponseStatus).ErrCode()) } t.Run("route", func(t *testing.T) { - _, err := osrm.Route(context.Background(), RouteRequest{ + _, err := osrm.Route(context.Background(), route.Request{ Profile: "car", Coordinates: geom, }) @@ -83,7 +86,7 @@ func TestErrorOnRequest(t *testing.T) { }) t.Run("match", func(t *testing.T) { - _, err := osrm.Match(context.Background(), MatchRequest{ + _, err := osrm.Match(context.Background(), match.Request{ Profile: "car", Coordinates: geom, }) @@ -92,7 +95,7 @@ func TestErrorOnRequest(t *testing.T) { }) t.Run("table", func(t *testing.T) { - _, err := osrm.Table(context.Background(), TableRequest{ + _, err := osrm.Table(context.Background(), table.Request{ Profile: "car", Coordinates: geom, }) @@ -101,7 +104,7 @@ func TestErrorOnRequest(t *testing.T) { }) t.Run("nearest", func(t *testing.T) { - _, err := osrm.Nearest(context.Background(), NearestRequest{ + _, err := osrm.Nearest(context.Background(), nearest.Request{ Profile: "car", Coordinates: geom, }) @@ -119,13 +122,13 @@ func TestRouteRequest(t *testing.T) { osrm := NewFromURL(ts.URL) - r, err := osrm.Route(context.Background(), RouteRequest{ + r, err := osrm.Route(context.Background(), route.Request{ Profile: "car", Coordinates: geometry, - Annotations: AnnotationsTrue, - Geometries: GeometriesPolyline6, - Overview: OverviewFull, - ContinueStraight: ContinueStraightTrue, + Annotations: types.AnnotationsTrue, + Geometries: types.GeometriesPolyline6, + Overview: types.OverviewFull, + ContinueStraight: types.ContinueStraightTrue, }) require := require.New(t) @@ -157,12 +160,10 @@ func TestRouteRequest(t *testing.T) { require.Equal("", step0.Name) require.Equal(float32(5.0), step0.Duration) require.Equal(float32(33.1), step0.Distance) - require.Equal(Geometry{ - Path: *geo.NewPathFromXYSlice([][]float64{ - {-73.9902, 40.7147}, - {-73.99023, 40.7146}, - {-73.99025, 40.71441}, - }), + require.Equal(geojson.LineString{ + {-73.9902, 40.7147}, + {-73.99023, 40.7146}, + {-73.99025, 40.71441}, }, step0.Geometry) } @@ -175,7 +176,7 @@ func TestTableRequest(t *testing.T) { osrm := NewFromURL(ts.URL) - r, err := osrm.Table(context.Background(), TableRequest{Profile: "car", Coordinates: geometry}) + r, err := osrm.Table(context.Background(), table.Request{Profile: "car", Coordinates: geometry}) require := require.New(t) @@ -197,7 +198,7 @@ func TestMatchRequest(t *testing.T) { osrm := NewFromURL(ts.URL) - r, err := osrm.Match(context.Background(), MatchRequest{ + r, err := osrm.Match(context.Background(), match.Request{ Profile: "car", Coordinates: geometry, }) @@ -230,11 +231,11 @@ func TestNearestRequest(t *testing.T) { osrm := NewFromURL(ts.URL) - r, err := osrm.Nearest(context.Background(), NearestRequest{ + r, err := osrm.Nearest(context.Background(), nearest.Request{ Profile: "car", - Coordinates: NewGeometryFromPointSet(geo.PointSet{ + Coordinates: geojson.LineString{ {-73.994550, 40.735551}, - }), + }, Number: 5, }) diff --git a/route.go b/route.go deleted file mode 100644 index 0baa00b..0000000 --- a/route.go +++ /dev/null @@ -1,141 +0,0 @@ -package osrm - -import ( - "fmt" - "strconv" - - geo "github.com/paulmach/go.geo" -) - -// RouteRequest represents a request to the route method -type RouteRequest struct { - Profile string - Coordinates Geometry - Bearings []Bearing - Steps Steps - Annotations Annotations - Overview Overview - Geometries Geometries - ContinueStraight ContinueStraight - Waypoints []int -} - -// RouteResponse represents a response from the route method -type RouteResponse struct { - ResponseStatus - Routes []Route `json:"routes"` - Waypoints []Waypoint `json:"waypoints"` -} - -type Waypoint struct { - Name string `json:"name"` - Location geo.Point `json:"location"` - Distance float32 `json:"distance"` - Hint string `json:"hint"` -} - -// Route represents a route through (potentially multiple) points. -type Route struct { - Distance float32 `json:"distance"` - Duration float32 `json:"duration"` - WeightName string `json:"weight_name"` - Wieght float32 `json:"weight"` - Geometry Geometry `json:"geometry"` - Legs []RouteLeg `json:"legs"` -} - -// RouteLeg represents a route between two waypoints. -type RouteLeg struct { - Annotation Annotation `json:"annotation"` - Distance float32 `json:"distance"` - Duration float32 `json:"duration"` - Summary string `json:"summary"` - Weight float32 `json:"weight"` - Steps []RouteStep `json:"steps"` -} - -// Annotation contains additional metadata for each coordinate along the route geometry -type Annotation struct { - Duration []float32 `json:"duration,omitempty"` - Distance []float32 `json:"distance,omitempty"` - Nodes []uint64 `json:"nodes,omitempty"` -} - -// RouteStep represents a route geometry -type RouteStep struct { - Distance float32 `json:"distance"` - Duration float32 `json:"duration"` - Geometry Geometry `json:"geometry"` - Name string `json:"name"` - Mode string `json:"mode"` - DrivingSide string `json:"driving_side"` - Weight float32 `json:"weight"` - Maneuver StepManeuver `json:"maneuver"` - Intersections []Intersection `json:"intersections,omitempty"` -} - -type Intersection struct { - Location geo.Point `json:"location"` - Bearings []uint16 `json:"bearings"` - Entry []bool `json:"entry"` - In *uint32 `json:"in,omitempty"` - Out *uint32 `json:"out,omitempty"` - Lanes []Lane `json:"lanes,omitempty"` -} - -type Lane struct { - Indications []string `json:"indications"` - Valid bool `json:"valid"` -} - -// StepManeuver contains information about maneuver in step -type StepManeuver struct { - Location geo.Point `json:"location"` - BearingBefore float32 `json:"bearing_before"` - BearingAfter float32 `json:"bearing_after"` - Type string `json:"type"` - Modifier string `json:"modifier,omitempty"` - Exit *uint32 `json:"exit,omitempty"` -} - -func (r RouteRequest) request() *request { - opts := stepsOptions(r.Steps, r.Annotations, r.Overview, r.Geometries). - setStringer("continue_straight", r.ContinueStraight) - - if len(r.Waypoints) > 0 { - waypoints := "" - for i, w := range r.Waypoints { - if i > 0 { - waypoints += ";" - } - waypoints += strconv.Itoa(w) - } - opts.set("waypoints", waypoints) - } - - if len(r.Bearings) > 0 { - opts.set("bearings", bearings(r.Bearings)) - } - - return &request{ - profile: r.Profile, - coords: r.Coordinates, - service: "route", - options: opts, - } -} - -func stepsOptions(steps Steps, annotations Annotations, overview Overview, geometries Geometries) options { - return options{}. - setStringer("steps", steps). - setStringer("annotations", annotations). - setStringer("geometries", valueOrDefault(geometries, GeometriesPolyline6)). - setStringer("overview", overview) -} - -func valueOrDefault(value, def fmt.Stringer) fmt.Stringer { - if value.String() == "" { - return def - } - return value -} diff --git a/route/route.go b/route/route.go new file mode 100644 index 0000000..256cd23 --- /dev/null +++ b/route/route.go @@ -0,0 +1,157 @@ +package route + +import ( + "fmt" + "github.com/openmarketplaceengine/go-osrm/types" + "github.com/paulmach/orb/geojson" + "strconv" + + "github.com/paulmach/orb" +) + +// Request represents a request to the Route method. +// The Route service finds the fastest route between coordinates in the supplied +// order. +type Request struct { + Profile string + Coordinates types.Coordinates + Bearings []types.Bearing + + // Returned route steps for each route leg + Steps types.Steps + + // Returns additional metadata for each coordinate along the route geometry. + Annotations types.Annotations + + // Add overview geometry either full, simplified according to highest zoom + // level it could be display on, or not at all. + Overview types.Overview + + // Returned route geometry format (influences overview and per step) + Geometries types.Geometries + + // Forces the route to keep going straight at waypoints constraining uturns + // there even if it would be faster. Default value depends on the profile. + ContinueStraight types.ContinueStraight + Waypoints []int +} + +// Response represents a response from the route method +type Response struct { + types.ResponseStatus + Routes []Route `json:"routes"` + Waypoints []Waypoint `json:"waypoints"` +} + +type Waypoint struct { + Name string `json:"name"` + Location orb.Point `json:"location"` + Distance float32 `json:"distance"` + Hint string `json:"hint"` +} + +// Route represents a route through (potentially multiple) points. +type Route struct { + Distance float32 `json:"distance"` + Duration float32 `json:"duration"` + WeightName string `json:"weight_name"` + Weight float32 `json:"weight"` + Geometry geojson.LineString `json:"geometry"` + Legs []Leg `json:"legs"` +} + +// Leg represents a route between two waypoints. +type Leg struct { + Annotation Annotation `json:"annotation"` + Distance float32 `json:"distance"` + Duration float32 `json:"duration"` + Summary string `json:"summary"` + Weight float32 `json:"weight"` + Steps []Step `json:"steps"` +} + +// Annotation contains additional metadata for each coordinate along the route geometry +type Annotation struct { + Duration []float32 `json:"duration,omitempty"` + Distance []float32 `json:"distance,omitempty"` + Nodes []uint64 `json:"nodes,omitempty"` +} + +// Step represents a route geometry +type Step struct { + Distance float32 `json:"distance"` + Duration float32 `json:"duration"` + Geometry geojson.LineString `json:"geometry"` + Name string `json:"name"` + Mode string `json:"mode"` + DrivingSide string `json:"driving_side"` + Weight float32 `json:"weight"` + Maneuver StepManeuver `json:"maneuver"` + Intersections []Intersection `json:"intersections,omitempty"` +} + +type Intersection struct { + Location orb.Point `json:"location"` + Bearings []uint16 `json:"bearings"` + Entry []bool `json:"entry"` + In *uint32 `json:"in,omitempty"` + Out *uint32 `json:"out,omitempty"` + Lanes []Lane `json:"lanes,omitempty"` +} + +type Lane struct { + Indications []string `json:"indications"` + Valid bool `json:"valid"` +} + +// StepManeuver contains information about maneuver in step +type StepManeuver struct { + Location orb.Point `json:"location"` + BearingBefore float32 `json:"bearing_before"` + BearingAfter float32 `json:"bearing_after"` + Type string `json:"type"` + Modifier string `json:"modifier,omitempty"` + Exit *uint32 `json:"exit,omitempty"` +} + +func (r Request) Request() *types.Request { + opts := StepsOptions(r.Steps, r.Annotations, r.Overview, r.Geometries). + SetStringer("continue_straight", r.ContinueStraight) + + if len(r.Waypoints) > 0 { + waypoints := "" + for i, w := range r.Waypoints { + if i > 0 { + waypoints += ";" + } + waypoints += strconv.Itoa(w) + } + opts.Set("waypoints", waypoints) + } + + if len(r.Bearings) > 0 { + opts.Set("bearings", types.Bearings(r.Bearings)) + } + + return &types.Request{ + Profile: r.Profile, + Coords: r.Coordinates, + Service: "route", + Options: opts, + } +} + +func StepsOptions(steps types.Steps, annotations types.Annotations, overview types.Overview, geometries types.Geometries) types.Options { + return types.Options{}. + SetStringer("steps", steps). + SetStringer("annotations", annotations). + SetStringer("geometries", valueOrDefault(geometries, types.GeometriesPolyline6)). + SetStringer("overview", overview) +} + +func valueOrDefault(value, def fmt.Stringer) fmt.Stringer { + if value.String() == "" { + return def + } + return value +} diff --git a/route_test.go b/route/route_test.go similarity index 51% rename from route_test.go rename to route/route_test.go index 49a4d59..8188fcf 100644 --- a/route_test.go +++ b/route/route_test.go @@ -1,53 +1,54 @@ -package osrm +package route import ( + "github.com/openmarketplaceengine/go-osrm/types" "testing" "github.com/stretchr/testify/assert" ) func TestEmptyRouteRequestOptions(t *testing.T) { - req := RouteRequest{} + req := Request{} assert.Equal( t, "geometries=polyline6", - req.request().options.encode()) + req.Request().Options.Encode()) } func TestRouteRequestOptionsWithBearings(t *testing.T) { - req := RouteRequest{ - Bearings: []Bearing{ + req := Request{ + Bearings: []types.Bearing{ {60, 380}, {45, 180}, }, - ContinueStraight: ContinueStraightTrue, + ContinueStraight: types.ContinueStraightTrue, } assert.Equal( t, "bearings=60%2C380%3B45%2C180&continue_straight=true&geometries=polyline6", - req.request().options.encode()) + req.Request().Options.Encode()) } func TestRouteRequestOverviewOption(t *testing.T) { - req := RouteRequest{ - Overview: OverviewFull, - ContinueStraight: ContinueStraightTrue, + req := Request{ + Overview: types.OverviewFull, + ContinueStraight: types.ContinueStraightTrue, } assert.Equal( t, "continue_straight=true&geometries=polyline6&overview=full", - req.request().options.encode()) + req.Request().Options.Encode()) } func TestRouteRequestGeometryOption(t *testing.T) { - req := RouteRequest{ - Geometries: GeometriesPolyline6, - Annotations: AnnotationsFalse, - Steps: StepsFalse, - ContinueStraight: ContinueStraightTrue, + req := Request{ + Geometries: types.GeometriesPolyline6, + Annotations: types.AnnotationsFalse, + Steps: types.StepsFalse, + ContinueStraight: types.ContinueStraightTrue, } assert.Equal( t, "annotations=false&continue_straight=true&geometries=polyline6&steps=false", - req.request().options.encode()) + req.Request().Options.Encode()) } diff --git a/table.go b/table.go deleted file mode 100644 index a539131..0000000 --- a/table.go +++ /dev/null @@ -1,31 +0,0 @@ -package osrm - -// TableRequest represents a request to the table method -type TableRequest struct { - Profile string - Coordinates Geometry - Sources, Destinations []int -} - -// TableResponse resresents a response from the table method -type TableResponse struct { - ResponseStatus - Durations [][]float32 `json:"durations"` -} - -func (r TableRequest) request() *request { - opts := options{} - if len(r.Sources) > 0 { - opts.addInt("sources", r.Sources...) - } - if len(r.Destinations) > 0 { - opts.addInt("destinations", r.Destinations...) - } - - return &request{ - profile: r.Profile, - coords: r.Coordinates, - service: "table", - options: opts, - } -} diff --git a/table/table.go b/table/table.go new file mode 100644 index 0000000..b27c340 --- /dev/null +++ b/table/table.go @@ -0,0 +1,41 @@ +package table + +import ( + "github.com/openmarketplaceengine/go-osrm/types" +) + +// Request represents a request to the Table method. +// The Table method computes the duration of the fastest route between all pairs +// of supplied coordinates. Returns the durations or distances or both between +// the coordinate pairs. Note that the distances are not the shortest distance +// between two coordinates, but rather the distances of the fastest routes. +// Duration is in seconds and distance is in meters. +// See https://github.com/Project-OSRM/osrm-backend/blob/master/docs/http.md#table-service +type Request struct { + Profile string + Coordinates types.Coordinates + Sources, Destinations []int +} + +// Response represents a response from the table method +type Response struct { + types.ResponseStatus + Durations [][]float32 `json:"durations"` +} + +func (r Request) Request() *types.Request { + opts := types.Options{} + if len(r.Sources) > 0 { + opts.AddInt("sources", r.Sources...) + } + if len(r.Destinations) > 0 { + opts.AddInt("destinations", r.Destinations...) + } + + return &types.Request{ + Profile: r.Profile, + Coords: r.Coordinates, + Service: "table", + Options: opts, + } +} diff --git a/table_test.go b/table/table_test.go similarity index 55% rename from table_test.go rename to table/table_test.go index cf8a55f..a857595 100644 --- a/table_test.go +++ b/table/table_test.go @@ -1,4 +1,4 @@ -package osrm +package table import ( "testing" @@ -7,14 +7,14 @@ import ( ) func TestEmptyTableRequestOptions(t *testing.T) { - req := TableRequest{} - assert.Empty(t, req.request().options.encode()) + req := Request{} + assert.Empty(t, req.Request().Options.Encode()) } func TestNotEmptyTableRequestOptions(t *testing.T) { - req := TableRequest{ + req := Request{ Sources: []int{0, 1, 2}, Destinations: []int{1, 3}, } - assert.Equal(t, "destinations=1;3&sources=0;1;2", req.request().options.encode()) + assert.Equal(t, "destinations=1;3&sources=0;1;2", req.Request().Options.Encode()) } diff --git a/types/coordinates.go b/types/coordinates.go new file mode 100644 index 0000000..b97e030 --- /dev/null +++ b/types/coordinates.go @@ -0,0 +1,76 @@ +package types + +import ( + "encoding/json" + "fmt" + "github.com/paulmach/orb" + "github.com/paulmach/orb/geojson" + "github.com/twpayne/go-polyline" +) + +type Coordinates struct { + geojson.MultiPoint +} + +func (c Coordinates) MarshalJSON() ([]byte, error) { + return c.toPolyline(), nil +} + +func (c *Coordinates) UnmarshalJSON(data []byte) error { + if len(data) == 0 { + return nil + } + + // Is it a string? + var encoded string + if err := json.Unmarshal(data, &encoded); err == nil { + codec := polyline.Codec{Dim: 2, Scale: 1e6} + coords, _, err := codec.DecodeCoords([]byte(encoded)) + if err != nil { + return err + } + c.MultiPoint = coordsToMultiPoint(coords) + return nil + } + + geom, err := geojson.UnmarshalGeometry(data) + if err != nil { + return fmt.Errorf("failed to unmarshal geojson geometry, err: %v", err) + } + if geom.Type != "LineString" { + return fmt.Errorf("unexpected geometry type: %v", geom.Type) + } + + var mp geojson.MultiPoint + for _, p := range geom.Coordinates.(orb.LineString) { + mp = append(mp, orb.Point([2]float64{p.X(), p.Y()})) + } + c.MultiPoint = mp + + return nil +} + +func (c Coordinates) toPolyline(in ...float64) []byte { + scale := 1e6 + if len(in) > 0 { + scale = in[0] + } + var coords [][]float64 + for _, p := range c.MultiPoint { + coords = append(coords, []float64{p.X(), p.Y()}) + } + codec := polyline.Codec{Dim: 2, Scale: scale} + return codec.EncodeCoords(nil, coords) +} + +func coordinatesFromLineString(ls geojson.LineString) Coordinates { + return Coordinates{geojson.MultiPoint(ls)} +} + +func coordsToMultiPoint(coords [][]float64) geojson.MultiPoint { + var mp geojson.MultiPoint + for _, p := range coords { + mp = append(mp, orb.Point([2]float64{p[1], p[0]})) + } + return mp +} diff --git a/types/coordinates_test.go b/types/coordinates_test.go new file mode 100644 index 0000000..e970b0c --- /dev/null +++ b/types/coordinates_test.go @@ -0,0 +1,103 @@ +package types + +import ( + "encoding/json" + "fmt" + "github.com/paulmach/orb" + "github.com/paulmach/orb/geojson" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func Test_Coordinates_toPolyline(t *testing.T) { + c := coordinatesFromLineString(geojson.LineString{ + {-73.990185, 40.714701}, + }) + b := c.toPolyline() + s := "\"" + string(b) + "\"" + assert.Equal(t, `"pa_clCy{_tlA"`, s) + var c2 Coordinates + err := json.Unmarshal([]byte(s), &c2) + require.NoError(t, err) + b, err = json.Marshal(c2.MultiPoint) + require.NoError(t, err) + fmt.Println(string(b)) +} + +func TestUnmarshal(t *testing.T) { + tests := map[string]struct { + b []byte + expect func(t *testing.T, c *Coordinates, err error) + }{ + "From GeoJSON": { + b: []byte(`{"type": "LineString", "coordinates": [[-73.982253,40.742926],[-73.985253,40.742926]]}`), + expect: func(t *testing.T, c *Coordinates, err error) { + require.NoError(t, err) + p := c.MultiPoint + require.Len(t, p, 2) + require.Equal(t, orb.Point{-73.982253, 40.742926}, p[0]) + require.Equal(t, orb.Point{-73.985253, 40.742926}, p[1]) + }, + }, + "From Polyline": { + b: []byte(`"nvnalCui}okAkgpk@u}hQf}_l@mbpL"`), + expect: func(t *testing.T, c *Coordinates, err error) { + require.NoError(t, err) + p := c.MultiPoint + require.Len(t, p, 3) + require.Equal(t, orb.Point{40.123563, -73.965432}, p[0]) + require.Equal(t, orb.Point{40.423574, -73.235698}, p[1]) + require.Equal(t, orb.Point{40.645325, -73.973462}, p[2]) + }, + }, + "Null": { + b: []byte(`null`), + expect: func(t *testing.T, c *Coordinates, err error) { + require.NoError(t, err) + require.Equal(t, 0, len(c.MultiPoint)) + }, + }, + "Empty": { + b: []byte(`{}`), + expect: func(t *testing.T, c *Coordinates, err error) { + require.Error(t, err) + require.EqualError(t, err, `failed to unmarshal geojson geometry, err: geojson: invalid geometry`) + }, + }, + } + for testName, tc := range tests { + t.Run(testName, func(t *testing.T) { + var c Coordinates + err := json.Unmarshal(tc.b, &c) + tc.expect(t, &c, err) + }) + } +} + +func TestMarshal(t *testing.T) { + tests := map[string]struct { + coordinates Coordinates + expect func(t *testing.T, b []byte, err error) + }{ + "OK": { + coordinates: coordinatesFromLineString( + geojson.LineString{ + {40.123563, -73.965432}, + {40.423574, -73.235698}, + {40.645325, -73.973462}, + }, + ), + expect: func(t *testing.T, b []byte, err error) { + require.NoError(t, err) + assert.Equal(t, `"nvnalCui}okAkgpk@u}hQf}_l@mbpL"`, string(b)) + }, + }, + } + for testName, tc := range tests { + t.Run(testName, func(t *testing.T) { + b, err := json.Marshal(tc.coordinates) + tc.expect(t, b, err) + }) + } +} diff --git a/errors.go b/types/errors.go similarity index 98% rename from errors.go rename to types/errors.go index b725126..a5da8c8 100644 --- a/errors.go +++ b/types/errors.go @@ -1,4 +1,4 @@ -package osrm +package types import "errors" diff --git a/options.go b/types/options.go similarity index 68% rename from options.go rename to types/options.go index 73726f4..23be701 100644 --- a/options.go +++ b/types/options.go @@ -1,4 +1,4 @@ -package osrm +package types import ( "fmt" @@ -7,28 +7,28 @@ import ( "strconv" ) -// options represents OSRM query params to be encoded in URL -type options map[string][]string +// Options represents OSRM query params to be encoded in URL +type Options map[string][]string // Set saves a string value by the key -func (opts options) set(k, v string) options { +func (opts Options) Set(k, v string) Options { if v != "" { opts[k] = []string{v} } return opts } -func (opts options) setStringer(k string, v fmt.Stringer) options { - return opts.set(k, v.String()) +func (opts Options) SetStringer(k string, v fmt.Stringer) Options { + return opts.Set(k, v.String()) } // SetBool converts bool to string and set a key -func (opts options) setBool(k string, v bool) options { - return opts.set(k, fmt.Sprintf("%t", v)) +func (opts Options) SetBool(k string, v bool) Options { + return opts.Set(k, fmt.Sprintf("%t", v)) } // AddInt converts int to string and appends it to the key -func (opts options) addInt(k string, v ...int) options { +func (opts Options) AddInt(k string, v ...int) Options { for _, n := range v { opts[k] = append(opts[k], strconv.Itoa(n)) } @@ -36,7 +36,7 @@ func (opts options) addInt(k string, v ...int) options { } // AddInt64 converts int64 to string and appends it to the key -func (opts options) addInt64(k string, v ...int64) options { +func (opts Options) AddInt64(k string, v ...int64) Options { for _, n := range v { opts[k] = append(opts[k], strconv.FormatInt(n, 10)) } @@ -44,7 +44,7 @@ func (opts options) addInt64(k string, v ...int64) options { } // AddFloat converts float to string and appends it to the key -func (opts options) addFloat(k string, v ...float64) options { +func (opts Options) AddFloat(k string, v ...float64) Options { for _, f := range v { opts[k] = append(opts[k], strconv.FormatFloat(f, 'f', -1, 64)) } @@ -52,14 +52,14 @@ func (opts options) addFloat(k string, v ...float64) options { } // Add appends values to the key -func (opts options) add(k string, v ...string) options { +func (opts Options) Add(k string, v ...string) Options { opts[k] = append(opts[k], v...) return opts } // Encode encodes the options into OSRM query form // ({option}={element};{element}[;{element} ... ]) sorted by key -func (opts options) encode() string { +func (opts Options) Encode() string { if opts == nil { return "" } diff --git a/types/options_test.go b/types/options_test.go new file mode 100644 index 0000000..e0074b1 --- /dev/null +++ b/types/options_test.go @@ -0,0 +1,103 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOptionBlank(t *testing.T) { + var opts Options + assert.Equal(t, "", opts.Encode()) +} + +func TestOptionSetVal(t *testing.T) { + opts := Options{} + opts.Set("foo", "bar") + assert.Equal(t, "foo=bar", opts.Encode()) +} + +func TestOptionSetTwoKeys(t *testing.T) { + opts := Options{} + opts.Set("foo", "bar") + opts.Set("baz", "quux") + assert.Equal(t, "baz=quux&foo=bar", opts.Encode()) +} + +func TestOptionSetBool(t *testing.T) { + opts := Options{} + opts.SetBool("foo", true) + assert.Equal(t, "foo=true", opts.Encode()) +} + +func TestOptionReplaceVal(t *testing.T) { + opts := Options{} + opts.Set("foo", "bar") + opts.Set("foo", "baz") + assert.Equal(t, "foo=baz", opts.Encode()) +} + +func TestOptionAddVal(t *testing.T) { + opts := Options{} + opts.Add("foo", "bar") + assert.Equal(t, "foo=bar", opts.Encode()) +} + +func TestOptionAddTwoVals(t *testing.T) { + opts := Options{} + opts.Add("foo", "bar") + opts.Add("foo", "baz") + assert.Equal(t, "foo=bar;baz", opts.Encode()) +} + +func TestOptionAddTwoValsAsVariadic(t *testing.T) { + opts := Options{} + opts.Add("foo", "bar", "baz") + assert.Equal(t, "foo=bar;baz", opts.Encode()) +} + +func TestOptionSetKeyAndAddTwoVals(t *testing.T) { + opts := Options{} + opts.Set("foo", "bar") + opts.Add("baz", "quux") + opts.Add("baz", "zuko") + assert.Equal(t, "baz=quux;zuko&foo=bar", opts.Encode()) +} + +func TestOptionAddTwoValsAndSetKey(t *testing.T) { + opts := Options{} + opts.Add("foo", "bar") + opts.Add("foo", "baz") + opts.Set("quux", "zuko") + assert.Equal(t, "foo=bar;baz&quux=zuko", opts.Encode()) +} + +func TestOptionAddIntVal(t *testing.T) { + opts := Options{} + opts.AddInt("foo", 1) + assert.Equal(t, "foo=1", opts.Encode()) +} + +func TestOptionAddIntValsAsVariadic(t *testing.T) { + opts := Options{} + opts.AddInt("foo", 1, 2) + assert.Equal(t, "foo=1;2", opts.Encode()) +} + +func TestOptionsAddInt64Val(t *testing.T) { + opts := Options{} + opts.AddInt64("foo", int64(1)) + assert.Equal(t, "foo=1", opts.Encode()) +} + +func TestOptionsAddFloatVal(t *testing.T) { + opts := Options{} + opts.AddFloat("foo", 0.1231) + assert.Equal(t, "foo=0.1231", opts.Encode()) +} + +func TestOptionsAddFloatValsAsVariadic(t *testing.T) { + opts := Options{} + opts.AddFloat("foo", 1.1231312, 2.1233) + assert.Equal(t, "foo=1.1231312;2.1233", opts.Encode()) +} diff --git a/types/response_status.go b/types/response_status.go new file mode 100644 index 0000000..297378f --- /dev/null +++ b/types/response_status.go @@ -0,0 +1,24 @@ +package types + +// ResponseStatus represent OSRM API response +type ResponseStatus struct { + Code string `json:"code"` + Message string `json:"message"` + DataVersion string `json:"data_version"` +} + +// ErrCode returns error code from OSRM response +func (r ResponseStatus) ErrCode() string { + return r.Code +} + +func (r ResponseStatus) Error() string { + return r.Code + " - " + r.Message +} + +func (r ResponseStatus) ApiError() error { + if r.Code != errorCodeOK { + return r + } + return nil +} diff --git a/types.go b/types/types.go similarity index 59% rename from types.go rename to types/types.go index 8f5a559..0e026b6 100644 --- a/types.go +++ b/types/types.go @@ -1,73 +1,11 @@ -package osrm +package types import ( - "encoding/json" "fmt" "net/url" "strings" - - geo "github.com/paulmach/go.geo" - geojson "github.com/paulmach/go.geojson" -) - -const ( - polyline5Factor = 1.0e5 - polyline6Factor = 1.0e6 ) -// Geometry represents a points set -type Geometry struct { - geo.Path -} - -// NewGeometryFromPath creates a geometry from a path. -func NewGeometryFromPath(path geo.Path) Geometry { - return Geometry{path} -} - -// NewGeometryFromPointSet creates a geometry from points set. -func NewGeometryFromPointSet(ps geo.PointSet) Geometry { - return NewGeometryFromPath(geo.Path{PointSet: ps}) -} - -// Polyline generates a polyline in Google format -func (g *Geometry) Polyline(factor ...int) string { - if len(factor) == 0 { - return g.Encode(polyline5Factor) - } - - return g.Encode(factor[0]) -} - -// UnmarshalJSON parses a geo path from points set or a polyline -func (g *Geometry) UnmarshalJSON(b []byte) error { - if len(b) == 0 { - return nil - } - - var encoded string - if err := json.Unmarshal(b, &encoded); err == nil { - g.Path = *geo.NewPathFromEncoding(encoded, polyline6Factor) - return nil - } - - geom, err := geojson.UnmarshalGeometry(b) - if err != nil { - return fmt.Errorf("failed to unmarshal geojson geometry, err: %v", err) - } - if !geom.IsLineString() { - return fmt.Errorf("unexpected geometry type: %v", geom.Type) - } - g.Path = *geo.NewPathFromXYSlice(geom.LineString) - - return nil -} - -// MarshalJSON generates a polyline in Google polyline6 format -func (g Geometry) MarshalJSON() ([]byte, error) { - return json.Marshal(g.Polyline(polyline6Factor)) -} - // Tidy represents a tidy param for osrm5 match request type Tidy string @@ -116,7 +54,8 @@ func (s Steps) String() string { return string(s) } -// Gaps represents a gaps param for osrm5 match request +// Gaps represents a gaps param for osrm5 match request. +// Allows the input track splitting based on huge timestamp gaps between points. type Gaps string // Supported gaps param values @@ -174,35 +113,35 @@ func (c ContinueStraight) String() string { return string(c) } -// request contains parameters for OSRM query -type request struct { - profile string - coords Geometry - service string - options options +// Request contains parameters for OSRM query +type Request struct { + Profile string + Coords Coordinates + Service string + Options Options } // URL generates a url for OSRM request -func (r *request) URL(serverURL string) (string, error) { - if r.service == "" { +func (r *Request) URL(serverURL string) (string, error) { + if r.Service == "" { return "", ErrEmptyServiceName } - if r.profile == "" { + if r.Profile == "" { return "", ErrEmptyProfileName } - if r.coords.Length() == 0 { + if len(r.Coords.MultiPoint) == 0 { return "", ErrNoCoordinates } // http://{server}/{service}/{version}/{profile}/{coordinates}[.{format}]?option=value&option=value url := strings.Join([]string{ serverURL, // server - r.service, // service - version, // version - r.profile, // profile - "polyline(" + url.PathEscape(r.coords.Polyline(polyline5Factor)) + ")", // coordinates + r.Service, // service + "v1", // version + r.Profile, // profile + "polyline(" + url.PathEscape(string(r.Coords.toPolyline())) + ")", // coordinates }, "/") - if len(r.options) > 0 { - url += "?" + r.options.encode() // options + if len(r.Options) > 0 { + url += "?" + r.Options.Encode() // options } return url, nil } @@ -216,7 +155,7 @@ func (b Bearing) String() string { return fmt.Sprintf("%d,%d", b.Value, b.Range) } -func bearings(br []Bearing) string { +func Bearings(br []Bearing) string { s := make([]string, len(br)) for i, b := range br { s[i] = b.String() diff --git a/types/types_test.go b/types/types_test.go new file mode 100644 index 0000000..0329ac7 --- /dev/null +++ b/types/types_test.go @@ -0,0 +1,94 @@ +package types + +import ( + "github.com/paulmach/orb/geojson" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var geometry = geojson.LineString{ + {-73.990185, 40.714701}, + {-73.991801, 40.717571}, + {-73.985751, 40.715651}, +} + +func TestBuildURL(t *testing.T) { + tests := map[string]struct { + buildRequest func() Request + expect func(t *testing.T, actual string, err error) + }{ + "Empty Options": { + buildRequest: func() Request { + return Request{ + Profile: "something", + Coords: coordinatesFromLineString(geometry), + Service: "foobar", + } + }, + expect: func(t *testing.T, actual string, err error) { + require.NoError(t, err) + assert.Equal(t, "localhost/foobar/v1/something/polyline(%7BaowFrerbM%7DPbI~Jyd@)", actual) + }, + }, + "With Options": { + buildRequest: func() Request { + opts := Options{} + opts.Set("baz", "quux") + return Request{ + Profile: "something", + Coords: coordinatesFromLineString(geometry), + Service: "foobar", + Options: opts, + } + }, + expect: func(t *testing.T, actual string, err error) { + require.NoError(t, err) + assert.Equal(t, "localhost/foobar/v1/something/polyline(%7BaowFrerbM%7DPbI~Jyd@)?baz=quux", actual) + }, + }, + "With Empty Service": { + buildRequest: func() Request { + return Request{} + }, + expect: func(t *testing.T, actual string, err error) { + require.Error(t, err) + assert.Equal(t, ErrEmptyServiceName, err) + assert.Empty(t, actual) + }, + }, + "With Empty Profile": { + buildRequest: func() Request { + return Request{ + Service: "foobar", + } + }, + expect: func(t *testing.T, actual string, err error) { + require.Error(t, err) + assert.Equal(t, ErrEmptyProfileName, err) + assert.Empty(t, actual) + }, + }, + "Without Coords": { + buildRequest: func() Request { + return Request{ + Profile: "something", + Service: "foobar", + } + }, + expect: func(t *testing.T, actual string, err error) { + require.Error(t, err) + assert.Equal(t, ErrNoCoordinates, err) + assert.Empty(t, actual) + }, + }, + } + for testName, tc := range tests { + t.Run(testName, func(t *testing.T) { + req := tc.buildRequest() + url, err := req.URL("localhost") + tc.expect(t, url, err) + }) + } +} diff --git a/types_test.go b/types_test.go deleted file mode 100644 index 05010cc..0000000 --- a/types_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package osrm - -import ( - "encoding/json" - "testing" - - "github.com/paulmach/go.geo" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestUnmarshalGeometryFromGeojson(t *testing.T) { - var g Geometry - in := []byte(`{"type": "LineString", "coordinates": [[-73.982253,40.742926],[-73.985253,40.742926]]}`) - - err := json.Unmarshal(in, &g) - - require.Nil(t, err) - require.Len(t, g.PointSet, 2) - require.Equal(t, *geo.NewPoint(-73.982253, 40.742926), g.PointSet[0]) - require.Equal(t, *geo.NewPoint(-73.985253, 40.742926), g.PointSet[1]) -} - -func TestUnmarshalGeometryFromPolyline(t *testing.T) { - var g Geometry - in := []byte(`"nvnalCui}okAkgpk@u}hQf}_l@mbpL"`) - - err := json.Unmarshal(in, &g) - - require.Nil(t, err) - require.Len(t, g.PointSet, 3) - require.Equal(t, *geo.NewPoint(40.123563, -73.965432), g.PointSet[0]) - require.Equal(t, *geo.NewPoint(40.423574, -73.235698), g.PointSet[1]) - require.Equal(t, *geo.NewPoint(40.645325, -73.973462), g.PointSet[2]) -} - -func TestUnmarshalGeometryFromNull(t *testing.T) { - var g Geometry - in := []byte(`null`) - err := json.Unmarshal(in, &g) - - require.Nil(t, err) - require.Equal(t, 0, len(g.Path.PointSet)) -} - -func TestUnmarshalGeometryFromEmptyJSON(t *testing.T) { - var g Geometry - in := []byte(`{}`) - err := json.Unmarshal(in, &g) - - require.Error(t, err) -} - -func TestPolylineGeometry(t *testing.T) { - g := Geometry{ - Path: geo.Path{ - PointSet: []geo.Point{ - {40.123563, -73.965432}, - {40.423574, -73.235698}, - {40.645325, -73.973462}, - }, - }, - } - - bytes, err := json.Marshal(g) - require.NoError(t, err) - - assert.Equal(t, `"nvnalCui}okAkgpk@u}hQf}_l@mbpL"`, string(bytes)) -} - -func TestRequestURLWithEmptyOptions(t *testing.T) { - req := request{ - profile: "something", - coords: geometry, - service: "foobar", - } - url, err := req.URL("localhost") - require.Nil(t, err) - assert.Equal(t, "localhost/foobar/v1/something/polyline(%7BaowFrerbM%7DPbI~Jyd@)", url) -} - -func TestRequestURLWithOptions(t *testing.T) { - opts := options{} - opts.set("baz", "quux") - req := request{ - profile: "something", - coords: geometry, - service: "foobar", - options: opts, - } - url, err := req.URL("localhost") - require.Nil(t, err) - assert.Equal(t, "localhost/foobar/v1/something/polyline(%7BaowFrerbM%7DPbI~Jyd@)?baz=quux", url) -} - -func TestRequestURLWithEmptyService(t *testing.T) { - req := request{} - url, err := req.URL("localhost") - require.NotNil(t, err) - assert.Equal(t, ErrEmptyServiceName, err) - assert.Empty(t, url) -} - -func TestRequestURLWithEmptyProfile(t *testing.T) { - req := request{ - service: "foobar", - } - url, err := req.URL("localhost") - require.NotNil(t, err) - assert.Equal(t, ErrEmptyProfileName, err) - assert.Empty(t, url) -} - -func TestRequestURLWithoutCoords(t *testing.T) { - req := request{ - profile: "something", - service: "foobar", - } - url, err := req.URL("localhost") - require.NotNil(t, err) - assert.Equal(t, ErrNoCoordinates, err) - assert.Empty(t, url) -}