/* 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/interpolationhints.h" #include "blackmisc/aviation/callsign.h" #include "blackmisc/pq/length.h" #include "blackmisc/logmessage.h" #include "blackmisc/worker.h" #include "blackmisc/directoryutils.h" #include #include using namespace BlackConfig; using namespace BlackMisc; using namespace BlackMisc::Aviation; using namespace BlackMisc::PhysicalQuantities; namespace BlackMisc { namespace Simulation { IInterpolator::IInterpolator(IRemoteAircraftProvider *provider, const QString &objectName, QObject *parent) : QObject(parent), CRemoteAircraftAware(provider) { Q_ASSERT_X(provider, Q_FUNC_INFO, "missing provider"); this->setObjectName(objectName); } IInterpolator::~IInterpolator() { } BlackMisc::Aviation::CAircraftSituation IInterpolator::getInterpolatedSituation( const CCallsign &callsign, qint64 currentTimeSinceEpoc, const CInterpolationHints &hints, InterpolationStatus &status) const { // has to be thread safe status.reset(); Q_ASSERT_X(!callsign.isEmpty(), Q_FUNC_INFO, "empty callsign"); auto currentSituation = this->getInterpolatedSituation(callsign, this->remoteAircraftSituations(callsign), currentTimeSinceEpoc, hints, status); currentSituation.setCallsign(callsign); // make sure callsign is correct return currentSituation; } CAircraftParts IInterpolator::getInterpolatedParts(const CCallsign &callsign, const CAircraftPartsList &parts, qint64 currentTimeMsSinceEpoch, IInterpolator::PartsStatus &partsStatus) const { partsStatus.reset(); if (currentTimeMsSinceEpoch < 0) { currentTimeMsSinceEpoch = QDateTime::currentMSecsSinceEpoch(); } // find the first parts not in the correct order, keep only the parts before that one const auto end = std::is_sorted_until(parts.begin(), parts.end(), [](auto && a, auto && b) { return b.getAdjustedMSecsSinceEpoch() < a.getAdjustedMSecsSinceEpoch(); }); const auto validParts = makeRange(parts.begin(), end); // stop if we don't have any parts if (validParts.isEmpty()) { return {}; } partsStatus.setSupportsParts(true); // find the first parts earlier than the current time const auto pivot = std::partition_point(validParts.begin(), validParts.end(), [ = ](auto && p) { return p.getAdjustedMSecsSinceEpoch() > currentTimeMsSinceEpoch; }); const auto partsNewer = makeRange(validParts.begin(), pivot).reverse(); const auto partsOlder = makeRange(pivot, validParts.end()); if (partsOlder.isEmpty()) { return *(partsNewer.end() - 1); } CAircraftParts currentParts = partsOlder.front(); if (currentParts.isOnGround()) { return currentParts; } // here we know aircraft is not on ground, and we check if it was recently on ground or if it will be on ground soon const auto latestTakeoff = std::adjacent_find(partsOlder.begin(), partsOlder.end(), [](auto &&, auto && p) { return p.isOnGround(); }); const auto soonestLanding = std::find_if(partsNewer.begin(), partsNewer.end(), [](auto && p) { return p.isOnGround(); }); // our clairvoyance is limited by the time offset const double significantPast = 5.0; // \fixme 20170121 KB would it make sense to centrally define the update time (5secs), in case it changes. I see a lot of 5.0 hardcoded here // I also wonder if the time is const (interim updates) const double predictableFuture = soonestLanding == partsNewer.end() ? 5.0 : std::min(5.0, static_cast(soonestLanding->getTimeOffsetMs()) / 1000.0); const double secondsSinceTakeoff = latestTakeoff == partsOlder.end() ? 5.0 : (currentTimeMsSinceEpoch - latestTakeoff->getAdjustedMSecsSinceEpoch()) / 1000.0; const double secondsUntilLanding = soonestLanding == partsNewer.end() ? 5.0 : (soonestLanding->getAdjustedMSecsSinceEpoch() - currentTimeMsSinceEpoch) / 1000.0; Q_ASSERT(secondsSinceTakeoff >= 0.0); Q_ASSERT(secondsUntilLanding >= 0.0); const double takeoffFactor = secondsSinceTakeoff / significantPast; const double landingFactor = secondsUntilLanding / predictableFuture; const double airborneFactor = std::min(std::min(takeoffFactor, landingFactor), 1.0); currentParts.setOnGroundInterpolated(1.0 - smootherStep(airborneFactor)); const CInterpolationAndRenderingSetup setup = this->getInterpolatorSetup(); if (setup.getLogCallsigns().contains(callsign)) { PartsLog log; log.timestamp = currentTimeMsSinceEpoch; log.parts = currentParts; IInterpolator::logParts(log); } return currentParts; } CAircraftParts IInterpolator::getInterpolatedParts(const CCallsign &callsign, qint64 currentTimeMsSinceEpoch, IInterpolator::PartsStatus &partsStatus) const { Q_ASSERT_X(!callsign.isEmpty(), Q_FUNC_INFO, "empty callsign"); partsStatus.reset(); partsStatus.setSupportsParts(this->isRemoteAircraftSupportingParts(callsign)); if (!partsStatus.isSupportingParts()) { return {}; } return this->getInterpolatedParts(callsign, this->remoteAircraftParts(callsign, -1), currentTimeMsSinceEpoch, partsStatus); } void IInterpolator::setInterpolatorSetup(const CInterpolationAndRenderingSetup &setup) { QWriteLocker l(&m_lockSetup); m_setup = setup; } CWorker *IInterpolator::writeLogInBackground() { // make sure logging is stopped { QWriteLocker l(&m_lockSetup); m_setup.clearInterpolatorLogCallsigns(); } QList interpolation; QList parts; { QReadLocker l(&m_lockLogs); interpolation = m_interpolationLogs; parts = m_partsLogs; } CWorker *worker = CWorker::fromTask(this, "WriteInterpolationLog", [interpolation, parts]() { const CStatusMessage msg = IInterpolator::writeLogFile(interpolation, parts); CLogMessage::preformatted(msg); }); return worker; } CStatusMessage IInterpolator::writeLogFile(const QList &interpolation, const QList &parts) { if (parts.isEmpty() && interpolation.isEmpty()) { return CStatusMessage(static_cast(nullptr)).warning("No data for log"); } const QString htmlInterpolation = IInterpolator::getHtmlInterpolationLog(interpolation); const QString htmlParts = IInterpolator::getHtmlPartsLog(parts); const QString html = htmlParts % QLatin1Literal("\n\n") % htmlInterpolation; const QString htmlTemplate = CFileUtils::readFileToString(CBuildConfig::getHtmlTemplateFileName()); const QString ts = QDateTime::currentDateTimeUtc().toString("yyyyMMddhhmmss"); const QString fn = CFileUtils::appendFilePaths(CDirectoryUtils::getLogDirectory(), QString("%1 interpolation.html").arg(ts)); const bool s = CFileUtils::writeStringToFile(htmlTemplate.arg(html), fn); if (s) { return CStatusMessage(static_cast(nullptr)).info("Written log file '%1'") << fn; } else { return CStatusMessage(static_cast(nullptr)).error("Failed to write log file '%1'") << fn; } } CInterpolationAndRenderingSetup IInterpolator::getInterpolatorSetup() const { QReadLocker l(&m_lockSetup); return m_setup; } void IInterpolator::logInterpolation(const IInterpolator::InterpolationLog &log) const { QWriteLocker l(&m_lockLogs); m_interpolationLogs.append(log); } void IInterpolator::logParts(const IInterpolator::PartsLog &parts) const { QWriteLocker l(&m_lockLogs); m_partsLogs.append(parts); } QString IInterpolator::getHtmlInterpolationLog(const QList &logs) { if (logs.isEmpty()) { return {}; } QString tableRows; const QString tableHeader = QLatin1Literal("") % QLatin1Literal("CSVTOLtimestamp") % QLatin1Literal("ts oldts newts cur") % QLatin1Literal("ΔtΔt fr.fraction") % QLatin1Literal("lat.oldlat.newlat.cur") % QLatin1Literal("lng.oldlng.newlng.cur") % QLatin1Literal("alt.oldalt.newalt.cur") % QLatin1Literal("elv.oldelv.newelv.cur") % QLatin1Literal("gnd.factor") % QLatin1Literal("onGnd.oldonGnd.newonGnd.cur") % QLatin1Literal("\n"); static const CLengthUnit ft = CLengthUnit::ft(); for (const InterpolationLog &log : logs) { // concatenating in multiple steps, otherwise C4503 warnings tableRows += QLatin1Literal("") % QLatin1Literal("") % log.callsign.asString() % QLatin1Literal("") % QLatin1Literal("") % boolToYesNo(log.vtolAircraft) % QLatin1Literal("") % QLatin1Literal("") % msSinceEpochToTime(log.timestamp) % QLatin1Literal("") % QLatin1Literal("") % msSinceEpochToTime(log.oldSituation.getAdjustedMSecsSinceEpoch()) % QLatin1Char('-') % QString::number(log.oldSituation.getTimeOffsetMs()) % QLatin1Literal("") % QLatin1Literal("") % msSinceEpochToTime(log.newSituation.getAdjustedMSecsSinceEpoch()) % QLatin1Char('-') % QString::number(log.newSituation.getTimeOffsetMs()) % QLatin1Literal("") % QLatin1Literal("") % msSinceEpochToTime(log.currentSituation.getAdjustedMSecsSinceEpoch()) % QLatin1Char('-') % QString::number(log.currentSituation.getTimeOffsetMs()) % QLatin1Literal("") % QLatin1Literal("") % QString::number(log.deltaTimeMs) % QLatin1Literal("") % QLatin1Literal("") % QString::number(log.deltaTimeFractionMs) % QLatin1Literal("") % QLatin1Literal("") % QString::number(log.simulationTimeFraction) % QLatin1Literal(""); tableRows += QLatin1Literal("") % log.oldSituation.latitudeAsString() % QLatin1Literal("") % QLatin1Literal("") % log.newSituation.latitudeAsString() % QLatin1Literal("") % QLatin1Literal("") % log.currentSituation.latitudeAsString() % QLatin1Literal("") % QLatin1Literal("") % log.oldSituation.longitudeAsString() % QLatin1Literal("") % QLatin1Literal("") % log.newSituation.longitudeAsString() % QLatin1Literal("") % QLatin1Literal("") % log.currentSituation.longitudeAsString() % QLatin1Literal(""); tableRows += QLatin1Literal("") % log.oldSituation.getAltitude().valueRoundedWithUnit(ft, 1) % QLatin1Literal("") % QLatin1Literal("") % log.newSituation.getAltitude().valueRoundedWithUnit(ft, 1) % QLatin1Literal("") % QLatin1Literal("") % log.currentSituation.getAltitude().valueRoundedWithUnit(ft, 1) % QLatin1Literal("") % QLatin1Literal("") % log.oldSituation.getGroundElevation().valueRoundedWithUnit(ft, 1) % QLatin1Literal("") % QLatin1Literal("") % log.newSituation.getGroundElevation().valueRoundedWithUnit(ft, 1) % QLatin1Literal("") % QLatin1Literal("") % log.currentSituation.getGroundElevation().valueRoundedWithUnit(ft, 1) % QLatin1Literal("") % QLatin1Literal("") % QString::number(log.groundFactor) % QLatin1Literal("") % QLatin1Literal("") % log.oldSituation.getOnGroundInfo() % QLatin1Literal("") % QLatin1Literal("") % log.newSituation.getOnGroundInfo() % QLatin1Literal("") % QLatin1Literal("") % log.currentSituation.getOnGroundInfo() % QLatin1Literal("") % QLatin1Literal("\n"); } return QLatin1Literal("\n") % tableHeader % tableRows % QLatin1Literal("
\n"); } QString IInterpolator::getHtmlPartsLog(const QList &logs) { if (logs.isEmpty()) { return {}; } QString tableRows; const QString tableHeader = QLatin1Literal("") % QLatin1Literal("CStimestamp") % QLatin1Literal("parts") % QLatin1Literal("\n"); for (const PartsLog &log : logs) { // concatenating in multiple steps, otherwise C4503 warnings tableRows += QLatin1Literal("") % QLatin1Literal("") % log.callsign.asString() % QLatin1Literal("") % QLatin1Literal("") % msSinceEpochToTime(log.timestamp) % QLatin1Literal("") % QLatin1Literal("") % log.parts.toQString() % QLatin1Literal(""); } return QLatin1Literal("\n") % tableHeader % tableRows % QLatin1Literal("
\n"); } void IInterpolator::clearLog() { QWriteLocker l(&m_lockLogs); this->m_partsLogs.clear(); this->m_interpolationLogs.clear(); } void IInterpolator::setGroundElevationFromHint(const CInterpolationHints &hints, CAircraftSituation &situation) { if (situation.hasGroundElevation()) { return; } const CAltitude elevation = hints.getGroundElevation(situation); if (elevation.isNull()) { return; } situation.setGroundElevation(elevation); } void IInterpolator::setGroundFlagFromInterpolator(const CInterpolationHints &hints, double groundFactor, CAircraftSituation &situation) { // by interpolation if (groundFactor >= 1.0) { situation.setOnGround(CAircraftSituation::OnGround, CAircraftSituation::OnGroundByInterpolation); return; } if (groundFactor < 1.0 && groundFactor >= 0.0) { situation.setOnGround(CAircraftSituation::NotOnGround, CAircraftSituation::OnGroundByInterpolation); return; } // no value by factor, guess on elevation const CLength heightAboveGround(situation.getHeightAboveGround()); const CLength cgAboveGround(hints.getCGAboveGround()); if (!heightAboveGround.isNull()) { const bool og = cgAboveGround.isNull() ? heightAboveGround.value(CLengthUnit::m()) < 1.0 : heightAboveGround <= cgAboveGround; situation.setOnGround(og ? CAircraftSituation::OnGround : CAircraftSituation::NotOnGround, CAircraftSituation::OnGroundByElevation); } // for VTOL aircraft we give up if (hints.isVtolAircraft()) { situation.setOnGround(CAircraftSituation::OnGroundSituationUnknown, CAircraftSituation::OnGroundReliabilityNoSet); return; } // we guess on speed, pitch and bank by excluding situations situation.setOnGround(CAircraftSituation::NotOnGround, CAircraftSituation::OnGroundByGuessing); if (qAbs(situation.getPitch().value(CAngleUnit::deg())) > 10) { return; } if (qAbs(situation.getBank().value(CAngleUnit::deg())) > 10) { return; } if (situation.getGroundSpeed().value(CSpeedUnit::km_h()) > 50) { return; } // not sure, but this is a guess situation.setOnGround(CAircraftSituation::OnGround, CAircraftSituation::OnGroundByGuessing); } QString IInterpolator::msSinceEpochToTime(qint64 ms) { static const QString dateFormat("hh:mm:ss.zzz"); return QDateTime::fromMSecsSinceEpoch(ms).toString(dateFormat); } QString IInterpolator::msSinceEpochToTime(qint64 t1, qint64 t2, qint64 t3) { if (t3 < 0) return QString("%1 %2").arg(msSinceEpochToTime(t1), msSinceEpochToTime(t2)); return QString("%1 %2 %3").arg(msSinceEpochToTime(t1), msSinceEpochToTime(t2), msSinceEpochToTime(t3)); } bool IInterpolator::InterpolationStatus::allTrue() const { return m_interpolationSucceeded && m_changedPosition; } void IInterpolator::InterpolationStatus::reset() { m_changedPosition = false; m_interpolationSucceeded = false; } bool IInterpolator::PartsStatus::allTrue() const { return m_supportsParts; } void IInterpolator::PartsStatus::reset() { m_supportsParts = false; } } // namespace } // namespace