diff --git a/routingv8/request.go b/routingv8/request.go index 393da31..a8409c7 100644 --- a/routingv8/request.go +++ b/routingv8/request.go @@ -58,6 +58,8 @@ type RoutesRequest struct { // If not specified the current time is used. // To not take time into account use DepartureTimeAny. DepartureTime string + // Spans define which content attributes that are included in the response spans + Spans []SpanAttribute } type ReturnAttribute string @@ -483,3 +485,23 @@ func (t *AreaFeature) MarshalJSON() ([]byte, error) { buffer.WriteString(`"`) return buffer.Bytes(), nil } + +type SpanAttribute string + +// For available span attributes to implementation see: +// https://www.here.com/docs/bundle/routing-api-v8-api-reference/page/index.html#tag/Routing/operation/calculateRoutes +const ( + SpanAttributeNames SpanAttribute = "names" + SpanAttributeMaxSpeed SpanAttribute = "maxSpeed" +) + +func (t *SpanAttribute) String() string { + switch *t { + case SpanAttributeNames: + return string(SpanAttributeNames) + case SpanAttributeMaxSpeed: + return string(SpanAttributeMaxSpeed) + default: + return invalid + } +} diff --git a/routingv8/response.go b/routingv8/response.go index 374f07c..62e55be 100644 --- a/routingv8/response.go +++ b/routingv8/response.go @@ -2,6 +2,7 @@ package routingv8 import ( "encoding/json" + "fmt" ) type ErrorCodes []ErrorCode @@ -146,6 +147,53 @@ type Section struct { Polyline Polyline `json:"polyline"` // Contains a list of issues related to this section of the route. Notices []VehicleNotice `json:"notices"` + // Spans attached to a `Section` describing vehicle content. + Spans []Span `json:"spans"` +} + +type Span struct { + // Length of the span. + Length int `json:"length"` + // Names of the span. + Names []Name `json:"names"` + // Speed in meters per second, or "unlimited" indicating that the speed is unlimited, e.g., on a German autobahn + MaxSpeed MaxSpeedEither `json:"maxSpeed"` + // Spans attached to a Section describing vehicle content. + Offset int `json:"offset"` +} + +// MaxSpeedEither holds either a speed or unlimited is true if speed is unlimited. +// MaxSpeed and Unlimited are mutually exclusive. +type MaxSpeedEither struct { + // MaxSpeed of the span. + MaxSpeed float32 + // Unlimited is true if unlimited speed. + Unlimited bool +} + +func (m *MaxSpeedEither) UnmarshalJSON(b []byte) error { + if b[0] == '"' { + // Value is a string + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + expected := "unlimited" + if s != expected { + return fmt.Errorf("expected value '%s' to be '%s'", s, expected) + } + m.Unlimited = true + return nil + } + return json.Unmarshal(b, &m.MaxSpeed) +} + +// Name for the span, e.g., a street name or a transport name. +type Name struct { + // Language in BCP47 format. + Language string `json:"language"` + // Value written in the language specified in the Language property. + Value string `json:"value"` } type VehicleDeparture struct { diff --git a/routingv8/response_test.go b/routingv8/response_test.go index 49206e9..f616bae 100644 --- a/routingv8/response_test.go +++ b/routingv8/response_test.go @@ -206,6 +206,85 @@ func TestUnmarshalRoute(t *testing.T) { rawMessageEqual(), ) }) + t.Run("route-with-spans.json", func(t *testing.T) { + t.Parallel() + resp := unmarshalRouteResponseFromFile(t, "route-with-spans.json") + assert.DeepEqual( + t, + resp, + RoutesResponse{ + Routes: []Route{ + { + ID: "eacbd184-24ff-43d2-84e7-08642957cb0e", + Sections: []Section{ + { + ID: "bfcb3fb9-2487-4155-9eb0-602789698fa3", + Type: "vehicle", + Arrival: VehicleDeparture{ + Place: Place{ + Type: "place", + Location: GeoWaypoint{ + Lat: 54.1528099, + Long: 10.8527, + }, + OriginalLocation: GeoWaypoint{ + Lat: 54.1528244, + Long: 10.8526714, + }, + }, + }, + Departure: VehicleDeparture{ + Place: Place{ + Type: "place", + Location: GeoWaypoint{ + Lat: 54.2213551, + Long: 10.901825, + }, + OriginalLocation: GeoWaypoint{ + Lat: 54.2213531, + Long: 10.9017949, + }, + }, + }, + Summary: Summary{ + Duration: 258, + Length: 8615, + BaseDuration: 258, + }, + Polyline: "BG2ittnDi0s5UxBK_J8Bze8G_Y4D3SwC_EU7LUz8BkD7kBnBjrB7B7LUvHA3IAnajD_lC_J3" + + "wBjIzwCzP3rB_J3pCjSjcvHnqDnanlDrYvH7BjI7BvCTvnFvqBjmB3I7sC3Sv5Lj9C3vEnkBnwF3rB" + + "3hB3Iv0BvMz1CjXzyBrOjhBzKrgC7Vr2BjXvgBjNnpBjS7fzP7kBrTvb_O_OjI_djSztBvbjc_TrsB" + + "_dj6BjrBjcnV3c3X7pB_iBz3B_xB3NjNjhB7fjwB3wBvvBzyB_EzFvH3I_sBnzBnzBn9B7kB7uBjwB" + + "zhC_7B_0CjX_iBj6Bn7Cr7BngDr0CzsE7jEn9GvrDz0FzjB_7B7VjhBnanuBjwB_vCvvBrvC36B_jD" + + "rYrnB3uCz9D3NvWnLvR_djwBvlBn4BnkBv0Bz1C75DjhBrsBzjBztBrOjSnQ_T", + Spans: []Span{ + { + Offset: 0, + MaxSpeed: MaxSpeedEither{ + Unlimited: true, + }, + Names: []Name{ + { + Language: "de", + Value: "Möllner Straße", + }, + }, + }, + { + Offset: 80, + MaxSpeed: MaxSpeedEither{ + MaxSpeed: 33.3333359, + }, + }, + }, + }, + }, + }, + }, + }, + rawMessageEqual(), + ) + }) } func unmarshalRouteResponseFromFile(t *testing.T, filename string) RoutesResponse { diff --git a/routingv8/routes.go b/routingv8/routes.go index 4f5f2b5..10bcd0a 100644 --- a/routingv8/routes.go +++ b/routingv8/routes.go @@ -2,6 +2,7 @@ package routingv8 import ( "context" + "errors" "fmt" "net/http" "net/url" @@ -41,7 +42,16 @@ func (s *RoutingService) Routes( values.Add("transportMode", tm) values.Add("origin", fmt.Sprintf("%v,%v", req.Origin.Lat, req.Origin.Long)) values.Add("destination", fmt.Sprintf("%v,%v", req.Destination.Lat, req.Destination.Long)) - + if len(req.Spans) > 0 { + if !returnContains(req.Return, PolylineReturnAttribute) { + return nil, errors.New("spans parameter also requires that the polyline option is set in the return parameter") + } + spanStrings := make([]string, 0, len(req.Spans)) + for _, span := range req.Spans { + spanStrings = append(spanStrings, string(span)) + } + values.Add("spans", strings.Join(spanStrings, ",")) + } if req.AvoidAreas != nil { areas := make([]string, 0, len(req.AvoidAreas)) for _, area := range req.AvoidAreas { @@ -66,3 +76,12 @@ func (s *RoutingService) Routes( } return &resp, nil } + +func returnContains(requested []ReturnAttribute, needle ReturnAttribute) bool { + for _, attr := range requested { + if attr == needle { + return true + } + } + return false +} diff --git a/routingv8/routes_test.go b/routingv8/routes_test.go index 9697a8e..faf3cf7 100644 --- a/routingv8/routes_test.go +++ b/routingv8/routes_test.go @@ -123,6 +123,7 @@ func TestRoutingervice_Routes_QueryParams(t *testing.T) { name string request *routingv8.RoutesRequest expected string + errStr string }{ { name: "minimal", @@ -164,6 +165,54 @@ func TestRoutingervice_Routes_QueryParams(t *testing.T) { expected: "destination=59.337492%2C18.063672&origin=57.707752%2C11.949767" + "&return=summary%2Cpolyline&transportMode=car", }, + { + name: "multiple return attributes", + request: &routingv8.RoutesRequest{ + Origin: origin, + Destination: destination, + TransportMode: routingv8.TransportModeCar, + Return: []routingv8.ReturnAttribute{ + routingv8.SummaryReturnAttribute, + routingv8.PolylineReturnAttribute, + }, + }, + expected: "destination=59.337492%2C18.063672&origin=57.707752%2C11.949767" + + "&return=summary%2Cpolyline&transportMode=car", + }, + { + name: "with spans", + request: &routingv8.RoutesRequest{ + Origin: origin, + Destination: destination, + TransportMode: routingv8.TransportModeCar, + Return: []routingv8.ReturnAttribute{ + routingv8.SummaryReturnAttribute, + routingv8.PolylineReturnAttribute, + }, + Spans: []routingv8.SpanAttribute{ + routingv8.SpanAttributeNames, + routingv8.SpanAttributeMaxSpeed, + }, + }, + expected: "destination=59.337492%2C18.063672&origin=57.707752%2C11.949767" + + "&return=summary%2Cpolyline&spans=names%2CmaxSpeed&transportMode=car", + }, + { + name: "with spans without wanted polyline returned", + request: &routingv8.RoutesRequest{ + Origin: origin, + Destination: destination, + TransportMode: routingv8.TransportModeCar, + Return: []routingv8.ReturnAttribute{ + routingv8.SummaryReturnAttribute, + }, + Spans: []routingv8.SpanAttribute{ + routingv8.SpanAttributeNames, + routingv8.SpanAttributeMaxSpeed, + }, + }, + errStr: "spans parameter also requires that the polyline option is set within the return parameter", + }, } { tt := tt t.Run(tt.name, func(t *testing.T) { @@ -171,7 +220,10 @@ func TestRoutingervice_Routes_QueryParams(t *testing.T) { client := RoutesMock{} routingClient := routingv8.NewClient(&client) - _, _ = routingClient.Routing.Routes(ctx, tt.request) + _, err := routingClient.Routing.Routes(ctx, tt.request) + if tt.errStr != "" { + assert.ErrorContains(t, err, tt.errStr) + } assert.Equal(t, client.requestRawQuery, tt.expected) }) } diff --git a/routingv8/testdata/route-with-spans.json b/routingv8/testdata/route-with-spans.json new file mode 100644 index 0000000..c5ca719 --- /dev/null +++ b/routingv8/testdata/route-with-spans.json @@ -0,0 +1,64 @@ +{ + "routes": [ + { + "id": "eacbd184-24ff-43d2-84e7-08642957cb0e", + "sections": [ + { + "id": "bfcb3fb9-2487-4155-9eb0-602789698fa3", + "type": "vehicle", + "departure": { + "place": { + "type": "place", + "location": { + "lat": 54.2213551, + "lng": 10.901825 + }, + "originalLocation": { + "lat": 54.2213531, + "lng": 10.9017949 + } + } + }, + "arrival": { + "place": { + "type": "place", + "location": { + "lat": 54.1528099, + "lng": 10.8527 + }, + "originalLocation": { + "lat": 54.1528244, + "lng": 10.8526714 + } + } + }, + "summary": { + "duration": 258, + "length": 8615, + "baseDuration": 258 + }, + "polyline": "BG2ittnDi0s5UxBK_J8Bze8G_Y4D3SwC_EU7LUz8BkD7kBnBjrB7B7LUvHA3IAnajD_lC_J3wBjIzwCzP3rB_J3pCjSjcvHnqDnanlDrYvH7BjI7BvCTvnFvqBjmB3I7sC3Sv5Lj9C3vEnkBnwF3rB3hB3Iv0BvMz1CjXzyBrOjhBzKrgC7Vr2BjXvgBjNnpBjS7fzP7kBrTvb_O_OjI_djSztBvbjc_TrsB_dj6BjrBjcnV3c3X7pB_iBz3B_xB3NjNjhB7fjwB3wBvvBzyB_EzFvH3I_sBnzBnzBn9B7kB7uBjwBzhC_7B_0CjX_iBj6Bn7Cr7BngDr0CzsE7jEn9GvrDz0FzjB_7B7VjhBnanuBjwB_vCvvBrvC36B_jDrYrnB3uCz9D3NvWnLvR_djwBvlBn4BnkBv0Bz1C75DjhBrsBzjBztBrOjSnQ_T", + "spans": [ + { + "offset": 0, + "maxSpeed": "unlimited", + "names": [ + { + "value": "Möllner Straße", + "language": "de" + } + ] + }, + { + "offset": 80, + "maxSpeed": 33.3333359 + } + ], + "transport": { + "mode": "car" + } + } + ] + } + ] +}