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

#include <QTemporaryFile>
#include <QPrintDialog>
#include <QProgressDialog>
#include <QHBoxLayout>
#include <QSvgGenerator>
#include <QPrinter>
37
#include <QStandardPaths>
Don Gagne's avatar
Don Gagne committed
38
39
40
41
#include <QDebug>

#include <cmath>

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

QGCDataPlot2D::QGCDataPlot2D(QWidget *parent) :
49
    QWidget(parent),
50
    plot(new IncrementalPlot(parent)),
51
52
    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

    // Allow style changes to propagate through this widget
75
    connect(qgcApp(), &QGCApplication::styleChanged, plot, &IncrementalPlot::styleChanged);
76
77
78
79
}

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

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

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

/**
118
119
120
121
122
 * 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(
123
        this, "Save Plot File", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation),
124
        "PDF Documents (*.pdf);;SVG Images (*.svg)",
125
        "pdf");
126
127
128
129
130
    return fileName;
}

/**
 * This function aks the user for a filename and exports to either PDF or SVG, depending on the filename
131
 */
132
133
void QGCDataPlot2D::savePlot()
{
134
    QString fileName = getSavePlotFilename();
135
136
    if (fileName.isEmpty())
        return;
lm's avatar
lm committed
137

138
    while(!(fileName.endsWith(".svg") || fileName.endsWith(".pdf"))) {
dogmaphobic's avatar
dogmaphobic committed
139
140
141
142
143
        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);
144
        // Abort if cancelled
Don Gagne's avatar
Don Gagne committed
145
146
147
        if (button == QMessageBox::Cancel) {
            return;
        }
148
149

        fileName = getSavePlotFilename();
150
151
        if (fileName.isEmpty())
            return; //Abort if cancelled
152
153
    }

154
    if (fileName.endsWith(".pdf")) {
155
        exportPDF(fileName);
156
    } else if (fileName.endsWith(".svg")) {
157
158
        exportSVG(fileName);
    }
159
160
161
162
163
164
165
166
167
168
169
}


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

    QString docName = plot->title().text();
170
    if ( !docName.isEmpty() ) {
171
172
173
174
175
176
177
178
        docName.replace (QRegExp (QString::fromLatin1 ("\n")), tr (" -- "));
        printer.setDocName (docName);
    }

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

    QPrintDialog dialog(&printer);
179
    if ( dialog.exec() ) {
pixhawk's avatar
pixhawk committed
180
        plot->setStyleSheet("QWidget { background-color: #FFFFFF; color: #000000; background-clip: border; font-size: 10pt;}");
181
        plot->setCanvasBackground(Qt::white);
182
183
184
185
186
187
188
189
190
191
192
193
194
195
        // 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
196
197
        plot->setStyleSheet("QWidget { background-color: #050508; color: #DDDDDF; background-clip: border; font-size: 11pt;}");
        //plot->setCanvasBackground(QColor(5, 5, 8));
198
199
200
    }
}

201
202
203
204
205
206
207
208
209
210
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();
211
    if ( !docName.isEmpty() ) {
212
213
214
215
216
217
218
219
220
        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);
221
    // FIXME: QwtPlotPrintFilter no longer exists in Qwt 6.1
222
223
224
225
226
227
228
229
230
231
232
233
234
    //        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);
    //        }
235
    //plot->print(printer);
236
237
238
239
    plot->setStyleSheet("QWidget { background-color: #050508; color: #DDDDDF; background-clip: border; font-size: 11pt;}");
    //plot->setCanvasBackground(QColor(5, 5, 8));
}

240
241
void QGCDataPlot2D::exportSVG(QString fileName)
{
242
    if ( !fileName.isEmpty() ) {
243
244
        plot->setStyleSheet("QWidget { background-color: #FFFFFF; color: #000000; background-clip: border; font-size: 10pt;}");
        //plot->setCanvasBackground(Qt::white);
245
246
247
248
        QSvgGenerator generator;
        generator.setFileName(fileName);
        generator.setSize(QSize(800, 600));

249
250
251
252
253
254
255
        // 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);
256

257
        //plot->print(generator);
258
        plot->setStyleSheet("QWidget { background-color: #050508; color: #DDDDDF; background-clip: border; font-size: 11pt;}");
259
260
261
262
263
264
265
266
    }
}

/**
 * Selects a filename and attempts immediately to load it.
 */
