Skip to content

Commit

Permalink
feat(routing): add support for spans
Browse files Browse the repository at this point in the history
To get more information about one section, such as
name, speed limit, stret attributes etc

https://www.here.com/docs/bundle/routing-api-developer-guide-v8/page/topics/span.html
  • Loading branch information
thall committed Dec 20, 2023
1 parent d9921fe commit 4f3241f
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 2 deletions.
22 changes: 22 additions & 0 deletions routingv8/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
48 changes: 48 additions & 0 deletions routingv8/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package routingv8

import (
"encoding/json"
"fmt"
)

type ErrorCodes []ErrorCode
Expand Down Expand Up @@ -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 {
Expand Down
79 changes: 79 additions & 0 deletions routingv8/response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
21 changes: 20 additions & 1 deletion routingv8/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package routingv8

import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
54 changes: 53 additions & 1 deletion routingv8/routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ func TestRoutingervice_Routes_QueryParams(t *testing.T) {
name string
request *routingv8.RoutesRequest
expected string
errStr string
}{
{
name: "minimal",
Expand Down Expand Up @@ -164,14 +165,65 @@ 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) {
t.Parallel()
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)
})
}
Expand Down
64 changes: 64 additions & 0 deletions routingv8/testdata/route-with-spans.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
]
}
]
}

0 comments on commit 4f3241f

Please sign in to comment.