mirror of
https://github.com/swift-project/pilotclient.git
synced 2026-04-02 15:15:50 +08:00
412 lines
15 KiB
C++
412 lines
15 KiB
C++
/* 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 "blackmisc/aviation/altitude.h"
|
|
#include "blackmisc/pq/measurementunit.h"
|
|
#include "blackmisc/pq/constants.h"
|
|
#include "blackmisc/pq/pqstring.h"
|
|
#include "blackmisc/math/mathutils.h"
|
|
#include "blackmisc/comparefunctions.h"
|
|
#include "blackmisc/iconlist.h"
|
|
#include "blackmisc/icons.h"
|
|
#include "altitude.h"
|
|
|
|
#include <Qt>
|
|
#include <QtGlobal>
|
|
#include <QStringBuilder>
|
|
|
|
using namespace BlackMisc::PhysicalQuantities;
|
|
using namespace BlackMisc::Math;
|
|
|
|
namespace BlackMisc
|
|
{
|
|
namespace Aviation
|
|
{
|
|
void CAltitude::registerMetadata()
|
|
{
|
|
Mixin::MetaType<CAltitude>::registerMetadata();
|
|
qRegisterMetaType<CAltitude::ReferenceDatum>();
|
|
qRegisterMetaType<CAltitude::AltitudeType>();
|
|
}
|
|
|
|
CAltitude::CAltitude(const QString &altitudeAsString, CPqString::SeparatorMode mode) : CLength(0, CLengthUnit::m()), m_datum(MeanSeaLevel)
|
|
{
|
|
this->parseFromString(altitudeAsString, mode);
|
|
}
|
|
|
|
CAltitude CAltitude::withOffset(const CLength &offset) const
|
|
{
|
|
if (this->isNull()) { return CAltitude::null(); }
|
|
CAltitude copy(*this);
|
|
if (!offset.isNull() && !offset.isZeroEpsilonConsidered())
|
|
{
|
|
copy += offset.switchedUnit(this->getUnit());
|
|
}
|
|
return copy;
|
|
}
|
|
|
|
void CAltitude::addOffset(const CLength &offset)
|
|
{
|
|
*this = this->withOffset(offset);
|
|
}
|
|
|
|
CAltitude &CAltitude::switchUnit(const CLengthUnit &newUnit)
|
|
{
|
|
if (newUnit.isNull() || this->getUnit().isNull() || this->getUnit() == newUnit) { return *this; }
|
|
CLength::switchUnit(newUnit);
|
|
return *this;
|
|
}
|
|
|
|
CAltitude CAltitude::switchedUnit(const CLengthUnit &newUnit) const
|
|
{
|
|
if (newUnit.isNull() || this->getUnit().isNull() || this->getUnit() == newUnit) { return *this; }
|
|
CAltitude copy(*this);
|
|
copy.switchUnit(newUnit);
|
|
return copy;
|
|
}
|
|
|
|
QString CAltitude::convertToQString(bool i18n) const
|
|
{
|
|
static const QString n("null");
|
|
if (this->isNull()) { return n; }
|
|
|
|
if (m_datum == FlightLevel)
|
|
{
|
|
static const QString fls("FL%1");
|
|
const int fl = qRound(this->CLength::value(CLengthUnit::ft()) / 100.0);
|
|
return fls.arg(fl);
|
|
}
|
|
else
|
|
{
|
|
return this->CLength::valueRoundedWithUnit(1, i18n) %
|
|
(this->isMeanSeaLevel() ? QStringLiteral(" MSL") : QStringLiteral(" AGL"));
|
|
}
|
|
}
|
|
|
|
bool CAltitude::toFlightLevel()
|
|
{
|
|
if (m_datum != MeanSeaLevel && m_datum != FlightLevel) { return false; }
|
|
m_datum = FlightLevel;
|
|
return true;
|
|
}
|
|
|
|
bool CAltitude::toMeanSeaLevel()
|
|
{
|
|
if (m_datum != MeanSeaLevel && m_datum != FlightLevel) { return false; }
|
|
m_datum = MeanSeaLevel;
|
|
return true;
|
|
}
|
|
|
|
void CAltitude::convertToPressureAltitude(const CPressure &seaLevelPressure)
|
|
{
|
|
if (m_altitudeType == PressureAltitude) { return; }
|
|
if (this->isNull()) { return; }
|
|
const CPressure deltaPressure = standardISASeaLevelPressure() - seaLevelPressure;
|
|
const double deltaPressureV = deltaPressure.value(CPressureUnit::mbar());
|
|
const double deltaAltitudeV = deltaPressureV * 30.0; // 30.0 ft per mbar
|
|
CLength deltaAltitude(deltaAltitudeV, CLengthUnit::ft());
|
|
*this += deltaAltitude;
|
|
m_altitudeType = PressureAltitude;
|
|
}
|
|
|
|
CAltitude CAltitude::toPressureAltitude(const CPressure &seaLevelPressure) const
|
|
{
|
|
if (seaLevelPressure.isNull()) { return CAltitude::null(); }
|
|
if (this->isNull()) { return CAltitude::null(); }
|
|
CAltitude other(*this);
|
|
other.convertToPressureAltitude(seaLevelPressure);
|
|
return other;
|
|
}
|
|
|
|
void CAltitude::parseFromString(const QString &value)
|
|
{
|
|
this->parseFromString(value, CPqString::SeparatorsBestGuess);
|
|
}
|
|
|
|
void CAltitude::parseFromString(const QString &value, CPqString::SeparatorMode mode)
|
|
{
|
|
QString v = value.trimmed();
|
|
|
|
// special case FL
|
|
if (v.contains("FL", Qt::CaseInsensitive) || v.startsWith("F"))
|
|
{
|
|
v = char09OnlyString(value);
|
|
bool ok = false;
|
|
const int dv = v.toInt(&ok) * 100;
|
|
const CAltitude a(ok ? dv : 0.0, FlightLevel, ok ? CLengthUnit::ft() : nullptr);
|
|
*this = a;
|
|
return;
|
|
}
|
|
|
|
// special case A (altitude
|
|
if (v.contains("ALT", Qt::CaseInsensitive) || v.startsWith("A"))
|
|
{
|
|
v = char09OnlyString(value);
|
|
bool ok = false;
|
|
const int dv = v.toInt(&ok) * 100;
|
|
const CAltitude a(ok ? dv : 0.0, MeanSeaLevel, ok ? CLengthUnit::ft() : nullptr);
|
|
*this = a;
|
|
return;
|
|
}
|
|
|
|
// normal altitude, AGL/MSL
|
|
ReferenceDatum rd = MeanSeaLevel;
|
|
if (v.contains("MSL", Qt::CaseInsensitive))
|
|
{
|
|
v = v.replace("MSL", "", Qt::CaseInsensitive).trimmed();
|
|
rd = MeanSeaLevel;
|
|
}
|
|
else if (v.contains("AGL"))
|
|
{
|
|
v = v.replace("AGL", "", Qt::CaseInsensitive).trimmed();
|
|
rd = AboveGround;
|
|
}
|
|
|
|
const CLength l = CPqString::parse<CLength>(v, mode);
|
|
*this = CAltitude(l, rd);
|
|
}
|
|
|
|
bool CAltitude::parseFromFpAltitudeString(const QString &value, CStatusMessageList *msgs)
|
|
{
|
|
QString v(value.trimmed()); // do not convert case because of units
|
|
if (v.startsWith("VFR", Qt::CaseInsensitive))
|
|
{
|
|
// we set a more or less meaningful value
|
|
*this = CAltitude(5000, MeanSeaLevel, CLengthUnit::ft());
|
|
return true;
|
|
}
|
|
|
|
this->setNull();
|
|
if (v.length() < 3)
|
|
{
|
|
if (msgs) { msgs->push_back(CStatusMessage(this).validationError("Altitude empty or too short")); }
|
|
return false;
|
|
}
|
|
|
|
if (!fpAltitudeRegExp().globalMatch(v).hasNext())
|
|
{
|
|
if (msgs) { msgs->push_back(CStatusMessage(this).validationError("Altitude '%1' needs to match format '%2'") << v << fpAltitudeExamples()); }
|
|
return false;
|
|
}
|
|
|
|
// normalize characters to upper/lower
|
|
// in same step get numeric value only
|
|
bool beforeDigit = true;
|
|
QString numericPart;
|
|
for (int i = 0; i < v.length(); i++)
|
|
{
|
|
const QChar c = v[i];
|
|
if (c.isDigit())
|
|
{
|
|
beforeDigit = false;
|
|
numericPart.push_back(c);
|
|
continue;
|
|
}
|
|
v[i] = beforeDigit ? c.toUpper() : c.toLower();
|
|
}
|
|
|
|
if (numericPart.isEmpty())
|
|
{
|
|
if (msgs) { msgs->push_back(CStatusMessage(this).validationError("Altitude '%1' has no numeric part")); }
|
|
return false;
|
|
}
|
|
|
|
bool ok;
|
|
if (v.startsWith("F", Qt::CaseInsensitive))
|
|
{
|
|
this->setUnit(CLengthUnit::ft());
|
|
this->setValueSameUnit(numericPart.toInt(&ok) * 100);
|
|
m_datum = FlightLevel;
|
|
}
|
|
else if (v.startsWith("S", Qt::CaseInsensitive))
|
|
{
|
|
this->setUnit(CLengthUnit::m());
|
|
this->setValueSameUnit(numericPart.toInt(&ok) * 10);
|
|
m_datum = FlightLevel;
|
|
}
|
|
else if (v.startsWith("A", Qt::CaseInsensitive))
|
|
{
|
|
this->setUnit(CLengthUnit::ft());
|
|
this->setValueSameUnit(numericPart.toInt(&ok) * 100);
|
|
m_datum = MeanSeaLevel;
|
|
}
|
|
else if (v.startsWith("M", Qt::CaseInsensitive))
|
|
{
|
|
this->setUnit(CLengthUnit::m());
|
|
this->setValueSameUnit(numericPart.toInt(&ok) * 10);
|
|
m_datum = MeanSeaLevel;
|
|
}
|
|
else if (v.endsWith(CLengthUnit::m().getSymbol()))
|
|
{
|
|
this->setUnit(CLengthUnit::m());
|
|
this->setValueSameUnit(numericPart.toInt(&ok));
|
|
m_datum = MeanSeaLevel;
|
|
}
|
|
else if (v.endsWith(CLengthUnit::ft().getSymbol()))
|
|
{
|
|
this->setUnit(CLengthUnit::ft());
|
|
this->setValueSameUnit(numericPart.toInt(&ok));
|
|
m_datum = MeanSeaLevel;
|
|
}
|
|
else
|
|
{
|
|
if (msgs) { msgs->push_back(CStatusMessage(this).validationError("Altitude '%1' needs to match format '%2'") << v << fpAltitudeExamples()); }
|
|
return false;
|
|
}
|
|
|
|
// further checks
|
|
const bool valid = this->isValidFpAltitude(msgs);
|
|
if (!valid) { this->setNull(); }
|
|
return valid;
|
|
}
|
|
|
|
bool CAltitude::isValidFpAltitude(CStatusMessageList *msgs) const
|
|
{
|
|
if (this->isNull())
|
|
{
|
|
if (msgs) { msgs->push_back(CStatusMessage(this).validationError("Altitude NULL value")); }
|
|
return false;
|
|
}
|
|
if (!(this->getReferenceDatum() == FlightLevel || this->getReferenceDatum() == MeanSeaLevel))
|
|
{
|
|
if (msgs) { msgs->push_back(CStatusMessage(this).validationError("Altitude, must be FL or MSL")); }
|
|
return false;
|
|
}
|
|
if (!(this->getUnit() == CLengthUnit::m() || this->getUnit() == CLengthUnit::ft()))
|
|
{
|
|
if (msgs) { msgs->push_back(CStatusMessage(this).validationError("Altitude, valid unit must be 'ft' or 'm'")); }
|
|
return false;
|
|
}
|
|
if (this->isNegativeWithEpsilonConsidered())
|
|
{
|
|
if (msgs) { msgs->push_back(CStatusMessage(this).validationError("Altitude must be positive")); }
|
|
return false;
|
|
}
|
|
if (this->isFlightLevel())
|
|
{
|
|
if (!this->isInteger())
|
|
{
|
|
if (msgs) { msgs->push_back(CStatusMessage(this).validationError("FL needs to be positive integer")); }
|
|
return false;
|
|
}
|
|
|
|
if (this->getUnit() == CLengthUnit::ft())
|
|
{
|
|
const int flInt = this->valueInteger() / 100; // internally represented as ft: FL10->1000
|
|
if (flInt < 10 || flInt >= 1000)
|
|
{
|
|
if (msgs) { msgs->push_back(CStatusMessage(this).validationError("FL range is 10-999")); }
|
|
return false;
|
|
}
|
|
if (!CMathUtils::epsilonZero(fmod(flInt, 5)))
|
|
{
|
|
if (msgs) { msgs->push_back(CStatusMessage(this).validationError("FL needs to end with 0 or 5")); }
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
QString CAltitude::asFpICAOAltitudeString() const
|
|
{
|
|
if (this->isNull()) { return QStringLiteral(""); }
|
|
if (this->isFlightLevel())
|
|
{
|
|
if (this->getUnit() == CLengthUnit::m())
|
|
{
|
|
int m = this->valueInteger() / 10;
|
|
return QStringLiteral("S%1").arg(m, 4, 10, QChar('0'));
|
|
}
|
|
int ft = this->valueInteger(CLengthUnit::ft()) / 100;
|
|
return QStringLiteral("FL%1").arg(ft, 3, 10, QChar('0'));
|
|
}
|
|
|
|
if (this->getUnit() == CLengthUnit::m())
|
|
{
|
|
int m = this->valueInteger() / 10;
|
|
return QStringLiteral("M%1").arg(m, 4, 10, QChar('0'));
|
|
}
|
|
int ft = this->valueInteger(CLengthUnit::ft()) / 100;
|
|
return QStringLiteral("A%1").arg(ft, 3, 10, QChar('0'));
|
|
}
|
|
|
|
QString CAltitude::asFpVatsimAltitudeString() const
|
|
{
|
|
if (this->isFlightLevel()) { return this->asFpICAOAltitudeString(); }
|
|
// the M/A formats are not supported by VATSIM, means by other clients
|
|
|
|
// as feed, as none of the other clients
|
|
const CAltitude a = this->roundedToNearest100ft();
|
|
return a.valueRoundedWithUnit(CLengthUnit::ft(), 0);
|
|
}
|
|
|
|
const QRegularExpression &CAltitude::fpAltitudeRegExp()
|
|
{
|
|
thread_local const QRegularExpression re("((FL|F)\\d{2,3})|(S\\d{2,4})|(A\\d{2,3})|(M\\d{2,4})|(\\d{3,5}(ft|m))");
|
|
return re;
|
|
}
|
|
|
|
QString CAltitude::fpAltitudeInfo(const QString &separator)
|
|
{
|
|
// remark use arg %01 to avoid clash with numbers, see https://stackoverflow.com/questions/35517025/qstringarg-with-number-after-placeholder
|
|
static const QString e("FL085, F85 flight level in hecto feets%1S0150 metric level in tens of meters%1A055 altitude in hundreds of feet%012000ft altitude in ft%1M0610 altitude in tens of meters%016100m altitude in meters");
|
|
return e.arg(separator);
|
|
}
|
|
|
|
QString CAltitude::fpAltitudeExamples()
|
|
{
|
|
static const QString e("FL085, F85, S0150, A055, 1200ft, M0610, 6100m");
|
|
return e;
|
|
}
|
|
|
|
CIcon CAltitude::toIcon() const
|
|
{
|
|
return BlackMisc::CIcon::iconByIndex(CIcons::GeoPosition);
|
|
}
|
|
|
|
int CAltitude::compare(const CAltitude &otherAltitude) const
|
|
{
|
|
if (this->getReferenceDatum() != otherAltitude.getReferenceDatum())
|
|
{
|
|
return Compare::compare(static_cast<int>(this->getReferenceDatum()), static_cast<int>(otherAltitude.getReferenceDatum()));
|
|
}
|
|
return CLength::compare(*this, otherAltitude);
|
|
}
|
|
|
|
CAltitude CAltitude::roundedToNearest100ft() const
|
|
{
|
|
// 23453 => 234.53
|
|
CAltitude a = this->switchedUnit(CLengthUnit::ft());
|
|
const double ft = a.value(CLengthUnit::ft()) / 100.0;
|
|
const int ftR = qRound(ft) * 100;
|
|
a.setValueSameUnit(ftR);
|
|
return a;
|
|
}
|
|
|
|
const CAltitude &CAltitude::null()
|
|
{
|
|
static const CAltitude null(0, CAltitude::MeanSeaLevel, CLengthUnit::nullUnit());
|
|
return null;
|
|
}
|
|
|
|
const CLengthUnit &CAltitude::defaultUnit()
|
|
{
|
|
return CLengthUnit::ft();
|
|
}
|
|
|
|
const CPressure &CAltitude::standardISASeaLevelPressure()
|
|
{
|
|
// Average sea-level pressure is 1013.25mbar or 1013.25hPa
|
|
static const CPressure standardPressure(CPhysicalQuantitiesConstants::ISASeaLevelPressure());
|
|
return standardPressure;
|
|
}
|
|
} // namespace
|
|
} // namespace
|