QGCDataPlot2D.cc 25.9 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

#include <QTemporaryFile>
dogmaphobic's avatar
dogmaphobic committed
32
#ifndef __mobile__
33
#include <QPrintDialog>
dogmaphobic's avatar
dogmaphobic committed
34
35
#include <QPrinter>
#endif
36
37
38
#include <QProgressDialog>
#include <QHBoxLayout>
#include <QSvgGenerator>
39
#include <QStandardPaths>
Don Gagne's avatar
Don Gagne committed
40
41
42
43
#include <QDebug>

#include <cmath>

44
45
46
#include "QGCDataPlot2D.h"
#include "ui_QGCDataPlot2D.h"
#include "MG.h"
Don Gagne's avatar
Don Gagne committed
47
#include "QGCFileDialog.h"
Don Gagne's avatar
Don Gagne committed
48
#include "QGCMessageBox.h"
49
50

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

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

    // Connect user actions
Tomaz Canabrava's avatar
Tomaz Canabrava committed
65
66
67
68
69
70
71
72
73
74
75
76
77
78
    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.
79
    connect(ui->regressionButton, SIGNAL(clicked()), this, SLOT(calculateRegression()));
80
81

    // Allow style changes to propagate through this widget
82
    connect(qgcApp(), &QGCApplication::styleChanged, plot, &IncrementalPlot::styleChanged);
83
84
85
86
}

void QGCDataPlot2D::reloadFile()
{
87
88
    if (QFileInfo(fileName).isReadable()) {
        if (ui->inputFileType->currentText().contains("pxIMU") || ui->inputFileType->currentText().contains("RAW")) {
89
            loadRawLog(fileName, ui->xAxis->currentText(), ui->yAxis->text());
90
        } else if (ui->inputFileType->currentText().contains("CSV")) {
91
92
93
94
95
96
97
            loadCsvLog(fileName, ui->xAxis->currentText(), ui->yAxis->text());
        }
    }
}

void QGCDataPlot2D::loadFile()
{
lm's avatar
lm committed
98
    qDebug() << "DATA PLOT: Loading file:" << fileName;
99
100
    if (QFileInfo(fileName).isReadable()) {
        if (ui->inputFileType->currentText().contains("pxIMU") || ui->inputFileType->currentText().contains("RAW")) {
101
            loadRawLog(fileName);
102
        } else if (ui->inputFileType->currentText().contains("CSV")) {
103
104
105
106
107
            loadCsvLog(fileName);
        }
    }
}

108
109
void QGCDataPlot2D::loadFile(QString file)
{
110
111
    // 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.
112
    fileName = file;
113
114
115
    QFileInfo fi(fileName);
    if (fi.isReadable()) {
        if (fi.suffix() == QString("raw") || fi.suffix() == QString("imu")) {
116
            loadRawLog(fileName);
117
        } else if (fi.suffix() == QString("txt") || fi.suffix() == QString("csv")) {
118
119
            loadCsvLog(fileName);
        }
120
        // TODO Else, tell the user it doesn't know what to do with the file...
121
122
123
124
    }
}

/**
125
126
127
128
129
 * 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(
130
        this, "Save Plot File", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation),
131
        "PDF Documents (*.pdf);;SVG Images (*.svg)",
132
        "pdf");
133
134
135
136
137
    return fileName;
}

/**
 * This function aks the user for a filename and exports to either PDF or SVG, depending on the filename
138
 */
139
140
void QGCDataPlot2D::savePlot()
{
141
    QString fileName = getSavePlotFilename();
142
143
    if (fileName.isEmpty())
        return;
lm's avatar
lm committed
144

145
    while(!(fileName.endsWith(".svg") || fileName.endsWith(".pdf"))) {
dogmaphobic's avatar
dogmaphobic committed
146
147
148
149
150
        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);
151
        // Abort if cancelled
Don Gagne's avatar
Don Gagne committed
152
153
154
        if (button == QMessageBox::Cancel) {
            return;
        }
155
156

        fileName = getSavePlotFilename();
157
158
        if (fileName.isEmpty())
            return; //Abort if cancelled
159
160
    }

161
    if (fileName.endsWith(".pdf")) {
162
        exportPDF(fileName);
163
    } else if (fileName.endsWith(".svg")) {
164
165
        exportSVG(fileName);
    }
166
167
168
169
170
}


