mirror of
https://github.com/swift-project/pilotclient.git
synced 2026-03-22 14:55:36 +08:00
502 lines
22 KiB
C++
502 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/logcategories.h"
|
|
#include "blackmisc/propertyindexref.h"
|
|
#include "blackmisc/statusmessage.h"
|
|
#include "blackmisc/stringutils.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;
|
|
}
|
|
|
|
QVariant CLivery::propertyByIndex(BlackMisc::CPropertyIndexRef index) const
|
|
{
|
|
if (index.isMyself()) { return QVariant::fromValue(*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 QVariant::fromValue(m_description);
|
|
case IndexCombinedCode: return QVariant::fromValue(m_combinedCode);
|
|
case IndexIsMilitary: return QVariant::fromValue(m_military);
|
|
default: return CValueObject::propertyByIndex(index);
|
|
}
|
|
}
|
|
|
|
void CLivery::setPropertyByIndex(CPropertyIndexRef index, const QVariant &variant)
|
|
{
|
|
if (index.isMyself()) { (*this) = variant.value<CLivery>(); return; }
|
|
if (IDatastoreObjectWithIntegerKey::canHandleIndex(index)) { IDatastoreObjectWithIntegerKey::setPropertyByIndex(index, variant); return; }
|
|
const ColumnIndex i = index.frontCasted<ColumnIndex>();
|
|
switch (i)
|
|
{
|
|
case IndexDescription: m_description = variant.toString(); 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.toString()); break;
|
|
case IndexIsMilitary: this->setMilitary(variant.toBool()); break;
|
|
default: CValueObject::setPropertyByIndex(index, variant); break;
|
|
}
|
|
}
|
|
|
|
int CLivery::comparePropertyByIndex(CPropertyIndexRef 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(" ", " ");
|
|
}
|
|
|
|
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
|