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}