/* 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 "interpolator.h" #include "blackconfig/buildconfig.h" #include "blackmisc/simulation/interpolationlogger.h" #include "blackmisc/simulation/interpolatorlinear.h" #include "blackmisc/simulation/interpolatorspline.h" #include "blackmisc/aviation/callsign.h" #include "blackmisc/aviation/heading.h" #include "blackmisc/pq/angle.h" #include "blackmisc/pq/speed.h" #include "blackmisc/pq/units.h" #include "blackmisc/pq/length.h" #include "blackmisc/logmessage.h" #include "blackmisc/verify.h" #include #include #include using namespace BlackConfig; using namespace BlackMisc::Aviation; using namespace BlackMisc::Geo; using namespace BlackMisc::Math; using namespace BlackMisc::PhysicalQuantities; namespace BlackMisc { namespace Simulation { template CInterpolator::CInterpolator(const CCallsign &callsign, ISimulationEnvironmentProvider *simEnvProvider, IInterpolationSetupProvider *setupProvider, IRemoteAircraftProvider *remoteProvider, CInterpolationLogger *logger) : m_callsign(callsign) { // normally when created m_cg is still null since there is no CG in the provider yet if (simEnvProvider) { this->setSimulationEnvironmentProvider(simEnvProvider); } if (setupProvider) { this->setInterpolationSetupProvider(setupProvider); } if (remoteProvider) { this->setRemoteAircraftProvider(remoteProvider); QObject::connect(&m_initTimer, &QTimer::timeout, [ = ] { this->deferredInit(); }); m_initTimer.setSingleShot(true); m_initTimer.start(2500); } this->attachLogger(logger); } template CLength CInterpolator::getAndFetchModelCG() { const CLength cg = this->getCG(m_callsign); m_model.setCG(cg); m_model.setCallsign(m_callsign); return cg; } template double CInterpolator::groundInterpolationFactor() { // done here so we can change value without "larfer" recompilations static constexpr double f = 0.95; return f; } template CAircraftSituationList CInterpolator::remoteAircraftSituationsAndChange(const CInterpolationAndRenderingSetupPerCallsign &setup) { const bool vtol = setup.isForcingVtolInterpolation() || m_model.isVtol(); CAircraftSituationList validSituations = this->remoteAircraftSituations(m_callsign); m_currentSituationChange = CAircraftSituationChange(validSituations, m_model.getCG(), vtol, true, true); if (setup.isFixingSceneryOffset() && m_currentSituationChange.hasSceneryDeviation() && m_model.hasCG()) { const CLength os = m_currentSituationChange.getGuessedSceneryDeviationCG(); m_currentSceneryOffset = os; if (!os.isNull()) { const CLength addValue = os * -1.0; // positive values means too high, negative values too low int changed = validSituations.addAltitudeOffset(addValue); m_currentSituationChange = CAircraftSituationChange(validSituations, m_model.getCG(), vtol, true, true); // recalculate Q_UNUSED(changed); } } else { m_currentSceneryOffset = CLength::null(); } return validSituations; } template void CInterpolator::deferredInit() { if (m_model.hasModelString()) { return; } // set in-between this->initCorrespondingModel(); } template bool CInterpolator::verifyInterpolationSituations(const CAircraftSituation &oldest, const CAircraftSituation &newer, const CAircraftSituation &latest, const CInterpolationAndRenderingSetupPerCallsign &setup) { if (!CBuildConfig::isLocalDeveloperDebugBuild()) { return true; } CAircraftSituationList situations; // oldest last, null ignored if (!latest.isNull()) { situations.push_back(latest); } if (!newer.isNull()) { situations.push_back(newer); } if (!oldest.isNull()) { situations.push_back(oldest); } const bool sorted = situations.isSortedAdjustedLatestFirstWithoutNullPositions(); if (CBuildConfig::isLocalDeveloperDebugBuild()) { BLACK_VERIFY_X(sorted, Q_FUNC_INFO, "Wrong adjusted timestamp order"); } if (setup.isNull() || !setup.isAircraftPartsEnabled()) { return sorted; } bool details = false; if (situations.containsOnGroundDetails(CAircraftSituation::InFromParts)) { // if a client supports parts, all situations are supposed to be parts based details = situations.areAllOnGroundDetailsSame(CAircraftSituation::InFromParts); BLACK_VERIFY_X(details, Q_FUNC_INFO, "Once gnd.from parts -> always gnd. from parts"); } // result return sorted && details; } template const CLogCategoryList &CInterpolator::getLogCategories() { static const CLogCategoryList cats { CLogCategory::interpolator() }; return cats; } template CInterpolationResult CInterpolator::getInterpolation(qint64 currentTimeSinceEpoc, const CInterpolationAndRenderingSetupPerCallsign &setup, int aircraftNumber) { CInterpolationResult result; do { // make sure we can also interpolate parts only (needed in unit tests) if (aircraftNumber < 0) { aircraftNumber = 0; } const bool init = this->initIniterpolationStepData(currentTimeSinceEpoc, setup, aircraftNumber); if (!m_unitTest && !init) { break; } // failure in real scenarios, unit tests move on Q_ASSERT_X(m_currentTimeMsSinceEpoch > 0, Q_FUNC_INFO, "No valid timestamp, interpolator initialized?"); const CAircraftSituation interpolatedSituation = this->getInterpolatedSituation(); const CAircraftParts interpolatedParts = this->getInterpolatedOrGuessedParts(aircraftNumber); result.setValues(interpolatedSituation, interpolatedParts); } while (false); result.setStatus(m_currentInterpolationStatus, m_currentPartsStatus); return result; } template CAircraftSituation CInterpolator::getInterpolatedSituation() { if (m_currentSituations.isEmpty()) { m_lastSituation = CAircraftSituation::null(); return CAircraftSituation::null(); } const CAircraftSituation latest = m_currentSituations.front(); CAircraftSituation currentSituation = m_lastSituation.isNull() ? latest : m_lastSituation; if (currentSituation.getCallsign() != m_callsign) { BLACK_VERIFY_X(false, Q_FUNC_INFO, "Wrong callsign"); currentSituation.setCallsign(m_callsign); } // set elevation if available if (!currentSituation.hasGroundElevation()) { const CElevationPlane currentGroundElevation = this->findClosestElevationWithinRange(currentSituation, currentSituation.getDistancePerTime(1000)); currentSituation.setGroundElevationChecked(currentGroundElevation); // set as default } // fetch CG once const CLength cg(this->getModelCG()); currentSituation.setCG(cg); // interpolant function from derived class // CInterpolatorLinear::Interpolant or CInterpolatorSpline::Interpolant SituationLog log; const auto interpolant = derived()->getInterpolant(log); // succeeded so far? if (!m_currentInterpolationStatus.isInterpolated()) { m_currentInterpolationStatus.checkIfValidSituation(currentSituation); return currentSituation; } // Pitch bank heading // first, so follow up steps could use those values const CInterpolatorPbh pbh = interpolant.pbh(); currentSituation.setHeading(pbh.getHeading()); currentSituation.setPitch(pbh.getPitch()); currentSituation.setBank(pbh.getBank()); currentSituation.setGroundSpeed(pbh.getGroundSpeed()); // use derived interpolant function const bool interpolateGndFlag = pbh.getNewSituation().hasGroundDetailsForGndInterpolation() && pbh.getOldSituation().hasGroundDetailsForGndInterpolation(); currentSituation = interpolant.interpolatePositionAndAltitude(currentSituation, interpolateGndFlag); if (!interpolateGndFlag) { currentSituation.guessOnGround(CAircraftSituationChange::null(), m_model); } // correct itself CAircraftSituation::AltitudeCorrection altCorrection = CAircraftSituation::NoCorrection; if (!interpolateGndFlag && currentSituation.getOnGroundDetails() != CAircraftSituation::OnGroundByGuessing) { // just in case altCorrection = currentSituation.correctAltitude(cg, true); } // status m_currentInterpolationStatus.setInterpolatedAndCheckSituation(true, currentSituation); // logging if (this->doLogging()) { static const QString elv("found %1 missed %2"); const QPair elvStats = this->getElevationsFoundMissed(); log.tsCurrent = m_currentTimeMsSinceEpoch; log.callsign = m_callsign; log.groundFactor = currentSituation.getOnGroundFactor(); log.altCorrection = CAircraftSituation::altitudeCorrectionToString(altCorrection); log.situationCurrent = currentSituation; log.change = m_currentSituationChange; log.usedSetup = m_currentSetup; log.elevationInfo = elv.arg(elvStats.first).arg(elvStats.second); log.cgAboveGround = cg; log.sceneryOffset = m_currentSceneryOffset; m_logger->logInterpolation(log); } // bye m_lastSituation = currentSituation; return currentSituation; } template CAircraftParts CInterpolator::getInterpolatedParts() { // (!) this code is used by linear and spline interpolator // Parts are supposed to be in correct order, latest first const CAircraftPartsList validParts = this->remoteAircraftParts(m_callsign); // log for empty parts aircraft parts if (validParts.isEmpty()) { static const CAircraftParts emptyParts; this->logParts(emptyParts, validParts.size(), true); return emptyParts; } m_currentPartsStatus.setSupportsParts(true); CAircraftParts currentParts; do { // find the first parts earlier than the current time const auto pivot = std::partition_point(validParts.begin(), validParts.end(), [ = ](auto &&p) { return p.getAdjustedMSecsSinceEpoch() > m_currentTimeMsSinceEpoch; }); const auto partsNewer = makeRange(validParts.begin(), pivot).reverse(); const auto partsOlder = makeRange(pivot, validParts.end()); // if (partsOlder.isEmpty()) { currentParts = *(partsNewer.end() - 1); break; } if (partsOlder.isEmpty()) { currentParts = *(partsNewer.begin()); break; } currentParts = partsOlder.front(); // latest older parts } while (false); this->logParts(currentParts, validParts.size(), false); return currentParts; } template CAircraftParts CInterpolator::getInterpolatedOrGuessedParts(int aircraftNumber) { Q_ASSERT_X(m_partsToSituationInterpolationRatio >= 1 && m_partsToSituationInterpolationRatio < 11, Q_FUNC_INFO, "Wrong ratio"); if (!m_unitTest && !m_lastParts.isNull() && ((m_interpolatedSituationsCounter + aircraftNumber) % m_partsToSituationInterpolationRatio) == 0) { m_currentPartsStatus = m_lastPartsStatus; m_currentPartsStatus.setReusedParts(true); return m_lastParts; } CAircraftParts parts; if (m_currentSetup.isAircraftPartsEnabled()) { // this already logs parts = this->getInterpolatedParts(); } // if we have supported parts, we skip this step, but it can happen // the parts are still empty if (!m_currentPartsStatus.isSupportingParts()) { // check if model has been thru model matching parts.guessParts(m_lastSituation, m_currentSituationChange, m_model); this->logParts(parts, 0, false); } m_lastParts = parts; m_lastPartsStatus = m_currentPartsStatus; return parts; } template bool CInterpolator::doLogging() const { return this->hasAttachedLogger() && m_currentSetup.logInterpolation(); } template void CInterpolator::logParts(const CAircraftParts &parts, int partsNo, bool empty) const { if (!this->doLogging()) { return; } PartsLog logInfo; logInfo.callsign = m_callsign; logInfo.noNetworkParts = partsNo; logInfo.tsCurrent = m_currentTimeMsSinceEpoch; logInfo.parts = parts; logInfo.empty = empty; m_logger->logParts(logInfo); } template QString CInterpolator::getInterpolatorInfo() const { return QStringLiteral("Callsign: ") % m_callsign.asString() % QStringLiteral(" situations: ") % QString::number(this->remoteAircraftSituationsCount(m_callsign)) % QStringLiteral(" parts: ") % QString::number(this->remoteAircraftPartsCount(m_callsign)) % QStringLiteral(" 1st interpolation: ") % boolToYesNo(m_lastSituation.isNull()); } template void CInterpolator::resetLastInterpolation() { m_lastSituation.setNull(); } template void CInterpolator::clear() { this->resetLastInterpolation(); m_model = CAircraftModel(); m_currentSceneryOffset = CLength::null(); m_currentSituationChange = CAircraftSituationChange::null(); m_currentSituations.clear(); m_currentTimeMsSinceEpoch = -1; m_situationsLastModified = -1; m_situationsLastModifiedUsed = -1; m_currentInterpolationStatus.reset(); m_currentPartsStatus.reset(); m_interpolatedSituationsCounter = 0; } template bool CInterpolator::initIniterpolationStepData(qint64 currentTimeSinceEpoc, const CInterpolationAndRenderingSetupPerCallsign &setup, int aircraftNumber) { Q_ASSERT_X(!m_callsign.isEmpty(), Q_FUNC_INFO, "Missing callsign"); const qint64 lastModifed = this->situationsLastModified(m_callsign); const bool slowUpdateStep = (((m_interpolatedSituationsCounter + aircraftNumber) % 25) == 0); // flag when parts are updated, which need not to be updated every time const bool changedSetup = m_currentSetup != setup; const bool changedSituations = lastModifed > m_situationsLastModified; m_currentTimeMsSinceEpoch = currentTimeSinceEpoc; m_currentInterpolationStatus.reset(); m_currentPartsStatus.reset(); if (changedSetup || changedSituations) { m_currentSetup = setup; m_situationsLastModified = lastModifed; m_currentSituations = this->remoteAircraftSituationsAndChange(setup); // only update when needed } if (!m_model.hasCG() || slowUpdateStep) { this->getAndFetchModelCG(); // update CG } bool success = false; const int situationsSize = m_currentSituations.sizeInt(); m_currentInterpolationStatus.setSituationsCount(situationsSize); if (m_currentSituations.isEmpty()) { const bool inRange = this->isAircraftInRange(m_callsign); m_lastSituation = CAircraftSituation::null(); // no interpolation possible for that step m_currentInterpolationStatus.setExtraInfo(inRange ? QString("No situations, but remote aircraft '%1'").arg(m_callsign.asString()) : QString("Unknown remote aircraft: '%1'").arg(m_callsign.asString())); } else { success = true; m_interpolatedSituationsCounter++; // counter updated in initIniterpolationStepData // with the latest updates of T243 the order and the offsets are supposed to be correct // so even mixing fast/slow updates shall work if (!CBuildConfig::isReleaseBuild()) { Q_ASSERT_X(m_currentSituations.isSortedAdjustedLatestFirstWithoutNullPositions(), Q_FUNC_INFO, "Wrong sort order"); Q_ASSERT_X(m_currentSituations.size() <= IRemoteAircraftProvider::MaxSituationsPerCallsign, Q_FUNC_INFO, "Wrong size"); } } return success; } template void CInterpolator::initCorrespondingModel(const CAircraftModel &model) { if (model.hasModelString()) { m_model = model; } else { CAircraftModel model = this->getAircraftInRangeForCallsign(m_callsign).getModel(); if (model.hasModelString()) { m_model = model; } } this->getAndFetchModelCG(); } CInterpolationResult::CInterpolationResult() { this->reset(); } void CInterpolationResult::setValues(const CAircraftSituation &situation, const CAircraftParts &parts) { m_interpolatedSituation = situation; m_interpolatedParts = parts; } void CInterpolationResult::setStatus(const CInterpolationStatus &interpolation, const CPartsStatus &parts) { m_interpolationStatus = interpolation; m_partsStatus = parts; } void CInterpolationResult::reset() { m_interpolatedSituation = CAircraftSituation::null(); m_interpolatedParts = CAircraftParts::null(); m_interpolationStatus.reset(); m_partsStatus.reset(); } void CInterpolationStatus::setExtraInfo(const QString &info) { m_extraInfo = info; } void CInterpolationStatus::setInterpolatedAndCheckSituation(bool succeeded, const CAircraftSituation &situation) { m_isInterpolated = succeeded; this->checkIfValidSituation(situation); } void CInterpolationStatus::checkIfValidSituation(const CAircraftSituation &situation) { m_isValidSituation = !situation.isPositionOrAltitudeNull(); } bool CInterpolationStatus::hasValidInterpolatedSituation() const { return m_isInterpolated && m_isValidSituation; } void CInterpolationStatus::reset() { m_extraInfo.clear(); m_isValidSituation = false; m_isInterpolated = false; m_situations = -1; } QString CInterpolationStatus::toQString() const { return QStringLiteral("Interpolated: ") % boolToYesNo(m_isInterpolated) % QStringLiteral(" | situations: ") % QString::number(m_situations) % QStringLiteral(" | situation valid: ") % boolToYesNo(m_isValidSituation) % ( m_extraInfo.isEmpty() ? QStringLiteral("") : QStringLiteral(" info: ") % m_extraInfo ); } bool CPartsStatus::allTrue() const { return m_supportsParts; } void CPartsStatus::reset() { m_supportsParts = false; m_resusedParts = false; } // see here for the reason of thess forward instantiations // https://isocpp.org/wiki/faq/templates#separate-template-fn-defn-from-decl //! \cond PRIVATE template class BLACKMISC_EXPORT_DEFINE_TEMPLATE CInterpolator; template class BLACKMISC_EXPORT_DEFINE_TEMPLATE CInterpolator; //! \endcond } // namespace } // namespace