package cmd import ( "context" "fmt" "os" "strings" "time" "bnbl.io/wx/metar" "github.com/spf13/cobra" ) // wx metar --past 1h kbos // wx metar --past 3h kbos kbed // wx metar --past 1h @MA // wx metar --since yesterday --until now kbos // wx metar --since yesterday --until now --rect 0,0,1,1 // wx metar --past 1h --rect 0,0,1,1 // wx metar --past 1h --radial 90,0,0 // wx metar --past 6h --most-recent-by-station=constraint --path-dist 50 --waypoint ksea --waypoint kbos --min-dist 3 type relTime struct { t time.Time } func (t *relTime) String() string { return t.t.String() } func (t *relTime) Set(s string) (err error) { // first, see if s is a duration that we can use relative to now, e.g. // "--since 3d" means since 3 days ago. if d, err := time.ParseDuration(s); err == nil { t.t = time.Now().Add(-1 * d) return nil } // next, try to parse it in a series of formats t.t, err = time.Parse("2006-01-02T15:04:05", s) return } func (t *relTime) Type() string { return "relative time" } type rectangle struct { minLat, minLon, maxLat, maxLon float64 } func (r *rectangle) String() string { return fmt.Sprintf("%f,%f,%f,%f", r.minLat, r.minLon, r.maxLat, r.maxLon) } func (r *rectangle) Set(s string) error { n, err := fmt.Sscanf(s, "%f,%f,%f,%f", &r.minLat, &r.minLon, &r.maxLat, &r.maxLon) if err != nil { return err } if n != 4 { return fmt.Errorf("excpected 4 values but got %d", n) } return nil } func (r *rectangle) Type() string { return "rect" } func (r *rectangle) IsZero() bool { return r.minLat == 0 && r.minLon == 0 && r.maxLat == 0 && r.maxLon == 0 } type ring struct { radius, lat, lon float64 } func (r *ring) String() string { return fmt.Sprintf("%f,%f,%f", r.radius, r.lat, r.lon) } func (r *ring) Set(s string) error { n, err := fmt.Sscanf(s, "%f,%f,%f", &r.radius, &r.lat, &r.lon) if err != nil { return err } if n != 3 { return fmt.Errorf("expected a radius, lat, and lon, but got %d values", n) } return nil } func (r *ring) Type() string { return "radial" } func (r *ring) IsZero() bool { return r.radius == 0 && r.lat == 0 && r.lon == 0 } var ( since, until relTime past time.Duration mostRecentByStation string mostRecent bool rect rectangle radial ring pathDist float64 waypoints []string minDist float64 ) func init() { rootCmd.AddCommand(metarCmd) // time constraints metarCmd.Flags().Var( &since, "since", `Starting time for METARs (e.g. "2020-01-01T01:31:00" or "3h"), used in conjunction with --until`) metarCmd.Flags().Var( &until, "until", `Ending time for METARs (e.g. "2020-01-01T01:31:00" or "3h"), used in conjunction with --since`) metarCmd.Flags().DurationVar( &past, "past", time.Hour, `Duration to retrieve METARs since (e.g. "3h")`) // location constraints metarCmd.Flags().Var( &rect, "rect", `A lat-lon rect to search within (minLat,minLon,maxLat,maxLon) e.g. "40,-71,41,-70"`) metarCmd.Flags().Var( &radial, "radial", `A distance around a radius to search within (dist,centerLat,centerLon) e.g. "3.5,40,-71"`) metarCmd.Flags().Float64Var( &pathDist, "path-dist", 0, "The distance along the flight path to search METARs for") metarCmd.Flags().StringSliceVar( &waypoints, "waypoint", []string{}, `Multiple waypoint arguments specify the waypoints that make up a flight path, ordered from origin to destination. They may be specified as ICAO identifiers e.g. "KBOS", or as lon-lat pairs, e.g. "-71,40".`) metarCmd.Flags().Float64Var( &minDist, "min-dist", 0, `The minimum distance in degrees latitude or longitude between stations reported. A higher value results in less dense results.`) // result count limiting metarCmd.Flags().StringVar( &mostRecentByStation, "most-recent-by-station", "constraint", `Method for fetching most recent for each station ("constraint", "postfilter", "false")`) metarCmd.Flags().BoolVar( &mostRecent, "most-recent", false, `Only fetch the single most recently-issued METAR of all the selected stations`) } var metarCmd = &cobra.Command{ Use: "metar", Short: "Fetch METARs", Long: "Fetch METARs matching the supplied criteria from the TDS", Run: func(cmd *cobra.Command, args []string) { reqArgs := []func(*metar.Request){} if len(args) > 0 { reqArgs = append(reqArgs, metar.StationString(strings.Join(args, " "))) } if !since.t.IsZero() || !until.t.IsZero() { reqArgs = append(reqArgs, metar.TimeRange(since.t, until.t)) } else if dur, _ := cmd.Flags().GetDuration("past"); dur > 0 { reqArgs = append(reqArgs, metar.HoursBeforeNow(dur.Hours())) } if mostRecent { reqArgs = append(reqArgs, metar.MostRecent(true)) } else if mostRecentByStation != "" { reqArgs = append(reqArgs, metar.MostRecentForEachStation(mostRecentByStation)) } if !rect.IsZero() { reqArgs = append(reqArgs, metar.LonLatRect(rect.minLat, rect.minLon, rect.maxLat, rect.maxLon)) } if !radial.IsZero() { reqArgs = append(reqArgs, metar.RadialDistance(radial.radius, radial.lat, radial.lon)) } if pathDist > 0 && len(waypoints) > 0 { reqArgs = append(reqArgs, metar.FlightPath(pathDist, waypoints...)) } if minDist > 0 { reqArgs = append(reqArgs, metar.MinDegreeDistance(minDist)) } var mc metar.Client res, err := mc.GetMETARs(context.Background(), metar.NewRequest(reqArgs...)) if err != nil { fmt.Fprintf(os.Stderr, "could not get metar: %v\n", err) os.Exit(1) } if len(res.Errors) > 0 { for _, err := range res.Errors { fmt.Fprintf(os.Stderr, "error: %s\n", err) } os.Exit(2) } for _, m := range res.Data.METARs { fmt.Printf("%s\n", strings.TrimSpace(m.RawText)) } }, }