void QGCDataPlot2D::selectFile()
{
267
268
    // Open a file dialog prompting the user for the file to load.
    // Note the special case for the Pixhawk.
269
    if (ui->inputFileType->currentText().contains("pxIMU") || ui->inputFileType->currentText().contains("RAW")) {
270
        fileName = QGCFileDialog::getOpenFileName(this, tr("Load Log File"), QString(), "Log Files (*.imu *.raw)");
271
272
273
    }
    else
    {
274
        fileName = QGCFileDialog::getOpenFileName(this, tr("Load Log File"), QString(), "Log Files (*.csv);;All Files (*)");
lm's avatar
lm committed
275
276
    }

277
    // Check if the user hit cancel, which results in an empty string.
278
    // If this is the case, we just stop.
279
    if (fileName.isEmpty())
280
281
282
    {
        return;
    }
283

284
    // Now attempt to open the file
285
    QFileInfo fileInfo(fileName);
286
    if (!fileInfo.isReadable())
287
    {
288
289
290
291
292
        // 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()));
293
    }
294
295
    else
    {
296
297
298
299
300
301
302
303
304
        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)
{
305
306
    Q_UNUSED(xAxisName);
    Q_UNUSED(yAxisFilter);
lm's avatar
lm committed
307

308
    if (logFile != NULL) {
309
310
311
312
        logFile->close();
        delete logFile;
    }
    // Postprocess log file
lm's avatar
lm committed
313
    logFile = new QTemporaryFile("qt_qgc_temp_log.XXXXXX.csv");
314
    compressor = new LogCompressor(file, logFile->fileName());
lm's avatar
lm committed
315
    connect(compressor, SIGNAL(finishedFile(QString)), this, SLOT(loadFile(QString)));
316
    compressor->startCompression();
317
318
}

319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
/**
 * 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
 */
337
338
void QGCDataPlot2D::loadCsvLog(QString file, QString xAxisName, QString yAxisFilter)
{
339
    if (logFile != NULL) {
340
341
        logFile->close();
        delete logFile;
342
        curveNames.clear();
343
344
345
346
347
348
349
    }
    logFile = new QFile(file);

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

350
351
352
353
354
    // 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());

355
356
357
358
359
360
361
362
363
    // Extract header

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

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

364
365
366
367
368
369
370
371
372
373
374
375
    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
376
377
    for (int i = 0; i < header.length(); i++) {
        if (sepCandidates.contains(header.at(i))) {
378
            // Separator found
379
            if (charRead) {
380
381
                separator += header[i];
            }
382
        } else {
383
384
385
386
387
388
389
390
391
392
393
394
395
            // 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;
396
397
398
399

    // Clear plot
    plot->removeData();

400
    QMap<QString, QVector<double>* > xValues;
401
402
    QMap<QString, QVector<double>* > yValues;

403
    curveNames.append(header.split(separator, QString::SkipEmptyParts));
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418

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

419
420
421
422
423
    QString curveName;

    // Clear UI elements
    ui->xAxis->clear();
    ui->yAxis->clear();
424
425
426
    ui->xRegressionComboBox->clear();
    ui->yRegressionComboBox->clear();
    ui->regressionOutput->clear();
427
428
429

    int curveNameIndex = 0;

430
    QString xAxisFilter;
431
    if (xAxisName == "") {
432
        xAxisFilter = curveNames.first();
433
    } else {
434
435
        xAxisFilter = xAxisName;
    }
436

LM's avatar
LM committed
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
463
464
    // 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));
//        }
    }


465
    foreach(curveName, curveNames) {
466
        // Add to plot x axis selection
467
        ui->xAxis->addItem(curveName);
468
469
470
        // Add to regression selection
        ui->xRegressionComboBox->addItem(curveName);
        ui->yRegressionComboBox->addItem(curveName);
471
        if (curveName != xAxisFilter) {
LM's avatar
LM committed
472
            if ((yAxisFilter == "") || yCurves.contains(curveName)) {
pixhawk's avatar
pixhawk committed
473
                yValues.insert(curveName, new QVector<double>());
474
                xValues.insert(curveName, new QVector<double>());
pixhawk's avatar
pixhawk committed
475
                // Add separator starting with second item
476
                if (curveNameIndex > 0 && curveNameIndex < curveNames.count()) {
pixhawk's avatar
pixhawk committed
477
478
                    ui->yAxis->setText(ui->yAxis->text()+"|");
                }
LM's avatar
LM committed
479
480
481
482
483
484
                // 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);
485
                curveNameIndex++;
486
487
488
489
            }
        }
    }

490
491
492
    // Select current axis in UI
    ui->xAxis->setCurrentIndex(curveNames.indexOf(xAxisFilter));

493
494
    // Read data

pixhawk's avatar
pixhawk committed
495
496
    double x = 0;
    double y = 0;
497

498
499
    while (!in.atEnd())
    {
500
501
        QString line = in.readLine();

502
503
504
505
        // Keep empty parts here - we still have to act on them
        QStringList values = line.split(separator, QString::KeepEmptyParts);

        bool headerfound = false;
506

507
508
509
510
511
        // First get header - ORDER MATTERS HERE!
        foreach(curveName, curveNames)
        {
            if (curveName == xAxisFilter)
            {
512
                // X  AXIS HANDLING
513

514
                // Take this value as x if it is selected
515
516
517
518
519
520
521
522
523
524
525
526
527
                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;
                    }
                }
            }
        }
528

529
530
531
532
533
534
        if (headerfound)
        {
            // Search again from start for values - ORDER MATTERS HERE!
            foreach(curveName, curveNames)
            {
                // Y  AXIS HANDLING
LM's avatar
LM committed
535
536
                // 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)))
537
538
539
540
541
542
543
544
545
546
547
548
549
550
                {
                    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);
551
552
553
                            yValues.value(curveName)->append(y);
                        }
                    }
554
555
556
557
558
                }
            }
        }
    }