void QGCDataPlot2D::print()
{
dogmaphobic's avatar
dogmaphobic committed
171
#ifndef __mobile__
172
173
174
175
176
177
    QPrinter printer(QPrinter::HighResolution);
    //    printer.setOutputFormat(QPrinter::PdfFormat);
    //    //QPrinter printer(QPrinter::HighResolution);
    //    printer.setOutputFileName(fileName);

    QString docName = plot->title().text();
178
    if ( !docName.isEmpty() ) {
179
180
181
182
183
184
185
186
        docName.replace (QRegExp (QString::fromLatin1 ("\n")), tr (" -- "));
        printer.setDocName (docName);
    }

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

    QPrintDialog dialog(&printer);
187
    if ( dialog.exec() ) {
pixhawk's avatar
pixhawk committed
188
        plot->setStyleSheet("QWidget { background-color: #FFFFFF; color: #000000; background-clip: border; font-size: 10pt;}");
189
        plot->setCanvasBackground(Qt::white);
190
191
192
193
194
195
196
197
198
199
200
201
202
203
        // 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);
pixhawk's avatar
pixhawk committed
204
205
        plot->setStyleSheet("QWidget { background-color: #050508; color: #DDDDDF; background-clip: border; font-size: 11pt;}");
        //plot->setCanvasBackground(QColor(5, 5, 8));
206
    }
dogmaphobic's avatar
dogmaphobic committed
207
#endif
208
209
}

210
211
void QGCDataPlot2D::exportPDF(QString fileName)
{
dogmaphobic's avatar
dogmaphobic committed
212
213
214
#ifdef __mobile__
    Q_UNUSED(fileName)
#else
215
216
217
218
219
220
221
222
    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();
223
    if ( !docName.isEmpty() ) {
224
225
226
227
228
229
230
231
232
        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);
233
    // FIXME: QwtPlotPrintFilter no longer exists in Qwt 6.1
234
235
236
237
238
239
240
241
242
243
244
245
246
    //        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);
    //        }
247
    //plot->print(printer);
248
249
    plot->setStyleSheet("QWidget { background-color: #050508; color: #DDDDDF; background-clip: border; font-size: 11pt;}");
    //plot->setCanvasBackground(QColor(5, 5, 8));
dogmaphobic's avatar
dogmaphobic committed
250
#endif
251
252
}

253
254
void QGCDataPlot2D::exportSVG(QString fileName)
{
dogmaphobic's avatar
dogmaphobic committed
255
256
257
#ifdef __mobile__
    Q_UNUSED(fileName)
#else
258
    if ( !fileName.isEmpty() ) {
259
260
        plot->setStyleSheet("QWidget { background-color: #FFFFFF; color: #000000; background-clip: border; font-size: 10pt;}");
        //plot->setCanvasBackground(Qt::white);
261
262
263
264
        QSvgGenerator generator;
        generator.setFileName(fileName);
        generator.setSize(QSize(800, 600));

265
266
267
268
269
270
271
        // 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);
272

273
        //plot->print(generator);
274
        plot->setStyleSheet("QWidget { background-color: #050508; color: #DDDDDF; background-clip: border; font-size: 11pt;}");
275
    }
dogmaphobic's avatar
dogmaphobic committed
276
#endif
277
278
279
280
281
282
283
}

/**
 * Selects a filename and attempts immediately to load it.
 */
