// routectl is the manual/CI front end for the router + plan primitives. // // Usage: // // routectl route --backend valhalla --from 44.054,-71.650 --to 40.729,-73.966 // routectl stopcost --backend valhalla --from A --to C --stop 42.277,-71.806:60:CascadeFalls // routectl optimize --backend valhalla --from A --to C --k 2 \ // --stop 42.277,-71.806:60:CascadeFalls --stop 43.507,-71.548:50:Ladd // // stop format: lat,lon[:dwellMin[:name]] package main import ( "context" "flag" "fmt" "os" "strconv" "strings" "time" "maps/router/internal/geo" "maps/router/internal/plan" "maps/router/internal/route" ) func main() { if len(os.Args) < 2 { usage() } var err error switch os.Args[1] { case "route": err = cmdRoute(os.Args[2:]) case "stopcost": err = cmdStopCost(os.Args[2:]) case "optimize": err = cmdOptimize(os.Args[2:]) default: usage() } if err != nil { fmt.Fprintln(os.Stderr, "error:", err) os.Exit(1) } } func usage() { fmt.Fprintln(os.Stderr, "usage: routectl {route|stopcost|optimize} [flags]") os.Exit(2) } type common struct { backend string from, to string } func (c *common) parse(fs *flag.FlagSet) { fs.StringVar(&c.backend, "backend", "valhalla", "valhalla|osrm") fs.StringVar(&c.from, "from", "", "lat,lon origin") fs.StringVar(&c.to, "to", "", "lat,lon destination") } func (c *common) router() route.Router { switch c.backend { case "valhalla": return route.NewValhalla("https://valhalla1.openstreetmap.de") case "osrm": return route.NewOSRM("http://localhost:5000") default: panic("bad backend " + c.backend) } } func parsePt(s string) (lat, lon float64) { parts := strings.Split(s, ",") lat, _ = strconv.ParseFloat(strings.TrimSpace(parts[0]), 64) lon, _ = strconv.ParseFloat(strings.TrimSpace(parts[1]), 64) return } func point(s string) geo.Point { lat, lon := parsePt(s) return geo.Point{Lat: lat, Lon: lon} } func parseStops(args []string) []plan.Stop { var stops []plan.Stop for _, a := range args { parts := strings.Split(a, ":") lat, lon := parsePt(parts[0]) s := plan.Stop{At: geo.Point{Lat: lat, Lon: lon}, ID: parts[0]} if len(parts) > 1 && parts[1] != "" { s.DwellMin, _ = strconv.Atoi(parts[1]) } if len(parts) > 2 && parts[2] != "" { s.Name = parts[2] s.ID = parts[2] } stops = append(stops, s) } return stops } func fmtDur(sec float64) string { m := int(0.5 + sec/60) return fmt.Sprintf("%dh%02dm", m/60, m%60) } // ---- commands ------------------------------------------------------- func cmdRoute(args []string) error { fs := flag.NewFlagSet("route", flag.ExitOnError) var c common c.parse(fs) var walk bool fs.BoolVar(&walk, "walk", false, "walking profile") fs.Parse(args) if c.from == "" || c.to == "" { return fmt.Errorf("--from and --to required") } r, err := c.router().Route(context.Background(), prof(walk), []geo.Point{point(c.from), point(c.to)}) if err != nil { return err } fmt.Printf("backend: %s\n", c.router().Name()) fmt.Printf("time: %s (%.0f s)\n", fmtDur(r.Duration), r.Duration) fmt.Printf("distance: %.1f km\n", r.Distance/1000) if len(r.Geometry) > 0 { fmt.Printf("geometry: %d points (exact corridor available)\n", len(r.Geometry)) } else { fmt.Printf("geometry: none (chord-corridor fallback, low confidence)\n") } return nil } func cmdStopCost(args []string) error { fs := flag.NewFlagSet("stopcost", flag.ExitOnError) var c common c.parse(fs) var stopArgs []string fs.Func("stop", "stop lat,lon[:dwellMin[:name]]", func(v string) error { stopArgs = append(stopArgs, v) return nil }) fs.Parse(args) if c.from == "" || c.to == "" || len(stopArgs) == 0 { return fmt.Errorf("--from, --to, and --stop required") } stops := parseStops(stopArgs) m, err := buildMatrix(c.router(), c.from, c.to, stops) if err != nil { return err } for i, s := range stops { cost := plan.StopCost(m, i, s) fmt.Printf("%-18s detour=%3d min dwell=%3d min overhead=%2d min total=%3d min (direct %d min)\n", s.ID, cost.DetourMin, cost.DwellMin, cost.OverheadMin, cost.TotalMin, cost.DirectMin) } return nil } func cmdOptimize(args []string) error { fs := flag.NewFlagSet("optimize", flag.ExitOnError) var c common c.parse(fs) var stopArgs []string var k, budget int fs.Func("stop", "stop lat,lon[:dwellMin[:name]]", func(v string) error { stopArgs = append(stopArgs, v) return nil }) fs.IntVar(&k, "k", 2, "number of stops") fs.IntVar(&budget, "budget", 0, "max total minutes (0 = none)") fs.Parse(args) if c.from == "" || c.to == "" || len(stopArgs) == 0 { return fmt.Errorf("--from, --to, and --stop required") } stops := parseStops(stopArgs) m, err := buildMatrix(c.router(), c.from, c.to, stops) if err != nil { return err } res := plan.OptimizeStops(m, stops, k, budget) names := make([]string, len(res.Order)) for i, si := range res.Order { names[i] = stops[si].ID } fmt.Printf("order: %s\n", strings.Join(names, " -> ")) fmt.Printf("detour: %d min\n", res.DetourMin) fmt.Printf("total: %d min (budget %d, feasible=%v, relaxed=%v, exhaustive=%v)\n", res.TotalMin, budget, res.Feasible, res.Relaxed, res.Exhaustive) return nil } // ---- helpers -------------------------------------------------------- func prof(walk bool) route.Profile { if walk { return route.ProfileWalk } return route.ProfileDrive } // buildMatrix builds the (n+2)-layout matrix: stops, then A, then C. func buildMatrix(r route.Router, fromS, toS string, stops []plan.Stop) (route.Matrix, error) { pts := make([]geo.Point, 0, len(stops)+2) for _, s := range stops { pts = append(pts, s.At) } pts = append(pts, point(fromS), point(toS)) // Politeness delay for hosted backends. delay := 150 * time.Millisecond if _, ok := r.(*route.OSRM); ok { delay = 0 } return route.BuildMatrix(context.Background(), r, route.ProfileDrive, pts, delay) }