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
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"))) {
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());
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;
    }
}