Files
pilotclient/src/blackmisc/aviation/livery.cpp
Mat Sutcliffe b80114213d Issue #77 Move mixin classes to separate files
By separating them from unrelated code, their dependents
can use them without depending on unrelated code, which
in turn helps to reduce cyclic dependencies.
2020-08-29 14:16:17 +01:00

504 lines
22 KiB
C++

/* 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. 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/db/datastoreutility.h"
#include "blackmisc/aviation/livery.h"
#include "blackmisc/aviation/logutils.h"
#include "blackmisc/mixin/mixincompare.h"
#include "blackmisc/comparefunctions.h"
#include "blackmisc/logcategory.h"
#include "blackmisc/logcategorylist.h"
#include "blackmisc/propertyindex.h"
#include "blackmisc/statusmessage.h"
#include "blackmisc/stringutils.h"
#include "blackmisc/variant.h"
#include "blackmisc/verify.h"
#include <QCoreApplication>
#include <QJsonValue>
#include <Qt>
#include <QtGlobal>
#include <tuple>
#include <QStringBuilder>
using namespace BlackMisc;
using namespace BlackMisc::Db;
using namespace BlackMisc::PhysicalQuantities;
using namespace BlackMisc::Simulation;
namespace BlackMisc
{
namespace Aviation
{
CLivery::CLivery()
{ }
CLivery::CLivery(const QString &combinedCode, const CAirlineIcaoCode &airline, const QString &description) :
CLivery(-1, combinedCode, airline, description, "", "", false)
{ }
CLivery::CLivery(const QString &combinedCode, const CAirlineIcaoCode &airline, const QString &description, const QString &colorFuselage, const QString &colorTail, bool isMilitary) :
CLivery(-1, combinedCode, airline, description, colorFuselage, colorTail, isMilitary)
{ }
CLivery::CLivery(const QString &combinedCode, const CAirlineIcaoCode &airline, const QString &description, const CRgbColor &colorFuselage, const CRgbColor &colorTail, bool isMilitary) :
CLivery(-1, combinedCode, airline, description, colorFuselage, colorTail, isMilitary)
{ }
CLivery::CLivery(int dbKey, const QString &combinedCode, const CAirlineIcaoCode &airline, const QString &description, const QString &colorFuselage, const QString &colorTail, bool isMilitary) :
IDatastoreObjectWithIntegerKey(dbKey),
m_airline(airline),
m_combinedCode(combinedCode.trimmed().toUpper()), m_description(description.trimmed()),
m_colorFuselage(CRgbColor(colorFuselage)), m_colorTail(CRgbColor(colorTail)),
m_military(isMilitary)
{ }
CLivery::CLivery(int dbKey, const QString &combinedCode, const CAirlineIcaoCode &airline, const QString &description, const CRgbColor &colorFuselage, const CRgbColor &colorTail, bool isMilitary) :
IDatastoreObjectWithIntegerKey(dbKey),
m_airline(airline),
m_combinedCode(combinedCode.trimmed().toUpper()), m_description(description.trimmed()),
m_colorFuselage(colorFuselage), m_colorTail(colorTail),
m_military(isMilitary)
{ }
QString CLivery::getCombinedCodePlusInfo() const
{
return this->getCombinedCode() % (
this->getDescription().isEmpty() ?
QString() :
(u' ' % this->getDescription()));
}
QString CLivery::getCombinedCodePlusId() const
{
return this->getCombinedCode() %
this->getDbKeyAsStringInParentheses(" ");
}
QString CLivery::getCombinedCodePlusInfoAndId() const
{
return this->getCombinedCodePlusInfo() %
this->getDbKeyAsStringInParentheses(" ");
}
bool CLivery::isContainedInSimplifiedAirlineName(const QString &candidate) const
{
return this->getAirlineIcaoCode().isContainedInSimplifiedName(candidate);
}
bool CLivery::setAirlineIcaoCode(const CAirlineIcaoCode &airlineIcao)
{
if (m_airline == airlineIcao) { return false; }
m_airline = airlineIcao;
return true;
}
bool CLivery::setAirlineIcaoCodeDesignator(const QString &airlineIcaoDesignator)
{
if (m_airline.getDesignator() == airlineIcaoDesignator) { return false; }
m_airline.setDesignator(airlineIcaoDesignator);
return true;
}
bool CLivery::hasColorFuselage() const
{
return m_colorFuselage.isValid();
}
bool CLivery::hasColorTail() const
{
return m_colorTail.isValid();
}
bool CLivery::hasValidColors() const
{
return this->hasColorFuselage() && this->hasColorTail();
}
bool CLivery::matchesCombinedCode(const QString &candidate) const
{
if (candidate.isEmpty() || !this->hasCombinedCode()) { return false; }
const QString c(candidate.trimmed().toUpper());
return c == m_combinedCode;
}
bool CLivery::matchesColors(const CRgbColor &fuselage, const CRgbColor &tail) const
{
return this->getColorFuselage() == fuselage && this->getColorTail() == tail;
}
QString CLivery::convertToQString(bool i18n) const
{
static const QString livery("Livery");
static const QString liveryI18n(QCoreApplication::translate("Aviation", "Livery"));
return (i18n ? liveryI18n : livery) %
u" cc: '" % m_combinedCode %
u"' airline: '" % m_airline.toQString(i18n) %
u"' desc.: '" % m_description %
u"' F: '" % m_colorFuselage.hex() %
u"' T: '" % m_colorTail.hex() %
u"' Mil: " % boolToYesNo(this->isMilitary());
// force strings for translation in resource files
(void)QT_TRANSLATE_NOOP("Aviation", "Livery");
}
bool CLivery::hasCompleteData() const
{
return !m_description.isEmpty() && !m_combinedCode.isEmpty();
}
CStatusMessageList CLivery::validate() const
{
static const CLogCategoryList cats(CLogCategoryList(this).withValidation());
CStatusMessageList msg;
if (!hasCombinedCode()) { msg.push_back(CStatusMessage(cats, CStatusMessage::SeverityError, u"Livery: missing livery code")); }
if (!hasColorFuselage()) { msg.push_back(CStatusMessage(cats, CStatusMessage::SeverityWarning, u"Livery: no fuselage color")); }
if (!hasColorTail()) { msg.push_back(CStatusMessage(cats, CStatusMessage::SeverityWarning, u"Livery: no tail color")); }
if (this->isColorLivery())
{
if (!this->getAirlineIcaoCodeDesignator().isEmpty())
{
// color livery, supposed to have empty airline
msg.push_back(CStatusMessage(cats, CStatusMessage::SeverityWarning, u"Livery: color livery, but airline looks odd"));
}
}
else
{
msg.push_back(m_airline.validate());
}
return msg;
}
bool CLivery::hasValidAirlineDesignator() const
{
return m_airline.hasValidDesignator();
}
bool CLivery::hasAirlineName() const
{
return m_airline.hasName();
}
bool CLivery::hasCombinedCode() const
{
Q_ASSERT_X(!m_combinedCode.startsWith("." + standardLiveryMarker()), Q_FUNC_INFO, "illegal combined code");
return !m_combinedCode.isEmpty();
}
bool CLivery::isAirlineLivery() const
{
return (m_airline.hasValidDesignator());
}
bool CLivery::isAirlineOperating() const
{
return this->isAirlineLivery() && this->getAirlineIcaoCode().isOperating();
}
bool CLivery::isAirlineStandardLivery() const
{
if (isColorLivery()) { return false; }
return (m_airline.hasValidDesignator() && m_combinedCode.endsWith(standardLiveryMarker()));
}
bool CLivery::isColorLivery() const
{
return m_combinedCode.startsWith(colorLiveryMarker());
}
double CLivery::getColorDistance(const CLivery &otherLivery) const
{
return this->getColorDistance(otherLivery.getColorFuselage(), otherLivery.getColorTail());
}
double CLivery::getColorDistance(const CRgbColor &fuselage, const CRgbColor &tail) const
{
if (!fuselage.isValid() || !tail.isValid()) { return 1.0; }
if (this->getColorFuselage().isValid() && this->getColorTail().isValid())
{
if (this->matchesColors(fuselage, tail)) { return 0.0; } // avoid rounding
const double xDist = this->getColorFuselage().colorDistance(fuselage);
const double yDist = this->getColorTail().colorDistance(tail);
const double d = xDist * xDist + yDist * yDist;
return d / 2.0; // normalize to 0..1
}
else
{
return 1.0;
}
}
CLivery CLivery::fromDatabaseJson(const QJsonObject &json, const QString &prefix)
{
if (!existsKey(json, prefix))
{
// when using relationship, this can be null
return CLivery();
}
const QString combinedCode(json.value(prefix % u"combinedcode").toString());
if (combinedCode.isEmpty())
{
CLivery liveryStub; // only consists of id, maybe key and timestamp
liveryStub.setKeyVersionTimestampFromDatabaseJson(json, prefix);
return liveryStub;
}
const bool isColorLivery = combinedCode.startsWith(colorLiveryMarker());
const QString description(json.value(prefix % u"description").toString());
const CRgbColor colorFuselage(json.value(prefix % u"colorfuselage").toString());
const CRgbColor colorTail(json.value(prefix % u"colortail").toString());
const bool military = CDatastoreUtility::dbBoolStringToBool(json.value(prefix % u"military").toString());
CAirlineIcaoCode airline;
if (!isColorLivery) { airline = CAirlineIcaoCode::fromDatabaseJson(json, "al_"); }
CLivery livery(combinedCode, airline, description, colorFuselage, colorTail, military);
livery.setKeyVersionTimestampFromDatabaseJson(json, prefix);
// color liveries must have default ICAO, but airline liveries must have DB airline
BLACK_VERIFY_X((livery.isColorLivery() && !livery.getAirlineIcaoCode().hasValidDbKey()) || (livery.isAirlineLivery() && livery.getAirlineIcaoCode().hasValidDbKey()), Q_FUNC_INFO, "inconsistent data");
return livery;
}
CLivery CLivery::fromDatabaseJsonCaching(const QJsonObject &json, AirlineIcaoIdMap &airlineIcaos, const QString &prefix)
{
if (!existsKey(json, prefix))
{
// when using relationship, this can be null
return CLivery();
}
const QString combinedCode(json.value(prefix % u"combinedcode").toString());
if (combinedCode.isEmpty())
{
CLivery liveryStub; // only consists of id, maybe key and timestamp
liveryStub.setKeyVersionTimestampFromDatabaseJson(json, prefix);
return liveryStub;
}
const bool isColorLivery = combinedCode.startsWith(colorLiveryMarker());
const QString description(json.value(prefix % u"description").toString());
const CRgbColor colorFuselage(json.value(prefix % u"colorfuselage").toString());
const CRgbColor colorTail(json.value(prefix % u"colortail").toString());
const bool military = CDatastoreUtility::dbBoolStringToBool(json.value(prefix % u"military").toString());
CAirlineIcaoCode airline;
if (!isColorLivery)
{
static const QString prefixAirline("al_");
const int idAirlineIcao = json.value(prefixAirline % u"id").toInt(-1);
const bool cachedAirlineIcao = idAirlineIcao >= 0 && airlineIcaos.contains(idAirlineIcao);
airline = cachedAirlineIcao ?
airlineIcaos[idAirlineIcao] :
CAirlineIcaoCode::fromDatabaseJson(json, prefixAirline);
if (!cachedAirlineIcao && airline.isLoadedFromDb())
{
airlineIcaos[idAirlineIcao] = airline;
}
}
CLivery livery(combinedCode, airline, description, colorFuselage, colorTail, military);
livery.setKeyVersionTimestampFromDatabaseJson(json, prefix);
// color liveries must have default ICAO, but airline liveries must have DB airline
BLACK_VERIFY_X((livery.isColorLivery() && !livery.getAirlineIcaoCode().hasValidDbKey()) || (livery.isAirlineLivery() && livery.getAirlineIcaoCode().hasValidDbKey()), Q_FUNC_INFO, "inconsistent data");
return livery;
}
bool CLivery::isValidCombinedCode(const QString &candidate)
{
if (candidate.isEmpty()) { return false; }
if (candidate.startsWith(colorLiveryMarker()))
{
return candidate.length() > colorLiveryMarker().length() + 1;
}
else
{
if (candidate.count('.') != 1) { return false; }
return candidate.length() > 2;
}
}
const QString &CLivery::standardLiveryMarker()
{
static const QString s("_STD");
return s;
}
QString CLivery::getStandardCode(const CAirlineIcaoCode &airline)
{
QString code(airline.getVDesignator());
return code.isEmpty() ? "" : code.append('.').append(standardLiveryMarker());
}
const QString &CLivery::colorLiveryMarker()
{
static const QString s("_CC");
return s;
}
const QString &CLivery::tempLiveryCode()
{
static const QString temp("_CC_NOCOLOR");
return temp;
}
CVariant CLivery::propertyByIndex(const BlackMisc::CPropertyIndex &index) const
{
if (index.isMyself()) { return CVariant::from(*this); }
if (IDatastoreObjectWithIntegerKey::canHandleIndex(index)) { return IDatastoreObjectWithIntegerKey::propertyByIndex(index); }
const ColumnIndex i = index.frontCasted<ColumnIndex>();
switch (i)
{
case IndexAirlineIcaoCode: return m_airline.propertyByIndex(index.copyFrontRemoved());
case IndexColorFuselage: return m_colorFuselage.propertyByIndex(index.copyFrontRemoved());;
case IndexColorTail: return m_colorTail.propertyByIndex(index.copyFrontRemoved());
case IndexDescription: return CVariant::fromValue(m_description);
case IndexCombinedCode: return CVariant::fromValue(m_combinedCode);
case IndexIsMilitary: return CVariant::fromValue(m_military);
default: return CValueObject::propertyByIndex(index);
}
}
void CLivery::setPropertyByIndex(const CPropertyIndex &index, const CVariant &variant)
{
if (index.isMyself()) { (*this) = variant.to<CLivery>(); return; }
if (IDatastoreObjectWithIntegerKey::canHandleIndex(index)) { IDatastoreObjectWithIntegerKey::setPropertyByIndex(index, variant); return; }
const ColumnIndex i = index.frontCasted<ColumnIndex>();
switch (i)
{
case IndexDescription: m_description = variant.toQString(false); break;
case IndexAirlineIcaoCode: m_airline.setPropertyByIndex(index.copyFrontRemoved(), variant); break;
case IndexColorFuselage: m_colorFuselage.setPropertyByIndex(index.copyFrontRemoved(), variant); break;
case IndexColorTail: m_colorTail.setPropertyByIndex(index.copyFrontRemoved(), variant); break;
case IndexCombinedCode: this->setCombinedCode(variant.toQString(false)); break;
case IndexIsMilitary: this->setMilitary(variant.toBool()); break;
default: CValueObject::setPropertyByIndex(index, variant); break;
}
}
int CLivery::comparePropertyByIndex(const CPropertyIndex &index, const CLivery &compareValue) const
{
if (index.isMyself()) { return this->getCombinedCode().compare(compareValue.getCombinedCode()); }
if (IDatastoreObjectWithIntegerKey::canHandleIndex(index)) { return IDatastoreObjectWithIntegerKey::comparePropertyByIndex(index, compareValue);}
const ColumnIndex i = index.frontCasted<ColumnIndex>();
switch (i)
{
case IndexDescription: return m_description.compare(compareValue.getDescription(), Qt::CaseInsensitive);
case IndexAirlineIcaoCode: return m_airline.comparePropertyByIndex(index.copyFrontRemoved(), compareValue.getAirlineIcaoCode());
case IndexColorFuselage: return m_colorFuselage.comparePropertyByIndex(index.copyFrontRemoved(), compareValue.getColorFuselage());
case IndexColorTail: return m_colorTail.comparePropertyByIndex(index.copyFrontRemoved(), compareValue.getColorTail());
case IndexCombinedCode: return this->getCombinedCode().compare(compareValue.getCombinedCode());
case IndexIsMilitary: return Compare::compare(this->isMilitary(), compareValue.isMilitary());
default: break;
}
Q_ASSERT_X(false, Q_FUNC_INFO, "No compare function");
return 0;
}
void CLivery::updateMissingParts(const CLivery &otherLivery)
{
if (!this->hasValidDbKey() && otherLivery.hasValidDbKey())
{
// we have no DB data, but the other one has
// so we change roles. We take the DB object as base, and update our parts
CLivery copy(otherLivery);
copy.updateMissingParts(*this);
*this = copy;
return;
}
if (!m_colorFuselage.isValid()) { this->setColorFuselage(otherLivery.getColorFuselage()); }
if (!m_colorTail.isValid()) { this->setColorTail(otherLivery.getColorTail()); }
if (m_combinedCode.isEmpty()) { this->setCombinedCode(otherLivery.getCombinedCode());}
if (m_description.isEmpty()) { this->setDescription(otherLivery.getDescription());}
m_airline.updateMissingParts(otherLivery.getAirlineIcaoCode());
if (!this->hasValidDbKey())
{
this->setDbKey(otherLivery.getDbKey());
this->setUtcTimestamp(otherLivery.getUtcTimestamp());
}
}
QString CLivery::asHtmlSummary(const QString &separator) const
{
return QStringLiteral("%1%2Airline: %3").arg(
this->getCombinedCodePlusInfoAndId(), separator,
this->getAirlineIcaoCode().getDesignator().isEmpty() ? "No airline" : this->getAirlineIcaoCode().getCombinedStringWithKey()
).replace(" ", "&nbsp;");
}
int CLivery::calculateScore(const CLivery &otherLivery, bool preferColorLiveries, CStatusMessageList *log) const
{
if (this->isDbEqual(otherLivery))
{
CLogUtilities::addLogDetailsToList(log, *this, QStringLiteral("Equal DB code: 100"));
return 100;
}
// get a level
static const int sameAirlineIcaoLevel = CAirlineIcaoCode("DLH").calculateScore(CAirlineIcaoCode("DLH"));
Q_ASSERT_X(sameAirlineIcaoLevel == 60, Q_FUNC_INFO, "airline scoring changed");
int score = 0;
const double colorMultiplier = 1.0 - this->getColorDistance(otherLivery);
if (this->isColorLivery() && otherLivery.isColorLivery())
{
// 2 color liveries 25..85
score = 25;
score += 60 * colorMultiplier;
CLogUtilities::addLogDetailsToList(log, *this, QStringLiteral("2 color liveries, color multiplier %1: %2").arg(colorMultiplier).arg(score));
}
else if (this->isAirlineLivery() && otherLivery.isAirlineLivery())
{
// 2 airline liveries 0..85
// 0..50 based on ICAO
// 0..25 based on color distance
// 0..10 based on mil.flag
// same ICAO at least means 30, max 50
score = qRound(0.5 * this->getAirlineIcaoCode().calculateScore(otherLivery.getAirlineIcaoCode(), log));
score += 25 * colorMultiplier;
CLogUtilities::addLogDetailsToList(log, *this, QStringLiteral("2 airline liveries, color multiplier %1: %2").arg(colorMultiplier).arg(score));
if (this->isMilitary() == otherLivery.isMilitary())
{
CLogUtilities::addLogDetailsToList(log, *this, QStringLiteral("Mil.flag '%1' matches: %2").arg(boolToYesNo(this->isMilitary())).arg(score));
score += 10;
}
}
else if ((this->isColorLivery() && otherLivery.isAirlineLivery()) || (otherLivery.isColorLivery() && this->isAirlineLivery()))
{
// 1 airline, 1 color livery
// 0 .. 50
// 25 is weaker as same ICAO code / 2 from above
score = preferColorLiveries ? 25 : 0;
score += 25 * colorMultiplier; // needs to be the same as in 2 airlines
CLogUtilities::addLogDetailsToList(log, *this, QStringLiteral("Color/airline mixed, color multiplier %1: %2").arg(colorMultiplier).arg(score));
}
return score;
}
bool CLivery::isNull() const
{
return m_airline.isNull() && m_combinedCode.isEmpty() && m_description.isEmpty();
}
const CLivery &CLivery::null()
{
static const CLivery null;
return null;
}
} // namespace
} // namespace