trips/router/internal/route/valhalla.go
Greg Pomerantz efde2cc71b maps project: design, survey, mock app, and route-aware planning backend
- DESIGN.md: full design incl. driving-trip requirements (R1-R4),
  stays model, focus mode, mobile, provenance rules
- SURVEY.md: open-source landscape
- mock/: interaction mock (Florence itinerary, focus mode, stays,
  region stops, mobile layout)
- router/: Go module (stdlib-only) with Router interface
  (Valhalla + OSRM backends), stop_cost, optimize_stops, corridor,
  routectl CLI, bench (5 real NE-corridor tasks, 26 checks passing),
  integration tests, and setup-osrm.sh for the self-hosted router
- osm/: NH+MA+CT+NY PBFs (gitignored) + setup artifacts
2026-09-06 00:05:17 -04:00

184 lines
4.2 KiB
Go

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
}