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).
31 #include <simgear/io/sg_socket.hxx>
32 #include <simgear/debug/logstream.hxx>
33 #include <simgear/structure/exception.hxx>
37 #define NaN SGMetarNaN
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
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: "")
55 * SGMetar *m = new SGMetar("METAR KSFO 061656Z 19004KT 9SM SCT100 OVC200 08/03 A3013");
56 * double t = m->getTemperature_F();
59 * SGMetar n("KSFO", "proxy.provider.foo", "3128", "proxy-password");
60 * double d = n.getDewpoint_C();
63 SGMetar::SGMetar(const string& m, const string& proxy, const string& port,
64 const string& auth, const time_t time) :
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]);
90 _data = loadData(_icao, proxy, port, auth, time);
92 _data = new char[m.length() + 2]; // make room for " \0"
93 strcpy(_data, m.c_str());
102 if (!scanPreambleDate())
108 if (!scanId() || !scanDate()) {
110 throw sg_io_exception("metar data bogus (" + _url + ')');
117 while (scanVisibility()) ;
118 while (scanRwyVisRange()) ;
119 while (scanWeather()) ;
120 while (scanSkyCondition()) ;
123 while (scanSkyCondition()) ;
124 while (scanRunwayReport()) ;
128 while (scanColorState()) ;
130 while (scanRunwayReport()) ;
136 throw sg_io_exception("metar data incomplete (" + _url + ')');
144 * Clears lists and maps to discourage access after destruction.
155 void SGMetar::useCurrentDate()
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;
166 * If called with "KSFO" loads data from
168 * http://weather.noaa.gov/pub/data/observations/metar/stations/KSFO.TXT.
170 * Throws sg_io_exception on failure. Gives up after waiting longer than 10 seconds.
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")
179 char *SGMetar::loadData(const char *id, const string& proxy, const string& port,
180 const string& auth, time_t time)
182 const int buflen = 512;
183 char buf[2 * buflen];
185 string host = proxy.empty() ? "weather.noaa.gov" : proxy;
186 string path = "/pub/data/observations/metar/stations/";
188 path += string(id) + ".TXT";
189 _url = "http://weather.noaa.gov" + path;
191 SGSocket *sock = new SGSocket(host, port.empty() ? "80" : port, "tcp");
192 sock->set_timeout(10000);
193 if (!sock->open(SG_IO_OUT)) {
195 throw sg_io_exception("cannot connect to " + host);
200 get += "http://weather.noaa.gov";
202 sprintf(buf, "%ld", time);
203 get += path + " HTTP/1.0\015\012X-Time: " + buf + "\015\012";
206 get += "Proxy-Authorization: " + auth + "\015\012";
209 sock->writestring(get.c_str());
214 while ((i = sock->readline(buf, buflen))) {
215 if (i <= 2 && isspace(buf[0]) && (!buf[1] || isspace(buf[1])))
217 if (!strncmp(buf, "X-MetarProxy: ", 9))
221 i = sock->readline(buf, buflen);
223 sock->readline(&buf[i], buflen);
232 throw sg_io_exception("no metar data available from " + _url);
234 char *metar = new char[strlen(b) + 2]; // make room for " \0"
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.
244 void SGMetar::normalizeData()
247 for (src = dest = _data; (*dest++ = *src++); )
248 while (*src == ' ' && src[1] == ' ')
250 for (dest--; isspace(*--dest); ) ;
256 // \d\d\d\d/\d\d/\d\d
257 bool SGMetar::scanPreambleDate()
260 int year, month, day;
261 if (!scanNumber(&m, &year, 4))
265 if (!scanNumber(&m, &month, 2))
269 if (!scanNumber(&m, &day, 2))
271 if (!scanBoundary(&m))
282 bool SGMetar::scanPreambleTime()
286 if (!scanNumber(&m, &hour, 2))
290 if (!scanNumber(&m, &minute, 2))
292 if (!scanBoundary(&m))
302 bool SGMetar::scanType()
304 if (strncmp(_m, "METAR ", 6) && strncmp(_m, "SPECI ", 6))
313 bool SGMetar::scanId()
316 for (int i = 0; i < 4; m++, i++)
317 if (!(isalpha(*m) || isdigit(*m)))
319 if (!scanBoundary(&m))
321 strncpy(_icao, _m, 4);
330 bool SGMetar::scanDate()
333 int day, hour, minute;
334 if (!scanNumber(&m, &day, 2))
336 if (!scanNumber(&m, &hour, 2))
338 if (!scanNumber(&m, &minute, 2))
342 if (!scanBoundary(&m))
353 // (NIL|AUTO|COR|RTD)
354 bool SGMetar::scanModifier()
358 if (!strncmp(m, "NIL", 3)) {
362 if (!strncmp(m, "AUTO", 4)) // automatically generated
364 else if (!strncmp(m, "COR", 3)) // manually corrected
366 else if (!strncmp(m, "RTD", 3)) // routine delayed
370 if (!scanBoundary(&m))
379 // (\d{3}|VRB)\d{1,3}(G\d{2,3})?(KT|KMH|MPS)
380 bool SGMetar::scanWind()
384 if (!strncmp(m, "VRB", 3))
386 else if (!scanNumber(&m, &dir, 3))
390 if (!scanNumber(&m, &i, 2, 3))
397 if (!scanNumber(&m, &i, 2, 3))
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;
412 if (!scanBoundary(&m))
416 _wind_speed = speed * factor;
418 _gust_speed = gust * factor;
425 bool SGMetar::scanVariability()
429 if (!scanNumber(&m, &from, 3))
433 if (!scanNumber(&m, &to, 3))
435 if (!scanBoundary(&m))
438 _wind_range_from = from;
445 bool SGMetar::scanVisibility()
446 // TODO: if only directed vis are given, do still set min/max
448 if (!strncmp(_m, "//// ", 5)) { // spec compliant?
457 int modifier = SGMetarVisibility::EQUALS;
458 // \d{4}(N|NE|E|SE|S|SW|W|NW)?
459 if (scanNumber(&m, &i, 4)) {
464 else if (*m == 'N') {
472 } else if (*m == 'S') {
482 i = 50, modifier = SGMetarVisibility::LESS_THAN;
484 i++, modifier = SGMetarVisibility::GREATER_THAN;
487 // M?(\d{1,2}|\d{1,2}/\d{1,2}|\d{1,2} \d{1,2}/\d{1,2})(SM|KM)
490 m++, modifier = SGMetarVisibility::LESS_THAN;
492 if (!scanNumber(&m, &i, 1, 2))
498 if (!scanNumber(&m, &i, 1, 2))
501 } else if (*m == ' ') {
504 if (!scanNumber(&m, &i, 1, 2))
508 if (!scanNumber(&m, &denom, 1, 2))
510 distance += (double)i / denom;
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;
520 if (!scanBoundary(&m))
523 SGMetarVisibility *v;
525 v = &_dir_visibility[dir / 45];
526 else if (_min_visibility._distance == NaN)
527 v = &_min_visibility;
529 v = &_max_visibility;
531 v->_distance = distance;
532 v->_modifier = modifier;
540 // R\d\d[LCR]?/([PM]?\d{4}V)?[PM]?\d{4}(FT)?[DNU]?
541 bool SGMetar::scanRwyVisRange()
548 if (!scanNumber(&m, &i, 2))
550 if (*m == 'L' || *m == 'C' || *m == 'R')
554 strncpy(id, _m + 1, i = m - _m - 1);
562 m++, r._min_visibility._modifier = SGMetarVisibility::GREATER_THAN;
564 m++, r._min_visibility._modifier = SGMetarVisibility::LESS_THAN;
565 if (!scanNumber(&m, &from, 4))
570 m++, r._max_visibility._modifier = SGMetarVisibility::GREATER_THAN;
572 m++, r._max_visibility._modifier = SGMetarVisibility::LESS_THAN;
573 if (!scanNumber(&m, &to, 4))
578 if (!strncmp(m, "FT", 2)) {
579 from = int(from * SG_FEET_TO_METER);
580 to = int(to * SG_FEET_TO_METER);
583 r._min_visibility._distance = from;
584 r._max_visibility._distance = to;
586 if (*m == '/') // this is not in the spec!
589 m++, r._min_visibility._tendency = SGMetarVisibility::DECREASING;
591 m++, r._min_visibility._tendency = SGMetarVisibility::STABLE;
593 m++, r._min_visibility._tendency = SGMetarVisibility::INCREASING;
595 if (!scanBoundary(&m))
599 _runways[id]._min_visibility = r._min_visibility;
600 _runways[id]._max_visibility = r._max_visibility;
606 static const struct Token special[] = {
607 "NSW", "no significant weather",
608 "VCSH", "showers in the vicinity",
609 "VCTS", "thunderstorm in the vicinity",
614 static const struct Token description[] = {
616 "TS", "thunderstorm with",
619 "DR", "low drifting",
627 static const struct Token phenomenon[] = {
630 "GS", "small hail and/or snow pellets",
631 "IC", "ice crystals",
636 "UP", "unknown precipitation",
638 "DU", "widespread dust",
645 "VA", "volcanic ash",
647 "FC", "funnel cloud/tornado waterspout",
648 "PO", "well-developed dust/sand whirls",
651 "UP", "unknown", // ... due to failed automatic acquisition
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()
661 const struct Token *a;
662 if ((a = scanToken(&m, special))) {
663 if (!scanBoundary(&m))
665 _weather.push_back(a->text);
673 m++, pre = "light ", intensity = 1;
675 m++, pre = "heavy ", intensity = 3;
676 else if (!strncmp(m, "VC", 2))
677 m += 2, post = "in the vicinity ";
679 pre = "moderate ", intensity = 2;
682 for (i = 0; i < 3; i++) {
683 if (!(a = scanToken(&m, description)))
685 weather += string(a->text) + " ";
687 for (i = 0; i < 3; i++) {
688 if (!(a = scanToken(&m, phenomenon)))
690 weather += string(a->text) + " ";
691 if (!strcmp(a->id, "RA"))
693 else if (!strcmp(a->id, "HA"))
695 else if (!strcmp(a->id, "SN"))
698 if (!weather.length())
700 if (!scanBoundary(&m))
703 weather = pre + weather + post;
704 weather.erase(weather.length() - 1);
705 _weather.push_back(weather);
711 static const struct Token cloud_types[] = {
713 "ACC", "altocumulus castellanus",
714 "ACSL", "altocumulus standing lenticular",
716 "CB", "cumulonimbus",
717 "CBMAM", "cumulonimbus mammatus",
718 "CC", "cirrocumulus",
719 "CCSL", "cirrocumulus standing lenticular",
721 "CS", "cirrostratus",
723 "CUFRA", "cumulus fractus",
724 "NS", "nimbostratus",
725 "SAC", "stratoaltocumulus", // guessed
726 "SC", "stratocumulus",
727 "SCSL", "stratocumulus standing lenticular",
729 "STFRA", "stratus fractus",
730 "TCU", "towering cumulus",
735 // (FEW|SCT|BKN|OVC|SKC|CLR|CAVOK|VV)([0-9]{3}|///)?[:cloud_type:]?
736 bool SGMetar::scanSkyCondition()
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)
747 if (!scanBoundary(&m))
752 _clouds.push_back(cl);
760 if (!strncmp(m, "VV", i = 2)) // vertical visibility
762 else if (!strncmp(m, "FEW", i = 3))
764 else if (!strncmp(m, "SCT", i = 3))
766 else if (!strncmp(m, "BKN", i = 3))
768 else if (!strncmp(m, "OVC", i = 3))
774 if (!strncmp(m, "///", 3)) // vis not measurable (e.g. because of heavy snowing)
776 else if (scanBoundary(&m)) {
778 return true; // ignore single OVC/BKN/...
779 } else if (!scanNumber(&m, &i, 3))
782 if (cl._coverage == -1) {
783 if (!scanBoundary(&m))
785 if (i == -1) // 'VV///'
786 _vert_visibility._modifier = SGMetarVisibility::NOGO;
788 _vert_visibility._distance = i * 100 * SG_FEET_TO_METER;
794 cl._altitude = i * 100 * SG_FEET_TO_METER;
796 const struct Token *a;
797 if ((a = scanToken(&m, cloud_types))) {
799 cl._type_long = a->text;
801 if (!scanBoundary(&m))
803 _clouds.push_back(cl);
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()
815 int sign = 1, temp, dew;
816 if (!strncmp(m, "XX/XX", 5)) { // not spec compliant!
818 return scanBoundary(&_m);
823 if (!scanNumber(&m, &temp, 2))
829 if (!scanBoundary(&m)) {
830 if (!strncmp(m, "XX", 2)) // not spec compliant!
836 if (!scanNumber(&m, &dew, 2))
839 if (!scanBoundary(&m))
851 double SGMetar::getRelHumidity() const
853 if (_temp == NaN || _dewp == 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;
862 // [AQ]\d{2}(\d{2}|//) (Namibia)
863 bool SGMetar::scanPressure()
870 factor = SG_INHG_TO_PA / 100;
876 if (!scanNumber(&m, &press, 2))
879 if (!strncmp(m, "//", 2)) // not spec compliant!
881 else if (scanNumber(&m, &i, 2))
885 if (!scanBoundary(&m))
887 _pressure = press * factor;
894 static const char *runway_deposit[] = {
908 static const char *runway_deposit_extent[] = {
909 0, "1-10%", "11-25%", 0, 0, "26-50%", 0, 0, 0, "51-100%"
913 static const char *runway_friction[] = {
915 "poor braking action",
916 "poor/medium braking action",
917 "medium braking action",
918 "medium/good braking action",
919 "good braking action",
921 "friction: unreliable measurement"
925 // \d\d(CLRD|[\d/]{4})(\d\d|//)
926 bool SGMetar::scanRunwayReport()
933 if (!scanNumber(&m, &i, 2))
938 strcpy(id, "REP"); // repetition of previous report
941 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = 'R', id[3] = '\0';
943 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = '\0';
945 if (!strncmp(m, "CLRD", 4)) {
946 m += 4; // runway cleared
947 r._deposit_string = "cleared";
949 if (scanNumber(&m, &i, 1)) {
951 r._deposit_string = runway_deposit[i];
952 } else if (*m == '/')
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 != '/')
963 if (!strncmp(m, "//", 2))
965 else if (!scanNumber(&m, &i, 2))
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;
975 r._comment = "runway not in use";
976 else if (i == -1) // no depth given ("//")
982 if (m[0] == '/' && m[1] == '/')
984 else if (!scanNumber(&m, &i, 2))
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];
991 if (!scanBoundary(&m))
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;
1007 // WS (ALL RWYS?|RWY ?\d\d[LCR]?)?
1008 bool SGMetar::scanWindShear()
1011 if (strncmp(m, "WS", 2))
1014 if (!scanBoundary(&m))
1017 if (!strncmp(m, "ALL", 3)) {
1019 if (!scanBoundary(&m))
1021 if (strncmp(m, "RWY", 3))
1026 if (!scanBoundary(&m))
1028 _runways["ALL"]._wind_shear = true;
1035 for (cnt = 0;; cnt++) { // ??
1036 if (strncmp(m, "RWY", 3))
1041 if (!scanNumber(&m, &i, 2))
1043 if (*m == 'L' || *m == 'C' || *m == 'R')
1045 strncpy(id, mm, i = m - mm);
1047 if (!scanBoundary(&m))
1049 _runways[id]._wind_shear = true;
1052 _runways["ALL"]._wind_shear = true;
1058 bool SGMetar::scanTrendForecast()
1061 if (strncmp(m, "NOSIG", 5))
1065 if (!scanBoundary(&m))
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
1084 bool SGMetar::scanColorState()
1087 const struct Token *a;
1088 if (!(a = scanToken(&m, colors)))
1090 if (!scanBoundary(&m))
1092 //printf(Y"Code %s\n"N, a->text);
1098 bool SGMetar::scanRemark()
1100 if (strncmp(_m, "RMK", 3))
1103 if (!scanBoundary(&_m))
1107 if (!scanRunwayReport()) {
1108 while (*_m && !isspace(*_m))
1117 bool SGMetar::scanRemainder()
1120 if (!(strncmp(m, "NOSIG", 5))) {
1122 if (scanBoundary(&m))
1123 _m = m; //_comment.push_back("No significant tendency");
1126 if (!scanBoundary(&m))
1133 bool SGMetar::scanBoundary(char **s)
1135 if (**s && !isspace(**s))
1137 while (isspace(**s))
1143 int SGMetar::scanNumber(char **src, int *num, int min, int max)
1148 for (i = 0; i < min; i++) {
1152 *num = *num * 10 + *s++ - '0';
1154 for (; i < max && isdigit(*s); i++)
1155 *num = *num * 10 + *s++ - '0';
1161 // find longest match of str in list
1162 const struct Token *SGMetar::scanToken(char **str, const struct Token *list)
1164 const struct Token *longest = 0;
1165 int maxlen = 0, len;
1167 for (int i = 0; (s = list[i].id); i++) {
1169 if (!strncmp(s, *str, len) && len > maxlen) {
1179 void SGMetarCloud::set(double alt, int cov)
1187 void SGMetarVisibility::set(double dist, int dir, int mod, int tend)