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);
160 now = *gmtime(&now_sec);
162 gmtime_r(&now_sec, &now);
164 _year = now.tm_year + 1900;
165 _month = now.tm_mon + 1;
170 * If called with "KSFO" loads data from
172 * http://weather.noaa.gov/pub/data/observations/metar/stations/KSFO.TXT.
174 * Throws sg_io_exception on failure. Gives up after waiting longer than 10 seconds.
176 * @param id four-letter ICAO Metar station code, e.g. "KSFO".
177 * @param proxy proxy host (optional; default: "")
178 * @param port proxy port (optional; default: "80")
179 * @param auth proxy authorization information (optional; default: "")
180 * @return pointer to Metar data string, allocated by new char[].
181 * @see rfc2068.txt for proxy spec ("Proxy-Authorization")
183 char *SGMetar::loadData(const char *id, const string& proxy, const string& port,
184 const string& auth, time_t time)
186 const int buflen = 512;
187 char buf[2 * buflen];
189 string host = proxy.empty() ? "weather.noaa.gov" : proxy;
190 string path = "/pub/data/observations/metar/stations/";
192 path += string(id) + ".TXT";
193 _url = "http://weather.noaa.gov" + path;
195 SGSocket *sock = new SGSocket(host, port.empty() ? "80" : port, "tcp");
196 sock->set_timeout(10000);
197 if (!sock->open(SG_IO_OUT)) {
199 throw sg_io_exception("cannot connect to " + host);
204 get += "http://weather.noaa.gov";
206 sprintf(buf, "%ld", time);
207 get += path + " HTTP/1.0\015\012X-Time: " + buf + "\015\012";
210 get += "Proxy-Authorization: " + auth + "\015\012";
213 sock->writestring(get.c_str());
218 while ((i = sock->readline(buf, buflen))) {
219 if (i <= 2 && isspace(buf[0]) && (!buf[1] || isspace(buf[1])))
221 if (!strncmp(buf, "X-MetarProxy: ", 9))
225 i = sock->readline(buf, buflen);
227 sock->readline(&buf[i], buflen);
236 throw sg_io_exception("no metar data available from " + _url);
238 char *metar = new char[strlen(b) + 2]; // make room for " \0"
245 * Replace any number of subsequent spaces by just one space, and add
246 * a trailing space. This makes scanning for things like "ALL RWY" easier.
248 void SGMetar::normalizeData()
251 for (src = dest = _data; (*dest++ = *src++); )
252 while (*src == ' ' && src[1] == ' ')
254 for (dest--; isspace(*--dest); ) ;
260 // \d\d\d\d/\d\d/\d\d
261 bool SGMetar::scanPreambleDate()
264 int year, month, day;
265 if (!scanNumber(&m, &year, 4))
269 if (!scanNumber(&m, &month, 2))
273 if (!scanNumber(&m, &day, 2))
275 if (!scanBoundary(&m))
286 bool SGMetar::scanPreambleTime()
290 if (!scanNumber(&m, &hour, 2))
294 if (!scanNumber(&m, &minute, 2))
296 if (!scanBoundary(&m))
306 bool SGMetar::scanType()
308 if (strncmp(_m, "METAR ", 6) && strncmp(_m, "SPECI ", 6))
317 bool SGMetar::scanId()
320 for (int i = 0; i < 4; m++, i++)
321 if (!(isalpha(*m) || isdigit(*m)))
323 if (!scanBoundary(&m))
325 strncpy(_icao, _m, 4);
334 bool SGMetar::scanDate()
337 int day, hour, minute;
338 if (!scanNumber(&m, &day, 2))
340 if (!scanNumber(&m, &hour, 2))
342 if (!scanNumber(&m, &minute, 2))
346 if (!scanBoundary(&m))
357 // (NIL|AUTO|COR|RTD)
358 bool SGMetar::scanModifier()
362 if (!strncmp(m, "NIL", 3)) {
366 if (!strncmp(m, "AUTO", 4)) // automatically generated
368 else if (!strncmp(m, "COR", 3)) // manually corrected
370 else if (!strncmp(m, "RTD", 3)) // routine delayed
374 if (!scanBoundary(&m))
383 // (\d{3}|VRB)\d{1,3}(G\d{2,3})?(KT|KMH|MPS)
384 bool SGMetar::scanWind()
388 if (!strncmp(m, "VRB", 3))
390 else if (!scanNumber(&m, &dir, 3))
394 if (!scanNumber(&m, &i, 2, 3))
401 if (!scanNumber(&m, &i, 2, 3))
406 if (!strncmp(m, "KT", 2))
407 m += 2, factor = SG_KT_TO_MPS;
408 else if (!strncmp(m, "KMH", 3))
409 m += 3, factor = SG_KMH_TO_MPS;
410 else if (!strncmp(m, "KPH", 3)) // ??
411 m += 3, factor = SG_KMH_TO_MPS;
412 else if (!strncmp(m, "MPS", 3))
413 m += 3, factor = 1.0;
416 if (!scanBoundary(&m))
420 _wind_speed = speed * factor;
422 _gust_speed = gust * factor;
429 bool SGMetar::scanVariability()
433 if (!scanNumber(&m, &from, 3))
437 if (!scanNumber(&m, &to, 3))
439 if (!scanBoundary(&m))
442 _wind_range_from = from;
449 bool SGMetar::scanVisibility()
450 // TODO: if only directed vis are given, do still set min/max
452 if (!strncmp(_m, "//// ", 5)) { // spec compliant?
461 int modifier = SGMetarVisibility::EQUALS;
462 // \d{4}(N|NE|E|SE|S|SW|W|NW)?
463 if (scanNumber(&m, &i, 4)) {
468 else if (*m == 'N') {
476 } else if (*m == 'S') {
486 i = 50, modifier = SGMetarVisibility::LESS_THAN;
488 i++, modifier = SGMetarVisibility::GREATER_THAN;
491 // M?(\d{1,2}|\d{1,2}/\d{1,2}|\d{1,2} \d{1,2}/\d{1,2})(SM|KM)
494 m++, modifier = SGMetarVisibility::LESS_THAN;
496 if (!scanNumber(&m, &i, 1, 2))
502 if (!scanNumber(&m, &i, 1, 2))
505 } else if (*m == ' ') {
508 if (!scanNumber(&m, &i, 1, 2))
512 if (!scanNumber(&m, &denom, 1, 2))
514 distance += (double)i / denom;
517 if (!strncmp(m, "SM", 2))
518 distance *= SG_SM_TO_METER, m += 2;
519 else if (!strncmp(m, "KM", 2))
520 distance *= 1000, m += 2;
524 if (!scanBoundary(&m))
527 SGMetarVisibility *v;
529 v = &_dir_visibility[dir / 45];
530 else if (_min_visibility._distance == NaN)
531 v = &_min_visibility;
533 v = &_max_visibility;
535 v->_distance = distance;
536 v->_modifier = modifier;
544 // R\d\d[LCR]?/([PM]?\d{4}V)?[PM]?\d{4}(FT)?[DNU]?
545 bool SGMetar::scanRwyVisRange()
552 if (!scanNumber(&m, &i, 2))
554 if (*m == 'L' || *m == 'C' || *m == 'R')
558 strncpy(id, _m + 1, i = m - _m - 1);
566 m++, r._min_visibility._modifier = SGMetarVisibility::GREATER_THAN;
568 m++, r._min_visibility._modifier = SGMetarVisibility::LESS_THAN;
569 if (!scanNumber(&m, &from, 4))
574 m++, r._max_visibility._modifier = SGMetarVisibility::GREATER_THAN;
576 m++, r._max_visibility._modifier = SGMetarVisibility::LESS_THAN;
577 if (!scanNumber(&m, &to, 4))
582 if (!strncmp(m, "FT", 2)) {
583 from = int(from * SG_FEET_TO_METER);
584 to = int(to * SG_FEET_TO_METER);
587 r._min_visibility._distance = from;
588 r._max_visibility._distance = to;
590 if (*m == '/') // this is not in the spec!
593 m++, r._min_visibility._tendency = SGMetarVisibility::DECREASING;
595 m++, r._min_visibility._tendency = SGMetarVisibility::STABLE;
597 m++, r._min_visibility._tendency = SGMetarVisibility::INCREASING;
599 if (!scanBoundary(&m))
603 _runways[id]._min_visibility = r._min_visibility;
604 _runways[id]._max_visibility = r._max_visibility;
610 static const struct Token special[] = {
611 "NSW", "no significant weather",
612 "VCSH", "showers in the vicinity",
613 "VCTS", "thunderstorm in the vicinity",
618 static const struct Token description[] = {
620 "TS", "thunderstorm with",
623 "DR", "low drifting",
631 static const struct Token phenomenon[] = {
634 "GS", "small hail and/or snow pellets",
635 "IC", "ice crystals",
640 "UP", "unknown precipitation",
642 "DU", "widespread dust",
649 "VA", "volcanic ash",
651 "FC", "funnel cloud/tornado waterspout",
652 "PO", "well-developed dust/sand whirls",
655 "UP", "unknown", // ... due to failed automatic acquisition
660 // (+|-|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}
661 bool SGMetar::scanWeather()
665 const struct Token *a;
666 if ((a = scanToken(&m, special))) {
667 if (!scanBoundary(&m))
669 _weather.push_back(a->text);
677 m++, pre = "light ", intensity = 1;
679 m++, pre = "heavy ", intensity = 3;
680 else if (!strncmp(m, "VC", 2))
681 m += 2, post = "in the vicinity ";
683 pre = "moderate ", intensity = 2;
686 for (i = 0; i < 3; i++) {
687 if (!(a = scanToken(&m, description)))
689 weather += string(a->text) + " ";
691 for (i = 0; i < 3; i++) {
692 if (!(a = scanToken(&m, phenomenon)))
694 weather += string(a->text) + " ";
695 if (!strcmp(a->id, "RA"))
697 else if (!strcmp(a->id, "HA"))
699 else if (!strcmp(a->id, "SN"))
702 if (!weather.length())
704 if (!scanBoundary(&m))
707 weather = pre + weather + post;
708 weather.erase(weather.length() - 1);
709 _weather.push_back(weather);
715 static const struct Token cloud_types[] = {
717 "ACC", "altocumulus castellanus",
718 "ACSL", "altocumulus standing lenticular",
720 "CB", "cumulonimbus",
721 "CBMAM", "cumulonimbus mammatus",
722 "CC", "cirrocumulus",
723 "CCSL", "cirrocumulus standing lenticular",
725 "CS", "cirrostratus",
727 "CUFRA", "cumulus fractus",
728 "NS", "nimbostratus",
729 "SAC", "stratoaltocumulus", // guessed
730 "SC", "stratocumulus",
731 "SCSL", "stratocumulus standing lenticular",
733 "STFRA", "stratus fractus",
734 "TCU", "towering cumulus",
739 // (FEW|SCT|BKN|OVC|SKC|CLR|CAVOK|VV)([0-9]{3}|///)?[:cloud_type:]?
740 bool SGMetar::scanSkyCondition()
746 if (!strncmp(m, "CLR", i = 3) // clear
747 || !strncmp(m, "SKC", i = 3) // sky clear
748 || !strncmp(m, "NSC", i = 3) // no significant clouds
749 || !strncmp(m, "CAVOK", i = 5)) { // ceiling and visibility OK (implies 9999)
751 if (!scanBoundary(&m))
756 _clouds.push_back(cl);
764 if (!strncmp(m, "VV", i = 2)) // vertical visibility
766 else if (!strncmp(m, "FEW", i = 3))
768 else if (!strncmp(m, "SCT", i = 3))
770 else if (!strncmp(m, "BKN", i = 3))
772 else if (!strncmp(m, "OVC", i = 3))
778 if (!strncmp(m, "///", 3)) // vis not measurable (e.g. because of heavy snowing)
780 else if (scanBoundary(&m)) {
782 return true; // ignore single OVC/BKN/...
783 } else if (!scanNumber(&m, &i, 3))
786 if (cl._coverage == -1) {
787 if (!scanBoundary(&m))
789 if (i == -1) // 'VV///'
790 _vert_visibility._modifier = SGMetarVisibility::NOGO;
792 _vert_visibility._distance = i * 100 * SG_FEET_TO_METER;
798 cl._altitude = i * 100 * SG_FEET_TO_METER;
800 const struct Token *a;
801 if ((a = scanToken(&m, cloud_types))) {
803 cl._type_long = a->text;
805 if (!scanBoundary(&m))
807 _clouds.push_back(cl);
814 // M?[0-9]{2}/(M?[0-9]{2})? (spec)
815 // (M?[0-9]{2}|XX)/(M?[0-9]{2}|XX)? (Namibia)
816 bool SGMetar::scanTemperature()
819 int sign = 1, temp, dew;
820 if (!strncmp(m, "XX/XX", 5)) { // not spec compliant!
822 return scanBoundary(&_m);
827 if (!scanNumber(&m, &temp, 2))
833 if (!scanBoundary(&m)) {
834 if (!strncmp(m, "XX", 2)) // not spec compliant!
840 if (!scanNumber(&m, &dew, 2))
843 if (!scanBoundary(&m))
855 double SGMetar::getRelHumidity() const
857 if (_temp == NaN || _dewp == NaN)
859 double dewp = pow(10.0, 7.5 * _dewp / (237.7 + _dewp));
860 double temp = pow(10.0, 7.5 * _temp / (237.7 + _temp));
861 return dewp * 100 / temp;
866 // [AQ]\d{2}(\d{2}|//) (Namibia)
867 bool SGMetar::scanPressure()
874 factor = SG_INHG_TO_PA / 100;
880 if (!scanNumber(&m, &press, 2))
883 if (!strncmp(m, "//", 2)) // not spec compliant!
885 else if (scanNumber(&m, &i, 2))
889 if (!scanBoundary(&m))
891 _pressure = press * factor;
898 static const char *runway_deposit[] = {
912 static const char *runway_deposit_extent[] = {
913 0, "1-10%", "11-25%", 0, 0, "26-50%", 0, 0, 0, "51-100%"
917 static const char *runway_friction[] = {
919 "poor braking action",
920 "poor/medium braking action",
921 "medium braking action",
922 "medium/good braking action",
923 "good braking action",
925 "friction: unreliable measurement"
929 // \d\d(CLRD|[\d/]{4})(\d\d|//)
930 bool SGMetar::scanRunwayReport()
937 if (!scanNumber(&m, &i, 2))
942 strcpy(id, "REP"); // repetition of previous report
945 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = 'R', id[3] = '\0';
947 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = '\0';
949 if (!strncmp(m, "CLRD", 4)) {
950 m += 4; // runway cleared
951 r._deposit_string = "cleared";
953 if (scanNumber(&m, &i, 1)) {
955 r._deposit_string = runway_deposit[i];
956 } else if (*m == '/')
960 if (*m == '1' || *m == '2' || *m == '5' || *m == '9') { // extent of deposit
961 r._extent = *m - '0';
962 r._extent_string = runway_deposit_extent[*m - '0'];
963 } else if (*m != '/')
967 if (!strncmp(m, "//", 2))
969 else if (!scanNumber(&m, &i, 2))
973 r._depth = 0.5; // < 1 mm deep (let's say 0.5 :-)
974 else if (i > 0 && i <= 90)
975 r._depth = i / 1000.0; // i mm deep
976 else if (i >= 92 && i <= 98)
977 r._depth = (i - 90) / 20.0;
979 r._comment = "runway not in use";
980 else if (i == -1) // no depth given ("//")
986 if (m[0] == '/' && m[1] == '/')
988 else if (!scanNumber(&m, &i, 2))
990 if (i >= 1 && i < 90) {
991 r._friction = i / 100.0;
992 } else if ((i >= 91 && i <= 95) || i == 99) {
993 r._friction_string = runway_friction[i - 90];
995 if (!scanBoundary(&m))
998 _runways[id]._deposit = r._deposit;
999 _runways[id]._extent = r._extent;
1000 _runways[id]._extent_string = r._extent_string;
1001 _runways[id]._depth = r._depth;
1002 _runways[id]._friction = r._friction;
1003 _runways[id]._friction_string = r._friction_string;
1004 _runways[id]._comment = r._comment;
1011 // WS (ALL RWYS?|RWY ?\d\d[LCR]?)?
1012 bool SGMetar::scanWindShear()
1015 if (strncmp(m, "WS", 2))
1018 if (!scanBoundary(&m))
1021 if (!strncmp(m, "ALL", 3)) {
1023 if (!scanBoundary(&m))
1025 if (strncmp(m, "RWY", 3))
1030 if (!scanBoundary(&m))
1032 _runways["ALL"]._wind_shear = true;
1039 for (cnt = 0;; cnt++) { // ??
1040 if (strncmp(m, "RWY", 3))
1045 if (!scanNumber(&m, &i, 2))
1047 if (*m == 'L' || *m == 'C' || *m == 'R')
1049 strncpy(id, mm, i = m - mm);
1051 if (!scanBoundary(&m))
1053 _runways[id]._wind_shear = true;
1056 _runways["ALL"]._wind_shear = true;
1062 bool SGMetar::scanTrendForecast()
1065 if (strncmp(m, "NOSIG", 5))
1069 if (!scanBoundary(&m))
1076 // (BLU|WHT|GRN|YLO|AMB|RED)
1077 static const struct Token colors[] = {
1078 "BLU", "Blue", // 2500 ft, 8.0 km
1079 "WHT", "White", // 1500 ft, 5.0 km
1080 "GRN", "Green", // 700 ft, 3.7 km
1081 "YLO", "Yellow", // 300 ft, 1.6 km
1082 "AMB", "Amber", // 200 ft, 0.8 km
1083 "RED", "Red", // <200 ft, <0.8 km
1088 bool SGMetar::scanColorState()
1091 const struct Token *a;
1092 if (!(a = scanToken(&m, colors)))
1094 if (!scanBoundary(&m))
1096 //printf(Y"Code %s\n"N, a->text);
1102 bool SGMetar::scanRemark()
1104 if (strncmp(_m, "RMK", 3))
1107 if (!scanBoundary(&_m))
1111 if (!scanRunwayReport()) {
1112 while (*_m && !isspace(*_m))
1121 bool SGMetar::scanRemainder()
1124 if (!(strncmp(m, "NOSIG", 5))) {
1126 if (scanBoundary(&m))
1127 _m = m; //_comment.push_back("No significant tendency");
1130 if (!scanBoundary(&m))
1137 bool SGMetar::scanBoundary(char **s)
1139 if (**s && !isspace(**s))
1141 while (isspace(**s))
1147 int SGMetar::scanNumber(char **src, int *num, int min, int max)
1152 for (i = 0; i < min; i++) {
1156 *num = *num * 10 + *s++ - '0';
1158 for (; i < max && isdigit(*s); i++)
1159 *num = *num * 10 + *s++ - '0';
1165 // find longest match of str in list
1166 const struct Token *SGMetar::scanToken(char **str, const struct Token *list)
1168 const struct Token *longest = 0;
1169 int maxlen = 0, len;
1171 for (int i = 0; (s = list[i].id); i++) {
1173 if (!strncmp(s, *str, len) && len > maxlen) {
1183 void SGMetarCloud::set(double alt, int cov)
1191 void SGMetarVisibility::set(double dist, int dir, int mod, int tend)