refs #448 metar decoder and unit tests

This commit is contained in:
Roland Winklmeier
2015-06-29 11:33:29 +00:00
committed by Mathew Sutcliffe
parent a94ea5618f
commit d9ab612154
5 changed files with 1034 additions and 0 deletions

View File

@@ -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 <QRegularExpression>
#include <QDebug>
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("^(?<reporttype>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<QString, CMetar::ReportType> &getReportTypeHash() const
{
static const QHash<QString, CMetar::ReportType> hash =
{
{ "METAR", CMetar::METAR },
{ "SPECI", CMetar::SPECI }
};
return hash;
}
};
class CMetarDecoderAirport : public IMetarDecoderPart
{
protected:
QString getRegExp() const override { return QStringLiteral("^(?<airport>\\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("^(?<day>\\d{2})(?<hour>\\d{2})(?<minute>\\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<QString, CSpeedUnit> &getWindUnitHash() const
{
static const QHash<QString, CSpeedUnit> 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("(?<direction>\\d{3}|VRB|/{3})");
// Wind speed in two digits (or three digits if required)
static const QString speed = QStringLiteral("(?<speed>\\d{2,3}|/{2})");
// Optional: Gust in two digits (or three digits if required)
static const QString gustSpeed = QStringLiteral("(G(?<gustSpeed>\\d{2,3}))?");
// Unit
static const QString unit = QStringLiteral("(?<unit>") + 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
{
// <from>V in degrees
static const QString directionFrom("(?<direction_from>\\d{3})V");
// <to> in degrees
static const QString directionTo("(?<direction_to>\\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<QString, QString> &getCardinalDirections() const
{
static const QHash<QString, QString> 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>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("(?<visibility>\\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("(?<distance>\\d{0,2}) ?M?((?<numerator>\\d)/(?<denominator>\\d))?(?<unit>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(?<runway>\\d{2}[LCR]*)");
// Visibility
static const QString visibility = QStringLiteral("/[PM]?(?<rwy_visibility>\\d{4})");
// Variability
static const QString variability = QStringLiteral("V?(?<variability>\\d{4})?");
// Unit
static const QString unit = QStringLiteral("(?<unit>FT)?");
// Trend
static const QString trend = QStringLiteral("/?(?<trend>[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<QString, CPresentWeather::Intensity> &getIntensityHash() const
{
static const QHash<QString, CPresentWeather::Intensity> hash =
{
{ "-", CPresentWeather::Light },
{ "+", CPresentWeather::Heavy },
{ "VC", CPresentWeather::InVincinity }
};
return hash;
}
const QHash<QString, CPresentWeather::Descriptor> &getDescriptorHash() const
{
static const QHash<QString, CPresentWeather::Descriptor> 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<QString, CPresentWeather::WeatherPhenomenon> &getWeatherPhenomenaHash() const
{
static const QHash<QString, CPresentWeather::WeatherPhenomenon> 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("(?<intensity>[-+]|VC)?");
// Descriptor, if any
static const QString qualifier_descriptor = "(?<descriptor>" + QStringList(getDescriptorHash().keys()).join('|') + ")?";
static const QString weatherPhenomenaJoined = QStringList(getWeatherPhenomenaHash().keys()).join('|');
static const QString weather_phenomina1 = "(?<wp1>" + weatherPhenomenaJoined + ")?";
static const QString weather_phenomina2 = "(?<wp2>" + weatherPhenomenaJoined + ")?";
static const QString weather_phenomina3 = "(?<wp3>" + weatherPhenomenaJoined + ")?";
static const QString weather_phenomina4 = "(?<wp4>" + 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<QString, CCloudLayer::Coverage> &getCoverage() const
{
static const QHash<QString, CCloudLayer::Coverage> 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("(?<clear_sky>") + getClearSkyTokens().join('|') + QString(")");
// Cloud coverage.
static const QString coverage = QString("(?<coverage>") + QStringList(getCoverage().keys()).join('|') + QString(")");
// Cloud ceiling
static const QString ceiling = QStringLiteral("(?<ceiling>\\d{3}|///)");
// CB (Cumulonimbus) or TCU (Towering Cumulus) are appended to the cloud group without a space
static const QString extra = QStringLiteral("(?<cb_tcu>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(?<vertical_visibility>\\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("(?<temperature>M?\\d{2}|//)");
// Separator
static const QString separator = "/";
// Dew point
static const QString dewPoint = QStringLiteral("(?<dew_point>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<QString, CPressureUnit> &getPressureUnits() const
{
static const QHash<QString, CPressureUnit> 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("((?<unit>Q|A)");
// Pressure
static const QString pressure = QStringLiteral("(?<pressure>\\d{4}|////) ?)");
// QFE
static const QString qfe = QStringLiteral("(QFE (?<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("(?<intensity>[-+]|VC)?");
// Descriptor, if any
static const QString qualifier_descriptor = "(?<descriptor>" + m_descriptor.join('|') + ")?";
static const QString weather_phenomina1 = "(?<wp1>" + m_phenomina.join('|') + ")?";
static const QString weather_phenomina2 = "(?<wp2>" + m_phenomina.join('|') + ")?";
static const QString weather_phenomina3 = "(?<wp3>" + m_phenomina.join('|') + ")?";
static const QString weather_phenomina4 = "(?<wp4>" + 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?(?<runway>\\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<CMetarDecoderReportType>());
m_decoders.push_back(make_unique<CMetarDecoderAirport>());
m_decoders.push_back(make_unique<CMetarDecoderDayTime>());
m_decoders.push_back(make_unique<CMetarDecoderStatus>());
m_decoders.push_back(make_unique<CMetarDecoderWind>());
m_decoders.push_back(make_unique<CMetarDecoderVariationsWindDirection>());
m_decoders.push_back(make_unique<CMetarDecoderVisibility>());
m_decoders.push_back(make_unique<CMetarDecoderRunwayVisualRange>());
m_decoders.push_back(make_unique<CMetarDecoderPresentWeather>());
m_decoders.push_back(make_unique<CMetarDecoderCloud>());
m_decoders.push_back(make_unique<CMetarDecoderVerticalVisibility>());
m_decoders.push_back(make_unique<CMetarDecoderTemperature>());
m_decoders.push_back(make_unique<CMetarDecoderPressure>());
m_decoders.push_back(make_unique<CMetarDecoderRecentWeather>());
m_decoders.push_back(make_unique<CMetarDecoderWindShear>());
}
} // namespace
} // namespace

View File

@@ -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 <QString>
#include <QObject>
#include <QVector>
#include <memory>
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<std::unique_ptr<IMetarDecoderPart>> m_decoders;
};
} // namespace
} // namespace
#endif // guard

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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 <QtTest/QtTest>
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