// 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" "fmt" "net/http" "net/url" "strconv" "strings" "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" ) // 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 { Name string `xml:"name,attr"` } `xml:"data_source"` Request struct { Type string `xml:"type,attr"` } `xml:"request"` Errors []string `xml:"errors>error"` Warnings []string `xml:"warnings>warning"` TimeTakenMs int `xml:"time_taken_ms"` Data struct { NumResults int `xml:"num_results,attr"` METARs []METAR `xml:"METAR"` } `xml:"data"` } // A METAR holds the raw observation data from a station included in a // Response. type METAR struct { // 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"` // 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 { // 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 } // 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 { r := &Request{v: make(url.Values)} for _, arg := range args { arg(r) } return r } // 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) } } // 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", strconv.FormatInt(from.Unix(), 10)) r.v.Add("endTime", strconv.FormatInt(to.Unix(), 10)) } } // 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', -1, 64)) } } // 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)) } } // 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) } } // 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', -1, 64)) r.v.Add("minLon", strconv.FormatFloat(minLon, 'f', -1, 64)) r.v.Add("maxLat", strconv.FormatFloat(maxLat, 'f', -1, 64)) r.v.Add("maxLon", strconv.FormatFloat(maxLon, 'f', -1, 64)) } } // 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", fmt.Sprintf("%f;%f,%f", r, lon, lon)) } } // 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", fmt.Sprintf("%f;%s", maxDist, strings.Join(waypoints, ";"))) } } // 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', -1, 64)) } } // 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, ",")) } } func requestToQueryString(m *Request) string { m.v.Add("dataSource", "metars") m.v.Add("requestType", "retrieve") m.v.Add("format", "xml") 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", Host: "www.aviationweather.gov", Path: "/adds/dataserver_current/httpparam", RawQuery: requestToQueryString(req), } hr, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) if err != nil { return nil, err } hr.Header.Add("user-agent", "wxbot/1.0 +bnbl.io/wx") resp, err := c.hc.Do(hr) if err != nil { return nil, err } defer resp.Body.Close() var r Response if err := xml.NewDecoder(resp.Body).Decode(&r); err != nil { return nil, err } return &r, nil }