9#include <QtConcurrent/QtConcurrent>
10#include <QtCore/QFileInfo>
11#include <QtCore/QFutureWatcher>
12#include <QtCore/QPointer>
24 const QString suffix = QFileInfo(filePath).suffix().toLower();
26 if (suffix == QStringLiteral(
"bin") || suffix == QStringLiteral(
"log")) {
30 if (suffix == QStringLiteral(
"ulg")) {
34 const QString fileTypeDescription = suffix.isEmpty()
35 ? LogFileParser::tr(
"no extension")
36 : QStringLiteral(
".%1").arg(suffix);
40 "Unsupported file type (%1) for file '%2'. Expected .bin, .log, or .ulg.")
41 .arg(fileTypeDescription, filePath);
54 qCDebug(LogFileParserLog) <<
this;
59 qCDebug(LogFileParserLog) <<
this;
72 qCDebug(LogFileParserLog) <<
"Parsed fields" << _availableFields.count()
73 <<
"parameters" << _parameters.count()
74 <<
"events" << _events.count();
82 _cancelToken = std::make_shared<std::atomic<bool>>(
false);
83 const quint64 requestId = ++_parseRequestId;
89 QPointer<LogFileParser> self(
this);
90 auto progressCallback = [self, requestId](
float v) {
92 QMetaObject::invokeMethod(self.data(), [self, requestId, v]() {
93 if (!self || requestId != self->_parseRequestId) return;
94 self->_parseProgress = v;
95 emit self->parseProgressChanged();
96 }, Qt::QueuedConnection);
99 auto *watcher =
new QFutureWatcher<LogParseResult>(
this);
100 (void) connect(watcher, &QFutureWatcher<LogParseResult>::finished,
this,
101 [
this, watcher, filePath, requestId]() {
103 watcher->deleteLater();
105 if (requestId != _parseRequestId) {
109 _parseProgress = 1.f;
120 _applyResult(result);
124 watcher->setFuture(QtConcurrent::run([filePath, progressCallback, cancelToken = _cancelToken]() {
125 return _parseFile(filePath, progressCallback, cancelToken);
137 if (!_parameters.isEmpty()) {
140#ifndef QGC_NO_ARDUPILOT_DIALECT
156 _modeColorCache.clear();
158 for (
const QVariant &v : _modeSegments) {
159 const QString mode = v.toMap().value(QStringLiteral(
"mode")).toString();
160 if (!mode.isEmpty() && !_modeNames.contains(mode)) {
161 _modeColorCache.insert(mode, _modeColorCache.size());
162 _modeNames.append(mode);
188 _parseComplete =
true;
197 _cancelToken->store(
true, std::memory_order_relaxed);
203 const bool oldParseComplete = _parseComplete;
204 _parseComplete =
false;
210 if (!_events.isEmpty()) { _events.clear(); emit
eventsChanged(); }
211 if (!_messages.isEmpty()) { _messages.clear(); emit
messagesChanged(); }
213 if (!_modeNames.isEmpty()) { _modeNames.clear(); _modeColorCache.clear(); emit
modeNamesChanged(); }
214 if (!_dropouts.isEmpty()) { _dropouts.clear(); emit
dropoutsChanged(); }
218 _fieldSamples.clear();
219 _gpsLatField.clear();
220 _gpsLonField.clear();
221 _gpsAltField.clear();
222 if (_minTimestamp != -1.0 || _maxTimestamp != -1.0) {
223 _minTimestamp = -1.0;
224 _maxTimestamp = -1.0;
228 if (!_startTime.isNull()) { _startTime = QDateTime(); emit
startTimeChanged(); }
235 const auto it = _fieldSamples.constFind(fieldName);
236 if (it == _fieldSamples.cend()) {
return output; }
237 const QVector<QPointF> &points = it.value();
238 output.reserve(points.size());
239 for (
const QPointF &p : points) { output.append(p); }
245 const auto it = _fieldSamples.constFind(fieldName);
246 if (it == _fieldSamples.cend() || it->isEmpty()) {
return {}; }
247 const QVector<QPointF> &points = it.value();
248 double minY = std::numeric_limits<double>::max();
249 double maxY = std::numeric_limits<double>::lowest();
250 for (
const QPointF &p : points) {
251 if (p.y() < minY) minY = p.y();
252 if (p.y() > maxY) maxY = p.y();
254 return QVariantMap{{QStringLiteral(
"min"), minY}, {QStringLiteral(
"max"), maxY}};
260 const auto it = _fieldSamples.constFind(fieldName);
261 if (it == _fieldSamples.cend() || pixelWidth <= 0 || maxX <= minX) {
return output; }
263 const QVector<QPointF> &points = it.value();
266 const auto sliceBegin = std::lower_bound(points.cbegin(), points.cend(), minX,
267 [](
const QPointF &p,
double t) { return p.x() < t; });
268 const auto sliceEnd = std::upper_bound(sliceBegin, points.cend(), maxX,
269 [](
double t,
const QPointF &p) { return t < p.x(); });
271 const qsizetype sliceCount = std::distance(sliceBegin, sliceEnd);
272 if (sliceCount == 0) {
return output; }
275 if (sliceCount <= 4 * pixelWidth) {
276 output.reserve(sliceCount);
277 for (
auto jt = sliceBegin; jt != sliceEnd; ++jt) { output.append(*jt); }
283 output.reserve(4 * pixelWidth);
284 const double range = maxX - minX;
286 auto columnOf = [&](
double x) ->
int {
287 return static_cast<int>((x - minX) / range * pixelWidth);
292 qsizetype firstIdx = -1;
293 qsizetype minIdx = -1;
294 qsizetype maxIdx = -1;
295 qsizetype lastIdx = -1;
298 if (firstIdx < 0)
return;
300 qsizetype indices[4] = { firstIdx, minIdx, maxIdx, lastIdx };
301 std::sort(indices, indices + 4);
303 for (qsizetype idx : indices) {
305 output.append(*(sliceBegin + idx));
311 for (qsizetype i = 0; i < sliceCount; ++i) {
312 const QPointF &p = *(sliceBegin + i);
313 const int col = std::clamp(columnOf(p.x()), 0, pixelWidth - 1);
321 if (p.y() < (sliceBegin + minIdx)->y()) minIdx = i;
322 if (p.y() > (sliceBegin + maxIdx)->y()) maxIdx = i;
333 const auto it = _fieldSamples.constFind(fieldName);
334 if (it == _fieldSamples.cend() || it->isEmpty()) {
335 return std::numeric_limits<double>::quiet_NaN();
337 const QVector<QPointF> &points = it.value();
338 const auto lower = std::lower_bound(points.cbegin(), points.cend(), timestampSeconds,
339 [](
const QPointF &p,
double t) { return p.x() < t; });
341 if (lower == points.cbegin()) {
return lower->y(); }
342 if (lower == points.cend()) {
return points.constLast().y(); }
344 const auto prev = std::prev(lower);
345 return (std::fabs(prev->x() - timestampSeconds) <= std::fabs(lower->x() - timestampSeconds))
346 ? prev->y() : lower->y();
351 static const QStringList modePalette = {
352 QStringLiteral(
"#E53935"),
353 QStringLiteral(
"#FB8C00"),
354 QStringLiteral(
"#FDD835"),
355 QStringLiteral(
"#43A047"),
356 QStringLiteral(
"#00897B"),
357 QStringLiteral(
"#00ACC1"),
358 QStringLiteral(
"#1E88E5"),
359 QStringLiteral(
"#5E35B1"),
360 QStringLiteral(
"#8E24AA"),
361 QStringLiteral(
"#D81B60"),
362 QStringLiteral(
"#6D4C41"),
363 QStringLiteral(
"#546E7A"),
366 const auto it = _modeColorCache.constFind(modeName);
367 if (it == _modeColorCache.constEnd()) {
368 return modePalette[0];
370 return modePalette[it.value() % modePalette.size()];
375 for (
const QVariant &v : _modeSegments) {
376 const QVariantMap seg = v.toMap();
377 const double start = seg.value(QStringLiteral(
"start")).toDouble();
378 const double end = seg.value(QStringLiteral(
"end")).toDouble();
379 if (timestampSeconds >= start && timestampSeconds <= end) {
380 return seg.value(QStringLiteral(
"mode")).toString();
388 QVariantList matches;
389 const double threshold = std::max(0.0, thresholdSeconds);
390 const auto lower = std::lower_bound(_events.cbegin(), _events.cend(),
391 timestampSeconds - threshold,
392 [](
const QVariant &v,
double t) {
393 return v.toMap().value(QStringLiteral(
"time")).toDouble() < t;
395 for (
auto it = lower; it != _events.cend(); ++it) {
396 const QVariantMap ev = it->toMap();
397 if (ev.value(QStringLiteral(
"time")).toDouble() > timestampSeconds + threshold) {
break; }
403void LogFileParser::_setParseError(
const QString &
error)
405 if (_parseError !=
error) {
418 if (_gpsLatField.isEmpty() || _gpsLonField.isEmpty()) {
422 const auto latIt = _fieldSamples.constFind(_gpsLatField);
423 const auto lonIt = _fieldSamples.constFind(_gpsLonField);
424 if (latIt == _fieldSamples.cend() || lonIt == _fieldSamples.cend()) {
428 const QVector<QPointF> &latPts = latIt.value();
429 const QVector<QPointF> &lonPts = lonIt.value();
430 if (latPts.isEmpty() || lonPts.isEmpty()) {
436 int hi = latPts.size() - 1;
438 const int mid = (lo + hi) / 2;
439 if (latPts[mid].x() < timestampSeconds) {
447 const double dPrev = timestampSeconds - latPts[lo - 1].x();
448 const double dCurr = latPts[lo].x() - timestampSeconds;
454 const int lonIdx = std::min(lo,
static_cast<int>(lonPts.size()) - 1);
456 coord[QStringLiteral(
"latitude")] = latPts[lo].y();
457 coord[QStringLiteral(
"longitude")] = lonPts[lonIdx].y();
467 struct CandidatePair {
468 const char *latField;
469 const char *lonField;
470 const char *altField;
471 const char *statusField;
472 double statusMinValue;
475 static const CandidatePair candidates[] = {
477 {
"vehicle_global_position.lat",
"vehicle_global_position.lon",
"vehicle_global_position.alt",
nullptr, 0 },
478 {
"vehicle_global_position.latitude_deg",
"vehicle_global_position.longitude_deg",
"vehicle_global_position.alt",
nullptr, 0 },
480 {
"vehicle_gps_position.latitude_deg",
"vehicle_gps_position.longitude_deg",
"vehicle_gps_position.altitude_msl_m",
nullptr, 0 },
481 {
"vehicle_gps_position[0].latitude_deg",
"vehicle_gps_position[0].longitude_deg",
"vehicle_gps_position[0].altitude_msl_m",
nullptr, 0 },
482 {
"sensor_gps.latitude_deg",
"sensor_gps.longitude_deg",
"sensor_gps.altitude_msl_m",
nullptr, 0 },
483 {
"sensor_gps[0].latitude_deg",
"sensor_gps[0].longitude_deg",
"sensor_gps[0].altitude_msl_m",
nullptr, 0 },
485 {
"GPS.Lat",
"GPS.Lng",
"GPS.Alt",
"GPS.Status", 3 },
486 {
"GPS2.Lat",
"GPS2.Lng",
"GPS2.Alt",
"GPS2.Status", 3 },
488 {
"POS.Lat",
"POS.Lng",
"POS.Alt",
nullptr, 0 },
491 for (
const auto &c : candidates) {
492 const auto latIt = _fieldSamples.constFind(QLatin1String(c.latField));
493 const auto lonIt = _fieldSamples.constFind(QLatin1String(c.lonField));
494 if (latIt == _fieldSamples.cend() || lonIt == _fieldSamples.cend()) {
498 const QVector<QPointF> &latPts = latIt.value();
499 const QVector<QPointF> &lonPts = lonIt.value();
500 if (latPts.isEmpty() || lonPts.isEmpty()) {
505 const QVector<QPointF> *statusPts =
nullptr;
507 const auto statusIt = _fieldSamples.constFind(QLatin1String(c.statusField));
508 if (statusIt != _fieldSamples.cend() && !statusIt.value().isEmpty()) {
509 statusPts = &statusIt.value();
513 qCDebug(LogFileParserLog) <<
"gpsPath: found candidate" << c.latField
514 <<
"samples:" << latPts.size()
515 <<
"first lat:" << latPts.first().y()
516 <<
"first lon:" << lonPts.first().y();
519 const int n = std::min(latPts.size(), lonPts.size());
522 for (
int i = 0; i < n; i++) {
524 if (statusPts && i < statusPts->size() && (*statusPts)[i].y() < c.statusMinValue) {
528 const double lat = latPts[i].y();
529 const double lon = lonPts[i].y();
531 if (lat < -90.0 || lat > 90.0 || lon < -180.0 || lon > 180.0
532 || (qFuzzyIsNull(lat) && qFuzzyIsNull(lon))) {
536 coord[QStringLiteral(
"latitude")] = lat;
537 coord[QStringLiteral(
"longitude")] = lon;
541 qCDebug(LogFileParserLog) <<
"gpsPath: valid points after filter:" << path.size();
543 if (!path.isEmpty()) {
544 _gpsLatField = QLatin1String(c.latField);
545 _gpsLonField = QLatin1String(c.lonField);
548 const QLatin1String altField(c.altField);
549 const auto altIt = _fieldSamples.constFind(altField);
550 _gpsAltField = (altIt != _fieldSamples.cend() && !altIt.value().isEmpty()) ? altField : QLatin1String{};
554 qCDebug(LogFileParserLog) <<
"gpsPath: all" << n <<
"points filtered out for candidate" << c.latField;
557 qCDebug(LogFileParserLog) <<
"gpsPath: no GPS data found; available fields containing 'lat' or 'lon':";
558 for (
auto it = _fieldSamples.cbegin(); it != _fieldSamples.cend(); ++it) {
559 const QString &fn = it.key();
560 if (fn.contains(QLatin1String(
"lat"), Qt::CaseInsensitive) || fn.contains(QLatin1String(
"lon"), Qt::CaseInsensitive)) {
561 qCDebug(LogFileParserLog) <<
" " << fn <<
"samples:" << it.value().size()
562 << (it.value().isEmpty() ? 0.0 : it.value().first().y());
std::function< void(float)> ProgressCallback
std::shared_ptr< std::atomic< bool > > CancelToken
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Q_INVOKABLE QVariantList eventsNear(double timestampSeconds, double thresholdSeconds) const
Q_INVOKABLE double fieldValueAt(const QString &fieldName, double timestampSeconds) const
Q_INVOKABLE QVariantList fieldSamplesFiltered(const QString &fieldName, double minX, double maxX, int pixelWidth) const
Q_INVOKABLE QVariantMap fieldMinMax(const QString &fieldName) const
Q_INVOKABLE QVariantList fieldSamples(const QString &fieldName) const
void parseProgressChanged()
Q_INVOKABLE void startParsingAsync(const QString &filePath)
void detectedVehicleTypeChanged()
void sampleCountChanged()
LogFileParser(QObject *parent=nullptr)
void plottableFieldsChanged()
void availableFieldsChanged()
void modeSegmentsChanged()
void parseCompleteChanged()
Q_INVOKABLE QVariantMap gpsCoordAt(double timestampSeconds) const
Q_INVOKABLE QString modeAt(double timestampSeconds) const
Q_INVOKABLE QString gpsAltitudeFieldName() const
Q_INVOKABLE QVariantList gpsPath() const
void parseFileFinished(const QString &filePath, bool ok, const QString &errorMessage)
Q_INVOKABLE bool parseFile(const QString &filePath)
Q_INVOKABLE QString modeColor(const QString &modeName) const
LogParseResult parseFile(const QString &filePath, const ProgressCallback &progressCallback, const CancelToken &cancelToken)
LogParseResult parseFile(const QString &filePath, const ProgressCallback &progressCallback, const CancelToken &cancelToken)
QString detectedVehicleType
QVariantList modeSegments
QStringList plottableFields
QStringList availableFields
QHash< QString, QVector< QPointF > > fieldSamples