From 8e6b633a19bb8c75ca081c1f0544d4534ae22d49 Mon Sep 17 00:00:00 2001 From: pixhawk Date: Sun, 15 Aug 2010 20:39:47 +0200 Subject: [PATCH] Added NEW data plot view, fixed zoom issue in linechart --- qgroundcontrol.pro | 11 +- src/LogCompressor.cc | 62 +++- src/LogCompressor.h | 9 +- src/ui/MainWindow.cc | 55 ++++ src/ui/MainWindow.h | 11 + src/ui/MainWindow.ui | 48 +++- src/ui/QGCDataPlot2D.cc | 429 ++++++++++++++++++++++++++++ src/ui/QGCDataPlot2D.h | 48 ++++ src/ui/QGCDataPlot2D.ui | 171 +++++++++++ src/ui/QGCParamWidget.cc | 2 +- src/ui/linechart/IncrementalPlot.cc | 277 ++++++++++++++++++ src/ui/linechart/IncrementalPlot.h | 68 +++++ src/ui/linechart/LinechartPlot.cc | 50 ++-- src/ui/linechart/LinechartPlot.h | 1 + src/ui/linechart/LinechartWidget.cc | 43 ++- testlog.txt | 15 + testlog2.txt | 59 ++++ 17 files changed, 1317 insertions(+), 42 deletions(-) create mode 100644 src/ui/QGCDataPlot2D.cc create mode 100644 src/ui/QGCDataPlot2D.h create mode 100644 src/ui/QGCDataPlot2D.ui create mode 100644 src/ui/linechart/IncrementalPlot.cc create mode 100644 src/ui/linechart/IncrementalPlot.h create mode 100644 testlog.txt create mode 100644 testlog2.txt diff --git a/qgroundcontrol.pro b/qgroundcontrol.pro index 252afdcc2..a7c721df2 100644 --- a/qgroundcontrol.pro +++ b/qgroundcontrol.pro @@ -73,7 +73,8 @@ FORMS += src/ui/MainWindow.ui \ src/ui/watchdog/WatchdogProcessView.ui \ src/ui/watchdog/WatchdogView.ui \ src/ui/QGCFirmwareUpdate.ui \ - src/ui/QGCPxImuFirmwareUpdate.ui + src/ui/QGCPxImuFirmwareUpdate.ui \ + src/ui/QGCDataPlot2D.ui INCLUDEPATH += src \ src/ui \ src/ui/linechart \ @@ -150,7 +151,9 @@ HEADERS += src/MG.h \ src/QGC.h \ src/ui/QGCFirmwareUpdate.h \ src/ui/QGCPxImuFirmwareUpdate.h \ - src/comm/MAVLinkLightProtocol.h + src/comm/MAVLinkLightProtocol.h \ + src/ui/QGCDataPlot2D.h \ + src/ui/linechart/IncrementalPlot.h SOURCES += src/main.cc \ src/Core.cc \ src/uas/UASManager.cc \ @@ -209,5 +212,7 @@ SOURCES += src/main.cc \ src/QGC.cc \ src/ui/QGCFirmwareUpdate.cc \ src/ui/QGCPxImuFirmwareUpdate.cc \ - src/comm/MAVLinkLightProtocol.cc + src/comm/MAVLinkLightProtocol.cc \ + src/ui/QGCDataPlot2D.cc \ + src/ui/linechart/IncrementalPlot.cc RESOURCES = mavground.qrc diff --git a/src/LogCompressor.cc b/src/LogCompressor.cc index 9b5dcc196..b78d1901b 100644 --- a/src/LogCompressor.cc +++ b/src/LogCompressor.cc @@ -1,12 +1,17 @@ #include #include #include +#include #include "LogCompressor.h" #include -LogCompressor::LogCompressor(QString logFileName, int uasid) : +LogCompressor::LogCompressor(QString logFileName, QString outFileName, int uasid) : logFileName(logFileName), + outFileName(outFileName), + running(true), + currentDataLine(0), + dataLines(1), uasid(uasid) { start(); @@ -17,13 +22,23 @@ void LogCompressor::run() QString separator = "\t"; QString fileName = logFileName; QFile file(fileName); + QFile outfile(outFileName); QStringList* keys = new QStringList(); QStringList* times = new QStringList(); if (!file.exists()) return; - if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return; + if (outFileName != "") + { + // Check if file is writeable + if (!QFileInfo(outfile).isWritable()) + { + return; + } + } + // Find all keys QTextStream in(&file); while (!in.atEnd()) { @@ -43,9 +58,9 @@ void LogCompressor::run() spacer += " " + separator; } - qDebug() << header; + //qDebug() << header; - qDebug() << "NOW READING TIMES"; + //qDebug() << "NOW READING TIMES"; // Find all times //in.reset(); @@ -62,6 +77,9 @@ void LogCompressor::run() times->append(time); } } + + dataLines = times->length(); + times->sort(); // Create lines @@ -74,7 +92,10 @@ void LogCompressor::run() // Fill in the values for all keys file.reset(); QTextStream data(&file); + int linecounter = 0; while (!data.atEnd()) { + linecounter++; + currentDataLine = linecounter; QString line = data.readLine(); QStringList parts = line.split(separator); // Get time @@ -100,20 +121,43 @@ void LogCompressor::run() // Add header, write out file file.close(); - QFile::remove(file.fileName()); - if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) + + if (outFileName == "") + { + QFile::remove(file.fileName()); + outfile.setFileName(file.fileName()); + } + if (!outfile.open(QIODevice::WriteOnly | QIODevice::Text)) return; - file.write(QString(QString("unix_timestamp") + separator + header.replace(" ", "_") + QString("\n")).toLatin1()); + outfile.write(QString(QString("unix_timestamp") + separator + header.replace(" ", "_") + QString("\n")).toLatin1()); //QString fileHeader = QString("unix_timestamp") + header.replace(" ", "_") + QString("\n"); - // Debug output + // File output for (int i = 0; i < outLines->length(); i++) { //qDebug() << outLines->at(i); - file.write(QString(outLines->at(i) + "\n").toLatin1()); + outfile.write(QString(outLines->at(i) + "\n").toLatin1()); } + currentDataLine = 0; + dataLines = 1; delete keys; qDebug() << "Done with logfile processing"; + running = false; +} + +bool LogCompressor::isFinished() +{ + return !running; +} + +int LogCompressor::getCurrentLine() +{ + return currentDataLine; +} + +int LogCompressor::getDataLines() +{ + return dataLines; } diff --git a/src/LogCompressor.h b/src/LogCompressor.h index 29d9681b2..e5ccdecd9 100644 --- a/src/LogCompressor.h +++ b/src/LogCompressor.h @@ -6,10 +6,17 @@ class LogCompressor : public QThread { public: - LogCompressor(QString logFileName, int uasid = 0); + LogCompressor(QString logFileName, QString outFileName="", int uasid = 0); + bool isFinished(); + int getDataLines(); + int getCurrentLine(); protected: void run(); QString logFileName; + QString outFileName; + bool running; + int currentDataLine; + int dataLines; int uasid; }; diff --git a/src/ui/MainWindow.cc b/src/ui/MainWindow.cc index c02f37139..e748dfb94 100644 --- a/src/ui/MainWindow.cc +++ b/src/ui/MainWindow.cc @@ -138,6 +138,7 @@ void MainWindow::buildWidgets() headDown1 = new HDDisplay(acceptList, this); headDown2 = new HDDisplay(acceptList2, this); joystick = new JoystickInput(); + dataplot = new QGCDataPlot2D(); } void MainWindow::connectWidgets() @@ -156,6 +157,7 @@ void MainWindow::arrangeCenterStack() centerStack->addWidget(protocol); centerStack->addWidget(map); centerStack->addWidget(hud); + centerStack->addWidget(dataplot); setCentralWidget(centerStack); } @@ -296,12 +298,59 @@ void MainWindow::connectActions() connect(ui.actionSettingsView, SIGNAL(triggered()), this, SLOT(loadSettingsView())); connect(ui.actionShow_full_view, SIGNAL(triggered()), this, SLOT(loadAllView())); connect(ui.actionShow_MAVLink_view, SIGNAL(triggered()), this, SLOT(loadMAVLinkView())); + connect(ui.actionShow_data_analysis_view, SIGNAL(triggered()), this, SLOT(loadDataView())); connect(ui.actionStyleConfig, SIGNAL(triggered()), this, SLOT(reloadStylesheet())); + connect(ui.actionOnline_documentation, SIGNAL(triggered()), this, SLOT(showHelp())); + connect(ui.actionCredits_Developers, SIGNAL(triggered()), this, SLOT(showCredits())); + connect(ui.actionProject_Roadmap, SIGNAL(triggered()), this, SLOT(showRoadMap())); + // Joystick configuration connect(ui.actionJoystickSettings, SIGNAL(triggered()), this, SLOT(configure())); } +void MainWindow::showHelp() +{ + if(!QDesktopServices::openUrl(QUrl("http://qgroundcontrol.org/user_guide"))) + { + QMessageBox msgBox; + msgBox.setIcon(QMessageBox::Critical); + msgBox.setText("Could not open help in browser"); + msgBox.setInformativeText("To get to the online help, please open http://qgroundcontrol.org/user_guide in a browser."); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setDefaultButton(QMessageBox::Ok); + msgBox.exec(); + } +} + +void MainWindow::showCredits() +{ + if(!QDesktopServices::openUrl(QUrl("http://qgroundcontrol.org/credits"))) + { + QMessageBox msgBox; + msgBox.setIcon(QMessageBox::Critical); + msgBox.setText("Could not open credits in browser"); + msgBox.setInformativeText("To get to the online help, please open http://qgroundcontrol.org/credits in a browser."); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setDefaultButton(QMessageBox::Ok); + msgBox.exec(); + } +} + +void MainWindow::showRoadMap() +{ + if(!QDesktopServices::openUrl(QUrl("http://qgroundcontrol.org/roadmap"))) + { + QMessageBox msgBox; + msgBox.setIcon(QMessageBox::Critical); + msgBox.setText("Could not open roadmap in browser"); + msgBox.setInformativeText("To get to the online help, please open http://qgroundcontrol.org/roadmap in a browser."); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setDefaultButton(QMessageBox::Ok); + msgBox.exec(); + } +} + void MainWindow::configure() { joystickWidget = new JoystickWidget(joystick, this); @@ -489,6 +538,12 @@ void MainWindow::loadPixhawkView() this->show(); } +void MainWindow::loadDataView() +{ + clearView(); + centerStack->setCurrentWidget(dataplot); +} + void MainWindow::loadPilotView() { clearView(); diff --git a/src/ui/MainWindow.h b/src/ui/MainWindow.h index d611fe095..1a698d247 100644 --- a/src/ui/MainWindow.h +++ b/src/ui/MainWindow.h @@ -62,6 +62,7 @@ This file is part of the PIXHAWK project #include "HDDisplay.h" #include "WatchdogControl.h" #include "HSIDisplay.h" +#include "QGCDataPlot2D.h" #include "LogCompressor.h" @@ -116,6 +117,15 @@ public slots: void loadAllView(); /** @brief Load MAVLink XML generator view */ void loadMAVLinkView(); + /** @brief Load data view, allowing to plot flight data */ + void loadDataView(); + + /** @brief Show the online help for users */ + void showHelp(); + /** @brief Show the authors / credits */ + void showCredits(); + /** @brief Show the project roadmap */ + void showRoadMap(); // Fixme find a nicer solution that scales to more AP types void loadSlugsView(); @@ -158,6 +168,7 @@ protected: HDDisplay* headDown2; WatchdogControl* watchdogControl; HSIDisplay* hsi; + QGCDataPlot2D* dataplot; // Popup widgets JoystickWidget* joystickWidget; diff --git a/src/ui/MainWindow.ui b/src/ui/MainWindow.ui index c39082db5..e047bd985 100644 --- a/src/ui/MainWindow.ui +++ b/src/ui/MainWindow.ui @@ -35,7 +35,7 @@ 0 0 1000 - 25 + 22 @@ -75,13 +75,23 @@ + + + + Help + + + + + + @@ -263,6 +273,42 @@ Show MAVLink view + + + + :/images/categories/applications-internet.svg:/images/categories/applications-internet.svg + + + Online documentation + + + + + + :/images/apps/utilities-system-monitor.svg:/images/apps/utilities-system-monitor.svg + + + Show data analysis view + + + + + + :/images/status/software-update-available.svg:/images/status/software-update-available.svg + + + Project Roadmap + + + + + + :/images/categories/preferences-system.svg:/images/categories/preferences-system.svg + + + Credits / Developers + + diff --git a/src/ui/QGCDataPlot2D.cc b/src/ui/QGCDataPlot2D.cc new file mode 100644 index 000000000..a9c614302 --- /dev/null +++ b/src/ui/QGCDataPlot2D.cc @@ -0,0 +1,429 @@ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "QGCDataPlot2D.h" +#include "ui_QGCDataPlot2D.h" +#include "MG.h" +#include + +#include + +QGCDataPlot2D::QGCDataPlot2D(QWidget *parent) : + QWidget(parent), + plot(new IncrementalPlot()), + logFile(NULL), + ui(new Ui::QGCDataPlot2D) +{ + ui->setupUi(this); + + // Add plot to ui + QHBoxLayout* layout = new QHBoxLayout(ui->plotFrame); + layout->addWidget(plot); + ui->plotFrame->setLayout(layout); + + // Connect user actions + connect(ui->selectFileButton, SIGNAL(clicked()), this, SLOT(selectFile())); + connect(ui->saveCsvButton, SIGNAL(clicked()), this, SLOT(saveCsvLog())); + connect(ui->reloadButton, SIGNAL(clicked()), this, SLOT(reloadFile())); + connect(ui->savePlotButton, SIGNAL(clicked()), this, SLOT(savePlot())); + connect(ui->printButton, SIGNAL(clicked()), this, SLOT(print())); +} + +void QGCDataPlot2D::reloadFile() +{ + if (QFileInfo(fileName).isReadable()) + { + if (ui->inputFileType->currentText().contains("pxIMU")) + { + loadRawLog(fileName, ui->xAxis->currentText(), ui->yAxis->text()); + } + else if (ui->inputFileType->currentText().contains("CSV")) + { + loadCsvLog(fileName, ui->xAxis->currentText(), ui->yAxis->text()); + } + } +} + +void QGCDataPlot2D::loadFile() +{ + if (QFileInfo(fileName).isReadable()) + { + if (ui->inputFileType->currentText().contains("pxIMU")) + { + loadRawLog(fileName); + } + else if (ui->inputFileType->currentText().contains("CSV")) + { + loadCsvLog(fileName); + } + } +} + +void QGCDataPlot2D::savePlot() +{ + QString fileName = "plot.svg"; + fileName = QFileDialog::getSaveFileName( + this, "Export File Name", QDesktopServices::storageLocation(QDesktopServices::DesktopLocation), + "SVG Documents (*.svg);;"); + while(!fileName.endsWith("svg")) + { + QMessageBox msgBox; + msgBox.setIcon(QMessageBox::Critical); + msgBox.setText("Unsuitable file extension for SVG"); + msgBox.setInformativeText("Please choose .svg as file extension. Click OK to change the file extension, cancel to not save the file."); + msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Ok); + if(msgBox.exec() == QMessageBox::Cancel) break; + fileName = QFileDialog::getSaveFileName( + this, "Export File Name", QDesktopServices::storageLocation(QDesktopServices::DesktopLocation), + "SVG Documents (*.svg);;"); + } + exportSVG(fileName); + + // else if (fileName.endsWith("pdf")) + // { + // print(fileName); + // } +} + + +void QGCDataPlot2D::print() +{ + QPrinter printer(QPrinter::HighResolution); + // printer.setOutputFormat(QPrinter::PdfFormat); + // //QPrinter printer(QPrinter::HighResolution); + // printer.setOutputFileName(fileName); + + QString docName = plot->title().text(); + if ( !docName.isEmpty() ) + { + docName.replace (QRegExp (QString::fromLatin1 ("\n")), tr (" -- ")); + printer.setDocName (docName); + } + + printer.setCreator("QGroundControl"); + printer.setOrientation(QPrinter::Landscape); + + QPrintDialog dialog(&printer); + if ( dialog.exec() ) + { + plot->setStyleSheet("QWidget { background-color: #FFFFFF; color: #000000; background-clip: border; font-size: 11pt;}"); + plot->setCanvasBackground(Qt::white); + QwtPlotPrintFilter filter; + filter.color(Qt::white, QwtPlotPrintFilter::CanvasBackground); + filter.color(Qt::black, QwtPlotPrintFilter::AxisScale); + filter.color(Qt::black, QwtPlotPrintFilter::AxisTitle); + filter.color(Qt::black, QwtPlotPrintFilter::MajorGrid); + filter.color(Qt::black, QwtPlotPrintFilter::MinorGrid); + if ( printer.colorMode() == QPrinter::GrayScale ) + { + int options = QwtPlotPrintFilter::PrintAll; + options &= ~QwtPlotPrintFilter::PrintBackground; + options |= QwtPlotPrintFilter::PrintFrameWithScales; + filter.setOptions(options); + } + plot->print(printer, filter); + } +} + +void QGCDataPlot2D::exportSVG(QString fileName) +{ + if ( !fileName.isEmpty() ) + { + QSvgGenerator generator; + generator.setFileName(fileName); + generator.setSize(QSize(800, 600)); + + QwtPlotPrintFilter filter; + filter.color(Qt::white, QwtPlotPrintFilter::CanvasBackground); + filter.color(Qt::black, QwtPlotPrintFilter::AxisScale); + filter.color(Qt::black, QwtPlotPrintFilter::AxisTitle); + filter.color(Qt::black, QwtPlotPrintFilter::MajorGrid); + filter.color(Qt::black, QwtPlotPrintFilter::MinorGrid); + + plot->print(generator, filter); + } +} + +/** + * Selects a filename and attempts immediately to load it. + */ +void QGCDataPlot2D::selectFile() +{ + // Let user select the log file name + //QDate date(QDate::currentDate()); + // QString("./pixhawk-log-" + date.toString("yyyy-MM-dd") + "-" + QString::number(logindex) + ".log") + fileName = QFileDialog::getOpenFileName(this, tr("Specify log file name"), tr("."), tr("Logfile (*.txt)")); + // Store reference to file + + QFileInfo fileInfo(fileName); + + if (!fileInfo.isReadable()) + { + QMessageBox msgBox; + msgBox.setIcon(QMessageBox::Critical); + msgBox.setText("Could not open file"); + msgBox.setInformativeText(tr("The file is owned by user %1. Is the file currently used by another program?").arg(fileInfo.owner())); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setDefaultButton(QMessageBox::Ok); + msgBox.exec(); + ui->filenameLabel->setText(tr("Could not open %1").arg(fileInfo.baseName()+"."+fileInfo.completeSuffix())); + } + else + { + ui->filenameLabel->setText(tr("Opened %1").arg(fileInfo.completeBaseName()+"."+fileInfo.completeSuffix())); + // Open and import the file + loadFile(); + } + +} + +void QGCDataPlot2D::loadRawLog(QString file, QString xAxisName, QString yAxisFilter) +{ + if (logFile != NULL) + { + logFile->close(); + delete logFile; + } + // Postprocess log file + logFile = new QTemporaryFile(); + compressor = new LogCompressor(file, logFile->fileName()); + + // Block UI + QProgressDialog progress("Transforming RAW log file to CSV", "Abort Transformation", 0, 1, this); + progress.setWindowModality(Qt::WindowModal); + + while (!compressor->isFinished()) + { + MG::SLEEP::usleep(100000); + progress.setMaximum(compressor->getDataLines()); + progress.setValue(compressor->getCurrentLine()); + } + // Enforce end + progress.setMaximum(compressor->getDataLines()); + progress.setValue(compressor->getDataLines()); + + // Done with preprocessing - now load csv log + loadCsvLog(logFile->fileName(), xAxisName, yAxisFilter); +} + +void QGCDataPlot2D::loadCsvLog(QString file, QString xAxisName, QString yAxisFilter) +{ + if (logFile != NULL) + { + logFile->close(); + delete logFile; + } + logFile = new QFile(file); + + // Load CSV data + if (!logFile->open(QIODevice::ReadOnly | QIODevice::Text)) + return; + + // Extract header + + // Read in values + // Find all keys + QTextStream in(logFile); + + // First line is header + QString header = in.readLine(); + QString separator = "\t"; + + qDebug() << "READING CSV:" << header; + + // Clear plot + plot->removeData(); + + QVector xValues; + QMap* > yValues; + + QStringList curveNames = header.split(separator); + QString curveName; + + // Clear UI elements + ui->xAxis->clear(); + ui->yAxis->clear(); + + ui->xAxis->addItem("*"); + int curveNameIndex = 0; + + int xValueIndex = curveNames.indexOf(xAxisName); + if (xValueIndex < 0 || xValueIndex > (curveNames.size() - 1)) xValueIndex = 0; + + foreach(curveName, curveNames) + { + if (curveNameIndex != xValueIndex) + { + // FIXME Add check for y value filter + yValues.insert(curveName, new QVector()); + ui->xAxis->addItem(curveName); + // Add separator starting with second item + if (curveNameIndex > 0 && curveNameIndex < curveNames.size()) + { + ui->yAxis->setText(ui->yAxis->text()+"|"); + } + ui->yAxis->setText(ui->yAxis->text()+curveName); + } + curveNameIndex++; + } + + // Read data + + double x,y; + + while (!in.atEnd()) { + QString line = in.readLine(); + + QStringList values = line.split(separator); + + bool okx; + + x = values.at(xValueIndex).toDouble(&okx); + + if(okx) + { + // Append X value + xValues.append(x); + QString yStr; + bool oky; + + int yCount = 0; + foreach(yStr, values) + { + // We have already x, so only handle + // true y values + if (yCount != xValueIndex && yCount < curveNames.size()) + { + y = yStr.toDouble(&oky); + // Append one of the Y values + yValues.value(curveNames.at(yCount))->append(y); + } + + yCount++; + } + } + } + + + QVector* yCurve; + int yCurveIndex = 0; + foreach(yCurve, yValues) + { + plot->appendData(yValues.keys().at(yCurveIndex), xValues.data(), yCurve->data(), xValues.size()); + yCurveIndex++; + } +} + +/** + * Linear regression (least squares) for n data points. + * Computes: + * + * y = a * x + b + * + * @param x values on x axis + * @param y corresponding values on y axis + * @param n Number of values + * @param a returned slope of line + * @param b y-axis intersection + * @param r regression coefficient. The larger the coefficient is, the better is + * the match of the regression. + * @return 1 on success, 0 on failure (e.g. because of infinite slope) + */ +int QGCDataPlot2D::linearRegression(double *x,double *y,int n,double *a,double *b,double *r) +{ + int i; + double sumx=0,sumy=0,sumx2=0,sumy2=0,sumxy=0; + double sxx,syy,sxy; + + *a = 0; + *b = 0; + *r = 0; + if (n < 2) + return(FALSE); + + /* Conpute some things we need */ + for (i=0;icopy(fileName); + + qDebug() << "Saved CSV log. Success: " << success; + + //qDebug() << "READE TO SAVE CSV LOG TO " << fileName; +} + +QGCDataPlot2D::~QGCDataPlot2D() +{ + delete ui; +} + +void QGCDataPlot2D::changeEvent(QEvent *e) +{ + QWidget::changeEvent(e); + switch (e->type()) { + case QEvent::LanguageChange: + ui->retranslateUi(this); + break; + default: + break; + } +} diff --git a/src/ui/QGCDataPlot2D.h b/src/ui/QGCDataPlot2D.h new file mode 100644 index 000000000..5c89ff071 --- /dev/null +++ b/src/ui/QGCDataPlot2D.h @@ -0,0 +1,48 @@ +#ifndef QGCDATAPLOT2D_H +#define QGCDATAPLOT2D_H + +#include +#include +#include "IncrementalPlot.h" +#include "LogCompressor.h" + +namespace Ui { + class QGCDataPlot2D; +} + +class QGCDataPlot2D : public QWidget { + Q_OBJECT +public: + QGCDataPlot2D(QWidget *parent = 0); + ~QGCDataPlot2D(); + + /** @brief Linear regression over data points */ + int linearRegression(double *x,double *y,int n,double *a,double *b,double *r); + +public slots: + void loadFile(); + /** @brief Reload a file, with filtering enabled */ + void reloadFile(); + void selectFile(); + void loadCsvLog(QString file, QString xAxisName="", QString yAxisFilter=""); + void loadRawLog(QString file, QString xAxisName="", QString yAxisFilter=""); + void saveCsvLog(); + /** @brief Save plot to PDF or SVG */ + void savePlot(); + /** @brief Export SVG file */ + void exportSVG(QString file); + /** @brief Print or save PDF file (MacOS/Linux) */ + void print(); + +protected: + void changeEvent(QEvent *e); + IncrementalPlot* plot; + LogCompressor* compressor; + QFile* logFile; + QString fileName; + +private: + Ui::QGCDataPlot2D *ui; +}; + +#endif // QGCDATAPLOT2D_H diff --git a/src/ui/QGCDataPlot2D.ui b/src/ui/QGCDataPlot2D.ui new file mode 100644 index 000000000..cad78db96 --- /dev/null +++ b/src/ui/QGCDataPlot2D.ui @@ -0,0 +1,171 @@ + + + QGCDataPlot2D + + + + 0 + 0 + 736 + 308 + + + + Form + + + + + + X + + + + + + + + + + Y + + + + + + + + + + + Style.. + + + + + Dots + + + + + Lines + + + + + + + + Reload + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Save Plot + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + File + + + + + + + + CSV + + + + + pxIMU + + + + + RAW + + + + + + + + Please select input file.. + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Print + + + + + + + Save CSV + + + + + + + Select file + + + + + + + Symmetric + + + + + + + + diff --git a/src/ui/QGCParamWidget.cc b/src/ui/QGCParamWidget.cc index e1f57d090..f46be42a5 100644 --- a/src/ui/QGCParamWidget.cc +++ b/src/ui/QGCParamWidget.cc @@ -405,7 +405,7 @@ void QGCParamWidget::loadParameters() changedValues.value(wpParams.at(1).toInt())->insert(wpParams.at(2), (float)wpParams.at(3).toDouble()); - qDebug() << "MARKING COMP" << wpParams.at(1).toInt() << "PARAM" << wpParams.at(2) << "VALUE" << (float)wpParams.at(3).toDouble() << "AS CHANGED"; + //qDebug() << "MARKING COMP" << wpParams.at(1).toInt() << "PARAM" << wpParams.at(2) << "VALUE" << (float)wpParams.at(3).toDouble() << "AS CHANGED"; // Mark in UI diff --git a/src/ui/linechart/IncrementalPlot.cc b/src/ui/linechart/IncrementalPlot.cc new file mode 100644 index 000000000..b5de68da3 --- /dev/null +++ b/src/ui/linechart/IncrementalPlot.cc @@ -0,0 +1,277 @@ +#include +#include +#include +#include +#include +#include +#include +#include "IncrementalPlot.h" +#include +#include +#include +#if QT_VERSION >= 0x040000 +#include +#endif + +CurveData::CurveData(): + d_count(0) +{ +} + +void CurveData::append(double *x, double *y, int count) +{ + int newSize = ( (d_count + count) / 1000 + 1 ) * 1000; + if ( newSize > size() ) + { + d_x.resize(newSize); + d_y.resize(newSize); + } + + for ( register int i = 0; i < count; i++ ) + { + d_x[d_count + i] = x[i]; + d_y[d_count + i] = y[i]; + } + d_count += count; +} + +int CurveData::count() const +{ + return d_count; +} + +int CurveData::size() const +{ + return d_x.size(); +} + +const double *CurveData::x() const +{ + return d_x.data(); +} + +const double *CurveData::y() const +{ + return d_y.data(); +} + +IncrementalPlot::IncrementalPlot(QWidget *parent): + QwtPlot(parent) +{ + setAutoReplot(false); + + setFrameStyle(QFrame::NoFrame); + setLineWidth(0); + setCanvasLineWidth(2); + + plotLayout()->setAlignCanvasToScales(true); + + QwtPlotGrid *grid = new QwtPlotGrid; + grid->setMajPen(QPen(Qt::gray, 0, Qt::DotLine)); + grid->attach(this); + + QwtLinearScaleEngine* yScaleEngine = new QwtLinearScaleEngine(); + setAxisScaleEngine(QwtPlot::yLeft, yScaleEngine); + + setAxisAutoScale(xBottom); + setAxisAutoScale(yLeft); + + resetScaling(); + + // enable zooming + + zoomer = new ScrollZoomer(canvas()); + zoomer->setRubberBandPen(QPen(Qt::red, 2, Qt::DotLine)); + zoomer->setTrackerPen(QPen(Qt::red)); + //zoomer->setZoomBase(QwtDoubleRect()); + + colors = QList(); + nextColor = 0; + + ///> Color map for plots, includes 20 colors + ///> Map will start from beginning when the first 20 colors are exceeded + colors.append(QColor(242,255,128)); + colors.append(QColor(70,80,242)); + colors.append(QColor(232,33,47)); + colors.append(QColor(116,251,110)); + colors.append(QColor(81,183,244)); + colors.append(QColor(234,38,107)); + colors.append(QColor(92,247,217)); + colors.append(QColor(151,59,239)); + colors.append(QColor(231,72,28)); + colors.append(QColor(236,48,221)); + colors.append(QColor(75,133,243)); + colors.append(QColor(203,254,121)); + colors.append(QColor(104,64,240)); + colors.append(QColor(200,54,238)); + colors.append(QColor(104,250,138)); + colors.append(QColor(235,43,165)); + colors.append(QColor(98,248,176)); + colors.append(QColor(161,252,116)); + colors.append(QColor(87,231,246)); + colors.append(QColor(230,126,23)); +} + +IncrementalPlot::~IncrementalPlot() +{ + +} + +void IncrementalPlot::resetScaling() +{ + xmin = 0; + xmax = 500; + ymin = 0; + ymax = 500; + + setAxisScale(xBottom, xmin+xmin*0.05, xmax+xmax*0.05); + setAxisScale(yLeft, ymin+ymin*0.05, ymax+ymax*0.05); + + replot(); + + // Make sure the first data access hits these + xmin = DBL_MAX; + xmax = DBL_MIN; + ymin = DBL_MAX; + ymax = DBL_MIN; +} + +void IncrementalPlot::appendData(QString key, double x, double y) +{ + appendData(key, &x, &y, 1); +} + +void IncrementalPlot::appendData(QString key, double *x, double *y, int size) +{ + CurveData* data; + QwtPlotCurve* curve; + if (!d_data.contains(key)) + { + data = new CurveData; + d_data.insert(key, data); + } + else + { + data = d_data.value(key); + } + + if (!d_curve.contains(key)) + { + curve = new QwtPlotCurve(key); + d_curve.insert(key, curve); + curve->setStyle(QwtPlotCurve::NoCurve); + curve->setPaintAttribute(QwtPlotCurve::PaintFiltered); + + const QColor &c = getNextColor(); + curve->setSymbol(QwtSymbol(QwtSymbol::XCross, + QBrush(c), QPen(c), QSize(5, 5)) ); + + curve->attach(this); + } + else + { + curve = d_curve.value(key); + } + + data->append(x, y, size); + curve->setRawData(data->x(), data->y(), data->count()); + + bool scaleChanged = false; + + // Update scales + for (int i = 0; i xmax) + { + xmax = x[i]; + scaleChanged = true; + } + + if (y[i] < ymin) + { + ymin = y[i]; + scaleChanged = true; + } + if (y[i] > ymax) + { + ymax = y[i]; + scaleChanged = true; + } + } + // setAxisScale(xBottom, xmin+xmin*0.05, xmax+xmax*0.05); + // setAxisScale(yLeft, ymin+ymin*0.05, ymax+ymax*0.05); + + //#ifdef __GNUC__ + //#warning better use QwtData + //#endif + + //replot(); + + if(scaleChanged) + { + setAxisScale(xBottom, xmin+xmin*0.05, xmax+xmax*0.05); + setAxisScale(yLeft, ymin+ymin*0.05, ymax+ymax*0.05); + zoomer->setZoomBase(true); + } + else + { + + const bool cacheMode = + canvas()->testPaintAttribute(QwtPlotCanvas::PaintCached); + +#if QT_VERSION >= 0x040000 && defined(Q_WS_X11) + // Even if not recommended by TrollTech, Qt::WA_PaintOutsidePaintEvent + // works on X11. This has an tremendous effect on the performance.. + + canvas()->setAttribute(Qt::WA_PaintOutsidePaintEvent, true); +#endif + + canvas()->setPaintAttribute(QwtPlotCanvas::PaintCached, false); + // FIXME Check if here all curves should be drawn +// QwtPlotCurve* plotCurve; +// foreach(plotCurve, d_curve) +// { +// plotCurve->draw(0, curve->dataSize()-1); +// } + + curve->draw(curve->dataSize() - size, curve->dataSize() - 1); + canvas()->setPaintAttribute(QwtPlotCanvas::PaintCached, cacheMode); + +#if QT_VERSION >= 0x040000 && defined(Q_WS_X11) + canvas()->setAttribute(Qt::WA_PaintOutsidePaintEvent, false); +#endif + + } + +} + +QList IncrementalPlot::getColorMap() +{ + return colors; +} + +QColor IncrementalPlot::getNextColor() +{ + /* Return current color and increment counter for next round */ + nextColor++; + if(nextColor >= colors.size()) nextColor = 0; + return colors[nextColor++]; +} + +QColor IncrementalPlot::getColorForCurve(QString id) +{ + return d_curve.value(id)->pen().color(); +} + +void IncrementalPlot::removeData() +{ + d_curve.clear(); + d_data.clear(); + resetScaling(); + replot(); +} diff --git a/src/ui/linechart/IncrementalPlot.h b/src/ui/linechart/IncrementalPlot.h new file mode 100644 index 000000000..14cd19465 --- /dev/null +++ b/src/ui/linechart/IncrementalPlot.h @@ -0,0 +1,68 @@ +#ifndef INCREMENTALPLOT_H +#define INCREMENTALPLOT_H + +#include +#include +#include +#include +#include "ScrollZoomer.h" + +class QwtPlotCurve; + +class CurveData +{ + // A container class for growing data +public: + + CurveData(); + + void append(double *x, double *y, int count); + + int count() const; + int size() const; + const double *x() const; + const double *y() const; + +private: + int d_count; + QwtArray d_x; + QwtArray d_y; + QTimer *d_timer; + int d_timerCount; +}; + +class IncrementalPlot : public QwtPlot +{ + Q_OBJECT +public: + IncrementalPlot(QWidget *parent = NULL); + virtual ~IncrementalPlot(); + + void appendData(QString key, double x, double y); + void appendData(QString key, double *x, double *y, int size); + + void resetScaling(); + void removeData(); + + /** @brief Get color map of this plot */ + QList IncrementalPlot::getColorMap(); + /** @brief Get next color of color map */ + QColor IncrementalPlot::getNextColor(); + /** @brief Get color for curve id */ + QColor IncrementalPlot::getColorForCurve(QString id); + +protected: + QList colors; + int nextColor; + ScrollZoomer* zoomer; + double xmin; + double xmax; + double ymin; + double ymax; + +private: + QMap d_data; + QMap d_curve; +}; + +#endif /* INCREMENTALPLOT_H */ diff --git a/src/ui/linechart/LinechartPlot.cc b/src/ui/linechart/LinechartPlot.cc index f63f4de17..96203ae3e 100644 --- a/src/ui/linechart/LinechartPlot.cc +++ b/src/ui/linechart/LinechartPlot.cc @@ -134,7 +134,8 @@ d_curve(NULL) setCanvasBackground(QColor(40, 40, 40)); // Enable zooming - Zoomer *zoomer = new Zoomer(canvas()); + //zoomer = new Zoomer(canvas()); + zoomer = new ScrollZoomer(canvas()); zoomer->setRubberBandPen(QPen(Qt::blue, 2, Qt::DotLine)); zoomer->setTrackerPen(QPen(Qt::blue)); @@ -581,16 +582,23 @@ void LinechartPlot::paintRealtime() { // Update plot window value to new max time if the last time was also the max time windowLock.lock(); - if (automaticScrollActive) { - if (MG::TIME::getGroundTimeNow() > maxTime && abs(MG::TIME::getGroundTimeNow() - maxTime) < 5000000) - { - plotPosition = MG::TIME::getGroundTimeNow(); - } - else - { + if (automaticScrollActive) + { + + // FIXME Check, but commenting this out should have been + // beneficial (does only add complexity) +// if (MG::TIME::getGroundTimeNow() > maxTime && abs(MG::TIME::getGroundTimeNow() - maxTime) < 5000000) +// { +// plotPosition = MG::TIME::getGroundTimeNow(); +// } +// else +// { plotPosition = maxTime;// + lastMaxTimeAdded.msec(); - } +// } setAxisScale(QwtPlot::xBottom, plotPosition - plotInterval, plotPosition, timeScaleStep); + + // FIXME Last fix for scroll zoomer is here + //setAxisScale(QwtPlot::yLeft, minValue + minValue * 0.05, maxValue + maxValue * 0.05f, (maxValue - minValue) / 10.0); /* Notify about change. Even if the window position was not changed * itself, the relative position of the window to the interval must * have changed, as the interval likely increased in length */ @@ -618,8 +626,17 @@ void LinechartPlot::paintRealtime() canvas()->setAttribute(Qt::WA_PaintOutsidePaintEvent, directPaint); #endif + // Only set current view as zoombase if zoomer is not active + // else we could not zoom out any more - replot(); + if(zoomer->zoomStack().size() < 2) + { + zoomer->setZoomBase(true); + } + else + { + replot(); + } #ifndef _WIN32 canvas()->setAttribute(Qt::WA_PaintOutsidePaintEvent, oldDirectPaint); @@ -726,10 +743,11 @@ void TimeSeriesData::append(quint64 ms, double value) { dataMutex.lock(); // Pre- allocate new space + // FIXME Check this for validity if(static_cast(size()) < (count + 100)) { - this->ms.resize(size() + 1000); - this->value.resize(size() + 1000); + this->ms.resize(size() + 10000); + this->value.resize(size() + 10000); } this->ms[count] = ms; this->value[count] = value; @@ -779,16 +797,16 @@ void TimeSeriesData::append(quint64 ms, double value) if(maxInterval > 0) { // maxInterval = 0 means infinite - if(interval >= maxInterval && !this->ms.isEmpty() && !this->value.isEmpty()) + if(interval > maxInterval && !this->ms.isEmpty() && !this->value.isEmpty()) { // The time at which this time series should be cut - quint64 minTime = stopTime - maxInterval; + double minTime = stopTime - maxInterval; // Delete elements from the start of the list as long the time // value of this elements is before the cut time while(this->ms.first() < minTime) { - this->ms.remove(0); - this->value.remove(0); + this->ms.pop_front(); + this->value.pop_front(); } } } diff --git a/src/ui/linechart/LinechartPlot.h b/src/ui/linechart/LinechartPlot.h index 0bf82f1bd..cf733e523 100644 --- a/src/ui/linechart/LinechartPlot.h +++ b/src/ui/linechart/LinechartPlot.h @@ -265,6 +265,7 @@ protected: QMap curves; QMap data; QMap scaleMaps; + ScrollZoomer* zoomer; QList colors; int nextColor; diff --git a/src/ui/linechart/LinechartWidget.cc b/src/ui/linechart/LinechartWidget.cc index c02f64f4b..d5ae0c6f7 100644 --- a/src/ui/linechart/LinechartWidget.cc +++ b/src/ui/linechart/LinechartWidget.cc @@ -42,6 +42,8 @@ This file is part of the PIXHAWK project #include #include #include +#include +#include #include "LinechartWidget.h" #include "LinechartPlot.h" @@ -259,20 +261,39 @@ void LinechartWidget::startLogging() // Let user select the log file name QDate date(QDate::currentDate()); // QString("./pixhawk-log-" + date.toString("yyyy-MM-dd") + "-" + QString::number(logindex) + ".log") - QString fileName = QFileDialog::getSaveFileName(this, tr("Specify log file name"), tr("."), tr("Logfile (*.txt)")); + QString fileName = QFileDialog::getSaveFileName(this, tr("Specify log file name"), QDesktopServices::storageLocation(QDesktopServices::DesktopLocation), tr("Logfile (*.txt, *.csv);;")); // Store reference to file - logFile = new QFile(fileName); - if (logFile->open(QIODevice::WriteOnly | QIODevice::Text)) + // Append correct file ending if needed + bool abort = false; + while (!(fileName.endsWith(".txt") || fileName.endsWith(".csv"))) { - logging = true; - logindex++; - logButton->setText(tr("Stop logging")); - disconnect(logButton, SIGNAL(clicked()), this, SLOT(startLogging())); - connect(logButton, SIGNAL(clicked()), this, SLOT(stopLogging())); + QMessageBox msgBox; + msgBox.setIcon(QMessageBox::Critical); + msgBox.setText("Unsuitable file extension for logfile"); + msgBox.setInformativeText("Please choose .txt or .csv as file extension. Click OK to change the file extension, cancel to not start logging."); + msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Ok); + if(msgBox.exec() == QMessageBox::Cancel) + { + abort = true; + break; + } + fileName = QFileDialog::getSaveFileName(this, tr("Specify log file name"), QDesktopServices::storageLocation(QDesktopServices::DesktopLocation), tr("Logfile (*.txt, *.csv);;")); + } - else + + // Check if the user did not abort the file save dialog + if (!abort && fileName != "") { - return; + logFile = new QFile(fileName); + if (logFile->open(QIODevice::WriteOnly | QIODevice::Text)) + { + logging = true; + logindex++; + logButton->setText(tr("Stop logging")); + disconnect(logButton, SIGNAL(clicked()), this, SLOT(startLogging())); + connect(logButton, SIGNAL(clicked()), this, SLOT(stopLogging())); + } } } @@ -418,7 +439,7 @@ void LinechartWidget::removeCurve(QString curve) Q_UNUSED(curve) //TODO @todo Ensure that the button for a curve gets deleted when the original curve is deleted // Remove name -} + } void LinechartWidget::setActive(bool active) { diff --git a/testlog.txt b/testlog.txt new file mode 100644 index 000000000..2c2f943fe --- /dev/null +++ b/testlog.txt @@ -0,0 +1,15 @@ +x y z +0 1 2 +1 2 3 +2 3 4 +3 4 5 +4 5 6 +5 10 15 +6 11 16 +7 12 19 +8 14 21 +9 15 21 +10 17 25 +200 200 200 +250 250 280 +300 320 340 \ No newline at end of file diff --git a/testlog2.txt b/testlog2.txt new file mode 100644 index 000000000..4bf04c5f7 --- /dev/null +++ b/testlog2.txt @@ -0,0 +1,59 @@ +unix_timestamp Accel._X Accel._Y Accel._Z Battery Bottom_Rotor CPU_Load Ground_Dist. Gyro_Phi Gyro_Psi Gyro_Theta Left_Servo Mag._X Mag._Y Mag._Z Pressure Right_Servo Temperature Top_Rotor pitch_IMU roll_IMU yaw_IMU +1270125571544 5 -197 982 0 29685 29866 29888 0 0 0 96186 354 -0.112812 0.13585 -1.94192 +1270125571564 6 -198 983 0 29781 29995 29871 0 0 0 96183 354 -0.112085 0.137193 -1.94139 +1270125571584 10 -197 982 0 29739 30018 29867 0 0 0 96171 354 -0.111693 0.136574 -1.94353 +1270125571604 7 -198 982 0 29802 29911 29803 0 0 0 96177 354 -0.112179 0.135778 -1.94617 +1270125571645 4 -197 984 0 29782 29844 29844 0 0 0 96180 354 -0.113056 0.135965 -1.94715 +1270125571665 4 -200 985 0 29685 29867 29884 0 0 0 96183 354 -0.113231 0.135327 -1.94586 +1270125571685 7 -198 983 0 29757 30018 29829 0 0 0 96177 354 -0.112604 0.136857 -1.94508 +1270125571706 6 -198 982 0 29759 29992 29867 0 0 0 96183 354 -0.1126 0.136602 -1.94915 +1270125571746 7 -196 985 0 29803 29910 29800 0 0 0 96183 354 -0.112392 0.136709 -1.95079 +1270125571766 4 -198 981 0 29750 29828 29888 0 0 0 96186 354 -0.113342 0.135532 -1.95064 +1270125571787 5 -197 984 0 29759 29931 29881 0 0 0 96174 354 -0.112628 0.135657 -1.9488 +1270125571807 10 -197 982 0 29739 29958 29819 0 0 0 96174 354 -0.112506 0.135777 -1.95001 +1270125571847 4 -198 981 0 29786 29829 29864 0 0 0 96232 356 -0.112359 0.135253 -1.94825 +1270125571867 5 -201 978 0 29719 29856 29890 0 0 0 96218 356 -0.11218 0.134669 -1.94657 +1270125571888 7 -198 986 0 29782 29813 29845 0 0 0 96223 356 -0.111451 0.135688 -1.9455 +1270125571908 10 -200 984 0 29693 30048 29885 0 0 0 96215 356 -0.110745 0.1365 -1.94668 +1270125571948 5 -197 982 0 29757 29899 29844 0 0 0 96229 356 -0.110004 0.135859 -1.94573 +1270125571969 6 -197 983 0 29727 29829 29814 0 0 0 96249 356 -0.110316 0.135751 -1.94565 +1270125571989 6 -197 978 0 29790 29828 29815 0 0 0 96220 356 -0.111159 0.136244 -1.94416 +1270125572009 5 -194 983 0 29718 29856 29890 0 0 0 96229 356 -0.111216 0.135992 -1.94196 +1270125572050 3 -196 985 0 29739 29978 29827 0 0 0 96223 356 -0.110727 0.136577 -1.94402 +1270125572070 8 -198 982 0 29782 29829 29815 0 0 0 96235 356 -0.111423 0.136697 -1.94566 +1270125572090 8 -195 982 0 29687 29865 29894 0 0 0 96218 356 -0.112088 0.136097 -1.9442 +1270125572130 8 -198 986 0 29793 29859 29844 0 0 0 96244 356 -0.110282 0.137491 -1.94235 +1270125572171 6 -198 982 0 29790 29856 29879 0 0 0 96244 356 -0.112179 0.136971 -1.94365 +1270125572191 3 -198 980 0 29759 30018 29829 0 0 0 96218 356 -0.111623 0.136194 -1.94286 +1270125572211 7 -198 984 0 29758 29829 29846 0 0 0 96229 356 -0.112275 0.136007 -1.94387 +1270125572252 8 -197 980 0 29743 30018 29824 0 0 0 96215 356 -0.111515 0.135539 -1.94281 +1270125572272 11 -195 984 0 29741 29867 29844 0 0 0 96249 356 -0.111995 0.135753 -1.94527 +1270125572292 4 -200 984 0 29771 29980 29865 0 0 0 96215 356 -0.111913 0.135829 -1.94455 +1270125572312 6 -198 983 0 29792 29837 29845 0 0 0 96229 356 -0.111884 0.134861 -1.94478 +1270125572353 5 -196 983 0 29685 29867 29884 0 0 0 96238 356 -0.111332 0.135644 -1.94784 +1270125572373 5 -198 982 0 29693 29852 29904 0 0 0 96238 356 -0.110772 0.136927 -1.94698 +1270125572393 6 -195 982 0 29738 29791 29818 0 0 0 96229 356 -0.109851 0.138106 -1.94592 +1270125572414 8 -198 986 0 29782 29844 29829 0 0 0 96223 356 -0.111077 0.137396 -1.9428 +1270125572454 11 -198 983 0 29743 29986 29833 0 0 0 96220 356 -0.110257 0.136744 -1.94155 +1270125572474 6 -198 983 0 29749 29866 29813 0 0 0 96223 356 -0.110497 0.136917 -1.94351 +1270125572495 6 -197 984 0 29719 29828 29890 0 0 0 96223 356 -0.111349 0.136959 -1.94295 +1270125572515 7 -197 983 0 29760 29791 29818 0 0 0 96232 356 -0.111323 0.137251 -1.93972 +1270125572555 10 -198 984 0 29800 30016 29857 0 0 0 96241 356 -0.110873 0.137041 -1.94196 +1270125572575 5 -200 982 0 29760 29962 29866 0 0 0 96232 356 -0.110648 0.136038 -1.9445 +1270125572596 4 -197 983 0 29740 30024 29867 0 0 0 96238 356 -0.110486 0.136046 -1.94576 +1270125572616 8 -196 985 0 29802 29783 29808 0 0 0 96235 356 -0.111227 0.135128 -1.94567 +1270125572656 5 -198 986 0 29771 29972 29867 0 0 0 96229 356 -0.111182 0.133833 -1.94823 +1270125572697 8 -196 980 0 29739 29978 29803 0 0 0 96238 356 -0.111457 0.133602 -1.94682 +1270125572717 4 -196 983 0 29749 30026 29867 0 0 0 96235 356 -0.116038 0.131922 -1.93861 +1270125592944 5 -195 985 0 29757 29994 29871 0 0 0 96220 356 -0.115848 0.1323 -1.93916 +1270125593025 5 -200 986 0 29780 29844 29845 0 0 0 96235 356 -0.115273 0.132222 -1.93576 +1270125593045 5 -197 982 0 29757 29995 29871 0 0 0 96229 356 -0.114713 0.132137 -1.93723 +1270125593086 4 -197 984 0 29802 29847 29819 0 0 0 96241 356 -0.114815 0.131529 -1.93626 +1270125593106 6 -202 984 0 29802 29783 29803 0 0 0 96247 356 -0.115379 0.130754 -1.93546 +1270125593126 8 -196 982 0 29803 29918 29793 0 0 0 96229 356 -0.116136 0.130336 -1.93339 +1270125593146 6 -196 987 0 29802 29825 29802 0 0 0 96244 356 -0.117635 0.128822 -1.93292 +1270125593187 7 -197 978 0 29802 29825 29864 0 0 0 96238 356 -0.117592 0.128252 -1.93468 +1270125593207 7 -197 980 0 29803 29918 29771 0 0 0 96235 356 -0.116961 0.127734 -1.93337 +1270125593227 6 -200 982 0 29733 29994 29867 0 0 0 96241 356 -0.11821 0.127208 -1.93399 +1270125593247 7 -194 984 0 29739 29982 29888 0 0 0 96235 356 -0.116428 0.128791 -1.9378 +1270125593288 10 -197 985 0 29771 30000 29865 0 0 0 96238 356 -0.116243 0.128666 -1.93672 \ No newline at end of file -- 2.22.0