Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(routing): add support for spans #164

Merged
merged 2 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love the type safety here 😄

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
}
40 changes: 39 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,51 @@ func TestRoutingervice_Routes_QueryParams(t *testing.T) {
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 in 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
18 changes: 0 additions & 18 deletions routingv8/testdata/route-restrictions.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,24 +124,6 @@
"baseDuration": 1490
},
"polyline": "BG6m_phDgnu3gBif3zBgKvR0K3S4NjXwHvM0FrJoGzK8G7LsJ_O0KvR8Q3c0FrJ0jBj6BsYnpBwR3c8a_sBsJzPoVzjBgUvgBgFjIsE7GoGzKwHjNgKjS0F_J8G7LsJnQ8Q3coG_JoQ7a4N7VwMzUwMnVkN7Vs7B_jDoQ7akI3NkIrO0KjSwHvMsEvHgFjIwHjNkN7VoG_JsEvHoGzK8GzKoG_J0F3I0F3IsEnGsEnGsEnGsEzFoGjIwMnQsEnGgF7GoGjIkIzKoLrO0FjI0FjI4I3NoG_J8GnLoLvRoG_J0F3IoGrJsJvMwRvWozB79B0PrTsJnL0tBn4BsOjSkNzP4SvWoVnawWvb8kB_sBkNzP8VnasE_EgPjSkI_J4IzK8G3I8GrJ8G_JgFvHgFjIgFjI4IrO0KvRkI3N0KvRkNvWsY_nBwM7VwHjI4DrEgFnGkIzKwH_J4D_E4D3DkDvCkDAkDAwCnB8BnBoBnB8B7BwC3DoBvCoB3DUjDU_EAjDTzF8BnG8B_EwCzFwHrOgFzKsErJsEnL4DzK4DnGoG_J4DnGwWvlB0FrJkN7V8L_TgFjI8G7LsJzPsEvHsEvHoajrB08B7lDsEvHsJzPsEvHoiC7vDwHvM4X_nBsO_YoQnaoLrTsEvH4IrOkcvvBwR3c4IrOsTjhBoGzKwWjmBkIrO4IrOgK7Q0FrJ0FrJoGzKwHvM4DnGoV3hBoGrJ0FjIoG3I0FvH8G3IsY_d8G3IwHrJwHrJ8QnV0KjNoGvH8G3I0F7GsJnLsOvRgKvM4IzKoGvHoGvHwHrJ8LrO8GjIoGvHwH3IoGvH4DrE4DrE0FnG0FnGoGnGgFzFsJzK4IrJgKzKgKzKoL7LwHvH4IrJwHjI0KnLsJ_JsJrJoL7LgK_J8GvHgF_EgF_E8GvHkI3IgK_JkN3N4IrJwMvM4IrJsE_EgKzK8LvMwMjN4S3S4IrJ8GvH4IrJsJrJ8LvMkSrTgK_JoGnG8GvH0FzFwHjI0UnVgenfsJ_JoG7G0FzFgF_E0FnG0KnL4IrJ8G7GwHvHgZna8G7GoG7GoG7GwHjI4I3IosC3uCgevgBgUzU8LvMsnBnpB0P7QoQ7QgP_O4uC3zCssBztBofjhBsYzZ8L7LoL7L0KnLoL7LsJ_J4IrJwHvHkhBriBkS3S8VjX0KnL4IrJ8G7G4I3IkI3I4IrJ8QvRwgBriB4I3IwW3X8VjXgK_JoL7LwMjNsJ_J8LvM8LvMwMvMsOrO4NvMkN7L0P3N4XnV0FzFgF_EsE_EgFnGgF7GwjCz6CoV3ckInLsJvM0Uvb8ankBsJ7L4I_J4IzKwH_J4I7LsJjN4N3S8QvW4I7LwHnL4IjNgKzPgerxBoLjSgK7QwHjNsEjIoGzK8LrT8V7kBsEvHgF3I4I_OgK7QgKnQ8Q7akN7VgPzZgPrY4DnGsEvHgFjIsEvHsEvH4DvHgF7L4InVsE_JgFrJwHjNoG_JwHzK8LnQ8GrJkDjDwCvCwCjDwCjDgK_OsEnG0ZnkB0KzPoQrYoajmBwWzewWnfsE7GkDrEgUrd8V7fwRrdgK7Q0KzPgKrOwHzK4I7LoGjI8GrJ4InLwH_JwHzK8LjS4X_iBkInLoG3I8G_JoQjXgFjI4D7G4DvHsE_JgF7GsEzFsJvMoG3IgPnVoLnQ4DzFsEnGkN3SkNjS0Z7kB4IjNwHnL4DzFsOzUsJjNsEnGgFvH8GrJ4I7L4IvMsEnGoG3IgU3cgF7GkI7LkI7LwHzKgF7GgFnG4DrEsE_EgF_E0F_EoG_E8G_EoLjI4NzKwM_JkIvH4DjD4DvC4D7B0F7BsJ7B8LvCoQjDsJ7B0P3DkI7B8G7BwHvC0FvC0FjD8GrEoG_EoGzFoG7G8GjI8VzZkI3IwHvHgF_E0FzFkIjI8V7V0FzF4D3DwMvMkIjI0FzF8anawbjcsOzPsE_EoL3NoL3NoL_OkInL0U3coG3IwHzKoG3IwR_YkInLkIzK8L_OkNzP4IzKgK7LwHjI4DrEkIT0FAsEAgFAoGUsEU0FoBoG8B4D8BsEwCwHgF4IgFsE8B4InpBoLnkBkDzFrEzFrEzFzK3NokBjzCwHvRgK3XkI_T8G3IoGjN4I3XsE_OkDjSwHjrBkIr2BwCrOsEzjBoBvMgF_O8BvH8B3I4D3X4Dna4DjhBkD3I8B_EwCrE4D3DkDjDsJA8GnB8GvCkInD",
"spans": [
{
"offset": 0
},
{
"offset": 507,
"notices": [
0
]
},
{
"offset": 527,
"notices": [
0,
1
]
}
],
"notices": [
{
"title": "Violated vehicle restriction.",
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"
}
}
]
}
]
}