response.go
1package valhalla
2
3import (
4 "encoding/json"
5 "fmt"
6 "log/slog"
7 "net/http"
8 "time"
9
10 "git.kilimanjaro.io/rtw/geo"
11 geom "github.com/paulmach/orb/geo"
12)
13
14// Response is the return type for the Stadia v1 routing API
15type Response struct {
16 Trip struct {
17 Locations json.RawMessage
18 Legs []struct {
19 Summary Summary `json:"summary"`
20 Shape geo.Polyline6 `json:"shape"`
21 } `json:"legs"`
22 Summary Summary `json:"summary"`
23 StatusMessage string `json:"status_message"`
24 Status int `json:"status"`
25 Units string `json:"units"`
26 Language string `json:"language"`
27 } `json:"trip"`
28}
29
30// Summary is a repeated field with statistics for the overall route and each leg
31type Summary struct {
32 HasTimeRestrictions bool `json:"has_time_restrictions,omitempty"`
33 HasToll bool `json:"has_toll"`
34 HasHighway bool `json:"has_highway"`
35 HasFerry bool `json:"has_ferry"`
36 MinLat float64 `json:"min_lat"`
37 MinLon float64 `json:"min_lon"`
38 MaxLat float64 `json:"max_lat"`
39 MaxLon float64 `json:"max_lon"`
40 Time float64 `json:"time"`
41 Length float64 `json:"length"`
42 Cost float64 `json:"cost"`
43}
44
45// Route constructs a request to the Stadia API and returns the response. Default timeout
46// is 30 seconds.
47func Route(pts []Location, opts ...RequestOption) (*Response, error) {
48 resp, err := doRoute(pts, opts...)
49 if err != nil {
50 return nil, err
51 }
52 legs := resp.Trip.Legs
53 if len(legs) == 0 {
54 return nil, fmt.Errorf("routing error: no route")
55 }
56 line, err := geo.Decode(legs[len(legs)-1].Shape)
57 if err != nil {
58 return nil, fmt.Errorf("routing error: req: could not decode polyline: %w", err)
59 }
60 intendedDest := [2]float64{pts[len(pts)-1].Lon, pts[len(pts)-1].Lat}
61 arrivedDest := [2]float64{line[len(line)-1][0], line[len(line)-1][1]}
62 diffDist := geom.Distance(intendedDest, arrivedDest)
63 slog.Info("difference between intended and arrived destination", "distance", diffDist)
64 if diffDist > 5000 {
65 slog.Error("routing failed, retrying with conservative parameters")
66 return doRoute(pts, append(opts, WithConservativeDefaults())...)
67 }
68 return resp, nil
69}
70
71func doRoute(pts []Location, opts ...RequestOption) (*Response, error) {
72 //req, err := NewRequest(pts, opts...)
73 req, err := NewLocalRequest(pts, opts...)
74 if err != nil {
75 return nil, fmt.Errorf("routing error: req: %w", err)
76 }
77 client := http.Client{Timeout: 30 * time.Second}
78 resp, err := client.Do(req)
79 if err != nil {
80 return nil, fmt.Errorf("routing error: send: %w", err)
81 }
82 defer resp.Body.Close()
83 var out = new(Response)
84 if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
85 return nil, fmt.Errorf("routing error: decode: %w", err)
86 }
87 return out, nil
88}
89
90// RouteWithBackoff retries the routing request up to four times, waiting a total of 45 seconds between
91// tries.
92func RouteWithBackoff(pts []Location, opts ...RequestOption) (*Response, error) {
93 sleepTimes := []time.Duration{0 * time.Second, 5 * time.Second, 10 * time.Second, 30 * time.Second}
94 var lastErr error
95 for _, s := range sleepTimes {
96 time.Sleep(s)
97 resp, err := Route(pts, opts...)
98 if err == nil {
99 return resp, nil
100 }
101 lastErr = err
102 }
103 return nil, fmt.Errorf("routing error: no response in %d tries: %w", len(sleepTimes), lastErr)
104}