From d9ab61215498feeccbc2d82c87413b8d7ac290ff Mon Sep 17 00:00:00 2001 From: Roland Winklmeier Date: Mon, 29 Jun 2015 11:33:29 +0000 Subject: [PATCH] refs #448 metar decoder and unit tests --- src/blackmisc/weather/metardecoder.cpp | 859 +++++++++++++++++++++++++ src/blackmisc/weather/metardecoder.h | 53 ++ tests/blackmisc/testblackmiscmain.cpp | 3 + tests/blackmisc/testweather.cpp | 83 +++ tests/blackmisc/testweather.h | 36 ++ 5 files changed, 1034 insertions(+) create mode 100644 src/blackmisc/weather/metardecoder.cpp create mode 100644 src/blackmisc/weather/metardecoder.h create mode 100644 tests/blackmisc/testweather.cpp create mode 100644 tests/blackmisc/testweather.h diff --git a/src/blackmisc/weather/metardecoder.cpp b/src/blackmisc/weather/metardecoder.cpp new file mode 100644 index 000000000..a2ba05ccc --- /dev/null +++ b/src/blackmisc/weather/metardecoder.cpp @@ -0,0 +1,859 @@ +/* Copyright (C) 2015 + * swift project Community / Contributors + * + * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level + * directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project, + * including this file, may be copied, modified, propagated, or distributed except according to the terms + * contained in the LICENSE file. + */ + +#include "metardecoder.h" +#include "blackmiscfreefunctions.h" +#include "blackmisc/logmessage.h" +#include "blackmisc/weather/presentweather.h" +#include +#include + +using namespace BlackMisc::PhysicalQuantities; +using namespace BlackMisc::Aviation; + +namespace BlackMisc +{ + namespace Weather + { + + // Implementation based on the following websites: + // http://meteocentre.com/doc/metar.html + // http://www.sigmet.de/key.php + // http://wx.erau.edu/reference/text/metar_code_format.pdf + + class IMetarDecoderPart + { + protected: + virtual bool isRepeatable() const { return false; } + virtual QString getRegExp() const = 0; + virtual bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const = 0; + virtual bool isMandatory() const = 0; + virtual QString getDecoderType() const = 0; + + public: + bool parse(QString &metarString, CMetar &metar) + { + bool isValid = false; + QRegularExpression re(getRegExp()); + Q_ASSERT(re.isValid()); + // Loop stop condition: + // - Invalid data + // - One match found and token not repeatable + do + { + QRegularExpressionMatch match = re.match(metarString); + if (match.hasMatch()) + { + // If invalid data, we return straight away + if (!validateAndSet(match, metar)) return false; + // Remove the captured part + metarString.replace(re, QString()); + isValid = true; + } + else + { + // No (more) match found. + if (!isMandatory()) { isValid = true; } + break; + } + } + while (isRepeatable()); + + if (!isValid) { CLogMessage(this).warning("Failed to match %1 in remaining metar: %2") << getDecoderType() << metarString; } + return isValid; + } + }; + + class CMetarDecoderReportType : public IMetarDecoderPart + { + protected: + QString getRegExp() const override { return QStringLiteral("^(?METAR|SPECI)? "); } + + bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override + { + QString reportTypeAsString = match.captured("reporttype"); + if (reportTypeAsString.isEmpty() || !getReportTypeHash().contains(reportTypeAsString)) + { + CLogMessage(this).warning("Invalid metar report type %1 in metar: %2") << match.captured(0); + return true; + } + + metar.setReportType(getReportTypeHash().value(reportTypeAsString)); + return true; + } + + virtual bool isMandatory() const override { return false; } + virtual QString getDecoderType() const override { return "ReportType"; } + + private: + const QHash &getReportTypeHash() const + { + static const QHash hash = + { + { "METAR", CMetar::METAR }, + { "SPECI", CMetar::SPECI } + }; + return hash; + } + }; + + class CMetarDecoderAirport : public IMetarDecoderPart + { + protected: + QString getRegExp() const override { return QStringLiteral("^(?\\w{4}) "); } + + bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const + { + QString airportAsString = match.captured("airport"); + Q_ASSERT(!airportAsString.isEmpty()); + metar.setAirportIcaoCode(CAirportIcaoCode(airportAsString)); + return true; + } + + virtual bool isMandatory() const override { return true; } + virtual QString getDecoderType() const override { return "Airport"; } + }; + + class CMetarDecoderDayTime : public IMetarDecoderPart + { + protected: + QString getRegExp() const override { return QStringLiteral("^(?\\d{2})(?\\d{2})(?\\d{2})Z "); } + + bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override + { + bool ok = false; + int day = match.captured("day").toInt(&ok); + int hour = match.captured("hour").toInt(&ok); + int minute = match.captured("minute").toInt(&ok); + if (!ok) return false; + + if (day < 1 || day > 31) return false; + if (hour < 0 || hour > 23) return false; + if (minute < 0 || minute > 59) return false; + + BlackMisc::PhysicalQuantities::CTime time(hour, minute, 0); + metar.setDayTime(day, time); + return true; + } + + virtual bool isMandatory() const override { return true; } + virtual QString getDecoderType() const override { return "DayTime"; } + }; + + class CMetarDecoderStatus : public IMetarDecoderPart + { + protected: + // Possible matches: + // * (AUTO) - Automatic Station Indicator + // * (NIL) - NO METAR + // * (BBB) - Correction Indicator + QString getRegExp() const override { return QStringLiteral("^([A-Z]+) "); } + + bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override + { + if (match.captured(1) == "AUTO") { metar.setAutomated(true); return true; } + else if (match.captured(1) == "NIL") { /* todo */ return true; } + else if (match.captured(1).size() == 3) { /* todo */ return true; } + else { return false; } + } + + virtual bool isMandatory() const override { return false; } + virtual QString getDecoderType() const override { return "Status"; } + }; + + class CMetarDecoderWind : public IMetarDecoderPart + { + protected: + + const QHash &getWindUnitHash() const + { + static const QHash hash = + { + { "KT", CSpeedUnit::kts() }, + { "MPS", CSpeedUnit::m_s() }, + { "KPH", CSpeedUnit::km_h() }, + { "KMH", CSpeedUnit::km_h() } + }; + return hash; + } + + QString getRegExp() const override + { + // Wind direction in three digits, 'VRB' or /// if no info available + static const QString direction = QStringLiteral("(?\\d{3}|VRB|/{3})"); + // Wind speed in two digits (or three digits if required) + static const QString speed = QStringLiteral("(?\\d{2,3}|/{2})"); + // Optional: Gust in two digits (or three digits if required) + static const QString gustSpeed = QStringLiteral("(G(?\\d{2,3}))?"); + // Unit + static const QString unit = QStringLiteral("(?") + QStringList(getWindUnitHash().keys()).join('|') + ")"; + // Regexp + static const QString regexp = "^" + direction + speed + gustSpeed + unit + " ?"; + return regexp; + } + + bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override + { + bool ok = false; + QString directionAsString = match.captured("direction"); + if (directionAsString == "///") return true; + int direction = 0; + bool directionVariable = false; + if (directionAsString == "VRB") + { + directionVariable = true; + } + else + { + direction = directionAsString.toInt(&ok); + if (!ok) return false; + } + + QString speedAsString = match.captured("speed"); + if (speedAsString == "//") return true; + int speed = speedAsString.toInt(&ok); + if (!ok) return false; + QString gustAsString = match.captured("gustSpeed"); + int gustSpeed = 0; + if (!gustAsString.isEmpty()) + { + gustSpeed = gustAsString.toInt(&ok); + if (!ok) return false; + } + QString unitAsString = match.captured("unit"); + if (!getWindUnitHash().contains(unitAsString)) return false; + + CWindLayer windLayer(CAltitude(0, CAltitude::AboveGround, CLengthUnit::ft()), CAngle(direction, CAngleUnit::deg()), CSpeed(speed, getWindUnitHash().value(unitAsString)), + CSpeed(gustSpeed, getWindUnitHash().value(unitAsString))); + windLayer.setDirectionVariable(directionVariable); + metar.setWindLayer(windLayer); + return true; + } + + virtual bool isMandatory() const override { return false; } + virtual QString getDecoderType() const override { return "Wind"; } + }; + + class CMetarDecoderVariationsWindDirection : public IMetarDecoderPart + { + protected: + QString getRegExp() const override + { + // V in degrees + static const QString directionFrom("(?\\d{3})V"); + // in degrees + static const QString directionTo("(?\\d{3})"); + // Add space at the end + static const QString regexp = "^" + directionFrom + directionTo + " "; + return regexp; + } + + bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override + { + QString directionFromAsString = match.captured("direction_from"); + QString directionToAsString = match.captured("direction_to"); + + int directionFrom = 0; + int directionTo = 0; + + bool ok = false; + directionFrom = directionFromAsString.toInt(&ok); + directionTo = directionToAsString.toInt(&ok); + if (!ok) return false; + + auto windLayer = metar.getWindLayer(); + windLayer.setDirection(CAngle(directionFrom, CAngleUnit::deg()), CAngle(directionTo, CAngleUnit::deg())); + metar.setWindLayer(windLayer); + return true; + } + + virtual bool isMandatory() const override { return false; } + virtual QString getDecoderType() const override { return "WindDirection"; } + }; + + class CMetarDecoderVisibility : public IMetarDecoderPart + { + protected: + + const QHash &getCardinalDirections() const + { + static const QHash hash = + { + { "N", "north" }, + { "NE", "north-east" }, + { "E", "east" }, + { "SE", "south-east" }, + { "S", "south" }, + { "SW", "south-west" }, + { "W", "west" }, + { "NW", "north-west" }, + }; + return hash; + } + + QString getRegExp() const override + { + + // CAVOK + static const QString cavok = QStringLiteral("(?CAVOK)"); + // European version: + // Visibility of 4 digits in meter + // Cardinal directions N, NE etc. + // "////" in case no info is available + // NDV = No Directional Variation + static const QString visibility_eu = QStringLiteral("(?\\d{4}|/{4})(NDV)?") + "(" + QStringList(getCardinalDirections().keys()).join('|') + ")?"; + // US/Canada version: + // Surface visibility reported in statute miles. + // A space divides whole miles and fractions. + // Group ends with SM to indicate statute miles. For example, + // 1 1/2SM. + // Auto only: M prefixed to value < 1/4 mile, e.g., M1/4S + static const QString visibility_us = QStringLiteral("(?\\d{0,2}) ?M?((?\\d)/(?\\d))?(?SM|KM)"); + + static const QString regexp = "^(" + cavok + "|" + visibility_eu + "|" + visibility_us + ") "; + return regexp; + } + + bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override + { + bool ok = false; + if (!match.captured("cavok").isEmpty()) { metar.setCavok(); return true; } + QString visibilityAsString = match.captured("visibility"); + if (visibilityAsString == "////") return true; + + double visibility = 0; + if (!visibilityAsString.isEmpty()) + { + visibility = visibilityAsString.toDouble(&ok); + if (!ok) return false; + metar.setVisibility(CLength(visibility, CLengthUnit::m())); + return true; + } + + QString distanceAsString = match.captured("distance"); + if (!distanceAsString.isEmpty()) + { + visibility += distanceAsString.toDouble(&ok); + if (!ok) return false; + } + QString numeratorAsString = match.captured("numerator"); + QString denominatorAsString = match.captured("denominator"); + if (!numeratorAsString.isEmpty() && !denominatorAsString.isEmpty()) + { + + double numerator = numeratorAsString.toDouble(&ok); + if (!ok) return false; + + double denominator = denominatorAsString.toDouble(&ok); + if (!ok) return false; + if (denominator < 1 || numerator < 1) return false; + visibility += (numerator / denominator); + } + + QString unitAsString = match.captured("unit"); + CLengthUnit unit = CLengthUnit::SM(); + if (unitAsString == "KM") unit = CLengthUnit::km(); + + metar.setVisibility(CLength(visibility, unit)); + + return true; + } + + virtual bool isMandatory() const override { return false; } + virtual QString getDecoderType() const override { return "Visibility"; } + }; + + class CMetarDecoderRunwayVisualRange : public IMetarDecoderPart + { + protected: + QString getRegExp() const override + { + // 10 Minute RVR value: Reported in hundreds of feet if visibility is ≤ one statute mile + // or RVR is ≤ 6000 feet. Group ends with FT to indicate feet. For example, R06L/2000FT. + // The RVR value is prefixed with either M or P to indicate the value is lower or higher + // than the RVR reportable values, e.g., R06L/P6000FT. If the RVR is variable during the 10 + // minute evaluation period, the variability is reported, e.g., R06L/2000V4000FT + + // Runway + static const QString runway = QStringLiteral("R(?\\d{2}[LCR]*)"); + // Visibility + static const QString visibility = QStringLiteral("/[PM]?(?\\d{4})"); + // Variability + static const QString variability = QStringLiteral("V?(?\\d{4})?"); + // Unit + static const QString unit = QStringLiteral("(?FT)?"); + // Trend + static const QString trend = QStringLiteral("/?(?[DNU])?"); + + static const QString regexp = "^" + runway + visibility + variability + unit + trend + " "; + return regexp; + } + + bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override + { + QString runway = match.captured("runway"); + QString runwayVisibilityAsString = match.captured("rwy_visibility"); + Q_ASSERT(!runway.isEmpty() && !runwayVisibilityAsString.isEmpty()); + + bool ok = false; + double runwayVisibility = runwayVisibilityAsString.toDouble(&ok); + if (!ok) return false; + CLengthUnit lengthUnit = CLengthUnit::m(); + if (match.captured("unit") == "FT") lengthUnit = CLengthUnit::ft(); + // Ignore for now until we make use of it. + Q_UNUSED(metar); + Q_UNUSED(runwayVisibility); + Q_UNUSED(lengthUnit); + return true; + } + + virtual bool isMandatory() const override { return false; } + virtual QString getDecoderType() const override { return "RunwayVisualRange"; } + }; + + class CMetarDecoderPresentWeather : public IMetarDecoderPart + { + protected: + const QHash &getIntensityHash() const + { + static const QHash hash = + { + { "-", CPresentWeather::Light }, + { "+", CPresentWeather::Heavy }, + { "VC", CPresentWeather::InVincinity } + }; + return hash; + } + + const QHash &getDescriptorHash() const + { + static const QHash hash = + { + { "MI", CPresentWeather::Shallow }, + { "BC", CPresentWeather::Patches }, + { "PR", CPresentWeather::Partial }, + { "DR", CPresentWeather::Drifting }, + { "BL", CPresentWeather::Blowing }, + { "SH", CPresentWeather::Showers }, + { "TS", CPresentWeather::Thunderstorm }, + { "FR", CPresentWeather::Freezing }, + }; + return hash; + } + + const QHash &getWeatherPhenomenaHash() const + { + static const QHash hash = + { + { "DZ", CPresentWeather::Drizzle }, + { "RA", CPresentWeather::Rain }, + { "SN", CPresentWeather::Snow }, + { "SG", CPresentWeather::SnowGrains }, + { "IC", CPresentWeather::IceCrystals }, + { "PC", CPresentWeather::IcePellets }, + { "GR", CPresentWeather::Hail }, + { "GS", CPresentWeather::SnowPellets }, + { "UP", CPresentWeather::Unknown }, + { "BR", CPresentWeather::Mist }, + { "FG", CPresentWeather::Fog }, + { "FU", CPresentWeather::Smoke }, + { "VA", CPresentWeather::VolcanicAsh }, + { "DU", CPresentWeather::Dust }, + { "SA", CPresentWeather::Sand }, + { "HZ", CPresentWeather::Haze }, + { "PO", CPresentWeather::DustSandWhirls }, + { "SQ", CPresentWeather::Squalls }, + { "FC", CPresentWeather::TornadoOrWaterspout }, + { "FC", CPresentWeather::FunnelCloud }, + { "SS", CPresentWeather::Sandstorm }, + { "DS", CPresentWeather::Duststorm }, + { "//", {} }, + }; + return hash; + } + + virtual bool isRepeatable() const { return true; } + + // w'w' represents present weather, coded in accordance with WMO Code Table 4678. + // As many groups as necessary are included, with each group containing from 2 to 9 characters. + // * Weather phenomena are preceded by one or two qualifiers + // * No w'w' group has more than one descriptor. + QString getRegExp() const override + { + // Qualifier intensity. (-) light (no sign) moderate (+) heavy or VC + static const QString qualifier_intensity("(?[-+]|VC)?"); + // Descriptor, if any + static const QString qualifier_descriptor = "(?" + QStringList(getDescriptorHash().keys()).join('|') + ")?"; + static const QString weatherPhenomenaJoined = QStringList(getWeatherPhenomenaHash().keys()).join('|'); + static const QString weather_phenomina1 = "(?" + weatherPhenomenaJoined + ")?"; + static const QString weather_phenomina2 = "(?" + weatherPhenomenaJoined + ")?"; + static const QString weather_phenomina3 = "(?" + weatherPhenomenaJoined + ")?"; + static const QString weather_phenomina4 = "(?" + weatherPhenomenaJoined + ")?"; + static const QString regexp = "^(" + qualifier_intensity + qualifier_descriptor + weather_phenomina1 + weather_phenomina2 + weather_phenomina3 + weather_phenomina4 + ") "; + return regexp; + } + + bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override + { + QString intensityAsString = match.captured("intensity"); + CPresentWeather::Intensity itensity = CPresentWeather::Moderate; + if (!intensityAsString.isEmpty()) { itensity = getIntensityHash().value(intensityAsString); } + + QString descriptorAsString = match.captured("descriptor"); + CPresentWeather::Descriptor descriptor = CPresentWeather::None; + if (!descriptorAsString.isEmpty()) { descriptor = getDescriptorHash().value(descriptorAsString); } + + int weatherPhenomena = 0; + QString wp1AsString = match.captured("wp1"); + if (!wp1AsString.isEmpty()) { weatherPhenomena |= getWeatherPhenomenaHash().value(wp1AsString); } + + QString wp2AsString = match.captured("wp2"); + if (!wp2AsString.isEmpty()) { weatherPhenomena |= getWeatherPhenomenaHash().value(wp2AsString); } + + CPresentWeather presentWeather(itensity, descriptor, weatherPhenomena); + metar.addPresentWeather(presentWeather); + return true; + } + + virtual bool isMandatory() const override { return false; } + virtual QString getDecoderType() const override { return "PresentWeather"; } + }; + + class CMetarDecoderCloud : public IMetarDecoderPart + { + protected: + const QStringList &getClearSkyTokens() const + { + static const QStringList list = + { + "SKC", + "NSC", + "CLR", + "NCD" + }; + return list; + } + + const QHash &getCoverage() const + { + static const QHash hash = + { + { "///", CCloudLayer::None }, + { "FEW", CCloudLayer::Few }, + { "SCT", CCloudLayer::Scattered }, + { "BKN", CCloudLayer::Broken }, + { "OVC", CCloudLayer::Overcast } + }; + return hash; + } + + virtual bool isRepeatable() const { return true; } + + QString getRegExp() const override + { + // Clear sky + static const QString clearSky = QString("(?") + getClearSkyTokens().join('|') + QString(")"); + // Cloud coverage. + static const QString coverage = QString("(?") + QStringList(getCoverage().keys()).join('|') + QString(")"); + // Cloud ceiling + static const QString ceiling = QStringLiteral("(?\\d{3}|///)"); + // CB (Cumulonimbus) or TCU (Towering Cumulus) are appended to the cloud group without a space + static const QString extra = QStringLiteral("(?CB|TCU|///)?"); + // Add space at the end + static const QString regexp = QString("^(") + clearSky + '|' + coverage + ceiling + extra + QString(") "); + return regexp; + } + + bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override + { + QString noClouds = match.captured("clear_sky"); + if (!noClouds.isEmpty()) + { + metar.removeAllClouds(); + return true; + } + + QString coverageAsString = match.captured("coverage"); + QString ceilingAsString = match.captured("ceiling"); + Q_ASSERT(!coverageAsString.isEmpty() && !ceilingAsString.isEmpty()); + Q_ASSERT(getCoverage().contains(coverageAsString)); + if (ceilingAsString == "///") return true; + + bool ok = false; + int ceiling = ceilingAsString.toInt(&ok); + // Factor 100 + ceiling *= 100; + if (!ok) return false; + + CCloudLayer cloudLayer(CAltitude(ceiling, CAltitude::AboveGround, CLengthUnit::ft()), getCoverage().value(coverageAsString)); + metar.addCloudLayer(cloudLayer); + QString cb_tcu = match.captured("cb_tcu"); + if (!cb_tcu.isEmpty()) { } + return true; + } + + virtual bool isMandatory() const override { return false; } + virtual QString getDecoderType() const override { return "Cloud"; } + }; + + class CMetarDecoderVerticalVisibility : public IMetarDecoderPart + { + protected: + QString getRegExp() const override + { + // Vertical visibility + static const QString verticalVisibility = QStringLiteral("VV(?\\d{3}|///)"); + + static const QString regexp = "^" + verticalVisibility + " "; + return regexp; + } + + bool validateAndSet(const QRegularExpressionMatch &/* match */, CMetar &/* metar */) const override + { + // todo + return true; + } + + virtual bool isMandatory() const override { return false; } + virtual QString getDecoderType() const override { return "VerticalVisibility"; } + }; + + class CMetarDecoderTemperature : public IMetarDecoderPart + { + protected: + QString getRegExp() const override + { + // Tmperature + static const QString temperature = QStringLiteral("(?M?\\d{2}|//)"); + // Separator + static const QString separator = "/"; + // Dew point + static const QString dewPoint = QStringLiteral("(?M?\\d{2}|//)"); + // Add space at the end + static const QString regexp = "^" + temperature + separator + dewPoint + " ?"; + return regexp; + } + + bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override + { + QString temperatureAsString = match.captured("temperature"); + if (temperatureAsString.isEmpty()) return false; + QString dewPointAsString = match.captured("dew_point"); + if (dewPointAsString.isEmpty()) return false; + + if (temperatureAsString == "//" || dewPointAsString == "//") return true; + + bool temperatureNegative = false; + if (temperatureAsString.startsWith('M')) + { + temperatureNegative = true; + temperatureAsString.remove('M'); + } + + bool dewPointNegative = false; + if (dewPointAsString.startsWith('M')) + { + dewPointNegative = true; + dewPointAsString.remove('M'); + } + + int temperature = temperatureAsString.toInt(); + if (temperatureNegative) { temperature *= -1; } + metar.setTemperature(CTemperature(temperature, CTemperatureUnit::C())); + + int dewPoint = dewPointAsString.toInt(); + if (dewPointNegative) { dewPoint *= -1; } + metar.setDewPoint(CTemperature(dewPoint, CTemperatureUnit::C())); + + return true; + } + + virtual bool isMandatory() const override { return false; } + virtual QString getDecoderType() const override { return "Temperature"; } + }; + + class CMetarDecoderPressure : public IMetarDecoderPart + { + protected: + + const QHash &getPressureUnits() const + { + static const QHash hash = + { + { "Q", CPressureUnit::hPa() }, + { "A", CPressureUnit::inHg() } + }; + return hash; + } + + QString getRegExp() const override + { + // Q => QNH comes in hPa + // A => QNH comes in inches of Mercury + static const QString unit = QStringLiteral("((?Q|A)"); + // Pressure + static const QString pressure = QStringLiteral("(?\\d{4}|////) ?)"); + // QFE + static const QString qfe = QStringLiteral("(QFE (?\\d+).\\d"); + + static const QString regexp = "^" + unit + pressure + "|" + qfe + " ?)"; + return regexp; + } + + bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override + { + QString unitAsString = match.captured("unit"); + QString pressureAsString = match.captured("pressure"); + QString qfeAsString = match.captured("qfe"); + if ((unitAsString.isEmpty() || pressureAsString.isEmpty()) && qfeAsString.isEmpty()) return false; + + // In case no value is defined + if (pressureAsString == "////") return true; + + if (!unitAsString.isEmpty() && !pressureAsString.isEmpty()) + { + Q_ASSERT(getPressureUnits().contains(unitAsString)); + bool ok = false; + double pressure = pressureAsString.toDouble(&ok); + if (!ok) return false; + CPressureUnit pressureUnit = getPressureUnits().value(unitAsString); + if (pressureUnit == CPressureUnit::inHg()) pressure /= 100; + metar.setAltimeter(CPressure(pressure, pressureUnit)); + return true; + } + + if (!qfeAsString.isEmpty()) + { + // todo QFE + return true; + } + return false; + } + + virtual bool isMandatory() const override { return false; } + virtual QString getDecoderType() const override { return "Pressure"; } + }; + + class CMetarDecoderRecentWeather : public IMetarDecoderPart + { + protected: + QString getRegExp() const override + { + // Qualifier intensity. (-) light (no sign) moderate (+) heavy or VC + static const QString qualifier_intensity("(?[-+]|VC)?"); + // Descriptor, if any + static const QString qualifier_descriptor = "(?" + m_descriptor.join('|') + ")?"; + static const QString weather_phenomina1 = "(?" + m_phenomina.join('|') + ")?"; + static const QString weather_phenomina2 = "(?" + m_phenomina.join('|') + ")?"; + static const QString weather_phenomina3 = "(?" + m_phenomina.join('|') + ")?"; + static const QString weather_phenomina4 = "(?" + m_phenomina.join('|') + ")?"; + + static const QString regexp = "^RE" + qualifier_intensity + qualifier_descriptor + weather_phenomina1 + weather_phenomina2 + weather_phenomina3 + weather_phenomina4 + " "; + return regexp; + } + + bool validateAndSet(const QRegularExpressionMatch &/** match **/, CMetar &/** metar **/) const override + { + // Ignore for now + return true; + } + + virtual bool isMandatory() const override { return false; } + virtual QString getDecoderType() const override { return "RecentWeather"; } + + private: + const QStringList m_descriptor = QStringList { "MI", "BC", "PR", "DR", "BL", "SH", "TS", "FZ" }; + const QStringList m_phenomina = QStringList { "DZ", "RA", "SN", "SG", "IC", "PE", "GR", "GS", + "BR", "FG", "FU", "VA", "IC", "DU", "SA", "HZ", + "PY", "PO", "SQ", "FC", "SS", "DS" + }; + }; + + class CMetarDecoderWindShear : public IMetarDecoderPart + { + protected: + QString getRegExp() const override + { + + // Wind shear on all runways + static const QString wsAllRwy = QStringLiteral("WS ALL RWY"); + // RWY designator + static const QString runway = QStringLiteral("RW?Y?(?\\d{2}[LCR]*)"); + + static const QString regexp = "^WS (" + wsAllRwy + "|" + runway + ") "; + return regexp; + } + + bool validateAndSet(const QRegularExpressionMatch &match, CMetar &metar) const override + { + QString runwayAsString = match.captured("runway"); + if (!runwayAsString.isEmpty()) + { + // Ignore for now until we make use of it. + Q_UNUSED(runwayAsString); + Q_UNUSED(metar); + } + + return true; + } + + virtual bool isMandatory() const override { return false; } + virtual QString getDecoderType() const override { return "WindShear"; } + }; + + + CMetarDecoder::CMetarDecoder() + { + allocateDecoders(); + } + + CMetarDecoder::~CMetarDecoder() + { + } + + CMetar CMetarDecoder::decode(const QString &metarString) const + { + CMetar metar; + QString metarStringCopy = metarString.simplified(); + + for (const auto &decoder : m_decoders) + { + if (!decoder->parse(metarStringCopy, metar)) + { + CLogMessage(this).info("Invalid metar: %1") << metarString; + return CMetar(); + } + } + + metar.setMessage(metarString); + return metar; + } + + void CMetarDecoder::allocateDecoders() + { + m_decoders.clear(); + m_decoders.push_back(make_unique()); + m_decoders.push_back(make_unique()); + m_decoders.push_back(make_unique()); + m_decoders.push_back(make_unique()); + m_decoders.push_back(make_unique()); + m_decoders.push_back(make_unique()); + m_decoders.push_back(make_unique()); + m_decoders.push_back(make_unique()); + m_decoders.push_back(make_unique()); + m_decoders.push_back(make_unique()); + m_decoders.push_back(make_unique()); + m_decoders.push_back(make_unique()); + m_decoders.push_back(make_unique()); + m_decoders.push_back(make_unique()); + m_decoders.push_back(make_unique()); + } + + } // namespace +} // namespace diff --git a/src/blackmisc/weather/metardecoder.h b/src/blackmisc/weather/metardecoder.h new file mode 100644 index 000000000..35510b8d6 --- /dev/null +++ b/src/blackmisc/weather/metardecoder.h @@ -0,0 +1,53 @@ +/* Copyright (C) 2015 + * swift project Community / Contributors + * + * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level + * directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project, + * including this file, may be copied, modified, propagated, or distributed except according to the terms + * contained in the LICENSE file. + */ + +//! \file + +#ifndef BLACKMISC_WEATHER_METARDECODER_H +#define BLACKMISC_WEATHER_METARDECODER_H + +#include "blackmisc/blackmiscexport.h" +#include "blackmisc/valueobject.h" +#include "blackmisc/weather/metar.h" +#include +#include +#include +#include + +namespace BlackMisc +{ + namespace Weather + { + + class IMetarDecoderPart; + + //! Metar Decoder + class BLACKMISC_EXPORT CMetarDecoder : public QObject + { + public: + + //! Default constructor + CMetarDecoder(); + + //! Default destructor + ~CMetarDecoder(); + + //! Decode metar + CMetar decode(const QString &metarString) const; + + private: + void allocateDecoders(); + std::vector> m_decoders; + + }; + + } // namespace +} // namespace + +#endif // guard diff --git a/tests/blackmisc/testblackmiscmain.cpp b/tests/blackmisc/testblackmiscmain.cpp index 6014a6ee8..a1440588c 100644 --- a/tests/blackmisc/testblackmiscmain.cpp +++ b/tests/blackmisc/testblackmiscmain.cpp @@ -16,6 +16,7 @@ #include "testhardware.h" #include "testvaluecache.h" #include "testblackmiscmain.h" +#include "testweather.h" namespace BlackMiscTest { @@ -34,6 +35,7 @@ namespace BlackMiscTest CTestHardware hardwareTests; CTestIdentifier identifierTests; CTestValueCache valueCacheTests; + CTestWeather weatherTests; status |= QTest::qExec(&pqBaseTests, argc, argv); status |= QTest::qExec(&avBaseTests, argc, argv); status |= QTest::qExec(&geoTests, argc, argv); @@ -42,6 +44,7 @@ namespace BlackMiscTest status |= QTest::qExec(&variantAndMap, argc, argv); status |= QTest::qExec(&hardwareTests, argc, argv); status |= QTest::qExec(&valueCacheTests, argc, argv); + status |= QTest::qExec(&weatherTests, argc, argv); } return status; } diff --git a/tests/blackmisc/testweather.cpp b/tests/blackmisc/testweather.cpp new file mode 100644 index 000000000..c12be9937 --- /dev/null +++ b/tests/blackmisc/testweather.cpp @@ -0,0 +1,83 @@ +/* Copyright (C) 2013 + * swift Project Community / Contributors + * + * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level + * directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project, + * including this file, may be copied, modified, propagated, or distributed except according to the terms + * contained in the LICENSE file. + */ + +#include "testweather.h" +#include "blackmisc/weather/metardecoder.h" + +using namespace BlackMisc::Weather; +using namespace BlackMisc::Aviation; +using namespace BlackMisc::PhysicalQuantities; + +namespace BlackMiscTest +{ + + /* + * Constructor + */ + CTestWeather::CTestWeather(QObject *parent): QObject(parent) + { + // void + } + + void CTestWeather::metarDecoder() + { + CMetarDecoder metarDecoder; + CMetar metar = metarDecoder.decode("KLBB 241753Z 20009KT 10SM -SHRA FEW045 SCT220 SCT300 28/17 A3022"); + QVERIFY2(metar.getAirportIcaoCode() == CAirportIcaoCode("KLBB"), "Failed to parse airport code"); + QVERIFY2(metar.getDay() == 24, "Failed to parse day of report"); + QVERIFY2(metar.getTime() == CTime(17, 53, 0), "Failed to parse time of report"); + QVERIFY2(metar.getWindLayer().getDirection() == CAngle(200, CAngleUnit::deg()), "Failed to parse wind direction"); + QVERIFY2(metar.getWindLayer().getSpeed() == CSpeed(9, CSpeedUnit::kts()), "Failed to parse wind speed"); + QVERIFY2(metar.getWindLayer().getGustSpeed() == CSpeed(0, CSpeedUnit::kts()), "Wind gust speed should be null"); + QVERIFY2(metar.getVisibility() == CLength(10, CLengthUnit::SM()), "Failed to parse visibility"); + QVERIFY2(metar.getTemperature() == CTemperature(28, CTemperatureUnit::C()), "Failed to parse temperature"); + QVERIFY2(metar.getDewPoint() == CTemperature(17, CTemperatureUnit::C()), "Failed to parse dew point"); + QVERIFY2(metar.getAltimeter() == CPressure(30.22, CPressureUnit::inHg()), "Failed to parse altimeter"); + + auto presentWeatherList = metar.getPresentWeather(); + QVERIFY2(presentWeatherList.size() == 1, "Present weather has an incorrect size"); + auto presentWeather = presentWeatherList.frontOrDefault(); + QVERIFY2(presentWeather.getIntensity() == CPresentWeather::Light, "Itensity should be light"); + QVERIFY2(presentWeather.getDescriptor() == CPresentWeather::Showers, "Descriptor should be showers"); + QVERIFY2(presentWeather.getWeatherPhenomena() & CPresentWeather::Rain, "Weather phenomina 'rain' should be set"); + QVERIFY2((presentWeather.getWeatherPhenomena() & CPresentWeather::Snow) == 0, "Weather phenomina 'Snow' should NOT be set"); + + auto cloudLayers = metar.getCloudLayers(); + QVERIFY2(cloudLayers.containsCeiling(CAltitude(4500, CAltitude::AboveGround, CLengthUnit::ft())), "Cloud layer 4500 ft missing"); + QVERIFY2(cloudLayers.containsCeiling(CAltitude(22000, CAltitude::AboveGround, CLengthUnit::ft())), "Cloud layer 22000 ft missing"); + QVERIFY2(cloudLayers.containsCeiling(CAltitude(30000, CAltitude::AboveGround, CLengthUnit::ft())), "Cloud layer 30000 ft missing"); + QVERIFY2(cloudLayers.findByCeiling(CAltitude(4500, CAltitude::AboveGround, CLengthUnit::ft())).getCoverage() == CCloudLayer::Few, "Failed to parse cloud layer in 4500 ft"); + QVERIFY2(cloudLayers.findByCeiling(CAltitude(22000, CAltitude::AboveGround, CLengthUnit::ft())).getCoverage() == CCloudLayer::Scattered, "Failed to parse cloud layer in 22000 ft"); + QVERIFY2(cloudLayers.findByCeiling(CAltitude(30000, CAltitude::AboveGround, CLengthUnit::ft())).getCoverage() == CCloudLayer::Scattered, "Failed to parse cloud layer in 30000 ft"); + + CMetar metar2 = metarDecoder.decode("EDDM 241753Z 20009G11KT 9000NDV FEW045 SCT220 SCT300 ///// Q1013"); + QVERIFY2(metar2.getAirportIcaoCode() == CAirportIcaoCode("EDDM"), "Failed to parse airport code"); + QVERIFY2(metar2.getDay() == 24, "Failed to parse day of report"); + QVERIFY2(metar2.getTime() == CTime(17, 53, 0), "Failed to parse time of report"); + QVERIFY2(metar2.getWindLayer().getDirection() == CAngle(200, CAngleUnit::deg()), "Failed to parse wind direction"); + QVERIFY2(metar2.getWindLayer().getSpeed() == CSpeed(9, CSpeedUnit::kts()), "Failed to parse wind speed"); + QVERIFY2(metar2.getWindLayer().getGustSpeed() == CSpeed(11, CSpeedUnit::kts()), "Wind gust speed should be null"); + QVERIFY2(metar2.getVisibility() == CLength(9000, CLengthUnit::m()), "Failed to parse visibility"); + QVERIFY2(metar2.getTemperature() == CTemperature(0, CTemperatureUnit::C()), "Failed to parse temperature"); + QVERIFY2(metar2.getDewPoint() == CTemperature(0, CTemperatureUnit::C()), "Failed to parse dew point"); + QVERIFY2(metar2.getAltimeter() == CPressure(1013, CPressureUnit::hPa()), "Failed to parse altimeter"); + + auto presentWeatherList2 = metar2.getPresentWeather(); + QVERIFY2(presentWeatherList2.size() == 0, "Present weather has an incorrect size"); + + auto cloudLayers2 = metar2.getCloudLayers(); + QVERIFY2(cloudLayers2.containsCeiling(CAltitude(4500, CAltitude::AboveGround, CLengthUnit::ft())), "Cloud layer 4500 ft missing"); + QVERIFY2(cloudLayers2.containsCeiling(CAltitude(22000, CAltitude::AboveGround, CLengthUnit::ft())), "Cloud layer 22000 ft missing"); + QVERIFY2(cloudLayers2.containsCeiling(CAltitude(30000, CAltitude::AboveGround, CLengthUnit::ft())), "Cloud layer 30000 ft missing"); + QVERIFY2(cloudLayers2.findByCeiling(CAltitude(4500, CAltitude::AboveGround, CLengthUnit::ft())).getCoverage() == CCloudLayer::Few, "Failed to parse cloud layer in 4500 ft"); + QVERIFY2(cloudLayers2.findByCeiling(CAltitude(22000, CAltitude::AboveGround, CLengthUnit::ft())).getCoverage() == CCloudLayer::Scattered, "Failed to parse cloud layer in 22000 ft"); + QVERIFY2(cloudLayers2.findByCeiling(CAltitude(30000, CAltitude::AboveGround, CLengthUnit::ft())).getCoverage() == CCloudLayer::Scattered, "Failed to parse cloud layer in 30000 ft"); + } + +} // namespace diff --git a/tests/blackmisc/testweather.h b/tests/blackmisc/testweather.h new file mode 100644 index 000000000..273a783a9 --- /dev/null +++ b/tests/blackmisc/testweather.h @@ -0,0 +1,36 @@ +/* Copyright (C) 2015 + * swift Project Community / Contributors + * + * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level + * directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project, + * including this file, may be copied, modified, propagated, or distributed except according to the terms + * contained in the LICENSE file. + */ + +//! \file + +#ifndef BLACKMISCTEST_TESTWEATHER_H +#define BLACKMISCTEST_TESTWEATHER_H + +#include + +namespace BlackMiscTest +{ + + //! Aviation classes basic tests + class CTestWeather : public QObject + { + Q_OBJECT + + public: + //! Standard test case constructor + explicit CTestWeather(QObject *parent = nullptr); + + private slots: + //! Testing METAR decoder + void metarDecoder(); + }; + +} // namespace + +#endif // guard