Newer
Older
/*=====================================================================
QGroundControl Open Source Ground Control Station
(c) 2009, 2010 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
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 <http://www.gnu.org/licenses/>.
======================================================================*/
/**
* @file
* @brief Implementation of QGCDataPlot2D
* @author Lorenz Meier <mavteam@student.ethz.ch>
*
*/
#include <QTemporaryFile>
#include <QMessageBox>
#include <QPrintDialog>
#include <QProgressDialog>
#include <QHBoxLayout>
#include <QSvgGenerator>
#include <QPrinter>
Bryant Mairs
committed
#include <QStandardPaths>
#include "QGCDataPlot2D.h"
#include "ui_QGCDataPlot2D.h"
#include "MG.h"
QGCDataPlot2D::QGCDataPlot2D(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(MainWindow::instance(), SIGNAL(styleChanged(MainWindow::QGC_MAINWINDOW_STYLE)),
plot, SLOT(styleChanged(MainWindow::QGC_MAINWINDOW_STYLE)));
}
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")) {
} 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 exports to either PDF or SVG, depending on the filename
*/
void QGCDataPlot2D::savePlot()
{
QString fileName = "plot.svg";
Bryant Mairs
committed
this, "Export File Name", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation),
"PDF Documents (*.pdf);;SVG Images (*.svg)");
if (fileName.isEmpty())
return;
while(!(fileName.endsWith(".svg") || fileName.endsWith(".pdf"))) {
QMessageBox msgBox;
msgBox.setIcon(QMessageBox::Critical);
msgBox.setText("Unsuitable file extension for PDF or SVG");
msgBox.setInformativeText("Please choose .pdf or .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);
// Abort if cancelled
if(msgBox.exec() == QMessageBox::Cancel) return;
Bryant Mairs
committed
this, "Export File Name", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation),
"PDF Documents (*.pdf);;SVG Images (*.svg)");
if (fileName.isEmpty())
return; //Abort if cancelled
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();
docName.replace (QRegExp (QString::fromLatin1 ("\n")), tr (" -- "));
printer.setDocName (docName);
}
printer.setCreator("QGroundControl");
printer.setOrientation(QPrinter::Landscape);
QPrintDialog dialog(&printer);
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();
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->setStyleSheet("QWidget { background-color: #050508; color: #DDDDDF; background-clip: border; font-size: 11pt;}");
//plot->setCanvasBackground(QColor(5, 5, 8));
}
void QGCDataPlot2D::exportSVG(QString fileName)
{
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->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);
Bryant Mairs
committed
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()));
Bryant Mairs
committed
}
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);
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(logProcessingStatusChanged(QString)), MainWindow::instance(), SLOT(showStatusMessage(QString)));
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: 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)
{
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
separator += header[i];
}
// 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();
LM
committed
QMap<QString, QVector<double>* > xValues;
QMap<QString, QVector<double>* > yValues;
curveNames.append(header.split(separator, QString::SkipEmptyParts));
LM
committed
// 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;
xAxisFilter = curveNames.first();
xAxisFilter = xAxisName;
}
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
// 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));
// }
}
// Add to plot x axis selection
ui->xAxis->addItem(curveName);
// Add to regression selection
ui->xRegressionComboBox->addItem(curveName);
ui->yRegressionComboBox->addItem(curveName);
if ((yAxisFilter == "") || yCurves.contains(curveName)) {
LM
committed
xValues.insert(curveName, new QVector<double>());
if (curveNameIndex > 0 && curveNameIndex < curveNames.count()) {
// 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));
LM
committed
while (!in.atEnd())
{
QString line = in.readLine();
LM
committed
// Keep empty parts here - we still have to act on them
QStringList values = line.split(separator, QString::KeepEmptyParts);
bool headerfound = false;
LM
committed
// First get header - ORDER MATTERS HERE!
foreach(curveName, curveNames)
{
if (curveName == xAxisFilter)
{
// X AXIS HANDLING
// Take this value as x if it is selected
LM
committed
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;
}
}
}
}
LM
committed
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)))
LM
committed
{
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());
}
LM
committed
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);
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;
function = tr("Linear regression failed. (Limit: %1 data points. Try with less)").arg(size);
}
function = tr("Regression method %1 not found").arg(method);
}
// 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 */
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 existant 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 = "export.csv";
Bryant Mairs
committed
this, "Export CSV File Name", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation),
"CSV file (*.csv);;Text file (*.txt)");
if (fileName.isEmpty())
return; //User cancelled
// .csv is default extension
fileName.append(".csv");
}
// QFileInfo fileInfo(fileName);
//
// // Check if we could create a new file in this directory
// QDir dir(fileInfo.absoluteDir());
// QFileInfo dirInfo(dir);
//
// while(!(dirInfo.isWritable()))
// {
// QMessageBox msgBox;
// msgBox.setIcon(QMessageBox::Critical);
// msgBox.setText("File cannot be written, Operating System denies permission");
// msgBox.setInformativeText("Please choose a different file name or directory. Click OK to change the file, cancel to not save the file.");
// msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
// msgBox.setDefaultButton(QMessageBox::Ok);
// if(msgBox.exec() == QMessageBox::Cancel) break;
Bryant Mairs
committed
// this, "Export CSV File Name", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation),
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
// "CSV file (*.csv);;Text file (*.txt)");
// }
bool success = logFile->copy(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;
}
}