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_F();
54 * double d = n.getDewpoint_C();
57 SGMetar::SGMetar(const char *m) :
74 if (isalnum(m[0]) && isalnum(m[1]) && isalnum(m[2]) && isalnum(m[3]) && !m[4]) {
75 for (int i = 0; i < 4; i++)
76 _icao[i] = toupper(m[i]);
78 _data = loadData(_icao);
80 _data = new char[strlen(m) + 2]; // make room for " \0"
95 if (!scanId() || !scanDate()) {
97 throw sg_io_exception("metar data incomplete (" + _url + ')');
104 while (scanVisibility()) ;
105 while (scanRwyVisRange()) ;
106 while (scanWeather()) ;
107 while (scanSkyCondition()) ;
110 while (scanSkyCondition()) ;
111 while (scanRunwayReport()) ;
115 while (scanColorState()) ;
117 while (scanRunwayReport()) ;
123 throw sg_io_exception("metar data invalid (" + _url + ')');
130 * Clears lists and maps to discourage access after destruction.
142 * If called with "KSFO" loads data from
144 * http://weather.noaa.gov/pub/data/observations/metar/stations/KSFO.TXT.
146 * Throws sg_io_exception on failure. Gives up after waiting longer than 10 seconds.
148 * @param id four-letter ICAO SGMetar station code, e.g. "KSFO".
149 * @return pointer to SGMetar data string, allocated by new char[].
151 char *SGMetar::loadData(const char *id)
153 string host = "weather.noaa.gov";
154 string path = "/pub/data/observations/metar/stations/";
155 path += string(id) + ".TXT";
156 _url = "http://" + host + path;
158 string get = string("GET ") + path + " HTTP/1.0\r\n\r\n";
160 SGSocket *sock = new SGSocket(host, "80", "tcp");
161 sock->set_timeout(10000);
162 if (!sock->open(SG_IO_OUT)) {
164 throw sg_io_exception("failed to load metar data from " + _url);
167 sock->writestring(get.c_str());
170 const int buflen = 512;
171 char buf[2 * buflen];
174 while ((i = sock->readline(buf, buflen)))
175 if (i <= 2 && isspace(buf[0]) && (!buf[1] || isspace(buf[1])))
178 i = sock->readline(buf, buflen);
180 sock->readline(&buf[i], buflen);
189 throw sg_io_exception("no metar data available from " + _url);
191 char *metar = new char[strlen(b) + 2]; // make room for " \0"
198 * Replace any number of subsequent spaces by just one space.
199 * This makes scanning for things like "ALL RWY" easier.
201 void SGMetar::normalizeData()
204 for (src = dest = _data; (*dest++ = *src++); )
205 while (*src == ' ' && src[1] == ' ')
210 // \d\d\d\d/\d\d/\d\d
211 bool SGMetar::scanPreambleDate()
214 int year, month, day;
215 if (!scanNumber(&m, &year, 4))
219 if (!scanNumber(&m, &month, 2))
223 if (!scanNumber(&m, &day, 2))
225 if (!scanBoundary(&m))
236 bool SGMetar::scanPreambleTime()
240 if (!scanNumber(&m, &hour, 2))
244 if (!scanNumber(&m, &minute, 2))
246 if (!scanBoundary(&m))
256 bool SGMetar::scanType()
258 if (strncmp(_m, "METAR ", 6) && strncmp(_m, "SPECI ", 6))
267 bool SGMetar::scanId()
270 for (int i = 0; i < 4; m++, i++)
271 if (!(isalpha(*m) || isdigit(*m)))
273 if (!scanBoundary(&m))
275 strncpy(_icao, _m, 4);
284 bool SGMetar::scanDate()
287 int day, hour, minute;
288 if (!scanNumber(&m, &day, 2))
290 if (!scanNumber(&m, &hour, 2))
292 if (!scanNumber(&m, &minute, 2))
296 if (!scanBoundary(&m))
307 // (NIL|AUTO|COR|RTD)
308 bool SGMetar::scanModifier()
312 if (!strncmp(m, "NIL", 3)) {
316 if (!strncmp(m, "AUTO", 4)) // automatically generated
318 else if (!strncmp(m, "COR", 3)) // manually corrected
320 else if (!strncmp(m, "RTD", 3)) // routine delayed
324 if (!scanBoundary(&m))
333 // (\d{3}|VRB)\d{1,3}(G\d{2,3})?(KT|KMH|MPS)
334 bool SGMetar::scanWind()
338 if (!strncmp(m, "VRB", 3))
340 else if (!scanNumber(&m, &dir, 3))
344 if (!scanNumber(&m, &i, 2, 3))
351 if (!scanNumber(&m, &i, 2, 3))
356 if (!strncmp(m, "KT", 2))
357 m += 2, factor = SG_KT_TO_MPS;
358 else if (!strncmp(m, "KMH", 3))
359 m += 3, factor = SG_KMH_TO_MPS;
360 else if (!strncmp(m, "KPH", 3)) // ??
361 m += 3, factor = SG_KMH_TO_MPS;
362 else if (!strncmp(m, "MPS", 3))
363 m += 3, factor = 1.0;
366 if (!scanBoundary(&m))
370 _wind_speed = speed * factor;
372 _gust_speed = gust * factor;
379 bool SGMetar::scanVariability()
383 if (!scanNumber(&m, &from, 3))
387 if (!scanNumber(&m, &to, 3))
389 if (!scanBoundary(&m))
392 _wind_range_from = from;
399 bool SGMetar::scanVisibility()
400 // TODO: if only directed vis are given, do still set min/max
402 if (!strncmp(_m, "//// ", 5)) { // spec compliant?
411 int modifier = SGMetarVisibility::EQUALS;
412 // \d{4}(N|NE|E|SE|S|SW|W|NW)?
413 if (scanNumber(&m, &i, 4)) {
418 else if (*m == 'N') {
426 } else if (*m == 'S') {
436 i = 50, modifier = SGMetarVisibility::LESS_THAN;
438 i++, modifier = SGMetarVisibility::GREATER_THAN;
441 // M?(\d{1,2}|\d{1,2}/\d{1,2}|\d{1,2} \d{1,2}/\d{1,2})(SM|KM)
444 m++, modifier = SGMetarVisibility::LESS_THAN;
446 if (!scanNumber(&m, &i, 1, 2))
452 if (!scanNumber(&m, &i, 1, 2))
455 } else if (*m == ' ') {
458 if (!scanNumber(&m, &i, 1, 2))
462 if (!scanNumber(&m, &denom, 1, 2))
464 distance += (double)i / denom;
467 if (!strncmp(m, "SM", 2))
468 distance *= SG_SM_TO_METER, m += 2;
469 else if (!strncmp(m, "KM", 2))
470 distance *= 1000, m += 2;
474 if (!scanBoundary(&m))
477 SGMetarVisibility *v;
479 v = &_dir_visibility[dir / 45];
480 else if (_min_visibility._distance == NaN)
481 v = &_min_visibility;
483 v = &_max_visibility;
485 v->_distance = distance;
486 v->_modifier = modifier;
494 // R\d\d[LCR]?/([PM]?\d{4}V)?[PM]?\d{4}(FT)?[DNU]?
495 bool SGMetar::scanRwyVisRange()
502 if (!scanNumber(&m, &i, 2))
504 if (*m == 'L' || *m == 'C' || *m == 'R')
508 strncpy(id, _m + 1, i = m - _m - 1);
516 m++, r._min_visibility._modifier = SGMetarVisibility::GREATER_THAN;
518 m++, r._min_visibility._modifier = SGMetarVisibility::LESS_THAN;
519 if (!scanNumber(&m, &from, 4))
524 m++, r._max_visibility._modifier = SGMetarVisibility::GREATER_THAN;
526 m++, r._max_visibility._modifier = SGMetarVisibility::LESS_THAN;
527 if (!scanNumber(&m, &to, 4))
532 if (!strncmp(m, "FT", 2)) {
533 from = int(from * SG_FEET_TO_METER);
534 to = int(to * SG_FEET_TO_METER);
537 r._min_visibility._distance = from;
538 r._max_visibility._distance = to;
540 if (*m == '/') // this is not in the spec!
543 m++, r._min_visibility._tendency = SGMetarVisibility::DECREASING;
545 m++, r._min_visibility._tendency = SGMetarVisibility::STABLE;
547 m++, r._min_visibility._tendency = SGMetarVisibility::INCREASING;
549 if (!scanBoundary(&m))
553 _runways[id]._min_visibility = r._min_visibility;
554 _runways[id]._max_visibility = r._max_visibility;
560 static const struct Token special[] = {
561 "NSW", "no significant weather",
562 "VCSH", "showers in the vicinity",
563 "VCTS", "thunderstorm in the vicinity",
568 static const struct Token description[] = {
570 "TS", "thunderstorm with",
573 "DR", "low drifting",
581 static const struct Token phenomenon[] = {
584 "GS", "small hail and/or snow pellets",
585 "IC", "ice crystals",
590 "UP", "unknown precipitation",
592 "DU", "widespread dust",
599 "VA", "volcanic ash",
601 "FC", "funnel cloud/tornado waterspout",
602 "PO", "well-developed dust/sand whirls",
605 "UP", "unknown", // ... due to failed automatic acquisition
610 // (+|-|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}
611 bool SGMetar::scanWeather()
615 const struct Token *a;
616 if ((a = scanToken(&m, special))) {
617 if (!scanBoundary(&m))
619 _weather.push_back(a->text);
629 else if (!strncmp(m, "VC", 2))
630 m += 2, post = "in the vicinity ";
635 for (i = 0; i < 3; i++) {
636 if (!(a = scanToken(&m, description)))
638 weather += string(a->text) + " ";
640 for (i = 0; i < 3; i++) {
641 if (!(a = scanToken(&m, phenomenon)))
643 weather += string(a->text) + " ";
645 if (!weather.length())
647 if (!scanBoundary(&m))
650 weather = pre + weather + post;
651 weather.erase(weather.length() - 1);
652 _weather.push_back(weather);
658 static const struct Token cloud_types[] = {
660 "ACC", "altocumulus castellanus",
661 "ACSL", "altocumulus standing lenticular",
663 "CB", "cumulonimbus",
664 "CBMAM", "cumulonimbus mammatus",
665 "CC", "cirrocumulus",
666 "CCSL", "cirrocumulus standing lenticular",
668 "CS", "cirrostratus",
670 "CUFRA", "cumulus fractus",
671 "NS", "nimbostratus",
672 "SAC", "stratoaltocumulus", // guessed
673 "SC", "stratocumulus",
674 "SCSL", "stratocumulus standing lenticular",
676 "STFRA", "stratus fractus",
677 "TCU", "towering cumulus",
682 // (FEW|SCT|BKN|OVC|SKC|CLR|CAVOK|VV)([0-9]{3}|///)?[:cloud_type:]?
683 bool SGMetar::scanSkyCondition()
689 if (!strncmp(m, "CLR", i = 3) // clear
690 || !strncmp(m, "SKC", i = 3) // sky clear
691 || !strncmp(m, "NSC", i = 3) // no significant clouds
692 || !strncmp(m, "CAVOK", i = 5)) { // ceiling and visibility OK (implies 9999)
694 if (!scanBoundary(&m))
697 _clouds.push_back(cl);
702 if (!strncmp(m, "VV", i = 2)) // vertical visibility
704 else if (!strncmp(m, "FEW", i = 3))
706 else if (!strncmp(m, "SCT", i = 3))
708 else if (!strncmp(m, "BKN", i = 3))
710 else if (!strncmp(m, "OVC", i = 3))
716 if (!strncmp(m, "///", 3)) // vis not measurable (e.g. because of heavy snowing)
718 else if (scanBoundary(&m)) {
720 return true; // ignore single OVC/BKN/...
721 } else if (!scanNumber(&m, &i, 3))
724 if (cl._coverage == -1) {
725 if (!scanBoundary(&m))
727 if (i == -1) // 'VV///'
728 _vert_visibility._modifier = SGMetarVisibility::NOGO;
730 _vert_visibility._distance = i * 100 * SG_FEET_TO_METER;
736 cl._altitude = i * 100 * SG_FEET_TO_METER;
738 const struct Token *a;
739 if ((a = scanToken(&m, cloud_types))) {
741 cl._type_long = a->text;
743 if (!scanBoundary(&m))
745 _clouds.push_back(cl);
752 // M?[0-9]{2}/(M?[0-9]{2})? (spec)
753 // (M?[0-9]{2}|XX)/(M?[0-9]{2}|XX)? (Namibia)
754 bool SGMetar::scanTemperature()
757 int sign = 1, temp, dew;
758 if (!strncmp(m, "XX/XX", 5)) { // not spec compliant!
760 return scanBoundary(&_m);
765 if (!scanNumber(&m, &temp, 2))
771 if (!scanBoundary(&m)) {
772 if (!strncmp(m, "XX", 2)) // not spec compliant!
778 if (!scanNumber(&m, &dew, 2))
781 if (!scanBoundary(&m))
793 double SGMetar::getRelHumidity() const
795 if (_temp == NaN || _dewp == NaN)
797 double dewp = pow(10, 7.5 * _dewp / (237.7 + _dewp));
798 double temp = pow(10, 7.5 * _temp / (237.7 + _temp));
799 return dewp * 100 / temp;
804 // [AQ]\d{2}(\d{2}|//) (Namibia)
805 bool SGMetar::scanPressure()
812 factor = SG_INHG_TO_PA / 100;
818 if (!scanNumber(&m, &press, 2))
821 if (!strncmp(m, "//", 2)) // not spec compliant!
823 else if (scanNumber(&m, &i, 2))
827 if (!scanBoundary(&m))
829 _pressure = press * factor;
836 static const char *runway_deposit[] = {
850 static const char *runway_deposit_extent[] = {
851 0, "1-10%", "11-25%", 0, 0, "26-50%", 0, 0, 0, "51-100%"
855 static const char *runway_friction[] = {
857 "poor braking action",
858 "poor/medium braking action",
859 "medium braking action",
860 "medium/good braking action",
861 "good braking action",
863 "friction: unreliable measurement"
867 // \d\d(CLRD|[\d/]{4})(\d\d|//)
868 bool SGMetar::scanRunwayReport()
875 if (!scanNumber(&m, &i, 2))
880 strcpy(id, "REP"); // repetition of previous report
883 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = 'R', id[3] = '\0';
885 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = '\0';
887 if (!strncmp(m, "CLRD", 4)) {
888 m += 4; // runway cleared
889 r._deposit = "cleared";
891 if (scanNumber(&m, &i, 1)) {
892 r._deposit = runway_deposit[i];
893 } else if (*m == '/')
897 if (*m == '1' || *m == '2' || *m == '5' || *m == '9') { // extent of deposit
898 r._extent = *m - '0';
899 r._extent_string = runway_deposit_extent[*m - '0'];
900 } else if (*m != '/')
904 if (!strncmp(m, "//", 2))
906 else if (!scanNumber(&m, &i, 2))
910 r._depth = 0.5; // < 1 mm deep (let's say 0.5 :-)
911 else if (i > 0 && i <= 90)
912 r._depth = i / 1000.0; // i mm deep
913 else if (i >= 92 && i <= 98)
914 r._depth = (i - 90) / 20.0;
916 r._comment = "runway not in use";
917 else if (i == -1) // no depth given ("//")
923 if (m[0] == '/' && m[1] == '/')
925 else if (!scanNumber(&m, &i, 2))
927 if (i >= 1 && i < 90) {
928 r._friction = i / 100.0;
929 } else if ((i >= 91 && i <= 95) || i == 99) {
930 r._friction_string = runway_friction[i - 90];
932 if (!scanBoundary(&m))
935 _runways[id]._deposit = r._deposit;
936 _runways[id]._extent = r._extent;
937 _runways[id]._extent_string = r._extent_string;
938 _runways[id]._depth = r._depth;
939 _runways[id]._friction = r._friction;
940 _runways[id]._friction_string = r._friction_string;
941 _runways[id]._comment = r._comment;
948 // WS (ALL RWYS?|RWY ?\d\d[LCR]?)?
949 bool SGMetar::scanWindShear()
952 if (strncmp(m, "WS", 2))
955 if (!scanBoundary(&m))
958 if (!strncmp(m, "ALL", 3)) {
960 if (!scanBoundary(&m))
962 if (strncmp(m, "RWY", 3))
967 if (!scanBoundary(&m))
969 _runways["ALL"]._wind_shear = true;
976 for (cnt = 0;; cnt++) { // ??
977 if (strncmp(m, "RWY", 3))
982 if (!scanNumber(&m, &i, 2))
984 if (*m == 'L' || *m == 'C' || *m == 'R')
986 strncpy(id, mm, i = m - mm);
988 if (!scanBoundary(&m))
990 _runways[id]._wind_shear = true;
993 _runways["ALL"]._wind_shear = true;
999 bool SGMetar::scanTrendForecast()
1002 if (strncmp(m, "NOSIG", 5))
1006 if (!scanBoundary(&m))
1013 // (BLU|WHT|GRN|YLO|AMB|RED)
1014 static const struct Token colors[] = {
1015 "BLU", "Blue", // 2500 ft, 8.0 km
1016 "WHT", "White", // 1500 ft, 5.0 km
1017 "GRN", "Green", // 700 ft, 3.7 km
1018 "YLO", "Yellow", // 300 ft, 1.6 km
1019 "AMB", "Amber", // 200 ft, 0.8 km
1020 "RED", "Red", // <200 ft, <0.8 km
1025 bool SGMetar::scanColorState()
1028 const struct Token *a;
1029 if (!(a = scanToken(&m, colors)))
1031 if (!scanBoundary(&m))
1033 //printf(Y"Code %s\n"N, a->text);
1039 bool SGMetar::scanRemark()
1041 if (strncmp(_m, "RMK", 3))
1044 if (!scanBoundary(&_m))
1048 if (!scanRunwayReport()) {
1049 while (*_m && !isspace(*_m))
1058 bool SGMetar::scanRemainder()
1061 if (!(strncmp(m, "NOSIG", 5))) {
1063 if (scanBoundary(&m))
1064 _m = m; //_comment.push_back("No significant tendency");
1067 if (!scanBoundary(&m))
1074 bool SGMetar::scanBoundary(char **s)
1076 if (**s && !isspace(**s))
1078 while (isspace(**s))
1084 int SGMetar::scanNumber(char **src, int *num, int min, int max)
1089 for (i = 0; i < min; i++) {
1093 *num = *num * 10 + *s++ - '0';
1095 for (; i < max && isdigit(*s); i++)
1096 *num = *num * 10 + *s++ - '0';
1102 // find longest match of str in list
1103 const struct Token *SGMetar::scanToken(char **str, const struct Token *list)
1105 const struct Token *longest = 0;
1106 int maxlen = 0, len;
1108 for (int i = 0; (s = list[i].id); i++) {
1110 if (!strncmp(s, *str, len) && len > maxlen) {