mirror of
https://github.com/swift-project/pilotclient.git
synced 2026-04-17 10:55:32 +08:00
refs #448 metar decoder and unit tests
This commit is contained in:
committed by
Mathew Sutcliffe
parent
a94ea5618f
commit
d9ab612154
859
src/blackmisc/weather/metardecoder.cpp
Normal file
859
src/blackmisc/weather/metardecoder.cpp
Normal 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
|
||||||
53
src/blackmisc/weather/metardecoder.h
Normal file
53
src/blackmisc/weather/metardecoder.h
Normal 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
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "testhardware.h"
|
#include "testhardware.h"
|
||||||
#include "testvaluecache.h"
|
#include "testvaluecache.h"
|
||||||
#include "testblackmiscmain.h"
|
#include "testblackmiscmain.h"
|
||||||
|
#include "testweather.h"
|
||||||
|
|
||||||
namespace BlackMiscTest
|
namespace BlackMiscTest
|
||||||
{
|
{
|
||||||
@@ -34,6 +35,7 @@ namespace BlackMiscTest
|
|||||||
CTestHardware hardwareTests;
|
CTestHardware hardwareTests;
|
||||||
CTestIdentifier identifierTests;
|
CTestIdentifier identifierTests;
|
||||||
CTestValueCache valueCacheTests;
|
CTestValueCache valueCacheTests;
|
||||||
|
CTestWeather weatherTests;
|
||||||
status |= QTest::qExec(&pqBaseTests, argc, argv);
|
status |= QTest::qExec(&pqBaseTests, argc, argv);
|
||||||
status |= QTest::qExec(&avBaseTests, argc, argv);
|
status |= QTest::qExec(&avBaseTests, argc, argv);
|
||||||
status |= QTest::qExec(&geoTests, argc, argv);
|
status |= QTest::qExec(&geoTests, argc, argv);
|
||||||
@@ -42,6 +44,7 @@ namespace BlackMiscTest
|
|||||||
status |= QTest::qExec(&variantAndMap, argc, argv);
|
status |= QTest::qExec(&variantAndMap, argc, argv);
|
||||||
status |= QTest::qExec(&hardwareTests, argc, argv);
|
status |= QTest::qExec(&hardwareTests, argc, argv);
|
||||||
status |= QTest::qExec(&valueCacheTests, argc, argv);
|
status |= QTest::qExec(&valueCacheTests, argc, argv);
|
||||||
|
status |= QTest::qExec(&weatherTests, argc, argv);
|
||||||
}
|
}
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|||||||
83
tests/blackmisc/testweather.cpp
Normal file
83
tests/blackmisc/testweather.cpp
Normal 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
|
||||||
36
tests/blackmisc/testweather.h
Normal file
36
tests/blackmisc/testweather.h
Normal 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
|
||||||
Reference in New Issue
Block a user