]> git.mxchange.org Git - simgear.git/blob - simgear/environment/metar.cxx
182a3995f2d8138944e415e04831c840a178d027
[simgear.git] / simgear / environment / metar.cxx
1 // metar interface class
2 //
3 // Written by Melchior FRANZ, started December 2003.
4 //
5 // Copyright (C) 2003  Melchior FRANZ - mfranz@aon.at
6 //
7 // This program is free software; you can redistribute it and/or
8 // modify it under the terms of the GNU General Public License as
9 // published by the Free Software Foundation; either version 2 of the
10 // License, or (at your option) any later version.
11 //
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 // General Public License for more details.
16 //
17 // You should have received a copy of the GNU General Public License
18 // along with this program; if not, write to the Free Software
19 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20 //
21 // $Id$
22
23 /**
24  * @file metar.cxx
25  * Interface for encoded Meteorological Aerodrome Reports (METAR).
26  */
27 #ifdef HAVE_CONFIG_H
28 #  include <simgear_config.h>
29 #endif
30
31 #include <string>
32 #include <time.h>
33 #include <cstring>
34
35 #include <simgear/io/sg_socket.hxx>
36 #include <simgear/debug/logstream.hxx>
37 #include <simgear/structure/exception.hxx>
38
39 #include "metar.hxx"
40
41 #define NaN SGMetarNaN
42
43 /**
44  * The constructor takes a Metar string, or a four-letter ICAO code. In the
45  * latter case the metar string is downloaded from
46  * http://weather.noaa.gov/pub/data/observations/metar/stations/.
47  * The constructor throws sg_io_exceptions on failure. The "METAR"
48  * keyword has no effect (apart from incrementing the group counter
49  * @a grpcount) and can be left away. A keyword "SPECI" is
50  * likewise accepted.
51  *
52  * @param m     ICAO station id or metar string
53  * @param proxy proxy host (optional; default: "")
54  * @param port  proxy port (optional; default: "80")
55  * @param auth  proxy authorization information (optional; default: "")
56  *
57  * @par Examples:
58  * @code
59  * SGMetar *m = new SGMetar("METAR KSFO 061656Z 19004KT 9SM SCT100 OVC200 08/03 A3013");
60  * double t = m->getTemperature_F();
61  * delete m;
62  *
63  * SGMetar n("KSFO", "proxy.provider.foo", "3128", "proxy-password");
64  * double d = n.getDewpoint_C();
65  * @endcode
66  */
67 SGMetar::SGMetar(const string& m, const string& proxy, const string& port,
68                 const string& auth, const time_t time) :
69         _grpcount(0),
70         _x_proxy(false),
71         _year(-1),
72         _month(-1),
73         _day(-1),
74         _hour(-1),
75         _minute(-1),
76         _report_type(-1),
77         _wind_dir(-1),
78         _wind_speed(NaN),
79         _gust_speed(NaN),
80         _wind_range_from(-1),
81         _wind_range_to(-1),
82         _temp(NaN),
83         _dewp(NaN),
84         _pressure(NaN),
85         _rain(false),
86         _hail(false),
87         _snow(false),
88         _cavok(false),
89         _intensity(0)
90 {
91         if (m.length() == 4 && isalnum(m[0]) && isalnum(m[1]) && isalnum(m[2]) && isalnum(m[3])) {
92                 for (int i = 0; i < 4; i++)
93                         _icao[i] = toupper(m[i]);
94                 _icao[4] = '\0';
95                 _data = loadData(_icao, proxy, port, auth, time);
96         } else {
97                 _data = new char[m.length() + 2];       // make room for " \0"
98                 strcpy(_data, m.c_str());
99                 _url = _data;
100         }
101         normalizeData();
102
103         _m = _data;
104         _icao[0] = '\0';
105
106         // NOAA preample
107         if (!scanPreambleDate())
108                 useCurrentDate();
109         scanPreambleTime();
110
111         // METAR header
112         scanType();
113         if (!scanId() || !scanDate()) {
114                 delete[] _data;
115                 throw sg_io_exception("metar data bogus ", sg_location(_url));
116         }
117         scanModifier();
118
119         // base set
120         scanWind();
121         scanVariability();
122         while (scanVisibility()) ;
123         while (scanRwyVisRange()) ;
124         while (scanWeather()) ;
125         while (scanSkyCondition()) ;
126         scanTemperature();
127         scanPressure();
128         while (scanSkyCondition()) ;
129         while (scanRunwayReport()) ;
130         scanWindShear();
131
132         // appendix
133         while (scanColorState()) ;
134         scanTrendForecast();
135         while (scanRunwayReport()) ;
136         scanRemainder();
137         scanRemark();
138
139         if (_grpcount < 4) {
140                 delete[] _data;
141                 throw sg_io_exception("metar data incomplete ", sg_location(_url));
142         }
143
144         _url = "";
145 }
146
147
148 /**
149   * Clears lists and maps to discourage access after destruction.
150   */
151 SGMetar::~SGMetar()
152 {
153         _clouds.clear();
154         _runways.clear();
155         _weather.clear();
156         delete[] _data;
157 }
158
159
160 void SGMetar::useCurrentDate()
161 {
162         struct tm now;
163         time_t now_sec = time(0);
164 #ifdef _WIN32
165         now = *gmtime(&now_sec);
166 #else
167         gmtime_r(&now_sec, &now);
168 #endif
169         _year = now.tm_year + 1900;
170         _month = now.tm_mon + 1;
171 }
172
173
174 /**
175   * If called with "KSFO" loads data from
176   * @code
177   * http://weather.noaa.gov/pub/data/observations/metar/stations/KSFO.TXT.
178   * @endcode
179   * Throws sg_io_exception on failure. Gives up after waiting longer than 10 seconds.
180   *
181   * @param id four-letter ICAO Metar station code, e.g. "KSFO".
182   * @param proxy proxy host (optional; default: "")
183   * @param port  proxy port (optional; default: "80")
184   * @param auth  proxy authorization information (optional; default: "")
185   * @return pointer to Metar data string, allocated by new char[].
186   * @see rfc2068.txt for proxy spec ("Proxy-Authorization")
187   */
188 char *SGMetar::loadData(const char *id, const string& proxy, const string& port,
189                 const string& auth, time_t time)
190 {
191         const int buflen = 512;
192         char buf[2 * buflen];
193
194         string metar_server = "weather.noaa.gov";
195         string host = proxy.empty() ? metar_server : proxy;
196         string path = "/pub/data/observations/metar/stations/";
197
198         path += string(id) + ".TXT";
199         _url = "http://" + metar_server + path;
200
201         SGSocket *sock = new SGSocket(host, port.empty() ? "80" : port, "tcp");
202         sock->set_timeout(10000);
203         if (!sock->open(SG_IO_OUT)) {
204                 delete sock;
205                 throw sg_io_exception("cannot connect to ", sg_location(host));
206         }
207
208         string get = "GET ";
209         if (!proxy.empty())
210                 get += "http://" + metar_server;
211
212         sprintf(buf, "%ld", time);
213         get += path + " HTTP/1.0\015\012X-Time: " + buf + "\015\012";
214         get += "Host: " + metar_server + "\015\012";
215
216         if (!auth.empty())
217                 get += "Proxy-Authorization: " + auth + "\015\012";
218
219         get += "\015\012";
220         sock->writestring(get.c_str());
221
222         int i;
223
224         // skip HTTP header
225         while ((i = sock->readline(buf, buflen))) {
226                 if (i <= 2 && isspace(buf[0]) && (!buf[1] || isspace(buf[1])))
227                         break;
228                 if (!strncmp(buf, "X-MetarProxy: ", 13))
229                         _x_proxy = true;
230         }
231         if (i) {
232                 i = sock->readline(buf, buflen);
233                 if (i)
234                         sock->readline(&buf[i], buflen);
235         }
236
237         sock->close();
238         delete sock;
239
240         char *b = buf;
241         scanBoundary(&b);
242         if (*b == '<')
243                 throw sg_io_exception("no metar data available from ", 
244                                 sg_location(_url));
245
246         char *metar = new char[strlen(b) + 2];  // make room for " \0"
247         strcpy(metar, b);
248         return metar;
249 }
250
251
252 /**
253   * Replace any number of subsequent spaces by just one space, and add
254   * a trailing space. This makes scanning for things like "ALL RWY" easier.
255   */
256 void SGMetar::normalizeData()
257 {
258         char *src, *dest;
259         for (src = dest = _data; (*dest++ = *src++); )
260                 while (*src == ' ' && src[1] == ' ')
261                         src++;
262         for (dest--; isspace(*--dest); ) ;
263         *++dest = ' ';
264         *++dest = '\0';
265 }
266
267
268 // \d\d\d\d/\d\d/\d\d
269 bool SGMetar::scanPreambleDate()
270 {
271         char *m = _m;
272         int year, month, day;
273         if (!scanNumber(&m, &year, 4))
274                 return false;
275         if (*m++ != '/')
276                 return false;
277         if (!scanNumber(&m, &month, 2))
278                 return false;
279         if (*m++ != '/')
280                 return false;
281         if (!scanNumber(&m, &day, 2))
282                 return false;
283         if (!scanBoundary(&m))
284                 return false;
285         _year = year;
286         _month = month;
287         _day = day;
288         _m = m;
289         return true;
290 }
291
292
293 // \d\d:\d\d
294 bool SGMetar::scanPreambleTime()
295 {
296         char *m = _m;
297         int hour, minute;
298         if (!scanNumber(&m, &hour, 2))
299                 return false;
300         if (*m++ != ':')
301                 return false;
302         if (!scanNumber(&m, &minute, 2))
303                 return false;
304         if (!scanBoundary(&m))
305                 return false;
306         _hour = hour;
307         _minute = minute;
308         _m = m;
309         return true;
310 }
311
312
313 // (METAR|SPECI)
314 bool SGMetar::scanType()
315 {
316         if (strncmp(_m, "METAR ", 6) && strncmp(_m, "SPECI ", 6))
317                 return false;
318         _m += 6;
319         _grpcount++;
320         return true;
321 }
322
323
324 // [A-Z]{4}
325 bool SGMetar::scanId()
326 {
327         char *m = _m;
328         for (int i = 0; i < 4; m++, i++)
329                 if (!(isalpha(*m) || isdigit(*m)))
330                         return false;
331         if (!scanBoundary(&m))
332                 return false;
333         strncpy(_icao, _m, 4);
334         _icao[4] = '\0';
335         _m = m;
336         _grpcount++;
337         return true;
338 }
339
340
341 // \d{6}Z
342 bool SGMetar::scanDate()
343 {
344         char *m = _m;
345         int day, hour, minute;
346         if (!scanNumber(&m, &day, 2))
347                 return false;
348         if (!scanNumber(&m, &hour, 2))
349                 return false;
350         if (!scanNumber(&m, &minute, 2))
351                 return false;
352         if (*m++ != 'Z')
353                 return false;
354         if (!scanBoundary(&m))
355                 return false;
356         _day = day;
357         _hour = hour;
358         _minute = minute;
359         _m = m;
360         _grpcount++;
361         return true;
362 }
363
364
365 // (NIL|AUTO|COR|RTD)
366 bool SGMetar::scanModifier()
367 {
368         char *m = _m;
369         int type;
370         if (!strncmp(m, "NIL", 3)) {
371                 _m += strlen(_m);
372                 return true;
373         }
374         if (!strncmp(m, "AUTO", 4))                     // automatically generated
375                 m += 4, type = AUTO;
376         else if (!strncmp(m, "COR", 3))                 // manually corrected
377                 m += 3, type = COR;
378         else if (!strncmp(m, "RTD", 3))                 // routine delayed
379                 m += 3, type = RTD;
380         else
381                 return false;
382         if (!scanBoundary(&m))
383                 return false;
384         _report_type = type;
385         _m = m;
386         _grpcount++;
387         return true;
388 }
389
390
391 // (\d{3}|VRB)\d{1,3}(G\d{2,3})?(KT|KMH|MPS)
392 bool SGMetar::scanWind()
393 {
394         char *m = _m;
395         int dir;
396         if (!strncmp(m, "VRB", 3))
397                 m += 3, dir = -1;
398         else if (!scanNumber(&m, &dir, 3))
399                 return false;
400
401         int i;
402         if (!scanNumber(&m, &i, 2, 3))
403                 return false;
404         double speed = i;
405
406         double gust = NaN;
407         if (*m == 'G') {
408                 m++;
409                 if (!scanNumber(&m, &i, 2, 3))
410                         return false;
411                 gust = i;
412         }
413         double factor;
414         if (!strncmp(m, "KT", 2))
415                 m += 2, factor = SG_KT_TO_MPS;
416         else if (!strncmp(m, "KMH", 3))
417                 m += 3, factor = SG_KMH_TO_MPS;
418         else if (!strncmp(m, "KPH", 3))         // ??
419                 m += 3, factor = SG_KMH_TO_MPS;
420         else if (!strncmp(m, "MPS", 3))
421                 m += 3, factor = 1.0;
422         else
423                 return false;
424         if (!scanBoundary(&m))
425                 return false;
426         _m = m;
427         _wind_dir = dir;
428         _wind_speed = speed * factor;
429         if (gust != NaN)
430                 _gust_speed = gust * factor;
431         _grpcount++;
432         return true;
433 }
434
435
436 // \d{3}V\d{3}
437 bool SGMetar::scanVariability()
438 {
439         char *m = _m;
440         int from, to;
441         if (!scanNumber(&m, &from, 3))
442                 return false;
443         if (*m++ != 'V')
444                 return false;
445         if (!scanNumber(&m, &to, 3))
446                 return false;
447         if (!scanBoundary(&m))
448                 return false;
449         _m = m;
450         _wind_range_from = from;
451         _wind_range_to = to;
452         _grpcount++;
453         return true;
454 }
455
456
457 bool SGMetar::scanVisibility()
458 // TODO: if only directed vis are given, do still set min/max
459 {
460         if (!strncmp(_m, "//// ", 5)) {         // spec compliant?
461                 _m += 5;
462                 _grpcount++;
463                 return true;
464         }
465
466         char *m = _m;
467         double distance;
468         int i, dir = -1;
469         int modifier = SGMetarVisibility::EQUALS;
470 // \d{4}(N|NE|E|SE|S|SW|W|NW)?
471         if (scanNumber(&m, &i, 4)) {
472                 if( strncmp( m, "NDV",3 ) == 0 ) {
473                         m+=3; // tolerate NDV (no directional validation)
474                 } else if (*m == 'E') {
475                         m++, dir = 90;
476                 } else if (*m == 'W') {
477                         m++, dir = 270;
478                 } else if (*m == 'N') {
479                         m++;
480                         if (*m == 'E')
481                                 m++, dir = 45;
482                         else if (*m == 'W')
483                                 m++, dir = 315;
484                         else
485                                 dir = 0;
486                 } else if (*m == 'S') {
487                         m++;
488                         if (*m == 'E')
489                                 m++, dir = 135;
490                         else if (*m == 'W')
491                                 m++, dir = 225;
492                         else
493                                 dir = 180;
494                 }
495                 if (i == 0)
496                         i = 50, modifier = SGMetarVisibility::LESS_THAN;
497                 else if (i == 9999)
498                         i++, modifier = SGMetarVisibility::GREATER_THAN;
499                 distance = i;
500         } else {
501 // M?(\d{1,2}|\d{1,2}/\d{1,2}|\d{1,2} \d{1,2}/\d{1,2})(SM|KM)
502                 if (*m == 'M')
503                         m++, modifier = SGMetarVisibility::LESS_THAN;
504
505                 if (!scanNumber(&m, &i, 1, 2))
506                         return false;
507                 distance = i;
508
509                 if (*m == '/') {
510                         m++;
511                         if (!scanNumber(&m, &i, 1, 2))
512                                 return false;
513                         distance /= i;
514                 } else if (*m == ' ') {
515                         m++;
516                         int denom;
517                         if (!scanNumber(&m, &i, 1, 2))
518                                 return false;
519                         if (*m++ != '/')
520                                 return false;
521                         if (!scanNumber(&m, &denom, 1, 2))
522                                 return false;
523                         distance += (double)i / denom;
524                 }
525
526                 if (!strncmp(m, "SM", 2))
527                         distance *= SG_SM_TO_METER, m += 2;
528                 else if (!strncmp(m, "KM", 2))
529                         distance *= 1000, m += 2;
530                 else
531                         return false;
532         }
533         if (!scanBoundary(&m))
534                 return false;
535
536         SGMetarVisibility *v;
537         if (dir != -1)
538                 v = &_dir_visibility[dir / 45];
539         else if (_min_visibility._distance == NaN)
540                 v = &_min_visibility;
541         else
542                 v = &_max_visibility;
543
544         v->_distance = distance;
545         v->_modifier = modifier;
546         v->_direction = dir;
547         _m = m;
548         _grpcount++;
549         return true;
550 }
551
552
553 // R\d\d[LCR]?/([PM]?\d{4}V)?[PM]?\d{4}(FT)?[DNU]?
554 bool SGMetar::scanRwyVisRange()
555 {
556         char *m = _m;
557         int i;
558         SGMetarRunway r;
559         if (*m++ != 'R')
560                 return false;
561         if (!scanNumber(&m, &i, 2))
562                 return false;
563         if (*m == 'L' || *m == 'C' || *m == 'R')
564                 m++;
565
566         char id[4];
567         strncpy(id, _m + 1, i = m - _m - 1);
568         id[i] = '\0';
569
570         if (*m++ != '/')
571                 return false;
572
573         int from, to;
574         if (*m == 'P')
575                 m++, r._min_visibility._modifier = SGMetarVisibility::GREATER_THAN;
576         else if (*m == 'M')
577                 m++, r._min_visibility._modifier = SGMetarVisibility::LESS_THAN;
578         if (!scanNumber(&m, &from, 4))
579                 return false;
580         if (*m == 'V') {
581                 m++;
582                 if (*m == 'P')
583                         m++, r._max_visibility._modifier = SGMetarVisibility::GREATER_THAN;
584                 else if (*m == 'M')
585                         m++, r._max_visibility._modifier = SGMetarVisibility::LESS_THAN;
586                 if (!scanNumber(&m, &to, 4))
587                         return false;
588         } else
589                 to = from;
590
591         if (!strncmp(m, "FT", 2)) {
592                 from = int(from * SG_FEET_TO_METER);
593                 to = int(to * SG_FEET_TO_METER);
594                 m += 2;
595         }
596         r._min_visibility._distance = from;
597         r._max_visibility._distance = to;
598
599         if (*m == '/')                                  // this is not in the spec!
600                 *m++;
601         if (*m == 'D')
602                 m++, r._min_visibility._tendency = SGMetarVisibility::DECREASING;
603         else if (*m == 'N')
604                 m++, r._min_visibility._tendency = SGMetarVisibility::STABLE;
605         else if (*m == 'U')
606                 m++, r._min_visibility._tendency = SGMetarVisibility::INCREASING;
607
608         if (!scanBoundary(&m))
609                 return false;
610         _m = m;
611
612         _runways[id]._min_visibility = r._min_visibility;
613         _runways[id]._max_visibility = r._max_visibility;
614         _grpcount++;
615         return true;
616 }
617
618
619 static const struct Token special[] = {
620         { "NSW",  "no significant weather" },
621         { "VCSH", "showers in the vicinity" },
622         { "VCTS", "thunderstorm in the vicinity" },
623         { 0, 0 }
624 };
625
626
627 static const struct Token description[] = {
628         { "SH", "showers of" },
629         { "TS", "thunderstorm with" },
630         { "BC", "patches of" },
631         { "BL", "blowing" },
632         { "DR", "low drifting" },
633         { "FZ", "freezing" },
634         { "MI", "shallow" },
635         { "PR", "partial" },
636         { 0, 0 }
637 };
638
639
640 static const struct Token phenomenon[] = {
641         { "DZ",   "drizzle" },
642         { "GR",   "hail" },
643         { "GS",   "small hail and/or snow pellets" },
644         { "IC",   "ice crystals" },
645         { "PE",   "ice pellets" },
646         { "RA",   "rain" },
647         { "SG",   "snow grains" },
648         { "SN",   "snow" },
649         { "UP",   "unknown precipitation" },
650         { "BR",   "mist" },
651         { "DU",   "widespread dust" },
652         { "FG",   "fog" },
653         { "FGBR", "fog bank" },
654         { "FU",   "smoke" },
655         { "HZ",   "haze" },
656         { "PY",   "spray" },
657         { "SA",   "sand" },
658         { "VA",   "volcanic ash" },
659         { "DS",   "duststorm" },
660         { "FC",   "funnel cloud/tornado waterspout" },
661         { "PO",   "well-developed dust/sand whirls" },
662         { "SQ",   "squalls" },
663         { "SS",   "sandstorm" },
664         { "UP",   "unknown" },  // ... due to failed automatic acquisition
665         { 0, 0 }
666 };
667
668
669 // (+|-|VC)?(NSW|MI|PR|BC|DR|BL|SH|TS|FZ)?((DZ|RA|SN|SG|IC|PE|GR|GS|UP){0,3})(BR|FG|FU|VA|DU|SA|HZ|PY|PO|SQ|FC|SS|DS){0,3}
670 bool SGMetar::scanWeather()
671 {
672         char *m = _m;
673         string weather;
674         const struct Token *a;
675         if ((a = scanToken(&m, special))) {
676                 if (!scanBoundary(&m))
677                         return false;
678                 _weather.push_back(a->text);
679                 _m = m;
680                 return true;
681         }
682
683         string pre, post;
684         if (*m == '-')
685                 m++, pre = "light ", _intensity = 1;
686         else if (*m == '+')
687                 m++, pre = "heavy ", _intensity = 3;
688         else if (!strncmp(m, "VC", 2))
689                 m += 2, post = "in the vicinity ";
690         else
691                 pre = "moderate ", _intensity = 2;
692
693         vector<string> descriptions;
694         int i;
695         for (i = 0; i < 3; i++) {
696                 if (!(a = scanToken(&m, description)))
697                         break;
698                 descriptions.push_back(a->id);
699                 weather += string(a->text) + " ";
700         }
701
702         for (i = 0; i < 3; i++) {
703                 if (!(a = scanToken(&m, phenomenon)))
704                         break;
705                 _phenomena[a->id] = descriptions;
706                 weather += string(a->text) + " ";
707                 if (!strcmp(a->id, "RA"))
708                         _rain = _intensity;
709                 else if (!strcmp(a->id, "HA"))
710                         _hail = _intensity;
711                 else if (!strcmp(a->id, "SN"))
712                         _snow = _intensity;
713         }
714         if (!weather.length())
715                 return false;
716         if (!scanBoundary(&m))
717                 return false;
718         _m = m;
719         weather = pre + weather + post;
720         weather.erase(weather.length() - 1);
721         _weather.push_back(weather);
722         _grpcount++;
723         return true;
724 }
725
726
727 static const struct Token cloud_types[] = {
728         { "AC",    "altocumulus" },
729         { "ACC",   "altocumulus castellanus" },
730         { "ACSL",  "altocumulus standing lenticular" },
731         { "AS",    "altostratus" },
732         { "CB",    "cumulonimbus" },
733         { "CBMAM", "cumulonimbus mammatus" },
734         { "CC",    "cirrocumulus" },
735         { "CCSL",  "cirrocumulus standing lenticular" },
736         { "CI",    "cirrus" },
737         { "CS",    "cirrostratus" },
738         { "CU",    "cumulus" },
739         { "CUFRA", "cumulus fractus" },
740         { "NS",    "nimbostratus" },
741         { "SAC",   "stratoaltocumulus" },               // guessed
742         { "SC",    "stratocumulus" },
743         { "SCSL",  "stratocumulus standing lenticular" },
744         { "ST",    "stratus" },
745         { "STFRA", "stratus fractus" },
746         { "TCU",   "towering cumulus" },
747         { 0, 0 }
748 };
749
750 #include <iostream>
751 // (FEW|SCT|BKN|OVC|SKC|CLR|CAVOK|VV)([0-9]{3}|///)?[:cloud_type:]?
752 bool SGMetar::scanSkyCondition()
753 {
754         char *m = _m;
755         int i;
756         SGMetarCloud cl;
757
758         if (!strncmp(m, "//////", 6)) {
759                 m += 6;
760                 if (!scanBoundary(&m))
761                         return false;
762                 _m = m;
763                 return true;
764         }
765
766         if (!strncmp(m, "CLR", i = 3)                           // clear
767                         || !strncmp(m, "SKC", i = 3)            // sky clear
768                         || !strncmp(m, "NSC", i = 3)            // no significant clouds
769                         || !strncmp(m, "CAVOK", i = 5)) {       // ceiling and visibility OK (implies 9999)
770                 m += i;
771                 if (!scanBoundary(&m))
772                         return false;
773
774                 if (i == 3) {
775                         cl._coverage = 0;
776                         _clouds.push_back(cl);
777                 } else {
778                         _cavok = true;
779                 }
780                 _m = m;
781                 return true;
782         }
783
784         if (!strncmp(m, "VV", i = 2))                           // vertical visibility
785                 ;
786         else if (!strncmp(m, "FEW", i = 3))
787                 cl._coverage = 1;
788         else if (!strncmp(m, "SCT", i = 3))
789                 cl._coverage = 2;
790         else if (!strncmp(m, "BKN", i = 3))
791                 cl._coverage = 3;
792         else if (!strncmp(m, "OVC", i = 3))
793                 cl._coverage = 4;
794         else
795                 return false;
796         m += i;
797
798         if (!strncmp(m, "///", 3))      // vis not measurable (e.g. because of heavy snowing)
799                 m += 3, i = -1;
800         else if (scanBoundary(&m)) {
801                 _m = m;
802                 return true;                            // ignore single OVC/BKN/...
803         } else if (!scanNumber(&m, &i, 3))
804                 i = -1;
805
806         if (cl._coverage == -1) {
807                 if (!scanBoundary(&m))
808                         return false;
809                 if (i == -1)                    // 'VV///'
810                         _vert_visibility._modifier = SGMetarVisibility::NOGO;
811                 else
812                         _vert_visibility._distance = i * 100 * SG_FEET_TO_METER;
813                 _m = m;
814                 return true;
815         }
816
817         if (i != -1)
818                 cl._altitude = i * 100 * SG_FEET_TO_METER;
819
820         const struct Token *a;
821         if ((a = scanToken(&m, cloud_types))) {
822                 cl._type = a->id;
823                 cl._type_long = a->text;
824         }
825         if (!scanBoundary(&m))
826                 return false;
827         _clouds.push_back(cl);
828         _m = m;
829         _grpcount++;
830         return true;
831 }
832
833
834 // M?[0-9]{2}/(M?[0-9]{2})?            (spec)
835 // (M?[0-9]{2}|XX)/(M?[0-9]{2}|XX)?    (Namibia)
836 bool SGMetar::scanTemperature()
837 {
838         char *m = _m;
839         int sign = 1, temp, dew;
840         if (!strncmp(m, "XX/XX", 5)) {          // not spec compliant!
841                 _m += 5;
842                 return scanBoundary(&_m);
843         }
844
845         if (*m == 'M')
846                 m++, sign = -1;
847         if (!scanNumber(&m, &temp, 2))
848                 return false;
849         temp *= sign;
850
851         if (*m++ != '/')
852                 return false;
853         if (!scanBoundary(&m)) {
854                 if (!strncmp(m, "XX", 2))       // not spec compliant!
855                         m += 2, sign = 0, dew = temp;
856                 else {
857                         sign = 1;
858                         if (*m == 'M')
859                                 m++, sign = -1;
860                         if (!scanNumber(&m, &dew, 2))
861                                 return false;
862                 }
863                 if (!scanBoundary(&m))
864                         return false;
865                 if (sign)
866                         _dewp = sign * dew;
867         }
868         _temp = temp;
869         _m = m;
870         _grpcount++;
871         return true;
872 }
873
874
875 double SGMetar::getRelHumidity() const
876 {
877         if (_temp == NaN || _dewp == NaN)
878                 return NaN;
879         double dewp = pow(10.0, 7.5 * _dewp / (237.7 + _dewp));
880         double temp = pow(10.0, 7.5 * _temp / (237.7 + _temp));
881         return dewp * 100 / temp;
882 }
883
884
885 // [AQ]\d{4}             (spec)
886 // [AQ]\d{2}(\d{2}|//)   (Namibia)
887 bool SGMetar::scanPressure()
888 {
889         char *m = _m;
890         double factor;
891         int press, i;
892
893         if (*m == 'A')
894                 factor = SG_INHG_TO_PA / 100;
895         else if (*m == 'Q')
896                 factor = 100;
897         else
898                 return false;
899         m++;
900         if (!scanNumber(&m, &press, 2))
901                 return false;
902         press *= 100;
903         if (!strncmp(m, "//", 2))       // not spec compliant!
904                 m += 2;
905         else if (scanNumber(&m, &i, 2))
906                 press += i;
907         else
908                 return false;
909         if (!scanBoundary(&m))
910                 return false;
911         _pressure = press * factor;
912         _m = m;
913         _grpcount++;
914         return true;
915 }
916
917
918 static const char *runway_deposit[] = {
919         "clear and dry",
920         "damp",
921         "wet or puddles",
922         "frost",
923         "dry snow",
924         "wet snow",
925         "slush",
926         "ice",
927         "compacted snow",
928         "frozen ridges"
929 };
930
931
932 static const char *runway_deposit_extent[] = {
933         0, "1-10%", "11-25%", 0, 0, "26-50%", 0, 0, 0, "51-100%"
934 };
935
936
937 static const char *runway_friction[] = {
938         0,
939         "poor braking action",
940         "poor/medium braking action",
941         "medium braking action",
942         "medium/good braking action",
943         "good braking action",
944         0, 0, 0,
945         "friction: unreliable measurement"
946 };
947
948
949 // \d\d(CLRD|[\d/]{4})(\d\d|//)
950 bool SGMetar::scanRunwayReport()
951 {
952         char *m = _m;
953         int i;
954         char id[4];
955         SGMetarRunway r;
956
957         if (!scanNumber(&m, &i, 2))
958                 return false;
959         if (i == 88)
960                 strcpy(id, "ALL");
961         else if (i == 99)
962                 strcpy(id, "REP");              // repetition of previous report
963         else if (i >= 50) {
964                 i -= 50;
965                 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = 'R', id[3] = '\0';
966         } else
967                 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = '\0';
968
969         if (!strncmp(m, "CLRD", 4)) {
970                 m += 4;                                                 // runway cleared
971                 r._deposit_string = "cleared";
972         } else {
973                 if (scanNumber(&m, &i, 1)) {
974                         r._deposit = i;
975                         r._deposit_string = runway_deposit[i];
976                 } else if (*m == '/')
977                         m++;
978                 else
979                         return false;
980
981                 if (*m == '1' || *m == '2' || *m == '5' || *m == '9') { // extent of deposit
982                         r._extent = *m - '0';
983                         r._extent_string = runway_deposit_extent[*m - '0'];
984                 } else if (*m != '/')
985                         return false;
986
987                 m++;
988                 i = -1;
989                 if (!strncmp(m, "//", 2))
990                         m += 2;
991                 else if (!scanNumber(&m, &i, 2))
992                         return false;
993
994                 if (i == 0)
995                         r._depth = 0.0005;                              // < 1 mm deep (let's say 0.5 :-)
996                 else if (i > 0 && i <= 90)
997                         r._depth = i / 1000.0;                          // i mm deep
998                 else if (i >= 92 && i <= 98)
999                         r._depth = (i - 90) / 20.0;
1000                 else if (i == 99)
1001                         r._comment = "runway not in use";
1002                 else if (i == -1)                                       // no depth given ("//")
1003                         ;
1004                 else
1005                         return false;
1006         }
1007         i = -1;
1008         if (m[0] == '/' && m[1] == '/')
1009                 m += 2;
1010         else if (!scanNumber(&m, &i, 2))
1011                 return false;
1012         if (i >= 1 && i < 90) {
1013                 r._friction = i / 100.0;
1014         } else if ((i >= 91 && i <= 95) || i == 99) {
1015                 r._friction_string = runway_friction[i - 90];
1016         }
1017         if (!scanBoundary(&m))
1018                 return false;
1019
1020         _runways[id]._deposit = r._deposit;
1021         _runways[id]._deposit_string = r._deposit_string;
1022         _runways[id]._extent = r._extent;
1023         _runways[id]._extent_string = r._extent_string;
1024         _runways[id]._depth = r._depth;
1025         _runways[id]._friction = r._friction;
1026         _runways[id]._friction_string = r._friction_string;
1027         _runways[id]._comment = r._comment;
1028         _m = m;
1029         _grpcount++;
1030         return true;
1031 }
1032
1033
1034 // WS (ALL RWYS?|RWY ?\d\d[LCR]?)?
1035 bool SGMetar::scanWindShear()
1036 {
1037         char *m = _m;
1038         if (strncmp(m, "WS", 2))
1039                 return false;
1040         m += 2;
1041         if (!scanBoundary(&m))
1042                 return false;
1043
1044         if (!strncmp(m, "ALL", 3)) {
1045                 m += 3;
1046                 if (!scanBoundary(&m))
1047                         return false;
1048                 if (strncmp(m, "RWY", 3))
1049                         return false;
1050                 m += 3;
1051                 if (*m == 'S')
1052                         m++;
1053                 if (!scanBoundary(&m))
1054                         return false;
1055                 _runways["ALL"]._wind_shear = true;
1056                 _m = m;
1057                 return true;
1058         }
1059
1060         char id[4], *mm;
1061         int i, cnt;
1062         for (cnt = 0;; cnt++) {                 // ??
1063                 if (strncmp(m, "RWY", 3))
1064                         break;
1065                 m += 3;
1066                 scanBoundary(&m);
1067                 mm = m;
1068                 if (!scanNumber(&m, &i, 2))
1069                         return false;
1070                 if (*m == 'L' || *m == 'C' || *m == 'R')
1071                         m++;
1072                 strncpy(id, mm, i = m - mm);
1073                 id[i] = '\0';
1074                 if (!scanBoundary(&m))
1075                         return false;
1076                 _runways[id]._wind_shear = true;
1077         }
1078         if (!cnt)
1079                 _runways["ALL"]._wind_shear = true;
1080         _m = m;
1081         return true;
1082 }
1083
1084
1085 bool SGMetar::scanTrendForecast()
1086 {
1087         char *m = _m;
1088         if (strncmp(m, "NOSIG", 5))
1089                 return false;
1090
1091         m += 5;
1092         if (!scanBoundary(&m))
1093                 return false;
1094         _m = m;
1095         return true;
1096 }
1097
1098
1099 // (BLU|WHT|GRN|YLO|AMB|RED)
1100 static const struct Token colors[] = {
1101         { "BLU", "Blue" },      // 2500 ft,  8.0 km
1102         { "WHT", "White" },     // 1500 ft,  5.0 km
1103         { "GRN", "Green" },     //  700 ft,  3.7 km
1104         { "YLO", "Yellow" },    //  300 ft,  1.6 km
1105         { "AMB", "Amber" },     //  200 ft,  0.8 km
1106         { "RED", "Red" },       // <200 ft, <0.8 km
1107         { 0, 0 }
1108 };
1109
1110
1111 bool SGMetar::scanColorState()
1112 {
1113         char *m = _m;
1114         const struct Token *a;
1115         if (!(a = scanToken(&m, colors)))
1116                 return false;
1117         if (!scanBoundary(&m))
1118                 return false;
1119         //printf(Y"Code %s\n"N, a->text);
1120         _m = m;
1121         return true;
1122 }
1123
1124
1125 bool SGMetar::scanRemark()
1126 {
1127         if (strncmp(_m, "RMK", 3))
1128                 return false;
1129         _m += 3;
1130         if (!scanBoundary(&_m))
1131                 return false;
1132
1133         while (*_m) {
1134                 if (!scanRunwayReport()) {
1135                         while (*_m && !isspace(*_m))
1136                                 _m++;
1137                         scanBoundary(&_m);
1138                 }
1139         }
1140         return true;
1141 }
1142
1143
1144 bool SGMetar::scanRemainder()
1145 {
1146         char *m = _m;
1147         if (!(strncmp(m, "NOSIG", 5))) {
1148                 m += 5;
1149                 if (scanBoundary(&m))
1150                         _m = m; //_comment.push_back("No significant tendency");
1151         }
1152
1153         if (!scanBoundary(&m))
1154                 return false;
1155         _m = m;
1156         return true;
1157 }
1158
1159
1160 bool SGMetar::scanBoundary(char **s)
1161 {
1162         if (**s && !isspace(**s))
1163                 return false;
1164         while (isspace(**s))
1165                 (*s)++;
1166         return true;
1167 }
1168
1169
1170 int SGMetar::scanNumber(char **src, int *num, int min, int max)
1171 {
1172         int i;
1173         char *s = *src;
1174         *num = 0;
1175         for (i = 0; i < min; i++) {
1176                 if (!isdigit(*s))
1177                         return 0;
1178                 else
1179                         *num = *num * 10 + *s++ - '0';
1180         }
1181         for (; i < max && isdigit(*s); i++)
1182                 *num = *num * 10 + *s++ - '0';
1183         *src = s;
1184         return i;
1185 }
1186
1187
1188 // find longest match of str in list
1189 const struct Token *SGMetar::scanToken(char **str, const struct Token *list)
1190 {
1191         const struct Token *longest = 0;
1192         int maxlen = 0, len;
1193         const char *s;
1194         for (int i = 0; (s = list[i].id); i++) {
1195                 len = strlen(s);
1196                 if (!strncmp(s, *str, len) && len > maxlen) {
1197                         maxlen = len;
1198                         longest = &list[i];
1199                 }
1200         }
1201         *str += maxlen;
1202         return longest;
1203 }
1204
1205
1206 void SGMetarCloud::set(double alt, int cov)
1207 {
1208         _altitude = alt;
1209         if (cov != -1)
1210                 _coverage = cov;
1211 }
1212
1213
1214 void SGMetarVisibility::set(double dist, int dir, int mod, int tend)
1215 {
1216         _distance = dist;
1217         if (dir != -1)
1218                 _direction = dir;
1219         if (mod != -1)
1220                 _modifier = mod;
1221         if (tend != 1)
1222                 _tendency = tend;
1223 }
1224
1225 #undef NaN