QGCDataPlot2D.cc 26.1 KB
Newer Older
lm's avatar
lm committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*=====================================================================

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>
 *
 */
30
31
32
33
34
35
36
37
38
39
40
41
42

#include <QFileDialog>
#include <QTemporaryFile>
#include <QMessageBox>
#include <QPrintDialog>
#include <QProgressDialog>
#include <QHBoxLayout>
#include <QSvgGenerator>
#include <QPrinter>
#include <QDesktopServices>
#include "QGCDataPlot2D.h"
#include "ui_QGCDataPlot2D.h"
#include "MG.h"
lm's avatar
lm committed
43
#include "MainWindow.h"
44
45
46
47
48
#include <cmath>

#include <QDebug>

QGCDataPlot2D::QGCDataPlot2D(QWidget *parent) :
49
50
51
52
    QWidget(parent),
    plot(new IncrementalPlot()),
    logFile(NULL),
    ui(new Ui::QGCDataPlot2D)
53
54
55
56
57
58
59
{
    ui->setupUi(this);

    // Add plot to ui
    QHBoxLayout* layout = new QHBoxLayout(ui->plotFrame);
    layout->addWidget(plot);
    ui->plotFrame->setLayout(layout);
60
    ui->gridCheckBox->setChecked(plot->gridEnabled());
61
62
63
64
65
66
67

    // 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()));
68
    connect(ui->legendCheckBox, SIGNAL(clicked(bool)), plot, SLOT(showLegend(bool)));
69
70
71
    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()));
72
    connect(ui->style, SIGNAL(currentIndexChanged(QString)), plot, SLOT(setStyleText(QString)));
73
74
75
76
}

void QGCDataPlot2D::reloadFile()
{
77
78
    if (QFileInfo(fileName).isReadable()) {
        if (ui->inputFileType->currentText().contains("pxIMU") || ui->inputFileType->currentText().contains("RAW")) {
79
            loadRawLog(fileName, ui->xAxis->currentText(), ui->yAxis->text());
80
        } else if (ui->inputFileType->currentText().contains("CSV")) {
81
82
83
84
85
86
87
            loadCsvLog(fileName, ui->xAxis->currentText(), ui->yAxis->text());
        }
    }
}

void QGCDataPlot2D::loadFile()
{
lm's avatar
lm committed
88
    qDebug() << "DATA PLOT: Loading file:" << fileName;
89
90
    if (QFileInfo(fileName).isReadable()) {
        if (ui->inputFileType->currentText().contains("pxIMU") || ui->inputFileType->currentText().contains("RAW")) {
91
            loadRawLog(fileName);
92
        } else if (ui->inputFileType->currentText().contains("CSV")) {
93
94
95
96
97
            loadCsvLog(fileName);
        }
    }
}

98
99
100
void QGCDataPlot2D::loadFile(QString file)
{
    fileName = file;
101
102
    if (QFileInfo(fileName).isReadable()) {
        if (fileName.contains(".raw") || fileName.contains(".imu")) {
103
            loadRawLog(fileName);
104
        } else if (fileName.contains(".txt") || fileName.contains(".csv") || fileName.contains(".csv")) {
105
106
107
108
109
110
111
112
            loadCsvLog(fileName);
        }
    }
}

/**
 * This function brings up a file name dialog and exports to either PDF or SVG, depending on the filename
 */
113
114
115
116
void QGCDataPlot2D::savePlot()
{
    QString fileName = "plot.svg";
    fileName = QFileDialog::getSaveFileName(
117
118
                   this, "Export File Name", QDesktopServices::storageLocation(QDesktopServices::DesktopLocation),
                   "PDF Documents (*.pdf);;SVG Images (*.svg)");
lm's avatar
lm committed
119

120
    if (!fileName.contains(".")) {
lm's avatar
lm committed
121
        // .pdf is default extension
lm's avatar
lm committed
122
123
124
        fileName.append(".pdf");
    }

125
    while(!(fileName.endsWith(".svg") || fileName.endsWith(".pdf"))) {
126
127
        QMessageBox msgBox;
        msgBox.setIcon(QMessageBox::Critical);
128
129
        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.");
130
131
        msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
        msgBox.setDefaultButton(QMessageBox::Ok);
132
133
        // Abort if cancelled
        if(msgBox.exec() == QMessageBox::Cancel) return;
134
        fileName = QFileDialog::getSaveFileName(
135
136
                       this, "Export File Name", QDesktopServices::storageLocation(QDesktopServices::DesktopLocation),
                       "PDF Documents (*.pdf);;SVG Images (*.svg)");
137
138
    }

139
    if (fileName.endsWith(".pdf")) {
140
        exportPDF(fileName);
141
    } else if (fileName.endsWith(".svg")) {
142
143
        exportSVG(fileName);
    }
144
145
146
147
148
149
150
151
152
153
154
}


