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