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 * Technical Regulations, Basic Documents No. 2 (WMO No. 49)
29 * Volume II - Meteorological Service for International Air Navigation
30 * http://library.wmo.int/pmb_ged/wmo_49-v2_2013_en.pdf
32 * Refer to Table A3-2 (Template for METAR and SPECI) following page 78.
34 * For general information:
35 * World Meteorological Organization http://library.wmo.int
38 # include <simgear_config.h>
45 #include <simgear/debug/logstream.hxx>
46 #include <simgear/structure/exception.hxx>
50 #define NaN SGMetarNaN
57 * The constructor takes a Metar string
58 * The constructor throws sg_io_exceptions on failure. The "METAR"
59 * keyword has no effect (apart from incrementing the group counter
60 * @a grpcount) and can be left away. A keyword "SPECI" is
63 * @param m ICAO station id or metar string
67 * SGMetar *m = new SGMetar("METAR KSFO 061656Z 19004KT 9SM SCT100 OVC200 08/03 A3013");
68 * double t = m->getTemperature_F();
73 SGMetar::SGMetar(const string& m) :
95 _data = new char[m.length() + 2]; // make room for " \0"
96 strcpy(_data, m.c_str());
105 if (!scanPreambleDate())
111 if (!scanId() || !scanDate()) {
113 throw sg_io_exception("metar data bogus ", sg_location(_url));
120 while (scanVisibility()) ;
121 while (scanRwyVisRange()) ;
122 while (scanWeather()) ;
123 while (scanSkyCondition()) ;
126 while (scanSkyCondition()) ;
127 while (scanRunwayReport()) ;
131 while (scanColorState()) ;
133 while (scanRunwayReport()) ;
139 throw sg_io_exception("metar data incomplete ", sg_location(_url));
147 * Clears lists and maps to discourage access after destruction.
158 void SGMetar::useCurrentDate()
161 time_t now_sec = time(0);
163 now = *gmtime(&now_sec);
165 gmtime_r(&now_sec, &now);
167 _year = now.tm_year + 1900;
168 _month = now.tm_mon + 1;
172 * Replace any number of subsequent spaces by just one space, and add
173 * a trailing space. This makes scanning for things like "ALL RWY" easier.
175 void SGMetar::normalizeData()
178 for (src = dest = _data; (*dest++ = *src++); )
179 while (*src == ' ' && src[1] == ' ')
181 for (dest--; isspace(*--dest); ) ;
187 // \d\d\d\d/\d\d/\d\d
188 bool SGMetar::scanPreambleDate()
191 int year, month, day;
192 if (!scanNumber(&m, &year, 4))
196 if (!scanNumber(&m, &month, 2))
200 if (!scanNumber(&m, &day, 2))
202 if (!scanBoundary(&m))
213 bool SGMetar::scanPreambleTime()
217 if (!scanNumber(&m, &hour, 2))
221 if (!scanNumber(&m, &minute, 2))
223 if (!scanBoundary(&m))
233 bool SGMetar::scanType()
235 if (strncmp(_m, "METAR ", 6) && strncmp(_m, "SPECI ", 6))
244 bool SGMetar::scanId()
247 for (int i = 0; i < 4; m++, i++)
248 if (!(isalpha(*m) || isdigit(*m)))
250 if (!scanBoundary(&m))
252 strncpy(_icao, _m, 4);
261 bool SGMetar::scanDate()
264 int day, hour, minute;
265 if (!scanNumber(&m, &day, 2))
267 if (!scanNumber(&m, &hour, 2))
269 if (!scanNumber(&m, &minute, 2))
273 if (!scanBoundary(&m))
284 // (NIL|AUTO|COR|RTD)
285 bool SGMetar::scanModifier()
289 if (!strncmp(m, "NIL", 3)) {
293 if (!strncmp(m, "AUTO", 4)) // automatically generated
295 else if (!strncmp(m, "COR", 3)) // manually corrected
297 else if (!strncmp(m, "RTD", 3)) // routine delayed
301 if (!scanBoundary(&m))
310 // (\d{3}|VRB)\d{1,3}(G\d{2,3})?(KT|KMH|MPS)
311 bool SGMetar::scanWind()
315 if (!strncmp(m, "VRB", 3))
317 else if (!scanNumber(&m, &dir, 3))
321 if (!scanNumber(&m, &i, 2, 3))
328 if (!scanNumber(&m, &i, 2, 3))
333 if (!strncmp(m, "KT", 2))
334 m += 2, factor = SG_KT_TO_MPS;
335 else if (!strncmp(m, "KMH", 3))
336 m += 3, factor = SG_KMH_TO_MPS;
337 else if (!strncmp(m, "KPH", 3)) // ??
338 m += 3, factor = SG_KMH_TO_MPS;
339 else if (!strncmp(m, "MPS", 3))
340 m += 3, factor = 1.0;
343 if (!scanBoundary(&m))
347 _wind_speed = speed * factor;
349 _gust_speed = gust * factor;
356 bool SGMetar::scanVariability()
360 if (!scanNumber(&m, &from, 3))
364 if (!scanNumber(&m, &to, 3))
366 if (!scanBoundary(&m))
369 _wind_range_from = from;
376 bool SGMetar::scanVisibility()
377 // TODO: if only directed vis are given, do still set min/max
379 if (!strncmp(_m, "//// ", 5)) { // spec compliant?
388 int modifier = SGMetarVisibility::EQUALS;
389 // \d{4}(N|NE|E|SE|S|SW|W|NW)?
390 if (scanNumber(&m, &i, 4)) {
391 if( strncmp( m, "NDV",3 ) == 0 ) {
392 m+=3; // tolerate NDV (no directional validation)
393 } else if (*m == 'E') {
395 } else if (*m == 'W') {
397 } else if (*m == 'N') {
405 } else if (*m == 'S') {
415 i = 50, modifier = SGMetarVisibility::LESS_THAN;
417 i++, modifier = SGMetarVisibility::GREATER_THAN;
420 // M?(\d{1,2}|\d{1,2}/\d{1,2}|\d{1,2} \d{1,2}/\d{1,2})(SM|KM)
422 m++, modifier = SGMetarVisibility::LESS_THAN;
424 if (!scanNumber(&m, &i, 1, 2))
430 if (!scanNumber(&m, &i, 1, 2))
433 } else if (*m == ' ') {
436 if (!scanNumber(&m, &i, 1, 2))
440 if (!scanNumber(&m, &denom, 1, 2))
442 distance += (double)i / denom;
445 if (!strncmp(m, "SM", 2))
446 distance *= SG_SM_TO_METER, m += 2;
447 else if (!strncmp(m, "KM", 2))
448 distance *= 1000, m += 2;
452 if (!scanBoundary(&m))
455 SGMetarVisibility *v;
457 v = &_dir_visibility[dir / 45];
458 else if (_min_visibility._distance == NaN)
459 v = &_min_visibility;
461 v = &_max_visibility;
463 v->_distance = distance;
464 v->_modifier = modifier;
472 // R\d\d[LCR]?/([PM]?\d{4}V)?[PM]?\d{4}(FT)?[DNU]?
473 bool SGMetar::scanRwyVisRange()
481 if (!scanNumber(&m, &i, 2))
483 if (*m == 'L' || *m == 'C' || *m == 'R')
487 strncpy(id, _m + 1, i = m - _m - 1);
495 m++, r._min_visibility._modifier = SGMetarVisibility::GREATER_THAN;
497 m++, r._min_visibility._modifier = SGMetarVisibility::LESS_THAN;
498 if (!scanNumber(&m, &from, 4))
503 m++, r._max_visibility._modifier = SGMetarVisibility::GREATER_THAN;
505 m++, r._max_visibility._modifier = SGMetarVisibility::LESS_THAN;
506 if (!scanNumber(&m, &to, 4))
511 if (!strncmp(m, "FT", 2)) {
512 from = int(from * SG_FEET_TO_METER);
513 to = int(to * SG_FEET_TO_METER);
516 r._min_visibility._distance = from;
517 r._max_visibility._distance = to;
519 if (*m == '/') // this is not in the spec!
522 m++, r._min_visibility._tendency = SGMetarVisibility::DECREASING;
524 m++, r._min_visibility._tendency = SGMetarVisibility::STABLE;
526 m++, r._min_visibility._tendency = SGMetarVisibility::INCREASING;
528 if (!scanBoundary(&m))
532 _runways[id]._min_visibility = r._min_visibility;
533 _runways[id]._max_visibility = r._max_visibility;
539 static const struct Token special[] = {
540 { "NSW", "no significant weather" },
541 /* { "VCSH", "showers in the vicinity" },
542 { "VCTS", "thunderstorm in the vicinity" }, */
547 static const struct Token description[] = {
548 { "SH", "showers of" },
549 { "TS", "thunderstorm with" },
550 { "BC", "patches of" },
552 { "DR", "low drifting" },
553 { "FZ", "freezing" },
560 static const struct Token phenomenon[] = {
563 { "GS", "small hail and/or snow pellets" },
564 { "IC", "ice crystals" },
565 { "PE", "ice pellets" },
567 { "SG", "snow grains" },
569 { "UP", "unknown precipitation" },
571 { "DU", "widespread dust" },
573 { "FGBR", "fog bank" },
578 { "VA", "volcanic ash" },
579 { "DS", "duststorm" },
580 { "FC", "funnel cloud/tornado waterspout" },
581 { "PO", "well-developed dust/sand whirls" },
583 { "SS", "sandstorm" },
584 { "UP", "unknown" }, // ... due to failed automatic acquisition
589 // (+|-|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}
590 bool SGMetar::scanWeather()
594 const struct Token *a;
596 // @see WMO-49 Section 4.4.2.9
597 // Denotes a temporary failure of the sensor
598 if (!strncmp(m, "// ", 3)) {
604 if ((a = scanToken(&m, special))) {
605 if (!scanBoundary(&m))
607 _weather.push_back(a->text);
615 m++, pre = "light ", w.intensity = LIGHT;
617 m++, pre = "heavy ", w.intensity = HEAVY;
618 else if (!strncmp(m, "VC", 2))
619 m += 2, post = "in the vicinity ", w.vincinity=true;
621 pre = "moderate ", w.intensity = MODERATE;
624 for (i = 0; i < 3; i++) {
625 if (!(a = scanToken(&m, description)))
627 w.descriptions.push_back(a->id);
628 weather += string(a->text) + " ";
631 for (i = 0; i < 3; i++) {
632 if (!(a = scanToken(&m, phenomenon)))
634 w.phenomena.push_back(a->id);
635 weather += string(a->text) + " ";
636 if (!strcmp(a->id, "RA"))
638 else if (!strcmp(a->id, "HA"))
640 else if (!strcmp(a->id, "SN"))
643 if (!weather.length())
645 if (!scanBoundary(&m))
648 weather = pre + weather + post;
649 weather.erase(weather.length() - 1);
650 _weather.push_back(weather);
651 if( ! w.phenomena.empty() )
652 _weather2.push_back( w );
658 static const struct Token cloud_types[] = {
659 { "AC", "altocumulus" },
660 { "ACC", "altocumulus castellanus" },
661 { "ACSL", "altocumulus standing lenticular" },
662 { "AS", "altostratus" },
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, "//////", 6)) {
691 if (!scanBoundary(&m))
697 if (!strncmp(m, "CLR", i = 3) // clear
698 || !strncmp(m, "SKC", i = 3) // sky clear
699 || !strncmp(m, "NCD", i = 3) // nil cloud detected
700 || !strncmp(m, "NSC", i = 3) // no significant clouds
701 || !strncmp(m, "CAVOK", i = 5)) { // ceiling and visibility OK (implies 9999)
703 if (!scanBoundary(&m))
707 cl._coverage = SGMetarCloud::COVERAGE_CLEAR;
708 _clouds.push_back(cl);
716 if (!strncmp(m, "VV", i = 2)) // vertical visibility
718 else if (!strncmp(m, "FEW", i = 3))
719 cl._coverage = SGMetarCloud::COVERAGE_FEW;
720 else if (!strncmp(m, "SCT", i = 3))
721 cl._coverage = SGMetarCloud::COVERAGE_SCATTERED;
722 else if (!strncmp(m, "BKN", i = 3))
723 cl._coverage = SGMetarCloud::COVERAGE_BROKEN;
724 else if (!strncmp(m, "OVC", i = 3))
725 cl._coverage = SGMetarCloud::COVERAGE_OVERCAST;
730 if (!strncmp(m, "///", 3)) // vis not measurable (e.g. because of heavy snowing)
732 else if (scanBoundary(&m)) {
734 return true; // ignore single OVC/BKN/...
735 } else if (!scanNumber(&m, &i, 3))
738 if (cl._coverage == SGMetarCloud::COVERAGE_NIL) {
739 if (!scanBoundary(&m))
741 if (i == -1) // 'VV///'
742 _vert_visibility._modifier = SGMetarVisibility::NOGO;
744 _vert_visibility._distance = i * 100 * SG_FEET_TO_METER;
750 cl._altitude = i * 100 * SG_FEET_TO_METER;
752 const struct Token *a;
753 if ((a = scanToken(&m, cloud_types))) {
755 cl._type_long = a->text;
758 // @see WMO-49 Section 4.5.4.5
759 // Denotes temporary failure of sensor and covers cases like FEW045///
760 if (!strncmp(m, "///", 3))
762 if (!scanBoundary(&m))
764 _clouds.push_back(cl);
772 // M?[0-9]{2}/(M?[0-9]{2})? (spec)
773 // (M?[0-9]{2}|XX)/(M?[0-9]{2}|XX)? (Namibia)
774 bool SGMetar::scanTemperature()
777 int sign = 1, temp, dew;
778 if (!strncmp(m, "XX/XX", 5)) { // not spec compliant!
780 return scanBoundary(&_m);
785 if (!scanNumber(&m, &temp, 2))
791 if (!scanBoundary(&m)) {
792 if (!strncmp(m, "XX", 2)) // not spec compliant!
793 m += 2, sign = 0, dew = temp;
798 if (!scanNumber(&m, &dew, 2))
801 if (!scanBoundary(&m))
813 double SGMetar::getRelHumidity() const
815 if (_temp == NaN || _dewp == NaN)
817 double dewp = pow(10.0, 7.5 * _dewp / (237.7 + _dewp));
818 double temp = pow(10.0, 7.5 * _temp / (237.7 + _temp));
819 return dewp * 100 / temp;
824 // [AQ]\d{2}(\d{2}|//) (Namibia)
825 bool SGMetar::scanPressure()
832 factor = SG_INHG_TO_PA / 100;
838 if (!scanNumber(&m, &press, 2))
841 if (!strncmp(m, "//", 2)) // not spec compliant!
843 else if (scanNumber(&m, &i, 2))
847 if (!scanBoundary(&m))
849 _pressure = press * factor;
856 static const char *runway_deposit[] = {
870 static const char *runway_deposit_extent[] = {
871 0, "1-10%", "11-25%", 0, 0, "26-50%", 0, 0, 0, "51-100%"
875 static const char *runway_friction[] = {
877 "poor braking action",
878 "poor/medium braking action",
879 "medium braking action",
880 "medium/good braking action",
881 "good braking action",
883 "friction: unreliable measurement"
887 // \d\d(CLRD|[\d/]{4})(\d\d|//)
888 bool SGMetar::scanRunwayReport()
895 if (!scanNumber(&m, &i, 2))
900 strcpy(id, "REP"); // repetition of previous report
903 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = 'R', id[3] = '\0';
905 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = '\0';
907 if (!strncmp(m, "CLRD", 4)) {
908 m += 4; // runway cleared
909 r._deposit_string = "cleared";
911 if (scanNumber(&m, &i, 1)) {
913 r._deposit_string = runway_deposit[i];
914 } else if (*m == '/')
919 if (*m == '1' || *m == '2' || *m == '5' || *m == '9') { // extent of deposit
920 r._extent = *m - '0';
921 r._extent_string = runway_deposit_extent[*m - '0'];
922 } else if (*m != '/')
927 if (!strncmp(m, "//", 2))
929 else if (!scanNumber(&m, &i, 2))
933 r._depth = 0.0005; // < 1 mm deep (let's say 0.5 :-)
934 else if (i > 0 && i <= 90)
935 r._depth = i / 1000.0; // i mm deep
936 else if (i >= 92 && i <= 98)
937 r._depth = (i - 90) / 20.0;
939 r._comment = "runway not in use";
940 else if (i == -1) // no depth given ("//")
946 if (m[0] == '/' && m[1] == '/')
948 else if (!scanNumber(&m, &i, 2))
950 if (i >= 1 && i < 90) {
951 r._friction = i / 100.0;
952 } else if ((i >= 91 && i <= 95) || i == 99) {
953 r._friction_string = runway_friction[i - 90];
955 if (!scanBoundary(&m))
958 _runways[id]._deposit = r._deposit;
959 _runways[id]._deposit_string = r._deposit_string;
960 _runways[id]._extent = r._extent;
961 _runways[id]._extent_string = r._extent_string;
962 _runways[id]._depth = r._depth;
963 _runways[id]._friction = r._friction;
964 _runways[id]._friction_string = r._friction_string;
965 _runways[id]._comment = r._comment;
972 // WS (ALL RWYS?|RWY ?\d\d[LCR]?)?
973 bool SGMetar::scanWindShear()
976 if (strncmp(m, "WS", 2))
979 if (!scanBoundary(&m))
982 if (!strncmp(m, "ALL", 3)) {
984 if (!scanBoundary(&m))
986 if (strncmp(m, "RWY", 3))
991 if (!scanBoundary(&m))
993 _runways["ALL"]._wind_shear = true;
1000 for (cnt = 0;; cnt++) { // ??
1001 if (strncmp(m, "RWY", 3))
1006 if (!scanNumber(&m, &i, 2))
1008 if (*m == 'L' || *m == 'C' || *m == 'R')
1010 strncpy(id, mm, i = m - mm);
1012 if (!scanBoundary(&m))
1014 _runways[id]._wind_shear = true;
1017 _runways["ALL"]._wind_shear = true;
1023 bool SGMetar::scanTrendForecast()
1026 if (strncmp(m, "NOSIG", 5))
1030 if (!scanBoundary(&m))
1037 // (BLU|WHT|GRN|YLO|AMB|RED)
1038 static const struct Token colors[] = {
1039 { "BLU", "Blue" }, // 2500 ft, 8.0 km
1040 { "WHT", "White" }, // 1500 ft, 5.0 km
1041 { "GRN", "Green" }, // 700 ft, 3.7 km
1042 { "YLO", "Yellow" }, // 300 ft, 1.6 km
1043 { "AMB", "Amber" }, // 200 ft, 0.8 km
1044 { "RED", "Red" }, // <200 ft, <0.8 km
1049 bool SGMetar::scanColorState()
1052 const struct Token *a;
1053 if (!(a = scanToken(&m, colors)))
1055 if (!scanBoundary(&m))
1057 //printf(Y"Code %s\n"N, a->text);
1063 bool SGMetar::scanRemark()
1065 if (strncmp(_m, "RMK", 3))
1068 if (!scanBoundary(&_m))
1072 if (!scanRunwayReport()) {
1073 while (*_m && !isspace(*_m))
1082 bool SGMetar::scanRemainder()
1085 if (!(strncmp(m, "NOSIG", 5))) {
1087 if (scanBoundary(&m))
1088 _m = m; //_comment.push_back("No significant tendency");
1091 if (!scanBoundary(&m))
1098 bool SGMetar::scanBoundary(char **s)
1100 if (**s && !isspace(**s))
1102 while (isspace(**s))
1108 int SGMetar::scanNumber(char **src, int *num, int min, int max)
1113 for (i = 0; i < min; i++) {
1117 *num = *num * 10 + *s++ - '0';
1119 for (; i < max && isdigit(*s); i++)
1120 *num = *num * 10 + *s++ - '0';
1126 // find longest match of str in list
1127 const struct Token *SGMetar::scanToken(char **str, const struct Token *list)
1129 const struct Token *longest = 0;
1130 int maxlen = 0, len;
1132 for (int i = 0; (s = list[i].id); i++) {
1134 if (!strncmp(s, *str, len) && len > maxlen) {
1144 void SGMetarCloud::set(double alt, Coverage cov)
1151 SGMetarCloud::Coverage SGMetarCloud::getCoverage( const std::string & coverage )
1153 if( coverage == "clear" ) return COVERAGE_CLEAR;
1154 if( coverage == "few" ) return COVERAGE_FEW;
1155 if( coverage == "scattered" ) return COVERAGE_SCATTERED;
1156 if( coverage == "broken" ) return COVERAGE_BROKEN;
1157 if( coverage == "overcast" ) return COVERAGE_OVERCAST;
1158 return COVERAGE_NIL;
1161 const char * SGMetarCloud::COVERAGE_NIL_STRING = "nil";
1162 const char * SGMetarCloud::COVERAGE_CLEAR_STRING = "clear";
1163 const char * SGMetarCloud::COVERAGE_FEW_STRING = "few";
1164 const char * SGMetarCloud::COVERAGE_SCATTERED_STRING = "scattered";
1165 const char * SGMetarCloud::COVERAGE_BROKEN_STRING = "broken";
1166 const char * SGMetarCloud::COVERAGE_OVERCAST_STRING = "overcast";
1168 void SGMetarVisibility::set(double dist, int dir, int mod, int tend)