1 // ----------------------------------------------------------------------------
2 // metar.cxx
3 //
4 // Copyright (C) 2019
5 // David Freese, W1HKJ
6 //
7 // This file is part of fldigi.
8 //
9 // Fldigi is free software: you can redistribute it and/or modify
10 // it under the terms of the GNU General Public License as published by
11 // the Free Software Foundation, either version 3 of the License, or
12 // (at your option) any later version.
13 //
14 // Fldigi is distributed in the hope that it will be useful,
15 // but WITHOUT ANY WARRANTY; without even the implied warranty of
16 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 // GNU General Public License for more details.
18 //
19 // You should have received a copy of the GNU General Public License
20 // along with fldigi. If not, see <http://www.gnu.org/licenses/>.
21 // ----------------------------------------------------------------------------
22
23 #include "metar.h"
24
25 /*======================================================================
26 *
27 * WEATHER The weather group
28 * iiddppooxx
29 * ii is intensity group
30 * ii Description
31 * - light
32 * moderate
33 * + heavy
34 * VC in the vicinity
35 *
36 * dd is the descriptor group
37 * dd Description
38 * MI shallow
39 * PR partial
40 * BC patches
41 * DR low drifting
42 * BL blowing
43 * SH shower
44 * TS thunderstorm
45 * FZ freezing
46 *
47 * pp is the precipitation group
48 * pp Description
49 * DZ drizzle
50 * RA rain
51 * SN snow
52 * SG snow grains
53 * IC ice crystals
54 * PE ice pellets
55 * GR hail
56 * GS small hail/snow pellets
57 * UP unknown
58 *
59 * oo is the obscuration group
60 * oo Description
61 * BR mist
62 * FG fog
63 * FU smoke
64 * VA volcanic ash
65 * DU dust
66 * SA sand
67 * HZ haze
68 * PY spray
69 *
70 * xx is the misc group
71 * xx Description
72 * PO dust whirls
73 * SQ squalls
74 * FC funnel cloud/tornado/waterspout
75 * SS duststorm
76 *
77 * CLOUDS
78 * The cloud levels
79 * ccchhhtt
80 * ccc is the coverage
81 * CLR or SKC = clear
82 * FEW = 1/8 coverage
83 * SCT = 2,3,4/8 coverage
84 * BKN = 5,6,7/8 coverage
85 * OVC = overcast
86 * VV = vertical visibility for obscuration
87 * hhh is the height of base in 30m or 100ft increments. ie 30 = 3000 feet
88 * tt is an optional type
89 * CU = cumulus
90 * CB = cumulonumbus
91 * TCU = towering cumulus
92 * CI = cirrus
93 */
94
95 struct wxpairs {const char *grp; const char *name;};
96
97 static wxpairs precip[] = {
98 {"DZ", "drizzle"},
99 {"RA", "rain"},
100 {"SN", "snow"},
101 {"SG", "snow grains"},
102 {"IC", "ice crystals"},
103 {"PE", "ice pellets"},
104 {"GR", "hail"},
105 {"GS", "small hail / show pellets"},
106 {"UP", "unknown"},
107 {NULL, NULL} };
108
109 static wxpairs intensity[] = {
110 {"-", "light"},
111 {"+", "heavy"},
112 {"VC", "in the vicinity"},
113 {NULL, NULL} };
114
115 static wxpairs descriptor[] = {
116 {"MI", "shallow "},
117 {"PR", "partial"},
118 {"BC", "patches"},
119 {"DR", "low drifting"},
120 {"BL", "blowing"},
121 {"SH", "shower"},
122 {"TS", "thunderstorm"},
123 {"FZ", "freezing"},
124 {NULL, NULL} };
125
126 static wxpairs obscure[] = {
127 {"BR", "mist"},
128 {"FG", "fog"},
129 {"FU", "smoke"},
130 {"VA", "volcanic ash"},
131 {"DU", "dust"},
132 {"SA", "sand"},
133 {"HZ", "haze"},
134 {"PY", "spray"},
135 {NULL, NULL} };
136
137 static wxpairs misc[] = {
138 {"PO", "dust whirls"},
139 {"SQ", "squalls"},
140 {"FC", "funnel cloud/tornado/waterspout"},
141 {"SS", "duststorm"},
142 {NULL, NULL} };
143
144 static wxpairs clouds[] = {
145 {"CLR", "clear skies"},
146 {"SKC", "clear skies"},
147 {"FEW", "few clouds"},
148 {"SCT", "scattered clouds"},
149 {"BKN", "broken cloud cover"},
150 {"OVC", "overcast"},
151 {NULL, NULL} };
152
153 static wxpairs cloud_type[] = {
154 {"CU", "cumulus"},
155 {"CB", "cumulonumbus"},
156 {"TCU", "towering cumulus"},
157 {"CI", "cirrus"},
158 {NULL, NULL} };
159
parse()160 void Metar::parse()
161 {
162 size_t p, p1, p2, p3;
163
164 p = _metar_text.find(_metar_station);
165 if (p == std::string::npos) {
166 _wx_text_full.assign(_metar_station).append(" not found in url response!");
167 _wx_text_full.append("\n").append(_metar_text).append("\n");
168 return;
169 }
170
171 const char *cl = "Content-Length:";
172 int content_length;
173 p1 = _metar_text.find(cl);
174 if (p1 != std::string::npos) {
175 content_length = atol(&_metar_text[p1 + strlen(cl)]);
176 _metar_text.erase(0, _metar_text.length() - content_length);
177 } else {
178 p2 = _metar_text.find("(");
179 if (p2 != std::string::npos) {
180 p3 = _metar_text.rfind("\n", p2);
181 if (p3 != std::string::npos)
182 _metar_text.erase(0,p3 + 1);
183 }
184 else while ( (p2 = _metar_text.find("\r\n")) != std::string::npos)
185 _metar_text.erase(0, p2 + 2);
186 }
187
188 p1 = _metar_text.find("\n");
189 _station_name = _metar_text.substr(0, p1);
190 if ((p2 = _station_name.find("(")) != std::string::npos)
191 _station_name.erase(p2 - 1);
192
193 p3 = _metar_text.find("ob:");
194 if (p3 == std::string::npos) {
195 _wx_text_full.assign(_metar_station).append(" observations not available");
196 _wx_text_parsed.assign(_wx_text_full);
197 return;
198 }
199
200 _wx_text_full.assign(_metar_text.substr(0, p3));
201
202 p = _metar_text.find(_metar_station, p3 + 1);
203 _metar_text.erase(0, p + 1 + _metar_station.length());
204 p = _metar_text.find("\n");
205 if (p != std::string::npos) _metar_text.erase(p);
206
207 // parse field contents
208 bool parsed = false;
209 _conditions.clear();
210 while(_metar_text.length()) { // each ob: field is separated by a space or end of file
211 parsed = false;
212 p = _metar_text.find(" ");
213 if (p != std::string::npos) {
214 _field = _metar_text.substr(0, p);
215 _metar_text.erase(0, p+1);
216 } else {
217 _field = _metar_text;
218 _metar_text.clear();
219 }
220 if (_field == "RMK") break;
221 // parse for general weather
222 // iiddppooxx
223 if (_field == "AUTO") ;
224 else if ((p = _field.rfind("KT")) != std::string::npos) {
225 if (p == _field.length() - 2) { // wind dir / speed
226 int knots;
227 _winds.clear();
228 if (sscanf(_field.substr(3,2).c_str(), "%d", &knots) == 1) {
229 _winds.append(_field.substr(0,3)).append(" at ");
230 char ctemp[10];
231 if (_mph) {
232 snprintf(ctemp, sizeof(ctemp), "%d mph ", (int)ceil(knots * 600.0 / 528.0 ));
233 _winds.append(ctemp);
234 }
235 if (_kph) {
236 snprintf(ctemp, sizeof(ctemp), "%d km/h ", (int)ceil(knots * 600.0 * 1.6094 / 528.0));
237 _winds.append(ctemp);
238 }
239 }
240 }
241 }
242 else if ((p = _field.rfind("MPS")) != std::string::npos) {
243 if (p == _field.length() - 3) { // wind dir / speed in meters / second
244 int mps;
245 _winds.clear();
246 if (sscanf(_field.substr(3,2).c_str(), "%d", &mps) == 1) {
247 _winds.append(_field.substr(0,3)).append(" at ");
248 char ctemp[10];
249 if (_mph) {
250 snprintf(ctemp, sizeof(ctemp), "%d mph ", (int)ceil(mps * 2.2369));
251 _winds.append(ctemp);
252 }
253 if (_kph) {
254 snprintf(ctemp, sizeof(ctemp), "%d km/h ", (int)ceil(mps * 3.6));
255 _winds.append(ctemp);
256 }
257 }
258 }
259 }
260 else if ((p = _field.find("/") ) != std::string::npos) { // temperature / dewpoint
261 std::string cent = _field.substr(0, p);
262 if (cent[0] == 'M') cent[0] = '-';
263 int tempC, tempF;
264 _temp.clear();
265 if (sscanf(cent.c_str(), "%d", &tempC) == 1) {
266 tempF = (int)(tempC * 1.8 + 32);
267 char ctemp[10];
268 if (_fahrenheit) {
269 snprintf(ctemp, sizeof(ctemp), "%d F ", tempF);
270 _temp.append(ctemp);
271 }
272 if (_celsius) {
273 snprintf(ctemp, sizeof(ctemp), "%d C", tempC);
274 _temp.append(ctemp);
275 }
276 }
277 }
278 else if ((_field[0] == 'A' && _field.length() == 5) || _field[0] == 'Q') {
279 float inches;
280 _baro.clear();
281 if (sscanf(_field.substr(1).c_str(), "%f", &inches) == 1) {
282 if (_field[0] == 'A')
283 inches /= 100.0;
284 else
285 inches /= 33.87;
286 char ctemp[20];
287 if (_inches) {
288 snprintf(ctemp, sizeof(ctemp), "%.2f in. Hg ", inches);
289 _baro.append(ctemp);
290 }
291 if (_mbars) {
292 snprintf(ctemp, sizeof(ctemp), "%.0f mbar", floor(inches * 33.87));
293 _baro.append(ctemp);
294 }
295 }
296 } if (!parsed) {
297 for (wxpairs *pp = precip; pp->grp != NULL; pp++) {
298 if (_field.find(pp->grp) != std::string::npos) { // found a precip group
299 wxpairs *ii, *dd, *oo, *xx;
300 for (ii = intensity; ii->grp != NULL; ii++)
301 if (_field.find(ii->grp) != std::string::npos) break;
302 for (dd = descriptor; dd->grp != NULL; dd++)
303 if (_field.find(dd->grp) != std::string::npos) break;
304 for (oo = obscure; oo->grp != NULL; oo++)
305 if (_field.find(oo->grp) != std::string::npos) break;
306 for (xx = misc; xx->grp != NULL; xx++)
307 if (_field.find(xx->grp) != std::string::npos) break;
308 if (ii->grp != NULL) _conditions.append(ii->name).append(" ");
309 if (dd->grp != NULL) _conditions.append(dd->name).append(" ");
310 _conditions.append(pp->name);
311 if (oo->grp != NULL) _conditions.append(", ").append(oo->name);
312 if (xx->grp != NULL) _conditions.append(", ").append(xx->name);
313 parsed = true;
314 }
315 }
316 } if (!parsed) {
317 wxpairs *oo;
318 for (oo = obscure; oo->grp != NULL; oo++)
319 if (_field.find(oo->grp) != std::string::npos) break;
320 if (oo->grp != NULL) {
321 _conditions.append(" ").append(oo->name);
322 parsed = true;
323 }
324 } if (!parsed) {
325 // parse for cloud cover
326 // use only the first occurance of sky cover report; it is lowest altitude
327 // cloud cover is reported multiple times for sounding stations
328 for (wxpairs *cc = clouds; cc->grp != NULL; cc++) {
329 if (_field.find(cc->grp) != std::string::npos) {
330 if (_conditions.find(cc->name) != std::string::npos) break;
331 if (_conditions.empty())
332 _conditions.append(cc->name);
333 else
334 _conditions.append(", ").append(cc->name);
335 wxpairs *ct;
336 for (ct = cloud_type; ct->grp != NULL; ct++) {
337 if (_field.find(ct->grp) != std::string::npos) {
338 if (ct->grp != NULL)
339 _conditions.append(" ").append(ct->name);
340 break;
341 }
342 }
343 parsed = true;
344 break;
345 }
346 }
347 }
348 }
349
350 _wx_text_parsed.clear();
351 if (_name && !_station_name.empty()) {
352 _wx_text_parsed.append("Loc: ").append(_station_name).append("\n");
353 }
354 if (_condx && !_conditions.empty()) {
355 _wx_text_parsed.append("Cond: ").append(_conditions).append("\n");
356 }
357 if ((_mph || _kph) && !_winds.empty()){
358 _wx_text_parsed.append("Wind: ").append(_winds).append("\n");
359 }
360 if ((_fahrenheit || _celsius) && !_temp.empty() ) {
361 _wx_text_parsed.append("Temp: ").append(_temp).append("\n");
362 }
363 if ((_inches || _mbars) && !_baro.empty()) {
364 _wx_text_parsed.append("Baro: ").append(_baro).append("\n");
365 }
366
367 return;
368
369 }
370
get()371 int Metar::get()
372 {
373 std::string metar_url = "https://tgftp.nws.noaa.gov/data/observations/metar/decoded/";
374 metar_url.append(_metar_station).append(".TXT");
375 int ret = url.get(metar_url, _metar_text);
376 if (ret == 0) parse();
377 return ret;
378 }
379