void QGCDataPlot2D::print()
{
    QPrinter printer(QPrinter::HighResolution);
    //    printer.setOutputFormat(QPrinter::PdfFormat);
    //    //QPrinter printer(QPrinter::HighResolution);
    //    printer.setOutputFileName(fileName);

    QString docName = plot->title().text();
155
    if ( !docName.isEmpty() ) {
156
157
158
159
160
161
162
163
        docName.replace (QRegExp (QString::fromLatin1 ("\n")), tr (" -- "));
        printer.setDocName (docName);
    }

    printer.setCreator("QGroundControl");
    printer.setOrientation(QPrinter::Landscape);

    QPrintDialog dialog(&printer);
164
    if ( dialog.exec() ) {
pixhawk's avatar
pixhawk committed
165
        plot->setStyleSheet("QWidget { background-color: #FFFFFF; color: #000000; background-clip: border; font-size: 10pt;}");
166
167
168
169
170
171
172
        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);
173
        if ( printer.colorMode() == QPrinter::GrayScale ) {
174
175
176
177
178
179
            int options = QwtPlotPrintFilter::PrintAll;
            options &= ~QwtPlotPrintFilter::PrintBackground;
            options |= QwtPlotPrintFilter::PrintFrameWithScales;
            filter.setOptions(options);
        }
        plot->print(printer, filter);
pixhawk's avatar
pixhawk committed
180
181
        plot->setStyleSheet("QWidget { background-color: #050508; color: #DDDDDF; background-clip: border; font-size: 11pt;}");
        //plot->setCanvasBackground(QColor(5, 5, 8));
182
183
184
    }
}

185
186
187
188
189
190
191
192
193
194
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();
195
    if ( !docName.isEmpty() ) {
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
        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);
    //        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);
    plot->setStyleSheet("QWidget { background-color: #050508; color: #DDDDDF; background-clip: border; font-size: 11pt;}");
    //plot->setCanvasBackground(QColor(5, 5, 8));
}

223
224
void QGCDataPlot2D::exportSVG(QString fileName)
{
225
    if ( !fileName.isEmpty() ) {
226
227
        plot->setStyleSheet("QWidget { background-color: #FFFFFF; color: #000000; background-clip: border; font-size: 10pt;}");
        //plot->setCanvasBackground(Qt::white);
228
229
230
231
232
233
234
235
236
237
238
239
        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);
240
        plot->setStyleSheet("QWidget { background-color: #050508; color: #DDDDDF; background-clip: border; font-size: 11pt;}");
241
242
243
244
245
246
247
248
    }
}

/**
 * Selects a filename and attempts immediately to load it.
 */
void QGCDataPlot2D::selectFile()
{
249
250
	// Open a file dialog prompting the user for the file to load.
	// Note the special case for the Pixhawk.
251
    if (ui->inputFileType->currentText().contains("pxIMU") || ui->inputFileType->currentText().contains("RAW")) {
lm's avatar
lm committed
252
        fileName = QFileDialog::getOpenFileName(this, tr("Specify log file name"), QString(), "Logfile (*.imu *.raw)");
253
254
255
	}
	else
	{
lm's avatar
lm committed
256
257
258
        fileName = QFileDialog::getOpenFileName(this, tr("Specify log file name"), QString(), "Logfile (*.csv *.txt *.log)");
    }

259
260
261
262
263
264
	// Check if the user hit cancel, which results in a Null string.
	// If this is the case, we just stop.
	if (fileName.isNull())
	{
		return;
	}
265

266
	// Now attempt to open the file
267
    QFileInfo fileInfo(fileName);
268
269
    if (!fileInfo.isReadable())
	{
270
271
272
273
274
275
276
277
        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()));
