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