- 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
137 lines
3.5 KiB
Go
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
|