278
279
280
    }
	else
	{
281
282
283
284
285
286
287
288
289
        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)
{
290
291
    Q_UNUSED(xAxisName);
    Q_UNUSED(yAxisFilter);
lm's avatar
lm committed
292

293
    if (logFile != NULL) {
294
295
296
297
        logFile->close();
        delete logFile;
    }
    // Postprocess log file
lm's avatar
lm committed
298
    logFile = new QTemporaryFile("qt_qgc_temp_log.XXXXXX.csv");
299
    compressor = new LogCompressor(file, logFile->fileName());
lm's avatar
lm committed
300
301
    connect(compressor, SIGNAL(logProcessingStatusChanged(QString)), MainWindow::instance(), SLOT(showStatusMessage(QString)));
    connect(compressor, SIGNAL(finishedFile(QString)), this, SLOT(loadFile(QString)));
302
    compressor->startCompression();
303
304
}

305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
/**
 * 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
 */
323
324
void QGCDataPlot2D::loadCsvLog(QString file, QString xAxisName, QString yAxisFilter)
{
325
    if (logFile != NULL) {
326
327
        logFile->close();
        delete logFile;
328
        curveNames.clear();
329
330
331
332
333
334
335
    }
    logFile = new QFile(file);

    // Load CSV data
    if (!logFile->open(QIODevice::ReadOnly | QIODevice::Text))
        return;

336
337
338
339
340
    // 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());

341
342
343
344
345
346
347
348
349
    // Extract header

    // Read in values
    // Find all keys
    QTextStream in(logFile);

    // First line is header
    QString header = in.readLine();

350
351
352
353
354
355
356
357
358
359
360
361
    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
362
363
    for (int i = 0; i < header.length(); i++) {
        if (sepCandidates.contains(header.at(i))) {
364
            // Separator found
365
            if (charRead) {
366
367
                separator += header[i];
            }
368
        } else {
369
370
371
372
373
374
375
376
377
378
379
380
381
            // 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;
382
383
384
385

    // Clear plot
    plot->removeData();

386
    QMap<QString, QVector<double>* > xValues;
387
388
    QMap<QString, QVector<double>* > yValues;

389
    curveNames.append(header.split(separator, QString::SkipEmptyParts));
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404

    // 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);
        }
    }

405
406
407
408
409
    QString curveName;

    // Clear UI elements
    ui->xAxis->clear();
    ui->yAxis->clear();
410
411
412
    ui->xRegressionComboBox->clear();
    ui->yRegressionComboBox->clear();
    ui->regressionOutput->clear();
413
414
415

    int curveNameIndex = 0;

416
    QString xAxisFilter;
417
    if (xAxisName == "") {
418
        xAxisFilter = curveNames.first();
419
    } else {
420
421
        xAxisFilter = xAxisName;
    }
422

LM's avatar
LM committed
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
    // 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));
//        }
    }


451
    foreach(curveName, curveNames) {
452
        // Add to plot x axis selection
453
        ui->xAxis->addItem(curveName);
454
455
456
        // Add to regression selection
        ui->xRegressionComboBox->addItem(curveName);
        ui->yRegressionComboBox->addItem(curveName);
457
        if (curveName != xAxisFilter) {
LM's avatar
LM committed
458
            if ((yAxisFilter == "") || yCurves.contains(curveName)) {
pixhawk's avatar
pixhawk committed
459
                yValues.insert(curveName, new QVector<double>());
460
                xValues.insert(curveName, new QVector<double>());
pixhawk's avatar
pixhawk committed
461
                // Add separator starting with second item
462
                if (curveNameIndex > 0 && curveNameIndex < curveNames.count()) {
pixhawk's avatar
pixhawk committed
463
464
                    ui->yAxis->setText(ui->yAxis->text()+"|");
                }
LM's avatar
LM committed
465
466
467
468
469
470
                // 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);
471
                curveNameIndex++;
472
473
474
475
            }
        }
    }

476
477
478
    // Select current axis in UI
    ui->xAxis->setCurrentIndex(curveNames.indexOf(xAxisFilter));

479
480
    // Read data

pixhawk's avatar
pixhawk committed
481
482
    double x = 0;
    double y = 0;
483