void QGCDataPlot2D::selectFile()
{
284
285
    // Open a file dialog prompting the user for the file to load.
    // Note the special case for the Pixhawk.
286
    if (ui->inputFileType->currentText().contains("pxIMU") || ui->inputFileType->currentText().contains("RAW")) {
287
        fileName = QGCFileDialog::getOpenFileName(this, tr("Load Log File"), QString(), "Log Files (*.imu *.raw)");
288
289
290
    }
    else
    {
291
        fileName = QGCFileDialog::getOpenFileName(this, tr("Load Log File"), QString(), "Log Files (*.csv);;All Files (*)");
lm's avatar
lm committed
292
293
    }

294
    // Check if the user hit cancel, which results in an empty string.
295
    // If this is the case, we just stop.
296
    if (fileName.isEmpty())
297
298
299
    {
        return;
    }
300

301
    // Now attempt to open the file
302
    QFileInfo fileInfo(fileName);
303
    if (!fileInfo.isReadable())
304
    {
305
306
307
308
309
        // 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()));
310
    }
311
312
    else
    {
313
314
315
316
317
318
319
320
321
        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)
{
322
323
    Q_UNUSED(xAxisName);
    Q_UNUSED(yAxisFilter);
lm's avatar
lm committed
324

325
    if (logFile != NULL) {
326
327
328
329
        logFile->close();
        delete logFile;
    }
    // Postprocess log file
lm's avatar
lm committed
330
    logFile = new QTemporaryFile("qt_qgc_temp_log.XXXXXX.csv");
331
    compressor = new LogCompressor(file, logFile->fileName());
Tomaz Canabrava's avatar
Tomaz Canabrava committed
332
    connect(compressor, &LogCompressor::finishedFile, this, static_cast<void (QGCDataPlot2D::*)(QString)>(&QGCDataPlot2D::loadFile));
333
    compressor->startCompression();
334
335
}

336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
/**
 * 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
 */
354
355
void QGCDataPlot2D::loadCsvLog(QString file, QString xAxisName, QString yAxisFilter)
{
356
    if (logFile != NULL) {
357
358
        logFile->close();
        delete logFile;
359
        curveNames.clear();
360
361
362
363
364
365
366
    }
    logFile = new QFile(file);

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

367
368
369
370
371
    // 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());

372
373
374
375
376
377
378
379
380
    // Extract header

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

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

381
382
383
384
385
386
387
388
389
390
391
392
    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
393
394
    for (int i = 0; i < header.length(); i++) {
        if (sepCandidates.contains(header.at(i))) {
395
            // Separator found
396
            if (charRead) {
397
398
                separator += header[i];
            }
399
        } else {
400
401
402
403
404
405
406
407
408
409
410
411
412
            // 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;
413
414
415
416

    // Clear plot
    plot->removeData();

417
    QMap<QString, QVector<double>* > xValues;
418
419
    QMap<QString, QVector<double>* > yValues;

420
    curveNames.append(header.split(separator, QString::SkipEmptyParts));
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435

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

436
437
438
439
440
    QString curveName;

    // Clear UI elements
    ui->xAxis->clear();
    ui->yAxis->clear();
441
442
443
    ui->xRegressionComboBox->clear();
    ui->yRegressionComboBox->clear();
    ui->regressionOutput->clear();
444
445
446

    int curveNameIndex = 0;

447
    QString xAxisFilter;
448
    if (xAxisName == "") {
449
        xAxisFilter = curveNames.first();
450
    } else {
451
452
        xAxisFilter = xAxisName;
    }
453

LM's avatar
LM committed
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
    // 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));
//        }
    }


482
    foreach(curveName, curveNames) {
483
        // Add to plot x axis selection
484
        ui->xAxis->addItem(curveName);
485
486
487
        // Add to regression selection
        ui->xRegressionComboBox->addItem(curveName);
        ui->yRegressionComboBox->addItem(curveName);
488
        if (curveName != xAxisFilter) {
LM's avatar
LM committed
489
            if ((yAxisFilter == "") || yCurves.contains(curveName)) {
pixhawk's avatar
pixhawk committed
490
                yValues.insert(curveName, new QVector<double>());
491
                xValues.insert(curveName, new QVector<double>());
pixhawk's avatar
pixhawk committed
492
                // Add separator starting with second item
493
                if (curveNameIndex > 0 && curveNameIndex < curveNames.count()) {
pixhawk's avatar
pixhawk committed
494
495
                    ui->yAxis->setText(ui->yAxis->text()+"|");
                }
LM's avatar
LM committed
496
497
498
499
500
501
                // 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);
502
                curveNameIndex++;
503
504
505
506
            }
        }
    }

507
508
509
    // Select current axis in UI
    ui->xAxis->setCurrentIndex(curveNames.indexOf(xAxisFilter));

510
511
    // Read data

pixhawk's avatar
pixhawk committed
512
513
    double x = 0;
    double y = 0;
514

515
516
    while (!in.atEnd())
    {
517
518
        QString line = in.readLine();

519
520
521
522
        // Keep empty parts here - we still have to act on them
        QStringList values = line.split(separator, QString::KeepEmptyParts);

        bool headerfound = false;
523

524
525
526
527
528
        // First get header - ORDER MATTERS HERE!
        foreach(curveName, curveNames)
        {
            if (curveName == xAxisFilter)
            {
529
                // X  AXIS HANDLING
530

531
                // Take this value as x if it is selected
532
533
534
535
536
537
                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);
538
                    if (okx && !qIsNaN(x) && !qIsInf(x))
539
540
541
542
543
544
                    {
                        headerfound = true;
                    }
                }
            }
        }
545

546
547
548
549
550
551
        if (headerfound)
        {
            // Search again from start for values - ORDER MATTERS HERE!
            foreach(curveName, curveNames)
            {
                // Y  AXIS HANDLING
LM's avatar
LM committed
552
553
                // 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)))
554
555
556
557
558
559
560
561
562
563
                {
                    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
564
                        if (oky && !qIsNaN(y) && !qIsInf(y) && text.length() > 0 && text != " " && text != "\n" && text != "\r" && text != "\t")
565
566
567
                        {
                            // Only append definitely valid values
                            xValues.value(curveName)->append(x);
568
569
570
                            yValues.value(curveName)->append(y);
                        }
                    }
571
572
573
574
575
                }
            }
        }
    }

