1 // metar interface class
3 // Written by Melchior FRANZ, started December 2003.
5 // Copyright (C) 2003 Melchior FRANZ - mfranz@aon.at
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.
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.
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
25 * Interface for encoded Meteorological Aerodrome Reports (METAR).
30 #include <simgear/io/sg_socket.hxx>
31 #include <simgear/debug/logstream.hxx>
32 #include <simgear/structure/exception.hxx>
36 #define NaN SGMetarNaN
39 * The constructor takes a Metar string, or a four-letter ICAO code. In the
40 * latter case the metar string is downloaded from
41 * http://weather.noaa.gov/pub/data/observations/metar/stations/.
42 * The constructor throws sg_io_exceptions on failure. The "METAR"
43 * keyword has no effect (apart from incrementing the group counter
44 * @a grpcount) and can be left away. A keyword "SPECI" is
47 * @param m ICAO station id or metar string
48 * @param proxy proxy host (optional; default: "")
49 * @param port proxy port (optional; default: "80")
50 * @param auth proxy authorization information (optional; default: "")
54 * SGMetar *m = new SGMetar("METAR KSFO 061656Z 19004KT 9SM SCT100 OVC200 08/03 A3013");
55 * double t = m->getTemperature_F();
58 * SGMetar n("KSFO", "proxy.provider.foo", "3128", "proxy-password");
59 * double d = n.getDewpoint_C();
62 SGMetar::SGMetar(const string& m, const string& proxy, const string& port, const string& auth) :
79 if (m.length() == 4 && isalnum(m[0]) && isalnum(m[1]) && isalnum(m[2]) && isalnum(m[3])) {
80 for (int i = 0; i < 4; i++)
81 _icao[i] = toupper(m[i]);
83 _data = loadData(_icao, proxy, port, auth);
85 _data = new char[m.length() + 2]; // make room for " \0"
86 strcpy(_data, m.c_str());
100 if (!scanId() || !scanDate()) {
102 throw sg_io_exception("metar data bogus (" + _url + ')');
109 while (scanVisibility()) ;
110 while (scanRwyVisRange()) ;
111 while (scanWeather()) ;
112 while (scanSkyCondition()) ;
115 while (scanSkyCondition()) ;
116 while (scanRunwayReport()) ;
120 while (scanColorState()) ;
122 while (scanRunwayReport()) ;
128 throw sg_io_exception("metar data incomplete (" + _url + ')');
135 * Clears lists and maps to discourage access after destruction.
147 * If called with "KSFO" loads data from
149 * http://weather.noaa.gov/pub/data/observations/metar/stations/KSFO.TXT.
151 * Throws sg_io_exception on failure. Gives up after waiting longer than 10 seconds.
153 * @param id four-letter ICAO Metar station code, e.g. "KSFO".
154 * @param proxy proxy host (optional; default: "")
155 * @param port proxy port (optional; default: "80")
156 * @param auth proxy authorization information (optional; default: "")
157 * @return pointer to Metar data string, allocated by new char[].
158 * @see rfc2068.txt for proxy spec ("Proxy-Authorization")
160 char *SGMetar::loadData(const char *id, const string& proxy, const string& port, const string& auth)
162 string host = proxy.empty() ? "weather.noaa.gov" : proxy;
163 string path = "/pub/data/observations/metar/stations/";
164 path += string(id) + ".TXT";
165 _url = "http://weather.noaa.gov" + path;
167 SGSocket *sock = new SGSocket(host, port.empty() ? "80" : port, "tcp");
168 sock->set_timeout(10000);
169 if (!sock->open(SG_IO_OUT)) {
171 throw sg_io_exception("cannot connect to " + host);
176 get += "http://weather.noaa.gov";
177 get += path + " HTTP/1.0\r\n";
178 sock->writestring(get.c_str());
181 get = "Proxy-Authorization: " + auth + "\r\n";
182 sock->writestring(get.c_str());
185 sock->writestring("\r\n");
188 const int buflen = 512;
189 char buf[2 * buflen];
192 while ((i = sock->readline(buf, buflen)))
193 if (i <= 2 && isspace(buf[0]) && (!buf[1] || isspace(buf[1])))
196 i = sock->readline(buf, buflen);
198 sock->readline(&buf[i], buflen);
207 throw sg_io_exception("no metar data available from " + _url);
209 char *metar = new char[strlen(b) + 2]; // make room for " \0"
216 * Replace any number of subsequent spaces by just one space, and add
217 * a trailing space. This makes scanning for things like "ALL RWY" easier.
219 void SGMetar::normalizeData()
222 for (src = dest = _data; (*dest++ = *src++); )
223 while (*src == ' ' && src[1] == ' ')
225 for (dest--; isspace(*--dest); ) ;
231 // \d\d\d\d/\d\d/\d\d
232 bool SGMetar::scanPreambleDate()
235 int year, month, day;
236 if (!scanNumber(&m, &year, 4))
240 if (!scanNumber(&m, &month, 2))
244 if (!scanNumber(&m, &day, 2))
246 if (!scanBoundary(&m))
257 bool SGMetar::scanPreambleTime()
261 if (!scanNumber(&m, &hour, 2))
265 if (!scanNumber(&m, &minute, 2))
267 if (!scanBoundary(&m))
277 bool SGMetar::scanType()
279 if (strncmp(_m, "METAR ", 6) && strncmp(_m, "SPECI ", 6))
288 bool SGMetar::scanId()
291 for (int i = 0; i < 4; m++, i++)
292 if (!(isalpha(*m) || isdigit(*m)))
294 if (!scanBoundary(&m))
296 strncpy(_icao, _m, 4);
305 bool SGMetar::scanDate()
308 int day, hour, minute;
309 if (!scanNumber(&m, &day, 2))
311 if (!scanNumber(&m, &hour, 2))
313 if (!scanNumber(&m, &minute, 2))
317 if (!scanBoundary(&m))
328 // (NIL|AUTO|COR|RTD)
329 bool SGMetar::scanModifier()
333 if (!strncmp(m, "NIL", 3)) {
337 if (!strncmp(m, "AUTO", 4)) // automatically generated
339 else if (!strncmp(m, "COR", 3)) // manually corrected
341 else if (!strncmp(m, "RTD", 3)) // routine delayed
345 if (!scanBoundary(&m))
354 // (\d{3}|VRB)\d{1,3}(G\d{2,3})?(KT|KMH|MPS)
355 bool SGMetar::scanWind()
359 if (!strncmp(m, "VRB", 3))
361 else if (!scanNumber(&m, &dir, 3))
365 if (!scanNumber(&m, &i, 2, 3))
372 if (!scanNumber(&m, &i, 2, 3))
377 if (!strncmp(m, "KT", 2))
378 m += 2, factor = SG_KT_TO_MPS;
379 else if (!strncmp(m, "KMH", 3))
380 m += 3, factor = SG_KMH_TO_MPS;
381 else if (!strncmp(m, "KPH", 3)) // ??
382 m += 3, factor = SG_KMH_TO_MPS;
383 else if (!strncmp(m, "MPS", 3))
384 m += 3, factor = 1.0;
387 if (!scanBoundary(&m))
391 _wind_speed = speed * factor;
393 _gust_speed = gust * factor;
400 bool SGMetar::scanVariability()
404 if (!scanNumber(&m, &from, 3))
408 if (!scanNumber(&m, &to, 3))
410 if (!scanBoundary(&m))
413 _wind_range_from = from;
420 bool SGMetar::scanVisibility()
421 // TODO: if only directed vis are given, do still set min/max
423 if (!strncmp(_m, "//// ", 5)) { // spec compliant?
432 int modifier = SGMetarVisibility::EQUALS;
433 // \d{4}(N|NE|E|SE|S|SW|W|NW)?
434 if (scanNumber(&m, &i, 4)) {
439 else if (*m == 'N') {
447 } else if (*m == 'S') {
457 i = 50, modifier = SGMetarVisibility::LESS_THAN;
459 i++, modifier = SGMetarVisibility::GREATER_THAN;
462 // M?(\d{1,2}|\d{1,2}/\d{1,2}|\d{1,2} \d{1,2}/\d{1,2})(SM|KM)
465 m++, modifier = SGMetarVisibility::LESS_THAN;
467 if (!scanNumber(&m, &i, 1, 2))
473 if (!scanNumber(&m, &i, 1, 2))
476 } else if (*m == ' ') {
479 if (!scanNumber(&m, &i, 1, 2))
483 if (!scanNumber(&m, &denom, 1, 2))
485 distance += (double)i / denom;
488 if (!strncmp(m, "SM", 2))
489 distance *= SG_SM_TO_METER, m += 2;
490 else if (!strncmp(m, "KM", 2))
491 distance *= 1000, m += 2;
495 if (!scanBoundary(&m))
498 SGMetarVisibility *v;
500 v = &_dir_visibility[dir / 45];
501 else if (_min_visibility._distance == NaN)
502 v = &_min_visibility;
504 v = &_max_visibility;
506 v->_distance = distance;
507 v->_modifier = modifier;
515 // R\d\d[LCR]?/([PM]?\d{4}V)?[PM]?\d{4}(FT)?[DNU]?
516 bool SGMetar::scanRwyVisRange()
523 if (!scanNumber(&m, &i, 2))
525 if (*m == 'L' || *m == 'C' || *m == 'R')
529 strncpy(id, _m + 1, i = m - _m - 1);
537 m++, r._min_visibility._modifier = SGMetarVisibility::GREATER_THAN;
539 m++, r._min_visibility._modifier = SGMetarVisibility::LESS_THAN;
540 if (!scanNumber(&m, &from, 4))
545 m++, r._max_visibility._modifier = SGMetarVisibility::GREATER_THAN;
547 m++, r._max_visibility._modifier = SGMetarVisibility::LESS_THAN;
548 if (!scanNumber(&m, &to, 4))
553 if (!strncmp(m, "FT", 2)) {
554 from = int(from * SG_FEET_TO_METER);
555 to = int(to * SG_FEET_TO_METER);
558 r._min_visibility._distance = from;
559 r._max_visibility._distance = to;
561 if (*m == '/') // this is not in the spec!
564 m++, r._min_visibility._tendency = SGMetarVisibility::DECREASING;
566 m++, r._min_visibility._tendency = SGMetarVisibility::STABLE;
568 m++, r._min_visibility._tendency = SGMetarVisibility::INCREASING;
570 if (!scanBoundary(&m))
574 _runways[id]._min_visibility = r._min_visibility;
575 _runways[id]._max_visibility = r._max_visibility;
581 static const struct Token special[] = {
582 "NSW", "no significant weather",
583 "VCSH", "showers in the vicinity",
584 "VCTS", "thunderstorm in the vicinity",
589 static const struct Token description[] = {
591 "TS", "thunderstorm with",
594 "DR", "low drifting",
602 static const struct Token phenomenon[] = {
605 "GS", "small hail and/or snow pellets",
606 "IC", "ice crystals",
611 "UP", "unknown precipitation",
613 "DU", "widespread dust",
620 "VA", "volcanic ash",
622 "FC", "funnel cloud/tornado waterspout",
623 "PO", "well-developed dust/sand whirls",
626 "UP", "unknown", // ... due to failed automatic acquisition
631 // (+|-|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}
632 bool SGMetar::scanWeather()
636 const struct Token *a;
637 if ((a = scanToken(&m, special))) {
638 if (!scanBoundary(&m))
640 _weather.push_back(a->text);
650 else if (!strncmp(m, "VC", 2))
651 m += 2, post = "in the vicinity ";
656 for (i = 0; i < 3; i++) {
657 if (!(a = scanToken(&m, description)))
659 weather += string(a->text) + " ";
661 for (i = 0; i < 3; i++) {
662 if (!(a = scanToken(&m, phenomenon)))
664 weather += string(a->text) + " ";
666 if (!weather.length())
668 if (!scanBoundary(&m))
671 weather = pre + weather + post;
672 weather.erase(weather.length() - 1);
673 _weather.push_back(weather);
679 static const struct Token cloud_types[] = {
681 "ACC", "altocumulus castellanus",
682 "ACSL", "altocumulus standing lenticular",
684 "CB", "cumulonimbus",
685 "CBMAM", "cumulonimbus mammatus",
686 "CC", "cirrocumulus",
687 "CCSL", "cirrocumulus standing lenticular",
689 "CS", "cirrostratus",
691 "CUFRA", "cumulus fractus",
692 "NS", "nimbostratus",
693 "SAC", "stratoaltocumulus", // guessed
694 "SC", "stratocumulus",
695 "SCSL", "stratocumulus standing lenticular",
697 "STFRA", "stratus fractus",
698 "TCU", "towering cumulus",
703 // (FEW|SCT|BKN|OVC|SKC|CLR|CAVOK|VV)([0-9]{3}|///)?[:cloud_type:]?
704 bool SGMetar::scanSkyCondition()
710 if (!strncmp(m, "CLR", i = 3) // clear
711 || !strncmp(m, "SKC", i = 3) // sky clear
712 || !strncmp(m, "NSC", i = 3) // no significant clouds
713 || !strncmp(m, "CAVOK", i = 5)) { // ceiling and visibility OK (implies 9999)
715 if (!scanBoundary(&m))
718 _clouds.push_back(cl);
723 if (!strncmp(m, "VV", i = 2)) // vertical visibility
725 else if (!strncmp(m, "FEW", i = 3))
727 else if (!strncmp(m, "SCT", i = 3))
729 else if (!strncmp(m, "BKN", i = 3))
731 else if (!strncmp(m, "OVC", i = 3))
737 if (!strncmp(m, "///", 3)) // vis not measurable (e.g. because of heavy snowing)
739 else if (scanBoundary(&m)) {
741 return true; // ignore single OVC/BKN/...
742 } else if (!scanNumber(&m, &i, 3))
745 if (cl._coverage == -1) {
746 if (!scanBoundary(&m))
748 if (i == -1) // 'VV///'
749 _vert_visibility._modifier = SGMetarVisibility::NOGO;
751 _vert_visibility._distance = i * 100 * SG_FEET_TO_METER;
757 cl._altitude = i * 100 * SG_FEET_TO_METER;
759 const struct Token *a;
760 if ((a = scanToken(&m, cloud_types))) {
762 cl._type_long = a->text;
764 if (!scanBoundary(&m))
766 _clouds.push_back(cl);
773 // M?[0-9]{2}/(M?[0-9]{2})? (spec)
774 // (M?[0-9]{2}|XX)/(M?[0-9]{2}|XX)? (Namibia)
775 bool SGMetar::scanTemperature()
778 int sign = 1, temp, dew;
779 if (!strncmp(m, "XX/XX", 5)) { // not spec compliant!
781 return scanBoundary(&_m);
786 if (!scanNumber(&m, &temp, 2))
792 if (!scanBoundary(&m)) {
793 if (!strncmp(m, "XX", 2)) // not spec compliant!
799 if (!scanNumber(&m, &dew, 2))
802 if (!scanBoundary(&m))
814 double SGMetar::getRelHumidity() const
816 if (_temp == NaN || _dewp == NaN)
818 double dewp = pow(10.0, 7.5 * _dewp / (237.7 + _dewp));
819 double temp = pow(10.0, 7.5 * _temp / (237.7 + _temp));
820 return dewp * 100 / temp;
825 // [AQ]\d{2}(\d{2}|//) (Namibia)
826 bool SGMetar::scanPressure()
833 factor = SG_INHG_TO_PA / 100;
839 if (!scanNumber(&m, &press, 2))
842 if (!strncmp(m, "//", 2)) // not spec compliant!
844 else if (scanNumber(&m, &i, 2))
848 if (!scanBoundary(&m))
850 _pressure = press * factor;
857 static const char *runway_deposit[] = {
871 static const char *runway_deposit_extent[] = {
872 0, "1-10%", "11-25%", 0, 0, "26-50%", 0, 0, 0, "51-100%"
876 static const char *runway_friction[] = {
878 "poor braking action",
879 "poor/medium braking action",
880 "medium braking action",
881 "medium/good braking action",
882 "good braking action",
884 "friction: unreliable measurement"
888 // \d\d(CLRD|[\d/]{4})(\d\d|//)
889 bool SGMetar::scanRunwayReport()
896 if (!scanNumber(&m, &i, 2))
901 strcpy(id, "REP"); // repetition of previous report
904 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = 'R', id[3] = '\0';
906 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = '\0';
908 if (!strncmp(m, "CLRD", 4)) {
909 m += 4; // runway cleared
910 r._deposit = "cleared";
912 if (scanNumber(&m, &i, 1)) {
913 r._deposit = runway_deposit[i];
914 } else if (*m == '/')
918 if (*m == '1' || *m == '2' || *m == '5' || *m == '9') { // extent of deposit
919 r._extent = *m - '0';
920 r._extent_string = runway_deposit_extent[*m - '0'];
921 } else if (*m != '/')
925 if (!strncmp(m, "//", 2))
927 else if (!scanNumber(&m, &i, 2))
931 r._depth = 0.5; // < 1 mm deep (let's say 0.5 :-)
932 else if (i > 0 && i <= 90)
933 r._depth = i / 1000.0; // i mm deep
934 else if (i >= 92 && i <= 98)
935 r._depth = (i - 90) / 20.0;
937 r._comment = "runway not in use";
938 else if (i == -1) // no depth given ("//")
944 if (m[0] == '/' && m[1] == '/')
946 else if (!scanNumber(&m, &i, 2))
948 if (i >= 1 && i < 90) {
949 r._friction = i / 100.0;
950 } else if ((i >= 91 && i <= 95) || i == 99) {
951 r._friction_string = runway_friction[i - 90];
953 if (!scanBoundary(&m))
956 _runways[id]._deposit = r._deposit;
957 _runways[id]._extent = r._extent;
958 _runways[id]._extent_string = r._extent_string;
959 _runways[id]._depth = r._depth;
960 _runways[id]._friction = r._friction;
961 _runways[id]._friction_string = r._friction_string;
962 _runways[id]._comment = r._comment;
969 // WS (ALL RWYS?|RWY ?\d\d[LCR]?)?
970 bool SGMetar::scanWindShear()
973 if (strncmp(m, "WS", 2))
976 if (!scanBoundary(&m))
979 if (!strncmp(m, "ALL", 3)) {
981 if (!scanBoundary(&m))
983 if (strncmp(m, "RWY", 3))
988 if (!scanBoundary(&m))
990 _runways["ALL"]._wind_shear = true;
997 for (cnt = 0;; cnt++) { // ??
998 if (strncmp(m, "RWY", 3))
1003 if (!scanNumber(&m, &i, 2))
1005 if (*m == 'L' || *m == 'C' || *m == 'R')
1007 strncpy(id, mm, i = m - mm);
1009 if (!scanBoundary(&m))
1011 _runways[id]._wind_shear = true;
1014 _runways["ALL"]._wind_shear = true;
1020 bool SGMetar::scanTrendForecast()
1023 if (strncmp(m, "NOSIG", 5))
1027 if (!scanBoundary(&m))
1034 // (BLU|WHT|GRN|YLO|AMB|RED)
1035 static const struct Token colors[] = {
1036 "BLU", "Blue", // 2500 ft, 8.0 km
1037 "WHT", "White", // 1500 ft, 5.0 km
1038 "GRN", "Green", // 700 ft, 3.7 km
1039 "YLO", "Yellow", // 300 ft, 1.6 km
1040 "AMB", "Amber", // 200 ft, 0.8 km
1041 "RED", "Red", // <200 ft, <0.8 km
1046 bool SGMetar::scanColorState()
1049 const struct Token *a;
1050 if (!(a = scanToken(&m, colors)))
1052 if (!scanBoundary(&m))
1054 //printf(Y"Code %s\n"N, a->text);
1060 bool SGMetar::scanRemark()
1062 if (strncmp(_m, "RMK", 3))
1065 if (!scanBoundary(&_m))
1069 if (!scanRunwayReport()) {
1070 while (*_m && !isspace(*_m))
1079 bool SGMetar::scanRemainder()
1082 if (!(strncmp(m, "NOSIG", 5))) {
1084 if (scanBoundary(&m))
1085 _m = m; //_comment.push_back("No significant tendency");
1088 if (!scanBoundary(&m))
1095 bool SGMetar::scanBoundary(char **s)
1097 if (**s && !isspace(**s))
1099 while (isspace(**s))
1105 int SGMetar::scanNumber(char **src, int *num, int min, int max)
1110 for (i = 0; i < min; i++) {
1114 *num = *num * 10 + *s++ - '0';
1116 for (; i < max && isdigit(*s); i++)
1117 *num = *num * 10 + *s++ - '0';
1123 // find longest match of str in list
1124 const struct Token *SGMetar::scanToken(char **str, const struct Token *list)
1126 const struct Token *longest = 0;
1127 int maxlen = 0, len;
1129 for (int i = 0; (s = list[i].id); i++) {
1131 if (!strncmp(s, *str, len) && len > maxlen) {