6 changed files with 0 additions and 1125 deletions
@ -1,734 +0,0 @@
@@ -1,734 +0,0 @@
|
||||
/****************************************************************************
|
||||
* |
||||
* (c) 2009-2016 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
|
||||
* |
||||
* QGroundControl is licensed according to the terms in the file |
||||
* COPYING.md in the root of the source code directory. |
||||
* |
||||
****************************************************************************/ |
||||
|
||||
|
||||
/**
|
||||
* @file |
||||
* @brief Implementation of QGCDataPlot2D |
||||
* @author Lorenz Meier <mavteam@student.ethz.ch> |
||||
* |
||||
*/ |
||||
|
||||
#include <QTemporaryFile> |
||||
#ifndef __mobile__ |
||||
#include <QPrintDialog> |
||||
#include <QPrinter> |
||||
#endif |
||||
#include <QProgressDialog> |
||||
#include <QHBoxLayout> |
||||
#include <QSvgGenerator> |
||||
#include <QStandardPaths> |
||||
#include <QDebug> |
||||
|
||||
#include <cmath> |
||||
|
||||
#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, &QPushButton::clicked, this, &QGCDataPlot2D::selectFile); |
||||
connect(ui->saveCsvButton, &QPushButton::clicked, this, &QGCDataPlot2D::saveCsvLog); |
||||
connect(ui->reloadButton, &QPushButton::clicked, this, &QGCDataPlot2D::reloadFile); |
||||
connect(ui->savePlotButton, &QPushButton::clicked, this, &QGCDataPlot2D::savePlot); |
||||
connect(ui->printButton, &QPushButton::clicked, this, &QGCDataPlot2D::print); |
||||
connect(ui->legendCheckBox, &QCheckBox::clicked, plot, &IncrementalPlot::showLegend); |
||||
connect(ui->symmetricCheckBox,&QCheckBox::clicked, plot, &IncrementalPlot::setSymmetric); |
||||
connect(ui->gridCheckBox, &QCheckBox::clicked, plot, &IncrementalPlot::showGrid); |
||||
|
||||
connect(ui->style, static_cast<void (QComboBox::*)(const QString&)>(&QComboBox::currentIndexChanged), |
||||
plot, &IncrementalPlot::setStyleText); |
||||
|
||||
//TODO: calculateRegression returns bool, slots are expected to return void, this makes
|
||||
// converting to new style way too hard.
|
||||
connect(ui->regressionButton, SIGNAL(clicked()), this, SLOT(calculateRegression())); |
||||
|
||||
// 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) |
||||
{ |
||||
// TODO This "filename" is a private/protected member variable. It should be named in such way
|
||||
// it indicates so. This same name is used in several places within this file in local scopes.
|
||||
fileName = file; |
||||
QFileInfo fi(fileName); |
||||
if (fi.isReadable()) { |
||||
if (fi.suffix() == QString("raw") || fi.suffix() == QString("imu")) { |
||||
loadRawLog(fileName); |
||||
} else if (fi.suffix() == QString("txt") || fi.suffix() == QString("csv")) { |
||||
loadCsvLog(fileName); |
||||
} |
||||
// TODO Else, tell the user it doesn't know what to do with the file...
|
||||
} |
||||
} |
||||
|
||||
/**
|
||||
* This function brings up a file name dialog and asks the user to enter a file to save to |
||||
*/ |
||||
QString QGCDataPlot2D::getSavePlotFilename() |
||||
{ |
||||
QString fileName = QGCFileDialog::getSaveFileName( |
||||
this, "Save Plot File", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation), |
||||
"PDF Documents (*.pdf);;SVG Images (*.svg)", |
||||
"pdf"); |
||||
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::warning( |
||||
tr("Unsuitable file extension for Plot document type."), |
||||
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() |
||||
{ |
||||
#ifndef __mobile__ |
||||
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));
|
||||
} |
||||
#endif |
||||
} |
||||
|
||||
void QGCDataPlot2D::exportPDF(QString fileName) |
||||
{ |
||||
#ifdef __mobile__ |
||||
Q_UNUSED(fileName) |
||||
#else |
||||
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));
|
||||
#endif |
||||
} |
||||
|
||||
void QGCDataPlot2D::exportSVG(QString fileName) |
||||
{ |
||||
#ifdef __mobile__ |
||||
Q_UNUSED(fileName) |
||||
#else |
||||
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;}"); |
||||
} |
||||
#endif |
||||
} |
||||
|
||||
/**
|
||||
* 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("Load Log File"), QString(), "Log Files (*.imu *.raw)"); |
||||
} |
||||
else |
||||
{ |
||||
fileName = QGCFileDialog::getOpenFileName(this, tr("Load Log File"), QString(), "Log Files (*.csv);;All Files (*)"); |
||||
} |
||||
|
||||
// Check if the user hit cancel, which results in an empty string.
|
||||
// If this is the case, we just stop.
|
||||
if (fileName.isEmpty()) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
// Now attempt to open the file
|
||||
QFileInfo fileInfo(fileName); |
||||
if (!fileInfo.isReadable()) |
||||
{ |
||||
// TODO This needs some TLC. File used by another program sounds like a Windows only issue.
|
||||
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.fileName())); |
||||
} |
||||
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, &LogCompressor::finishedFile, this, static_cast<void (QGCDataPlot2D::*)(QString)>(&QGCDataPlot2D::loadFile)); |
||||
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: x<tab>y<tab>z
|
||||
* 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<QChar> 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", "<tab>"); |
||||
ui->filenameLabel->setText(file.split("/").last().split("\\").last()+" Separator: \""+out+"\""); |
||||
//qDebug() << "READING CSV:" << header;
|
||||
|
||||
// Clear plot
|
||||
plot->removeData(); |
||||
|
||||
QMap<QString, QVector<double>* > xValues; |
||||
QMap<QString, QVector<double>* > 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<QString, QString> 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<double>()); |
||||
xValues.insert(curveName, new QVector<double>()); |
||||
// 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 && !qIsNaN(x) && !qIsInf(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 && !qIsNaN(y) && !qIsInf(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; i<n; i++) { |
||||
sumx += x[i]; |
||||
sumy += y[i]; |
||||
sumx2 += (x[i] * x[i]); |
||||
sumy2 += (y[i] * y[i]); |
||||
sumxy += (x[i] * y[i]); |
||||
} |
||||
sxx = sumx2 - sumx * sumx / n; |
||||
syy = sumy2 - sumy * sumy / n; |
||||
sxy = sumxy - sumx * sumy / n; |
||||
|
||||
/* Infinite slope (b), non existent intercept (a) */ |
||||
if (fabs(sxx) == 0) |
||||
return false; |
||||
|
||||
/* Calculate the slope (b) and intercept (a) */ |
||||
*b = sxy / sxx; |
||||
*a = sumy / n - (*b) * sumx / n; |
||||
|
||||
/* Compute the regression coefficient */ |
||||
if (fabs(syy) == 0) |
||||
*r = 1; |
||||
else |
||||
*r = sxy / sqrt(sxx * syy); |
||||
|
||||
return false; |
||||
} |
||||
|
||||
void QGCDataPlot2D::saveCsvLog() |
||||
{ |
||||
QString fileName = QGCFileDialog::getSaveFileName( |
||||
this, "Save CSV Log File", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation), |
||||
"CSV Files (*.csv)", |
||||
"csv", |
||||
true); |
||||
|
||||
if (fileName.isEmpty()) { |
||||
return; //User cancelled
|
||||
} |
||||
|
||||
bool success = logFile->copy(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; |
||||
} |
||||
} |
@ -1,77 +0,0 @@
@@ -1,77 +0,0 @@
|
||||
#ifndef QGCDATAPLOT2D_H |
||||
#define QGCDATAPLOT2D_H |
||||
|
||||
#include <QWidget> |
||||
#include <QFile> |
||||
#include "IncrementalPlot.h" |
||||
#include "LogCompressor.h" |
||||
|
||||
namespace Ui |
||||
{ |
||||
class QGCDataPlot2D; |
||||
} |
||||
|
||||
class QGCDataPlot2D : public QWidget |
||||
{ |
||||
Q_OBJECT |
||||
public: |
||||
QGCDataPlot2D(QWidget *parent = 0); |
||||
~QGCDataPlot2D(); |
||||
|
||||
/** @brief Calculate and display regression function*/ |
||||
bool calculateRegression(QString xName, QString yName, QString method="linear"); |
||||
|
||||
/** @brief Linear regression over data points */ |
||||
bool linearRegression(double *x, double *y, int n, double *a, double *b, double *r); |
||||
|
||||
public slots: |
||||
/** @brief Load previously selected file */ |
||||
void loadFile(); |
||||
/** @brief Load file with this name */ |
||||
void loadFile(QString file); |
||||
/** @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 PDF file */ |
||||
void exportPDF(QString fileName); |
||||
/** @brief Export SVG file */ |
||||
void exportSVG(QString file); |
||||
/** @brief Print or save PDF file (MacOS/Linux) */ |
||||
void print(); |
||||
/** @brief Calculate and display regression function*/ |
||||
bool calculateRegression(); |
||||
|
||||
signals: |
||||
void visibilityChanged(bool visible); |
||||
|
||||
protected: |
||||
void showEvent(QShowEvent* event) |
||||
{ |
||||
QWidget::showEvent(event); |
||||
emit visibilityChanged(true); |
||||
} |
||||
|
||||
void hideEvent(QHideEvent* event) |
||||
{ |
||||
QWidget::hideEvent(event); |
||||
emit visibilityChanged(false); |
||||
} |
||||
|
||||
void changeEvent(QEvent *e); |
||||
QString getSavePlotFilename(); |
||||
IncrementalPlot* plot; |
||||
LogCompressor* compressor; |
||||
QFile* logFile; |
||||
QString fileName; |
||||
QStringList curveNames; |
||||
|
||||
private: |
||||
Ui::QGCDataPlot2D *ui; |
||||
}; |
||||
|
||||
#endif // QGCDATAPLOT2D_H
|
@ -1,309 +0,0 @@
@@ -1,309 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<ui version="4.0"> |
||||
<class>QGCDataPlot2D</class> |
||||
<widget class="QWidget" name="QGCDataPlot2D"> |
||||
<property name="geometry"> |
||||
<rect> |
||||
<x>0</x> |
||||
<y>0</y> |
||||
<width>1463</width> |
||||
<height>311</height> |
||||
</rect> |
||||
</property> |
||||
<property name="windowTitle"> |
||||
<string>Form</string> |
||||
</property> |
||||
<layout class="QGridLayout" name="gridLayout" columnstretch="1,1,1,1,100,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1"> |
||||
<item row="0" column="0"> |
||||
<widget class="QLabel" name="label_2"> |
||||
<property name="text"> |
||||
<string>X</string> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="0" column="1" colspan="2"> |
||||
<widget class="QComboBox" name="xAxis"/> |
||||
</item> |
||||
<item row="0" column="3"> |
||||
<widget class="QLabel" name="label_3"> |
||||
<property name="text"> |
||||
<string>Y</string> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="0" column="4"> |
||||
<widget class="QLineEdit" name="yAxis"/> |
||||
</item> |
||||
<item row="0" column="5" colspan="2"> |
||||
<widget class="QComboBox" name="style"> |
||||
<property name="toolTip"> |
||||
<string>Set line style</string> |
||||
</property> |
||||
<item> |
||||
<property name="text"> |
||||
<string>Only lines</string> |
||||
</property> |
||||
</item> |
||||
<item> |
||||
<property name="text"> |
||||
<string>Only crosses</string> |
||||
</property> |
||||
</item> |
||||
<item> |
||||
<property name="text"> |
||||
<string>Only circles</string> |
||||
</property> |
||||
</item> |
||||
<item> |
||||
<property name="text"> |
||||
<string>Only rectangles</string> |
||||
</property> |
||||
</item> |
||||
<item> |
||||
<property name="text"> |
||||
<string>Lines and crosses</string> |
||||
</property> |
||||
</item> |
||||
<item> |
||||
<property name="text"> |
||||
<string>Lines and circles</string> |
||||
</property> |
||||
</item> |
||||
<item> |
||||
<property name="text"> |
||||
<string>Lines and rects</string> |
||||
</property> |
||||
</item> |
||||
<item> |
||||
<property name="text"> |
||||
<string>Dotted lines and crosses</string> |
||||
</property> |
||||
</item> |
||||
<item> |
||||
<property name="text"> |
||||
<string>Dotted lines and circles</string> |
||||
</property> |
||||
</item> |
||||
<item> |
||||
<property name="text"> |
||||
<string>Dotted lines and rects</string> |
||||
</property> |
||||
</item> |
||||
<item> |
||||
<property name="text"> |
||||
<string>Dashed lines and crosses</string> |
||||
</property> |
||||
</item> |
||||
<item> |
||||
<property name="text"> |
||||
<string>Dashed lines and circles</string> |
||||
</property> |
||||
</item> |
||||
<item> |
||||
<property name="text"> |
||||
<string>Dashed lines and rects</string> |
||||
</property> |
||||
</item> |
||||
</widget> |
||||
</item> |
||||
<item row="0" column="7" colspan="2"> |
||||
<widget class="QPushButton" name="reloadButton"> |
||||
<property name="text"> |
||||
<string>Replot</string> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="0" column="11"> |
||||
<widget class="Line" name="line"> |
||||
<property name="orientation"> |
||||
<enum>Qt::Vertical</enum> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="0" column="12" colspan="2"> |
||||
<widget class="QPushButton" name="savePlotButton"> |
||||
<property name="text"> |
||||
<string>Save Image</string> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="0" column="19"> |
||||
<widget class="QPushButton" name="printButton"> |
||||
<property name="text"> |
||||
<string>Print</string> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="1" column="0" colspan="2"> |
||||
<widget class="QLabel" name="label_4"> |
||||
<property name="text"> |
||||
<string>Title</string> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="1" column="2" colspan="3"> |
||||
<widget class="QLineEdit" name="plotTitle"/> |
||||
</item> |
||||
<item row="1" column="5"> |
||||
<widget class="QLabel" name="label_5"> |
||||
<property name="text"> |
||||
<string>X label</string> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="1" column="6" colspan="2"> |
||||
<widget class="QLineEdit" name="plotXAxisLabel"/> |
||||
</item> |
||||
<item row="1" column="8"> |
||||
<widget class="QLabel" name="label_6"> |
||||
<property name="text"> |
||||
<string>Y label</string> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="1" column="9" colspan="3"> |
||||
<widget class="QLineEdit" name="plotYAxisLabel"/> |
||||
</item> |
||||
<item row="1" column="12" colspan="2"> |
||||
<widget class="QCheckBox" name="symmetricCheckBox"> |
||||
<property name="text"> |
||||
<string>Symmetric</string> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="1" column="14" colspan="2"> |
||||
<widget class="QCheckBox" name="legendCheckBox"> |
||||
<property name="text"> |
||||
<string>Legend</string> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="1" column="16"> |
||||
<widget class="QCheckBox" name="gridCheckBox"> |
||||
<property name="text"> |
||||
<string>Grid</string> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="2" column="0" colspan="20"> |
||||
<widget class="QFrame" name="plotFrame"> |
||||
<property name="frameShape"> |
||||
<enum>QFrame::StyledPanel</enum> |
||||
</property> |
||||
<property name="frameShadow"> |
||||
<enum>QFrame::Raised</enum> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="3" column="0"> |
||||
<widget class="QLabel" name="label"> |
||||
<property name="text"> |
||||
<string>File</string> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="3" column="1" colspan="2"> |
||||
<widget class="QComboBox" name="inputFileType"> |
||||
<item> |
||||
<property name="text"> |
||||
<string>CSV</string> |
||||
</property> |
||||
</item> |
||||
<item> |
||||
<property name="text"> |
||||
<string>pxIMU</string> |
||||
</property> |
||||
</item> |
||||
<item> |
||||
<property name="text"> |
||||
<string>RAW</string> |
||||
</property> |
||||
</item> |
||||
</widget> |
||||
</item> |
||||
<item row="3" column="3" colspan="4"> |
||||
<widget class="QLabel" name="filenameLabel"> |
||||
<property name="text"> |
||||
<string>Please select input file..</string> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="3" column="8" colspan="2"> |
||||
<widget class="QPushButton" name="selectFileButton"> |
||||
<property name="text"> |
||||
<string>Select file</string> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="3" column="10" colspan="2"> |
||||
<spacer name="horizontalSpacer"> |
||||
<property name="orientation"> |
||||
<enum>Qt::Horizontal</enum> |
||||
</property> |
||||
<property name="sizeHint" stdset="0"> |
||||
<size> |
||||
<width>40</width> |
||||
<height>20</height> |
||||
</size> |
||||
</property> |
||||
</spacer> |
||||
</item> |
||||
<item row="3" column="12"> |
||||
<widget class="Line" name="line_2"> |
||||
<property name="orientation"> |
||||
<enum>Qt::Vertical</enum> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="3" column="13"> |
||||
<widget class="QLabel" name="label_7"> |
||||
<property name="text"> |
||||
<string>Regression</string> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="3" column="14" colspan="2"> |
||||
<widget class="QComboBox" name="xRegressionComboBox"/> |
||||
</item> |
||||
<item row="3" column="16" colspan="2"> |
||||
<widget class="QComboBox" name="yRegressionComboBox"/> |
||||
</item> |
||||
<item row="3" column="19"> |
||||
<widget class="QPushButton" name="regressionButton"> |
||||
<property name="text"> |
||||
<string>Calculate</string> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="3" column="18"> |
||||
<widget class="QLineEdit" name="regressionOutput"> |
||||
<property name="readOnly"> |
||||
<bool>true</bool> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="0" column="14"> |
||||
<widget class="QPushButton" name="saveCsvButton"> |
||||
<property name="text"> |
||||
<string>Save Data</string> |
||||
</property> |
||||
</widget> |
||||
</item> |
||||
<item row="0" column="9" colspan="2"> |
||||
<spacer name="horizontalSpacer_2"> |
||||
<property name="orientation"> |
||||
<enum>Qt::Horizontal</enum> |
||||
</property> |
||||
<property name="sizeHint" stdset="0"> |
||||
<size> |
||||
<width>5</width> |
||||
<height>20</height> |
||||
</size> |
||||
</property> |
||||
</spacer> |
||||
</item> |
||||
</layout> |
||||
</widget> |
||||
<resources/> |
||||
<connections/> |
||||
</ui> |
Loading…
Reference in new issue