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 ", sg_location(_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 ", sg_location(_url));
144 * Clears lists and maps to discourage access after destruction.
155 void SGMetar::useCurrentDate()
158 time_t now_sec = time(0);
159 #if defined( _MSC_VER ) || defined ( __MINGW32__ )
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 ", sg_location(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 ",
239 char *metar = new char[strlen(b) + 2]; // make room for " \0"
246 * Replace any number of subsequent spaces by just one space, and add
247 * a trailing space. This makes scanning for things like "ALL RWY" easier.
249 void SGMetar::normalizeData()
252 for (src = dest = _data; (*dest++ = *src++); )
253 while (*src == ' ' && src[1] == ' ')
255 for (dest--; isspace(*--dest); ) ;
261 // \d\d\d\d/\d\d/\d\d
262 bool SGMetar::scanPreambleDate()
265 int year, month, day;
266 if (!scanNumber(&m, &year, 4))
270 if (!scanNumber(&m, &month, 2))
274 if (!scanNumber(&m, &day, 2))
276 if (!scanBoundary(&m))
287 bool SGMetar::scanPreambleTime()
291 if (!scanNumber(&m, &hour, 2))
295 if (!scanNumber(&m, &minute, 2))
297 if (!scanBoundary(&m))
307 bool SGMetar::scanType()
309 if (strncmp(_m, "METAR ", 6) && strncmp(_m, "SPECI ", 6))
318 bool SGMetar::scanId()
321 for (int i = 0; i < 4; m++, i++)
322 if (!(isalpha(*m) || isdigit(*m)))
324 if (!scanBoundary(&m))
326 strncpy(_icao, _m, 4);
335 bool SGMetar::scanDate()
338 int day, hour, minute;
339 if (!scanNumber(&m, &day, 2))
341 if (!scanNumber(&m, &hour, 2))
343 if (!scanNumber(&m, &minute, 2))
347 if (!scanBoundary(&m))
358 // (NIL|AUTO|COR|RTD)
359 bool SGMetar::scanModifier()
363 if (!strncmp(m, "NIL", 3)) {
367 if (!strncmp(m, "AUTO", 4)) // automatically generated
369 else if (!strncmp(m, "COR", 3)) // manually corrected
371 else if (!strncmp(m, "RTD", 3)) // routine delayed
375 if (!scanBoundary(&m))
384 // (\d{3}|VRB)\d{1,3}(G\d{2,3})?(KT|KMH|MPS)
385 bool SGMetar::scanWind()
389 if (!strncmp(m, "VRB", 3))
391 else if (!scanNumber(&m, &dir, 3))
395 if (!scanNumber(&m, &i, 2, 3))
402 if (!scanNumber(&m, &i, 2, 3))
407 if (!strncmp(m, "KT", 2))
408 m += 2, factor = SG_KT_TO_MPS;
409 else if (!strncmp(m, "KMH", 3))
410 m += 3, factor = SG_KMH_TO_MPS;
411 else if (!strncmp(m, "KPH", 3)) // ??
412 m += 3, factor = SG_KMH_TO_MPS;
413 else if (!strncmp(m, "MPS", 3))
414 m += 3, factor = 1.0;
417 if (!scanBoundary(&m))
421 _wind_speed = speed * factor;
423 _gust_speed = gust * factor;
430 bool SGMetar::scanVariability()
434 if (!scanNumber(&m, &from, 3))
438 if (!scanNumber(&m, &to, 3))
440 if (!scanBoundary(&m))
443 _wind_range_from = from;
450 bool SGMetar::scanVisibility()
451 // TODO: if only directed vis are given, do still set min/max
453 if (!strncmp(_m, "//// ", 5)) { // spec compliant?
462 int modifier = SGMetarVisibility::EQUALS;
463 // \d{4}(N|NE|E|SE|S|SW|W|NW)?
464 if (scanNumber(&m, &i, 4)) {
469 else if (*m == 'N') {
477 } else if (*m == 'S') {
487 i = 50, modifier = SGMetarVisibility::LESS_THAN;
489 i++, modifier = SGMetarVisibility::GREATER_THAN;
492 // M?(\d{1,2}|\d{1,2}/\d{1,2}|\d{1,2} \d{1,2}/\d{1,2})(SM|KM)
495 m++, modifier = SGMetarVisibility::LESS_THAN;
497 if (!scanNumber(&m, &i, 1, 2))
503 if (!scanNumber(&m, &i, 1, 2))
506 } else if (*m == ' ') {
509 if (!scanNumber(&m, &i, 1, 2))
513 if (!scanNumber(&m, &denom, 1, 2))
515 distance += (double)i / denom;
518 if (!strncmp(m, "SM", 2))
519 distance *= SG_SM_TO_METER, m += 2;
520 else if (!strncmp(m, "KM", 2))
521 distance *= 1000, m += 2;
525 if (!scanBoundary(&m))
528 SGMetarVisibility *v;
530 v = &_dir_visibility[dir / 45];
531 else if (_min_visibility._distance == NaN)
532 v = &_min_visibility;
534 v = &_max_visibility;
536 v->_distance = distance;
537 v->_modifier = modifier;
545 // R\d\d[LCR]?/([PM]?\d{4}V)?[PM]?\d{4}(FT)?[DNU]?
546 bool SGMetar::scanRwyVisRange()
553 if (!scanNumber(&m, &i, 2))
555 if (*m == 'L' || *m == 'C' || *m == 'R')
559 strncpy(id, _m + 1, i = m - _m - 1);
567 m++, r._min_visibility._modifier = SGMetarVisibility::GREATER_THAN;
569 m++, r._min_visibility._modifier = SGMetarVisibility::LESS_THAN;
570 if (!scanNumber(&m, &from, 4))
575 m++, r._max_visibility._modifier = SGMetarVisibility::GREATER_THAN;
577 m++, r._max_visibility._modifier = SGMetarVisibility::LESS_THAN;
578 if (!scanNumber(&m, &to, 4))
583 if (!strncmp(m, "FT", 2)) {
584 from = int(from * SG_FEET_TO_METER);
585 to = int(to * SG_FEET_TO_METER);
588 r._min_visibility._distance = from;
589 r._max_visibility._distance = to;
591 if (*m == '/') // this is not in the spec!
594 m++, r._min_visibility._tendency = SGMetarVisibility::DECREASING;
596 m++, r._min_visibility._tendency = SGMetarVisibility::STABLE;
598 m++, r._min_visibility._tendency = SGMetarVisibility::INCREASING;
600 if (!scanBoundary(&m))
604 _runways[id]._min_visibility = r._min_visibility;
605 _runways[id]._max_visibility = r._max_visibility;
611 static const struct Token special[] = {
612 "NSW", "no significant weather",
613 "VCSH", "showers in the vicinity",
614 "VCTS", "thunderstorm in the vicinity",
619 static const struct Token description[] = {
621 "TS", "thunderstorm with",
624 "DR", "low drifting",
632 static const struct Token phenomenon[] = {
635 "GS", "small hail and/or snow pellets",
636 "IC", "ice crystals",
641 "UP", "unknown precipitation",
643 "DU", "widespread dust",
650 "VA", "volcanic ash",
652 "FC", "funnel cloud/tornado waterspout",
653 "PO", "well-developed dust/sand whirls",
656 "UP", "unknown", // ... due to failed automatic acquisition
661 // (+|-|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}
662 bool SGMetar::scanWeather()
666 const struct Token *a;
667 if ((a = scanToken(&m, special))) {
668 if (!scanBoundary(&m))
670 _weather.push_back(a->text);
678 m++, pre = "light ", intensity = 1;
680 m++, pre = "heavy ", intensity = 3;
681 else if (!strncmp(m, "VC", 2))
682 m += 2, post = "in the vicinity ";
684 pre = "moderate ", intensity = 2;
687 for (i = 0; i < 3; i++) {
688 if (!(a = scanToken(&m, description)))
690 weather += string(a->text) + " ";
692 for (i = 0; i < 3; i++) {
693 if (!(a = scanToken(&m, phenomenon)))
695 weather += string(a->text) + " ";
696 if (!strcmp(a->id, "RA"))
698 else if (!strcmp(a->id, "HA"))
700 else if (!strcmp(a->id, "SN"))
703 if (!weather.length())
705 if (!scanBoundary(&m))
708 weather = pre + weather + post;
709 weather.erase(weather.length() - 1);
710 _weather.push_back(weather);
716 static const struct Token cloud_types[] = {
718 "ACC", "altocumulus castellanus",
719 "ACSL", "altocumulus standing lenticular",
721 "CB", "cumulonimbus",
722 "CBMAM", "cumulonimbus mammatus",
723 "CC", "cirrocumulus",
724 "CCSL", "cirrocumulus standing lenticular",
726 "CS", "cirrostratus",
728 "CUFRA", "cumulus fractus",
729 "NS", "nimbostratus",
730 "SAC", "stratoaltocumulus", // guessed
731 "SC", "stratocumulus",
732 "SCSL", "stratocumulus standing lenticular",
734 "STFRA", "stratus fractus",
735 "TCU", "towering cumulus",
740 // (FEW|SCT|BKN|OVC|SKC|CLR|CAVOK|VV)([0-9]{3}|///)?[:cloud_type:]?
741 bool SGMetar::scanSkyCondition()
747 if (!strncmp(m, "CLR", i = 3) // clear
748 || !strncmp(m, "SKC", i = 3) // sky clear
749 || !strncmp(m, "NSC", i = 3) // no significant clouds
750 || !strncmp(m, "CAVOK", i = 5)) { // ceiling and visibility OK (implies 9999)
752 if (!scanBoundary(&m))
757 _clouds.push_back(cl);
765 if (!strncmp(m, "VV", i = 2)) // vertical visibility
767 else if (!strncmp(m, "FEW", i = 3))
769 else if (!strncmp(m, "SCT", i = 3))
771 else if (!strncmp(m, "BKN", i = 3))
773 else if (!strncmp(m, "OVC", i = 3))
779 if (!strncmp(m, "///", 3)) // vis not measurable (e.g. because of heavy snowing)
781 else if (scanBoundary(&m)) {
783 return true; // ignore single OVC/BKN/...
784 } else if (!scanNumber(&m, &i, 3))
787 if (cl._coverage == -1) {
788 if (!scanBoundary(&m))
790 if (i == -1) // 'VV///'
791 _vert_visibility._modifier = SGMetarVisibility::NOGO;
793 _vert_visibility._distance = i * 100 * SG_FEET_TO_METER;
799 cl._altitude = i * 100 * SG_FEET_TO_METER;
801 const struct Token *a;
802 if ((a = scanToken(&m, cloud_types))) {
804 cl._type_long = a->text;
806 if (!scanBoundary(&m))
808 _clouds.push_back(cl);
815 // M?[0-9]{2}/(M?[0-9]{2})? (spec)
816 // (M?[0-9]{2}|XX)/(M?[0-9]{2}|XX)? (Namibia)
817 bool SGMetar::scanTemperature()
820 int sign = 1, temp, dew;
821 if (!strncmp(m, "XX/XX", 5)) { // not spec compliant!
823 return scanBoundary(&_m);
828 if (!scanNumber(&m, &temp, 2))
834 if (!scanBoundary(&m)) {
835 if (!strncmp(m, "XX", 2)) // not spec compliant!
841 if (!scanNumber(&m, &dew, 2))
844 if (!scanBoundary(&m))
856 double SGMetar::getRelHumidity() const
858 if (_temp == NaN || _dewp == NaN)
860 double dewp = pow(10.0, 7.5 * _dewp / (237.7 + _dewp));
861 double temp = pow(10.0, 7.5 * _temp / (237.7 + _temp));
862 return dewp * 100 / temp;
867 // [AQ]\d{2}(\d{2}|//) (Namibia)
868 bool SGMetar::scanPressure()
875 factor = SG_INHG_TO_PA / 100;
881 if (!scanNumber(&m, &press, 2))
884 if (!strncmp(m, "//", 2)) // not spec compliant!
886 else if (scanNumber(&m, &i, 2))
890 if (!scanBoundary(&m))
892 _pressure = press * factor;
899 static const char *runway_deposit[] = {
913 static const char *runway_deposit_extent[] = {
914 0, "1-10%", "11-25%", 0, 0, "26-50%", 0, 0, 0, "51-100%"
918 static const char *runway_friction[] = {
920 "poor braking action",
921 "poor/medium braking action",
922 "medium braking action",
923 "medium/good braking action",
924 "good braking action",
926 "friction: unreliable measurement"
930 // \d\d(CLRD|[\d/]{4})(\d\d|//)
931 bool SGMetar::scanRunwayReport()
938 if (!scanNumber(&m, &i, 2))
943 strcpy(id, "REP"); // repetition of previous report
946 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = 'R', id[3] = '\0';
948 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = '\0';
950 if (!strncmp(m, "CLRD", 4)) {
951 m += 4; // runway cleared
952 r._deposit_string = "cleared";
954 if (scanNumber(&m, &i, 1)) {
956 r._deposit_string = runway_deposit[i];
957 } else if (*m == '/')
961 if (*m == '1' || *m == '2' || *m == '5' || *m == '9') { // extent of deposit
962 r._extent = *m - '0';
963 r._extent_string = runway_deposit_extent[*m - '0'];
964 } else if (*m != '/')
968 if (!strncmp(m, "//", 2))
970 else if (!scanNumber(&m, &i, 2))
974 r._depth = 0.5; // < 1 mm deep (let's say 0.5 :-)
975 else if (i > 0 && i <= 90)
976 r._depth = i / 1000.0; // i mm deep
977 else if (i >= 92 && i <= 98)
978 r._depth = (i - 90) / 20.0;
980 r._comment = "runway not in use";
981 else if (i == -1) // no depth given ("//")
987 if (m[0] == '/' && m[1] == '/')
989 else if (!scanNumber(&m, &i, 2))
991 if (i >= 1 && i < 90) {
992 r._friction = i / 100.0;
993 } else if ((i >= 91 && i <= 95) || i == 99) {
994 r._friction_string = runway_friction[i - 90];
996 if (!scanBoundary(&m))
999 _runways[id]._deposit = r._deposit;
1000 _runways[id]._extent = r._extent;
1001 _runways[id]._extent_string = r._extent_string;
1002 _runways[id]._depth = r._depth;
1003 _runways[id]._friction = r._friction;
1004 _runways[id]._friction_string = r._friction_string;
1005 _runways[id]._comment = r._comment;
1012 // WS (ALL RWYS?|RWY ?\d\d[LCR]?)?
1013 bool SGMetar::scanWindShear()
1016 if (strncmp(m, "WS", 2))
1019 if (!scanBoundary(&m))
1022 if (!strncmp(m, "ALL", 3)) {
1024 if (!scanBoundary(&m))
1026 if (strncmp(m, "RWY", 3))
1031 if (!scanBoundary(&m))
1033 _runways["ALL"]._wind_shear = true;
1040 for (cnt = 0;; cnt++) { // ??
1041 if (strncmp(m, "RWY", 3))
1046 if (!scanNumber(&m, &i, 2))
1048 if (*m == 'L' || *m == 'C' || *m == 'R')
1050 strncpy(id, mm, i = m - mm);
1052 if (!scanBoundary(&m))
1054 _runways[id]._wind_shear = true;
1057 _runways["ALL"]._wind_shear = true;
1063 bool SGMetar::scanTrendForecast()
1066 if (strncmp(m, "NOSIG", 5))
1070 if (!scanBoundary(&m))
1077 // (BLU|WHT|GRN|YLO|AMB|RED)
1078 static const struct Token colors[] = {
1079 "BLU", "Blue", // 2500 ft, 8.0 km
1080 "WHT", "White", // 1500 ft, 5.0 km
1081 "GRN", "Green", // 700 ft, 3.7 km
1082 "YLO", "Yellow", // 300 ft, 1.6 km
1083 "AMB", "Amber", // 200 ft, 0.8 km
1084 "RED", "Red", // <200 ft, <0.8 km
1089 bool SGMetar::scanColorState()
1092 const struct Token *a;
1093 if (!(a = scanToken(&m, colors)))
1095 if (!scanBoundary(&m))
1097 //printf(Y"Code %s\n"N, a->text);
1103 bool SGMetar::scanRemark()
1105 if (strncmp(_m, "RMK", 3))
1108 if (!scanBoundary(&_m))
1112 if (!scanRunwayReport()) {
1113 while (*_m && !isspace(*_m))
1122 bool SGMetar::scanRemainder()
1125 if (!(strncmp(m, "NOSIG", 5))) {
1127 if (scanBoundary(&m))
1128 _m = m; //_comment.push_back("No significant tendency");
1131 if (!scanBoundary(&m))
1138 bool SGMetar::scanBoundary(char **s)
1140 if (**s && !isspace(**s))
1142 while (isspace(**s))
1148 int SGMetar::scanNumber(char **src, int *num, int min, int max)
1153 for (i = 0; i < min; i++) {
1157 *num = *num * 10 + *s++ - '0';
1159 for (; i < max && isdigit(*s); i++)
1160 *num = *num * 10 + *s++ - '0';
1166 // find longest match of str in list
1167 const struct Token *SGMetar::scanToken(char **str, const struct Token *list)
1169 const struct Token *longest = 0;
1170 int maxlen = 0, len;
1172 for (int i = 0; (s = list[i].id); i++) {
1174 if (!strncmp(s, *str, len) && len > maxlen) {
1184 void SGMetarCloud::set(double alt, int cov)
1192 void SGMetarVisibility::set(double dist, int dir, int mod, int tend)