aboutsummaryrefslogtreecommitdiff
path: root/cmd/metar.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/metar.go')
-rw-r--r--cmd/metar.go223
1 files changed, 223 insertions, 0 deletions
diff --git a/cmd/metar.go b/cmd/metar.go
new file mode 100644
index 0000000..6355105
--- /dev/null
+++ b/cmd/metar.go
@@ -0,0 +1,223 @@
+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", 0,
+ `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().BoolVarP(
+ &mostRecent, "one", "1", 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))
+ }
+
+ dur, _ := cmd.Flags().GetDuration("past")
+ if dur > 0 {
+ reqArgs = append(reqArgs, metar.HoursBeforeNow(dur.Hours()))
+ }
+
+ if mostRecentByStation != "" {
+ reqArgs = append(reqArgs, metar.MostRecentForEachStation(mostRecentByStation))
+ }
+
+ if mostRecent {
+ reqArgs = append(reqArgs, metar.MostRecent(true))
+ }
+
+ 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))
+ }
+ },
+}