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()  }  | 
