summaryrefslogtreecommitdiff
path: root/metar/metar.go
blob: 072a2cd1bfcf4b085e3ef2c5a08b158d43b2b238 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
// 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
}