-
Notifications
You must be signed in to change notification settings - Fork 3
/
metar.go
365 lines (335 loc) · 10.4 KB
/
metar.go
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
361
362
363
364
365
// Package metar provides METAR (METeorological Aerodrome Report) message decoding
package metar
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
"github.com/urkk/metar/clouds"
cnv "github.com/urkk/metar/conversion"
ph "github.com/urkk/metar/phenomena"
rwy "github.com/urkk/metar/runways"
vis "github.com/urkk/metar/visibility"
"github.com/urkk/metar/wind"
)
// CurYearStr - year of message. By default read all messages in the current date. Can be redefined if necessary
var CurYearStr string
// CurMonthStr - month of message
var CurMonthStr string
// CurDayStr - day of message
var CurDayStr string
func init() {
now := time.Now()
CurYearStr = strconv.Itoa(now.Year())
CurMonthStr = fmt.Sprintf("%02d", now.Month())
CurDayStr = fmt.Sprintf("%02d", now.Day())
}
// MetarMessage - Meteorological report presented as a data structure
type MetarMessage struct {
rawData string // The raw METAR
COR bool // Correction to observation
Station string // 4-letter ICAO station identifier
DateTime time.Time // Time (in ISO8601 date/time format) this METAR was observed
Auto bool // METAR from automatic observing systems with no human intervention
NIL bool // event of missing METAR
wind.Wind // Surface wind
// Ceiling And Visibility OK, indicating no cloud below 5,000 ft (1,500 m) or the highest minimum sector
// altitude and no cumulonimbus or towering cumulus at any level, a visibility of 10 km (6 mi) or more and no significant weather change.
CAVOK bool
vis.Visibility // Horizontal visibility
RWYvisibility []rwy.VisualRange // Runway visual range
ph.Phenomena // Present Weather
PhenomenaNotDefined bool // Not detected by the automatic station - “//”
VerticalVisibility int // Vertical visibility (ft)
VerticalVisibilityNotDefined bool // “///”
clouds.Clouds // Cloud amount and height
Temperature int // Temperature in degrees Celsius
Dewpoint int // Dew point in degrees Celsius
QNHhPa int // Altimeter setting. Atmospheric pressure adjusted to mean sea level
// Supplementary informaton
//Recent weather
RecentPhenomena ph.Phenomena
// Information on the state of the runway(s)
RWYState []rwy.State
// Wind shear on runway(s)
WindShear []rwy.RunwayDesignator
// Prevision
TREND []Trend
//OR NO SIGnificant changes coming within the next two hours
NOSIG bool
// Remarks consisting of recent operationally significant weather as well as additive and automated maintenance data
Remarks *Remark
// An array of tokens that couldn't be decoded
NotDecodedTokens []string
}
// RAW - returns the original message text
func (m *MetarMessage) RAW() string { return m.rawData }
func (m *MetarMessage) appendTrend(input []string) {
if trend := parseTrendData(input); trend != nil {
m.TREND = append(m.TREND, *trend)
}
}
// NewMETAR - creates a new METAR based on the original message
func NewMETAR(inputtext string) (*MetarMessage, error) {
m := &MetarMessage{
rawData: inputtext,
}
headerRx := myRegexp{regexp.MustCompile(`^(?P<type>(METAR|SPECI)\s)?(?P<cor>COR\s)?(?P<station>\w{4})\s(?P<time>\d{6}Z)(?P<auto>\sAUTO)?(?P<nil>\sNIL)?`)}
headermap := headerRx.FindStringSubmatchMap(m.rawData)
m.Station = headermap["station"]
m.DateTime, _ = time.Parse("200601021504Z", CurYearStr+CurMonthStr+headermap["time"])
m.COR = headermap["cor"] != ""
m.Auto = headermap["auto"] != ""
m.NIL = headermap["nil"] != ""
if m.Station == "" && m.DateTime.IsZero() {
return m, fmt.Errorf("Not valid message in input")
}
if m.NIL {
return m, nil
}
tokens := strings.Split(m.rawData, " ")
count := 0
totalcount := len(tokens)
// skip station info, date/time, etc.
for _, value := range headermap {
if value != "" {
count++
}
}
var trends [][]string
var remarks []string
// split the array of tokens to parts: main section, remarks and trends
// First, let's remove the RMK group, as it can contain TEMPO (RMK WHT TEMPO GRN)
for i := totalcount - 1; i > count; i-- {
if tokens[i] == "RMK" {
remarks = append(remarks, tokens[i:totalcount]...)
totalcount = i
}
}
for i := totalcount - 1; i > count; i-- {
if tokens[i] == TEMPO || tokens[i] == BECMG {
//for correct order of following on reverse parsing append []trends to current trend
trends = append([][]string{tokens[i:totalcount]}, trends[0:]...)
totalcount = i
}
}
// trends
for _, trendstr := range trends {
m.appendTrend(trendstr)
}
// remarks
m.Remarks = parseRemarks(remarks)
// main section
m.decodeMetar(tokens[count:totalcount])
return m, nil
}
type myRegexp struct {
*regexp.Regexp
}
func (r *myRegexp) FindStringSubmatchMap(s string) map[string]string {
captures := make(map[string]string)
match := r.FindStringSubmatch(s)
if match == nil {
return captures
}
for i, name := range r.SubexpNames() {
// Ignore the whole regexp match and unnamed groups
if i == 0 || name == "" {
continue
}
captures[name] = match[i]
}
return captures
}
func (m *MetarMessage) decodeMetar(tokens []string) {
if tokens[len(tokens)-1] == "NOSIG" {
m.NOSIG = true
tokens = tokens[:len(tokens)-1]
}
totalcount := len(tokens)
for count := 0; count < totalcount; {
// Surface wind
count += m.ParseWind(strings.Join(tokens[count:], " "))
if tokens[count] == "CAVOK" {
m.CAVOK = true
count++
} else {
count = setMetarWeatherCondition(m, count, tokens)
} //end !CAVOK
// Temperature and dew point
if m.setTemperature(tokens[count]) {
count++
}
// Altimeter setting
if m.setAltimetr(tokens[count]) {
count++
}
// All the following elements are optional
// Recent weather
for count < totalcount && m.RecentPhenomena.AppendRecentPhenomena(tokens[count]) {
count++
}
// Wind shear
if ok, tokensused := m.appendWindShears(tokens, count); ok {
count += tokensused
}
// TODO Sea surface condition
// W19/S4 W15/Н7 W15/Н17 W15/Н175
// State of the runway(s)
for count < totalcount && m.appendRunwayState(tokens[count]) {
count++
}
// The token is not recognized or is located in the wrong position
if count < totalcount {
m.NotDecodedTokens = append(m.NotDecodedTokens, tokens[count])
count++
}
} // End main section
}
func setMetarWeatherCondition(m *MetarMessage, count int, tokens []string) int {
// Horizontal visibility
if tokensused := m.ParseVisibility(tokens[count:]); tokensused > 0 {
count += tokensused
}
// Runway visual range
for count < len(tokens) && m.appendRunwayVisualRange(tokens[count]) {
count++
}
// Present Weather
if count < len(tokens) && tokens[count] == "//" {
m.PhenomenaNotDefined = true
count++
}
for count < len(tokens) && m.AppendPhenomena(tokens[count]) {
count++
}
// Vertical visibility
if count < len(tokens) && m.setVerticalVisibility(tokens[count]) {
count++
}
// Cloudiness description
for count < len(tokens) && m.AppendCloud(tokens[count]) {
count++
}
return count
}
// Checks whether the string is a temperature and dew point values and writes this values
func (m *MetarMessage) setTemperature(input string) bool {
regex := regexp.MustCompile(`^(M)?(\d{2})/(M)?(\d{2})$`)
matches := regex.FindStringSubmatch(input)
if len(matches) != 0 {
m.Temperature, _ = strconv.Atoi(matches[2])
m.Dewpoint, _ = strconv.Atoi(matches[4])
if matches[1] == "M" {
m.Temperature = -m.Temperature
}
if matches[3] == "M" {
m.Dewpoint = -m.Dewpoint
}
return true
}
return false
}
func (m *MetarMessage) setAltimetr(input string) bool {
regex := regexp.MustCompile(`([Q|A])(\d{4})`)
matches := regex.FindStringSubmatch(input)
if len(matches) != 0 {
if matches[1] == "A" {
inHg, _ := strconv.ParseFloat(matches[2][:2]+"."+matches[2][2:4], 64)
m.QNHhPa = int(cnv.InHgTohPa(inHg))
} else {
m.QNHhPa, _ = strconv.Atoi(matches[2])
}
return true
}
return false
}
func (m *MetarMessage) appendRunwayVisualRange(input string) bool {
if RWYvis, ok := rwy.ParseVisualRange(input); ok {
m.RWYvisibility = append(m.RWYvisibility, RWYvis)
return true
}
return false
}
func (m *MetarMessage) setVerticalVisibility(input string) bool {
if vv, nd, ok := parseVerticalVisibility(input); ok {
m.VerticalVisibility = vv
m.VerticalVisibilityNotDefined = nd
return true
}
return false
}
func (m *MetarMessage) appendRunwayState(input string) bool {
if input == "R/SNOCLO" {
rwc := new(rwy.State)
rwc.SNOCLO = true
m.RWYState = append(m.RWYState, *rwc)
return true
}
if rwc, ok := rwy.ParseState(input); ok {
m.RWYState = append(m.RWYState, rwc)
return true
}
return false
}
func parseVerticalVisibility(input string) (vv int, nd bool, ok bool) {
regex := regexp.MustCompile(`VV(\d{3}|///)`)
matches := regex.FindStringSubmatch(input)
if len(matches) != 0 && matches[0] != "" {
ok = true
if matches[1] != "///" {
vv, _ = strconv.Atoi(matches[1])
vv *= 100
} else {
nd = true
}
}
return
}
func (m *MetarMessage) appendWindShears(tokens []string, count int) (ok bool, tokensused int) {
regex := regexp.MustCompile(`^WS\s((R\d{2}[LCR]?)|(ALL\sRWY))`)
matches := regex.FindStringSubmatch(strings.Join(tokens[count:], " "))
for ; len(matches) > 0; matches = regex.FindStringSubmatch(strings.Join(tokens[count:], " ")) {
ok = true
if matches[3] != "" { // WS ALL RWY
rd := new(rwy.RunwayDesignator)
rd.AllRunways = true
m.WindShear = append(m.WindShear, *rd)
tokensused += 3
count += 3
}
if matches[2] != "" { // WS R03
m.WindShear = append(m.WindShear, rwy.NewRD(matches[1]))
tokensused += 2
count += 2
}
}
return
}
type weatherCondition interface {
ParseVisibility([]string) int
AppendPhenomena(string) bool
setVerticalVisibility(string) bool
AppendCloud(string) bool
}
// decoder for not CAVOK conditions in TAF messages and trends. Returns new current position in []string
func decodeWeatherCondition(t weatherCondition, count int, tokens []string) int {
// Horizontal visibility.
if count < len(tokens) {
count += t.ParseVisibility(tokens[count:])
}
// Weather or NSW - no significant weather
for count < len(tokens) && t.AppendPhenomena(tokens[count]) {
count++
}
// Vertical visibility
if count < len(tokens) && t.setVerticalVisibility(tokens[count]) {
count++
}
// Clouds.
for count < len(tokens) && t.AppendCloud(tokens[count]) {
count++
}
return count
}