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