diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile | 14 | ||||
-rw-r--r-- | cmd/metar.go | 223 | ||||
-rw-r--r-- | cmd/root.go | 23 | ||||
-rw-r--r-- | go.mod | 7 | ||||
-rw-r--r-- | go.sum | 36 | ||||
-rw-r--r-- | main.go | 21 |
7 files changed, 303 insertions, 22 deletions
@@ -1 +1,2 @@ wxcli +wx diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b9a7829 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +SOURCES=$(wildcard *.go) $(wildcard */*.go) +NAME=wx +BINDIR=/usr/bin + +$(NAME): $(SOURCES) + go build -o $(NAME) . + +.PHONY: clean +clean: + rm -f $(NAME) + +.PHONY: install +install: + install -m755 $(NAME) $(BINDIR) 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)) + } + }, +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..09660f0 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +var rootCmd = &cobra.Command{ + Use: "wx", + Short: "wx is a front-end for NOAA's Aviation Weather Center", + Long: `A front-end for NOAA's Aviation Weather Center. +Capable of fetching weather information from the Center's Text Data Server +(TDS) and displaying it in various formats.`, +} @@ -2,4 +2,9 @@ module bnbl.io/wxcli go 1.13 -require bnbl.io/wx v0.1.0 +require ( + bnbl.io/wx v0.1.1 + github.com/spf13/cobra v0.0.5 +) + +// replace bnbl.io/wx => ../wx @@ -1,2 +1,34 @@ -bnbl.io/wx v0.1.0 h1:FNrthzkLcsbqupDmcBNil3cd+KA3TRmo+6fkjbXac3Y= -bnbl.io/wx v0.1.0/go.mod h1:0oU9MUdR+IKQRitmtKTLG1bVQvaI00iVvmzqWlIhi0c= +bnbl.io/wx v0.1.1 h1:iY8XIY+z5rWCTY/rWNyCDpKpOrvA/jVKiyBNt7GKq+Q= +bnbl.io/wx v0.1.1/go.mod h1:0oU9MUdR+IKQRitmtKTLG1bVQvaI00iVvmzqWlIhi0c= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1,26 +1,9 @@ package main import ( - "context" - "fmt" - "strings" - - "bnbl.io/wx/metar" + "bnbl.io/wxcli/cmd" ) func main() { - var mc metar.Client - req := metar.NewRequest( - metar.StationString("KBOS KBED KOWD KORH"), - metar.HoursBeforeNow(4.0), - metar.MostRecentForEachStation(metar.MostRecentConstraint), - ) - res, err := mc.GetMETARs(context.Background(), req) - if err != nil { - fmt.Printf("could not get metar: %v\n", err) - return - } - for _, m := range res.Data.METARs { - fmt.Printf("%s\n", strings.TrimSpace(m.RawText)) - } + cmd.Execute() } |