484
485
    while (!in.atEnd())
    {
486
487
        QString line = in.readLine();

488
489
490
491
        // Keep empty parts here - we still have to act on them
        QStringList values = line.split(separator, QString::KeepEmptyParts);

        bool headerfound = false;
492

493
494
495
496
497
        // First get header - ORDER MATTERS HERE!
        foreach(curveName, curveNames)
        {
            if (curveName == xAxisFilter)
            {
498
                // X  AXIS HANDLING
499

500
                // Take this value as x if it is selected
501
502
503
504
505
506
507
508
509
510
511
512
513
                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;
                    }
                }
            }
        }
514

515
516
517
518
519
520
        if (headerfound)
        {
            // Search again from start for values - ORDER MATTERS HERE!
            foreach(curveName, curveNames)
            {
                // Y  AXIS HANDLING
LM's avatar
LM committed
521
522
                // 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)))
523
524
525
526
527
528
529
530
531
532
533
534
535
536
                {
                    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);
537
538
539
                            yValues.value(curveName)->append(y);
                        }
                    }
540
541
542
543
544
                }
            }
        }
    }

545
546
    // Add data array of each curve to the plot at once (fast)
    // Iterates through all x-y curve combinations
547
    for (int i = 0; i < yValues.count(); i++) {
LM's avatar
LM committed
548
549
550
551
552
553
554
555
        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());
        }
556
    }
557
    plot->updateScale();
558
559
560
561
562
    plot->setStyleText(ui->style->currentText());
}

bool QGCDataPlot2D::calculateRegression()
{
563
564
    // TODO: Add support for quadratic / cubic curve fitting
	return calculateRegression(ui->xRegressionComboBox->currentText(), ui->yRegressionComboBox->currentText(), "linear");
565
566
567
568
569
570
571
572
573
574
575
}

/**
 * @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;
576
577
    if (xName != yName) {
        if (QFileInfo(fileName).isReadable()) {
578
579
580
581
            loadCsvLog(fileName, xName, yName);
            ui->xRegressionComboBox->setCurrentIndex(curveNames.indexOf(xName));
            ui->yRegressionComboBox->setCurrentIndex(curveNames.indexOf(yName));
        }
582
583
584
585
586
587

		// 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().
588
        const int size = 100000;
589
590
		double *x = new double[size];
		double *y = new double[size];
591
592
        int copied = plot->data(yName, x, y, size);

593
        if (method == "linear") {
594
595
596
            double a;  // Y-axis crossing
            double b;  // Slope
            double r;  // Regression coefficient
597
            if (linearRegression(x, y, copied, &a, &b, &r)) {
598
599
600
601
602
603
604
605
606
                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)));
607
608
                
				result = true;
609
            } else {
610
611
                function = tr("Linear regression failed. (Limit: %1 data points. Try with less)").arg(size);
            }
612
        } else {
613
614
            function = tr("Regression method %1 not found").arg(method);
        }
615

616
617
        delete x;
        delete y;
618
    } else {
619
620
        // xName == yName
        function = tr("Please select different X and Y dimensions, not %1 = %2").arg(xName, yName);
621
    }
622
623
    ui->regressionOutput->setText(function);
    return result;
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
}

/**
 * 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)
 */
641
bool QGCDataPlot2D::linearRegression(double *x, double *y, int n, double *a, double *b, double *r)
642
643
644
645
646
647
648
649
650
{
    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)
651
        return true;
652
653

    /* Conpute some things we need */
654
    for (i=0; i<n; i++) {
655
656
657
658
659
660
661
662
663
664
665
666
        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)
667
        return false;
668
669
670
671
672
673
674
675
676
677
678

    /* 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);

679
    return false;
680
681
682
683
684
685
}

void QGCDataPlot2D::saveCsvLog()
{
    QString fileName = "export.csv";
    fileName = QFileDialog::getSaveFileName(
686
687
                   this, "Export CSV File Name", QDesktopServices::storageLocation(QDesktopServices::DesktopLocation),
                   "CSV file (*.csv);;Text file (*.txt)");
lm's avatar
lm committed
688

689
    if (!fileName.contains(".")) {
lm's avatar
lm committed
690
691
692
693
        // .csv is default extension
        fileName.append(".csv");
    }

694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
    //    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;
    //        fileName = QFileDialog::getSaveFileName(
    //                this, "Export CSV File Name", QDesktopServices::storageLocation(QDesktopServices::DesktopLocation),
    //            "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;
    }
}