/*===================================================================== QGroundControl Open Source Ground Control Station (c) 2009, 2010 QGROUNDCONTROL PROJECT This file is part of the QGROUNDCONTROL project QGROUNDCONTROL is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. QGROUNDCONTROL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with QGROUNDCONTROL. If not, see . ======================================================================*/ /** * @file * @brief Implementation of QGCDataPlot2D * @author Lorenz Meier * */ #include #include #include #include #include #include #include #include #include #include "QGCDataPlot2D.h" #include "ui_QGCDataPlot2D.h" #include "MG.h" #include "QGCFileDialog.h" #include "QGCMessageBox.h" QGCDataPlot2D::QGCDataPlot2D(QWidget *parent) : QWidget(parent), plot(new IncrementalPlot(parent)), 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); ui->gridCheckBox->setChecked(plot->gridEnabled()); // 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())); connect(ui->legendCheckBox, SIGNAL(clicked(bool)), plot, SLOT(showLegend(bool))); connect(ui->symmetricCheckBox, SIGNAL(clicked(bool)), plot, SLOT(setSymmetric(bool))); connect(ui->gridCheckBox, SIGNAL(clicked(bool)), plot, SLOT(showGrid(bool))); connect(ui->regressionButton, SIGNAL(clicked()), this, SLOT(calculateRegression())); connect(ui->style, SIGNAL(currentIndexChanged(QString)), plot, SLOT(setStyleText(QString))); // Allow style changes to propagate through this widget connect(qgcApp(), &QGCApplication::styleChanged, plot, &IncrementalPlot::styleChanged); } void QGCDataPlot2D::reloadFile() { if (QFileInfo(fileName).isReadable()) { if (ui->inputFileType->currentText().contains("pxIMU") || ui->inputFileType->currentText().contains("RAW")) { 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() { qDebug() << "DATA PLOT: Loading file:" << fileName; if (QFileInfo(fileName).isReadable()) { if (ui->inputFileType->currentText().contains("pxIMU") || ui->inputFileType->currentText().contains("RAW")) { loadRawLog(fileName); } else if (ui->inputFileType->currentText().contains("CSV")) { loadCsvLog(fileName); } } } void QGCDataPlot2D::loadFile(QString file) { fileName = file; if (QFileInfo(fileName).isReadable()) { if (fileName.contains(".raw") || fileName.contains(".imu")) { loadRawLog(fileName); } else if (fileName.contains(".txt") || fileName.contains(".csv") || fileName.contains(".csv")) { loadCsvLog(fileName); } } } /** * This function brings up a file name dialog and asks the user to enter a file to save to */ QString QGCDataPlot2D::getSavePlotFilename() { QString defaultSuffix("pdf"); QString fileName = QGCFileDialog::getSaveFileName( this, "Export File Name", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation), "PDF Documents (*.pdf);;SVG Images (*.svg)", 0,0, &defaultSuffix); return fileName; } /** * This function aks the user for a filename and exports to either PDF or SVG, depending on the filename */ void QGCDataPlot2D::savePlot() { QString fileName = getSavePlotFilename(); if (fileName.isEmpty()) return; while(!(fileName.endsWith(".svg") || fileName.endsWith(".pdf"))) { QMessageBox::StandardButton button = QGCMessageBox::critical(tr("Unsuitable file extension for PDF or SVG"), tr("Please choose .pdf or .svg as file extension. Click OK to change the file extension, cancel to not save the file."), QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok); // Abort if cancelled if (button == QMessageBox::Cancel) { return; } fileName = getSavePlotFilename(); if (fileName.isEmpty()) return; //Abort if cancelled } if (fileName.endsWith(".pdf")) { exportPDF(fileName); } else if (fileName.endsWith(".svg")) { exportSVG(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: 10pt;}"); plot->setCanvasBackground(Qt::white); // FIXME: QwtPlotPrintFilter no longer exists in Qwt 6.1 //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); plot->setStyleSheet("QWidget { background-color: #050508; color: #DDDDDF; background-clip: border; font-size: 11pt;}"); //plot->setCanvasBackground(QColor(5, 5, 8)); } } void QGCDataPlot2D::exportPDF(QString fileName) { QPrinter printer; printer.setOutputFormat(QPrinter::PdfFormat); printer.setOutputFileName(fileName); //printer.setFullPage(true); printer.setPageMargins(10.0, 10.0, 10.0, 10.0, QPrinter::Millimeter); printer.setPageSize(QPrinter::A4); 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); plot->setStyleSheet("QWidget { background-color: #FFFFFF; color: #000000; background-clip: border; font-size: 10pt;}"); // plot->setCanvasBackground(Qt::white); // FIXME: QwtPlotPrintFilter no longer exists in Qwt 6.1 // 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); plot->setStyleSheet("QWidget { background-color: #050508; color: #DDDDDF; background-clip: border; font-size: 11pt;}"); //plot->setCanvasBackground(QColor(5, 5, 8)); } void QGCDataPlot2D::exportSVG(QString fileName) { if ( !fileName.isEmpty() ) { plot->setStyleSheet("QWidget { background-color: #FFFFFF; color: #000000; background-clip: border; font-size: 10pt;}"); //plot->setCanvasBackground(Qt::white); QSvgGenerator generator; generator.setFileName(fileName); generator.setSize(QSize(800, 600)); // FIXME: QwtPlotPrintFilter no longer exists in Qwt 6.1 //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); plot->setStyleSheet("QWidget { background-color: #050508; color: #DDDDDF; background-clip: border; font-size: 11pt;}"); } } /** * Selects a filename and attempts immediately to load it. */ void QGCDataPlot2D::selectFile() { // Open a file dialog prompting the user for the file to load. // Note the special case for the Pixhawk. if (ui->inputFileType->currentText().contains("pxIMU") || ui->inputFileType->currentText().contains("RAW")) { fileName = QGCFileDialog::getOpenFileName(this, tr("Specify log file name"), QString(), "Logfile (*.imu *.raw)"); } else { fileName = QGCFileDialog::getOpenFileName(this, tr("Specify log file name"), QString(), "Logfile (*.csv *.txt *.log)"); } // Check if the user hit cancel, which results in a Null string. // If this is the case, we just stop. if (fileName.isNull()) { return; } // Now attempt to open the file QFileInfo fileInfo(fileName); if (!fileInfo.isReadable()) { QGCMessageBox::critical(tr("Could not open file"), tr("The file is owned by user %1. Is the file currently used by another program?").arg(fileInfo.owner())); 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) { Q_UNUSED(xAxisName); Q_UNUSED(yAxisFilter); if (logFile != NULL) { logFile->close(); delete logFile; } // Postprocess log file logFile = new QTemporaryFile("qt_qgc_temp_log.XXXXXX.csv"); compressor = new LogCompressor(file, logFile->fileName()); connect(compressor, SIGNAL(finishedFile(QString)), this, SLOT(loadFile(QString))); compressor->startCompression(); } /** * This function loads a CSV file into the plot. It tries to assign the dimension names * based on the first data row and tries to guess the separator char. * * @param file Name of the file to open * @param xAxisName Optional paramater. If given, the x axis dimension will be selected to match this string * @param yAxisFilter Optional parameter. If given, only data dimension names present in the filter string will be * plotted * * @code * * QString file = "/home/user/datalog.txt"; // With header: xyz * QString xAxis = "x"; * QString yAxis = "z"; * * // Plotted result will be x vs z with y ignored. * @endcode */ void QGCDataPlot2D::loadCsvLog(QString file, QString xAxisName, QString yAxisFilter) { if (logFile != NULL) { logFile->close(); delete logFile; curveNames.clear(); } logFile = new QFile(file); // Load CSV data if (!logFile->open(QIODevice::ReadOnly | QIODevice::Text)) return; // Set plot title if (ui->plotTitle->text() != "") plot->setTitle(ui->plotTitle->text()); if (ui->plotXAxisLabel->text() != "") plot->setAxisTitle(QwtPlot::xBottom, ui->plotXAxisLabel->text()); if (ui->plotYAxisLabel->text() != "") plot->setAxisTitle(QwtPlot::yLeft, ui->plotYAxisLabel->text()); // Extract header // Read in values // Find all keys QTextStream in(logFile); // First line is header QString header = in.readLine(); bool charRead = false; QString separator = ""; QList sepCandidates; sepCandidates << '\t'; sepCandidates << ','; sepCandidates << ';'; sepCandidates << ' '; sepCandidates << '~'; sepCandidates << '|'; // Iterate until separator is found // or full header is parsed for (int i = 0; i < header.length(); i++) { if (sepCandidates.contains(header.at(i))) { // Separator found if (charRead) { separator += header[i]; } } else { // Char found charRead = true; // If the separator is not empty, this char // has been read after a separator, so detection // is now complete if (separator != "") break; } } QString out = separator; out.replace("\t", ""); ui->filenameLabel->setText(file.split("/").last().split("\\").last()+" Separator: \""+out+"\""); //qDebug() << "READING CSV:" << header; // Clear plot plot->removeData(); QMap* > xValues; QMap* > yValues; curveNames.append(header.split(separator, QString::SkipEmptyParts)); // Eliminate any non-string curve names for (int i = 0; i < curveNames.count(); ++i) { if (curveNames.at(i).length() == 0 || curveNames.at(i) == " " || curveNames.at(i) == "\n" || curveNames.at(i) == "\t" || curveNames.at(i) == "\r") { // Remove bogus curve name curveNames.removeAt(i); } } QString curveName; // Clear UI elements ui->xAxis->clear(); ui->yAxis->clear(); ui->xRegressionComboBox->clear(); ui->yRegressionComboBox->clear(); ui->regressionOutput->clear(); int curveNameIndex = 0; QString xAxisFilter; if (xAxisName == "") { xAxisFilter = curveNames.first(); } else { xAxisFilter = xAxisName; } // Fill y-axis renaming lookup table // Allow the user to rename data dimensions in the plot QMap renaming; QStringList yCurves = yAxisFilter.split("|", QString::SkipEmptyParts); // Figure out the correct renaming for (int i = 0; i < yCurves.count(); ++i) { if (yCurves.at(i).contains(":")) { QStringList parts = yCurves.at(i).split(":", QString::SkipEmptyParts); if (parts.count() > 1) { // Insert renaming map renaming.insert(parts.first(), parts.last()); // Replace curve value with first part only yCurves.replace(i, parts.first()); } } // else // { // // Insert same value, not renaming anything // renaming.insert(yCurves.at(i), yCurves.at(i)); // } } foreach(curveName, curveNames) { // Add to plot x axis selection ui->xAxis->addItem(curveName); // Add to regression selection ui->xRegressionComboBox->addItem(curveName); ui->yRegressionComboBox->addItem(curveName); if (curveName != xAxisFilter) { if ((yAxisFilter == "") || yCurves.contains(curveName)) { yValues.insert(curveName, new QVector()); xValues.insert(curveName, new QVector()); // Add separator starting with second item if (curveNameIndex > 0 && curveNameIndex < curveNames.count()) { ui->yAxis->setText(ui->yAxis->text()+"|"); } // If this curve was renamed, re-add the renaming to the text field QString renamingText = ""; if (renaming.contains(curveName)) renamingText = QString(":%1").arg(renaming.value(curveName)); ui->yAxis->setText(ui->yAxis->text()+curveName+renamingText); // Insert same value, not renaming anything if (!renaming.contains(curveName)) renaming.insert(curveName, curveName); curveNameIndex++; } } } // Select current axis in UI ui->xAxis->setCurrentIndex(curveNames.indexOf(xAxisFilter)); // Read data double x = 0; double y = 0; while (!in.atEnd()) { QString line = in.readLine(); // Keep empty parts here - we still have to act on them QStringList values = line.split(separator, QString::KeepEmptyParts); bool headerfound = false; // First get header - ORDER MATTERS HERE! foreach(curveName, curveNames) { if (curveName == xAxisFilter) { // X AXIS HANDLING // Take this value as x if it is selected QString text = values.at(curveNames.indexOf(curveName)); text = text.trimmed(); if (text.length() > 0 && text != " " && text != "\n" && text != "\r" && text != "\t") { bool okx = true; x = text.toDouble(&okx); if (okx && !isnan(x) && !isinf(x)) { headerfound = true; } } } } if (headerfound) { // Search again from start for values - ORDER MATTERS HERE! foreach(curveName, curveNames) { // Y AXIS HANDLING // Only plot non-x curver and those selected in the yAxisFilter (or all if the filter is not set) if(curveName != xAxisFilter && (yAxisFilter == "" || yCurves.contains(curveName))) { bool oky; int curveNameIndex = curveNames.indexOf(curveName); if (values.count() > curveNameIndex) { QString text(values.at(curveNameIndex)); text = text.trimmed(); y = text.toDouble(&oky); // Only INF is really an issue for the plot // NaN is fine if (oky && !isnan(y) && !isinf(y) && text.length() > 0 && text != " " && text != "\n" && text != "\r" && text != "\t") { // Only append definitely valid values xValues.value(curveName)->append(x); yValues.value(curveName)->append(y); } } } } } } // Add data array of each curve to the plot at once (fast) // Iterates through all x-y curve combinations for (int i = 0; i < yValues.count(); i++) { if (renaming.contains(yValues.keys().at(i))) { plot->appendData(renaming.value(yValues.keys().at(i)), xValues.values().at(i)->data(), yValues.values().at(i)->data(), xValues.values().at(i)->count()); } else { plot->appendData(yValues.keys().at(i), xValues.values().at(i)->data(), yValues.values().at(i)->data(), xValues.values().at(i)->count()); } } plot->updateScale(); plot->setStyleText(ui->style->currentText()); } bool QGCDataPlot2D::calculateRegression() { // TODO: Add support for quadratic / cubic curve fitting return calculateRegression(ui->xRegressionComboBox->currentText(), ui->yRegressionComboBox->currentText(), "linear"); } /** * @param xName Name of the x dimension * @param yName Name of the y dimension * @param method Regression method, either "linear", "quadratic" or "cubic". Only linear is supported at this point */ bool QGCDataPlot2D::calculateRegression(QString xName, QString yName, QString method) { bool result = false; QString function; if (xName != yName) { if (QFileInfo(fileName).isReadable()) { loadCsvLog(fileName, xName, yName); ui->xRegressionComboBox->setCurrentIndex(curveNames.indexOf(xName)); ui->yRegressionComboBox->setCurrentIndex(curveNames.indexOf(yName)); } // Create a couple of arrays for us to use to temporarily store some of the data from the plot. // These arrays are allocated on the heap as they are far too big to go in the stack and will // cause an overflow. // TODO: Look into if this would be better done by having a getter return const double pointers instead // of using memcpy(). const int size = 100000; double *x = new double[size]; double *y = new double[size]; int copied = plot->data(yName, x, y, size); if (method == "linear") { double a; // Y-axis crossing double b; // Slope double r; // Regression coefficient if (linearRegression(x, y, copied, &a, &b, &r)) { function = tr("%1 = %2 * %3 + %4 | R-coefficient: %5").arg(yName, QString::number(b), xName, QString::number(a), QString::number(r)); // Plot curve // y-axis crossing (x = 0) // Set plotting to lines only plot->appendData(tr("regression %1-%2").arg(xName, yName), 0.0, a); plot->setStyleText("lines"); // x-value of the current rightmost x position in the plot plot->appendData(tr("regression %1-%2").arg(xName, yName), plot->invTransform(QwtPlot::xBottom, plot->width() - plot->width()*0.08f), (a + b*plot->invTransform(QwtPlot::xBottom, plot->width() - plot->width() * 0.08f))); result = true; } else { function = tr("Linear regression failed. (Limit: %1 data points. Try with less)").arg(size); } } else { function = tr("Regression method %1 not found").arg(method); } delete x; delete y; } else { // xName == yName function = tr("Please select different X and Y dimensions, not %1 = %2").arg(xName, yName); } ui->regressionOutput->setText(function); return result; } /** * 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) */ bool 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 true; /* Conpute some things we need */ for (i=0; icopy(fileName); qDebug() << "Saved CSV log (" << fileName << "). 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; } }