576
577
    // Add data array of each curve to the plot at once (fast)
    // Iterates through all x-y curve combinations
578
    for (int i = 0; i < yValues.count(); i++) {
LM's avatar
LM committed
579
580
581
582
583
584
585
586
        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());
        }
587
    }
588
    plot->updateScale();
589
590
591
592
593
    plot->setStyleText(ui->style->currentText());
}

bool QGCDataPlot2D::calculateRegression()
{
594
    // TODO: Add support for quadratic / cubic curve fitting
595
    return calculateRegression(ui->xRegressionComboBox->currentText(), ui->yRegressionComboBox->currentText(), "linear");
596
597
598
599
600
601
602
603
604
605
606
}

/**
 * @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;
607
608
    if (xName != yName) {
        if (QFileInfo(fileName).isReadable()) {
609
610
611
612
            loadCsvLog(fileName, xName, yName);
            ui->xRegressionComboBox->setCurrentIndex(curveNames.indexOf(xName));
            ui->yRegressionComboBox->setCurrentIndex(curveNames.indexOf(yName));
        }
613

614
615
616
617
618
        // 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().
619
        const int size = 100000;
620
621
        double *x = new double[size];
        double *y = new double[size];
622
623
        int copied = plot->data(yName, x, y, size);

624
        if (method == "linear") {
625
626
627
            double a;  // Y-axis crossing
            double b;  // Slope
            double r;  // Regression coefficient
628
            if (linearRegression(x, y, copied, &a, &b, &r)) {
629
630
631
632
633
634
635
636
637
                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)));
638
639

                result = true;
640
            } else {
641
642
                function = tr("Linear regression failed. (Limit: %1 data points. Try with less)").arg(size);
            }
643
        } else {
644
645
            function = tr("Regression method %1 not found").arg(method);
        }
646

Don Gagne's avatar
Don Gagne committed
647
648
        delete[] x;
        delete[] y;
649
    } else {
650
651
        // xName == yName
        function = tr("Please select different X and Y dimensions, not %1 = %2").arg(xName, yName);
652
    }
653
654
    ui->regressionOutput->setText(function);
    return result;
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
}

/**
 * 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)
 */
672
bool QGCDataPlot2D::linearRegression(double *x, double *y, int n, double *a, double *b, double *r)
673
674
675
676
677
678
679
680
681
{
    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)
682
        return true;
683
684

    /* Conpute some things we need */
685
    for (i=0; i<n; i++) {
686
687
688
689
690
691
692
693
694
695
696
697
        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)
698
        return false;
699
700
701
702
703
704
705
706
707
708
709

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

710
    return false;
711
712
713
714
}

void QGCDataPlot2D::saveCsvLog()
{
715
    QString fileName = QGCFileDialog::getSaveFileName(
716
        this, "Save CSV Log File", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation),
717
        "CSV Files (*.csv)",
dogmaphobic's avatar
dogmaphobic committed
718
719
        "csv",
        true);
720

dogmaphobic's avatar
dogmaphobic committed
721
    if (fileName.isEmpty()) {
722
        return; //User cancelled
dogmaphobic's avatar
dogmaphobic committed
723
    }
lm's avatar
lm committed
724

725
726
    bool success = logFile->copy(fileName);

727
    qDebug() << "Saved CSV log (" << fileName << "). Success: " << success;
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747

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