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, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
25 * Interface for encoded Meteorological Aerodrome Reports (METAR).
28 # include <simgear_config.h>
35 #include <simgear/io/sg_socket.hxx>
36 #include <simgear/debug/logstream.hxx>
37 #include <simgear/structure/exception.hxx>
41 #define NaN SGMetarNaN
44 * The constructor takes a Metar string, or a four-letter ICAO code. In the
45 * latter case the metar string is downloaded from
46 * http://weather.noaa.gov/pub/data/observations/metar/stations/.
47 * The constructor throws sg_io_exceptions on failure. The "METAR"
48 * keyword has no effect (apart from incrementing the group counter
49 * @a grpcount) and can be left away. A keyword "SPECI" is
52 * @param m ICAO station id or metar string
53 * @param proxy proxy host (optional; default: "")
54 * @param port proxy port (optional; default: "80")
55 * @param auth proxy authorization information (optional; default: "")
59 * SGMetar *m = new SGMetar("METAR KSFO 061656Z 19004KT 9SM SCT100 OVC200 08/03 A3013");
60 * double t = m->getTemperature_F();
63 * SGMetar n("KSFO", "proxy.provider.foo", "3128", "proxy-password");
64 * double d = n.getDewpoint_C();
67 SGMetar::SGMetar(const string& m, const string& proxy, const string& port,
68 const string& auth, const time_t time) :
91 if (m.length() == 4 && isalnum(m[0]) && isalnum(m[1]) && isalnum(m[2]) && isalnum(m[3])) {
92 for (int i = 0; i < 4; i++)
93 _icao[i] = toupper(m[i]);
95 _data = loadData(_icao, proxy, port, auth, time);
97 _data = new char[m.length() + 2]; // make room for " \0"
98 strcpy(_data, m.c_str());
107 if (!scanPreambleDate())
113 if (!scanId() || !scanDate()) {
115 throw sg_io_exception("metar data bogus ", sg_location(_url));
122 while (scanVisibility()) ;
123 while (scanRwyVisRange()) ;
124 while (scanWeather()) ;
125 while (scanSkyCondition()) ;
128 while (scanSkyCondition()) ;
129 while (scanRunwayReport()) ;
133 while (scanColorState()) ;
135 while (scanRunwayReport()) ;
141 throw sg_io_exception("metar data incomplete ", sg_location(_url));
149 * Clears lists and maps to discourage access after destruction.
160 void SGMetar::useCurrentDate()
163 time_t now_sec = time(0);
165 now = *gmtime(&now_sec);
167 gmtime_r(&now_sec, &now);
169 _year = now.tm_year + 1900;
170 _month = now.tm_mon + 1;
175 * If called with "KSFO" loads data from
177 * http://weather.noaa.gov/pub/data/observations/metar/stations/KSFO.TXT.
179 * Throws sg_io_exception on failure. Gives up after waiting longer than 10 seconds.
181 * @param id four-letter ICAO Metar station code, e.g. "KSFO".
182 * @param proxy proxy host (optional; default: "")
183 * @param port proxy port (optional; default: "80")
184 * @param auth proxy authorization information (optional; default: "")
185 * @return pointer to Metar data string, allocated by new char[].
186 * @see rfc2068.txt for proxy spec ("Proxy-Authorization")
188 char *SGMetar::loadData(const char *id, const string& proxy, const string& port,
189 const string& auth, time_t time)
191 const int buflen = 512;
192 char buf[2 * buflen];
194 string metar_server = "weather.noaa.gov";
195 string host = proxy.empty() ? metar_server : proxy;
196 string path = "/pub/data/observations/metar/stations/";
198 path += string(id) + ".TXT";
199 _url = "http://" + metar_server + path;
201 SGSocket *sock = new SGSocket(host, port.empty() ? "80" : port, "tcp");
202 sock->set_timeout(10000);
203 if (!sock->open(SG_IO_OUT)) {
205 throw sg_io_exception("cannot connect to ", sg_location(host));
210 get += "http://" + metar_server;
212 sprintf(buf, "%ld", time);
213 get += path + " HTTP/1.0\015\012X-Time: " + buf + "\015\012";
214 get += "Host: " + metar_server + "\015\012";
217 get += "Proxy-Authorization: " + auth + "\015\012";
220 sock->writestring(get.c_str());
225 while ((i = sock->readline(buf, buflen))) {
226 if (i <= 2 && isspace(buf[0]) && (!buf[1] || isspace(buf[1])))
228 if (!strncmp(buf, "X-MetarProxy: ", 13))
232 i = sock->readline(buf, buflen);
234 sock->readline(&buf[i], buflen);
243 throw sg_io_exception("no metar data available from ",
246 char *metar = new char[strlen(b) + 2]; // make room for " \0"
253 * Replace any number of subsequent spaces by just one space, and add
254 * a trailing space. This makes scanning for things like "ALL RWY" easier.
256 void SGMetar::normalizeData()
259 for (src = dest = _data; (*dest++ = *src++); )
260 while (*src == ' ' && src[1] == ' ')
262 for (dest--; isspace(*--dest); ) ;
268 // \d\d\d\d/\d\d/\d\d
269 bool SGMetar::scanPreambleDate()
272 int year, month, day;
273 if (!scanNumber(&m, &year, 4))
277 if (!scanNumber(&m, &month, 2))
281 if (!scanNumber(&m, &day, 2))
283 if (!scanBoundary(&m))
294 bool SGMetar::scanPreambleTime()
298 if (!scanNumber(&m, &hour, 2))
302 if (!scanNumber(&m, &minute, 2))
304 if (!scanBoundary(&m))
314 bool SGMetar::scanType()
316 if (strncmp(_m, "METAR ", 6) && strncmp(_m, "SPECI ", 6))
325 bool SGMetar::scanId()
328 for (int i = 0; i < 4; m++, i++)
329 if (!(isalpha(*m) || isdigit(*m)))
331 if (!scanBoundary(&m))
333 strncpy(_icao, _m, 4);
342 bool SGMetar::scanDate()
345 int day, hour, minute;
346 if (!scanNumber(&m, &day, 2))
348 if (!scanNumber(&m, &hour, 2))
350 if (!scanNumber(&m, &minute, 2))
354 if (!scanBoundary(&m))
365 // (NIL|AUTO|COR|RTD)
366 bool SGMetar::scanModifier()
370 if (!strncmp(m, "NIL", 3)) {
374 if (!strncmp(m, "AUTO", 4)) // automatically generated
376 else if (!strncmp(m, "COR", 3)) // manually corrected
378 else if (!strncmp(m, "RTD", 3)) // routine delayed
382 if (!scanBoundary(&m))
391 // (\d{3}|VRB)\d{1,3}(G\d{2,3})?(KT|KMH|MPS)
392 bool SGMetar::scanWind()
396 if (!strncmp(m, "VRB", 3))
398 else if (!scanNumber(&m, &dir, 3))
402 if (!scanNumber(&m, &i, 2, 3))
409 if (!scanNumber(&m, &i, 2, 3))
414 if (!strncmp(m, "KT", 2))
415 m += 2, factor = SG_KT_TO_MPS;
416 else if (!strncmp(m, "KMH", 3))
417 m += 3, factor = SG_KMH_TO_MPS;
418 else if (!strncmp(m, "KPH", 3)) // ??
419 m += 3, factor = SG_KMH_TO_MPS;
420 else if (!strncmp(m, "MPS", 3))
421 m += 3, factor = 1.0;
424 if (!scanBoundary(&m))
428 _wind_speed = speed * factor;
430 _gust_speed = gust * factor;
437 bool SGMetar::scanVariability()
441 if (!scanNumber(&m, &from, 3))
445 if (!scanNumber(&m, &to, 3))
447 if (!scanBoundary(&m))
450 _wind_range_from = from;
457 bool SGMetar::scanVisibility()
458 // TODO: if only directed vis are given, do still set min/max
460 if (!strncmp(_m, "//// ", 5)) { // spec compliant?
469 int modifier = SGMetarVisibility::EQUALS;
470 // \d{4}(N|NE|E|SE|S|SW|W|NW)?
471 if (scanNumber(&m, &i, 4)) {
472 if( strncmp( m, "NDV",3 ) == 0 ) {
473 m+=3; // tolerate NDV (no directional validation)
474 } else if (*m == 'E') {
476 } else if (*m == 'W') {
478 } else if (*m == 'N') {
486 } else if (*m == 'S') {
496 i = 50, modifier = SGMetarVisibility::LESS_THAN;
498 i++, modifier = SGMetarVisibility::GREATER_THAN;
501 // M?(\d{1,2}|\d{1,2}/\d{1,2}|\d{1,2} \d{1,2}/\d{1,2})(SM|KM)
503 m++, modifier = SGMetarVisibility::LESS_THAN;
505 if (!scanNumber(&m, &i, 1, 2))
511 if (!scanNumber(&m, &i, 1, 2))
514 } else if (*m == ' ') {
517 if (!scanNumber(&m, &i, 1, 2))
521 if (!scanNumber(&m, &denom, 1, 2))
523 distance += (double)i / denom;
526 if (!strncmp(m, "SM", 2))
527 distance *= SG_SM_TO_METER, m += 2;
528 else if (!strncmp(m, "KM", 2))
529 distance *= 1000, m += 2;
533 if (!scanBoundary(&m))
536 SGMetarVisibility *v;
538 v = &_dir_visibility[dir / 45];
539 else if (_min_visibility._distance == NaN)
540 v = &_min_visibility;
542 v = &_max_visibility;
544 v->_distance = distance;
545 v->_modifier = modifier;
553 // R\d\d[LCR]?/([PM]?\d{4}V)?[PM]?\d{4}(FT)?[DNU]?
554 bool SGMetar::scanRwyVisRange()
561 if (!scanNumber(&m, &i, 2))
563 if (*m == 'L' || *m == 'C' || *m == 'R')
567 strncpy(id, _m + 1, i = m - _m - 1);
575 m++, r._min_visibility._modifier = SGMetarVisibility::GREATER_THAN;
577 m++, r._min_visibility._modifier = SGMetarVisibility::LESS_THAN;
578 if (!scanNumber(&m, &from, 4))
583 m++, r._max_visibility._modifier = SGMetarVisibility::GREATER_THAN;
585 m++, r._max_visibility._modifier = SGMetarVisibility::LESS_THAN;
586 if (!scanNumber(&m, &to, 4))
591 if (!strncmp(m, "FT", 2)) {
592 from = int(from * SG_FEET_TO_METER);
593 to = int(to * SG_FEET_TO_METER);
596 r._min_visibility._distance = from;
597 r._max_visibility._distance = to;
599 if (*m == '/') // this is not in the spec!
602 m++, r._min_visibility._tendency = SGMetarVisibility::DECREASING;
604 m++, r._min_visibility._tendency = SGMetarVisibility::STABLE;
606 m++, r._min_visibility._tendency = SGMetarVisibility::INCREASING;
608 if (!scanBoundary(&m))
612 _runways[id]._min_visibility = r._min_visibility;
613 _runways[id]._max_visibility = r._max_visibility;
619 static const struct Token special[] = {
620 { "NSW", "no significant weather" },
621 { "VCSH", "showers in the vicinity" },
622 { "VCTS", "thunderstorm in the vicinity" },
627 static const struct Token description[] = {
628 { "SH", "showers of" },
629 { "TS", "thunderstorm with" },
630 { "BC", "patches of" },
632 { "DR", "low drifting" },
633 { "FZ", "freezing" },
640 static const struct Token phenomenon[] = {
643 { "GS", "small hail and/or snow pellets" },
644 { "IC", "ice crystals" },
645 { "PE", "ice pellets" },
647 { "SG", "snow grains" },
649 { "UP", "unknown precipitation" },
651 { "DU", "widespread dust" },
653 { "FGBR", "fog bank" },
658 { "VA", "volcanic ash" },
659 { "DS", "duststorm" },
660 { "FC", "funnel cloud/tornado waterspout" },
661 { "PO", "well-developed dust/sand whirls" },
663 { "SS", "sandstorm" },
664 { "UP", "unknown" }, // ... due to failed automatic acquisition
669 // (+|-|VC)?(NSW|MI|PR|BC|DR|BL|SH|TS|FZ)?((DZ|RA|SN|SG|IC|PE|GR|GS|UP){0,3})(BR|FG|FU|VA|DU|SA|HZ|PY|PO|SQ|FC|SS|DS){0,3}
670 bool SGMetar::scanWeather()
674 const struct Token *a;
675 if ((a = scanToken(&m, special))) {
676 if (!scanBoundary(&m))
678 _weather.push_back(a->text);
685 m++, pre = "light ", _intensity = 1;
687 m++, pre = "heavy ", _intensity = 3;
688 else if (!strncmp(m, "VC", 2))
689 m += 2, post = "in the vicinity ";
691 pre = "moderate ", _intensity = 2;
693 vector<string> descriptions;
695 for (i = 0; i < 3; i++) {
696 if (!(a = scanToken(&m, description)))
698 descriptions.push_back(a->id);
699 weather += string(a->text) + " ";
702 for (i = 0; i < 3; i++) {
703 if (!(a = scanToken(&m, phenomenon)))
705 _phenomena[a->id] = descriptions;
706 weather += string(a->text) + " ";
707 if (!strcmp(a->id, "RA"))
709 else if (!strcmp(a->id, "HA"))
711 else if (!strcmp(a->id, "SN"))
714 if (!weather.length())
716 if (!scanBoundary(&m))
719 weather = pre + weather + post;
720 weather.erase(weather.length() - 1);
721 _weather.push_back(weather);
727 static const struct Token cloud_types[] = {
728 { "AC", "altocumulus" },
729 { "ACC", "altocumulus castellanus" },
730 { "ACSL", "altocumulus standing lenticular" },
731 { "AS", "altostratus" },
732 { "CB", "cumulonimbus" },
733 { "CBMAM", "cumulonimbus mammatus" },
734 { "CC", "cirrocumulus" },
735 { "CCSL", "cirrocumulus standing lenticular" },
737 { "CS", "cirrostratus" },
739 { "CUFRA", "cumulus fractus" },
740 { "NS", "nimbostratus" },
741 { "SAC", "stratoaltocumulus" }, // guessed
742 { "SC", "stratocumulus" },
743 { "SCSL", "stratocumulus standing lenticular" },
745 { "STFRA", "stratus fractus" },
746 { "TCU", "towering cumulus" },
751 // (FEW|SCT|BKN|OVC|SKC|CLR|CAVOK|VV)([0-9]{3}|///)?[:cloud_type:]?
752 bool SGMetar::scanSkyCondition()
758 if (!strncmp(m, "//////", 6)) {
760 if (!scanBoundary(&m))
766 if (!strncmp(m, "CLR", i = 3) // clear
767 || !strncmp(m, "SKC", i = 3) // sky clear
768 || !strncmp(m, "NSC", i = 3) // no significant clouds
769 || !strncmp(m, "CAVOK", i = 5)) { // ceiling and visibility OK (implies 9999)
771 if (!scanBoundary(&m))
776 _clouds.push_back(cl);
784 if (!strncmp(m, "VV", i = 2)) // vertical visibility
786 else if (!strncmp(m, "FEW", i = 3))
788 else if (!strncmp(m, "SCT", i = 3))
790 else if (!strncmp(m, "BKN", i = 3))
792 else if (!strncmp(m, "OVC", i = 3))
798 if (!strncmp(m, "///", 3)) // vis not measurable (e.g. because of heavy snowing)
800 else if (scanBoundary(&m)) {
802 return true; // ignore single OVC/BKN/...
803 } else if (!scanNumber(&m, &i, 3))
806 if (cl._coverage == -1) {
807 if (!scanBoundary(&m))
809 if (i == -1) // 'VV///'
810 _vert_visibility._modifier = SGMetarVisibility::NOGO;
812 _vert_visibility._distance = i * 100 * SG_FEET_TO_METER;
818 cl._altitude = i * 100 * SG_FEET_TO_METER;
820 const struct Token *a;
821 if ((a = scanToken(&m, cloud_types))) {
823 cl._type_long = a->text;
825 if (!scanBoundary(&m))
827 _clouds.push_back(cl);
834 // M?[0-9]{2}/(M?[0-9]{2})? (spec)
835 // (M?[0-9]{2}|XX)/(M?[0-9]{2}|XX)? (Namibia)
836 bool SGMetar::scanTemperature()
839 int sign = 1, temp, dew;
840 if (!strncmp(m, "XX/XX", 5)) { // not spec compliant!
842 return scanBoundary(&_m);
847 if (!scanNumber(&m, &temp, 2))
853 if (!scanBoundary(&m)) {
854 if (!strncmp(m, "XX", 2)) // not spec compliant!
855 m += 2, sign = 0, dew = temp;
860 if (!scanNumber(&m, &dew, 2))
863 if (!scanBoundary(&m))
875 double SGMetar::getRelHumidity() const
877 if (_temp == NaN || _dewp == NaN)
879 double dewp = pow(10.0, 7.5 * _dewp / (237.7 + _dewp));
880 double temp = pow(10.0, 7.5 * _temp / (237.7 + _temp));
881 return dewp * 100 / temp;
886 // [AQ]\d{2}(\d{2}|//) (Namibia)
887 bool SGMetar::scanPressure()
894 factor = SG_INHG_TO_PA / 100;
900 if (!scanNumber(&m, &press, 2))
903 if (!strncmp(m, "//", 2)) // not spec compliant!
905 else if (scanNumber(&m, &i, 2))
909 if (!scanBoundary(&m))
911 _pressure = press * factor;
918 static const char *runway_deposit[] = {
932 static const char *runway_deposit_extent[] = {
933 0, "1-10%", "11-25%", 0, 0, "26-50%", 0, 0, 0, "51-100%"
937 static const char *runway_friction[] = {
939 "poor braking action",
940 "poor/medium braking action",
941 "medium braking action",
942 "medium/good braking action",
943 "good braking action",
945 "friction: unreliable measurement"
949 // \d\d(CLRD|[\d/]{4})(\d\d|//)
950 bool SGMetar::scanRunwayReport()
957 if (!scanNumber(&m, &i, 2))
962 strcpy(id, "REP"); // repetition of previous report
965 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = 'R', id[3] = '\0';
967 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = '\0';
969 if (!strncmp(m, "CLRD", 4)) {
970 m += 4; // runway cleared
971 r._deposit_string = "cleared";
973 if (scanNumber(&m, &i, 1)) {
975 r._deposit_string = runway_deposit[i];
976 } else if (*m == '/')
981 if (*m == '1' || *m == '2' || *m == '5' || *m == '9') { // extent of deposit
982 r._extent = *m - '0';
983 r._extent_string = runway_deposit_extent[*m - '0'];
984 } else if (*m != '/')
989 if (!strncmp(m, "//", 2))
991 else if (!scanNumber(&m, &i, 2))
995 r._depth = 0.0005; // < 1 mm deep (let's say 0.5 :-)
996 else if (i > 0 && i <= 90)
997 r._depth = i / 1000.0; // i mm deep
998 else if (i >= 92 && i <= 98)
999 r._depth = (i - 90) / 20.0;
1001 r._comment = "runway not in use";
1002 else if (i == -1) // no depth given ("//")
1008 if (m[0] == '/' && m[1] == '/')
1010 else if (!scanNumber(&m, &i, 2))
1012 if (i >= 1 && i < 90) {
1013 r._friction = i / 100.0;
1014 } else if ((i >= 91 && i <= 95) || i == 99) {
1015 r._friction_string = runway_friction[i - 90];
1017 if (!scanBoundary(&m))
1020 _runways[id]._deposit = r._deposit;
1021 _runways[id]._deposit_string = r._deposit_string;
1022 _runways[id]._extent = r._extent;
1023 _runways[id]._extent_string = r._extent_string;
1024 _runways[id]._depth = r._depth;
1025 _runways[id]._friction = r._friction;
1026 _runways[id]._friction_string = r._friction_string;
1027 _runways[id]._comment = r._comment;
1034 // WS (ALL RWYS?|RWY ?\d\d[LCR]?)?
1035 bool SGMetar::scanWindShear()
1038 if (strncmp(m, "WS", 2))
1041 if (!scanBoundary(&m))
1044 if (!strncmp(m, "ALL", 3)) {
1046 if (!scanBoundary(&m))
1048 if (strncmp(m, "RWY", 3))
1053 if (!scanBoundary(&m))
1055 _runways["ALL"]._wind_shear = true;
1062 for (cnt = 0;; cnt++) { // ??
1063 if (strncmp(m, "RWY", 3))
1068 if (!scanNumber(&m, &i, 2))
1070 if (*m == 'L' || *m == 'C' || *m == 'R')
1072 strncpy(id, mm, i = m - mm);
1074 if (!scanBoundary(&m))
1076 _runways[id]._wind_shear = true;
1079 _runways["ALL"]._wind_shear = true;
1085 bool SGMetar::scanTrendForecast()
1088 if (strncmp(m, "NOSIG", 5))
1092 if (!scanBoundary(&m))
1099 // (BLU|WHT|GRN|YLO|AMB|RED)
1100 static const struct Token colors[] = {
1101 { "BLU", "Blue" }, // 2500 ft, 8.0 km
1102 { "WHT", "White" }, // 1500 ft, 5.0 km
1103 { "GRN", "Green" }, // 700 ft, 3.7 km
1104 { "YLO", "Yellow" }, // 300 ft, 1.6 km
1105 { "AMB", "Amber" }, // 200 ft, 0.8 km
1106 { "RED", "Red" }, // <200 ft, <0.8 km
1111 bool SGMetar::scanColorState()
1114 const struct Token *a;
1115 if (!(a = scanToken(&m, colors)))
1117 if (!scanBoundary(&m))
1119 //printf(Y"Code %s\n"N, a->text);
1125 bool SGMetar::scanRemark()
1127 if (strncmp(_m, "RMK", 3))
1130 if (!scanBoundary(&_m))
1134 if (!scanRunwayReport()) {
1135 while (*_m && !isspace(*_m))
1144 bool SGMetar::scanRemainder()
1147 if (!(strncmp(m, "NOSIG", 5))) {
1149 if (scanBoundary(&m))
1150 _m = m; //_comment.push_back("No significant tendency");
1153 if (!scanBoundary(&m))
1160 bool SGMetar::scanBoundary(char **s)
1162 if (**s && !isspace(**s))
1164 while (isspace(**s))
1170 int SGMetar::scanNumber(char **src, int *num, int min, int max)
1175 for (i = 0; i < min; i++) {
1179 *num = *num * 10 + *s++ - '0';
1181 for (; i < max && isdigit(*s); i++)
1182 *num = *num * 10 + *s++ - '0';
1188 // find longest match of str in list
1189 const struct Token *SGMetar::scanToken(char **str, const struct Token *list)
1191 const struct Token *longest = 0;
1192 int maxlen = 0, len;
1194 for (int i = 0; (s = list[i].id); i++) {
1196 if (!strncmp(s, *str, len) && len > maxlen) {
1206 void SGMetarCloud::set(double alt, int cov)
1214 void SGMetarVisibility::set(double dist, int dir, int mod, int tend)