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 SGMetar aviation weather data.
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 SGMetar 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
49 * SGMetar *m = new SGMetar("METAR KSFO 061656Z 19004KT 9SM SCT100 OVC200 08/03 A3013");
50 * double t = m->getTemperature();
54 * double d = n.getDewpoint_C();
57 SGMetar::SGMetar(const char *m) :
75 if (isalpha(m[0]) && isalpha(m[1]) && isalpha(m[2]) && isalpha(m[3]) && !m[4]) {
76 for (i = 0; i < 4; i++)
77 _icao[i] = toupper(m[i]);
79 _data = loadData(_icao);
81 _data = new char[strlen(m) + 1];
95 if (!scanId() || !scanDate())
96 throw sg_io_exception("metar data incomplete");
102 while (scanVisibility()) ;
103 while (scanRwyVisRange()) ;
104 while (scanWeather()) ;
105 while (scanSkyCondition()) ;
108 while (scanSkyCondition()) ;
109 while (scanRunwayReport()) ;
113 while (scanColorState()) ;
115 while (scanRunwayReport()) ;
120 throw sg_io_exception("metar data invalid");
125 * Clears lists and maps to discourage access after destruction.
137 * If called with "KSFO" loads data from
139 * http://weather.noaa.gov/pub/data/observations/metar/stations/KSFO.TXT.
141 * Throws sg_io_exception on failure. Gives up after waiting longer than 10 seconds.
143 * @param id four-letter ICAO SGMetar station code, e.g. "KSFO".
144 * @return pointer to SGMetar data string, allocated by new char[].
146 char *SGMetar::loadData(const char *id)
148 string host = "weather.noaa.gov";
149 string path = "/pub/data/observations/metar/stations/";
150 path += string(id) + ".TXT";
151 string get = string("GET ") + path + " HTTP/1.0\r\n\r\n";
153 SGSocket *sock = new SGSocket(host, "80", "tcp");
154 sock->set_timeout(10000);
155 if (!sock->open(SG_IO_OUT)) {
157 string err = "failed to load metar data from http://" + host + path;
158 throw sg_io_exception(err);
161 sock->writestring(get.c_str());
164 const int buflen = 512;
165 char buf[2 * buflen];
168 while ((i = sock->readline(buf, buflen)))
169 if (i <= 2 && isspace(buf[0]) && (!buf[1] || isspace(buf[1])))
172 i = sock->readline(buf, buflen);
174 sock->readline(&buf[i], buflen);
180 char *metar = new char[strlen(buf) + 1];
187 * Replace any number of subsequent spaces by just one space.
188 * This makes scanning for things like "ALL RWY" easier.
190 void SGMetar::normalizeData()
193 for (src = dest = _data; (*dest++ = *src++); )
194 while (*src == ' ' && src[1] == ' ')
199 // \d\d\d\d/\d\d/\d\d
200 bool SGMetar::scanPreambleDate()
203 int year, month, day;
204 if (!scanNumber(&m, &year, 4))
208 if (!scanNumber(&m, &month, 2))
212 if (!scanNumber(&m, &day, 2))
214 if (!scanBoundary(&m))
225 bool SGMetar::scanPreambleTime()
229 if (!scanNumber(&m, &hour, 2))
233 if (!scanNumber(&m, &minute, 2))
235 if (!scanBoundary(&m))
245 bool SGMetar::scanType()
247 if (strncmp(_m, "METAR ", 6) && strncmp(_m, "SPECI ", 6))
256 bool SGMetar::scanId()
259 if (!(isupper(*m++) && isupper(*m++) && isupper(*m++) && isupper(*m++)))
261 if (!scanBoundary(&m))
263 strncpy(_icao, _m, 4);
272 bool SGMetar::scanDate()
275 int day, hour, minute;
276 if (!scanNumber(&m, &day, 2))
278 if (!scanNumber(&m, &hour, 2))
280 if (!scanNumber(&m, &minute, 2))
284 if (!scanBoundary(&m))
295 // (NIL|AUTO|COR|RTD)
296 bool SGMetar::scanModifier()
300 if (!strncmp(m, "NIL", 3)) {
304 if (!strncmp(m, "AUTO", 4)) // automatically generated
306 else if (!strncmp(m, "COR", 3)) // manually corrected
308 else if (!strncmp(m, "RTD", 3)) // routine delayed
312 if (!scanBoundary(&m))
321 // (\d{3}|VRB)\d{1,3}(G\d{2,3})?(KT|KMH|MPS)
322 bool SGMetar::scanWind()
326 if (!strncmp(m, "VRB", 3))
328 else if (!scanNumber(&m, &dir, 3))
332 if (!scanNumber(&m, &i, 2, 3))
339 if (!scanNumber(&m, &i, 2, 3))
344 if (!strncmp(m, "KT", 2))
345 m += 2, factor = SG_KT_TO_MPS;
346 else if (!strncmp(m, "KMH", 3))
347 m += 3, factor = SG_KMH_TO_MPS;
348 else if (!strncmp(m, "KPH", 3)) // ??
349 m += 3, factor = SG_KMH_TO_MPS;
350 else if (!strncmp(m, "MPS", 3))
351 m += 3, factor = 1.0;
354 if (!scanBoundary(&m))
358 _wind_speed = speed * factor;
360 _gust_speed = gust * factor;
367 bool SGMetar::scanVariability()
371 if (!scanNumber(&m, &from, 3))
375 if (!scanNumber(&m, &to, 3))
377 if (!scanBoundary(&m))
380 _wind_range_from = from;
387 bool SGMetar::scanVisibility()
388 // TODO: if only directed vis are given, do still set min/max
393 int modifier = SGMetarVisibility::EQUALS;
394 // \d{4}(N|NE|E|SE|S|SW|W|NW)?
395 if (scanNumber(&m, &i, 4)) {
400 else if (*m == 'N') {
408 } else if (*m == 'S') {
418 i = 50, modifier = SGMetarVisibility::LESS_THAN;
420 i++, modifier = SGMetarVisibility::GREATER_THAN;
423 // M?(\d{1,2}|\d{1,2}/\d{1,2}|\d{1,2} \d{1,2}/\d{1,2})(SM|KM)
426 m++, modifier = SGMetarVisibility::LESS_THAN;
428 if (!scanNumber(&m, &i, 1, 2))
434 if (!scanNumber(&m, &i, 1, 2))
437 } else if (*m == ' ') {
440 if (!scanNumber(&m, &i, 1, 2))
444 if (!scanNumber(&m, &denom, 1, 2))
446 distance += (double)i / denom;
449 if (!strncmp(m, "SM", 2))
450 distance *= SG_SM_TO_METER, m += 2;
451 else if (!strncmp(m, "KM", 2))
452 distance *= 1000, m += 2;
456 if (!scanBoundary(&m))
459 SGMetarVisibility *v;
461 v = &_dir_visibility[dir / 45];
462 else if (_min_visibility._distance == NaN)
463 v = &_min_visibility;
465 v = &_max_visibility;
467 v->_distance = distance;
468 v->_modifier = modifier;
476 // R\d\d[LCR]?/([PM]?\d{4}V)?[PM]?\d{4}(FT)?[DNU]?
477 bool SGMetar::scanRwyVisRange()
484 if (!scanNumber(&m, &i, 2))
486 if (*m == 'L' || *m == 'C' || *m == 'R')
490 strncpy(id, _m + 1, i = m - _m - 1);
498 m++, r._min_visibility._modifier = SGMetarVisibility::GREATER_THAN;
500 m++, r._min_visibility._modifier = SGMetarVisibility::LESS_THAN;
501 if (!scanNumber(&m, &from, 4))
506 m++, r._max_visibility._modifier = SGMetarVisibility::GREATER_THAN;
508 m++, r._max_visibility._modifier = SGMetarVisibility::LESS_THAN;
509 if (!scanNumber(&m, &to, 4))
514 if (!strncmp(m, "FT", 2)) {
515 from = int(from * SG_FEET_TO_METER);
516 to = int(to * SG_FEET_TO_METER);
519 r._min_visibility._distance = from;
520 r._max_visibility._distance = to;
522 if (*m == '/') // this is not in the spec!
525 m++, r._min_visibility._tendency = SGMetarVisibility::DECREASING;
527 m++, r._min_visibility._tendency = SGMetarVisibility::STABLE;
529 m++, r._min_visibility._tendency = SGMetarVisibility::INCREASING;
531 if (!scanBoundary(&m))
535 _runways[id]._min_visibility = r._min_visibility;
536 _runways[id]._max_visibility = r._max_visibility;
542 static const struct Token special[] = {
543 "NSW", "no significant weather",
544 "VCSH", "showers in the vicinity",
545 "VCTS", "thunderstorm in the vicinity",
550 static const struct Token description[] = {
552 "TS", "thunderstorm with",
555 "DR", "low drifting",
563 static const struct Token phenomenon[] = {
566 "GS", "small hail and/or snow pellets",
567 "IC", "ice crystals",
572 "UP", "unknown precipitation",
574 "DU", "widespread dust",
581 "VA", "volcanic ash",
583 "FC", "funnel cloud/tornado waterspout",
584 "PO", "well-developed dust/sand whirls",
587 "UP", "unknown", // ... due to failed automatic acquisition
592 // (+|-|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}
593 bool SGMetar::scanWeather()
597 const struct Token *a;
598 if ((a = scanToken(&m, special))) {
599 if (!scanBoundary(&m))
601 _weather.push_back(a->text);
611 else if (!strncmp(m, "VC", 2))
612 m += 2, post = "in the vicinity ";
617 for (i = 0; i < 3; i++) {
618 if (!(a = scanToken(&m, description)))
620 weather += string(a->text) + " ";
622 for (i = 0; i < 3; i++) {
623 if (!(a = scanToken(&m, phenomenon)))
625 weather += string(a->text) + " ";
627 if (!weather.length())
629 if (!scanBoundary(&m))
632 weather = pre + weather + post;
633 weather.erase(weather.length() - 1);
634 _weather.push_back(weather);
640 static const struct Token cloud_types[] = {
642 "ACC", "altocumulus castellanus",
643 "ACSL", "altocumulus standing lenticular",
645 "CB", "cumulonimbus",
646 "CBMAM", "cumulonimbus mammatus",
647 "CC", "cirrocumulus",
648 "CCSL", "cirrocumulus standing lenticular",
650 "CS", "cirrostratus",
652 "CUFRA", "cumulus fractus",
653 "NS", "nimbostratus",
654 "SAC", "stratoaltocumulus", // guessed
655 "SC", "stratocumulus",
656 "SCSL", "stratocumulus standing lenticular",
658 "STFRA", "stratus fractus",
659 "TCU", "towering cumulus",
664 // (FEW|SCT|BKN|OVC|SKC|CLR|CAVOK|VV)([0-9]{3}|///)?[:cloud_type:]?
665 bool SGMetar::scanSkyCondition()
671 if (!strncmp(m, "CLR", i = 3) // clear
672 || !strncmp(m, "SKC", i = 3) // sky clear
673 || !strncmp(m, "NSC", i = 3) // no significant clouds
674 || !strncmp(m, "CAVOK", i = 5)) { // ceiling and visibility OK (implies 9999)
676 if (!scanBoundary(&m))
679 _clouds.push_back(cl);
684 if (!strncmp(m, "VV", i = 2)) // vertical visibility
686 else if (!strncmp(m, "FEW", i = 3))
688 else if (!strncmp(m, "SCT", i = 3))
690 else if (!strncmp(m, "BKN", i = 3))
692 else if (!strncmp(m, "OVC", i = 3))
698 if (!strncmp(m, "///", 3)) // vis not measurable (e.g. because of heavy snowing)
700 else if (scanBoundary(&m)) {
702 return true; // ignore single OVC/BKN/...
703 } else if (!scanNumber(&m, &i, 3))
706 if (cl._coverage == -1) {
707 if (!scanBoundary(&m))
709 if (i == -1) // 'VV///'
710 _vert_visibility._modifier = SGMetarVisibility::NOGO;
712 _vert_visibility._distance = i * 100 * SG_FEET_TO_METER;
718 cl._altitude = i * 100 * SG_FEET_TO_METER;
720 const struct Token *a;
721 if ((a = scanToken(&m, cloud_types))) {
723 cl._type_long = a->text;
725 if (!scanBoundary(&m))
727 _clouds.push_back(cl);
734 // M?[0-9]{2}/(M?[0-9]{2})? (spec)
735 // (M?[0-9]{2}|XX)/(M?[0-9]{2}|XX)? (Namibia)
736 bool SGMetar::scanTemperature()
739 int sign = 1, temp, dew;
740 if (!strncmp(m, "XX/XX", 5)) { // not spec compliant!
742 return scanBoundary(&_m);
747 if (!scanNumber(&m, &temp, 2))
753 if (!scanBoundary(&m)) {
754 if (!strncmp(m, "XX", 2)) // not spec compliant!
760 if (!scanNumber(&m, &dew, 2))
763 if (!scanBoundary(&m))
775 double SGMetar::getRelHumidity() const
777 if (_temp == NaN || _dewp == NaN)
779 double dewp = pow(10, 7.5 * _dewp / (237.7 + _dewp));
780 double temp = pow(10, 7.5 * _temp / (237.7 + _temp));
781 return dewp * 100 / temp;
786 // [AQ]\d{2}(\d{2}|//) (Namibia)
787 bool SGMetar::scanPressure()
794 factor = SG_INHG_TO_PA / 100;
800 if (!scanNumber(&m, &press, 2))
803 if (!strncmp(m, "//", 2)) // not spec compliant!
805 else if (scanNumber(&m, &i, 2))
809 if (!scanBoundary(&m))
811 _pressure = press * factor;
818 static const char *runway_deposit[] = {
832 static const char *runway_deposit_extent[] = {
833 0, "1-10%", "11-25%", 0, 0, "26-50%", 0, 0, 0, "51-100%"
837 static const char *runway_friction[] = {
839 "poor braking action",
840 "poor/medium braking action",
841 "medium braking action",
842 "medium/good braking action",
843 "good braking action",
845 "friction: unreliable measurement"
849 // \d\d(CLRD|[\d/]{4})(\d\d|//)
850 bool SGMetar::scanRunwayReport()
857 if (!scanNumber(&m, &i, 2))
862 strcpy(id, "REP"); // repetition of previous report
865 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = 'R', id[3] = '\0';
867 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = '\0';
869 if (!strncmp(m, "CLRD", 4)) {
870 m += 4; // runway cleared
871 r._deposit = "cleared";
873 if (scanNumber(&m, &i, 1)) {
874 r._deposit = runway_deposit[i];
875 } else if (*m == '/')
879 if (*m == '1' || *m == '2' || *m == '5' || *m == '9') { // extent of deposit
880 r._extent = *m - '0';
881 r._extent_string = runway_deposit_extent[*m - '0'];
882 } else if (*m != '/')
886 if (!strncmp(m, "//", 2))
888 else if (!scanNumber(&m, &i, 2))
892 r._depth = 0.5; // < 1 mm deep (let's say 0.5 :-)
893 else if (i > 0 && i <= 90)
894 r._depth = i / 1000.0; // i mm deep
895 else if (i >= 92 && i <= 98)
896 r._depth = (i - 90) / 20.0;
898 r._comment = "runway not in use";
899 else if (i == -1) // no depth given ("//")
905 if (m[0] == '/' && m[1] == '/')
907 else if (!scanNumber(&m, &i, 2))
909 if (i >= 1 && i < 90) {
910 r._friction = i / 100.0;
911 } else if ((i >= 91 && i <= 95) || i == 99) {
912 r._friction_string = runway_friction[i - 90];
914 if (!scanBoundary(&m))
917 _runways[id]._deposit = r._deposit;
918 _runways[id]._extent = r._extent;
919 _runways[id]._extent_string = r._extent_string;
920 _runways[id]._depth = r._depth;
921 _runways[id]._friction = r._friction;
922 _runways[id]._friction_string = r._friction_string;
923 _runways[id]._comment = r._comment;
930 // WS (ALL RWYS?|RWY ?\d\d[LCR]?)?
931 bool SGMetar::scanWindShear()
934 if (strncmp(m, "WS", 2))
937 if (!scanBoundary(&m))
940 if (!strncmp(m, "ALL", 3)) {
942 if (!scanBoundary(&m))
944 if (strncmp(m, "RWY", 3))
949 if (!scanBoundary(&m))
951 _runways["ALL"]._wind_shear = true;
958 for (cnt = 0;; cnt++) { // ??
959 if (strncmp(m, "RWY", 3))
964 if (!scanNumber(&m, &i, 2))
966 if (*m == 'L' || *m == 'C' || *m == 'R')
968 strncpy(id, mm, i = m - mm);
970 if (!scanBoundary(&m))
972 _runways[id]._wind_shear = true;
975 _runways["ALL"]._wind_shear = true;
981 bool SGMetar::scanTrendForecast()
984 if (strncmp(m, "NOSIG", 5))
988 if (!scanBoundary(&m))
995 // (BLU|WHT|GRN|YLO|AMB|RED)
996 static const struct Token colors[] = {
997 "BLU", "Blue", // 2500 ft, 8.0 km
998 "WHT", "White", // 1500 ft, 5.0 km
999 "GRN", "Green", // 700 ft, 3.7 km
1000 "YLO", "Yellow", // 300 ft, 1.6 km
1001 "AMB", "Amber", // 200 ft, 0.8 km
1002 "RED", "Red", // <200 ft, <0.8 km
1007 bool SGMetar::scanColorState()
1010 const struct Token *a;
1011 if (!(a = scanToken(&m, colors)))
1013 if (!scanBoundary(&m))
1015 //printf(Y"Code %s\n"N, a->text);
1021 bool SGMetar::scanRemark()
1023 if (strncmp(_m, "RMK", 3))
1026 if (!scanBoundary(&_m))
1030 if (!scanRunwayReport()) {
1031 while (*_m && !isspace(*_m))
1040 bool SGMetar::scanRemainder()
1043 if (!(strncmp(m, "NOSIG", 5))) {
1045 if (scanBoundary(&m))
1046 _m = m; //_comment.push_back("No significant tendency");
1049 if (!scanBoundary(&m))
1056 bool SGMetar::scanBoundary(char **s)
1058 if (**s && !isspace(**s))
1060 while (isspace(**s))
1066 int SGMetar::scanNumber(char **src, int *num, int min, int max)
1071 for (i = 0; i < min; i++) {
1075 *num = *num * 10 + *s++ - '0';
1077 for (; i < max && isdigit(*s); i++)
1078 *num = *num * 10 + *s++ - '0';
1084 // find longest match of str in list
1085 const struct Token *SGMetar::scanToken(char **str, const struct Token *list)
1087 const struct Token *longest = 0;
1088 int maxlen = 0, len;
1090 for (int i = 0; (s = list[i].id); i++) {
1092 if (!strncmp(s, *str, len) && len > maxlen) {