/* 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/simulation/interpolationlogger.h" #include "blackmisc/aviation/callsign.h" #include "blackmisc/aviation/heading.h" #include "blackmisc/geo/kmlutils.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/worker.h" #include "blackmisc/swiftdirectories.h" #include "blackmisc/directoryutils.h" #include "blackmisc/stringutils.h" #include "blackconfig/buildconfig.h" #include #include using namespace BlackConfig; using namespace BlackMisc; using namespace BlackMisc::Aviation; using namespace BlackMisc::Geo; using namespace BlackMisc::Math; using namespace BlackMisc::PhysicalQuantities; using namespace BlackMisc::Simulation; namespace BlackMisc::Simulation { CInterpolationLogger::CInterpolationLogger(QObject *parent) : QObject(parent) { this->setObjectName("CInterpolationLogger"); } const QStringList &CInterpolationLogger::getLogCategories() { static const QStringList cats { CLogCategories::interpolator() }; return cats; } CWorker *CInterpolationLogger::writeLogInBackground(bool clearLog) { QList situations; QList parts; { QReadLocker l(&m_lockSituations); situations = m_situationLogs; } { QReadLocker l(&m_lockParts); parts = m_partsLogs; } QPointer myself(this); CWorker *worker = CWorker::fromTask(this, "WriteInterpolationLog", [situations, parts, myself, clearLog]() { const CStatusMessageList msg = CInterpolationLogger::writeLogFiles(situations, parts); CLogMessage::preformatted(msg); if (clearLog && myself) { myself->clearLog(); } }); return worker; } QStringList CInterpolationLogger::getLatestLogFiles() { QStringList files({ "", "" }); const QString logDir = CSwiftDirectories::logDirectory(); QDir logs(logDir); if (!logs.exists()) { return files; } logs.setNameFilters(filePatterns()); const QStringList interpolations = logs.entryList(QStringList({filePatternInterpolationLog()}), QDir::NoFilter, QDir::Time); if (!interpolations.isEmpty()) { files[0] = CFileUtils::appendFilePaths(logDir, interpolations.first()); } const QStringList parts = logs.entryList(QStringList({filePatternPartsLog()}), QDir::NoFilter, QDir::Time); if (!parts.isEmpty()) { files[1] = CFileUtils::appendFilePaths(logDir, parts.first()); } return files; } QString CInterpolationLogger::getLogDirectory() { return CSwiftDirectories::logDirectory(); } CStatusMessageList CInterpolationLogger::writeLogFiles(const QList &interpolation, const QList &parts) { if (parts.isEmpty() && interpolation.isEmpty()) { return CStatusMessage(static_cast(nullptr)).warning(u"No data for log"); } static const QString html = QStringLiteral("Entries: %1\n\n%2"); const QString htmlTemplate = CFileUtils::readFileToString(CSwiftDirectories::htmlTemplateFilePath()); CStatusMessageList msgs; const QString ts = QDateTime::currentDateTimeUtc().toString("yyyyMMddhhmmss"); const QString htmlInterpolation = CInterpolationLogger::getHtmlInterpolationLog(interpolation); if (!htmlInterpolation.isEmpty()) { QString file = filePatternInterpolationLog(); file.remove('*'); const QString fn = CFileUtils::appendFilePaths(CSwiftDirectories::logDirectory(), QStringLiteral("%1 %2").arg(ts, file)); const bool s = CFileUtils::writeStringToFile(htmlTemplate.arg(html.arg(interpolation.size()).arg(htmlInterpolation)), fn); msgs.push_back(CInterpolationLogger::logStatusFileWriting(s, fn)); } const QString htmlParts = CInterpolationLogger::getHtmlPartsLog(parts); if (!htmlParts.isEmpty()) { QString file = filePatternPartsLog(); file.remove('*'); const QString fn = CFileUtils::appendFilePaths(CSwiftDirectories::logDirectory(), QStringLiteral("%1 %2").arg(ts, file)); const bool s = CFileUtils::writeStringToFile(htmlTemplate.arg(html.arg(parts.size()).arg(htmlParts)), fn); msgs.push_back(CInterpolationLogger::logStatusFileWriting(s, fn)); } QString kml = CKmlUtils::wrapAsKmlDocument(CInterpolationLogger::getKmlChangedSituations(interpolation)); if (!kml.isEmpty()) { const QString fn = CFileUtils::appendFilePaths(CSwiftDirectories::logDirectory(), QStringLiteral("%1_changedSituations.kml").arg(ts)); const bool s = CFileUtils::writeStringToFile(kml, fn); msgs.push_back(CInterpolationLogger::logStatusFileWriting(s, fn)); } kml = CKmlUtils::wrapAsKmlDocument(CInterpolationLogger::getKmlInterpolatedSituations(interpolation)); if (!kml.isEmpty()) { const QString fn = CFileUtils::appendFilePaths(CSwiftDirectories::logDirectory(), QStringLiteral("%1_interpolatedSituations.kml").arg(ts)); const bool s = CFileUtils::writeStringToFile(kml, fn); msgs.push_back(CInterpolationLogger::logStatusFileWriting(s, fn)); } kml = CKmlUtils::wrapAsKmlDocument(CInterpolationLogger::getKmlElevations(interpolation)); if (!kml.isEmpty()) { const QString fn = CFileUtils::appendFilePaths(CSwiftDirectories::logDirectory(), QStringLiteral("%1_elevations.kml").arg(ts)); const bool s = CFileUtils::writeStringToFile(kml, fn); msgs.push_back(CInterpolationLogger::logStatusFileWriting(s, fn)); } return msgs; } CStatusMessage CInterpolationLogger::logStatusFileWriting(bool success, const QString &fileName) { return success ? CStatusMessage(static_cast(nullptr)).info(u"Written log file '%1'") << fileName : CStatusMessage(static_cast(nullptr)).error(u"Failed to write log file '%1'") << fileName; } void CInterpolationLogger::logInterpolation(const SituationLog &log) { QWriteLocker l(&m_lockSituations); m_situationLogs.push_back(log); if (m_situationLogs.size() > m_maxSituations) { m_situationLogs.removeFirst(); } } void CInterpolationLogger::logParts(const PartsLog &log) { QWriteLocker l(&m_lockParts); m_partsLogs.push_back(log); } void CInterpolationLogger::setMaxSituations(int max) { QReadLocker l(&m_lockSituations); m_maxSituations = max; } QList CInterpolationLogger::getSituationsLog() const { QReadLocker l(&m_lockSituations); return m_situationLogs; } QList CInterpolationLogger::getPartsLog() const { QReadLocker l(&m_lockParts); return m_partsLogs; } QList CInterpolationLogger::getSituationsLog(const CCallsign &cs) const { const QList copy(this->getSituationsLog()); QList logs; for (const SituationLog &log : copy) { if (log.callsign != cs) { continue; } logs.push_back(log); } return logs; } QList CInterpolationLogger::getPartsLog(const CCallsign &cs) const { const QList copy(this->getPartsLog()); QList logs; for (const PartsLog &log : copy) { if (log.callsign != cs) { continue; } logs.push_back(log); } return logs; } SituationLog CInterpolationLogger::getLastSituationLog() const { QReadLocker l(&m_lockSituations); if (m_situationLogs.isEmpty()) { return SituationLog(); } return m_situationLogs.last(); } SituationLog CInterpolationLogger::getLastSituationLog(const CCallsign &cs) const { const QList copy(this->getSituationsLog(cs)); if (copy.isEmpty()) { return SituationLog(); } return copy.last(); } CAircraftSituation CInterpolationLogger::getLastSituation() const { QReadLocker l(&m_lockSituations); if (m_situationLogs.isEmpty()) { return CAircraftSituation(); } return m_situationLogs.last().situationCurrent; } CAircraftSituation CInterpolationLogger::getLastSituation(const CCallsign &cs) const { const QList copy(this->getSituationsLog(cs)); if (copy.isEmpty()) { return CAircraftSituation(); } return copy.last().situationCurrent; } CAircraftParts CInterpolationLogger::getLastParts() const { QReadLocker l(&m_lockParts); if (m_partsLogs.isEmpty()) { return CAircraftParts(); } return m_partsLogs.last().parts; } CAircraftParts CInterpolationLogger::getLastParts(const CCallsign &cs) const { const QList copy(this->getPartsLog(cs)); if (copy.isEmpty()) { return CAircraftParts(); } return copy.last().parts; } PartsLog CInterpolationLogger::getLastPartsLog() const { QReadLocker l(&m_lockParts); if (m_partsLogs.isEmpty()) { return PartsLog(); } return m_partsLogs.last(); } PartsLog CInterpolationLogger::getLastPartsLog(const CCallsign &cs) const { const QList copy(this->getPartsLog(cs)); if (copy.isEmpty()) { return PartsLog(); } return copy.last(); } const QString &CInterpolationLogger::filePatternInterpolationLog() { static const QString p("*interpolation.html"); return p; } const QString &CInterpolationLogger::filePatternPartsLog() { static const QString p("*parts.html"); return p; } const QStringList &CInterpolationLogger::filePatterns() { static const QStringList l({ filePatternInterpolationLog(), filePatternPartsLog() }); return l; } QString CInterpolationLogger::getHtmlInterpolationLog(const QList &logs) { if (logs.isEmpty()) { return {}; } static const QString tableHeader = QStringLiteral( u"" u"cs.Int" u"recalc" u"CSVTOLtimestampsince" u"ts oldts newts cur" u"Interpolation ts.Sample Δtfraction" u"lat.oldlat.newlat.cur" u"lng.oldlng.newlng.cur" u"alt.oldalt.2ndalt.newalt.cur" u"elv.oldelv.2ndelv.newelv.cur" u"gnd.factor" u"onGnd.oldonGnd.newonGnd.cur" u"CG" u"partscp.parts details" u"\n"); static const CLengthUnit ft = CLengthUnit::ft(); const SituationLog firstLog = logs.first(); qint64 newPosTs = firstLog.newestInterpolationSituation().getMSecsSinceEpoch(); CAircraftParts lastParts; // default, so shown if parts are different from default QString tableRows("\n"); for (const SituationLog &log : logs) { const CAircraftSituation situationOld = log.oldestInterpolationSituation(); const CAircraftSituation situationNew = log.newestInterpolationSituation(); const CAircraftSituation situation2nd = log.secondInterpolationSituation(); const bool changedNewPosition = (newPosTs != situationNew.getMSecsSinceEpoch()); const bool changedParts = (lastParts != log.parts); newPosTs = situationNew.getMSecsSinceEpoch(); lastParts = log.parts; // fixme KB 2018/12 comment and tableRows can be removed if there are no further issues // concatenating in multiple steps, otherwise C4503 warnings tableRows += u"" % (changedNewPosition ? QStringLiteral("*") : QStringLiteral("")) % u"" % log.interpolator % u"" % u"" % boolToYesNo(log.interpolantRecalc) % u"" u"" % log.callsign.asString() % u"" % u"" % boolToYesNo(log.vtolAircraft) % u"" % u"" % msSinceEpochToTime(log.tsCurrent) % u"" % u"" % QString::number(log.tsCurrent - firstLog.tsCurrent) % u"" % u"" % situationOld.getTimestampAndOffset(true) % u"" % u"" % situationNew.getTimestampAndOffset(true) % u"" % u"" % log.situationCurrent.getTimestampAndOffset(true) % u"" % u"" % msSinceEpochToTime(log.tsInterpolated) % u"" % u"" % QString::number(log.deltaSampleTimesMs) % u"ms" % u"" % QString::number(log.simTimeFraction) % u"" % // tableRows += u"" % situationOld.latitudeAsString() % u"" % u"" % situationNew.latitudeAsString() % u"" % u"" % log.situationCurrent.latitudeAsString() % u"" % u"" % situationOld.longitudeAsString() % u"" % u"" % situationNew.longitudeAsString() % u"" % u"" % log.situationCurrent.longitudeAsString() % u"" % // tableRows += u"" % situationOld.getAltitude().valueRoundedWithUnit(ft, 1) % u"" % u"" % situation2nd.getAltitude().valueRoundedWithUnit(ft, 1) % u"" % u"" % situationNew.getAltitude().valueRoundedWithUnit(ft, 1) % u"" % u"" % log.situationCurrent.getAltitude().valueRoundedWithUnit(ft, 1) % u"" % u"" % situationOld.getGroundElevation().valueRoundedWithUnit(ft, 1) % u" " % situationOld.getGroundElevationInfoAsString() % u"" % u"" % situation2nd.getGroundElevation().valueRoundedWithUnit(ft, 1) % u" " % situation2nd.getGroundElevationInfoAsString() % u"" % u"" % situationNew.getGroundElevation().valueRoundedWithUnit(ft, 1) % u" " % situationNew.getGroundElevationInfoAsString() % u"" % u"" % log.situationCurrent.getGroundElevation().valueRoundedWithUnit(ft, 1) % u" " % log.situationCurrent.getGroundElevationInfoAsString() % u"" % u"" % QString::number(log.groundFactor) % u"" % u"" % situationOld.getOnGroundInfo() % u"" % u"" % situationNew.getOnGroundInfo() % u"" % u"" % log.situationCurrent.getOnGroundInfo() % u"" % // tableRows += u"" % log.cgAboveGround.valueRoundedWithUnit(ft, 0) % u"" % u"" % boolToYesNo(log.useParts) % u"" % (changedParts ? u"*" : u"") % u"" % (!log.useParts || log.parts.isNull() ? QString() : log.parts.toQString(true).toHtmlEscaped()) % u"" % u"\n"; } tableRows += QStringLiteral("\n"); return u"\n" % tableHeader % tableRows % u"
\n"; } QString CInterpolationLogger::getKmlChangedSituations(const QList &logs) { if (logs.isEmpty()) { return {}; } QString kml; qint64 newPosTs = -1; int n = 1; const CKmlUtils::KMLSettings s(true, true); for (const SituationLog &log : logs) { const CAircraftSituation situationNew = log.newestInterpolationSituation(); const bool changedNewPosition = (newPosTs != situationNew.getMSecsSinceEpoch()); const bool recalc = log.interpolantRecalc; if (!changedNewPosition && !recalc) { continue; } newPosTs = situationNew.getMSecsSinceEpoch(); kml += CKmlUtils::asPlacemark( QStringLiteral("%1: %2 new pos: %3 recalc: %4").arg(n++).arg(situationNew.getFormattedUtcTimestampHmsz(), boolToYesNo(changedNewPosition), boolToYesNo(recalc)), situationNew.toQString(true), situationNew, s) % u"\n"; } return kml; } QString CInterpolationLogger::getKmlElevations(const QList &logs) { if (logs.isEmpty()) { return {}; } QString kml; qint64 newPosTs = -1; int n = 1; const CKmlUtils::KMLSettings s(true, true); for (const SituationLog &log : logs) { const CAircraftSituation situationNew = log.newestInterpolationSituation(); const bool changedNewPosition = (newPosTs != situationNew.getMSecsSinceEpoch()); const bool recalc = log.interpolantRecalc; if (!changedNewPosition && !recalc) { continue; } if (!situationNew.hasGroundElevation()) { continue; } newPosTs = situationNew.getMSecsSinceEpoch(); kml += CKmlUtils::asPlacemark( QStringLiteral("%1: %2 %3 info: %4 alt.cor: %5").arg(n++).arg( situationNew.getFormattedUtcTimestampHmsz(), situationNew.getGroundElevationAndInfo(), log.elevationInfo, log.altCorrection), situationNew.getGroundElevationPlane().toQString(true), situationNew.getGroundElevationPlane(), s) % u"\n"; } return kml; } QString CInterpolationLogger::getKmlInterpolatedSituations(const QList &logs) { if (logs.isEmpty()) { return {}; } const CKmlUtils::KMLSettings s(true, false); QString coordinates; for (const SituationLog &log : logs) { const CAircraftSituation situation = log.situationCurrent; coordinates += CKmlUtils::asRawCoordinates(situation, s.withAltitude) % u"\n"; } return u"\n" u"Interpolation " % QString::number(logs.size()) % u"entries\n" % CKmlUtils::asLineString(coordinates, s) % u"\n"; } QString CInterpolationLogger::getHtmlPartsLog(const QList &logs) { if (logs.isEmpty()) { return {}; } static const QString tableHeader = QStringLiteral( u"" u"CStimestamp" u"c." u"parts" u"\n"); CAircraftParts lastParts; // default, so shown if parts are different from default QString tableRows("\n"); for (const PartsLog &log : logs) { const bool changedParts = (lastParts != log.parts); lastParts = log.parts; tableRows += u"" % log.callsign.asString() % u"" % u"" % msSinceEpochToTime(log.tsCurrent) % u"" % (changedParts ? u"*" : u"") % u"" % (log.empty ? QStringLiteral("empty") : log.parts.toQString()) % u""; } tableRows += "\n"; return QStringLiteral("\n") % tableHeader % tableRows % QStringLiteral("
\n"); } void CInterpolationLogger::clearLog() { { QWriteLocker l(&m_lockSituations); m_situationLogs.clear(); } { QWriteLocker l(&m_lockParts); m_partsLogs.clear(); } } QString CInterpolationLogger::msSinceEpochToTime(qint64 ms) { static const QString dateFormat("hh:mm:ss.zzz"); return QDateTime::fromMSecsSinceEpoch(ms).toString(dateFormat); } QString CInterpolationLogger::msSinceEpochToTimeAndTimestamp(qint64 ms) { return CInterpolationLogger::msSinceEpochToTime(ms) % u"/" % QString::number(ms); } QString CInterpolationLogger::msSinceEpochToTime(qint64 t1, qint64 t2, qint64 t3) { if (t3 < 0) { return QStringLiteral("%1 %2").arg(msSinceEpochToTime(t1), msSinceEpochToTime(t2)); } return QStringLiteral("%1 %2 %3").arg(msSinceEpochToTime(t1), msSinceEpochToTime(t2), msSinceEpochToTime(t3)); } QString SituationLog::toQString( bool withSetup, bool withCurrentSituation, bool withElevation, bool withOtherPositions, bool withDeltaTimes, const QString &separator) const { const CAircraftSituation situationOldInterpolation = this->oldestInterpolationSituation(); const CAircraftSituation situationNewInterpolation = this->newestInterpolationSituation(); return ( withSetup ? u"setup: " % usedSetup.toQString(true) % separator : QString() ) % ( withElevation ? u"Elev.info: " % elevationInfo % u" scenery os: " % sceneryOffset.valueRoundedWithUnit(1) % separator : QString() ) % u"change: " % change.toQString(true) % separator % u"Interpolation CS: " % callsign.asString() % separator % u"ts: " % CInterpolationLogger::msSinceEpochToTimeAndTimestamp(tsCurrent) % u" | type: " % this->interpolationType() % u" | gnd.fa.: " % QString::number(groundFactor) % u" | CG: " % cgAboveGround.valueRoundedWithUnit(CLengthUnit::m(), 1) % u" " % cgAboveGround.valueRoundedWithUnit(CLengthUnit::ft(), 1) % u" | alt.cor.: " % altCorrection % u" | #nw.sit.: " % QString::number(noNetworkSituations) % u" | #invalid: " % QString::number(noInvalidSituations) % ( withDeltaTimes ? separator % u"cur.time: " % CInterpolationLogger::msSinceEpochToTimeAndTimestamp(tsCurrent) % u" | int.time: " % CInterpolationLogger::msSinceEpochToTimeAndTimestamp(tsInterpolated) % u" | dt.cur.int.: " % QString::number(deltaCurrentToInterpolatedTime()) % u"ms" % u" | sample dt: " % QString::number(deltaSampleTimesMs) % u"ms" % u" | fr.[0-1]: " % QString::number(simTimeFraction) % u" | old int.pos.: " % situationOldInterpolation.getTimestampAndOffset(true) % u" | new int.pos.: " % situationNewInterpolation.getTimestampAndOffset(true) % u" | #int.pos.: " % QString::number(interpolationSituations.size()) : QString() ) % ( withCurrentSituation ? separator % u"cur.sit.(interpolated): " % situationCurrent.toQString(true) : QString() ) % ( withOtherPositions ? separator % u"old: " % situationOldInterpolation.toQString(true) % separator % u"new: " % situationNewInterpolation.toQString(true) : QString() ); } QString PartsLog::toQString(const QString &separator) const { return u"CS: " % callsign.asString() % separator % u"ts: " % CInterpolationLogger::msSinceEpochToTimeAndTimestamp(tsCurrent) % u" | #nw.parts: " % QString::number(noNetworkParts) % separator % u"parts: " % parts.toQString(true); } const QString &SituationLog::interpolationType() const { static const QString s("spline"); static const QString l("linear"); static const QString u("unknown"); if (interpolator == 's') { return s; } if (interpolator == 'l') { return l; } return u; } #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wstrict-overflow" #endif const CAircraftSituation &SituationLog::secondInterpolationSituation() const { if (interpolationSituations.size() < 2) { return Aviation::CAircraftSituation::null(); } const Aviation::CAircraftSituationList::size_type i = interpolationSituations.size() - 2; // 2nd latest, latest at end return interpolationSituations[i]; } #ifdef __GNUC__ #pragma GCC diagnostic pop #endif } // namespace