package route import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" "maps/router/internal/geo" ) // Valhalla is a client for a Valhalla routing API // (e.g. the hosted https://valhalla1.openstreetmap.de or a self-hosted // valhalla instance). Note: hosted instances strip route geometry, so // Route.Geometry will be nil. type Valhalla struct { BaseURL string HTTP *http.Client name string } func NewValhalla(baseURL string) *Valhalla { return &Valhalla{ BaseURL: baseURL, HTTP: &http.Client{Timeout: 60 * time.Second}, name: "valhalla(" + baseURL + ")", } } func (v *Valhalla) Name() string { return v.name } func (v *Valhalla) costing(p Profile) string { if p == ProfileWalk { return "pedestrian" } return "auto" } type vLocation struct { Lat float64 `json:"lat"` Lon float64 `json:"lon"` } type vRouteReq struct { Locations []vLocation `json:"locations"` Costing string `json:"costing"` Polyline bool `json:"polyline"` } type vLeg struct { Summary struct { Time float64 `json:"time"` Length float64 `json:"length"` // km } `json:"summary"` } type vTrip struct { Summary struct { Time float64 `json:"time"` Length float64 `json:"length"` // km MinLat float64 `json:"min_lat"` MaxLat float64 `json:"max_lat"` MinLon float64 `json:"min_lon"` MaxLon float64 `json:"max_lon"` } `json:"summary"` Legs []vLeg `json:"legs"` } type vResp struct { Trip vTrip `json:"trip"` Message string `json:"message"` Error string `json:"error"` } func (v *Valhalla) Route(ctx context.Context, profile Profile, pts []geo.Point) (*Route, error) { if len(pts) < 2 { return nil, fmt.Errorf("route: need >= 2 points, got %d", len(pts)) } req := vRouteReq{Costing: v.costing(profile), Polyline: true} for _, p := range pts { req.Locations = append(req.Locations, vLocation{p.Lat, p.Lon}) } body, _ := json.Marshal(req) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, v.BaseURL+"/route", bytes.NewReader(body)) if err != nil { return nil, err } httpReq.Header.Set("Content-Type", "application/json") resp, err := v.HTTP.Do(httpReq) if err != nil { return nil, err } defer resp.Body.Close() raw, err := io.ReadAll(io.LimitReader(resp.Body, 20<<20)) if err != nil { return nil, err } var vr vResp if err := json.Unmarshal(raw, &vr); err != nil { return nil, fmt.Errorf("valhalla: bad response (%s): %q", resp.Status, string(raw[:min(len(raw), 200)])) } if vr.Message != "" { return nil, fmt.Errorf("valhalla: %s", vr.Message) } if vr.Error != "" { return nil, fmt.Errorf("valhalla: %s", vr.Error) } out := &Route{ Duration: vr.Trip.Summary.Time, Distance: vr.Trip.Summary.Length * 1000, BBox: BBox{ MinLat: vr.Trip.Summary.MinLat, MaxLat: vr.Trip.Summary.MaxLat, MinLon: vr.Trip.Summary.MinLon, MaxLon: vr.Trip.Summary.MaxLon, }, } for _, l := range vr.Trip.Legs { out.Legs = append(out.Legs, Leg{Duration: l.Summary.Time, Distance: l.Summary.Length * 1000}) } // Geometry: decode encoded polyline if the instance returns it. if pl, ok := decodePolylineField(raw); ok { out.Geometry = pl } return out, nil } // decodePolylineField checks for trip.polyline (encoded polyline v0) — // hosted instances omit it; self-hosted valhalla with "polyline":true // returns it. func decodePolylineField(raw []byte) ([]geo.Point, bool) { var probe struct { Trip struct { Polyline string `json:"polyline"` } `json:"trip"` } if err := json.Unmarshal(raw, &probe); err != nil || probe.Trip.Polyline == "" { return nil, false } return decodePolyline(probe.Trip.Polyline), true } // decodePolyline decodes a Google encoded polyline (precision 1e-5 or 1e-6). func decodePolyline(s string) []geo.Point { var pts []geo.Point lat, lon := 0, 0 i := 0 for i < len(s) { var dlat, dlon int shift := 0 for { b := int(s[i]) - 63 i++ dlat |= (b & 0x1f) << shift if b&0x20 == 0 { break } shift += 5 } shift = 0 for { b := int(s[i]) - 63 i++ dlon |= (b & 0x1f) << shift if b&0x20 == 0 { break } shift += 5 } lat += dlat&1 ^ dlat>>1 lon += dlon&1 ^ dlon>>1 pts = append(pts, geo.Point{Lat: float64(lat) / 1e5, Lon: float64(lon) / 1e5}) } return pts } func min(a, b int) int { if a < b { return a } return b }