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/debug/logstream.hxx>
36 #include <simgear/structure/exception.hxx>
40 #define NaN SGMetarNaN
47 * The constructor takes a Metar string
48 * The constructor throws sg_io_exceptions on failure. The "METAR"
49 * keyword has no effect (apart from incrementing the group counter
50 * @a grpcount) and can be left away. A keyword "SPECI" is
53 * @param m ICAO station id or metar string
57 * SGMetar *m = new SGMetar("METAR KSFO 061656Z 19004KT 9SM SCT100 OVC200 08/03 A3013");
58 * double t = m->getTemperature_F();
63 SGMetar::SGMetar(const string& m) :
85 _data = new char[m.length() + 2]; // make room for " \0"
86 strcpy(_data, m.c_str());
95 if (!scanPreambleDate())
101 if (!scanId() || !scanDate()) {
103 throw sg_io_exception("metar data bogus ", sg_location(_url));
110 while (scanVisibility()) ;
111 while (scanRwyVisRange()) ;
112 while (scanWeather()) ;
113 while (scanSkyCondition()) ;
116 while (scanSkyCondition()) ;
117 while (scanRunwayReport()) ;
121 while (scanColorState()) ;
123 while (scanRunwayReport()) ;
129 throw sg_io_exception("metar data incomplete ", sg_location(_url));
137 * Clears lists and maps to discourage access after destruction.
148 void SGMetar::useCurrentDate()
151 time_t now_sec = time(0);
153 now = *gmtime(&now_sec);
155 gmtime_r(&now_sec, &now);
157 _year = now.tm_year + 1900;
158 _month = now.tm_mon + 1;
162 * Replace any number of subsequent spaces by just one space, and add
163 * a trailing space. This makes scanning for things like "ALL RWY" easier.
165 void SGMetar::normalizeData()
168 for (src = dest = _data; (*dest++ = *src++); )
169 while (*src == ' ' && src[1] == ' ')
171 for (dest--; isspace(*--dest); ) ;
177 // \d\d\d\d/\d\d/\d\d
178 bool SGMetar::scanPreambleDate()
181 int year, month, day;
182 if (!scanNumber(&m, &year, 4))
186 if (!scanNumber(&m, &month, 2))
190 if (!scanNumber(&m, &day, 2))
192 if (!scanBoundary(&m))
203 bool SGMetar::scanPreambleTime()
207 if (!scanNumber(&m, &hour, 2))
211 if (!scanNumber(&m, &minute, 2))
213 if (!scanBoundary(&m))
223 bool SGMetar::scanType()
225 if (strncmp(_m, "METAR ", 6) && strncmp(_m, "SPECI ", 6))
234 bool SGMetar::scanId()
237 for (int i = 0; i < 4; m++, i++)
238 if (!(isalpha(*m) || isdigit(*m)))
240 if (!scanBoundary(&m))
242 strncpy(_icao, _m, 4);
251 bool SGMetar::scanDate()
254 int day, hour, minute;
255 if (!scanNumber(&m, &day, 2))
257 if (!scanNumber(&m, &hour, 2))
259 if (!scanNumber(&m, &minute, 2))
263 if (!scanBoundary(&m))
274 // (NIL|AUTO|COR|RTD)
275 bool SGMetar::scanModifier()
279 if (!strncmp(m, "NIL", 3)) {
283 if (!strncmp(m, "AUTO", 4)) // automatically generated
285 else if (!strncmp(m, "COR", 3)) // manually corrected
287 else if (!strncmp(m, "RTD", 3)) // routine delayed
291 if (!scanBoundary(&m))
300 // (\d{3}|VRB)\d{1,3}(G\d{2,3})?(KT|KMH|MPS)
301 bool SGMetar::scanWind()
305 if (!strncmp(m, "VRB", 3))
307 else if (!scanNumber(&m, &dir, 3))
311 if (!scanNumber(&m, &i, 2, 3))
318 if (!scanNumber(&m, &i, 2, 3))
323 if (!strncmp(m, "KT", 2))
324 m += 2, factor = SG_KT_TO_MPS;
325 else if (!strncmp(m, "KMH", 3))
326 m += 3, factor = SG_KMH_TO_MPS;
327 else if (!strncmp(m, "KPH", 3)) // ??
328 m += 3, factor = SG_KMH_TO_MPS;
329 else if (!strncmp(m, "MPS", 3))
330 m += 3, factor = 1.0;
333 if (!scanBoundary(&m))
337 _wind_speed = speed * factor;
339 _gust_speed = gust * factor;
346 bool SGMetar::scanVariability()
350 if (!scanNumber(&m, &from, 3))
354 if (!scanNumber(&m, &to, 3))
356 if (!scanBoundary(&m))
359 _wind_range_from = from;
366 bool SGMetar::scanVisibility()
367 // TODO: if only directed vis are given, do still set min/max
369 if (!strncmp(_m, "//// ", 5)) { // spec compliant?
378 int modifier = SGMetarVisibility::EQUALS;
379 // \d{4}(N|NE|E|SE|S|SW|W|NW)?
380 if (scanNumber(&m, &i, 4)) {
381 if( strncmp( m, "NDV",3 ) == 0 ) {
382 m+=3; // tolerate NDV (no directional validation)
383 } else if (*m == 'E') {
385 } else if (*m == 'W') {
387 } else if (*m == 'N') {
395 } else if (*m == 'S') {
405 i = 50, modifier = SGMetarVisibility::LESS_THAN;
407 i++, modifier = SGMetarVisibility::GREATER_THAN;
410 // M?(\d{1,2}|\d{1,2}/\d{1,2}|\d{1,2} \d{1,2}/\d{1,2})(SM|KM)
412 m++, modifier = SGMetarVisibility::LESS_THAN;
414 if (!scanNumber(&m, &i, 1, 2))
420 if (!scanNumber(&m, &i, 1, 2))
423 } else if (*m == ' ') {
426 if (!scanNumber(&m, &i, 1, 2))
430 if (!scanNumber(&m, &denom, 1, 2))
432 distance += (double)i / denom;
435 if (!strncmp(m, "SM", 2))
436 distance *= SG_SM_TO_METER, m += 2;
437 else if (!strncmp(m, "KM", 2))
438 distance *= 1000, m += 2;
442 if (!scanBoundary(&m))
445 SGMetarVisibility *v;
447 v = &_dir_visibility[dir / 45];
448 else if (_min_visibility._distance == NaN)
449 v = &_min_visibility;
451 v = &_max_visibility;
453 v->_distance = distance;
454 v->_modifier = modifier;
462 // R\d\d[LCR]?/([PM]?\d{4}V)?[PM]?\d{4}(FT)?[DNU]?
463 bool SGMetar::scanRwyVisRange()
470 if (!scanNumber(&m, &i, 2))
472 if (*m == 'L' || *m == 'C' || *m == 'R')
476 strncpy(id, _m + 1, i = m - _m - 1);
484 m++, r._min_visibility._modifier = SGMetarVisibility::GREATER_THAN;
486 m++, r._min_visibility._modifier = SGMetarVisibility::LESS_THAN;
487 if (!scanNumber(&m, &from, 4))
492 m++, r._max_visibility._modifier = SGMetarVisibility::GREATER_THAN;
494 m++, r._max_visibility._modifier = SGMetarVisibility::LESS_THAN;
495 if (!scanNumber(&m, &to, 4))
500 if (!strncmp(m, "FT", 2)) {
501 from = int(from * SG_FEET_TO_METER);
502 to = int(to * SG_FEET_TO_METER);
505 r._min_visibility._distance = from;
506 r._max_visibility._distance = to;
508 if (*m == '/') // this is not in the spec!
511 m++, r._min_visibility._tendency = SGMetarVisibility::DECREASING;
513 m++, r._min_visibility._tendency = SGMetarVisibility::STABLE;
515 m++, r._min_visibility._tendency = SGMetarVisibility::INCREASING;
517 if (!scanBoundary(&m))
521 _runways[id]._min_visibility = r._min_visibility;
522 _runways[id]._max_visibility = r._max_visibility;
528 static const struct Token special[] = {
529 { "NSW", "no significant weather" },
530 /* { "VCSH", "showers in the vicinity" },
531 { "VCTS", "thunderstorm in the vicinity" }, */
536 static const struct Token description[] = {
537 { "SH", "showers of" },
538 { "TS", "thunderstorm with" },
539 { "BC", "patches of" },
541 { "DR", "low drifting" },
542 { "FZ", "freezing" },
549 static const struct Token phenomenon[] = {
552 { "GS", "small hail and/or snow pellets" },
553 { "IC", "ice crystals" },
554 { "PE", "ice pellets" },
556 { "SG", "snow grains" },
558 { "UP", "unknown precipitation" },
560 { "DU", "widespread dust" },
562 { "FGBR", "fog bank" },
567 { "VA", "volcanic ash" },
568 { "DS", "duststorm" },
569 { "FC", "funnel cloud/tornado waterspout" },
570 { "PO", "well-developed dust/sand whirls" },
572 { "SS", "sandstorm" },
573 { "UP", "unknown" }, // ... due to failed automatic acquisition
578 // (+|-|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}
579 bool SGMetar::scanWeather()
583 const struct Token *a;
585 if ((a = scanToken(&m, special))) {
586 if (!scanBoundary(&m))
588 _weather.push_back(a->text);
596 m++, pre = "light ", w.intensity = LIGHT;
598 m++, pre = "heavy ", w.intensity = HEAVY;
599 else if (!strncmp(m, "VC", 2))
600 m += 2, post = "in the vicinity ", w.vincinity=true;
602 pre = "moderate ", w.intensity = MODERATE;
605 for (i = 0; i < 3; i++) {
606 if (!(a = scanToken(&m, description)))
608 w.descriptions.push_back(a->id);
609 weather += string(a->text) + " ";
612 for (i = 0; i < 3; i++) {
613 if (!(a = scanToken(&m, phenomenon)))
615 w.phenomena.push_back(a->id);
616 weather += string(a->text) + " ";
617 if (!strcmp(a->id, "RA"))
619 else if (!strcmp(a->id, "HA"))
621 else if (!strcmp(a->id, "SN"))
624 if (!weather.length())
626 if (!scanBoundary(&m))
629 weather = pre + weather + post;
630 weather.erase(weather.length() - 1);
631 _weather.push_back(weather);
632 if( w.phenomena.size() > 0 )
633 _weather2.push_back( w );
639 static const struct Token cloud_types[] = {
640 { "AC", "altocumulus" },
641 { "ACC", "altocumulus castellanus" },
642 { "ACSL", "altocumulus standing lenticular" },
643 { "AS", "altostratus" },
644 { "CB", "cumulonimbus" },
645 { "CBMAM", "cumulonimbus mammatus" },
646 { "CC", "cirrocumulus" },
647 { "CCSL", "cirrocumulus standing lenticular" },
649 { "CS", "cirrostratus" },
651 { "CUFRA", "cumulus fractus" },
652 { "NS", "nimbostratus" },
653 { "SAC", "stratoaltocumulus" }, // guessed
654 { "SC", "stratocumulus" },
655 { "SCSL", "stratocumulus standing lenticular" },
657 { "STFRA", "stratus fractus" },
658 { "TCU", "towering cumulus" },
663 // (FEW|SCT|BKN|OVC|SKC|CLR|CAVOK|VV)([0-9]{3}|///)?[:cloud_type:]?
664 bool SGMetar::scanSkyCondition()
670 if (!strncmp(m, "//////", 6)) {
672 if (!scanBoundary(&m))
678 if (!strncmp(m, "CLR", i = 3) // clear
679 || !strncmp(m, "SKC", i = 3) // sky clear
680 || !strncmp(m, "NCD", i = 3) // nil cloud detected
681 || !strncmp(m, "NSC", i = 3) // no significant clouds
682 || !strncmp(m, "CAVOK", i = 5)) { // ceiling and visibility OK (implies 9999)
684 if (!scanBoundary(&m))
688 cl._coverage = SGMetarCloud::COVERAGE_CLEAR;
689 _clouds.push_back(cl);
697 if (!strncmp(m, "VV", i = 2)) // vertical visibility
699 else if (!strncmp(m, "FEW", i = 3))
700 cl._coverage = SGMetarCloud::COVERAGE_FEW;
701 else if (!strncmp(m, "SCT", i = 3))
702 cl._coverage = SGMetarCloud::COVERAGE_SCATTERED;
703 else if (!strncmp(m, "BKN", i = 3))
704 cl._coverage = SGMetarCloud::COVERAGE_BROKEN;
705 else if (!strncmp(m, "OVC", i = 3))
706 cl._coverage = SGMetarCloud::COVERAGE_OVERCAST;
711 if (!strncmp(m, "///", 3)) // vis not measurable (e.g. because of heavy snowing)
713 else if (scanBoundary(&m)) {
715 return true; // ignore single OVC/BKN/...
716 } else if (!scanNumber(&m, &i, 3))
719 if (cl._coverage == SGMetarCloud::COVERAGE_NIL) {
720 if (!scanBoundary(&m))
722 if (i == -1) // 'VV///'
723 _vert_visibility._modifier = SGMetarVisibility::NOGO;
725 _vert_visibility._distance = i * 100 * SG_FEET_TO_METER;
731 cl._altitude = i * 100 * SG_FEET_TO_METER;
733 const struct Token *a;
734 if ((a = scanToken(&m, cloud_types))) {
736 cl._type_long = a->text;
738 if (!scanBoundary(&m))
740 _clouds.push_back(cl);
747 // M?[0-9]{2}/(M?[0-9]{2})? (spec)
748 // (M?[0-9]{2}|XX)/(M?[0-9]{2}|XX)? (Namibia)
749 bool SGMetar::scanTemperature()
752 int sign = 1, temp, dew;
753 if (!strncmp(m, "XX/XX", 5)) { // not spec compliant!
755 return scanBoundary(&_m);
760 if (!scanNumber(&m, &temp, 2))
766 if (!scanBoundary(&m)) {
767 if (!strncmp(m, "XX", 2)) // not spec compliant!
768 m += 2, sign = 0, dew = temp;
773 if (!scanNumber(&m, &dew, 2))
776 if (!scanBoundary(&m))
788 double SGMetar::getRelHumidity() const
790 if (_temp == NaN || _dewp == NaN)
792 double dewp = pow(10.0, 7.5 * _dewp / (237.7 + _dewp));
793 double temp = pow(10.0, 7.5 * _temp / (237.7 + _temp));
794 return dewp * 100 / temp;
799 // [AQ]\d{2}(\d{2}|//) (Namibia)
800 bool SGMetar::scanPressure()
807 factor = SG_INHG_TO_PA / 100;
813 if (!scanNumber(&m, &press, 2))
816 if (!strncmp(m, "//", 2)) // not spec compliant!
818 else if (scanNumber(&m, &i, 2))
822 if (!scanBoundary(&m))
824 _pressure = press * factor;
831 static const char *runway_deposit[] = {
845 static const char *runway_deposit_extent[] = {
846 0, "1-10%", "11-25%", 0, 0, "26-50%", 0, 0, 0, "51-100%"
850 static const char *runway_friction[] = {
852 "poor braking action",
853 "poor/medium braking action",
854 "medium braking action",
855 "medium/good braking action",
856 "good braking action",
858 "friction: unreliable measurement"
862 // \d\d(CLRD|[\d/]{4})(\d\d|//)
863 bool SGMetar::scanRunwayReport()
870 if (!scanNumber(&m, &i, 2))
875 strcpy(id, "REP"); // repetition of previous report
878 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = 'R', id[3] = '\0';
880 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = '\0';
882 if (!strncmp(m, "CLRD", 4)) {
883 m += 4; // runway cleared
884 r._deposit_string = "cleared";
886 if (scanNumber(&m, &i, 1)) {
888 r._deposit_string = runway_deposit[i];
889 } else if (*m == '/')
894 if (*m == '1' || *m == '2' || *m == '5' || *m == '9') { // extent of deposit
895 r._extent = *m - '0';
896 r._extent_string = runway_deposit_extent[*m - '0'];
897 } else if (*m != '/')
902 if (!strncmp(m, "//", 2))
904 else if (!scanNumber(&m, &i, 2))
908 r._depth = 0.0005; // < 1 mm deep (let's say 0.5 :-)
909 else if (i > 0 && i <= 90)
910 r._depth = i / 1000.0; // i mm deep
911 else if (i >= 92 && i <= 98)
912 r._depth = (i - 90) / 20.0;
914 r._comment = "runway not in use";
915 else if (i == -1) // no depth given ("//")
921 if (m[0] == '/' && m[1] == '/')
923 else if (!scanNumber(&m, &i, 2))
925 if (i >= 1 && i < 90) {
926 r._friction = i / 100.0;
927 } else if ((i >= 91 && i <= 95) || i == 99) {
928 r._friction_string = runway_friction[i - 90];
930 if (!scanBoundary(&m))
933 _runways[id]._deposit = r._deposit;
934 _runways[id]._deposit_string = r._deposit_string;
935 _runways[id]._extent = r._extent;
936 _runways[id]._extent_string = r._extent_string;
937 _runways[id]._depth = r._depth;
938 _runways[id]._friction = r._friction;
939 _runways[id]._friction_string = r._friction_string;
940 _runways[id]._comment = r._comment;
947 // WS (ALL RWYS?|RWY ?\d\d[LCR]?)?
948 bool SGMetar::scanWindShear()
951 if (strncmp(m, "WS", 2))
954 if (!scanBoundary(&m))
957 if (!strncmp(m, "ALL", 3)) {
959 if (!scanBoundary(&m))
961 if (strncmp(m, "RWY", 3))
966 if (!scanBoundary(&m))
968 _runways["ALL"]._wind_shear = true;
975 for (cnt = 0;; cnt++) { // ??
976 if (strncmp(m, "RWY", 3))
981 if (!scanNumber(&m, &i, 2))
983 if (*m == 'L' || *m == 'C' || *m == 'R')
985 strncpy(id, mm, i = m - mm);
987 if (!scanBoundary(&m))
989 _runways[id]._wind_shear = true;
992 _runways["ALL"]._wind_shear = true;
998 bool SGMetar::scanTrendForecast()
1001 if (strncmp(m, "NOSIG", 5))
1005 if (!scanBoundary(&m))
1012 // (BLU|WHT|GRN|YLO|AMB|RED)
1013 static const struct Token colors[] = {
1014 { "BLU", "Blue" }, // 2500 ft, 8.0 km
1015 { "WHT", "White" }, // 1500 ft, 5.0 km
1016 { "GRN", "Green" }, // 700 ft, 3.7 km
1017 { "YLO", "Yellow" }, // 300 ft, 1.6 km
1018 { "AMB", "Amber" }, // 200 ft, 0.8 km
1019 { "RED", "Red" }, // <200 ft, <0.8 km
1024 bool SGMetar::scanColorState()
1027 const struct Token *a;
1028 if (!(a = scanToken(&m, colors)))
1030 if (!scanBoundary(&m))
1032 //printf(Y"Code %s\n"N, a->text);
1038 bool SGMetar::scanRemark()
1040 if (strncmp(_m, "RMK", 3))
1043 if (!scanBoundary(&_m))
1047 if (!scanRunwayReport()) {
1048 while (*_m && !isspace(*_m))
1057 bool SGMetar::scanRemainder()
1060 if (!(strncmp(m, "NOSIG", 5))) {
1062 if (scanBoundary(&m))
1063 _m = m; //_comment.push_back("No significant tendency");
1066 if (!scanBoundary(&m))
1073 bool SGMetar::scanBoundary(char **s)
1075 if (**s && !isspace(**s))
1077 while (isspace(**s))
1083 int SGMetar::scanNumber(char **src, int *num, int min, int max)
1088 for (i = 0; i < min; i++) {
1092 *num = *num * 10 + *s++ - '0';
1094 for (; i < max && isdigit(*s); i++)
1095 *num = *num * 10 + *s++ - '0';
1101 // find longest match of str in list
1102 const struct Token *SGMetar::scanToken(char **str, const struct Token *list)
1104 const struct Token *longest = 0;
1105 int maxlen = 0, len;
1107 for (int i = 0; (s = list[i].id); i++) {
1109 if (!strncmp(s, *str, len) && len > maxlen) {
1119 void SGMetarCloud::set(double alt, Coverage cov)
1126 SGMetarCloud::Coverage SGMetarCloud::getCoverage( const std::string & coverage )
1128 if( coverage == "clear" ) return COVERAGE_CLEAR;
1129 if( coverage == "few" ) return COVERAGE_FEW;
1130 if( coverage == "scattered" ) return COVERAGE_SCATTERED;
1131 if( coverage == "broken" ) return COVERAGE_BROKEN;
1132 if( coverage == "overcast" ) return COVERAGE_OVERCAST;
1133 return COVERAGE_NIL;
1136 const char * SGMetarCloud::COVERAGE_NIL_STRING = "nil";
1137 const char * SGMetarCloud::COVERAGE_CLEAR_STRING = "clear";
1138 const char * SGMetarCloud::COVERAGE_FEW_STRING = "few";
1139 const char * SGMetarCloud::COVERAGE_SCATTERED_STRING = "scattered";
1140 const char * SGMetarCloud::COVERAGE_BROKEN_STRING = "broken";
1141 const char * SGMetarCloud::COVERAGE_OVERCAST_STRING = "overcast";
1143 void SGMetarVisibility::set(double dist, int dir, int mod, int tend)