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"` Routes []struct { Distance float64 `json:"distance"` Duration float64 `json:"duration"` Legs []struct { Distance float64 `json:"distance"` Duration float64 `json:"duration"` } `json:"legs"` Geometry string `json:"geometry"` } `json:"routes"` } 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)) } // overview=full keeps the (encoded-polyline) geometry in the response; // the client trims it after. alternatives is left off: one route is what // stop_cost needs. u := fmt.Sprintf("%s/route/v1/%s/%s?alternatives=false&overview=full", 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" || len(or.Routes) == 0 { return nil, fmt.Errorf("osrm: %s", or.Code) } r0 := or.Routes[0] out := &Route{Duration: r0.Duration, Distance: r0.Distance} for _, l := range r0.Legs { out.Legs = append(out.Legs, Leg{Duration: l.Duration, Distance: l.Distance}) } // Default OSRM responses carry the geometry as an encoded polyline; // with ?geometries=geojson it is a GeoJSON LineString. Handle both. if r0.Geometry != "" { var gj struct { Geometry struct { Coords [][]float64 `json:"coordinates"` } `json:"geometry"` } if json.Unmarshal([]byte(r0.Geometry), &gj) == nil && len(gj.Geometry.Coords) > 0 { for _, c := range gj.Geometry.Coords { out.Geometry = append(out.Geometry, geo.Point{Lat: c[1], Lon: c[0]}) } } else { out.Geometry = decodePolyline(r0.Geometry) } } 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