559
560
    // Add data array of each curve to the plot at once (fast)
    // Iterates through all x-y curve combinations
561
    for (int i = 0; i < yValues.count(); i++) {
LM's avatar
LM committed
562
563
564
565
566
567
568
569
        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());
        }
570
    }
571
    plot->updateScale();
572
573
574
575
576
    plot->setStyleText(ui->style->currentText());
}

bool QGCDataPlot2D::calculateRegression()
{
577
    // TODO: Add support for quadratic / cubic curve fitting
578
    return calculateRegression(ui->xRegressionComboBox->currentText(), ui->yRegressionComboBox->currentText(), "linear");
579
580
581
582
583
584
585
586
587
588
589
}

/**
 * @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;
590
591
    if (xName != yName) {
        if (QFileInfo(fileName).isReadable()) {
592
593
594
595
            loadCsvLog(fileName, xName, yName);
            ui->xRegressionComboBox->setCurrentIndex(curveNames.indexOf(xName));
            ui->yRegressionComboBox->setCurrentIndex(curveNames.indexOf(yName));
        }
596

597
598
599
600
601
        // 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().
602
        const int size = 100000;
603
604
        double *x = new double[size];
        double *y = new double[size];
605
606
        int copied = plot->data(yName, x, y, size);

607
        if (method == "linear") {
608
609
610
            double a;  // Y-axis crossing
            double b;  // Slope
            double r;  // Regression coefficient
611
            if (linearRegression(x, y, copied, &a, &b, &r)) {
612
613
614
615
616
617
618
619
620
                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)));
621
622

                result = true;
623
            } else {
624
625
                function = tr("Linear regression failed. (Limit: %1 data points. Try with less)").arg(size);
            }
626
        } else {
627
628
            function = tr("Regression method %1 not found").arg(method);
        }
629

630
631
        delete x;
        delete y;
632
    } else {
633
634
        // xName == yName
        function = tr("Please select different X and Y dimensions, not %1 = %2").arg(xName, yName);
635
    }
636
637
    ui->regressionOutput->setText(function);
    return result;
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
}

/**
 * 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)
 */
655
bool QGCDataPlot2D::linearRegression(double *x, double *y, int n, double *a, double *b, double *r)
656
657
658
659
660
661
662
663
664
{
    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)
665
        return true;
666
667

    /* Conpute some things we need */
668
    for (i=0; i<n; i++) {
669
670
671
672
673
674
675
676
677
678
679
680
        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)
681
        return false;
682
683
684
685
686
687
688
689
690
691
692

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

693
    return false;
694
695
696
697
}

void QGCDataPlot2D::saveCsvLog()
{
698
    QString fileName = QGCFileDialog::getSaveFileName(
699
        this, "Save CSV Log File", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation),
700
        "CSV Files (*.csv)",
dogmaphobic's avatar
dogmaphobic committed
701
702
        "csv",
        true);
703

dogmaphobic's avatar
dogmaphobic committed
704
    if (fileName.isEmpty()) {
705
        return; //User cancelled
dogmaphobic's avatar
dogmaphobic committed
706
    }
lm's avatar
lm committed
707

708
709
    bool success = logFile->copy(fileName);

710
    qDebug() << "Saved CSV log (" << fileName << "). Success: " << success;
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730

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