diff --git a/configure.ac b/configure.ac index 922f7cbc..c0aac66f 100644 --- a/configure.ac +++ b/configure.ac @@ -377,6 +377,7 @@ AC_CONFIG_FILES([ \ simgear/magvar/Makefile \ simgear/math/Makefile \ simgear/metar/Makefile \ + simgear/environment/Makefile \ simgear/misc/Makefile \ simgear/nasal/Makefile \ simgear/props/Makefile \ diff --git a/simgear/Makefile.am b/simgear/Makefile.am index 26761fe8..77036479 100644 --- a/simgear/Makefile.am +++ b/simgear/Makefile.am @@ -5,7 +5,7 @@ SGTHREAD_DIR = endif # METAR_DIRS = -METAR_DIRS = metar +METAR_DIRS = metar environment EXTRA_DIST = simgear_config.h.vc5 version.h.in diff --git a/simgear/environment/.cvsignore b/simgear/environment/.cvsignore new file mode 100644 index 00000000..8c8fabdf --- /dev/null +++ b/simgear/environment/.cvsignore @@ -0,0 +1,4 @@ +.deps +Makefile +Makefile.in +metar diff --git a/simgear/environment/Makefile.am b/simgear/environment/Makefile.am new file mode 100644 index 00000000..1948696e --- /dev/null +++ b/simgear/environment/Makefile.am @@ -0,0 +1,9 @@ +includedir = @includedir@/environment + +lib_LIBRARIES = libsgenvironment.a + +include_HEADERS = metar.hxx + +libsgenvironment_a_SOURCES = metar.cxx + +INCLUDES = -I$(top_srcdir) diff --git a/simgear/environment/metar.cxx b/simgear/environment/metar.cxx new file mode 100644 index 00000000..a3e01623 --- /dev/null +++ b/simgear/environment/metar.cxx @@ -0,0 +1,1101 @@ +// metar interface class +// +// Written by Melchior FRANZ, started December 2003. +// +// Copyright (C) 2003 Melchior FRANZ - mfranz@aon.at +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA +// +// $Id$ + +/** + * @file metar.cxx + * Interface for encoded SGMetar aviation weather data. + */ + +#include + +#include +#include +#include + +#include "metar.hxx" + +#define NaN SGMetarNaN + +/** + * The constructor takes a SGMetar string, or a four-letter ICAO code. In the + * latter case the metar string is downloaded from + * http://weather.noaa.gov/pub/data/observations/metar/stations/. + * The constructor throws sg_io_exceptions on failure. The "METAR" + * keyword has no effect (apart from incrementing the group counter + * @a grpcount) and can be left away. A keyword "SPECI" is + * likewise accepted. + * + * @par Examples: + * @code + * SGMetar *m = new SGMetar("METAR KSFO 061656Z 19004KT 9SM SCT100 OVC200 08/03 A3013"); + * double t = m->getTemperature(); + * delete m; + * + * SGMetar n("KSFO"); + * double d = n.getDewpoint_C(); + * @endcode + */ +SGMetar::SGMetar(const char *m) : + _grpcount(0), + _year(-1), + _month(-1), + _day(-1), + _hour(-1), + _minute(-1), + _report_type(-1), + _wind_dir(-1), + _wind_speed(NaN), + _gust_speed(NaN), + _wind_range_from(-1), + _wind_range_to(-1), + _temp(NaN), + _dewp(NaN), + _pressure(NaN) +{ + int i; + if (isalpha(m[0]) && isalpha(m[1]) && isalpha(m[2]) && isalpha(m[3]) && !m[4]) { + for (i = 0; i < 4; i++) + _icao[i] = toupper(m[i]); + _icao[4] = '\0'; + _data = loadData(_icao); + } else { + _data = new char[strlen(m) + 1]; + strcpy(_data, m); + } + normalizeData(); + + _m = _data; + _icao[0] = '\0'; + + // NOAA preample + scanPreambleDate(); + scanPreambleTime(); + + // METAR header + scanType(); + if (!scanId() || !scanDate()) + throw sg_io_exception("metar data incomplete"); + scanModifier(); + + // base set + scanWind(); + scanVariability(); + while (scanVisibility()) ; + while (scanRwyVisRange()) ; + while (scanWeather()) ; + while (scanSkyCondition()) ; + scanTemperature(); + scanPressure(); + while (scanSkyCondition()) ; + while (scanRunwayReport()) ; + scanWindShear(); + + // appendix + while (scanColorState()) ; + scanTrendForecast(); + while (scanRunwayReport()) ; + scanRemainder(); + scanRemark(); + + if (_grpcount < 4) + throw sg_io_exception("metar data invalid"); +} + + +/** + * Clears lists and maps to discourage access after destruction. + */ +SGMetar::~SGMetar() +{ + _clouds.clear(); + _runways.clear(); + _weather.clear(); + delete[] _data; +} + + +/** + * If called with "KSFO" loads data from + * @code + * http://weather.noaa.gov/pub/data/observations/metar/stations/KSFO.TXT. + * @endcode + * Throws sg_io_exception on failure. Gives up after waiting longer than 10 seconds. + * + * @param id four-letter ICAO SGMetar station code, e.g. "KSFO". + * @return pointer to SGMetar data string, allocated by new char[]. + */ +char *SGMetar::loadData(const char *id) +{ + string host = "weather.noaa.gov"; + string path = "/pub/data/observations/metar/stations/"; + path += string(id) + ".TXT"; + string get = string("GET ") + path + " HTTP/1.0\r\n\r\n"; + + SGSocket *sock = new SGSocket(host, "80", "tcp"); + sock->set_timeout(10000); + if (!sock->open(SG_IO_OUT)) { + delete sock; + string err = "failed to load metar data from http://" + host + path; + throw sg_io_exception(err); + } + + sock->writestring(get.c_str()); + + int i; + const int buflen = 512; + char buf[2 * buflen]; + + // skip HTTP header + while ((i = sock->readline(buf, buflen))) + if (i <= 2 && isspace(buf[0]) && (!buf[1] || isspace(buf[1]))) + break; + if (i) { + i = sock->readline(buf, buflen); + if (i) + sock->readline(&buf[i], buflen); + } + + sock->close(); + delete sock; + + char *metar = new char[strlen(buf) + 1]; + strcpy(metar, buf); + return metar; +} + + +/** + * Replace any number of subsequent spaces by just one space. + * This makes scanning for things like "ALL RWY" easier. + */ +void SGMetar::normalizeData() +{ + char *src, *dest; + for (src = dest = _data; (*dest++ = *src++); ) + while (*src == ' ' && src[1] == ' ') + src++; +} + + +// \d\d\d\d/\d\d/\d\d +bool SGMetar::scanPreambleDate() +{ + char *m = _m; + int year, month, day; + if (!scanNumber(&m, &year, 4)) + return false; + if (*m++ != '/') + return false; + if (!scanNumber(&m, &month, 2)) + return false; + if (*m++ != '/') + return false; + if (!scanNumber(&m, &day, 2)) + return false; + if (!scanBoundary(&m)) + return false; + _year = year; + _month = month; + _day = day; + _m = m; + return true; +} + + +// \d\d:\d\d +bool SGMetar::scanPreambleTime() +{ + char *m = _m; + int hour, minute; + if (!scanNumber(&m, &hour, 2)) + return false; + if (*m++ != ':') + return false; + if (!scanNumber(&m, &minute, 2)) + return false; + if (!scanBoundary(&m)) + return false; + _hour = hour; + _minute = minute; + _m = m; + return true; +} + + +// (METAR|SPECI) +bool SGMetar::scanType() +{ + if (strncmp(_m, "METAR ", 6) && strncmp(_m, "SPECI ", 6)) + return false; + _m += 6; + _grpcount++; + return true; +} + + +// [A-Z]{4} +bool SGMetar::scanId() +{ + char *m = _m; + if (!(isupper(*m++) && isupper(*m++) && isupper(*m++) && isupper(*m++))) + return false; + if (!scanBoundary(&m)) + return false; + strncpy(_icao, _m, 4); + _icao[4] = '\0'; + _m = m; + _grpcount++; + return true; +} + + +// \d{6}Z +bool SGMetar::scanDate() +{ + char *m = _m; + int day, hour, minute; + if (!scanNumber(&m, &day, 2)) + return false; + if (!scanNumber(&m, &hour, 2)) + return false; + if (!scanNumber(&m, &minute, 2)) + return false; + if (*m++ != 'Z') + return false; + if (!scanBoundary(&m)) + return false; + _day = day; + _hour = hour; + _minute = minute; + _m = m; + _grpcount++; + return true; +} + + +// (NIL|AUTO|COR|RTD) +bool SGMetar::scanModifier() +{ + char *m = _m; + int type; + if (!strncmp(m, "NIL", 3)) { + _m += strlen(_m); + return true; + } + if (!strncmp(m, "AUTO", 4)) // automatically generated + m += 4, type = AUTO; + else if (!strncmp(m, "COR", 3)) // manually corrected + m += 3, type = COR; + else if (!strncmp(m, "RTD", 3)) // routine delayed + m += 3, type = RTD; + else + return false; + if (!scanBoundary(&m)) + return false; + _report_type = type; + _m = m; + _grpcount++; + return true; +} + + +// (\d{3}|VRB)\d{1,3}(G\d{2,3})?(KT|KMH|MPS) +bool SGMetar::scanWind() +{ + char *m = _m; + int dir; + if (!strncmp(m, "VRB", 3)) + m += 3, dir = -1; + else if (!scanNumber(&m, &dir, 3)) + return false; + + int i; + if (!scanNumber(&m, &i, 2, 3)) + return false; + double speed = i; + + double gust = NaN; + if (*m == 'G') { + m++; + if (!scanNumber(&m, &i, 2, 3)) + return false; + gust = i; + } + double factor; + if (!strncmp(m, "KT", 2)) + m += 2, factor = SG_KT_TO_MPS; + else if (!strncmp(m, "KMH", 3)) + m += 3, factor = SG_KMH_TO_MPS; + else if (!strncmp(m, "KPH", 3)) // ?? + m += 3, factor = SG_KMH_TO_MPS; + else if (!strncmp(m, "MPS", 3)) + m += 3, factor = 1.0; + else + return false; + if (!scanBoundary(&m)) + return false; + _m = m; + _wind_dir = dir; + _wind_speed = speed * factor; + if (gust != NaN) + _gust_speed = gust * factor; + _grpcount++; + return false; +} + + +// \d{3}V\d{3} +bool SGMetar::scanVariability() +{ + char *m = _m; + int from, to; + if (!scanNumber(&m, &from, 3)) + return false; + if (*m++ != 'V') + return false; + if (!scanNumber(&m, &to, 3)) + return false; + if (!scanBoundary(&m)) + return false; + _m = m; + _wind_range_from = from; + _wind_range_to = to; + _grpcount++; + return true; +} + + +bool SGMetar::scanVisibility() +// TODO: if only directed vis are given, do still set min/max +{ + char *m = _m; + double distance; + int i, dir = -1; + int modifier = SGMetarVisibility::EQUALS; +// \d{4}(N|NE|E|SE|S|SW|W|NW)? + if (scanNumber(&m, &i, 4)) { + if (*m == 'E') + m++, dir = 90; + else if (*m == 'W') + m++, dir = 270; + else if (*m == 'N') { + m++; + if (*m == 'E') + m++, dir = 45; + else if (*m == 'W') + m++, dir = 315; + else + dir = 0; + } else if (*m == 'S') { + m++; + if (*m == 'E') + m++, dir = 135; + else if (*m == 'W') + m++, dir = 225; + else + dir = 180; + } + if (i == 0) + i = 50, modifier = SGMetarVisibility::LESS_THAN; + else if (i == 9999) + i++, modifier = SGMetarVisibility::GREATER_THAN; + distance = i; + } else { +// M?(\d{1,2}|\d{1,2}/\d{1,2}|\d{1,2} \d{1,2}/\d{1,2})(SM|KM) + modifier = 0; + if (*m == 'M') + m++, modifier = SGMetarVisibility::LESS_THAN; + + if (!scanNumber(&m, &i, 1, 2)) + return false; + distance = i; + + if (*m == '/') { + m++; + if (!scanNumber(&m, &i, 1, 2)) + return false; + distance /= i; + } else if (*m == ' ') { + m++; + int denom; + if (!scanNumber(&m, &i, 1, 2)) + return false; + if (*m++ != '/') + return false; + if (!scanNumber(&m, &denom, 1, 2)) + return false; + distance += (double)i / denom; + } + + if (!strncmp(m, "SM", 2)) + distance *= SG_SM_TO_METER, m += 2; + else if (!strncmp(m, "KM", 2)) + distance *= 1000, m += 2; + else + return false; + } + if (!scanBoundary(&m)) + return false; + + SGMetarVisibility *v; + if (dir != -1) + v = &_dir_visibility[dir / 45]; + else if (_min_visibility._distance == NaN) + v = &_min_visibility; + else + v = &_max_visibility; + + v->_distance = distance; + v->_modifier = modifier; + v->_direction = dir; + _m = m; + _grpcount++; + return true; +} + + +// R\d\d[LCR]?/([PM]?\d{4}V)?[PM]?\d{4}(FT)?[DNU]? +bool SGMetar::scanRwyVisRange() +{ + char *m = _m; + int i; + SGMetarRunway r; + if (*m++ != 'R') + return false; + if (!scanNumber(&m, &i, 2)) + return false; + if (*m == 'L' || *m == 'C' || *m == 'R') + m++; + + char id[4]; + strncpy(id, _m + 1, i = m - _m - 1); + id[i] = '\0'; + + if (*m++ != '/') + return false; + + int from, to; + if (*m == 'P') + m++, r._min_visibility._modifier = SGMetarVisibility::GREATER_THAN; + else if (*m == 'M') + m++, r._min_visibility._modifier = SGMetarVisibility::LESS_THAN; + if (!scanNumber(&m, &from, 4)) + return false; + if (*m == 'V') { + m++; + if (*m == 'P') + m++, r._max_visibility._modifier = SGMetarVisibility::GREATER_THAN; + else if (*m == 'M') + m++, r._max_visibility._modifier = SGMetarVisibility::LESS_THAN; + if (!scanNumber(&m, &to, 4)) + return false; + } else + to = from; + + if (!strncmp(m, "FT", 2)) { + from = int(from * SG_FEET_TO_METER); + to = int(to * SG_FEET_TO_METER); + m += 2; + } + r._min_visibility._distance = from; + r._max_visibility._distance = to; + + if (*m == '/') // this is not in the spec! + *m++; + if (*m == 'D') + m++, r._min_visibility._tendency = SGMetarVisibility::DECREASING; + else if (*m == 'N') + m++, r._min_visibility._tendency = SGMetarVisibility::STABLE; + else if (*m == 'U') + m++, r._min_visibility._tendency = SGMetarVisibility::INCREASING; + + if (!scanBoundary(&m)) + return false; + _m = m; + + _runways[id]._min_visibility = r._min_visibility; + _runways[id]._max_visibility = r._max_visibility; + _grpcount++; + return true; +} + + +static const struct Token special[] = { + "NSW", "no significant weather", + "VCSH", "showers in the vicinity", + "VCTS", "thunderstorm in the vicinity", + 0, 0 +}; + + +static const struct Token description[] = { + "SH", "showers of", + "TS", "thunderstorm with", + "BC", "patches of", + "BL", "blowing", + "DR", "low drifting", + "FZ", "freezing", + "MI", "shallow", + "PR", "partial", + 0, 0 +}; + + +static const struct Token phenomenon[] = { + "DZ", "drizzle", + "GR", "hail", + "GS", "small hail and/or snow pellets", + "IC", "ice crystals", + "PE", "ice pellets", + "RA", "rain", + "SG", "snow grains", + "SN", "snow", + "UP", "unknown precipitation", + "BR", "mist", + "DU", "widespread dust", + "SG", "fog", + "SGBR", "fog bank", + "FU", "smoke", + "HZ", "haze", + "PY", "spray", + "SA", "sand", + "VA", "volcanic ash", + "DS", "duststorm", + "FC", "funnel cloud/tornado waterspout", + "PO", "well-developed dust/sand whirls", + "SQ", "squalls", + "SS", "sandstorm", + "UP", "unknown", // ... due to failed automatic acquisition + 0, 0 +}; + + +// (+|-|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} +bool SGMetar::scanWeather() +{ + char *m = _m; + string weather; + const struct Token *a; + if ((a = scanToken(&m, special))) { + if (!scanBoundary(&m)) + return false; + _weather.push_back(a->text); + _m = m; + return true; + } + + string pre, post; + if (*m == '-') + m++, pre = "light "; + else if (*m == '+') + m++, pre = "heavy "; + else if (!strncmp(m, "VC", 2)) + m += 2, post = "in the vicinity "; + else + pre = "moderate "; + + int i; + for (i = 0; i < 3; i++) { + if (!(a = scanToken(&m, description))) + break; + weather += string(a->text) + " "; + } + for (i = 0; i < 3; i++) { + if (!(a = scanToken(&m, phenomenon))) + break; + weather += string(a->text) + " "; + } + if (!weather.length()) + return false; + if (!scanBoundary(&m)) + return false; + _m = m; + weather = pre + weather + post; + weather.erase(weather.length() - 1); + _weather.push_back(weather); + _grpcount++; + return true; +} + + +static const struct Token cloud_types[] = { + "AC", "altocumulus", + "ACC", "altocumulus castellanus", + "ACSL", "altocumulus standing lenticular", + "AS", "altostratus", + "CB", "cumulonimbus", + "CBMAM", "cumulonimbus mammatus", + "CC", "cirrocumulus", + "CCSL", "cirrocumulus standing lenticular", + "CI", "cirrus", + "CS", "cirrostratus", + "CU", "cumulus", + "CUFRA", "cumulus fractus", + "NS", "nimbostratus", + "SAC", "stratoaltocumulus", // guessed + "SC", "stratocumulus", + "SCSL", "stratocumulus standing lenticular", + "ST", "stratus", + "STFRA", "stratus fractus", + "TCU", "towering cumulus", + 0, 0 +}; + + +// (FEW|SCT|BKN|OVC|SKC|CLR|CAVOK|VV)([0-9]{3}|///)?[:cloud_type:]? +bool SGMetar::scanSkyCondition() +{ + char *m = _m; + int i; + SGMetarCloud cl; + + if (!strncmp(m, "CLR", i = 3) // clear + || !strncmp(m, "SKC", i = 3) // sky clear + || !strncmp(m, "NSC", i = 3) // no significant clouds + || !strncmp(m, "CAVOK", i = 5)) { // ceiling and visibility OK (implies 9999) + m += i; + if (!scanBoundary(&m)) + return false; + cl._coverage = 0; + _clouds.push_back(cl); + _m = m; + return true; + } + + if (!strncmp(m, "VV", i = 2)) // vertical visibility + ; + else if (!strncmp(m, "FEW", i = 3)) + cl._coverage = 1; + else if (!strncmp(m, "SCT", i = 3)) + cl._coverage = 2; + else if (!strncmp(m, "BKN", i = 3)) + cl._coverage = 3; + else if (!strncmp(m, "OVC", i = 3)) + cl._coverage = 4; + else + return false; + m += i; + + if (!strncmp(m, "///", 3)) // vis not measurable (e.g. because of heavy snowing) + m += 3, i = -1; + else if (scanBoundary(&m)) { + _m = m; + return true; // ignore single OVC/BKN/... + } else if (!scanNumber(&m, &i, 3)) + i = -1; + + if (cl._coverage == -1) { + if (!scanBoundary(&m)) + return false; + if (i == -1) // 'VV///' + _vert_visibility._modifier = SGMetarVisibility::NOGO; + else + _vert_visibility._distance = i * 100 * SG_FEET_TO_METER; + _m = m; + return true; + } + + if (i != -1) + cl._altitude = i * 100 * SG_FEET_TO_METER; + + const struct Token *a; + if ((a = scanToken(&m, cloud_types))) { + cl._type = a->id; + cl._type_long = a->text; + } + if (!scanBoundary(&m)) + return false; + _clouds.push_back(cl); + _m = m; + _grpcount++; + return true; +} + + +// M?[0-9]{2}/(M?[0-9]{2})? (spec) +// (M?[0-9]{2}|XX)/(M?[0-9]{2}|XX)? (Namibia) +bool SGMetar::scanTemperature() +{ + char *m = _m; + int sign = 1, temp, dew; + if (!strncmp(m, "XX/XX", 5)) { // not spec compliant! + _m += 5; + return scanBoundary(&_m); + } + + if (*m == 'M') + m++, sign = -1; + if (!scanNumber(&m, &temp, 2)) + return false; + temp *= sign; + + if (*m++ != '/') + return false; + if (!scanBoundary(&m)) { + if (!strncmp(m, "XX", 2)) // not spec compliant! + m += 2, sign = 0; + else { + sign = 1; + if (*m == 'M') + m++, sign = -1; + if (!scanNumber(&m, &dew, 2)) + return false; + } + if (!scanBoundary(&m)) + return false; + if (sign) + _dewp = sign * dew; + } + _temp = temp; + _m = m; + _grpcount++; + return true; +} + + +double SGMetar::getRelHumidity() const +{ + if (_temp == NaN || _dewp == NaN) + return NaN; + double dewp = pow(10, 7.5 * _dewp / (237.7 + _dewp)); + double temp = pow(10, 7.5 * _temp / (237.7 + _temp)); + return dewp * 100 / temp; +} + + +// [AQ]\d{4} (spec) +// [AQ]\d{2}(\d{2}|//) (Namibia) +bool SGMetar::scanPressure() +{ + char *m = _m; + double factor; + int press, i; + + if (*m == 'A') + factor = SG_INHG_TO_PA / 100; + else if (*m == 'Q') + factor = 100; + else + return false; + m++; + if (!scanNumber(&m, &press, 2)) + return false; + press *= 100; + if (!strncmp(m, "//", 2)) // not spec compliant! + m += 2; + else if (scanNumber(&m, &i, 2)) + press += i; + else + return false; + if (!scanBoundary(&m)) + return false; + _pressure = press * factor; + _m = m; + _grpcount++; + return true; +} + + +static const char *runway_deposit[] = { + "clear and dry", + "damp", + "wet or puddles", + "frost", + "dry snow", + "wet snow", + "slush", + "ice", + "compacted snow", + "frozen ridges" +}; + + +static const char *runway_deposit_extent[] = { + 0, "1-10%", "11-25%", 0, 0, "26-50%", 0, 0, 0, "51-100%" +}; + + +static const char *runway_friction[] = { + 0, + "poor braking action", + "poor/medium braking action", + "medium braking action", + "medium/good braking action", + "good braking action", + 0, 0, 0, + "friction: unreliable measurement" +}; + + +// \d\d(CLRD|[\d/]{4})(\d\d|//) +bool SGMetar::scanRunwayReport() +{ + char *m = _m; + int i; + char id[4]; + SGMetarRunway r; + + if (!scanNumber(&m, &i, 2)) + return false; + if (i == 88) + strcpy(id, "ALL"); + else if (i == 99) + strcpy(id, "REP"); // repetition of previous report + else if (i >= 50) { + i -= 50; + id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = 'R', id[3] = '\0'; + } else + id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = '\0'; + + if (!strncmp(m, "CLRD", 4)) { + m += 4; // runway cleared + r._deposit = "cleared"; + } else { + if (scanNumber(&m, &i, 1)) { + r._deposit = runway_deposit[i]; + } else if (*m == '/') + m++; + else + return false; + if (*m == '1' || *m == '2' || *m == '5' || *m == '9') { // extent of deposit + r._extent = *m - '0'; + r._extent_string = runway_deposit_extent[*m - '0']; + } else if (*m != '/') + return false; + m++; + i = -1; + if (!strncmp(m, "//", 2)) + m += 2; + else if (!scanNumber(&m, &i, 2)) + return false; + + if (i == 0) + r._depth = 0.5; // < 1 mm deep (let's say 0.5 :-) + else if (i > 0 && i <= 90) + r._depth = i / 1000.0; // i mm deep + else if (i >= 92 && i <= 98) + r._depth = (i - 90) / 20.0; + else if (i == 99) + r._comment = "runway not in use"; + else if (i == -1) // no depth given ("//") + ; + else + return false; + } + i = -1; + if (m[0] == '/' && m[1] == '/') + m += 2; + else if (!scanNumber(&m, &i, 2)) + return false; + if (i >= 1 && i < 90) { + r._friction = i / 100.0; + } else if ((i >= 91 && i <= 95) || i == 99) { + r._friction_string = runway_friction[i - 90]; + } + if (!scanBoundary(&m)) + return false; + + _runways[id]._deposit = r._deposit; + _runways[id]._extent = r._extent; + _runways[id]._extent_string = r._extent_string; + _runways[id]._depth = r._depth; + _runways[id]._friction = r._friction; + _runways[id]._friction_string = r._friction_string; + _runways[id]._comment = r._comment; + _m = m; + _grpcount++; + return true; +} + + +// WS (ALL RWYS?|RWY ?\d\d[LCR]?)? +bool SGMetar::scanWindShear() +{ + char *m = _m; + if (strncmp(m, "WS", 2)) + return false; + m += 2; + if (!scanBoundary(&m)) + return false; + + if (!strncmp(m, "ALL", 3)) { + m += 3; + if (!scanBoundary(&m)) + return false; + if (strncmp(m, "RWY", 3)) + return false; + m += 3; + if (*m == 'S') + m++; + if (!scanBoundary(&m)) + return false; + _runways["ALL"]._wind_shear = true; + _m = m; + return true; + } + + char id[4], *mm; + int i, cnt; + for (cnt = 0;; cnt++) { // ?? + if (strncmp(m, "RWY", 3)) + break; + m += 3; + scanBoundary(&m); + mm = m; + if (!scanNumber(&m, &i, 2)) + return false; + if (*m == 'L' || *m == 'C' || *m == 'R') + m++; + strncpy(id, mm, i = m - mm); + id[i] = '\0'; + if (!scanBoundary(&m)) + return false; + _runways[id]._wind_shear = true; + } + if (!cnt) + _runways["ALL"]._wind_shear = true; + _m = m; + return true; +} + + +bool SGMetar::scanTrendForecast() +{ + char *m = _m; + if (strncmp(m, "NOSIG", 5)) + return false; + + m += 5; + if (!scanBoundary(&m)) + return false; + _m = m; + return true; +} + + +// (BLU|WHT|GRN|YLO|AMB|RED) +static const struct Token colors[] = { + "BLU", "Blue", // 2500 ft, 8.0 km + "WHT", "White", // 1500 ft, 5.0 km + "GRN", "Green", // 700 ft, 3.7 km + "YLO", "Yellow", // 300 ft, 1.6 km + "AMB", "Amber", // 200 ft, 0.8 km + "RED", "Red", // <200 ft, <0.8 km + 0, 0 +}; + + +bool SGMetar::scanColorState() +{ + char *m = _m; + const struct Token *a; + if (!(a = scanToken(&m, colors))) + return false; + if (!scanBoundary(&m)) + return false; + //printf(Y"Code %s\n"N, a->text); + _m = m; + return true; +} + + +bool SGMetar::scanRemark() +{ + if (strncmp(_m, "RMK", 3)) + return false; + _m += 3; + if (!scanBoundary(&_m)) + return false; + + while (*_m) { + if (!scanRunwayReport()) { + while (*_m && !isspace(*_m)) + _m++; + scanBoundary(&_m); + } + } + return true; +} + + +bool SGMetar::scanRemainder() +{ + char *m = _m; + if (!(strncmp(m, "NOSIG", 5))) { + m += 5; + if (scanBoundary(&m)) + _m = m; //_comment.push_back("No significant tendency"); + } + + if (!scanBoundary(&m)) + return false; + _m = m; + return true; +} + + +bool SGMetar::scanBoundary(char **s) +{ + if (**s && !isspace(**s)) + return false; + while (isspace(**s)) + (*s)++; + return true; +} + + +int SGMetar::scanNumber(char **src, int *num, int min, int max) +{ + int i; + char *s = *src; + *num = 0; + for (i = 0; i < min; i++) { + if (!isdigit(*s)) + return 0; + else + *num = *num * 10 + *s++ - '0'; + } + for (; i < max && isdigit(*s); i++) + *num = *num * 10 + *s++ - '0'; + *src = s; + return i; +} + + +// find longest match of str in list +const struct Token *SGMetar::scanToken(char **str, const struct Token *list) +{ + const struct Token *longest = 0; + int maxlen = 0, len; + char *s; + for (int i = 0; (s = list[i].id); i++) { + len = strlen(s); + if (!strncmp(s, *str, len) && len > maxlen) { + maxlen = len; + longest = &list[i]; + } + } + *str += maxlen; + return longest; +} + +#undef NaN diff --git a/simgear/environment/metar.hxx b/simgear/environment/metar.hxx new file mode 100644 index 00000000..275c7cbe --- /dev/null +++ b/simgear/environment/metar.hxx @@ -0,0 +1,261 @@ +// metar interface class +// +// Written by Melchior FRANZ, started December 2003. +// +// Copyright (C) 2003 Melchior FRANZ - mfranz@aon.at +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA +// +// $Id$ + +#ifndef _METAR_HXX +#define _METAR_HXX + +#include +#include +#include + +#include + +SG_USING_STD(vector); +SG_USING_STD(map); +SG_USING_STD(string); + +const double SGMetarNaN = -1E20; +#define NaN SGMetarNaN + +struct Token { + char *id; + char *text; +}; + + +class SGMetar; + +class SGMetarVisibility { + friend class SGMetar; +public: + SGMetarVisibility() : + _distance(NaN), + _direction(-1), + _modifier(EQUALS), + _tendency(NONE) {} + + enum Modifier { + NOGO, + EQUALS, + LESS_THAN, + GREATER_THAN + }; + + enum Tendency { + NONE, + STABLE, + INCREASING, + DECREASING + }; + + inline double getVisibility_m() const { return _distance; } + inline double getVisibility_ft() const { return _distance == NaN ? NaN : _distance * SG_METER_TO_FEET; } + inline double getVisibility_sm() const { return _distance == NaN ? NaN : _distance * SG_METER_TO_SM; } + inline int getDirection() const { return _direction; } + inline int getModifier() const { return _modifier; } + inline int getTendency() const { return _tendency; } + +protected: + double _distance; + int _direction; + int _modifier; + int _tendency; +}; + + +// runway condition (surface and visibility) +class SGMetarRunway { + friend class SGMetar; +public: + SGMetarRunway() : + _deposit(0), + _extent(-1), + _extent_string(0), + _depth(NaN), + _friction(NaN), + _friction_string(0), + _comment(0), + _wind_shear(false) {} + + inline const char *getDeposit() const { return _deposit; } + inline double getExtent() const { return _extent; } + inline const char *getExtentString() const { return _extent_string; } + inline double getDepth() const { return _depth; } + inline double getFriction() const { return _friction; } + inline const char *getFrictionString() const { return _friction_string; } + inline const char *getComment() const { return _comment; } + inline const bool getWindShear() const { return _wind_shear; } + inline SGMetarVisibility getMinVisibility() const { return _min_visibility; } + inline SGMetarVisibility getMaxVisibility() const { return _max_visibility; } + +protected: + SGMetarVisibility _min_visibility; + SGMetarVisibility _max_visibility; + const char *_deposit; + int _extent; + const char *_extent_string; + double _depth; + double _friction; + const char *_friction_string; + const char *_comment; + bool _wind_shear; +}; + + +// cloud layer +class SGMetarCloud { + friend class SGMetar; +public: + SGMetarCloud() : + _coverage(-1), + _altitude(NaN), + _type(0), + _type_long(0) {} + + inline int getCoverage() const { return _coverage; } + inline double getAltitude_m() const { return _altitude; } + inline double getAltitude_ft() const { return _altitude == NaN ? NaN : _altitude * SG_METER_TO_FEET; } + inline char *getTypeString() const { return _type; } + inline char *getTypeLongString() const { return _type_long; } + +protected: + int _coverage; // quarters: 0 -> clear ... 4 -> overcast + double _altitude; // 1000 m + char *_type; // CU + char *_type_long; // cumulus +}; + + +class SGMetar { +public: + SGMetar(const char *m); + SGMetar(const string m) { SGMetar(m.c_str()); } + ~SGMetar(); + + enum ReportType { + NONE, + AUTO, + COR, + RTD + }; + + inline const char *getData() const { return _data; } + inline const char *getUnusedData() const { return _m; } + inline const char *getId() const { return _icao; } + inline int getYear() const { return _year; } + inline int getMonth() const { return _month; } + inline int getDay() const { return _day; } + inline int getHour() const { return _hour; } + inline int getMinute() const { return _minute; } + inline int getReportType() const { return _report_type; } + + inline int getWindDir() const { return _wind_dir; } + inline double getWindSpeed_mps() const { return _wind_speed; } + inline double getWindSpeed_kmh() const { return _wind_speed == NaN ? NaN : _wind_speed * 3.6; } + inline double getWindSpeed_kt() const { return _wind_speed == NaN ? NaN : _wind_speed * SG_MPS_TO_KT; } + inline double getWindSpeed_mph() const { return _wind_speed == NaN ? NaN : _wind_speed * SG_MPS_TO_MPH; } + + inline double getGustSpeed_mps() const { return _gust_speed; } + inline double getGustSpeed_kmh() const { return _gust_speed == NaN ? NaN : _gust_speed * 3.6; } + inline double getGustSpeed_kt() const { return _gust_speed == NaN ? NaN : _gust_speed * SG_MPS_TO_KT; } + inline double getGustSpeed_mph() const { return _gust_speed == NaN ? NaN : _gust_speed * SG_MPS_TO_MPH; } + + inline int getWindRangeFrom() const { return _wind_range_from; } + inline int getWindRangeTo() const { return _wind_range_to; } + + inline SGMetarVisibility& getMinVisibility() { return _min_visibility; } + inline SGMetarVisibility& getMaxVisibility() { return _max_visibility; } + inline SGMetarVisibility& getVertVisibility() { return _vert_visibility; } + inline SGMetarVisibility *getDirVisibility() { return _dir_visibility; } + + inline double getTemperature_C() const { return _temp; } + inline double getTemperature_F() const { return _temp == NaN ? NaN : 1.8 * _temp + 32; } + inline double getDewpoint_C() const { return _dewp; } + inline double getDewpoint_F() const { return _dewp == NaN ? NaN : 1.8 * _dewp + 32; } + inline double getPressure_hPa() const { return _pressure == NaN ? NaN : _pressure / 100; } + inline double getPressure_inHg() const { return _pressure == NaN ? NaN : _pressure * SG_PA_TO_INHG; } + + double getRelHumidity() const; + + inline vector& getClouds() { return _clouds; } + inline map& getRunways() { return _runways; } + inline vector& getWeather() { return _weather; } + +protected: + int _grpcount; + char *_data; + char *_m; + char _icao[5]; + int _year; + int _month; + int _day; + int _hour; + int _minute; + int _report_type; + int _wind_dir; + double _wind_speed; + double _gust_speed; + int _wind_range_from; + int _wind_range_to; + double _temp; + double _dewp; + double _pressure; + + SGMetarVisibility _min_visibility; + SGMetarVisibility _max_visibility; + SGMetarVisibility _vert_visibility; + SGMetarVisibility _dir_visibility[8]; + vector _clouds; + map _runways; + vector _weather; + + bool scanPreambleDate(); + bool scanPreambleTime(); + + bool scanType(); + bool scanId(); + bool scanDate(); + bool scanModifier(); + bool scanWind(); + bool scanVariability(); + bool scanVisibility(); + bool scanRwyVisRange(); + bool scanSkyCondition(); + bool scanWeather(); + bool scanTemperature(); + bool scanPressure(); + bool scanRunwayReport(); + bool scanWindShear(); + bool scanTrendForecast(); + bool scanColorState(); + bool scanRemark(); + bool scanRemainder(); + + int scanNumber(char **str, int *num, int min, int max = 0); + bool scanBoundary(char **str); + const struct Token *scanToken(char **str, const struct Token *list); + char *loadData(const char *id); + void normalizeData(); +}; + +#undef NaN +#endif // _METAR_HXX