From 380002c0cfd213b8d0bb70a387181e424aa969aa Mon Sep 17 00:00:00 2001 From: Ben Burwell Date: Wed, 5 Feb 2020 23:28:40 -0500 Subject: Flesh out docs --- metar/metar.go | 281 +++++++++++++++++++++++++++++++++++++++++++++------------ 1 file 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 { -- cgit v1.2.3