diff options
Diffstat (limited to 'metar/metar.go')
| -rw-r--r-- | metar/metar.go | 281 | 
1 files changed, 221 insertions, 60 deletions
| diff --git a/metar/metar.go b/metar/metar.go index 1bb57f5..d273e2b 100644 --- a/metar/metar.go +++ b/metar/metar.go @@ -1,9 +1,13 @@ +// Package metar fetches METAR observations from the Aviation Weather Center's +// Text Data Server (TDS). +// +// Note that the TDS stores only the past 3 days of data.  package metar  import (  	"context"  	"encoding/xml" -	"errors" +	"fmt"  	"net/http"  	"net/url"  	"strconv" @@ -11,16 +15,27 @@ import (  	"time"  ) +// A Client can fetch METARs from the TDS.  type Client struct {  	hc http.Client  }  const ( +	// MostRecentConstraint requests the most recent observation for each METAR +	// station in the fastest fashion. Not appropriate for historical data +	// retrieval.  	MostRecentConstraint = "constraint" + +	// MostRecentPostFilter requests the most recent observation for each METAR +	// station by filtering results after applying all other constraints. +	// +	// This is the older, slower filtering method.  	MostRecentPostFilter = "postfilter" -	MostRecentTrue       = "true"  ) +// Response is the top-level XML document returned from the TDS. It contains +// some metadata about the request handling, as well as zero or more METARs +// (individual station observations).  type Response struct {  	RequestIndex int `xml:"request_index"`  	DataSource   struct { @@ -38,60 +53,155 @@ type Response struct {  	} `xml:"data"`  } +// A METAR holds the raw observation data from a station included in a +// Response.  type METAR struct { -	RawText             string  `xml:"raw_text"` -	StationID           string  `xml:"station_id"` -	ObservationTime     string  `xml:"observation_time"` -	Latitude            float32 `xml:"latitude"` -	Longitude           float32 `xml:"longitude"` -	TempC               float32 `xml:"temp_c"` -	DewpointC           float32 `xml:"dewpoint_c"` -	WindDirDegrees      int     `xml:"wind_dir_degress"` -	WindSpeedKt         int     `xml:"wind_speed_kt"` -	WindGustKt          int     `xml:"wind_gust_kt"` +	// The raw METAR +	RawText string `xml:"raw_text"` + +	// Station identifier, always a four character alphanumeric +	StationID string `xml:"station_id"` + +	// Time this METAR was observed (ISO 8601 date/time format) +	ObservationTime string `xml:"observation_time"` + +	// The latitude of the station that reported this METAR (decimal degrees) +	Latitude float32 `xml:"latitude"` + +	// The longitude of the station that reported this METAR (decimal degrees) +	Longitude float32 `xml:"longitude"` + +	// Air temperature (C) +	TempC float32 `xml:"temp_c"` + +	// Dewpoint temperature (C) +	DewpointC float32 `xml:"dewpoint_c"` + +	// Direction from which the wind is blowing (degrees) +	WindDirDegrees int `xml:"wind_dir_degress"` + +	// Wind speed, 0 degree direction and 0 speed = calm winds (kts) +	WindSpeedKt int `xml:"wind_speed_kt"` + +	// Wind gust (kts) +	WindGustKt int `xml:"wind_gust_kt"` + +	// Horizontal visibility (statute miles)  	VisibilityStatuteMi float32 `xml:"visibility_statute_mi"` -	AltimInHg           float32 `xml:"altim_in_hg"` -	SeaLevelPressureMb  float32 `xml:"sea_level_pressure_mb"` -	QualityControlFlags struct { -		Corrected               string `xml:"corrected"` -		Auto                    string `xml:"auto"` -		AutoStation             string `xml:"auto_station"` -		MaintenanceIndicatorOn  string `xml:"maintenance_indicator_on"` -		NoSignal                string `xml:"no_signal"` -		LightningSensorOff      string `xml:"lightning_sensor_off"` -		FreezingRainSensorOff   string `xml:"freezing_rain_sensor_off"` -		PresentWeatherSensorOff string `xml:"present_weather_sensor_off"` -	} `xml:"quality_control_flags"` -	WxString                  string         `xml:"wx_string"` -	SkyConditions             []SkyCondition `xml:"sky_condition"` -	FlightCategory            string         `xml:"flight_category"` -	ThreeHrPressureTendencyMb float32        `xml:"three_hr_pressure_tendency_mb"` -	MaxTC                     float32        `xml:"maxT_c"` -	MinTC                     float32        `xml:"minT_c"` -	MaxT24HrC                 float32        `xml:"maxT24hr_c"` -	MinT24HrC                 float32        `xml:"minT24hr_c"` -	PrecipIn                  float32        `xml:"precip_in"` -	Pcp3HrIn                  float32        `xml:"pcp3hr_in"` -	Pcp6HrIn                  float32        `xml:"pcp6hr_in"` -	Pcp24HrIn                 float32        `xml:"pcp24hr_in"` -	SnowIn                    float32        `xml:"snow_in"` -	VertVisFt                 int            `xml:"vert_vis_ft"` -	METARType                 string         `xml:"metar_type"` -	ElevationM                float32        `xml:"elevation_m"` + +	// Altimeter (inches of Hg) +	AltimInHg float32 `xml:"altim_in_hg"` + +	// Sea-level pressure (mb) +	SeaLevelPressureMb float32 `xml:"sea_level_pressure_mb"` + +	// Quality control flags +	QualityControlFlags QCFlags `xml:"quality_control_flags"` + +	// Present weather string +	WxString string `xml:"wx_string"` + +	// Up to four levels of sky cover and base can be reported +	SkyConditions []SkyCondition `xml:"sky_condition"` + +	// Flight category of this METAR: VFR, MVFR, IFR, or LIFR +	FlightCategory string `xml:"flight_category"` + +	// Pressure change in the past 3 hours +	ThreeHrPressureTendencyMb float32 `xml:"three_hr_pressure_tendency_mb"` + +	// Maximum air temperature from the past 6 hours +	MaxTC float32 `xml:"maxT_c"` + +	// Minimum air temperature from the past 6 hours +	MinTC float32 `xml:"minT_c"` + +	// Maximum air temperature from the past 24 hours +	MaxT24HrC float32 `xml:"maxT24hr_c"` + +	// Minimum air temperature from the past 24 hours +	MinT24HrC float32 `xml:"minT24hr_c"` + +	// Liquid precipitation since the last regular METAR +	PrecipIn float32 `xml:"precip_in"` + +	// Liquid precipitation from the past 3 hours. 0.0005 in = trace precipitation. +	Pcp3HrIn float32 `xml:"pcp3hr_in"` + +	// Liquid precipitation from the past 6 hours. 0.0005 in = trace precipitation. +	Pcp6HrIn float32 `xml:"pcp6hr_in"` + +	// Liquid precipitation from the past 24 hours. 0.0005 in = trace precipitation. +	Pcp24HrIn float32 `xml:"pcp24hr_in"` + +	// Snow depth on the ground (in) +	SnowIn float32 `xml:"snow_in"` + +	// Vertical visibility (ft) +	VertVisFt int `xml:"vert_vis_ft"` + +	// METAR or SPECI +	METARType string `xml:"metar_type"` + +	// The elevation of the station that reported this METAR +	ElevationM float32 `xml:"elevation_m"`  } +// A QCFlags holds quality control flags indicating the status of the station +// and information about the METAR. +type QCFlags struct { +	// Corrected +	Corrected string `xml:"corrected"` + +	// Fully automated +	Auto string `xml:"auto"` + +	// Indicates that the automated station type is one of the following: A01, +	// A01A, A02, A02A, AOA, AWOS. +	// +	// Note: The type of station is not returned. This simply indicates that +	// this station is one of the six stations enumerated above. +	AutoStation string `xml:"auto_station"` + +	// Maintenance check indicator - maintenance is needed +	MaintenanceIndicatorOn string `xml:"maintenance_indicator_on"` + +	// No signal +	NoSignal string `xml:"no_signal"` + +	// The lightning detection sensor is not operating - thunderstorm +	// information is not available. +	LightningSensorOff string `xml:"lightning_sensor_off"` + +	// The freezing rain sensor is not operating +	FreezingRainSensorOff string `xml:"freezing_rain_sensor_off"` + +	// The present weather sensor is not operating +	PresentWeatherSensorOff string `xml:"present_weather_sensor_off"` +} + +// A SkyCondition describes the condition of the sky at a particular altitude.  type SkyCondition struct { -	SkyCover       string `xml:"sky_cover,attr"` -	CloudBaseFtAGL int    `xml:"cloud_base_ft_agl,attr"` +	// SKC, CLR, CAVOK, FEW, SCT, BKN, OVC, OVX +	SkyCover string `xml:"sky_cover,attr"` + +	// Height of cloud base in feet AGL. A value exists when SkyCover is FEW, +	// SCT, BKN, or OVC. +	CloudBaseFtAGL int `xml:"cloud_base_ft_agl,attr"`  } +// A Request holds options for a METAR request such as a station, area, or +// flight path to get METARs for, the time range for which to request +// observations, etc.  type Request struct {  	v url.Values  } -type requestArg func(*Request) - -func NewRequest(args ...requestArg) *Request { +// NewRequest builds a Request with the provided arguments. Supported Arguments +// include StationString, TimeRange, HoursBeforeNow, MostRecent, +// MostRecentForEachStation, LatLongRect, RadialDistance, FlightPath, +// MinDegreeDistance, and Fields. +func NewRequest(args ...func(*Request)) *Request {  	var r Request  	for _, arg := range args {  		arg(&r) @@ -99,38 +209,66 @@ func NewRequest(args ...requestArg) *Request {  	return &r  } -func StationString(s string) requestArg { +// StationString sets which station(s) METARs are fetched from. A Station +// String can contain one or more complete or partial ICAO IDs separated by +// whitespace and/or commas, US states or Canadian provinces prefixed with '@', +// or two-letter country abbrevations prefixed with '~'. +// +// Examples: +// - "KDEN KSEA, PHNL" obtains all available METARs for KDEN, KSEA, and PHNL +// - "KSEA KDE" obtains all available METARs for KSEA and all ICAO IDs +//   beginning with KDE (i.e. KDEN, KDEH, KDEW, etc) +// - "KSEA KDE*" is equivalent to "KSEA KDE" +// - "@WA" obtains METARs for all ICAO IDs from Washington state +// - "@BC" obtains METARs for all ICAO IDs from British Columbia, Canada +// - "~au" obtains METARs for all ICAO IDs from Australia +func StationString(s string) func(*Request) {  	return func(r *Request) {  		r.v.Add("stationString", s)  	}  } -func TimeRange(from, to time.Time) requestArg { +// TimeRange requests METARs observed between the from and to times. +func TimeRange(from, to time.Time) func(*Request) {  	return func(r *Request) {  		r.v.Add("startTime", string(from.Unix()))  		r.v.Add("endTime", string(to.Unix()))  	}  } -func HoursBeforeNow(h float64) requestArg { +// HoursBeforeNow requests METARs observed during the previous h hours. +func HoursBeforeNow(h float64) func(*Request) {  	return func(r *Request) {  		r.v.Add("hoursBeforeNow", strconv.FormatFloat(h, 'f', 64, -1))  	}  } -func MostRecent(mr bool) requestArg { +// MostRecent sets whether only the most recent METAR should be fetched. When +// requesting multiple stations, this means that only the METAR from one of +// them will be returned. +// +// Use MostRecentForEachStation to apply a most-recent constraint to multiple +// stations. +func MostRecent(mr bool) func(*Request) {  	return func(r *Request) {  		r.v.Add("mostRecent", strconv.FormatBool(mr))  	}  } -func MostRecentForEachStation(s string) requestArg { +// MostRecentForEachStation sets whether to fetch only the most recent METAR +// for each requested station. The provided string should be either +// MostRecentConstraint or MostRecentPostFilter. +func MostRecentForEachStation(s string) func(*Request) {  	return func(r *Request) {  		r.v.Add("mostRecentForEachStation", s)  	}  } -func LatLongRect(minLat, minLon, maxLat, maxLon float64) requestArg { +// LonLatRect specifies a rectangular bounding box of geographic coordinates in +// which to fetch METARs. +// +// Does not support bounding boxes that encompass a pole. +func LonLatRect(minLat, minLon, maxLat, maxLon float64) func(*Request) {  	return func(r *Request) {  		r.v.Add("minLat", strconv.FormatFloat(minLat, 'f', 64, -1))  		r.v.Add("minLon", strconv.FormatFloat(minLon, 'f', 64, -1)) @@ -139,25 +277,45 @@ func LatLongRect(minLat, minLon, maxLat, maxLon float64) requestArg {  	}  } -func RadialDistance(s string) requestArg { +// RadialDistance specifies a radius around a point in which to fetch METARs. +// +// Described area may not cross the international date line or either pole. +func RadialDistance(r, lat, lon float64) func(*Request) {  	return func(r *Request) { -		r.v.Add("radialDistance", s) +		r.v.Add("radialDistance", fmt.Sprintf("%f;%f,%f", r, lon, lon))  	}  } -func FlightPath(s string) requestArg { +// FlightPath obtains all METARs for the specified flight path, up to a maximum +// distance. +// +// Waypoints may take the form of "lon,lat" or ICAO station IDs, and may be +// mixed interchangably. The ordering of waypoints is significant, always start +// with the origin and end with the destination. +// +// Note: Flight path results are sorted by distance along the flight path from +// origin to destination. The flight path constraint does not support flight +// paths that cross the poles or flight paths that cross the international date +// line. +func FlightPath(maxDist float64, waypoints ...string) func(*Request) {  	return func(r *Request) { -		r.v.Add("flightPath", s) +		r.v.Add("flightPath", fmt.Sprintf("%f;%s", maxDist, strings.Join(waypoints, ";")))  	}  } -func MinDegreeDistance(d float64) requestArg { +// MinDegreeDistance can be used to fetch fewer results by specifying a minimum +// degree distance (based on longitude and latitude) between stations. +// +// A large MinDegreeDistance will yield less dense results. Duplicate stations +// are filtered and the most recent of duplicate stations is reported. +func MinDegreeDistance(d float64) func(*Request) {  	return func(r *Request) {  		r.v.Add("minDegreeDistance", strconv.FormatFloat(d, 'f', 64, -1))  	}  } -func Fields(ss ...string) requestArg { +// Fields specifies a subset of METAR fields to be collected. +func Fields(ss ...string) func(*Request) {  	return func(r *Request) {  		r.v.Add("fields", strings.Join(ss, ","))  	} @@ -170,6 +328,12 @@ func requestToQueryString(m *Request) string {  	return m.v.Encode()  } +// GetMETARs fetches METARs from the TDS based on the constraints specified by +// the Request and returns the Response and an error. +// +// Note that an error is only returned if there is a failure at the HTTP layer +// or the response could not be interpreted. The response may additionally +// contain error messages if the request was unable to be processed.  func (c *Client) GetMETARs(ctx context.Context, req *Request) (*Response, error) {  	u := url.URL{  		Scheme:   "https", @@ -186,9 +350,6 @@ func (c *Client) GetMETARs(ctx context.Context, req *Request) (*Response, error)  	if err != nil {  		return nil, err  	} -	if resp.StatusCode != http.StatusOK { -		return nil, errors.New(resp.Status) -	}  	defer resp.Body.Close()  	var r Response  	if err := xml.NewDecoder(resp.Body).Decode(&r); err != nil { | 
