Compilation fix
[opentx.git] / companion / src / logsdialog.cpp
blobc2e5228099e86e4974aa1559a1b2bd382563c754
1 /*
2 * Copyright (C) OpenTX
4 * Based on code named
5 * th9x - http://code.google.com/p/th9x
6 * er9x - http://code.google.com/p/er9x
7 * gruvin9x - http://code.google.com/p/gruvin9x
9 * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License version 2 as
13 * published by the Free Software Foundation.
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
21 #include <math.h>
22 #include "logsdialog.h"
23 #include "appdata.h"
24 #include "ui_logsdialog.h"
25 #include "helpers.h"
26 #if defined _MSC_VER || !defined __GNUC__
27 #include <windows.h>
28 #else
29 #include <unistd.h>
30 #endif
32 LogsDialog::LogsDialog(QWidget *parent) :
33 QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint),
34 ui(new Ui::LogsDialog),
35 tracerMaxAlt(0),
36 cursorA(0),
37 cursorB(0),
38 cursorLine(0)
40 csvlog.clear();
42 ui->setupUi(this);
43 setWindowIcon(CompanionIcon("logs.png"));
45 plotLock=false;
47 colors.append(Qt::green);
48 colors.append(Qt::red);
49 colors.append(Qt::yellow);
50 colors.append(Qt::magenta);
51 colors.append(Qt::cyan);
52 colors.append(Qt::darkBlue);
53 colors.append(Qt::darkGreen);
54 colors.append(Qt::darkRed);
55 colors.append(Qt::darkYellow);
56 colors.append(Qt::darkMagenta);
57 colors.append(Qt::darkCyan);
58 colors.append(Qt::blue);
59 pen.setWidthF(1.0);
61 // create and prepare a plot title layout element
62 QCPPlotTitle *title = new QCPPlotTitle(ui->customPlot);
63 title->setText(tr("Telemetry logs"));
65 // add it to the main plot layout
66 ui->customPlot->plotLayout()->insertRow(0);
67 ui->customPlot->plotLayout()->addElement(0, 0, title);
68 ui->customPlot->setNoAntialiasingOnDrag(true);
69 ui->customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectAxes | QCP::iSelectLegend | QCP::iSelectPlottables);
71 axisRect = ui->customPlot->axisRect();
72 axisRect->axis(QCPAxis::atBottom)->setLabel(tr("Time (hh:mm:ss)"));
73 axisRect->axis(QCPAxis::atBottom)->setTickLabelType(QCPAxis::ltDateTime);
74 axisRect->axis(QCPAxis::atBottom)->setDateTimeFormat("hh:mm:ss");
75 QDateTime now = QDateTime::currentDateTime();
76 axisRect->axis(QCPAxis::atBottom)->setRange(now.addSecs(-60*60*2).toTime_t(), now.toTime_t());
77 axisRect->axis(QCPAxis::atLeft)->setTickLabels(false);
78 axisRect->addAxis(QCPAxis::atLeft);
79 axisRect->addAxis(QCPAxis::atRight);
80 axisRect->axis(QCPAxis::atLeft, 1)->setVisible(false);
81 axisRect->axis(QCPAxis::atRight, 1)->setVisible(false);
83 QFont legendFont = font();
84 legendFont.setPointSize(10);
85 ui->customPlot->legend->setFont(legendFont);
86 ui->customPlot->legend->setSelectedFont(legendFont);
87 axisRect->insetLayout()->setInsetAlignment(0, Qt::AlignTop | Qt::AlignLeft);
89 rightLegend = new QCPLegend;
90 axisRect->insetLayout()->addElement(rightLegend, Qt::AlignTop | Qt::AlignRight);
91 rightLegend->setLayer("legend");
92 rightLegend->setFont(legendFont);
93 rightLegend->setSelectedFont(legendFont);
94 rightLegend->setVisible(false);
96 ui->customPlot->setAutoAddPlottableToLegend(false);
98 QString path = g.gePath();
99 if (path.isEmpty() || !QFile(path).exists()) {
100 ui->mapsButton->hide();
103 // connect slot that ties some axis selections together (especially opposite axes):
104 connect(ui->customPlot, SIGNAL(selectionChangedByUser()), this, SLOT(selectionChanged()));
105 // connect slots that takes care that when an axis is selected, only that direction can be dragged and zoomed:
106 connect(ui->customPlot, SIGNAL(mousePress(QMouseEvent*)), this, SLOT(mousePress(QMouseEvent*)));
107 connect(ui->customPlot, SIGNAL(mouseWheel(QWheelEvent*)), this, SLOT(mouseWheel()));
109 // make left axes transfer its range to right axes:
110 connect(axisRect->axis(QCPAxis::atLeft), SIGNAL(rangeChanged(QCPRange)), this, SLOT(yAxisChangeRanges(QCPRange)));
112 // connect some interaction slots:
113 connect(ui->customPlot, SIGNAL(titleDoubleClick(QMouseEvent*, QCPPlotTitle*)), this, SLOT(titleDoubleClick(QMouseEvent*, QCPPlotTitle*)));
114 connect(ui->customPlot, SIGNAL(axisDoubleClick(QCPAxis*,QCPAxis::SelectablePart,QMouseEvent*)), this, SLOT(axisLabelDoubleClick(QCPAxis*,QCPAxis::SelectablePart)));
115 connect(ui->customPlot, SIGNAL(legendDoubleClick(QCPLegend*,QCPAbstractLegendItem*,QMouseEvent*)), this, SLOT(legendDoubleClick(QCPLegend*,QCPAbstractLegendItem*)));
116 connect(ui->FieldsTW, SIGNAL(itemSelectionChanged()), this, SLOT(plotLogs()));
117 connect(ui->logTable, SIGNAL(itemSelectionChanged()), this, SLOT(plotLogs()));
118 connect(ui->Reset_PB, SIGNAL(clicked()), this, SLOT(plotLogs()));
121 LogsDialog::~LogsDialog()
123 delete ui;
126 void LogsDialog::titleDoubleClick(QMouseEvent *evt, QCPPlotTitle *title)
128 // Set the plot title by double clicking on it
129 bool ok;
130 QString newTitle = QInputDialog::getText(this, tr("Plot Title Change"), tr("New plot title:"), QLineEdit::Normal, title->text(), &ok);
131 if (ok) {
132 title->setText(newTitle);
133 ui->customPlot->replot();
137 void LogsDialog::axisLabelDoubleClick(QCPAxis *axis, QCPAxis::SelectablePart part)
139 // Set an axis label by double clicking on it
140 if (part == QCPAxis::spAxisLabel) {
141 // only react when the actual axis label is clicked, not tick label or axis backbone
142 bool ok;
143 QString newLabel = QInputDialog::getText(this, tr("Axis Label Change"), tr("New axis label:"), QLineEdit::Normal, axis->label(), &ok);
144 if (ok) {
145 axis->setLabel(newLabel);
146 ui->customPlot->replot();
151 void LogsDialog::legendDoubleClick(QCPLegend *legend, QCPAbstractLegendItem *item)
153 // Rename a graph by double clicking on its legend item
154 if (item) {
155 // only react if item was clicked (user could have clicked on border padding of legend where there is no item, then item is 0)
156 QCPPlottableLegendItem *plItem = qobject_cast<QCPPlottableLegendItem*>(item);
157 bool ok;
158 QString newName = QInputDialog::getText(this, tr("Graph Name Change"), tr("New graph name:"), QLineEdit::Normal, plItem->plottable()->name(), &ok);
159 if (ok) {
160 plItem->plottable()->setName(newName);
161 ui->customPlot->replot();
166 void LogsDialog::selectionChanged()
169 normally, axis base line, axis tick labels and axis labels are selectable separately, but we want
170 the user only to be able to select the axis as a whole, so we tie the selected states of the tick labels
171 and the axis base line together. However, the axis label shall be selectable individually.
173 The selection state of the left and right axes shall be synchronized as well as the state of the
174 bottom and top axes.
176 Further, we want to synchronize the selection of the graphs with the selection state of the respective
177 legend item belonging to that graph. So the user can select a graph by either clicking on the graph itself
178 or on its legend item.
181 if (plotLock) return;
183 // handle bottom axis and tick labels as one selectable object:
184 if (axisRect->axis(QCPAxis::atBottom)->selectedParts().testFlag(QCPAxis::spAxis) ||
185 axisRect->axis(QCPAxis::atBottom)->selectedParts().testFlag(QCPAxis::spTickLabels))
187 axisRect->axis(QCPAxis::atBottom)->setSelectedParts(QCPAxis::spAxis |
188 QCPAxis::spTickLabels);
190 // make left and right axes be selected synchronously,
191 // and handle axis and tick labels as one selectable object:
192 if (axisRect->axis(QCPAxis::atLeft)->selectedParts().testFlag(QCPAxis::spAxis) ||
193 axisRect->axis(QCPAxis::atLeft)->selectedParts().testFlag(QCPAxis::spTickLabels) ||
195 axisRect->axis(QCPAxis::atRight)->visible() &&
196 (axisRect->axis(QCPAxis::atRight)->selectedParts().testFlag(QCPAxis::spAxis) ||
197 axisRect->axis(QCPAxis::atRight)->selectedParts().testFlag(QCPAxis::spTickLabels))
198 ) || (
199 axisRect->axis(QCPAxis::atLeft, 1)->visible() &&
200 (axisRect->axis(QCPAxis::atLeft, 1)->selectedParts().testFlag(QCPAxis::spAxis) ||
201 axisRect->axis(QCPAxis::atLeft, 1)->selectedParts().testFlag(QCPAxis::spTickLabels))
202 ) || (
203 axisRect->axis(QCPAxis::atRight)->visible() &&
204 (axisRect->axis(QCPAxis::atRight, 1)->selectedParts().testFlag(QCPAxis::spAxis) ||
205 axisRect->axis(QCPAxis::atRight, 1)->selectedParts().testFlag(QCPAxis::spTickLabels))
208 axisRect->axis(QCPAxis::atLeft)->setSelectedParts(QCPAxis::spAxis |
209 QCPAxis::spTickLabels);
210 if (axisRect->axis(QCPAxis::atRight)->visible()) {
211 axisRect->axis(QCPAxis::atRight)->setSelectedParts(QCPAxis::spAxis |
212 QCPAxis::spTickLabels);
213 if (axisRect->axis(QCPAxis::atLeft, 1)->visible()) {
214 axisRect->axis(QCPAxis::atLeft, 1)->setSelectedParts(QCPAxis::spAxis |
215 QCPAxis::spTickLabels);
216 if (axisRect->axis(QCPAxis::atRight, 1)->visible()) {
217 axisRect->axis(QCPAxis::atRight, 1)->setSelectedParts(QCPAxis::spAxis |
218 QCPAxis::spTickLabels);
224 // synchronize selection of graphs with selection of corresponding legend items:
225 for (int i=0; i<ui->customPlot->graphCount(); ++i) {
226 QCPGraph *graph = ui->customPlot->graph(i);
227 QCPPlottableLegendItem *item = ui->customPlot->legend->itemWithPlottable(graph);
228 if (item == NULL) item = rightLegend->itemWithPlottable(graph);
229 if (item->selected() || graph->selected()) {
230 item->setSelected(true);
231 graph->setSelected(true);
236 QList<QStringList> LogsDialog::filterGePoints(const QList<QStringList> & input)
238 QList<QStringList> result;
240 int n = input.count();
241 if (n == 0) {
242 return result;
245 int gpscol = 0;
246 for (int i=1; i<input.at(0).count(); i++) {
247 if (input.at(0).at(i) == "GPS") {
248 gpscol=i;
251 if (gpscol == 0) {
252 QMessageBox::critical(this, tr("Error: no GPS data not found"),
253 tr("The column containing GPS coordinates must be named \"GPS\".\n\n\
254 The columns for altitude \"GAlt\" and for speed \"GSpd\" are optional"));
255 return result;
258 result.append(input.at(0));
259 bool rangeSelected = ui->logTable->selectionModel()->selectedRows().length() > 0;
261 GpsGlitchFilter glitchFilter;
262 GpsLatLonFilter latLonFilter;
264 for (int i = 1; i < n; i++) {
265 if ((ui->logTable->item(i-1, 1)->isSelected() && rangeSelected) || !rangeSelected) {
267 GpsCoord coord = extractGpsCoordinates(input.at(i).at(gpscol));
269 // glitch filter
270 if ( glitchFilter.isGlitch(coord) ) {
271 // qDebug() << "filterGePoints(): GPS glitch detected at" << i << coord.latitude << coord.longitude;
272 continue;
275 // lat long pair filter
276 if ( !latLonFilter.isValid(coord) ) {
277 // qDebug() << "filterGePoints(): Lat-Lon pair wrong, skipping at" << i << coord.latitude << coord.longitude;
278 continue;
281 // qDebug() << "point " << latitude << longitude;
282 result.append(input.at(i));
286 // qDebug() << "filterGePoints(): filtered from" << input.count() << "to " << result.count() << "points";
287 return result;
290 void LogsDialog::exportToGoogleEarth()
292 // filter data points
293 QList<QStringList> dataPoints = filterGePoints(csvlog);
294 int n = dataPoints.count(); // number of points to export
295 if (n==0) return;
297 int gpscol=0, altcol=0, speedcol=0;
298 double altMultiplier = 1.0;
300 QSet<int> nondataCols;
301 for (int i=1; i<dataPoints.at(0).count(); i++) {
302 // Long,Lat,Course,GPS Speed,GPS Alt
303 if (dataPoints.at(0).at(i) == "GPS") {
304 gpscol=i;
306 if (dataPoints.at(0).at(i).contains("GAlt")) {
307 altcol = i;
308 nondataCols << i;
309 if (dataPoints.at(0).at(i).contains("(ft)")) {
310 altMultiplier = 0.3048; // feet to meters
313 if (dataPoints.at(0).at(i).contains("GSpd")) {
314 speedcol = i;
315 nondataCols << i;
319 if (gpscol==0 ) {
320 return;
323 // qDebug() << "gpscol" << gpscol << "altcol" << altcol << "speedcol" << speedcol << "altMultiplier" << altMultiplier;
324 QString geIconFilename = generateProcessUniqueTempFileName("track0.png");
325 if (QFile::exists(geIconFilename)) {
326 QFile::remove(geIconFilename);
328 QFile::copy(":/images/track0.png", geIconFilename);
330 QString geFilename = generateProcessUniqueTempFileName("flight.kml");
331 if (QFile::exists(geFilename)) {
332 QFile::remove(geFilename);
334 QFile geFile(geFilename);
335 if (!geFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
336 QMessageBox::warning(this, tr("Error"),
337 tr("Cannot write file %1:\n%2.")
338 .arg(geFilename)
339 .arg(geFile.errorString()));
340 return;
343 QTextStream outputStream(&geFile);
345 // file header
346 outputStream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\" xmlns:gx=\"http://www.google.com/kml/ext/2.2\">\n";
347 outputStream << "\t<Document>\n\t\t<name>" << logFilename << "</name>\n";
348 outputStream << "\t\t<Style id=\"multiTrack_n\">\n\t\t\t<IconStyle>\n\t\t\t\t<Icon>\n\t\t\t\t\t<href>file://" << geIconFilename << "</href>\n\t\t\t\t</Icon>\n\t\t\t</IconStyle>\n\t\t\t<LineStyle>\n\t\t\t\t<color>991081f4</color>\n\t\t\t\t<width>6</width>\n\t\t\t</LineStyle>\n\t\t</Style>\n";
349 outputStream << "\t\t<Style id=\"multiTrack_h\">\n\t\t\t<IconStyle>\n\t\t\t\t<scale>0</scale>\n\t\t\t\t<Icon>\n\t\t\t\t\t<href>file://" << geIconFilename << "</href>\n\t\t\t\t</Icon>\n\t\t\t</IconStyle>\n\t\t\t<LineStyle>\n\t\t\t\t<color>991081f4</color>\n\t\t\t\t<width>8</width>\n\t\t\t</LineStyle>\n\t\t</Style>\n";
350 outputStream << "\t\t<StyleMap id=\"multiTrack\">\n\t\t\t<Pair>\n\t\t\t\t<key>normal</key>\n\t\t\t\t<styleUrl>#multiTrack_n</styleUrl>\n\t\t\t</Pair>\n\t\t\t<Pair>\n\t\t\t\t<key>highlight</key>\n\t\t\t\t<styleUrl>#multiTrack_h</styleUrl>\n\t\t\t</Pair>\n\t\t</StyleMap>\n";
351 outputStream << "\t\t<Style id=\"lineStyle\">\n\t\t\t<LineStyle>\n\t\t\t\t<color>991081f4</color>\n\t\t\t\t<width>6</width>\n\t\t\t</LineStyle>\n\t\t</Style>\n";
352 outputStream << "\t\t<Schema id=\"schema\">\n";
353 outputStream << "\t\t\t<gx:SimpleArrayField name=\"GPSSpeed\" type=\"float\">\n\t\t\t\t<displayName>GPS Speed</displayName>\n\t\t\t</gx:SimpleArrayField>\n";
355 // declare additional fields
356 for (int i=0; i<dataPoints.at(0).count()-2; i++) {
357 if (ui->FieldsTW->item(i, 0) && ui->FieldsTW->item(i, 0)->isSelected() && !nondataCols.contains(i+2)) {
358 QString origName = dataPoints.at(0).at(i+2);
359 QString safeName = origName;
360 safeName.replace(" ","_");
361 outputStream << "\t\t\t<gx:SimpleArrayField name=\""<< safeName <<"\" ";
362 outputStream << "type=\"string\""; // additional fields have fixed type: string
363 outputStream << ">\n\t\t\t\t<displayName>" << origName << "</displayName>\n\t\t\t</gx:SimpleArrayField>\n";
367 QString planeName;
368 if (logFilename.indexOf("-")>0) {
369 planeName=logFilename.left(logFilename.indexOf("-"));
370 } else {
371 planeName=logFilename;
374 outputStream << "\t\t</Schema>\n";
375 outputStream << "\t\t<Folder>\n\t\t\t<name>Log Data</name>\n\t\t\t<Placemark>\n\t\t\t\t<name>" << planeName << "</name>";
376 outputStream << "\n\t\t\t\t<styleUrl>#multiTrack</styleUrl>";
377 outputStream << "\n\t\t\t\t<gx:Track>\n";
378 outputStream << "\n\t\t\t\t\t<altitudeMode>absolute</altitudeMode>\n";
380 // time data points
381 for (int i=1; i<n; i++) {
382 QString tstamp=dataPoints.at(i).at(0)+QString("T")+dataPoints.at(i).at(1)+QString("Z");
383 outputStream << "\t\t\t\t\t<when>"<< tstamp <<"</when>\n";
386 // coordinate data points
387 outputStream.setRealNumberNotation(QTextStream::FixedNotation);
388 outputStream.setRealNumberPrecision(8);
389 for (int i=1; i<n; i++) {
390 GpsCoord coord = extractGpsCoordinates(dataPoints.at(i).at(gpscol));
391 int altitude = altcol ? (dataPoints.at(i).at(altcol).toFloat() * altMultiplier) : 0;
392 outputStream << "\t\t\t\t\t<gx:coord>" << coord.longitude << " " << coord.latitude << " " << altitude << " </gx:coord>\n" ;
395 // additional data for data points
396 outputStream << "\t\t\t\t\t<ExtendedData>\n\t\t\t\t\t\t<SchemaData schemaUrl=\"#schema\">\n";
398 if (speedcol) {
399 // gps speed data points
400 outputStream << "\t\t\t\t\t\t\t<gx:SimpleArrayData name=\"GPSSpeed\">\n";
401 for (int i=1; i<n; i++) {
402 outputStream << "\t\t\t\t\t\t\t\t<gx:value>"<< dataPoints.at(i).at(speedcol) <<"</gx:value>\n";
404 outputStream << "\t\t\t\t\t\t\t</gx:SimpleArrayData>\n";
407 // add values for additional fields
408 for (int i=0; i<dataPoints.at(0).count()-2; i++) {
409 if (ui->FieldsTW->item(i, 0) && ui->FieldsTW->item(i, 0)->isSelected() && !nondataCols.contains(i+2)) {
410 QString safeName = dataPoints.at(0).at(i+2);;
411 safeName.replace(" ","_");
412 outputStream << "\t\t\t\t\t\t\t<gx:SimpleArrayData name=\""<< safeName <<"\">\n";
413 for (int j=1; j<n; j++) {
414 outputStream << "\t\t\t\t\t\t\t\t<gx:value>"<< dataPoints.at(j).at(i+2) <<"</gx:value>\n";
416 outputStream << "\t\t\t\t\t\t\t</gx:SimpleArrayData>\n";
420 outputStream << "\t\t\t\t\t\t</SchemaData>\n\t\t\t\t\t</ExtendedData>\n\t\t\t\t</gx:Track>\n\t\t\t</Placemark>\n\t\t</Folder>\n\t</Document>\n</kml>";
421 geFile.close();
423 QString gePath = g.gePath();
424 QStringList parameters;
425 #ifdef __APPLE__
426 parameters << "-a";
427 parameters << gePath;
428 gePath = "/usr/bin/open";
429 #endif
430 parameters << geFilename;
431 QProcess *process = new QProcess(this);
432 process->start(gePath, parameters);
435 void LogsDialog::on_mapsButton_clicked()
437 ui->FieldsTW->setDisabled(true);
438 ui->logTable->setDisabled(true);
440 exportToGoogleEarth();
442 ui->FieldsTW->setDisabled(false);
443 ui->logTable->setDisabled(false);
446 void LogsDialog::mousePress(QMouseEvent * event)
448 // if an axis is selected, only allow the direction of that axis to be dragged
449 // if no axis is selected, both directions may be dragged
451 if (axisRect->axis(QCPAxis::atBottom)->selectedParts().testFlag(QCPAxis::spAxis))
452 axisRect->setRangeDrag(axisRect->axis(QCPAxis::atBottom)->orientation());
453 else if (axisRect->axis(QCPAxis::atLeft)->selectedParts().testFlag(QCPAxis::spAxis))
454 axisRect->setRangeDrag(axisRect->axis(QCPAxis::atLeft)->orientation());
455 else
456 axisRect->setRangeDrag(Qt::Horizontal | Qt::Vertical);
458 if (event->button() == Qt::RightButton) {
459 double x = axisRect->axis(QCPAxis::atBottom)->pixelToCoord(event->pos().x());
460 placeCursor(x, event->modifiers() & Qt::ShiftModifier);
464 void LogsDialog::placeCursor(double x, bool second)
466 QCPItemTracer * cursor = second ? cursorB : cursorA;
468 if (cursor) {
469 cursor->setGraphKey(x);
470 cursor->updatePosition();
471 cursor->setVisible(true);
474 if (cursorA && cursorB) {
475 updateCursorsLabel();
479 void LogsDialog::updateCursorsLabel()
481 QString text = QString("Max Altitude: %1 m\n").arg(tracerMaxAlt->position->value(), 0, 'f', 1);
482 if (cursorA->visible() ) {
483 text += tr("Cursor A: %1 m").arg(cursorA->position->value(), 0, 'f', 1) + "\n";
485 if (cursorB->visible() ) {
486 text += tr("Cursor B: %1 m").arg(cursorB->position->value(), 0, 'f', 1) + "\n";
488 if (cursorA->visible() && cursorB->visible()) {
489 cursorLine->setVisible(true);
490 // calc deltas
491 double deltaX = cursorB->position->key() - cursorA->position->key();
492 double deltaY = cursorB->position->value() - cursorA->position->value();
493 double slope = 0;
494 if (deltaX != 0) {
495 slope = deltaY / deltaX;
497 qDebug() << "Cursors: dt:" << formatTimeDelta(deltaX) << "dy:" << deltaY << "rate:" << slope;
498 text += tr("Time delta: %1").arg(formatTimeDelta(deltaX)) + "\n";
499 text += tr("Climb rate: %1 m/s").arg(slope, 0, 'f', fabs(slope)<1.0 ? 2 : 1) + "\n";
501 ui->labelCursors->setText(text);
504 QString LogsDialog::formatTimeDelta(double timeDelta)
506 if (abs(int(timeDelta)) < 10) {
507 return QString("%1 s").arg(timeDelta, 1, 'f', 1);
510 int seconds = (int)round(fabs(timeDelta));
511 int hours = seconds / 3600;
512 seconds %= 3600;
513 int minutes = seconds / 60;
514 seconds %= 60;
516 if (hours) {
517 return QString("%1h:%2:%3").arg(hours).arg(minutes, 2, 10, QChar('0')).arg(seconds, 2, 10, QChar('0'));
519 else if (minutes) {
520 return QString("%1m:%2").arg(minutes).arg(seconds, 2, 10, QChar('0'));
522 else {
523 return QString("%1 s").arg(seconds);
527 void LogsDialog::mouseWheel()
529 // if an axis is selected, only allow the direction of that axis to be zoomed
530 // if no axis is selected, both directions may be zoomed
531 int orientation=0;
532 if (ui->ZoomX_ChkB->isChecked()) {
533 orientation|=Qt::Horizontal;
535 if (ui->ZoomY_ChkB->isChecked()) {
536 orientation|=Qt::Vertical;
538 if (orientation) {
539 axisRect->setRangeZoom((Qt::Orientation)orientation);
540 } else {
541 axisRect->setRangeZoom(Qt::Horizontal|Qt::Vertical);
545 void LogsDialog::removeAllGraphs()
547 ui->customPlot->clearGraphs();
548 ui->customPlot->clearItems();
549 ui->customPlot->legend->setVisible(false);
550 rightLegend->clearItems();
551 rightLegend->setVisible(false);
552 axisRect->axis(QCPAxis::atRight)->setSelectedParts(QCPAxis::spNone);
553 axisRect->axis(QCPAxis::atRight)->setVisible(false);
554 axisRect->axis(QCPAxis::atLeft)->setSelectedParts(QCPAxis::spNone);
555 axisRect->axis(QCPAxis::atLeft)->setTickLabels(false);
556 axisRect->axis(QCPAxis::atLeft, 1)->setVisible(false);
557 axisRect->axis(QCPAxis::atLeft, 1)->setSelectedParts(QCPAxis::spNone);
558 axisRect->axis(QCPAxis::atRight, 1)->setVisible(false);
559 axisRect->axis(QCPAxis::atRight, 1)->setSelectedParts(QCPAxis::spNone);
560 axisRect->axis(QCPAxis::atBottom)->setSelectedParts(QCPAxis::spNone);
561 ui->customPlot->replot();
562 tracerMaxAlt = 0;
563 cursorA = 0;
564 cursorB = 0;
565 cursorLine = 0;
566 ui->labelCursors->setText("");
569 void LogsDialog::on_fileOpen_BT_clicked()
571 QString fileName = QFileDialog::getOpenFileName(this, tr("Select your log file"), g.logDir());
572 if (!fileName.isEmpty()) {
573 g.logDir(fileName);
574 ui->FileName_LE->setText(fileName);
575 if (cvsFileParse()) {
576 ui->FieldsTW->clear();
577 ui->logTable->clear();
578 ui->FieldsTW->setShowGrid(false);
579 ui->FieldsTW->setContentsMargins(0,0,0,0);
580 ui->FieldsTW->setRowCount(csvlog.at(0).count()-2);
581 ui->FieldsTW->setColumnCount(1);
582 ui->FieldsTW->setHorizontalHeaderLabels(QStringList(tr("Available fields")));
583 ui->logTable->setSelectionBehavior(QAbstractItemView::SelectRows);
584 for (int i=2; i<csvlog.at(0).count(); i++) {
585 QTableWidgetItem* item= new QTableWidgetItem(csvlog.at(0).at(i));
586 ui->FieldsTW->setItem(i-2, 0, item);
588 ui->FieldsTW->resizeRowsToContents();
589 ui->logTable->setColumnCount(csvlog.at(0).count());
590 ui->logTable->setRowCount(csvlog.count()-1);
591 ui->logTable->setHorizontalHeaderLabels(csvlog.at(0));
593 QAbstractItemModel *model = ui->logTable->model();
594 for (int i=1; i<csvlog.count(); i++) {
595 for (int j=0; j<csvlog.at(0).count(); j++) {
596 model->setData(model->index(i - 1, j, QModelIndex()), csvlog.at(i).at(j));
600 ui->logTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
601 QVarLengthArray<int> sizes;
602 for (int i = 0; i < ui->logTable->columnCount(); i++) {
603 sizes.append(ui->logTable->columnWidth(i));
605 ui->logTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
606 for (int i = 0; i < ui->logTable->columnCount(); i++) {
607 ui->logTable->setColumnWidth(i, sizes.at(i));
613 bool LogsDialog::cvsFileParse()
615 QFile file(ui->FileName_LE->text());
616 int errors=0;
617 int lines=-1;
619 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { // reading HEX TEXT file
620 return false;
622 else {
623 csvlog.clear();
624 logFilename.clear();
625 QTextStream inputStream(&file);
626 QString buffer = file.readLine();
628 if (buffer.startsWith("Date,Time")) {
629 file.reset();
631 else {
632 return false;
635 int numfields=-1;
636 while (!file.atEnd()) {
637 QString line = file.readLine().trimmed();
638 QStringList columns = line.split(',');
639 if (numfields==-1) {
640 numfields=columns.count();
642 if (columns.count()==numfields) {
643 csvlog.append(columns);
645 else {
646 errors++;
648 lines++;
651 logFilename = QFileInfo(file.fileName()).baseName();
654 file.close();
655 if (errors > 1) {
656 QMessageBox::warning(this, "Companion", tr("The selected logfile contains %1 invalid lines out of %2 total lines").arg(errors).arg(lines));
659 int n = csvlog.count();
660 if (n == 1) {
661 csvlog.clear();
662 return false;
665 plotLock = true;
666 setFlightSessions();
667 plotLock = false;
669 return true;
672 struct FlightSession {
673 QDateTime start;
674 QDateTime end;
677 QDateTime LogsDialog::getRecordTimeStamp(int index)
679 QString tstamp = csvlog.at(index).at(0) + " " + csvlog.at(index).at(1);
680 if (csvlog.at(index).at(1).contains("."))
681 return QDateTime::fromString(tstamp, "yyyy-MM-dd HH:mm:ss.zzz");
682 return QDateTime::fromString(tstamp, "yyyy-MM-dd HH:mm:ss");
685 QString LogsDialog::generateDuration(const QDateTime & start, const QDateTime & end)
687 int secs = start.secsTo(end);
688 QString durationString;
689 if (secs >= 3600) {
690 durationString = QString("%1:").arg(secs/3600);
691 secs %= 3600;
693 durationString += QString("%1:%2").arg(secs/60, 2, 10, QChar('0')).arg(secs%60, 2, 10, QChar('0'));
694 return durationString;
697 void LogsDialog::setFlightSessions()
699 ui->sessions_CB->clear();
701 int n = csvlog.count();
702 // qDebug() << "records" << n;
704 // find session breaks
705 QList<int> sessions;
706 QDateTime lastvalue;
707 for (int i = 1; i < n; i++) {
708 QDateTime tmp = getRecordTimeStamp(i);
709 if (!lastvalue.isValid() || lastvalue.secsTo(tmp) > 60) {
710 sessions.push_back(i-1);
711 // qDebug() << "session index" << i-1;
713 lastvalue = tmp;
715 sessions.push_back(n-1);
717 //now construct a list of sessions with their times
718 //total time
719 int noSesions = sessions.size()-1;
720 QString label = QString("%1 ").arg(noSesions);
721 label += tr(noSesions > 1 ? "sessions" : "session");
722 label += " <" + tr("total duration ") + generateDuration(getRecordTimeStamp(1), getRecordTimeStamp(n-1)) + ">";
723 ui->sessions_CB->addItem(label);
725 // add individual sessions
726 if (sessions.size() > 2) {
727 for (int i = 1; i < sessions.size(); i++) {
728 QDateTime sessionStart = getRecordTimeStamp(sessions.at(i-1)+1);
729 QDateTime sessionEnd = getRecordTimeStamp(sessions.at(i));
730 QString label = sessionStart.toString("HH:mm:ss") + " <" + tr("duration ") + generateDuration(sessionStart, sessionEnd) + ">";
731 ui->sessions_CB->addItem(label, sessions.at(i-1));
732 // qDebug() << "added label" << label << sessions.at(i-1);
737 void LogsDialog::on_sessions_CB_currentIndexChanged(int index)
739 if (plotLock) return;
740 plotLock = true;
742 ui->logTable->clearSelection();
744 if (index != 0) {
745 int bottom;
746 if (index < ui->sessions_CB->count() - 1) {
747 bottom = ui->sessions_CB->itemData(index + 1, Qt::UserRole).toInt();
748 } else {
749 bottom = ui->logTable->rowCount();
752 QModelIndex topLeft = ui->logTable->model()->index(
753 ui->sessions_CB->itemData(index, Qt::UserRole).toInt(), 0 , QModelIndex());
754 QModelIndex bottomRight = ui->logTable->model()->index(
755 bottom - 1, ui->logTable->columnCount() - 1, QModelIndex());
757 QItemSelection selection(topLeft, bottomRight);
758 ui->logTable->selectionModel()->select(selection, QItemSelectionModel::Select);
761 plotLock = false;
762 plotLogs();
765 void LogsDialog::plotLogs()
767 if (plotLock) return;
769 if (!ui->FieldsTW->selectedItems().length()) {
770 removeAllGraphs();
771 return;
774 plotsCollection plots;
776 QModelIndexList selection = ui->logTable->selectionModel()->selectedRows();
777 int rowCount = selection.length();
778 bool hasLogSelection;
779 QVarLengthArray<int> selectedRows;
781 if (rowCount) {
782 hasLogSelection = true;
783 foreach (QModelIndex index, selection) {
784 selectedRows.append(index.row());
786 qSort(selectedRows.begin(), selectedRows.end());
787 } else {
788 hasLogSelection = false;
789 rowCount = ui->logTable->rowCount();
792 plots.min_x = QDateTime::currentDateTime().toTime_t();
793 plots.max_x = 0;
795 foreach (QTableWidgetItem *plot, ui->FieldsTW->selectedItems()) {
796 coords plotCoords;
797 int plotColumn = plot->row() + 2; // Date and Time first
799 plotCoords.min_y = INVALID_MIN;
800 plotCoords.max_y = INVALID_MAX;
801 plotCoords.yaxis = firstLeft;
802 plotCoords.name = plot->text();
804 for (int row = 0; row < rowCount; row++) {
805 QTableWidgetItem *logValue;
806 double y;
807 double time;
808 QString time_str;
810 if (hasLogSelection) {
811 logValue = ui->logTable->item(selectedRows.at(row), plotColumn);
812 time_str = ui->logTable->item(selectedRows.at(row), 0)->text() +
813 QString(" ") + ui->logTable->item(selectedRows.at(row), 1)->text();
814 } else {
815 logValue = ui->logTable->item(row, plotColumn);
816 time_str = ui->logTable->item(row, 0)->text() + QString(" ") +
817 ui->logTable->item(row, 1)->text();
820 y = logValue->text().toDouble();
821 plotCoords.y.push_back(y);
823 if (plotCoords.min_y > y) plotCoords.min_y = y;
824 if (plotCoords.max_y < y) plotCoords.max_y = y;
826 if (time_str.contains('.')) {
827 time = QDateTime::fromString(time_str, "yyyy-MM-dd HH:mm:ss.zzz")
828 .toTime_t();
829 time += time_str.mid(time_str.indexOf('.')).toDouble();
830 } else {
831 time = QDateTime::fromString(time_str, "yyyy-MM-dd HH:mm:ss")
832 .toTime_t();
834 plotCoords.x.push_back(time);
836 if (plots.min_x > time) plots.min_x = time;
837 if (plots.max_x < time) plots.max_x = time;
840 double range_inc = (plotCoords.max_y - plotCoords.min_y) / 100;
841 if (range_inc == 0) range_inc = 1;
842 plotCoords.max_y += range_inc;
843 plotCoords.min_y -= range_inc;
845 plots.coords.append(plotCoords);
848 yAxesRanges[firstLeft].min = plots.coords.at(0).min_y;
849 yAxesRanges[firstLeft].max = plots.coords.at(0).max_y;
850 for (int i = firstRight; i < AXES_LIMIT; i++) {
851 yAxesRanges[i].min = INVALID_MIN;
852 yAxesRanges[i].max = INVALID_MAX;
854 plots.tooManyRanges = false;
856 for (int i = 1; i < plots.coords.size(); i++) {
857 double actualRange = yAxesRanges[firstLeft].max - yAxesRanges[firstLeft].min;
858 double thisRange = plots.coords.at(i).max_y - plots.coords.at(i).min_y;
860 while (yAxesRanges[plots.coords.at(i).yaxis].max != INVALID_MAX &&
861 (thisRange > actualRange * 1.3 || thisRange * 1.3 < actualRange ||
862 plots.coords.at(i).min_y > yAxesRanges[plots.coords.at(i).yaxis].max ||
863 plots.coords.at(i).max_y < yAxesRanges[plots.coords.at(i).yaxis].min)
866 switch (plots.coords[i].yaxis) {
867 case firstLeft:
868 plots.coords[i].yaxis = firstRight;
869 break;
870 case firstRight:
871 plots.coords[i].yaxis = secondLeft;
872 break;
873 case secondLeft:
874 plots.coords[i].yaxis = secondRight;
875 break;
876 case secondRight:
877 plots.tooManyRanges = true;
878 break;
879 default:
880 break;
882 if (plots.tooManyRanges) break;
884 actualRange = yAxesRanges[plots.coords.at(i).yaxis].max
885 - yAxesRanges[plots.coords.at(i).yaxis].min;
888 if (plots.tooManyRanges) {
889 break;
890 } else {
891 if (plots.coords.at(i).min_y < yAxesRanges[plots.coords.at(i).yaxis].min) {
892 yAxesRanges[plots.coords.at(i).yaxis].min = plots.coords.at(i).min_y;
894 if (plots.coords.at(i).max_y > yAxesRanges[plots.coords.at(i).yaxis].max) {
895 yAxesRanges[plots.coords.at(i).yaxis].max = plots.coords.at(i).max_y;
900 if (plots.tooManyRanges) {
901 yAxesRanges[firstLeft].max = 101;
902 yAxesRanges[firstLeft].min = -1;
903 yAxesRanges[firstRight].max = INVALID_MAX;
904 yAxesRanges[firstRight].min = INVALID_MIN;
905 yAxesRanges[secondLeft].max = INVALID_MAX;
906 yAxesRanges[secondLeft].min = INVALID_MIN;
907 yAxesRanges[secondRight].max = INVALID_MAX;
908 yAxesRanges[secondRight].min = INVALID_MIN;
910 for (int i = 0; i < plots.coords.size(); i++) {
911 plots.coords[i].yaxis = firstLeft;
913 double factor = 100 / (plots.coords.at(i).max_y - plots.coords.at(i).min_y);
914 for (int j = 0; j < plots.coords.at(i).y.count(); j++) {
915 plots.coords[i].y[j] = factor * (plots.coords.at(i).y.at(j) - plots.coords.at(i).min_y);
918 } else {
919 for (int i = firstRight; i < AXES_LIMIT; i++) {
920 if (yAxesRanges[i].max == INVALID_MAX) break;
922 yAxesRatios[i] = (yAxesRanges[i].max - yAxesRanges[i].min) /
923 (yAxesRanges[firstLeft].max - yAxesRanges[firstLeft].min);
927 removeAllGraphs();
929 axisRect->axis(QCPAxis::atBottom)->setRange(plots.min_x, plots.max_x);
931 axisRect->axis(QCPAxis::atLeft)->setRange(yAxesRanges[firstLeft].min,
932 yAxesRanges[firstLeft].max);
934 if (plots.tooManyRanges) {
935 axisRect->axis(QCPAxis::atLeft)->setTickLabels(false);
936 } else {
937 axisRect->axis(QCPAxis::atLeft)->setTickLabels(true);
940 if (yAxesRanges[firstRight].max != INVALID_MAX) {
941 axisRect->axis(QCPAxis::atRight)->setRange(yAxesRanges[firstRight].min,
942 yAxesRanges[firstRight].max);
943 axisRect->axis(QCPAxis::atRight)->setVisible(true);
945 rightLegend->setVisible(true);
947 if (yAxesRanges[secondLeft].max != INVALID_MAX) {
948 axisRect->axis(QCPAxis::atLeft, 1)->setVisible(true);
949 axisRect->axis(QCPAxis::atLeft, 1)->setRange(yAxesRanges[secondLeft].min,
950 yAxesRanges[secondLeft].max);
952 if (yAxesRanges[secondRight].max != INVALID_MAX) {
953 axisRect->axis(QCPAxis::atRight, 1)->setVisible(true);
954 axisRect->axis(QCPAxis::atRight, 1)->setRange(yAxesRanges[secondRight].min,
955 yAxesRanges[secondRight].max);
960 for (int i = 0; i < plots.coords.size(); i++) {
961 switch (plots.coords[i].yaxis) {
962 case firstLeft:
963 ui->customPlot->addGraph();
964 if (yAxesRanges[secondLeft].max != INVALID_MAX) {
965 ui->customPlot->graph(i)->setName(plots.coords.at(i).name + tr(" (L1)"));
966 } else {
967 ui->customPlot->graph(i)->setName(plots.coords.at(i).name);
969 ui->customPlot->legend->addItem(
970 new QCPPlottableLegendItem(ui->customPlot->legend, ui->customPlot->graph(i)));
971 break;
972 case firstRight:
973 ui->customPlot->addGraph(axisRect->axis(QCPAxis::atBottom),
974 axisRect->axis(QCPAxis::atRight));
975 if (yAxesRanges[secondRight].max != INVALID_MAX) {
976 ui->customPlot->graph(i)->setName(plots.coords.at(i).name + tr(" (R1)"));
977 } else {
978 ui->customPlot->graph(i)->setName(plots.coords.at(i).name);
980 rightLegend->addItem(
981 new QCPPlottableLegendItem(rightLegend, ui->customPlot->graph(i)));
982 break;
983 case secondLeft:
984 ui->customPlot->addGraph(axisRect->axis(QCPAxis::atBottom),
985 axisRect->axis(QCPAxis::atLeft, 1));
986 ui->customPlot->graph(i)->setName(plots.coords.at(i).name + tr(" (L2)"));
987 ui->customPlot->legend->addItem(
988 new QCPPlottableLegendItem(ui->customPlot->legend, ui->customPlot->graph(i)));
989 break;
990 case secondRight:
991 ui->customPlot->addGraph(axisRect->axis(QCPAxis::atBottom),
992 axisRect->axis(QCPAxis::atRight, 1));
993 ui->customPlot->graph(i)->setName(plots.coords.at(i).name + tr(" (R2)"));
994 rightLegend->addItem(
995 new QCPPlottableLegendItem(rightLegend, ui->customPlot->graph(i)));
996 break;
997 default:
998 break;
1001 ui->customPlot->graph(i)->setData(plots.coords.at(i).x,
1002 plots.coords.at(i).y);
1003 pen.setColor(colors.at(i % colors.size()));
1004 ui->customPlot->graph(i)->setPen(pen);
1006 if (!tracerMaxAlt && (plots.coords.at(i).name.endsWith("(m)") ||
1007 plots.coords.at(i).name.endsWith(" Alt") ||
1008 plots.coords.at(i).name.endsWith("(ft)"))) {
1009 addMaxAltitudeMarker(plots.coords.at(i), ui->customPlot->graph(i));
1010 countNumberOfThrows(plots.coords.at(i), ui->customPlot->graph(i));
1011 addCursor(&cursorA, ui->customPlot->graph(i), Qt::blue);
1012 addCursor(&cursorB, ui->customPlot->graph(i), Qt::red);
1013 addCursorLine(&cursorLine, ui->customPlot->graph(i), Qt::black);
1014 updateCursorsLabel();
1018 ui->customPlot->legend->setVisible(true);
1019 ui->customPlot->replot();
1022 void LogsDialog::yAxisChangeRanges(QCPRange range)
1024 if (axisRect->axis(QCPAxis::atRight)->visible()) {
1025 double lowerChange = (range.lower - yAxesRanges[firstLeft].min) *
1026 yAxesRatios[firstRight];
1027 double upperChange = (range.upper - yAxesRanges[firstLeft].max) *
1028 yAxesRatios[firstRight];
1030 yAxesRanges[firstRight].min += lowerChange;
1031 yAxesRanges[firstRight].max += upperChange;
1032 axisRect->axis(QCPAxis::atRight)->setRange(yAxesRanges[firstRight].min,
1033 yAxesRanges[firstRight].max);
1035 if (axisRect->axisCount(QCPAxis::atLeft) == 2) {
1036 lowerChange = (range.lower - yAxesRanges[firstLeft].min) *
1037 yAxesRatios[secondLeft];
1038 upperChange = (range.upper - yAxesRanges[firstLeft].max) *
1039 yAxesRatios[secondLeft];
1041 yAxesRanges[secondLeft].min += lowerChange;
1042 yAxesRanges[secondLeft].max += upperChange;
1043 axisRect->axis(QCPAxis::atLeft, 1)->setRange(yAxesRanges[secondLeft].min,
1044 yAxesRanges[secondLeft].max);
1046 if (axisRect->axisCount(QCPAxis::atRight) == 2) {
1047 lowerChange = (range.lower - yAxesRanges[firstLeft].min) *
1048 yAxesRatios[secondRight];
1049 upperChange = (range.upper - yAxesRanges[firstLeft].max) *
1050 yAxesRatios[secondRight];
1052 yAxesRanges[secondRight].min += lowerChange;
1053 yAxesRanges[secondRight].max += upperChange;
1054 axisRect->axis(QCPAxis::atRight, 1)->setRange(yAxesRanges[secondRight].min,
1055 yAxesRanges[secondRight].max);
1059 yAxesRanges[firstLeft].min = range.lower;
1060 yAxesRanges[firstLeft].max = range.upper;
1065 void LogsDialog::addMaxAltitudeMarker(const coords & c, QCPGraph * graph) {
1066 // find max altitude
1067 int positionIndex = 0;
1068 double maxAlt = -100000;
1070 for(int i=0; i<c.x.count(); ++i) {
1071 double alt = c.y.at(i);
1072 if (alt > maxAlt) {
1073 maxAlt = alt;
1074 positionIndex = i;
1075 // qDebug() << "max alt: " << maxAlt << "@" << result;
1078 // qDebug() << "max alt: " << maxAlt << "@" << positionIndex;
1080 // add max altitude marker
1081 tracerMaxAlt = new QCPItemTracer(ui->customPlot);
1082 ui->customPlot->addItem(tracerMaxAlt);
1083 tracerMaxAlt->setGraph(graph);
1084 tracerMaxAlt->setInterpolating(true);
1085 tracerMaxAlt->setStyle(QCPItemTracer::tsSquare);
1086 tracerMaxAlt->setPen(QPen(Qt::blue));
1087 tracerMaxAlt->setBrush(Qt::NoBrush);
1088 tracerMaxAlt->setSize(7);
1089 tracerMaxAlt->setGraphKey(c.x.at(positionIndex));
1090 tracerMaxAlt->updatePosition();
1093 void LogsDialog::countNumberOfThrows(const coords & c, QCPGraph * graph)
1095 #if 0
1096 // find all launches
1097 // TODO
1098 double startTime = c.x.at(0);
1100 for(int i=0; i<c.x.count(); ++i) {
1101 double alt = c.y.at(i);
1102 double time = c.x.at(i);
1104 #endif
1107 void LogsDialog::addCursor(QCPItemTracer ** cursor, QCPGraph * graph, const QColor & color) {
1108 QCPItemTracer * c = new QCPItemTracer(ui->customPlot);
1109 ui->customPlot->addItem(c);
1110 c->setGraph(graph);
1111 c->setInterpolating(false);
1112 c->setStyle(QCPItemTracer::tsCrosshair);
1113 QPen pen(color);
1114 pen.setStyle(Qt::DashLine);
1115 c->setPen(pen);
1116 c->setBrush(color);
1117 c->setSize(7);
1118 c->setVisible(false);
1119 *cursor = c;
1122 void LogsDialog::addCursorLine(QCPItemStraightLine ** line, QCPGraph * graph, const QColor & color) {
1123 QCPItemStraightLine * l = new QCPItemStraightLine(ui->customPlot);
1124 ui->customPlot->addItem(l);
1125 l->point1->setParentAnchor(cursorA->position);
1126 l->point2->setParentAnchor(cursorB->position);
1127 QPen pen(color);
1128 pen.setStyle(Qt::DashLine);
1129 l->setPen(pen);
1130 l->setVisible(false);
1131 *line = l;