diff --git a/src/blackmisc/aviation/aircrafticaocode.cpp b/src/blackmisc/aviation/aircrafticaocode.cpp index 0deb35415..89d48610b 100644 --- a/src/blackmisc/aviation/aircrafticaocode.cpp +++ b/src/blackmisc/aviation/aircrafticaocode.cpp @@ -108,6 +108,55 @@ namespace BlackMisc return this->getCombinedIcaoStringWithKey(); } + int CAircraftIcaoCode::calculateScore(const CAircraftIcaoCode &otherCode) const + { + const bool bothFromDb = this->isLoadedFromDb() && otherCode.isLoadedFromDb(); + if (bothFromDb && otherCode.getDbKey() == this->getDbKey()) { return 100; } + + int score = 0; + if (this->hasValidDesignator() && this->getDesignator() == otherCode.getDesignator()) + { + score += 50; + + if (this->getRank() == 0) { score += 15; } + else if (this->getRank() == 1) { score += 12; } + else if (this->getRank() < 10) { score += (10 - this->getRank()); } + } + else + { + if (this->hasFamily() && this->getFamily() == otherCode.getFamily()) + { + score += 30; + } + else if (this->hasValidCombinedType() && otherCode.getCombinedType() == this->getCombinedType()) + { + score += 20; + } + else if (this->hasValidCombinedType()) + { + if (this->getEngineCount() == otherCode.getEngineCount()) { score += 2; } + if (this->getEngineType() == otherCode.getEngineType()) { score += 2; } + if (this->getAircraftType() == otherCode.getAircraftType()) { score += 2; } + } + } + + // score needs to higher than ranking + if (this->hasManufacturer() && otherCode.hasManufacturer()) + { + if (this->matchesManufacturer(otherCode.getManufacturer())) + { + score += 20; + } + else if (this->getManufacturer().contains(otherCode.getManufacturer(), Qt::CaseInsensitive)) + { + score += 15; + } + } + + // 0..85 + return score; + } + bool CAircraftIcaoCode::hasDesignator() const { return !this->m_designator.isEmpty(); @@ -153,7 +202,7 @@ namespace BlackMisc int CAircraftIcaoCode::getEngineCount() const { if (this->m_combinedType.length() < 2) { return -1; } - QString c(this->m_combinedType.mid(1, 1)); + const QString c(this->m_combinedType.mid(1, 1)); if (c == "-") { return -1; } bool ok; int ec = c.toInt(&ok); @@ -212,6 +261,12 @@ namespace BlackMisc return !m_manufacturer.isEmpty(); } + bool CAircraftIcaoCode::matchesManufacturer(const QString &manufacturer) const + { + if (manufacturer.isEmpty()) { return false; } + return (manufacturer.length() == this->m_manufacturer.length() && this->m_manufacturer.startsWith(manufacturer, Qt::CaseInsensitive)); + } + bool CAircraftIcaoCode::isVtol() const { // special designators diff --git a/src/blackmisc/aviation/aircrafticaocode.h b/src/blackmisc/aviation/aircrafticaocode.h index 9a74dd7f0..5f38f5371 100644 --- a/src/blackmisc/aviation/aircrafticaocode.h +++ b/src/blackmisc/aviation/aircrafticaocode.h @@ -159,6 +159,9 @@ namespace BlackMisc //! Manufacturer bool hasManufacturer() const; + //! Matching the manufacturer? + bool matchesManufacturer(const QString &manufacturer) const; + //! Get WTC const QString &getWtc() const { return m_wtc; } @@ -249,6 +252,10 @@ namespace BlackMisc //! As a brief HTML summary (e.g. used in tooltips) QString asHtmlSummary () const; + //! Considers rank, manufacturer and family 0..90 + //! \remark normally used with a selected set of ICAO codes or combined types + int calculateScore(const CAircraftIcaoCode &otherCode) const; + //! Valid designator? static bool isValidDesignator(const QString &designator); diff --git a/src/blackmisc/aviation/aircrafticaocodelist.cpp b/src/blackmisc/aviation/aircrafticaocodelist.cpp index a0af82388..df2ea1e5d 100644 --- a/src/blackmisc/aviation/aircrafticaocodelist.cpp +++ b/src/blackmisc/aviation/aircrafticaocodelist.cpp @@ -146,6 +146,11 @@ namespace BlackMisc this->sortBy(&CAircraftIcaoCode::getDesignator, &CAircraftIcaoCode::getRank); } + void CAircraftIcaoCodeList::sortByDesignatorManufacturerAndRank() + { + this->sortBy(&CAircraftIcaoCode::getDesignator, &CAircraftIcaoCode::getManufacturer, &CAircraftIcaoCode::getRank); + } + QStringList CAircraftIcaoCodeList::toCompleterStrings(bool withIataCodes, bool withFamily, bool sort) const { QStringList c; @@ -230,49 +235,38 @@ namespace BlackMisc } // get an initial set of data we can choose from + const QString d(icaoPattern.getDesignator()); + if (d.isEmpty()) { return CAircraftIcaoCode(); } CAircraftIcaoCodeList codes; - - // try all designators, even unvalid ones - if (!icaoPattern.getDesignator().isEmpty()) + do { - const QString d(icaoPattern.getDesignator()); codes = this->findByDesignator(d); + if (!codes.isEmpty()) break; - // we have one exact match - if (codes.size() == 1) { return codes.front(); } + // now we search if the ICAO designator is actually an IATA code + codes = this->findByIataCode(d); + if (!codes.isEmpty()) break; - if (codes.isEmpty()) + if (d.length() > 4) { - // now we search if the ICAO designator is - // actually an IATA code - codes = this->findByIataCode(d); - - // we have one exact match - if (codes.size() == 1) { return codes.front(); } - - if (codes.isEmpty()) - { - // still empty, try to find by family - codes = this->findByFamily(d); - - // we have one exact match - if (codes.size() == 1) { return codes.front(); } - - // now try to find as ending - codes = this->findEndingWith(d); - if (codes.size() == 1) { return codes.front(); } - - // still empty, hopeless - if (codes.isEmpty()) { return icaoPattern; } - - // continue here, we have more than one code and - // will try to further reduce - } + codes = this->findByDesignator(d.left(4)); + if (!codes.isEmpty()) break; } - codes.sortByRank(); + + // still empty, try to find by family + codes = this->findByFamily(d); + if (!codes.isEmpty()) break; + + // now try to find as ending + codes = this->findEndingWith(d); } + while (false); + + if (codes.isEmpty()) { return icaoPattern; } + if (codes.size() == 1) { return codes.front(); } // further reduce by manufacturer + codes.sortByRank(); if (icaoPattern.hasManufacturer() && codes.contains(&CAircraftIcaoCode::getManufacturer, icaoPattern.getManufacturer())) { const QString m(icaoPattern.getManufacturer()); @@ -302,5 +296,24 @@ namespace BlackMisc } return codes.frontOrDefault(); // sorted by rank } + + CAircraftIcaoCodeList CAircraftIcaoCodeList::groupByDesignatorAndManufacturer() const + { + CAircraftIcaoCodeList copy(*this); + copy.sortByDesignatorManufacturerAndRank(); + CAircraftIcaoCodeList grouped; + QString designator; + QString manufacturer; + for (const CAircraftIcaoCode code : as_const(copy)) + { + if (code.getDesignator() != designator || code.getManufacturer() != manufacturer) + { + designator = code.getDesignator(); + manufacturer = code.getManufacturer(); + grouped.push_back(code); + } + } + return grouped; + } } // namespace } // namespace diff --git a/src/blackmisc/aviation/aircrafticaocodelist.h b/src/blackmisc/aviation/aircrafticaocodelist.h index 0578f5867..2bd739c20 100644 --- a/src/blackmisc/aviation/aircrafticaocodelist.h +++ b/src/blackmisc/aviation/aircrafticaocodelist.h @@ -82,12 +82,18 @@ namespace BlackMisc //! Best selection by given pattern, also searches IATA and family information CAircraftIcaoCode smartAircraftIcaoSelector(const CAircraftIcaoCode &icaoPattern) const; + //! Group by designator and manufacturer + CAircraftIcaoCodeList groupByDesignatorAndManufacturer() const; + //! Sort by rank void sortByRank(); //! Sort by designator first, then by rank void sortByDesignatorAndRank(); + //! Sort by designator first, then by manufacturer and rank + void sortByDesignatorManufacturerAndRank(); + //! For selection completion QStringList toCompleterStrings(bool withIataCodes = false, bool withFamily = false, bool sort = true) const; diff --git a/src/blackmisc/aviation/airlineicaocode.cpp b/src/blackmisc/aviation/airlineicaocode.cpp index 129158126..bd7b3bb2e 100644 --- a/src/blackmisc/aviation/airlineicaocode.cpp +++ b/src/blackmisc/aviation/airlineicaocode.cpp @@ -153,6 +153,11 @@ namespace BlackMisc return it != simplifiedName.end(); } + bool CAirlineIcaoCode::hasSimplifiedName() const + { + return this->hasName() && !this->getSimplifiedName().isEmpty(); + } + bool CAirlineIcaoCode::hasCompleteData() const { return this->hasValidDesignator() && this->hasValidCountry() && this->hasName(); @@ -391,6 +396,38 @@ namespace BlackMisc return this->getCombinedStringWithKey(); } + int CAirlineIcaoCode::calculateScore(const CAirlineIcaoCode &otherCode) const + { + const bool bothFromDb = otherCode.isLoadedFromDb() && this->isLoadedFromDb(); + if (bothFromDb && this->getDbKey() == otherCode.getDbKey()) + { + return 100; + } + int score = 0; + if (otherCode.hasValidDesignator() && this->getDesignator() == otherCode.getDesignator()) + { + score += 60; + } + + if (bothFromDb && this->isVirtualAirline() == otherCode.isVirtualAirline()) + { + score += 20; + } + if (this->hasName() && this->getName() == otherCode.getName()) + { + score += 20; + } + else if (this->hasTelephonyDesignator() && this->getTelephonyDesignator() == otherCode.getTelephonyDesignator()) + { + score += 15; + } + else if (this->hasSimplifiedName() && this->getSimplifiedName() == otherCode.getSimplifiedName()) + { + score += 10; + } + return score; + } + CAirlineIcaoCode CAirlineIcaoCode::fromDatabaseJson(const QJsonObject &json, const QString &prefix) { if (!existsKey(json, prefix)) diff --git a/src/blackmisc/aviation/airlineicaocode.h b/src/blackmisc/aviation/airlineicaocode.h index 87c0512bc..6fb03eb19 100644 --- a/src/blackmisc/aviation/airlineicaocode.h +++ b/src/blackmisc/aviation/airlineicaocode.h @@ -164,6 +164,9 @@ namespace BlackMisc //! Has (airline) name? bool hasName() const { return !m_name.isEmpty(); } + //! Has simplified airline name? + bool hasSimplifiedName() const; + //! Complete data bool hasCompleteData() const; @@ -197,6 +200,9 @@ namespace BlackMisc //! As a brief HTML summary (e.g. used in tooltips) QString asHtmlSummary () const; + //! Score against other code 0..100 + int calculateScore(const CAirlineIcaoCode &otherCode) const; + //! Valid designator? static bool isValidAirlineDesignator(const QString &airline); diff --git a/src/blackmisc/aviation/airlineicaocodelist.cpp b/src/blackmisc/aviation/airlineicaocodelist.cpp index 39867bc5a..89f535e2b 100644 --- a/src/blackmisc/aviation/airlineicaocodelist.cpp +++ b/src/blackmisc/aviation/airlineicaocodelist.cpp @@ -27,7 +27,7 @@ namespace BlackMisc CAirlineIcaoCodeList CAirlineIcaoCodeList::findByDesignator(const QString &designator) const { - if (CAirlineIcaoCode::isValidAirlineDesignator(designator)) { return CAirlineIcaoCodeList(); } + if (!CAirlineIcaoCode::isValidAirlineDesignator(designator)) { return CAirlineIcaoCodeList(); } return this->findBy([&](const CAirlineIcaoCode & code) { return code.matchesDesignator(designator); @@ -115,7 +115,16 @@ namespace BlackMisc CAirlineIcaoCodeList codesFound; if (patternUsed.hasValidDesignator()) { - codesFound = this->findByVDesignator(patternUsed.getVDesignator()); + if (patternUsed.isVirtualAirline()) + { + // we can tell for sure we search an VA + codesFound = this->findByVDesignator(patternUsed.getVDesignator()); + } + else + { + // we do not know if we are looking for an VA + codesFound = this->findByDesignator(patternUsed.getDesignator()); + } } else { diff --git a/src/blackmisc/aviation/livery.cpp b/src/blackmisc/aviation/livery.cpp index 362cac36f..fec548e2e 100644 --- a/src/blackmisc/aviation/livery.cpp +++ b/src/blackmisc/aviation/livery.cpp @@ -98,6 +98,11 @@ namespace BlackMisc 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; } @@ -185,6 +190,11 @@ namespace BlackMisc 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; } @@ -256,7 +266,7 @@ namespace BlackMisc QString CLivery::getStandardCode(const CAirlineIcaoCode &airline) { - QString code(airline.getDesignator()); + QString code(airline.getVDesignator()); return code.isEmpty() ? "" : code.append('.').append(standardLiveryMarker()); } @@ -383,5 +393,34 @@ namespace BlackMisc return this->getCombinedCodePlusInfo(); } + int CLivery::calculateScore(const CLivery &otherLivery) const + { + int score = 0; + if (this->isLoadedFromDb() && otherLivery.isLoadedFromDb() && (this->getCombinedCode() == otherLivery.getCombinedCode())) + { + return 100; // exact + } + else + { + score += 0.35 * this->getAirlineIcaoCode().calculateScore(otherLivery.getAirlineIcaoCode()); + } + + // 0..50 so far + if (score == 0) { return 0; } + if (this->isAirlineStandardLivery()) { score += 10; } + + // 0..60 so far + if (!this->hasValidColors()) { return score; } + const double cd = this->getColorDistance(otherLivery); // 0..1 + if (cd == 0) + { + score += 40; + } + else + { + score += (1.0 - cd) * 25; // 0..25 + } + return score; + } } // namespace } // namespace diff --git a/src/blackmisc/aviation/livery.h b/src/blackmisc/aviation/livery.h index 6fa48eeb6..d0888457a 100644 --- a/src/blackmisc/aviation/livery.h +++ b/src/blackmisc/aviation/livery.h @@ -113,6 +113,9 @@ namespace BlackMisc //! Tail color set? bool hasColorTail() const; + //! Has valid (fuselage/tail) colors? + bool hasValidColors() const; + //! Set description void setDescription(const QString &description) { this->m_description = description; } @@ -158,14 +161,21 @@ namespace BlackMisc //! Color livery bool isColorLivery() const; - //! Combined color distance (fuselage/tail): 0..1 + //! Color distance 0..1 (0 is best) + double getColorDistance(const CLivery &otherLivery) const; + + //! Combined color distance (fuselage/tail): 0..1 (0 is best) double getColorDistance(const BlackMisc::CRgbColor &fuselage, const BlackMisc::CRgbColor &tail) const; //! Update missing parts void updateMissingParts(const CLivery &otherLivery); //! As a brief HTML summary (e.g. used in tooltips) - QString asHtmlSummary () const; + QString asHtmlSummary() const; + + //! Score by comparison to another livery 0..100 + //! \remark normally used with liveries preselect by airline ICAO code + int calculateScore(const CLivery &otherLivery) const; //! Object from JSON static CLivery fromDatabaseJson(const QJsonObject &json, const QString &prefix = QString("liv_")); diff --git a/src/blackmisc/simulation/aircraftmodel.cpp b/src/blackmisc/simulation/aircraftmodel.cpp index d9a4f8df2..5840c82bb 100644 --- a/src/blackmisc/simulation/aircraftmodel.cpp +++ b/src/blackmisc/simulation/aircraftmodel.cpp @@ -584,6 +584,13 @@ namespace BlackMisc this->m_modelString.startsWith(modelString, sensitivity); } + int CAircraftModel::calculateScore(const CAircraftModel &compareModel) const + { + int score = this->getAircraftIcaoCode().calculateScore(compareModel.getAircraftIcaoCode()); + score += this->getLivery().calculateScore(compareModel.getLivery()); + return 0.5 * score; + } + CStatusMessageList CAircraftModel::validate(bool withNestedObjects) const { static const CLogCategoryList cats(CLogCategoryList(this).join({ CLogCategory::validation() })); diff --git a/src/blackmisc/simulation/aircraftmodel.h b/src/blackmisc/simulation/aircraftmodel.h index 541ae7dad..e94fe7c4f 100644 --- a/src/blackmisc/simulation/aircraftmodel.h +++ b/src/blackmisc/simulation/aircraftmodel.h @@ -338,6 +338,9 @@ namespace BlackMisc //! Matches model string? bool matchesModelString(const QString &modelString, Qt::CaseSensitivity sensitivity) const; + //! Calculate score + int calculateScore(const CAircraftModel &compareModel) const; + //! Validate BlackMisc::CStatusMessageList validate(bool withNestedObjects) const; @@ -368,7 +371,7 @@ namespace BlackMisc //! Model mode static ModelMode modelModeFromString(const QString &mode); - //! Model mode- + //! Model mode static const QString &modelModeToString(ModelMode mode); //! From swift DB JSON diff --git a/src/blackmisc/simulation/aircraftmodellist.cpp b/src/blackmisc/simulation/aircraftmodellist.cpp index a1f90aa74..bbbd739f0 100644 --- a/src/blackmisc/simulation/aircraftmodellist.cpp +++ b/src/blackmisc/simulation/aircraftmodellist.cpp @@ -528,6 +528,18 @@ namespace BlackMisc } } + ScoredModels CAircraftModelList::scoreFull(const CAircraftModel &remoteModel, bool ignoreZeroScores) const + { + ScoredModels scoreMap; + for (const CAircraftModel &model : *this) + { + const int score = model.calculateScore(remoteModel); + if (ignoreZeroScores && score < 1) { continue; } + scoreMap.insertMulti(score, model); + } + return scoreMap; + } + QStringList CAircraftModelList::toCompleterStrings(bool sorted) const { QStringList c; diff --git a/src/blackmisc/simulation/aircraftmodellist.h b/src/blackmisc/simulation/aircraftmodellist.h index fc6a65192..6ef60a724 100644 --- a/src/blackmisc/simulation/aircraftmodellist.h +++ b/src/blackmisc/simulation/aircraftmodellist.h @@ -43,6 +43,9 @@ namespace BlackMisc namespace Simulation { + //! Individual (matching) score for each model + using ScoredModels = QMap; + //! Value object encapsulating a list of aircraft models class BLACKMISC_EXPORT CAircraftModelList : public BlackMisc::CSequence, @@ -204,6 +207,9 @@ namespace BlackMisc //! File name normalized for DB void normalizeFileNamesForDb(); + //! Score by aircraft ICAO code + ScoredModels scoreFull(const CAircraftModel &remoteModel, bool ignoreZeroScores = true) const; + //! Completer strings QStringList toCompleterStrings(bool sorted = true) const;