trips/router/internal/route/osrm.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

137 lines
3.5 KiB
Go

package route
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"maps/router/internal/geo"
)
// OSRM is a client for a local OSRM backend (osrm-routed), e.g.
// http://localhost:5000. Returns full route geometry.
type OSRM struct {
BaseURL string
HTTP *http.Client
profile string
name string
}
func NewOSRM(baseURL string) *OSRM {
return &OSRM{
BaseURL: baseURL,
HTTP: &http.Client{Timeout: 60 * time.Second},
profile: "driving",
name: "osrm(" + baseURL + ")",
}
}
func (o *OSRM) Name() string { return o.name }
func (o *OSRM) osrmProfile(p Profile) string {
if p == ProfileWalk {
return "walking"
}
return "driving"
}
type osrmRoute struct {
Code string `json:"code"`
Distance float64 `json:"distance"`
Duration float64 `json:"duration"`
Legs []struct {
Distance float64 `json:"distance"`
Duration float64 `json:"duration"`
} `json:"legs"`
Geometry string `json:"geometry"`
}
func (o *OSRM) Route(ctx context.Context, profile Profile, pts []geo.Point) (*Route, error) {
if len(pts) < 2 {
return nil, fmt.Errorf("osrm: need >= 2 points, got %d", len(pts))
}
var coords []string
for _, p := range pts {
coords = append(coords, fmt.Sprintf("%.6f,%.6f", p.Lon, p.Lat))
}
u := fmt.Sprintf("%s/route/v1/%s/%s?overview=false&alternatives=false&geometries=geojson",
o.BaseURL, o.osrmProfile(profile), strings.Join(coords, ";"))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
return nil, err
}
resp, err := o.HTTP.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var or osrmRoute
if err := json.NewDecoder(resp.Body).Decode(&or); err != nil {
return nil, fmt.Errorf("osrm: bad response: %w", err)
}
if or.Code != "Ok" {
return nil, fmt.Errorf("osrm: %s", or.Code)
}
out := &Route{Duration: or.Duration, Distance: or.Distance}
for _, l := range or.Legs {
out.Legs = append(out.Legs, Leg{Duration: l.Duration, Distance: l.Distance})
}
// OSRM geometries=geojson returns a GeoJSON LineString.
var gj struct {
Geometry struct {
Coords [][]float64 `json:"coordinates"`
} `json:"geometry"`
}
if or.Geometry != "" && json.Unmarshal([]byte(or.Geometry), &gj) == nil {
for _, c := range gj.Geometry.Coords {
out.Geometry = append(out.Geometry, geo.Point{Lat: c[1], Lon: c[0]})
}
}
return out, nil
}
// OSRMTable builds a matrix via the OSRM table service (one call) when
// available; the generic BuildMatrix (pairwise) works on any backend.
func (o *OSRM) Table(ctx context.Context, profile Profile, pts []geo.Point) (Matrix, error) {
var coords []string
for _, p := range pts {
coords = append(coords, fmt.Sprintf("%.6f,%.6f", p.Lon, p.Lat))
}
u := fmt.Sprintf("%s/table/v1/%s/%s?annotations=duration",
o.BaseURL, o.osrmProfile(profile), strings.Join(coords, ";"))
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
resp, err := o.HTTP.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var tr struct {
Code string `json:"code"`
Durations [][]float64 `json:"durations"`
}
if err := json.NewDecoder(resp.Body).Decode(&tr); err != nil {
return nil, err
}
if tr.Code != "Ok" {
return nil, fmt.Errorf("osrm table: %s", tr.Code)
}
m := make(Matrix, len(pts))
for i := range m {
m[i] = make([]float64, len(pts))
for j := range m[i] {
if tr.Durations[i][j] > 0 {
m[i][j] = tr.Durations[i][j]
} else {
m[i][j] = -1
}
}
}
return m, nil
}
var _ = url.Values{} // keep net/url import for future query building