Kstars

analyze.cpp
1/*
2 SPDX-FileCopyrightText: 2020 Hy Murveit <hy@murveit.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "analyze.h"
8
9#include <knotification.h>
10#include <QDateTime>
11#include <QShortcut>
12#include <QtGlobal>
13#include <QColor>
14
15#include "auxiliary/kspaths.h"
16#include "dms.h"
17#include "ekos/manager.h"
18#include "ekos/focus/curvefit.h"
19#include "fitsviewer/fitsdata.h"
20#include "fitsviewer/fitsviewer.h"
21#include "ksmessagebox.h"
22#include "kstars.h"
23#include "kstarsdata.h"
24#include "Options.h"
25#include "qcustomplot.h"
26
27#include <ekos_analyze_debug.h>
28#include <version.h>
29#include <QDesktopServices>
30#include <QFileDialog>
31
32// Subclass QCPAxisTickerDateTime, so that times are offset from the start
33// of the log, instead of being offset from the UNIX 0-seconds time.
34class OffsetDateTimeTicker : public QCPAxisTickerDateTime
35{
36 public:
37 void setOffset(double offset)
38 {
39 timeOffset = offset;
40 }
41 QString getTickLabel(double tick, const QLocale &locale, QChar formatChar, int precision) override
42 {
43 Q_UNUSED(precision);
44 Q_UNUSED(formatChar);
45 // Seconds are offset from the unix origin by
46 return locale.toString(keyToDateTime(tick + timeOffset).toTimeSpec(mDateTimeSpec), mDateTimeFormat);
47 }
48 private:
49 double timeOffset = 0;
50};
51
52namespace
53{
54
55// QDateTime is written to file with this format.
56QString timeFormat = "yyyy-MM-dd hh:mm:ss.zzz";
57
58// The resolution of the scroll bar.
59constexpr int MAX_SCROLL_VALUE = 10000;
60
61// Half the height of a timeline line.
62// That is timeline lines are horizontal bars along y=1 or y=2 ... and their
63// vertical widths are from y-halfTimelineHeight to y+halfTimelineHeight.
64constexpr double halfTimelineHeight = 0.35;
65
66// These are initialized in initStatsPlot when the graphs are added.
67// They index the graphs in statsPlot, e.g. statsPlot->graph(HFR_GRAPH)->addData(...)
68int HFR_GRAPH = -1;
69int TEMPERATURE_GRAPH = -1;
70int FOCUS_POSITION_GRAPH = -1;
71int NUM_CAPTURE_STARS_GRAPH = -1;
72int MEDIAN_GRAPH = -1;
73int ECCENTRICITY_GRAPH = -1;
74int NUMSTARS_GRAPH = -1;
75int SKYBG_GRAPH = -1;
76int SNR_GRAPH = -1;
77int RA_GRAPH = -1;
78int DEC_GRAPH = -1;
79int RA_PULSE_GRAPH = -1;
80int DEC_PULSE_GRAPH = -1;
81int DRIFT_GRAPH = -1;
82int RMS_GRAPH = -1;
83int CAPTURE_RMS_GRAPH = -1;
84int MOUNT_RA_GRAPH = -1;
85int MOUNT_DEC_GRAPH = -1;
86int MOUNT_HA_GRAPH = -1;
87int AZ_GRAPH = -1;
88int ALT_GRAPH = -1;
89int PIER_SIDE_GRAPH = -1;
90int TARGET_DISTANCE_GRAPH = -1;
91
92// This one is in timelinePlot.
93int ADAPTIVE_FOCUS_GRAPH = -1;
94
95// Initialized in initGraphicsPlot().
96int FOCUS_GRAPHICS = -1;
97int FOCUS_GRAPHICS_FINAL = -1;
98int FOCUS_GRAPHICS_CURVE = -1;
99int GUIDER_GRAPHICS = -1;
100
101// Brushes used in the timeline plot.
102const QBrush temporaryBrush(Qt::green, Qt::DiagCrossPattern);
103const QBrush timelineSelectionBrush(QColor(255, 100, 100, 150), Qt::SolidPattern);
104const QBrush successBrush(Qt::green, Qt::SolidPattern);
105const QBrush failureBrush(Qt::red, Qt::SolidPattern);
106const QBrush offBrush(Qt::gray, Qt::SolidPattern);
107const QBrush progressBrush(Qt::blue, Qt::SolidPattern);
108const QBrush progress2Brush(QColor(0, 165, 255), Qt::SolidPattern);
109const QBrush progress3Brush(Qt::cyan, Qt::SolidPattern);
110const QBrush progress4Brush(Qt::darkGreen, Qt::SolidPattern);
111const QBrush stoppedBrush(Qt::yellow, Qt::SolidPattern);
112const QBrush stopped2Brush(Qt::darkYellow, Qt::SolidPattern);
113
114// Utility to checks if a file exists and is not a directory.
115bool fileExists(const QString &path)
116{
117 QFileInfo info(path);
118 return info.exists() && info.isFile();
119}
120
121// Utilities to go between a mount status and a string.
122// Move to inditelescope.h/cpp?
123const QString mountStatusString(ISD::Mount::Status status)
124{
125 switch (status)
126 {
127 case ISD::Mount::MOUNT_IDLE:
128 return i18n("Idle");
129 case ISD::Mount::MOUNT_PARKED:
130 return i18n("Parked");
131 case ISD::Mount::MOUNT_PARKING:
132 return i18n("Parking");
133 case ISD::Mount::MOUNT_SLEWING:
134 return i18n("Slewing");
135 case ISD::Mount::MOUNT_MOVING:
136 return i18n("Moving");
137 case ISD::Mount::MOUNT_TRACKING:
138 return i18n("Tracking");
139 case ISD::Mount::MOUNT_ERROR:
140 return i18n("Error");
141 }
142 return i18n("Error");
143}
144
145ISD::Mount::Status toMountStatus(const QString &str)
146{
147 if (str == i18n("Idle"))
148 return ISD::Mount::MOUNT_IDLE;
149 else if (str == i18n("Parked"))
150 return ISD::Mount::MOUNT_PARKED;
151 else if (str == i18n("Parking"))
152 return ISD::Mount::MOUNT_PARKING;
153 else if (str == i18n("Slewing"))
154 return ISD::Mount::MOUNT_SLEWING;
155 else if (str == i18n("Moving"))
156 return ISD::Mount::MOUNT_MOVING;
157 else if (str == i18n("Tracking"))
158 return ISD::Mount::MOUNT_TRACKING;
159 else
160 return ISD::Mount::MOUNT_ERROR;
161}
162
163// Returns the stripe color used when drawing the capture timeline for various filters.
164// TODO: Not sure how to internationalize this.
165bool filterStripeBrush(const QString &filter, QBrush *brush)
166{
168
169 const QString rPattern("^(red|r)$");
170 if (QRegularExpression(rPattern, c).match(filter).hasMatch())
171 {
173 return true;
174 }
175 const QString gPattern("^(green|g)$");
176 if (QRegularExpression(gPattern, c).match(filter).hasMatch())
177 {
179 return true;
180 }
181 const QString bPattern("^(blue|b)$");
182 if (QRegularExpression(bPattern, c).match(filter).hasMatch())
183 {
185 return true;
186 }
187 const QString hPattern("^(ha|h|h-a|h_a|h-alpha|hydrogen|hydrogen_alpha|hydrogen-alpha|h_alpha|halpha)$");
188 if (QRegularExpression(hPattern, c).match(filter).hasMatch())
189 {
191 return true;
192 }
193 const QString oPattern("^(oiii|oxygen|oxygen_3|oxygen-3|oxygen_iii|oxygen-iii|o_iii|o-iii|o_3|o-3|o3)$");
194 if (QRegularExpression(oPattern, c).match(filter).hasMatch())
195 {
197 return true;
198 }
199 const QString
200 sPattern("^(sii|sulphur|sulphur_2|sulphur-2|sulphur_ii|sulphur-ii|sulfur|sulfur_2|sulfur-2|sulfur_ii|sulfur-ii|s_ii|s-ii|s_2|s-2|s2)$");
201 if (QRegularExpression(sPattern, c).match(filter).hasMatch())
202 {
203 // Pink.
204 *brush = QBrush(QColor(255, 182, 193), Qt::SolidPattern);
205 return true;
206 }
207 const QString lPattern("^(lpr|L|UV-IR cut|UV-IR|white|monochrome|broadband|clear|focus|luminance|lum|lps|cls)$");
208 if (QRegularExpression(lPattern, c).match(filter).hasMatch())
209 {
211 return true;
212 }
213 return false;
214}
215
216// Used when searching for FITS files to display.
217// If filename isn't found as is, it tries Options::analyzeAlternativeImageDirectory() in several ways
218// e.g. if filename = /1/2/3/4/name is not found, then try $dir/name,
219// then $dir/4/name, then $dir/3/4/name,
220// then $dir/2/3/4/name, and so on.
221// If it cannot find the FITS file, it returns an empty string, otherwise it returns
222// the full path where the file was found.
223QString findFilename(const QString &filename)
224{
225 const QString &alternateDirectory = Options::analyzeAlternativeDirectoryName();
226
227 // Try the origial full path.
228 QFileInfo info(filename);
229 if (info.exists() && info.isFile())
230 return filename;
231
232 // Try putting the filename at the end of the full path onto alternateDirectory.
233 QString name = info.fileName();
234 QString temp = QString("%1/%2").arg(alternateDirectory, name);
235 if (fileExists(temp))
236 return temp;
237
238 // Try appending the filename plus the ending directories onto alternateDirectory.
239 int size = filename.size();
240 int searchBackFrom = size - name.size();
241 int num = 0;
242 while (searchBackFrom >= 0)
243 {
244 int index = filename.lastIndexOf('/', searchBackFrom);
245 if (index < 0)
246 break;
247
248 QString temp2 = QString("%1%2").arg(alternateDirectory, filename.right(size - index));
249 if (fileExists(temp2))
250 return temp2;
251
252 searchBackFrom = index - 1;
253
254 // Paranoia
255 if (++num > 20)
256 break;
257 }
258 return "";
259}
260
261// This is an exhaustive search for now.
262// This is reasonable as the number of sessions should be limited.
263template <class T>
264class IntervalFinder
265{
266 public:
267 IntervalFinder() {}
268 ~IntervalFinder() {}
269 void add(T value)
270 {
271 intervals.append(value);
272 }
273 void clear()
274 {
275 intervals.clear();
276 }
277 QList<T> find(double t)
278 {
279 QList<T> result;
280 for (const auto &interval : intervals)
281 {
282 if (t >= interval.start && t <= interval.end)
283 result.push_back(interval);
284 }
285 return result;
286 }
287 // Finds the interval AFTER t, not including t
288 T *findNext(double t)
289 {
290 double bestStart = 1e7;
291 T *result = nullptr;
292 for (auto &interval : intervals)
293 {
294 if (interval.start > t && interval.start < bestStart)
295 {
296 bestStart = interval.start;
297 result = &interval;
298 }
299 }
300 return result;
301 }
302 // Finds the interval BEFORE t, not including t
303 T *findPrevious(double t)
304 {
305 double bestStart = -1e7;
306 T *result = nullptr;
307 for (auto &interval : intervals)
308 {
309 if (interval.start < t && interval.start > bestStart)
310 {
311 bestStart = interval.start;
312 result = &interval;
313 }
314 }
315 return result;
316 }
317 private:
318 QList<T> intervals;
319};
320
321IntervalFinder<Ekos::Analyze::CaptureSession> captureSessions;
322IntervalFinder<Ekos::Analyze::FocusSession> focusSessions;
323IntervalFinder<Ekos::Analyze::GuideSession> guideSessions;
324IntervalFinder<Ekos::Analyze::MountSession> mountSessions;
325IntervalFinder<Ekos::Analyze::AlignSession> alignSessions;
326IntervalFinder<Ekos::Analyze::MountFlipSession> mountFlipSessions;
327IntervalFinder<Ekos::Analyze::SchedulerJobSession> schedulerJobSessions;
328
329} // namespace
330
331namespace Ekos
332{
333
334// RmsFilter computes the RMS error of a 2-D sequence. Input the x error and y error
335// into newSample(). It returns the sqrt of a moving average of the squared
336// errors averaged over 40 samples.
337// It's used to compute RMS guider errors, where x and y would be RA and DEC errors.
338class RmsFilter
339{
340 public:
341 RmsFilter(int size = 40) : m_WindowSize(size) {}
342
343 double newSample(double x, double y)
344 {
345 m_XData.push_back(x);
346 m_YData.push_back(y);
347 m_XSum += x;
348 m_YSum += y;
349 m_XSumOfSquares += x * x;
350 m_YSumOfSquares += y * y;
351
352 if (m_XData.size() > m_WindowSize)
353 {
354 double oldValue = m_XData.front();
355 m_XData.pop_front();
356 m_XSum -= oldValue;
357 m_XSumOfSquares -= oldValue * oldValue;
358
359 oldValue = m_YData.front();
360 m_YData.pop_front();
361 m_YSum -= oldValue;
362 m_YSumOfSquares -= oldValue * oldValue;
363 }
364 const int size = m_XData.size();
365 if (size < 2)
366 return 0.0;
367
368 const double xVariance = (m_XSumOfSquares - (m_XSum * m_XSum) / size) / (size - 1);
369 const double yVariance = (m_YSumOfSquares - (m_YSum * m_YSum) / size) / (size - 1);
370 return std::sqrt(xVariance + yVariance);
371 }
372
373 void resetFilter()
374 {
375 m_XSum = 0;
376 m_YSum = 0;
377 m_XSumOfSquares = 0;
378 m_YSumOfSquares = 0;
379 m_XData.clear();
380 m_YData.clear();
381 }
382
383 private:
384 std::deque<double> m_XData;
385 std::deque<double> m_YData;
386
387 unsigned long m_WindowSize;
388 double m_XSum = 0, m_YSum = 0;
389 double m_XSumOfSquares = 0, m_YSumOfSquares = 0;
390};
391
392bool Analyze::eventFilter(QObject *obj, QEvent *ev)
393{
394 // Quit if click wasn't on a QLineEdit.
395 if (qobject_cast<QLineEdit*>(obj) == nullptr)
396 return false;
397
398 // This filter only applies to single or double clicks.
400 return false;
401
402 auto axisEntry = yAxisMap.find(obj);
403 if (axisEntry == yAxisMap.end())
404 return false;
405
406 const bool isRightClick = (ev->type() == QEvent::MouseButtonPress) &&
407 (static_cast<QMouseEvent*>(ev)->button() == Qt::RightButton);
408 const bool isControlClick = (ev->type() == QEvent::MouseButtonPress) &&
409 (static_cast<QMouseEvent*>(ev)->modifiers() &
410 Qt::KeyboardModifier::ControlModifier);
411 const bool isShiftClick = (ev->type() == QEvent::MouseButtonPress) &&
412 (static_cast<QMouseEvent*>(ev)->modifiers() &
413 Qt::KeyboardModifier::ShiftModifier);
414
415 if (ev->type() == QEvent::MouseButtonDblClick || isRightClick || isControlClick || isShiftClick)
416 {
417 startYAxisTool(axisEntry->first, axisEntry->second);
418 clickTimer.stop();
419 return true;
420 }
421 else if (ev->type() == QEvent::MouseButtonPress)
422 {
423 clickTimer.setSingleShot(true);
424 clickTimer.setInterval(250);
425 clickTimer.start();
426 m_ClickTimerInfo = axisEntry->second;
427 // Wait 0.25 seconds to see if this is a double click or just a single click.
428 connect(&clickTimer, &QTimer::timeout, this, [&]()
429 {
430 m_YAxisTool.reject();
431 if (m_ClickTimerInfo.checkBox && !m_ClickTimerInfo.checkBox->isChecked())
432 {
433 // Enable the graph.
434 m_ClickTimerInfo.checkBox->setChecked(true);
435 statsPlot->graph(m_ClickTimerInfo.graphIndex)->setVisible(true);
436 statsPlot->graph(m_ClickTimerInfo.graphIndex)->addToLegend();
437 }
438 userSetLeftAxis(m_ClickTimerInfo.axis);
439 });
440 return true;
441 }
442 return false;
443}
444
445Analyze::Analyze() : m_YAxisTool(this)
446{
447 setupUi(this);
448
449 captureRms.reset(new RmsFilter);
450 guiderRms.reset(new RmsFilter);
451
452 initInputSelection();
453 initTimelinePlot();
454
455 initStatsPlot();
456 connect(&m_YAxisTool, &YAxisTool::axisChanged, this, &Analyze::userChangedYAxis);
457 connect(&m_YAxisTool, &YAxisTool::leftAxisChanged, this, &Analyze::userSetLeftAxis);
458 connect(&m_YAxisTool, &YAxisTool::axisColorChanged, this, &Analyze::userSetAxisColor);
459 qApp->installEventFilter(this);
460
461 initGraphicsPlot();
462 fullWidthCB->setChecked(true);
463 keepCurrentCB->setChecked(true);
464 runtimeDisplay = true;
465 fullWidthCB->setVisible(true);
466 fullWidthCB->setDisabled(false);
467
468 // Initialize the checkboxes that allow the user to make (in)visible
469 // each of the 4 main displays in Analyze.
470 detailsCB->setChecked(true);
471 statsCB->setChecked(true);
472 graphsCB->setChecked(true);
473 timelineCB->setChecked(true);
474 setVisibility();
475 connect(timelineCB, &QCheckBox::stateChanged, this, &Analyze::setVisibility);
476 connect(graphsCB, &QCheckBox::stateChanged, this, &Analyze::setVisibility);
477 connect(statsCB, &QCheckBox::stateChanged, this, &Analyze::setVisibility);
478 connect(detailsCB, &QCheckBox::stateChanged, this, &Analyze::setVisibility);
479
480 connect(fullWidthCB, &QCheckBox::toggled, [ = ](bool checked)
481 {
482 if (checked)
483 this->replot();
484 });
485
486 initStatsCheckboxes();
487
488 connect(zoomInB, &QPushButton::clicked, this, &Analyze::zoomIn);
489 connect(zoomOutB, &QPushButton::clicked, this, &Analyze::zoomOut);
490 connect(prevSessionB, &QPushButton::clicked, this, &Analyze::previousTimelineItem);
491 connect(nextSessionB, &QPushButton::clicked, this, &Analyze::nextTimelineItem);
492 connect(timelinePlot, &QCustomPlot::mousePress, this, &Analyze::timelineMousePress);
493 connect(timelinePlot, &QCustomPlot::mouseDoubleClick, this, &Analyze::timelineMouseDoubleClick);
494 connect(timelinePlot, &QCustomPlot::mouseWheel, this, &Analyze::timelineMouseWheel);
495 connect(statsPlot, &QCustomPlot::mousePress, this, &Analyze::statsMousePress);
496 connect(statsPlot, &QCustomPlot::mouseDoubleClick, this, &Analyze::statsMouseDoubleClick);
497 connect(statsPlot, &QCustomPlot::mouseMove, this, &Analyze::statsMouseMove);
498 connect(analyzeSB, &QScrollBar::valueChanged, this, &Analyze::scroll);
499 analyzeSB->setRange(0, MAX_SCROLL_VALUE);
500 connect(keepCurrentCB, &QCheckBox::stateChanged, this, &Analyze::keepCurrent);
501
502 setupKeyboardShortcuts(this);
503
504 reset();
505 replot();
506}
507
508void Analyze::setVisibility()
509{
510 detailsWidget->setVisible(detailsCB->isChecked());
511 statsGridWidget->setVisible(statsCB->isChecked());
512 timelinePlot->setVisible(timelineCB->isChecked());
513 statsPlot->setVisible(graphsCB->isChecked());
514 replot();
515}
516
517// Mouse wheel over the Timeline plot causes an x-axis zoom.
518void Analyze::timelineMouseWheel(QWheelEvent *event)
519{
520 if (event->angleDelta().y() > 0)
521 zoomIn();
522 else if (event->angleDelta().y() < 0)
523 zoomOut();
524}
525
526// This callback is used so that when keepCurrent is checked, we replot immediately.
527// The actual keepCurrent work is done in replot().
528void Analyze::keepCurrent(int state)
529{
530 Q_UNUSED(state);
531 if (keepCurrentCB->isChecked())
532 {
533 removeStatsCursor();
534 replot();
535 }
536}
537
538// Get the following or previous .analyze file from the directory currently being displayed.
539QString Analyze::getNextFile(bool after)
540{
541 QDir dir;
542 QString filename;
543 QString dirString;
544 if (runtimeDisplay)
545 {
546 // Use the directory and file we're currently writing to.
547 dirString = QUrl::fromLocalFile(QDir(KSPaths::writableLocation(
548 QStandardPaths::AppLocalDataLocation)).filePath("analyze")).toLocalFile();
549 filename = QFileInfo(logFilename).fileName();
550 }
551 else
552 {
553 // Use the directory and file we're currently displaying.
554 dirString = dirPath.toLocalFile();
555 filename = QFileInfo(displayedSession.toLocalFile()).fileName();
556 }
557
558 // Set the sorting by name and filter by a .analyze suffix and get the file list.
559 dir.setPath(dirString);
560 QStringList filters;
561 filters << "*.analyze";
562 dir.setNameFilters(filters);
563 dir.setSorting(QDir::Name);
564 QStringList fileList = dir.entryList();
565
566 if (fileList.size() == 0)
567 return "";
568
569 // This would be the case on startup in 'Current Session' mode, but it hasn't started up yet.
570 if (filename.isEmpty() && fileList.size() > 0 && !after)
571 return QFileInfo(dirString, fileList.last()).absoluteFilePath();
572
573 // Go through all the files in this directory and find the file currently being displayed.
574 int index = -1;
575 for (int i = fileList.size() - 1; i >= 0; --i)
576 {
577 if (fileList[i] == filename)
578 {
579 index = i;
580 break;
581 }
582 }
583
584 // Make sure we can go before or after.
585 if (index < 0)
586 return "";
587 else if (!after && index <= 0)
588 return "";
589 else if (after && index >= fileList.size() - 1)
590 return "";
591
592 return QFileInfo(dirString, after ? fileList[index + 1] : fileList[index - 1]).absoluteFilePath();
593}
594
595void Analyze::nextFile()
596{
597 QString filename = getNextFile(true);
598 if (filename.isEmpty())
599 displayFile(QUrl(), true);
600 else
601 displayFile(QUrl::fromLocalFile(filename));
602
603}
604
605void Analyze::prevFile()
606{
607 QString filename = getNextFile(false);
608 if (filename.isEmpty())
609 return;
610 displayFile(QUrl::fromLocalFile(filename));
611}
612
613// Do what's necessary to display the .analyze file passed in.
614void Analyze::displayFile(const QUrl &url, bool forceCurrentSession)
615{
616 if (forceCurrentSession || (logFilename.size() > 0 && url.toLocalFile() == logFilename))
617 {
618 // Input from current session
619 inputCombo->setCurrentIndex(0);
620 inputValue->setText("");
621 if (!runtimeDisplay)
622 {
623 reset();
624 maxXValue = readDataFromFile(logFilename);
625 }
626 runtimeDisplay = true;
627 fullWidthCB->setChecked(true);
628 fullWidthCB->setVisible(true);
629 fullWidthCB->setDisabled(false);
630 displayedSession = QUrl();
631 replot();
632 return;
633 }
634
635 inputCombo->setCurrentIndex(1);
636 displayedSession = url;
637 dirPath = QUrl(url.url(QUrl::RemoveFilename));
638
639 reset();
640 inputValue->setText(url.fileName());
641
642 // If we do this after the readData call below, it would animate the sequence.
643 runtimeDisplay = false;
644
645 maxXValue = readDataFromFile(url.toLocalFile());
646 checkForMissingSchedulerJobEnd(maxXValue);
647 plotStart = 0;
648 plotWidth = maxXValue + 5;
649 replot();
650}
651
652// Implements the input selection UI.
653// User can either choose the current Ekos session, or a file read from disk.
654void Analyze::initInputSelection()
655{
656 // Setup the input combo box.
657 dirPath = QUrl::fromLocalFile(QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("analyze"));
658
659 inputCombo->addItem(i18n("Current Session"));
660 inputCombo->addItem(i18n("Read from File"));
661 inputValue->setText("");
662 inputCombo->setCurrentIndex(0);
663 connect(inputCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, [&](int index)
664 {
665 if (index == 0)
666 {
667 displayFile(QUrl::fromLocalFile(logFilename), true);
668 }
669 else if (index == 1)
670 {
671 // The i18n call below is broken up (and the word "analyze" is protected from it) because i18n
672 // translates "analyze" to "analyse" for the English UK locale, but we need to keep it ".analyze"
673 // because that's what how the files are named.
674 QUrl inputURL = QFileDialog::getOpenFileUrl(this, i18nc("@title:window", "Select input file"), dirPath,
675 QString("Analyze %1 (*.analyze);;%2").arg(i18n("Log")).arg(i18n("All Files (*)")));
676 if (inputURL.isEmpty())
677 return;
678 displayFile(inputURL);
679 }
680 });
681 connect(nextFileB, &QPushButton::clicked, this, &Analyze::nextFile);
682 connect(prevFileB, &QPushButton::clicked, this, &Analyze::prevFile);
683}
684
685void Analyze::setupKeyboardShortcuts(QWidget *plot)
686{
687 // Shortcuts defined: https://doc.qt.io/archives/qt-4.8/qkeysequence.html#standard-shortcuts
689 connect(s, &QShortcut::activated, this, &Analyze::zoomIn);
691 connect(s, &QShortcut::activated, this, &Analyze::zoomOut);
692
694 connect(s, &QShortcut::activated, this, &Analyze::scrollRight);
696 connect(s, &QShortcut::activated, this, &Analyze::scrollLeft);
697
699 connect(s, &QShortcut::activated, this, &Analyze::nextTimelineItem);
701 connect(s, &QShortcut::activated, this, &Analyze::previousTimelineItem);
702
704 connect(s, &QShortcut::activated, this, &Analyze::nextTimelineItem);
706 connect(s, &QShortcut::activated, this, &Analyze::previousTimelineItem);
707
709 connect(s, &QShortcut::activated, this, &Analyze::statsYZoomIn);
711 connect(s, &QShortcut::activated, this, &Analyze::statsYZoomOut);
712}
713
714Analyze::~Analyze()
715{
716 // TODO:
717 // We should write out to disk any sessions that haven't terminated
718 // (e.g. capture, focus, guide)
719}
720
721void Analyze::setSelectedSession(const Session &s)
722{
723 m_selectedSession = s;
724}
725
726void Analyze::clearSelectedSession()
727{
728 m_selectedSession = Session();
729}
730
731// When a user selects a timeline session, the previously selected one
732// is deselected. Note: this does not replot().
733void Analyze::unhighlightTimelineItem()
734{
735 clearSelectedSession();
736 if (selectionHighlight != nullptr)
737 {
738 timelinePlot->removeItem(selectionHighlight);
739 selectionHighlight = nullptr;
740 }
741 detailsTable->clear();
742 prevSessionB->setDisabled(true);
743 nextSessionB->setDisabled(true);
744}
745
746// Highlight the area between start and end of the session on row y in Timeline.
747// Note that this doesn't replot().
748void Analyze::highlightTimelineItem(const Session &session)
749{
750 constexpr double halfHeight = 0.5;
751 unhighlightTimelineItem();
752
753 setSelectedSession(session);
754 QCPItemRect *rect = new QCPItemRect(timelinePlot);
755 rect->topLeft->setCoords(session.start, session.offset + halfHeight);
756 rect->bottomRight->setCoords(session.end, session.offset - halfHeight);
757 rect->setBrush(timelineSelectionBrush);
758 selectionHighlight = rect;
759 prevSessionB->setDisabled(false);
760 nextSessionB->setDisabled(false);
761
762}
763
764// Creates a fat line-segment on the Timeline, optionally with a stripe in the middle.
765QCPItemRect * Analyze::addSession(double start, double end, double y,
766 const QBrush &brush, const QBrush *stripeBrush)
767{
768 QPen pen = QPen(Qt::black, 1, Qt::SolidLine);
769 QCPItemRect *rect = new QCPItemRect(timelinePlot);
770 rect->topLeft->setCoords(start, y + halfTimelineHeight);
771 rect->bottomRight->setCoords(end, y - halfTimelineHeight);
772 rect->setPen(pen);
773 rect->setSelectedPen(pen);
774 rect->setBrush(brush);
775 rect->setSelectedBrush(brush);
776
777 if (stripeBrush != nullptr)
778 {
779 QCPItemRect *stripe = new QCPItemRect(timelinePlot);
780 stripe->topLeft->setCoords(start, y + halfTimelineHeight / 2.0);
781 stripe->bottomRight->setCoords(end, y - halfTimelineHeight / 2.0);
782 stripe->setPen(pen);
783 stripe->setBrush(*stripeBrush);
784 }
785 return rect;
786}
787
788// Add the guide stats values to the Stats graphs.
789// We want to avoid drawing guide-stat values when not guiding.
790// That is, we have no input samples then, but the graph would connect
791// two points with a line. By adding NaN values into the graph,
792// those places are made invisible.
793void Analyze::addGuideStats(double raDrift, double decDrift, int raPulse, int decPulse, double snr,
794 int numStars, double skyBackground, double time)
795{
796 double MAX_GUIDE_STATS_GAP = 30;
797
798 if (time - lastGuideStatsTime > MAX_GUIDE_STATS_GAP &&
799 lastGuideStatsTime >= 0)
800 {
801 addGuideStatsInternal(qQNaN(), qQNaN(), 0, 0, qQNaN(), qQNaN(), qQNaN(), qQNaN(), qQNaN(),
802 lastGuideStatsTime + .0001);
803 addGuideStatsInternal(qQNaN(), qQNaN(), 0, 0, qQNaN(), qQNaN(), qQNaN(), qQNaN(), qQNaN(), time - .0001);
804 guiderRms->resetFilter();
805 }
806
807 const double drift = std::hypot(raDrift, decDrift);
808
809 // To compute the RMS error, which is sqrt(sum square error / N), filter the squared
810 // error, which effectively returns sum squared error / N, and take the sqrt.
811 // This is done by RmsFilter::newSample().
812 const double rms = guiderRms->newSample(raDrift, decDrift);
813 addGuideStatsInternal(raDrift, decDrift, double(raPulse), double(decPulse), snr, numStars, skyBackground, drift, rms, time);
814
815 // If capture is active, plot the capture RMS.
816 if (captureStartedTime >= 0)
817 {
818 // lastCaptureRmsTime is the last time we plotted a capture RMS value.
819 // If we have plotted values previously, and there's a gap in guiding
820 // we must place NaN values in the graph surrounding the gap.
821 if ((lastCaptureRmsTime >= 0) &&
822 (time - lastCaptureRmsTime > MAX_GUIDE_STATS_GAP))
823 {
824 // this is the first sample in a series with a gap behind us.
825 statsPlot->graph(CAPTURE_RMS_GRAPH)->addData(lastCaptureRmsTime + .0001, qQNaN());
826 statsPlot->graph(CAPTURE_RMS_GRAPH)->addData(time - .0001, qQNaN());
827 captureRms->resetFilter();
828 }
829 const double rmsC = captureRms->newSample(raDrift, decDrift);
830 statsPlot->graph(CAPTURE_RMS_GRAPH)->addData(time, rmsC);
831 lastCaptureRmsTime = time;
832 }
833
834 lastGuideStatsTime = time;
835}
836
837void Analyze::addGuideStatsInternal(double raDrift, double decDrift, double raPulse,
838 double decPulse, double snr,
839 double numStars, double skyBackground,
840 double drift, double rms, double time)
841{
842 statsPlot->graph(RA_GRAPH)->addData(time, raDrift);
843 statsPlot->graph(DEC_GRAPH)->addData(time, decDrift);
844 statsPlot->graph(RA_PULSE_GRAPH)->addData(time, raPulse);
845 statsPlot->graph(DEC_PULSE_GRAPH)->addData(time, decPulse);
846 statsPlot->graph(DRIFT_GRAPH)->addData(time, drift);
847 statsPlot->graph(RMS_GRAPH)->addData(time, rms);
848
849 // Set the SNR axis' maximum to 95% of the way up from the middle to the top.
850 if (!qIsNaN(snr))
851 snrMax = std::max(snr, snrMax);
852 if (!qIsNaN(skyBackground))
853 skyBgMax = std::max(skyBackground, skyBgMax);
854 if (!qIsNaN(numStars))
855 numStarsMax = std::max(numStars, static_cast<double>(numStarsMax));
856
857 statsPlot->graph(SNR_GRAPH)->addData(time, snr);
858 statsPlot->graph(NUMSTARS_GRAPH)->addData(time, numStars);
859 statsPlot->graph(SKYBG_GRAPH)->addData(time, skyBackground);
860}
861
862void Analyze::addTemperature(double temperature, double time)
863{
864 // The HFR corresponds to the last capture
865 // If there is no temperature sensor, focus sends a large negative value.
866 if (temperature > -200)
867 statsPlot->graph(TEMPERATURE_GRAPH)->addData(time, temperature);
868}
869
870void Analyze::addFocusPosition(double focusPosition, double time)
871{
872 statsPlot->graph(FOCUS_POSITION_GRAPH)->addData(time, focusPosition);
873}
874
875void Analyze::addTargetDistance(double targetDistance, double time)
876{
877 // The target distance corresponds to the last capture
878 if (previousCaptureStartedTime >= 0 && previousCaptureCompletedTime >= 0 &&
879 previousCaptureStartedTime < previousCaptureCompletedTime &&
880 previousCaptureCompletedTime <= time)
881 {
882 statsPlot->graph(TARGET_DISTANCE_GRAPH)->addData(previousCaptureStartedTime - .0001, qQNaN());
883 statsPlot->graph(TARGET_DISTANCE_GRAPH)->addData(previousCaptureStartedTime, targetDistance);
884 statsPlot->graph(TARGET_DISTANCE_GRAPH)->addData(previousCaptureCompletedTime, targetDistance);
885 statsPlot->graph(TARGET_DISTANCE_GRAPH)->addData(previousCaptureCompletedTime + .0001, qQNaN());
886 }
887}
888
889// Add the HFR values to the Stats graph, as a constant value between startTime and time.
890void Analyze::addHFR(double hfr, int numCaptureStars, int median, double eccentricity,
891 double time, double startTime)
892{
893 // The HFR corresponds to the last capture
894 statsPlot->graph(HFR_GRAPH)->addData(startTime - .0001, qQNaN());
895 statsPlot->graph(HFR_GRAPH)->addData(startTime, hfr);
896 statsPlot->graph(HFR_GRAPH)->addData(time, hfr);
897 statsPlot->graph(HFR_GRAPH)->addData(time + .0001, qQNaN());
898
899 statsPlot->graph(NUM_CAPTURE_STARS_GRAPH)->addData(startTime - .0001, qQNaN());
900 statsPlot->graph(NUM_CAPTURE_STARS_GRAPH)->addData(startTime, numCaptureStars);
901 statsPlot->graph(NUM_CAPTURE_STARS_GRAPH)->addData(time, numCaptureStars);
902 statsPlot->graph(NUM_CAPTURE_STARS_GRAPH)->addData(time + .0001, qQNaN());
903
904 statsPlot->graph(MEDIAN_GRAPH)->addData(startTime - .0001, qQNaN());
905 statsPlot->graph(MEDIAN_GRAPH)->addData(startTime, median);
906 statsPlot->graph(MEDIAN_GRAPH)->addData(time, median);
907 statsPlot->graph(MEDIAN_GRAPH)->addData(time + .0001, qQNaN());
908
909 statsPlot->graph(ECCENTRICITY_GRAPH)->addData(startTime - .0001, qQNaN());
910 statsPlot->graph(ECCENTRICITY_GRAPH)->addData(startTime, eccentricity);
911 statsPlot->graph(ECCENTRICITY_GRAPH)->addData(time, eccentricity);
912 statsPlot->graph(ECCENTRICITY_GRAPH)->addData(time + .0001, qQNaN());
913
914 medianMax = std::max(median, medianMax);
915 numCaptureStarsMax = std::max(numCaptureStars, numCaptureStarsMax);
916}
917
918// Add the Mount Coordinates values to the Stats graph.
919// All but pierSide are in double degrees.
920void Analyze::addMountCoords(double ra, double dec, double az,
921 double alt, int pierSide, double ha, double time)
922{
923 statsPlot->graph(MOUNT_RA_GRAPH)->addData(time, ra);
924 statsPlot->graph(MOUNT_DEC_GRAPH)->addData(time, dec);
925 statsPlot->graph(MOUNT_HA_GRAPH)->addData(time, ha);
926 statsPlot->graph(AZ_GRAPH)->addData(time, az);
927 statsPlot->graph(ALT_GRAPH)->addData(time, alt);
928 statsPlot->graph(PIER_SIDE_GRAPH)->addData(time, double(pierSide));
929}
930
931// Read a .analyze file, and setup all the graphics.
932double Analyze::readDataFromFile(const QString &filename)
933{
934 double lastTime = 10;
935 QFile inputFile(filename);
936 if (inputFile.open(QIODevice::ReadOnly))
937 {
938 QTextStream in(&inputFile);
939 while (!in.atEnd())
940 {
941 QString line = in.readLine();
942 double time = processInputLine(line);
943 if (time > lastTime)
944 lastTime = time;
945 }
946 inputFile.close();
947 }
948 return lastTime;
949}
950
951// Process an input line read from a .analyze file.
952double Analyze::processInputLine(const QString &line)
953{
954 bool ok;
955 // Break the line into comma-separated components
956 QStringList list = line.split(QLatin1Char(','));
957 // We need at least a command and a timestamp
958 if (list.size() < 2)
959 return 0;
960 if (list[0].at(0).toLatin1() == '#')
961 {
962 // Comment character # must be at start of line.
963 return 0;
964 }
965
966 if ((list[0] == "AnalyzeStartTime") && list.size() == 3)
967 {
968 displayStartTime = QDateTime::fromString(list[1], timeFormat);
969 startTimeInitialized = true;
970 analyzeTimeZone = list[2];
971 return 0;
972 }
973
974 // Except for comments and the above AnalyzeStartTime, the second item
975 // in the csv line is a double which represents seconds since start of the log.
976 const double time = QString(list[1]).toDouble(&ok);
977 if (!ok)
978 return 0;
979 if (time < 0 || time > 3600 * 24 * 10)
980 return 0;
981
982 if ((list[0] == "CaptureStarting") && (list.size() == 4))
983 {
984 const double exposureSeconds = QString(list[2]).toDouble(&ok);
985 if (!ok)
986 return 0;
987 const QString filter = list[3];
988 processCaptureStarting(time, exposureSeconds, filter);
989 }
990 else if ((list[0] == "CaptureComplete") && (list.size() >= 6) && (list.size() <= 9))
991 {
992 const double exposureSeconds = QString(list[2]).toDouble(&ok);
993 if (!ok)
994 return 0;
995 const QString filter = list[3];
996 const double hfr = QString(list[4]).toDouble(&ok);
997 if (!ok)
998 return 0;
999 const QString filename = list[5];
1000 const int numStars = (list.size() > 6) ? QString(list[6]).toInt(&ok) : 0;
1001 if (!ok)
1002 return 0;
1003 const int median = (list.size() > 7) ? QString(list[7]).toInt(&ok) : 0;
1004 if (!ok)
1005 return 0;
1006 const double eccentricity = (list.size() > 8) ? QString(list[8]).toDouble(&ok) : 0;
1007 if (!ok)
1008 return 0;
1009 processCaptureComplete(time, filename, exposureSeconds, filter, hfr, numStars, median, eccentricity, true);
1010 }
1011 else if ((list[0] == "CaptureAborted") && (list.size() == 3))
1012 {
1013 const double exposureSeconds = QString(list[2]).toDouble(&ok);
1014 if (!ok)
1015 return 0;
1016 processCaptureAborted(time, exposureSeconds, true);
1017 }
1018 else if ((list[0] == "AutofocusStarting") && (list.size() >= 4))
1019 {
1020 QString filter = list[2];
1021 double temperature = QString(list[3]).toDouble(&ok);
1022 if (!ok)
1023 return 0;
1024 AutofocusReason reason;
1025 QString reasonInfo;
1026 if (list.size() == 4)
1027 {
1028 reason = AutofocusReason::FOCUS_NONE;
1029 reasonInfo = "";
1030 }
1031 else
1032 {
1033 reason = static_cast<AutofocusReason>(QString(list[4]).toInt(&ok));
1034 if (!ok)
1035 return 0;
1036 reasonInfo = list[5];
1037 }
1038 processAutofocusStarting(time, temperature, filter, reason, reasonInfo);
1039 }
1040 else if ((list[0] == "AutofocusComplete") && (list.size() >= 8))
1041 {
1042 // Version 2
1043 double temperature = QString(list[2]).toDouble(&ok);
1044 if (!ok)
1045 return 0;
1046 QVariant reasonV = QString(list[3]);
1047 int reasonInt = reasonV.toInt();
1048 if (reasonInt < 0 || reasonInt >= AutofocusReason::FOCUS_MAX_REASONS)
1049 return 0;
1050 AutofocusReason reason = static_cast<AutofocusReason>(reasonInt);
1051 const QString reasonInfo = list[4];
1052 const QString filter = list[5];
1053 const QString samples = list[6];
1054 const bool useWeights = QString(list[7]).toInt(&ok);
1055 if (!ok)
1056 return 0;
1057 const QString curve = list.size() > 8 ? list[8] : "";
1058 const QString title = list.size() > 9 ? list[9] : "";
1059 processAutofocusCompleteV2(time, temperature, filter, reason, reasonInfo, samples, useWeights, curve, title, true);
1060 }
1061 else if ((list[0] == "AutofocusComplete") && (list.size() >= 4))
1062 {
1063 // Version 1
1064 const QString filter = list[2];
1065 const QString samples = list[3];
1066 const QString curve = list.size() > 4 ? list[4] : "";
1067 const QString title = list.size() > 5 ? list[5] : "";
1068 processAutofocusComplete(time, filter, samples, curve, title, true);
1069 }
1070 else if ((list[0] == "AutofocusAborted") && (list.size() >= 9))
1071 {
1072 double temperature = QString(list[2]).toDouble(&ok);
1073 if (!ok)
1074 return 0;
1075 QVariant reasonV = QString(list[3]);
1076 int reasonInt = reasonV.toInt();
1077 if (reasonInt < 0 || reasonInt >= AutofocusReason::FOCUS_MAX_REASONS)
1078 return 0;
1079 AutofocusReason reason = static_cast<AutofocusReason>(reasonInt);
1080 QString reasonInfo = list[4];
1081 QString filter = list[5];
1082 QString samples = list[6];
1083 bool useWeights = QString(list[7]).toInt(&ok);
1084 if (!ok)
1085 return 0;
1086 AutofocusFailReason failCode;
1087 QVariant failCodeV = QString(list[8]);
1088 int failCodeInt = failCodeV.toInt();
1089 if (failCodeInt < 0 || failCodeInt >= AutofocusFailReason::FOCUS_FAIL_MAX_REASONS)
1090 return 0;
1091 failCode = static_cast<AutofocusFailReason>(failCodeInt);
1092 if (!ok)
1093 return 0;
1094 QString failCodeInfo;
1095 if (list.size() > 9)
1096 failCodeInfo = QString(list[9]);
1097 processAutofocusAbortedV2(time, temperature, filter, reason, reasonInfo, samples, useWeights, failCode, failCodeInfo, true);
1098 }
1099 else if ((list[0] == "AutofocusAborted") && (list.size() >= 4))
1100 {
1101 QString filter = list[2];
1102 QString samples = list[3];
1103 processAutofocusAborted(time, filter, samples, true);
1104 }
1105 else if ((list[0] == "AdaptiveFocusComplete") && (list.size() == 12))
1106 {
1107 // This is the second version of the AdaptiveFocusComplete message
1108 const QString filter = list[2];
1109 double temperature = QString(list[3]).toDouble(&ok);
1110 const double tempTicks = QString(list[4]).toDouble(&ok);
1111 double altitude = QString(list[5]).toDouble(&ok);
1112 const double altTicks = QString(list[6]).toDouble(&ok);
1113 const int prevPosError = QString(list[7]).toInt(&ok);
1114 const int thisPosError = QString(list[8]).toInt(&ok);
1115 const int totalTicks = QString(list[9]).toInt(&ok);
1116 const int position = QString(list[10]).toInt(&ok);
1117 const bool focuserMoved = QString(list[11]).toInt(&ok) != 0;
1118 processAdaptiveFocusComplete(time, filter, temperature, tempTicks, altitude, altTicks, prevPosError,
1119 thisPosError, totalTicks, position, focuserMoved, true);
1120 }
1121 else if ((list[0] == "AdaptiveFocusComplete") && (list.size() >= 9))
1122 {
1123 // This is the first version of the AdaptiveFocusComplete message - retained os Analyze can process
1124 // historical messages correctly
1125 const QString filter = list[2];
1126 double temperature = QString(list[3]).toDouble(&ok);
1127 const int tempTicks = QString(list[4]).toInt(&ok);
1128 double altitude = QString(list[5]).toDouble(&ok);
1129 const int altTicks = QString(list[6]).toInt(&ok);
1130 const int totalTicks = QString(list[7]).toInt(&ok);
1131 const int position = QString(list[8]).toInt(&ok);
1132 const bool focuserMoved = list.size() < 10 || QString(list[9]).toInt(&ok) != 0;
1133 processAdaptiveFocusComplete(time, filter, temperature, tempTicks,
1134 altitude, altTicks, 0, 0, totalTicks, position, focuserMoved, true);
1135 }
1136 else if ((list[0] == "GuideState") && list.size() == 3)
1137 {
1138 processGuideState(time, list[2], true);
1139 }
1140 else if ((list[0] == "GuideStats") && list.size() == 9)
1141 {
1142 const double ra = QString(list[2]).toDouble(&ok);
1143 if (!ok)
1144 return 0;
1145 const double dec = QString(list[3]).toDouble(&ok);
1146 if (!ok)
1147 return 0;
1148 const double raPulse = QString(list[4]).toInt(&ok);
1149 if (!ok)
1150 return 0;
1151 const double decPulse = QString(list[5]).toInt(&ok);
1152 if (!ok)
1153 return 0;
1154 const double snr = QString(list[6]).toDouble(&ok);
1155 if (!ok)
1156 return 0;
1157 const double skyBg = QString(list[7]).toDouble(&ok);
1158 if (!ok)
1159 return 0;
1160 const double numStars = QString(list[8]).toInt(&ok);
1161 if (!ok)
1162 return 0;
1163 processGuideStats(time, ra, dec, raPulse, decPulse, snr, skyBg, numStars, true);
1164 }
1165 else if ((list[0] == "Temperature") && list.size() == 3)
1166 {
1167 const double temperature = QString(list[2]).toDouble(&ok);
1168 if (!ok)
1169 return 0;
1170 processTemperature(time, temperature, true);
1171 }
1172 else if ((list[0] == "TargetDistance") && list.size() == 3)
1173 {
1174 const double targetDistance = QString(list[2]).toDouble(&ok);
1175 if (!ok)
1176 return 0;
1177 processTargetDistance(time, targetDistance, true);
1178 }
1179 else if ((list[0] == "MountState") && list.size() == 3)
1180 {
1181 processMountState(time, list[2], true);
1182 }
1183 else if ((list[0] == "MountCoords") && (list.size() == 7 || list.size() == 8))
1184 {
1185 const double ra = QString(list[2]).toDouble(&ok);
1186 if (!ok)
1187 return 0;
1188 const double dec = QString(list[3]).toDouble(&ok);
1189 if (!ok)
1190 return 0;
1191 const double az = QString(list[4]).toDouble(&ok);
1192 if (!ok)
1193 return 0;
1194 const double alt = QString(list[5]).toDouble(&ok);
1195 if (!ok)
1196 return 0;
1197 const int side = QString(list[6]).toInt(&ok);
1198 if (!ok)
1199 return 0;
1200 const double ha = (list.size() > 7) ? QString(list[7]).toDouble(&ok) : 0;
1201 if (!ok)
1202 return 0;
1203 processMountCoords(time, ra, dec, az, alt, side, ha, true);
1204 }
1205 else if ((list[0] == "AlignState") && list.size() == 3)
1206 {
1207 processAlignState(time, list[2], true);
1208 }
1209 else if ((list[0] == "MeridianFlipState") && list.size() == 3)
1210 {
1211 processMountFlipState(time, list[2], true);
1212 }
1213 else if ((list[0] == "SchedulerJobStart") && list.size() == 3)
1214 {
1215 QString jobName = list[2];
1216 processSchedulerJobStarted(time, jobName);
1217 }
1218 else if ((list[0] == "SchedulerJobEnd") && list.size() == 4)
1219 {
1220 QString jobName = list[2];
1221 QString reason = list[3];
1222 processSchedulerJobEnded(time, jobName, reason, true);
1223 }
1224 else
1225 {
1226 return 0;
1227 }
1228 return time;
1229}
1230
1231namespace
1232{
1233void addDetailsRow(QTableWidget *table, const QString &col1, const QColor &color1,
1234 const QString &col2, const QColor &color2,
1235 const QString &col3 = "", const QColor &color3 = Qt::white)
1236{
1237 int row = table->rowCount();
1238 table->setRowCount(row + 1);
1239
1240 QTableWidgetItem *item = new QTableWidgetItem();
1241 if (col1 == "Filename")
1242 {
1243 // Special case filenames--they tend to be too long and get elided.
1244 QFont ft = item->font();
1245 ft.setPointSizeF(8.0);
1246 item->setFont(ft);
1247 item->setText(col2);
1249 item->setForeground(color2);
1250 table->setItem(row, 0, item);
1251 table->setSpan(row, 0, 1, 3);
1252 return;
1253 }
1254
1255 item->setText(col1);
1257 item->setForeground(color1);
1258 table->setItem(row, 0, item);
1259
1260 item = new QTableWidgetItem();
1261 item->setText(col2);
1263 item->setForeground(color2);
1264 if (col1 == "Filename")
1265 {
1266 // Special Case long filenames.
1267 QFont ft = item->font();
1268 ft.setPointSizeF(8.0);
1269 item->setFont(ft);
1270 }
1271 table->setItem(row, 1, item);
1272
1273 if (col3.size() > 0)
1274 {
1275 item = new QTableWidgetItem();
1276 item->setText(col3);
1278 item->setForeground(color3);
1279 table->setItem(row, 2, item);
1280 }
1281 else
1282 {
1283 // Column 1 spans 2nd and 3rd columns
1284 table->setSpan(row, 1, 1, 2);
1285 }
1286}
1287}
1288
1289// Helper to create tables in the details display.
1290// Start the table, displaying the heading and timing information, common to all sessions.
1291void Analyze::Session::setupTable(const QString &name, const QString &status,
1292 const QDateTime &startClock, const QDateTime &endClock, QTableWidget *table)
1293{
1294 details = table;
1295 details->clear();
1296 details->setRowCount(0);
1297 details->setEditTriggers(QAbstractItemView::NoEditTriggers);
1298 details->setColumnCount(3);
1299 details->verticalHeader()->setDefaultSectionSize(20);
1300 details->horizontalHeader()->setStretchLastSection(true);
1301 details->setColumnWidth(0, 100);
1302 details->setColumnWidth(1, 100);
1303 details->setShowGrid(false);
1304 details->setWordWrap(true);
1305 details->horizontalHeader()->hide();
1306 details->verticalHeader()->hide();
1307
1308 QString startDateStr = startClock.toString("dd.MM.yyyy");
1309 QString startTimeStr = startClock.toString("hh:mm:ss");
1310 QString endTimeStr = isTemporary() ? "Ongoing"
1311 : endClock.toString("hh:mm:ss");
1312
1313 addDetailsRow(details, name, Qt::yellow, status, Qt::yellow);
1314 addDetailsRow(details, "Date", Qt::yellow, startDateStr, Qt::white);
1315 addDetailsRow(details, "Interval", Qt::yellow, QString::number(start, 'f', 3), Qt::white,
1316 isTemporary() ? "Ongoing" : QString::number(end, 'f', 3), Qt::white);
1317 addDetailsRow(details, "Clock", Qt::yellow, startTimeStr, Qt::white, endTimeStr, Qt::white);
1318 addDetailsRow(details, "Duration", Qt::yellow, QString::number(end - start, 'f', 1), Qt::white);
1319}
1320
1321// Add a new row to the table, which is specific to the particular Timeline line.
1322void Analyze::Session::addRow(const QString &key, const QString &value)
1323{
1324 addDetailsRow(details, key, Qt::yellow, value, Qt::white);
1325}
1326
1327bool Analyze::Session::isTemporary() const
1328{
1329 return rect != nullptr;
1330}
1331
1332// This is version 2 of FocusSession that includes weights, outliers and reason codes
1333// The focus session parses the "pipe-separate-values" list of positions
1334// and HFRs given it, eventually to be used to plot the focus v-curve.
1335Analyze::FocusSession::FocusSession(double start_, double end_, QCPItemRect *rect, bool ok, double temperature_,
1336 const QString &filter_, const AutofocusReason reason_, const QString &reasonInfo_, const QString &points_,
1337 const bool useWeights_, const QString &curve_, const QString &title_, const AutofocusFailReason failCode_,
1338 const QString failCodeInfo_)
1339 : Session(start_, end_, FOCUS_Y, rect), success(ok), temperature(temperature_), filter(filter_), reason(reason_),
1340 reasonInfo(reasonInfo_), points(points_), useWeights(useWeights_), curve(curve_), title(title_), failCode(failCode_),
1341 failCodeInfo(failCodeInfo_)
1342{
1343 const QStringList list = points.split(QLatin1Char('|'));
1344 const int size = list.size();
1345 // Size can be 1 if points_ is an empty string.
1346 if (size < 2)
1347 return;
1348
1349 for (int i = 0; i < size; )
1350 {
1351 bool parsed1, parsed2, parsed3, parsed4;
1352 int position = QString(list[i++]).toInt(&parsed1);
1353 if (i >= size)
1354 break;
1355 double hfr = QString(list[i++]).toDouble(&parsed2);
1356 double weight = QString(list[i++]).toDouble(&parsed3);
1357 bool outlier = QString(list[i++]).toInt(&parsed4);
1358 if (!parsed1 || !parsed2 || !parsed3 || !parsed4)
1359 {
1360 positions.clear();
1361 hfrs.clear();
1362 weights.clear();
1363 outliers.clear();
1364 return;
1365 }
1366 positions.push_back(position);
1367 hfrs.push_back(hfr);
1368 weights.push_back(weight);
1369 outliers.push_back(outlier);
1370 }
1371}
1372
1373// This is the original version of FocusSession
1374// The focus session parses the "pipe-separate-values" list of positions
1375// and HFRs given it, eventually to be used to plot the focus v-curve.
1376Analyze::FocusSession::FocusSession(double start_, double end_, QCPItemRect *rect, bool ok, double temperature_,
1377 const QString &filter_, const QString &points_, const QString &curve_, const QString &title_)
1378 : Session(start_, end_, FOCUS_Y, rect), success(ok),
1379 temperature(temperature_), filter(filter_), points(points_), curve(curve_), title(title_)
1380{
1381 // Set newer variables, not part of the original message, to default values
1382 reason = AutofocusReason::FOCUS_NONE;
1383 reasonInfo = "";
1384 useWeights = false;
1385 failCode = AutofocusFailReason::FOCUS_FAIL_NONE;
1386 failCodeInfo = "";
1387
1388 const QStringList list = points.split(QLatin1Char('|'));
1389 const int size = list.size();
1390 // Size can be 1 if points_ is an empty string.
1391 if (size < 2)
1392 return;
1393
1394 for (int i = 0; i < size; )
1395 {
1396 bool parsed1, parsed2;
1397 int position = QString(list[i++]).toInt(&parsed1);
1398 if (i >= size)
1399 break;
1400 double hfr = QString(list[i++]).toDouble(&parsed2);
1401 if (!parsed1 || !parsed2)
1402 {
1403 positions.clear();
1404 hfrs.clear();
1405 weights.clear();
1406 outliers.clear();
1407 return;
1408 }
1409 positions.push_back(position);
1410 hfrs.push_back(hfr);
1411 weights.push_back(1.0);
1412 outliers.push_back(false);
1413 }
1414}
1415
1416Analyze::FocusSession::FocusSession(double start_, double end_, QCPItemRect *rect,
1417 const QString &filter_, double temperature_, double tempTicks_, double altitude_,
1418 double altTicks_, int prevPosError_, int thisPosError_, int totalTicks_, int position_)
1419 : Session(start_, end_, FOCUS_Y, rect), temperature(temperature_), filter(filter_), tempTicks(tempTicks_),
1420 altitude(altitude_), altTicks(altTicks_), prevPosError(prevPosError_), thisPosError(thisPosError_),
1421 totalTicks(totalTicks_), adaptedPosition(position_)
1422{
1423 standardSession = false;
1424}
1425
1426double Analyze::FocusSession::focusPosition()
1427{
1428 if (!standardSession)
1429 return adaptedPosition;
1430
1431 if (positions.size() > 0)
1432 return positions.last();
1433 return 0;
1434}
1435
1436namespace
1437{
1438bool isTemporaryFile(const QString &filename)
1439{
1441 return filename.startsWith(tempFileLocation);
1442}
1443}
1444
1445// When the user clicks on a particular capture session in the timeline,
1446// a table is rendered in the details section, and, if it was a double click,
1447// the fits file is displayed, if it can be found.
1448void Analyze::captureSessionClicked(CaptureSession &c, bool doubleClick)
1449{
1450 highlightTimelineItem(c);
1451
1452 if (c.isTemporary())
1453 c.setupTable("Capture", "in progress", clockTime(c.start), clockTime(c.start), detailsTable);
1454 else if (c.aborted)
1455 c.setupTable("Capture", "ABORTED", clockTime(c.start), clockTime(c.end), detailsTable);
1456 else
1457 c.setupTable("Capture", "successful", clockTime(c.start), clockTime(c.end), detailsTable);
1458
1459 c.addRow("Filter", c.filter);
1460
1461 double raRMS, decRMS, totalRMS;
1462 int numSamples;
1463 displayGuideGraphics(c.start, c.end, &raRMS, &decRMS, &totalRMS, &numSamples);
1464 if (numSamples > 0)
1465 c.addRow("GuideRMS", QString::number(totalRMS, 'f', 2));
1466
1467 c.addRow("Exposure", QString::number(c.duration, 'f', 2));
1468 if (!c.isTemporary())
1469 c.addRow("Filename", c.filename);
1470
1471
1472 // Don't try to display images from temporary sessions (they aren't done yet).
1473 if (doubleClick && !c.isTemporary())
1474 {
1475 QString filename = findFilename(c.filename);
1476 // Don't display temporary files from completed sessions either.
1477 bool tempImage = isTemporaryFile(c.filename);
1478 if (!tempImage && filename.size() == 0)
1479 appendLogText(i18n("Could not find image file: %1", c.filename));
1480 else if (!tempImage)
1481 displayFITS(filename);
1482 else appendLogText(i18n("Cannot display temporary image file: %1", c.filename));
1483 }
1484}
1485
1486namespace
1487{
1488QString getSign(int val)
1489{
1490 if (val == 0) return "";
1491 else if (val > 0) return "+";
1492 else return "-";
1493}
1494QString signedIntString(int val)
1495{
1496 return QString("%1%2").arg(getSign(val)).arg(abs(val));
1497}
1498}
1499
1500
1501// When the user clicks on a focus session in the timeline,
1502// a table is rendered in the details section, and the HFR/position plot
1503// is displayed in the graphics plot. If focus is ongoing
1504// the information for the graphics is not plotted as it is not yet available.
1505void Analyze::focusSessionClicked(FocusSession &c, bool doubleClick)
1506{
1507 Q_UNUSED(doubleClick);
1508 highlightTimelineItem(c);
1509
1510 if (!c.standardSession)
1511 {
1512 // This is an adaptive focus session
1513 c.setupTable("Focus", "Adaptive", clockTime(c.end), clockTime(c.end), detailsTable);
1514 c.addRow("Filter", c.filter);
1515 addDetailsRow(detailsTable, "Temperature", Qt::yellow, QString("%1°").arg(c.temperature, 0, 'f', 1),
1516 Qt::white, QString("%1").arg(c.tempTicks, 0, 'f', 1));
1517 addDetailsRow(detailsTable, "Altitude", Qt::yellow, QString("%1°").arg(c.altitude, 0, 'f', 1),
1518 Qt::white, QString("%1").arg(c.altTicks, 0, 'f', 1));
1519 addDetailsRow(detailsTable, "Pos Error", Qt::yellow, "Start / End", Qt::white,
1520 QString("%1 / %2").arg(c.prevPosError).arg(c.thisPosError));
1521 addDetailsRow(detailsTable, "Position", Qt::yellow, QString::number(c.adaptedPosition),
1522 Qt::white, signedIntString(c.totalTicks));
1523 return;
1524 }
1525
1526 if (c.success)
1527 c.setupTable("Focus", "successful", clockTime(c.start), clockTime(c.end), detailsTable);
1528 else if (c.isTemporary())
1529 c.setupTable("Focus", "in progress", clockTime(c.start), clockTime(c.start), detailsTable);
1530 else
1531 c.setupTable("Focus", "FAILED", clockTime(c.start), clockTime(c.end), detailsTable);
1532
1533 if (!c.isTemporary())
1534 {
1535 if (c.success)
1536 {
1537 if (c.hfrs.size() > 0)
1538 c.addRow("HFR", QString::number(c.hfrs.last(), 'f', 2));
1539 if (c.positions.size() > 0)
1540 c.addRow("Solution", QString::number(c.positions.last(), 'f', 0));
1541 }
1542 c.addRow("Iterations", QString::number(c.positions.size()));
1543 }
1544 addDetailsRow(detailsTable, "Reason", Qt::yellow, AutofocusReasonStr[c.reason], Qt::white, c.reasonInfo, Qt::white);
1545 if (!c.success && !c.isTemporary())
1546 addDetailsRow(detailsTable, "Fail Reason", Qt::yellow, AutofocusFailReasonStr[c.failCode], Qt::white, c.failCodeInfo,
1547 Qt::white);
1548
1549 c.addRow("Filter", c.filter);
1550 c.addRow("Temperature", (c.temperature == INVALID_VALUE) ? "N/A" : QString::number(c.temperature, 'f', 1));
1551
1552 if (c.isTemporary())
1553 resetGraphicsPlot();
1554 else
1555 displayFocusGraphics(c.positions, c.hfrs, c.useWeights, c.weights, c.outliers, c.curve, c.title, c.success);
1556}
1557
1558// When the user clicks on a guide session in the timeline,
1559// a table is rendered in the details section. If it has a G_GUIDING state
1560// then a drift plot is generated and RMS values are calculated
1561// for the guiding session's time interval.
1562void Analyze::guideSessionClicked(GuideSession &c, bool doubleClick)
1563{
1564 Q_UNUSED(doubleClick);
1565 highlightTimelineItem(c);
1566
1567 QString st;
1568 if (c.simpleState == G_IDLE)
1569 st = "Idle";
1570 else if (c.simpleState == G_GUIDING)
1571 st = "Guiding";
1572 else if (c.simpleState == G_CALIBRATING)
1573 st = "Calibrating";
1574 else if (c.simpleState == G_SUSPENDED)
1575 st = "Suspended";
1576 else if (c.simpleState == G_DITHERING)
1577 st = "Dithering";
1578
1579 c.setupTable("Guide", st, clockTime(c.start), clockTime(c.end), detailsTable);
1580 resetGraphicsPlot();
1581 if (c.simpleState == G_GUIDING)
1582 {
1583 double raRMS, decRMS, totalRMS;
1584 int numSamples;
1585 displayGuideGraphics(c.start, c.end, &raRMS, &decRMS, &totalRMS, &numSamples);
1586 if (numSamples > 0)
1587 {
1588 c.addRow("total RMS", QString::number(totalRMS, 'f', 2));
1589 c.addRow("ra RMS", QString::number(raRMS, 'f', 2));
1590 c.addRow("dec RMS", QString::number(decRMS, 'f', 2));
1591 }
1592 c.addRow("Num Samples", QString::number(numSamples));
1593 }
1594}
1595
1596void Analyze::displayGuideGraphics(double start, double end, double *raRMS,
1597 double *decRMS, double *totalRMS, int *numSamples)
1598{
1599 resetGraphicsPlot();
1600 auto ra = statsPlot->graph(RA_GRAPH)->data()->findBegin(start);
1601 auto dec = statsPlot->graph(DEC_GRAPH)->data()->findBegin(start);
1602 auto raEnd = statsPlot->graph(RA_GRAPH)->data()->findEnd(end);
1603 auto decEnd = statsPlot->graph(DEC_GRAPH)->data()->findEnd(end);
1604 int num = 0;
1605 double raSquareErrorSum = 0, decSquareErrorSum = 0;
1606 while (ra != raEnd && dec != decEnd &&
1607 ra->mainKey() < end && dec->mainKey() < end &&
1608 ra != statsPlot->graph(RA_GRAPH)->data()->constEnd() &&
1609 dec != statsPlot->graph(DEC_GRAPH)->data()->constEnd() &&
1610 ra->mainKey() < end && dec->mainKey() < end)
1611 {
1612 const double raVal = ra->mainValue();
1613 const double decVal = dec->mainValue();
1614 graphicsPlot->graph(GUIDER_GRAPHICS)->addData(raVal, decVal);
1615 if (!qIsNaN(raVal) && !qIsNaN(decVal))
1616 {
1617 raSquareErrorSum += raVal * raVal;
1618 decSquareErrorSum += decVal * decVal;
1619 num++;
1620 }
1621 ra++;
1622 dec++;
1623 }
1624 if (numSamples != nullptr)
1625 *numSamples = num;
1626 if (num > 0)
1627 {
1628 if (raRMS != nullptr)
1629 *raRMS = sqrt(raSquareErrorSum / num);
1630 if (decRMS != nullptr)
1631 *decRMS = sqrt(decSquareErrorSum / num);
1632 if (totalRMS != nullptr)
1633 *totalRMS = sqrt((raSquareErrorSum + decSquareErrorSum) / num);
1634 if (numSamples != nullptr)
1635 *numSamples = num;
1636 }
1637 QCPItemEllipse *c1 = new QCPItemEllipse(graphicsPlot);
1638 c1->bottomRight->setCoords(1.0, -1.0);
1639 c1->topLeft->setCoords(-1.0, 1.0);
1640 QCPItemEllipse *c2 = new QCPItemEllipse(graphicsPlot);
1641 c2->bottomRight->setCoords(2.0, -2.0);
1642 c2->topLeft->setCoords(-2.0, 2.0);
1643 c1->setPen(QPen(Qt::green));
1644 c2->setPen(QPen(Qt::yellow));
1645
1646 // Since the plot is wider than it is tall, these lines set the
1647 // vertical range to 2.5, and the horizontal range to whatever it
1648 // takes to keep the two axes' scales (number of pixels per value)
1649 // the same, so that circles stay circular (i.e. circles are not stretch
1650 // wide even though the graph area is not square).
1651 graphicsPlot->xAxis->setRange(-2.5, 2.5);
1652 graphicsPlot->yAxis->setRange(-2.5, 2.5);
1653 graphicsPlot->xAxis->setScaleRatio(graphicsPlot->yAxis);
1654}
1655
1656// When the user clicks on a particular mount session in the timeline,
1657// a table is rendered in the details section.
1658void Analyze::mountSessionClicked(MountSession &c, bool doubleClick)
1659{
1660 Q_UNUSED(doubleClick);
1661 highlightTimelineItem(c);
1662
1663 c.setupTable("Mount", mountStatusString(c.state), clockTime(c.start),
1664 clockTime(c.isTemporary() ? c.start : c.end), detailsTable);
1665}
1666
1667// When the user clicks on a particular align session in the timeline,
1668// a table is rendered in the details section.
1669void Analyze::alignSessionClicked(AlignSession &c, bool doubleClick)
1670{
1671 Q_UNUSED(doubleClick);
1672 highlightTimelineItem(c);
1673 c.setupTable("Align", getAlignStatusString(c.state), clockTime(c.start),
1674 clockTime(c.isTemporary() ? c.start : c.end), detailsTable);
1675}
1676
1677// When the user clicks on a particular meridian flip session in the timeline,
1678// a table is rendered in the details section.
1679void Analyze::mountFlipSessionClicked(MountFlipSession &c, bool doubleClick)
1680{
1681 Q_UNUSED(doubleClick);
1682 highlightTimelineItem(c);
1683 c.setupTable("Meridian Flip", MeridianFlipState::meridianFlipStatusString(c.state),
1684 clockTime(c.start), clockTime(c.isTemporary() ? c.start : c.end), detailsTable);
1685}
1686
1687// When the user clicks on a particular scheduler session in the timeline,
1688// a table is rendered in the details section.
1689void Analyze::schedulerSessionClicked(SchedulerJobSession &c, bool doubleClick)
1690{
1691 Q_UNUSED(doubleClick);
1692 highlightTimelineItem(c);
1693 c.setupTable("Scheduler Job", c.jobName,
1694 clockTime(c.start), clockTime(c.isTemporary() ? c.start : c.end), detailsTable);
1695 c.addRow("End reason", c.reason);
1696}
1697
1698// This method determines which timeline session (if any) was selected
1699// when the user clicks in the Timeline plot. It also sets a cursor
1700// in the stats plot.
1701void Analyze::processTimelineClick(QMouseEvent *event, bool doubleClick)
1702{
1703 unhighlightTimelineItem();
1704 double xval = timelinePlot->xAxis->pixelToCoord(event->x());
1705 double yval = timelinePlot->yAxis->pixelToCoord(event->y());
1706 if (yval >= CAPTURE_Y - 0.5 && yval <= CAPTURE_Y + 0.5)
1707 {
1708 QList<CaptureSession> candidates = captureSessions.find(xval);
1709 if (candidates.size() > 0)
1710 captureSessionClicked(candidates[0], doubleClick);
1711 else if ((temporaryCaptureSession.rect != nullptr) &&
1712 (xval > temporaryCaptureSession.start))
1713 captureSessionClicked(temporaryCaptureSession, doubleClick);
1714 }
1715 else if (yval >= FOCUS_Y - 0.5 && yval <= FOCUS_Y + 0.5)
1716 {
1717 QList<FocusSession> candidates = focusSessions.find(xval);
1718 if (candidates.size() > 0)
1719 focusSessionClicked(candidates[0], doubleClick);
1720 else if ((temporaryFocusSession.rect != nullptr) &&
1721 (xval > temporaryFocusSession.start))
1722 focusSessionClicked(temporaryFocusSession, doubleClick);
1723 }
1724 else if (yval >= GUIDE_Y - 0.5 && yval <= GUIDE_Y + 0.5)
1725 {
1726 QList<GuideSession> candidates = guideSessions.find(xval);
1727 if (candidates.size() > 0)
1728 guideSessionClicked(candidates[0], doubleClick);
1729 else if ((temporaryGuideSession.rect != nullptr) &&
1730 (xval > temporaryGuideSession.start))
1731 guideSessionClicked(temporaryGuideSession, doubleClick);
1732 }
1733 else if (yval >= MOUNT_Y - 0.5 && yval <= MOUNT_Y + 0.5)
1734 {
1735 QList<MountSession> candidates = mountSessions.find(xval);
1736 if (candidates.size() > 0)
1737 mountSessionClicked(candidates[0], doubleClick);
1738 else if ((temporaryMountSession.rect != nullptr) &&
1739 (xval > temporaryMountSession.start))
1740 mountSessionClicked(temporaryMountSession, doubleClick);
1741 }
1742 else if (yval >= ALIGN_Y - 0.5 && yval <= ALIGN_Y + 0.5)
1743 {
1744 QList<AlignSession> candidates = alignSessions.find(xval);
1745 if (candidates.size() > 0)
1746 alignSessionClicked(candidates[0], doubleClick);
1747 else if ((temporaryAlignSession.rect != nullptr) &&
1748 (xval > temporaryAlignSession.start))
1749 alignSessionClicked(temporaryAlignSession, doubleClick);
1750 }
1751 else if (yval >= MERIDIAN_MOUNT_FLIP_Y - 0.5 && yval <= MERIDIAN_MOUNT_FLIP_Y + 0.5)
1752 {
1753 QList<MountFlipSession> candidates = mountFlipSessions.find(xval);
1754 if (candidates.size() > 0)
1755 mountFlipSessionClicked(candidates[0], doubleClick);
1756 else if ((temporaryMountFlipSession.rect != nullptr) &&
1757 (xval > temporaryMountFlipSession.start))
1758 mountFlipSessionClicked(temporaryMountFlipSession, doubleClick);
1759 }
1760 else if (yval >= SCHEDULER_Y - 0.5 && yval <= SCHEDULER_Y + 0.5)
1761 {
1762 QList<SchedulerJobSession> candidates = schedulerJobSessions.find(xval);
1763 if (candidates.size() > 0)
1764 schedulerSessionClicked(candidates[0], doubleClick);
1765 else if ((temporarySchedulerJobSession.rect != nullptr) &&
1766 (xval > temporarySchedulerJobSession.start))
1767 schedulerSessionClicked(temporarySchedulerJobSession, doubleClick);
1768 }
1769 setStatsCursor(xval);
1770 replot();
1771}
1772
1773void Analyze::nextTimelineItem()
1774{
1775 changeTimelineItem(true);
1776}
1777
1778void Analyze::previousTimelineItem()
1779{
1780 changeTimelineItem(false);
1781}
1782
1783void Analyze::changeTimelineItem(bool next)
1784{
1785 if (m_selectedSession.start == 0 && m_selectedSession.end == 0) return;
1786 switch(m_selectedSession.offset)
1787 {
1788 case CAPTURE_Y:
1789 {
1790 auto nextSession = next ? captureSessions.findNext(m_selectedSession.start)
1791 : captureSessions.findPrevious(m_selectedSession.start);
1792
1793 // Since we're displaying the images, don't want to stop at an aborted capture.
1794 // Continue searching until a good session (or no session) is found.
1795 while (nextSession && nextSession->aborted)
1796 nextSession = next ? captureSessions.findNext(nextSession->start)
1797 : captureSessions.findPrevious(nextSession->start);
1798
1799 if (nextSession)
1800 {
1801 // True because we want to display the image (so simulate a double-click on that session).
1802 captureSessionClicked(*nextSession, true);
1803 setStatsCursor((nextSession->end + nextSession->start) / 2);
1804 }
1805 break;
1806 }
1807 case FOCUS_Y:
1808 {
1809 auto nextSession = next ? focusSessions.findNext(m_selectedSession.start)
1810 : focusSessions.findPrevious(m_selectedSession.start);
1811 if (nextSession)
1812 {
1813 focusSessionClicked(*nextSession, true);
1814 setStatsCursor((nextSession->end + nextSession->start) / 2);
1815 }
1816 break;
1817 }
1818 case ALIGN_Y:
1819 {
1820 auto nextSession = next ? alignSessions.findNext(m_selectedSession.start)
1821 : alignSessions.findPrevious(m_selectedSession.start);
1822 if (nextSession)
1823 {
1824 alignSessionClicked(*nextSession, true);
1825 setStatsCursor((nextSession->end + nextSession->start) / 2);
1826 }
1827 break;
1828 }
1829 case GUIDE_Y:
1830 {
1831 auto nextSession = next ? guideSessions.findNext(m_selectedSession.start)
1832 : guideSessions.findPrevious(m_selectedSession.start);
1833 if (nextSession)
1834 {
1835 guideSessionClicked(*nextSession, true);
1836 setStatsCursor((nextSession->end + nextSession->start) / 2);
1837 }
1838 break;
1839 }
1840 case MOUNT_Y:
1841 {
1842 auto nextSession = next ? mountSessions.findNext(m_selectedSession.start)
1843 : mountSessions.findPrevious(m_selectedSession.start);
1844 if (nextSession)
1845 {
1846 mountSessionClicked(*nextSession, true);
1847 setStatsCursor((nextSession->end + nextSession->start) / 2);
1848 }
1849 break;
1850 }
1851 case SCHEDULER_Y:
1852 {
1853 auto nextSession = next ? schedulerJobSessions.findNext(m_selectedSession.start)
1854 : schedulerJobSessions.findPrevious(m_selectedSession.start);
1855 if (nextSession)
1856 {
1857 schedulerSessionClicked(*nextSession, true);
1858 setStatsCursor((nextSession->end + nextSession->start) / 2);
1859 }
1860 break;
1861 }
1862 //case MERIDIAN_MOUNT_FLIP_Y:
1863 }
1864 if (!isVisible(m_selectedSession) && !isVisible(m_selectedSession))
1865 adjustView((m_selectedSession.start + m_selectedSession.end) / 2.0);
1866 replot();
1867}
1868
1869bool Analyze::isVisible(const Session &s) const
1870{
1871 if (fullWidthCB->isChecked())
1872 return true;
1873 return !((s.start < plotStart && s.end < plotStart) ||
1874 (s.start > (plotStart + plotWidth) && s.end > (plotStart + plotWidth)));
1875}
1876
1877void Analyze::adjustView(double time)
1878{
1879 if (!fullWidthCB->isChecked())
1880 {
1881 plotStart = time - plotWidth / 2;
1882 }
1883}
1884
1885void Analyze::setStatsCursor(double time)
1886{
1887 removeStatsCursor();
1888
1889 // Cursor on the stats graph.
1890 QCPItemLine *line = new QCPItemLine(statsPlot);
1892 const double top = statsPlot->yAxis->range().upper;
1893 const double bottom = statsPlot->yAxis->range().lower;
1894 line->start->setCoords(time, bottom);
1895 line->end->setCoords(time, top);
1896 statsCursor = line;
1897
1898 // Cursor on the timeline.
1899 QCPItemLine *line2 = new QCPItemLine(timelinePlot);
1901 const double top2 = timelinePlot->yAxis->range().upper;
1902 const double bottom2 = timelinePlot->yAxis->range().lower;
1903 line2->start->setCoords(time, bottom2);
1904 line2->end->setCoords(time, top2);
1905 timelineCursor = line2;
1906
1907 cursorTimeOut->setText(QString("%1s").arg(time));
1908 cursorClockTimeOut->setText(QString("%1")
1909 .arg(clockTime(time).toString("hh:mm:ss")));
1910 statsCursorTime = time;
1911 keepCurrentCB->setCheckState(Qt::Unchecked);
1912}
1913
1914void Analyze::removeStatsCursor()
1915{
1916 if (statsCursor != nullptr)
1917 statsPlot->removeItem(statsCursor);
1918 statsCursor = nullptr;
1919
1920 if (timelineCursor != nullptr)
1921 timelinePlot->removeItem(timelineCursor);
1922 timelineCursor = nullptr;
1923
1924 cursorTimeOut->setText("");
1925 cursorClockTimeOut->setText("");
1926 statsCursorTime = -1;
1927}
1928
1929// When the users clicks in the stats plot, the cursor is set at the corresponding time.
1930void Analyze::processStatsClick(QMouseEvent *event, bool doubleClick)
1931{
1932 Q_UNUSED(doubleClick);
1933 double xval = statsPlot->xAxis->pixelToCoord(event->x());
1934 setStatsCursor(xval);
1935 replot();
1936}
1937
1938void Analyze::timelineMousePress(QMouseEvent *event)
1939{
1940 processTimelineClick(event, false);
1941}
1942
1943void Analyze::timelineMouseDoubleClick(QMouseEvent *event)
1944{
1945 processTimelineClick(event, true);
1946}
1947
1948void Analyze::statsMousePress(QMouseEvent *event)
1949{
1950 QCPAxis *yAxis = activeYAxis;
1951 if (!yAxis) return;
1952
1953 // If we're on the y-axis, adjust the y-axis.
1954 if (statsPlot->xAxis->pixelToCoord(event->x()) < plotStart)
1955 {
1956 yAxisInitialPos = yAxis->pixelToCoord(event->y());
1957 return;
1958 }
1959 processStatsClick(event, false);
1960}
1961
1962void Analyze::statsMouseDoubleClick(QMouseEvent *event)
1963{
1964 processStatsClick(event, true);
1965}
1966
1967// Allow the user to click and hold, causing the cursor to move in real-time.
1968void Analyze::statsMouseMove(QMouseEvent *event)
1969{
1970 QCPAxis *yAxis = activeYAxis;
1971 if (!yAxis) return;
1972
1973 // If we're on the y-axis, adjust the y-axis.
1974 if (statsPlot->xAxis->pixelToCoord(event->x()) < plotStart)
1975 {
1976 auto range = yAxis->range();
1977 double yDiff = yAxisInitialPos - yAxis->pixelToCoord(event->y());
1978 yAxis->setRange(range.lower + yDiff, range.upper + yDiff);
1979 replot();
1980 if (m_YAxisTool.isVisible() && m_YAxisTool.getAxis() == yAxis)
1981 m_YAxisTool.replot(true);
1982 return;
1983 }
1984 processStatsClick(event, false);
1985}
1986
1987// Called by the scrollbar, to move the current view.
1988void Analyze::scroll(int value)
1989{
1990 double pct = static_cast<double>(value) / MAX_SCROLL_VALUE;
1991 plotStart = std::max(0.0, maxXValue * pct - plotWidth / 2.0);
1992 // Normally replot adjusts the position of the slider.
1993 // If the user has done that, we don't want replot to re-do it.
1994 replot(false);
1995
1996}
1997void Analyze::scrollRight()
1998{
1999 plotStart = std::min(maxXValue - plotWidth / 5, plotStart + plotWidth / 5);
2000 fullWidthCB->setChecked(false);
2001 replot();
2002
2003}
2004void Analyze::scrollLeft()
2005{
2006 plotStart = std::max(0.0, plotStart - plotWidth / 5);
2007 fullWidthCB->setChecked(false);
2008 replot();
2009
2010}
2011void Analyze::replot(bool adjustSlider)
2012{
2013 adjustTemporarySessions();
2014 if (fullWidthCB->isChecked())
2015 {
2016 plotStart = 0;
2017 plotWidth = std::max(10.0, maxXValue);
2018 }
2019 else if (keepCurrentCB->isChecked())
2020 {
2021 plotStart = std::max(0.0, maxXValue - plotWidth);
2022 }
2023 // If we're keeping to the latest values,
2024 // set the time display to the latest time.
2025 if (keepCurrentCB->isChecked() && statsCursor == nullptr)
2026 {
2027 cursorTimeOut->setText(QString("%1s").arg(maxXValue));
2028 cursorClockTimeOut->setText(QString("%1")
2029 .arg(clockTime(maxXValue).toString("hh:mm:ss")));
2030 }
2031 analyzeSB->setPageStep(
2032 std::min(MAX_SCROLL_VALUE,
2033 static_cast<int>(MAX_SCROLL_VALUE * plotWidth / maxXValue)));
2034 if (adjustSlider)
2035 {
2036 double sliderCenter = plotStart + plotWidth / 2.0;
2037 analyzeSB->setSliderPosition(MAX_SCROLL_VALUE * (sliderCenter / maxXValue));
2038 }
2039
2040 timelinePlot->xAxis->setRange(plotStart, plotStart + plotWidth);
2041 timelinePlot->yAxis->setRange(0, LAST_Y);
2042
2043 statsPlot->xAxis->setRange(plotStart, plotStart + plotWidth);
2044
2045 // Rescale any automatic y-axes.
2046 if (statsPlot->isVisible())
2047 {
2048 for (auto &pairs : yAxisMap)
2049 {
2050 const YAxisInfo &info = pairs.second;
2051 if (statsPlot->graph(info.graphIndex)->visible() && info.rescale)
2052 {
2053 QCPAxis *axis = info.axis;
2054 axis->rescale();
2055 axis->scaleRange(1.1, axis->range().center());
2056 }
2057 }
2058 }
2059
2060 dateTicker->setOffset(displayStartTime.toMSecsSinceEpoch() / 1000.0);
2061
2062 timelinePlot->replot();
2063 statsPlot->replot();
2064 graphicsPlot->replot();
2065
2066 if (activeYAxis != nullptr)
2067 {
2068 // Adjust the statsPlot padding to align statsPlot and timelinePlot.
2069 const int widthDiff = statsPlot->axisRect()->width() - timelinePlot->axisRect()->width();
2070 const int paddingSize = activeYAxis->padding();
2071 constexpr int maxPadding = 100;
2072 // Don't quite following why a positive difference should INCREASE padding, but it works.
2073 const int newPad = std::min(maxPadding, std::max(0, paddingSize + widthDiff));
2074 if (newPad != paddingSize)
2075 {
2076 activeYAxis->setPadding(newPad);
2077 statsPlot->replot();
2078 }
2079 }
2080 updateStatsValues();
2081}
2082
2083void Analyze::statsYZoom(double zoomAmount)
2084{
2085 auto axis = activeYAxis;
2086 if (!axis) return;
2087 auto range = axis->range();
2088 const double halfDiff = (range.upper - range.lower) / 2.0;
2089 const double middle = (range.upper + range.lower) / 2.0;
2090 axis->setRange(QCPRange(middle - halfDiff * zoomAmount, middle + halfDiff * zoomAmount));
2091 if (m_YAxisTool.isVisible() && m_YAxisTool.getAxis() == axis)
2092 m_YAxisTool.replot(true);
2093}
2094void Analyze::statsYZoomIn()
2095{
2096 statsYZoom(0.80);
2097 statsPlot->replot();
2098}
2099void Analyze::statsYZoomOut()
2100{
2101 statsYZoom(1.25);
2102 statsPlot->replot();
2103}
2104
2105namespace
2106{
2107// Pass in a function that converts the double graph value to a string
2108// for the value box.
2109template<typename Func>
2110void updateStat(double time, QLineEdit *valueBox, QCPGraph *graph, Func func, bool useLastRealVal = false)
2111{
2112 auto begin = graph->data()->findBegin(time);
2113 double timeDiffThreshold = 10000000.0;
2114 if ((begin != graph->data()->constEnd()) &&
2115 (fabs(begin->mainKey() - time) < timeDiffThreshold))
2116 {
2117 double foundVal = begin->mainValue();
2118 valueBox->setDisabled(false);
2119 if (qIsNaN(foundVal))
2120 {
2121 int index = graph->findBegin(time);
2122 const double MAX_TIME_DIFF = 600;
2123 while (useLastRealVal && index >= 0)
2124 {
2125 const double val = graph->data()->at(index)->mainValue();
2126 const double t = graph->data()->at(index)->mainKey();
2127 if (time - t > MAX_TIME_DIFF)
2128 break;
2129 if (!qIsNaN(val))
2130 {
2131 valueBox->setText(func(val));
2132 return;
2133 }
2134 index--;
2135 }
2136 valueBox->clear();
2137 }
2138 else
2139 valueBox->setText(func(foundVal));
2140 }
2141 else valueBox->setDisabled(true);
2142}
2143
2144} // namespace
2145
2146// This populates the output boxes below the stats plot with the correct statistics.
2147void Analyze::updateStatsValues()
2148{
2149 const double time = statsCursorTime < 0 ? maxXValue : statsCursorTime;
2150
2151 auto d2Fcn = [](double d) -> QString { return QString::number(d, 'f', 2); };
2152 auto d1Fcn = [](double d) -> QString { return QString::number(d, 'f', 1); };
2153 // HFR, numCaptureStars, median & eccentricity are the only ones to use the last real value,
2154 // that is, it keeps those values from the last exposure.
2155 updateStat(time, hfrOut, statsPlot->graph(HFR_GRAPH), d2Fcn, true);
2156 updateStat(time, eccentricityOut, statsPlot->graph(ECCENTRICITY_GRAPH), d2Fcn, true);
2157 updateStat(time, skyBgOut, statsPlot->graph(SKYBG_GRAPH), d1Fcn);
2158 updateStat(time, snrOut, statsPlot->graph(SNR_GRAPH), d1Fcn);
2159 updateStat(time, raOut, statsPlot->graph(RA_GRAPH), d2Fcn);
2160 updateStat(time, decOut, statsPlot->graph(DEC_GRAPH), d2Fcn);
2161 updateStat(time, driftOut, statsPlot->graph(DRIFT_GRAPH), d2Fcn);
2162 updateStat(time, rmsOut, statsPlot->graph(RMS_GRAPH), d2Fcn);
2163 updateStat(time, rmsCOut, statsPlot->graph(CAPTURE_RMS_GRAPH), d2Fcn);
2164 updateStat(time, azOut, statsPlot->graph(AZ_GRAPH), d1Fcn);
2165 updateStat(time, altOut, statsPlot->graph(ALT_GRAPH), d2Fcn);
2166 updateStat(time, temperatureOut, statsPlot->graph(TEMPERATURE_GRAPH), d2Fcn);
2167
2168 auto asFcn = [](double d) -> QString { return QString("%1\"").arg(d, 0, 'f', 0); };
2169 updateStat(time, targetDistanceOut, statsPlot->graph(TARGET_DISTANCE_GRAPH), asFcn, true);
2170
2171 auto hmsFcn = [](double d) -> QString
2172 {
2173 dms ra;
2174 ra.setD(d);
2175 return QString("%1:%2:%3").arg(ra.hour()).arg(ra.minute()).arg(ra.second());
2176 //return ra.toHMSString();
2177 };
2178 updateStat(time, mountRaOut, statsPlot->graph(MOUNT_RA_GRAPH), hmsFcn);
2179 auto dmsFcn = [](double d) -> QString { dms dec; dec.setD(d); return dec.toDMSString(); };
2180 updateStat(time, mountDecOut, statsPlot->graph(MOUNT_DEC_GRAPH), dmsFcn);
2181 auto haFcn = [](double d) -> QString
2182 {
2183 dms ha;
2184 QChar z('0');
2185 QChar sgn('+');
2186 ha.setD(d);
2187 if (ha.Hours() > 12.0)
2188 {
2189 ha.setH(24.0 - ha.Hours());
2190 sgn = '-';
2191 }
2192 return QString("%1%2:%3").arg(sgn).arg(ha.hour(), 2, 10, z)
2193 .arg(ha.minute(), 2, 10, z);
2194 };
2195 updateStat(time, mountHaOut, statsPlot->graph(MOUNT_HA_GRAPH), haFcn);
2196
2197 auto intFcn = [](double d) -> QString { return QString::number(d, 'f', 0); };
2198 updateStat(time, numStarsOut, statsPlot->graph(NUMSTARS_GRAPH), intFcn);
2199 updateStat(time, raPulseOut, statsPlot->graph(RA_PULSE_GRAPH), intFcn);
2200 updateStat(time, decPulseOut, statsPlot->graph(DEC_PULSE_GRAPH), intFcn);
2201 updateStat(time, numCaptureStarsOut, statsPlot->graph(NUM_CAPTURE_STARS_GRAPH), intFcn, true);
2202 updateStat(time, medianOut, statsPlot->graph(MEDIAN_GRAPH), intFcn, true);
2203 updateStat(time, focusPositionOut, statsPlot->graph(FOCUS_POSITION_GRAPH), intFcn);
2204
2205 auto pierFcn = [](double d) -> QString
2206 {
2207 return d == 0.0 ? "W->E" : d == 1.0 ? "E->W" : "?";
2208 };
2209 updateStat(time, pierSideOut, statsPlot->graph(PIER_SIDE_GRAPH), pierFcn);
2210}
2211
2212void Analyze::initStatsCheckboxes()
2213{
2214 hfrCB->setChecked(Options::analyzeHFR());
2215 numCaptureStarsCB->setChecked(Options::analyzeNumCaptureStars());
2216 medianCB->setChecked(Options::analyzeMedian());
2217 eccentricityCB->setChecked(Options::analyzeEccentricity());
2218 numStarsCB->setChecked(Options::analyzeNumStars());
2219 skyBgCB->setChecked(Options::analyzeSkyBg());
2220 snrCB->setChecked(Options::analyzeSNR());
2221 temperatureCB->setChecked(Options::analyzeTemperature());
2222 focusPositionCB->setChecked(Options::focusPosition());
2223 targetDistanceCB->setChecked(Options::analyzeTargetDistance());
2224 raCB->setChecked(Options::analyzeRA());
2225 decCB->setChecked(Options::analyzeDEC());
2226 raPulseCB->setChecked(Options::analyzeRAp());
2227 decPulseCB->setChecked(Options::analyzeDECp());
2228 driftCB->setChecked(Options::analyzeDrift());
2229 rmsCB->setChecked(Options::analyzeRMS());
2230 rmsCCB->setChecked(Options::analyzeRMSC());
2231 mountRaCB->setChecked(Options::analyzeMountRA());
2232 mountDecCB->setChecked(Options::analyzeMountDEC());
2233 mountHaCB->setChecked(Options::analyzeMountHA());
2234 azCB->setChecked(Options::analyzeAz());
2235 altCB->setChecked(Options::analyzeAlt());
2236 pierSideCB->setChecked(Options::analyzePierSide());
2237}
2238
2239void Analyze::zoomIn()
2240{
2241 if (plotWidth > 0.5)
2242 {
2243 if (keepCurrentCB->isChecked())
2244 // If we're keeping to the end of the data, keep the end on the right.
2245 plotStart = std::max(0.0, maxXValue - plotWidth / 4.0);
2246 else if (statsCursorTime >= 0)
2247 // If there is a cursor, try to move it to the center.
2248 plotStart = std::max(0.0, statsCursorTime - plotWidth / 4.0);
2249 else
2250 // Keep the center the same.
2251 plotStart += plotWidth / 4.0;
2252 plotWidth = plotWidth / 2.0;
2253 }
2254 fullWidthCB->setChecked(false);
2255 replot();
2256}
2257
2258void Analyze::zoomOut()
2259{
2260 if (plotWidth < maxXValue)
2261 {
2262 plotStart = std::max(0.0, plotStart - plotWidth / 2.0);
2263 plotWidth = plotWidth * 2;
2264 }
2265 fullWidthCB->setChecked(false);
2266 replot();
2267}
2268
2269namespace
2270{
2271
2272void setupAxisDefaults(QCPAxis *axis)
2273{
2274 axis->setBasePen(QPen(Qt::white, 1));
2275 axis->grid()->setPen(QPen(QColor(140, 140, 140, 140), 1, Qt::DotLine));
2276 axis->grid()->setSubGridPen(QPen(QColor(40, 40, 40), 1, Qt::DotLine));
2277 axis->grid()->setZeroLinePen(Qt::NoPen);
2278 axis->setBasePen(QPen(Qt::white, 1));
2279 axis->setTickPen(QPen(Qt::white, 1));
2280 axis->setSubTickPen(QPen(Qt::white, 1));
2282 axis->setLabelColor(Qt::white);
2283 axis->grid()->setVisible(true);
2284}
2285
2286// Generic initialization of a plot, applied to all plots in this tab.
2287void initQCP(QCustomPlot *plot)
2288{
2290 setupAxisDefaults(plot->yAxis);
2291 setupAxisDefaults(plot->xAxis);
2293}
2294} // namespace
2295
2296void Analyze::initTimelinePlot()
2297{
2298 initQCP(timelinePlot);
2299
2300 // This places the labels on the left of the timeline.
2302 textTicker->addTick(CAPTURE_Y, i18n("Capture"));
2303 textTicker->addTick(FOCUS_Y, i18n("Focus"));
2304 textTicker->addTick(ALIGN_Y, i18n("Align"));
2305 textTicker->addTick(GUIDE_Y, i18n("Guide"));
2306 textTicker->addTick(MERIDIAN_MOUNT_FLIP_Y, i18n("Flip"));
2307 textTicker->addTick(MOUNT_Y, i18n("Mount"));
2308 textTicker->addTick(SCHEDULER_Y, i18n("Job"));
2309 timelinePlot->yAxis->setTicker(textTicker);
2310
2311 ADAPTIVE_FOCUS_GRAPH = initGraph(timelinePlot, timelinePlot->yAxis, QCPGraph::lsNone, Qt::red, "adaptiveFocus");
2312 timelinePlot->graph(ADAPTIVE_FOCUS_GRAPH)->setPen(QPen(Qt::red, 2));
2313 timelinePlot->graph(ADAPTIVE_FOCUS_GRAPH)->setScatterStyle(QCPScatterStyle::ssDisc);
2314}
2315
2316// Turn on and off the various statistics, adding/removing them from the legend.
2317void Analyze::toggleGraph(int graph_id, bool show)
2318{
2319 statsPlot->graph(graph_id)->setVisible(show);
2320 if (show)
2321 statsPlot->graph(graph_id)->addToLegend();
2322 else
2323 statsPlot->graph(graph_id)->removeFromLegend();
2324 replot();
2325}
2326
2327int Analyze::initGraph(QCustomPlot * plot, QCPAxis * yAxis, QCPGraph::LineStyle lineStyle,
2328 const QColor &color, const QString &name)
2329{
2330 int num = plot->graphCount();
2331 plot->addGraph(plot->xAxis, yAxis);
2332 plot->graph(num)->setLineStyle(lineStyle);
2333 plot->graph(num)->setPen(QPen(color));
2334 plot->graph(num)->setName(name);
2335 return num;
2336}
2337
2338void Analyze::updateYAxisMap(QObject * key, const YAxisInfo &axisInfo)
2339{
2340 if (key == nullptr) return;
2341 auto axisEntry = yAxisMap.find(key);
2342 if (axisEntry == yAxisMap.end())
2343 yAxisMap.insert(std::make_pair(key, axisInfo));
2344 else
2345 axisEntry->second = axisInfo;
2346}
2347
2348template <typename Func>
2349int Analyze::initGraphAndCB(QCustomPlot * plot, QCPAxis * yAxis, QCPGraph::LineStyle lineStyle,
2350 const QColor &color, const QString &name, const QString &shortName,
2351 QCheckBox * cb, Func setCb, QLineEdit * out)
2352{
2353 const int num = initGraph(plot, yAxis, lineStyle, color, shortName);
2354 if (out != nullptr)
2355 {
2356 const bool autoAxis = YAxisInfo::isRescale(yAxis->range());
2357 updateYAxisMap(out, YAxisInfo(yAxis, yAxis->range(), autoAxis, num, plot, cb, name, shortName, color));
2358 }
2359 if (cb != nullptr)
2360 {
2361 // Don't call toggleGraph() here, as it's too early for replot().
2362 bool show = cb->isChecked();
2363 plot->graph(num)->setVisible(show);
2364 if (show)
2365 plot->graph(num)->addToLegend();
2366 else
2367 plot->graph(num)->removeFromLegend();
2368
2370 [ = ](bool show)
2371 {
2372 this->toggleGraph(num, show);
2373 setCb(show);
2374 });
2375 }
2376 return num;
2377}
2378
2379
2380void Analyze::userSetAxisColor(QObject *key, const YAxisInfo &axisInfo, const QColor &color)
2381{
2382 updateYAxisMap(key, axisInfo);
2383 statsPlot->graph(axisInfo.graphIndex)->setPen(QPen(color));
2384 Options::setAnalyzeStatsYAxis(serializeYAxes());
2385 replot();
2386}
2387
2388void Analyze::userSetLeftAxis(QCPAxis *axis)
2389{
2390 setLeftAxis(axis);
2391 Options::setAnalyzeStatsYAxis(serializeYAxes());
2392 replot();
2393}
2394
2395void Analyze::userChangedYAxis(QObject *key, const YAxisInfo &axisInfo)
2396{
2397 updateYAxisMap(key, axisInfo);
2398 Options::setAnalyzeStatsYAxis(serializeYAxes());
2399 replot();
2400}
2401
2402// TODO: Doesn't seem like this is ever getting called. Not sure why not receiving the rangeChanged signal.
2403void Analyze::yAxisRangeChanged(const QCPRange &newRange)
2404{
2405 Q_UNUSED(newRange);
2406 if (m_YAxisTool.isVisible() && m_YAxisTool.getAxis() == activeYAxis)
2407 m_YAxisTool.replot(true);
2408}
2409
2410void Analyze::setLeftAxis(QCPAxis *axis)
2411{
2412 if (axis != nullptr && axis != activeYAxis)
2413 {
2414 for (const auto &pair : yAxisMap)
2415 {
2416 disconnect(pair.second.axis, QOverload<const QCPRange &>::of(&QCPAxis::rangeChanged), this,
2417 QOverload<const QCPRange &>::of(&Analyze::yAxisRangeChanged));
2418 pair.second.axis->setVisible(false);
2419 }
2420 axis->setVisible(true);
2421 activeYAxis = axis;
2422 statsPlot->axisRect()->setRangeZoomAxes(0, axis);
2423 connect(axis, QOverload<const QCPRange &>::of(&QCPAxis::rangeChanged), this,
2424 QOverload<const QCPRange &>::of(&Analyze::yAxisRangeChanged));
2425 }
2426}
2427
2428void Analyze::startYAxisTool(QObject * key, const YAxisInfo &info)
2429{
2430 if (info.checkBox && !info.checkBox->isChecked())
2431 {
2432 // Enable the graph.
2433 info.checkBox->setChecked(true);
2434 statsPlot->graph(info.graphIndex)->setVisible(true);
2435 statsPlot->graph(info.graphIndex)->addToLegend();
2436 }
2437
2438 m_YAxisTool.reset(key, info, info.axis == activeYAxis);
2439 m_YAxisTool.show();
2440}
2441
2442QCPAxis *Analyze::newStatsYAxis(const QString &label, double lower, double upper)
2443{
2444 QCPAxis *axis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0); // 0 means QCP creates the axis.
2445 axis->setVisible(false);
2446 axis->setRange(lower, upper);
2447 axis->setLabel(label);
2448 setupAxisDefaults(axis);
2449 return axis;
2450}
2451
2452bool Analyze::restoreYAxes(const QString &encoding)
2453{
2454 constexpr int headerSize = 2;
2455 constexpr int itemSize = 5;
2456 QStringList items = encoding.split(',');
2457 if (items.size() <= headerSize) return false;
2458 if ((items.size() - headerSize) % itemSize != 0) return false;
2459 if (items[0] != "AnalyzeStatsYAxis1.0") return false;
2460
2461 // Restore the active Y axis
2462 const QString leftID = "left=";
2463 if (!items[1].startsWith(leftID)) return false;
2464 QString left = items[1].mid(leftID.size());
2465 if (left.size() <= 0) return false;
2466 for (const auto &pair : yAxisMap)
2467 {
2468 if (pair.second.axis->label() == left)
2469 {
2470 setLeftAxis(pair.second.axis);
2471 break;
2472 }
2473 }
2474
2475 // Restore the various upper/lower/rescale axis values.
2476 for (int i = headerSize; i < items.size(); i += itemSize)
2477 {
2478 const QString shortName = items[i];
2479 const double lower = items[i + 1].toDouble();
2480 const double upper = items[i + 2].toDouble();
2481 const bool rescale = items[i + 3] == "T";
2482 const QColor color(items[i + 4]);
2483 for (auto &pair : yAxisMap)
2484 {
2485 auto &info = pair.second;
2486 if (info.axis->label() == shortName)
2487 {
2488 info.color = color;
2489 statsPlot->graph(info.graphIndex)->setPen(QPen(color));
2490 info.rescale = rescale;
2491 if (rescale)
2492 info.axis->setRange(
2493 QCPRange(YAxisInfo::LOWER_RESCALE,
2494 YAxisInfo::UPPER_RESCALE));
2495 else
2496 info.axis->setRange(QCPRange(lower, upper));
2497 break;
2498 }
2499 }
2500 }
2501 return true;
2502}
2503
2504// This would be sensitive to short names with commas in them, but we don't do that.
2505QString Analyze::serializeYAxes()
2506{
2507 QString encoding = QString("AnalyzeStatsYAxis1.0,left=%1").arg(activeYAxis->label());
2508 QList<QString> savedAxes;
2509 for (const auto &pair : yAxisMap)
2510 {
2511 const YAxisInfo &info = pair.second;
2512 const bool rescale = info.rescale;
2513
2514 // Only save if something has changed.
2515 bool somethingChanged = (info.initialColor != info.color) ||
2516 (rescale != YAxisInfo::isRescale(info.initialRange)) ||
2517 (!rescale && info.axis->range() != info.initialRange);
2518
2519 if (!somethingChanged) continue;
2520
2521 // Don't save the same axis twice
2522 if (savedAxes.contains(info.axis->label())) continue;
2523
2524 double lower = rescale ? YAxisInfo::LOWER_RESCALE : info.axis->range().lower;
2525 double upper = rescale ? YAxisInfo::UPPER_RESCALE : info.axis->range().upper;
2526 encoding.append(QString(",%1,%2,%3,%4,%5")
2527 .arg(info.axis->label()).arg(lower).arg(upper)
2528 .arg(info.rescale ? "T" : "F").arg(info.color.name()));
2529 savedAxes.append(info.axis->label());
2530 }
2531 return encoding;
2532}
2533
2534void Analyze::initStatsPlot()
2535{
2536 initQCP(statsPlot);
2537
2538 // Setup the main y-axis
2539 statsPlot->yAxis->setVisible(true);
2540 statsPlot->yAxis->setLabel("RA/DEC");
2541 statsPlot->yAxis->setRange(-2, 5);
2542 setLeftAxis(statsPlot->yAxis);
2543
2544 // Setup the legend
2545 statsPlot->legend->setVisible(true);
2546 statsPlot->legend->setFont(QFont("Helvetica", 6));
2547 statsPlot->legend->setTextColor(Qt::white);
2548 // Legend background is transparent.
2549 statsPlot->legend->setBrush(QBrush(QColor(0, 0, 0, 50)));
2550 // Legend stacks vertically.
2551 statsPlot->legend->setFillOrder(QCPLegend::foRowsFirst);
2552 // Rows pretty tightly packed.
2553 statsPlot->legend->setRowSpacing(-10);
2554
2555 statsPlot->axisRect()->insetLayout()->setInsetAlignment(0, Qt::AlignLeft | Qt::AlignTop);
2556 statsPlot->legend->setSelectableParts(QCPLegend::spLegendBox);
2557
2558 // Make the lines part of the legend less long.
2559 statsPlot->legend->setIconSize(10, 18);
2560 statsPlot->legend->setIconTextPadding(3);
2561
2562 // Clicking on the legend makes it very small (and thus less obscuring the plot).
2563 // Clicking again restores it to its original size.
2564 connect(statsPlot, &QCustomPlot::legendClick, [ = ](QCPLegend * legend, QCPAbstractLegendItem * item, QMouseEvent * event)
2565 {
2566 Q_UNUSED(legend);
2567 Q_UNUSED(item);
2568 Q_UNUSED(event);
2569 if (statsPlot->legend->font().pointSize() < 6)
2570 {
2571 // Restore the original legend.
2572 statsPlot->legend->setRowSpacing(-10);
2573 statsPlot->legend->setIconSize(10, 18);
2574 statsPlot->legend->setFont(QFont("Helvetica", 6));
2575 statsPlot->legend->setBrush(QBrush(QColor(0, 0, 0, 50)));
2576 }
2577 else
2578 {
2579 // Make the legend very small (unreadable, but clickable).
2580 statsPlot->legend->setRowSpacing(-10);
2581 statsPlot->legend->setIconSize(5, 5);
2582 statsPlot->legend->setFont(QFont("Helvetica", 1));
2583 statsPlot->legend->setBrush(QBrush(QColor(0, 0, 0, 0)));
2584 }
2585 statsPlot->replot();
2586 });
2587
2588
2589 // Add the graphs.
2590 QString shortName = "HFR";
2591 QCPAxis *hfrAxis = newStatsYAxis(shortName, -2, 6);
2592 HFR_GRAPH = initGraphAndCB(statsPlot, hfrAxis, QCPGraph::lsStepRight, Qt::cyan, "Capture Image HFR", shortName, hfrCB,
2593 Options::setAnalyzeHFR, hfrOut);
2595 [ = ](bool show)
2596 {
2597 if (show && !Options::autoHFR())
2598 KSNotification::info(
2599 i18n("The \"Auto Compute HFR\" option in the KStars "
2600 "FITS options menu is not set. You won't get HFR values "
2601 "without it. Once you set it, newly captured images "
2602 "will have their HFRs computed."));
2603 });
2604
2605 shortName = "#SubStars";
2606 QCPAxis *numCaptureStarsAxis = newStatsYAxis(shortName);
2607 NUM_CAPTURE_STARS_GRAPH = initGraphAndCB(statsPlot, numCaptureStarsAxis, QCPGraph::lsStepRight, Qt::darkGreen,
2608 "#Stars in Capture", shortName,
2609 numCaptureStarsCB, Options::setAnalyzeNumCaptureStars, numCaptureStarsOut);
2610 connect(numCaptureStarsCB, &QCheckBox::clicked,
2611 [ = ](bool show)
2612 {
2613 if (show && !Options::autoHFR())
2614 KSNotification::info(
2615 i18n("The \"Auto Compute HFR\" option in the KStars "
2616 "FITS options menu is not set. You won't get # stars in capture image values "
2617 "without it. Once you set it, newly captured images "
2618 "will have their stars detected."));
2619 });
2620
2621 shortName = "median";
2622 QCPAxis *medianAxis = newStatsYAxis(shortName);
2623 MEDIAN_GRAPH = initGraphAndCB(statsPlot, medianAxis, QCPGraph::lsStepRight, Qt::darkGray, "Median Pixel", shortName,
2624 medianCB, Options::setAnalyzeMedian, medianOut);
2625
2626 shortName = "ecc";
2627 QCPAxis *eccAxis = newStatsYAxis(shortName, 0, 1.0);
2628 ECCENTRICITY_GRAPH = initGraphAndCB(statsPlot, eccAxis, QCPGraph::lsStepRight, Qt::darkMagenta, "Eccentricity",
2629 shortName, eccentricityCB, Options::setAnalyzeEccentricity, eccentricityOut);
2630 shortName = "#Stars";
2631 QCPAxis *numStarsAxis = newStatsYAxis(shortName);
2632 NUMSTARS_GRAPH = initGraphAndCB(statsPlot, numStarsAxis, QCPGraph::lsStepRight, Qt::magenta, "#Stars in Guide Image",
2633 shortName, numStarsCB, Options::setAnalyzeNumStars, numStarsOut);
2634 shortName = "SkyBG";
2635 QCPAxis *skyBgAxis = newStatsYAxis(shortName);
2636 SKYBG_GRAPH = initGraphAndCB(statsPlot, skyBgAxis, QCPGraph::lsStepRight, Qt::darkYellow, "Sky Background Brightness",
2637 shortName, skyBgCB, Options::setAnalyzeSkyBg, skyBgOut);
2638
2639 shortName = "temp";
2640 QCPAxis *temperatureAxis = newStatsYAxis(shortName, -40, 40);
2641 TEMPERATURE_GRAPH = initGraphAndCB(statsPlot, temperatureAxis, QCPGraph::lsLine, Qt::yellow, "Temperature", shortName,
2642 temperatureCB, Options::setAnalyzeTemperature, temperatureOut);
2643 shortName = "focus";
2644 QCPAxis *focusPositionAxis = newStatsYAxis(shortName);
2645 FOCUS_POSITION_GRAPH = initGraphAndCB(statsPlot, focusPositionAxis, QCPGraph::lsStepLeft, Qt::lightGray, "Focus", shortName,
2646 focusPositionCB, Options::setFocusPosition, focusPositionOut);
2647 shortName = "tDist";
2648 QCPAxis *targetDistanceAxis = newStatsYAxis(shortName, 0, 60);
2649 TARGET_DISTANCE_GRAPH = initGraphAndCB(statsPlot, targetDistanceAxis, QCPGraph::lsLine,
2650 QColor(253, 185, 200), // pink
2651 "Distance to Target (arcsec)", shortName, targetDistanceCB, Options::setAnalyzeTargetDistance, targetDistanceOut);
2652 shortName = "SNR";
2653 QCPAxis *snrAxis = newStatsYAxis(shortName, -100, 100);
2654 SNR_GRAPH = initGraphAndCB(statsPlot, snrAxis, QCPGraph::lsLine, Qt::yellow, "Guider SNR", shortName, snrCB,
2655 Options::setAnalyzeSNR, snrOut);
2656 shortName = "RA";
2657 auto raColor = KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError");
2658 RA_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, raColor, "Guider RA Drift", shortName, raCB,
2659 Options::setAnalyzeRA, raOut);
2660 shortName = "DEC";
2661 auto decColor = KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError");
2662 DEC_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, decColor, "Guider DEC Drift", shortName, decCB,
2663 Options::setAnalyzeDEC, decOut);
2664 shortName = "RAp";
2665 auto raPulseColor = KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError");
2666 raPulseColor.setAlpha(75);
2667 QCPAxis *pulseAxis = newStatsYAxis(shortName, -2 * 150, 5 * 150);
2668 RA_PULSE_GRAPH = initGraphAndCB(statsPlot, pulseAxis, QCPGraph::lsLine, raPulseColor, "RA Correction Pulse (ms)", shortName,
2669 raPulseCB, Options::setAnalyzeRAp, raPulseOut);
2670 statsPlot->graph(RA_PULSE_GRAPH)->setBrush(QBrush(raPulseColor, Qt::Dense4Pattern));
2671
2672 shortName = "DECp";
2673 auto decPulseColor = KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError");
2674 decPulseColor.setAlpha(75);
2675 DEC_PULSE_GRAPH = initGraphAndCB(statsPlot, pulseAxis, QCPGraph::lsLine, decPulseColor, "DEC Correction Pulse (ms)",
2676 shortName, decPulseCB, Options::setAnalyzeDECp, decPulseOut);
2677 statsPlot->graph(DEC_PULSE_GRAPH)->setBrush(QBrush(decPulseColor, Qt::Dense4Pattern));
2678
2679 shortName = "Drift";
2680 DRIFT_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, Qt::lightGray, "Guider Instantaneous Drift",
2681 shortName, driftCB, Options::setAnalyzeDrift, driftOut);
2682 shortName = "RMS";
2683 RMS_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, Qt::red, "Guider RMS Drift", shortName, rmsCB,
2684 Options::setAnalyzeRMS, rmsOut);
2685 shortName = "RMSc";
2686 CAPTURE_RMS_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, Qt::red,
2687 "Guider RMS Drift (during capture)", shortName, rmsCCB,
2688 Options::setAnalyzeRMSC, rmsCOut);
2689 shortName = "MOUNT_RA";
2690 QCPAxis *mountRaDecAxis = newStatsYAxis(shortName, -10, 370);
2691 // Colors of these two unimportant--not really plotted.
2692 MOUNT_RA_GRAPH = initGraphAndCB(statsPlot, mountRaDecAxis, QCPGraph::lsLine, Qt::red, "Mount RA Degrees", shortName,
2693 mountRaCB, Options::setAnalyzeMountRA, mountRaOut);
2694 shortName = "MOUNT_DEC";
2695 MOUNT_DEC_GRAPH = initGraphAndCB(statsPlot, mountRaDecAxis, QCPGraph::lsLine, Qt::red, "Mount DEC Degrees", shortName,
2696 mountDecCB, Options::setAnalyzeMountDEC, mountDecOut);
2697 shortName = "MOUNT_HA";
2698 MOUNT_HA_GRAPH = initGraphAndCB(statsPlot, mountRaDecAxis, QCPGraph::lsLine, Qt::red, "Mount Hour Angle", shortName,
2699 mountHaCB, Options::setAnalyzeMountHA, mountHaOut);
2700 shortName = "AZ";
2701 QCPAxis *azAxis = newStatsYAxis(shortName, -10, 370);
2702 AZ_GRAPH = initGraphAndCB(statsPlot, azAxis, QCPGraph::lsLine, Qt::darkGray, "Mount Azimuth", shortName, azCB,
2703 Options::setAnalyzeAz, azOut);
2704 shortName = "ALT";
2705 QCPAxis *altAxis = newStatsYAxis(shortName, 0, 90);
2706 ALT_GRAPH = initGraphAndCB(statsPlot, altAxis, QCPGraph::lsLine, Qt::white, "Mount Altitude", shortName, altCB,
2707 Options::setAnalyzeAlt, altOut);
2708 shortName = "PierSide";
2709 QCPAxis *pierSideAxis = newStatsYAxis(shortName, -2, 2);
2710 PIER_SIDE_GRAPH = initGraphAndCB(statsPlot, pierSideAxis, QCPGraph::lsLine, Qt::darkRed, "Mount Pier Side", shortName,
2711 pierSideCB, Options::setAnalyzePierSide, pierSideOut);
2712
2713 // This makes mouseMove only get called when a button is pressed.
2714 statsPlot->setMouseTracking(false);
2715
2716 // Setup the clock-time labels on the x-axis of the stats plot.
2717 dateTicker.reset(new OffsetDateTimeTicker);
2718 dateTicker->setDateTimeFormat("hh:mm:ss");
2719 statsPlot->xAxis->setTicker(dateTicker);
2720
2721 // Didn't include QCP::iRangeDrag as it interacts poorly with the curson logic.
2722 statsPlot->setInteractions(QCP::iRangeZoom);
2723
2724 restoreYAxes(Options::analyzeStatsYAxis());
2725}
2726
2727// Clear the graphics and state when changing input data.
2728void Analyze::reset()
2729{
2730 maxXValue = 10.0;
2731 plotStart = 0.0;
2732 plotWidth = 10.0;
2733
2734 guiderRms->resetFilter();
2735 captureRms->resetFilter();
2736
2737 unhighlightTimelineItem();
2738
2739 for (int i = 0; i < statsPlot->graphCount(); ++i)
2740 statsPlot->graph(i)->data()->clear();
2741 statsPlot->clearItems();
2742
2743 for (int i = 0; i < timelinePlot->graphCount(); ++i)
2744 timelinePlot->graph(i)->data()->clear();
2745 timelinePlot->clearItems();
2746
2747 resetGraphicsPlot();
2748
2749 detailsTable->clear();
2750 QPalette p = detailsTable->palette();
2753 detailsTable->setPalette(p);
2754
2755 inputValue->clear();
2756
2757 captureSessions.clear();
2758 focusSessions.clear();
2759 guideSessions.clear();
2760 mountSessions.clear();
2761 alignSessions.clear();
2762 mountFlipSessions.clear();
2763 schedulerJobSessions.clear();
2764
2765 numStarsOut->setText("");
2766 skyBgOut->setText("");
2767 snrOut->setText("");
2768 temperatureOut->setText("");
2769 focusPositionOut->setText("");
2770 targetDistanceOut->setText("");
2771 eccentricityOut->setText("");
2772 medianOut->setText("");
2773 numCaptureStarsOut->setText("");
2774
2775 raOut->setText("");
2776 decOut->setText("");
2777 driftOut->setText("");
2778 rmsOut->setText("");
2779 rmsCOut->setText("");
2780
2781 removeStatsCursor();
2782 removeTemporarySessions();
2783
2784 resetCaptureState();
2785 resetAutofocusState();
2786 resetGuideState();
2787 resetGuideStats();
2788 resetAlignState();
2789 resetMountState();
2790 resetMountCoords();
2791 resetMountFlipState();
2792 resetSchedulerJob();
2793
2794 // Note: no replot().
2795}
2796
2797void Analyze::initGraphicsPlot()
2798{
2799 initQCP(graphicsPlot);
2800 FOCUS_GRAPHICS = initGraph(graphicsPlot, graphicsPlot->yAxis,
2801 QCPGraph::lsNone, Qt::cyan, "Focus");
2802 graphicsPlot->graph(FOCUS_GRAPHICS)->setScatterStyle(
2804 errorBars = new QCPErrorBars(graphicsPlot->xAxis, graphicsPlot->yAxis);
2805 errorBars->setAntialiased(false);
2806 errorBars->setDataPlottable(graphicsPlot->graph(FOCUS_GRAPHICS));
2807 errorBars->setPen(QPen(QColor(180, 180, 180)));
2808
2809 FOCUS_GRAPHICS_FINAL = initGraph(graphicsPlot, graphicsPlot->yAxis,
2810 QCPGraph::lsNone, Qt::cyan, "FocusBest");
2811 graphicsPlot->graph(FOCUS_GRAPHICS_FINAL)->setScatterStyle(
2813 finalErrorBars = new QCPErrorBars(graphicsPlot->xAxis, graphicsPlot->yAxis);
2814 finalErrorBars->setAntialiased(false);
2815 finalErrorBars->setDataPlottable(graphicsPlot->graph(FOCUS_GRAPHICS_FINAL));
2816 finalErrorBars->setPen(QPen(QColor(180, 180, 180)));
2817
2818 FOCUS_GRAPHICS_CURVE = initGraph(graphicsPlot, graphicsPlot->yAxis,
2819 QCPGraph::lsLine, Qt::white, "FocusCurve");
2820 graphicsPlot->setInteractions(QCP::iRangeZoom);
2821 graphicsPlot->setInteraction(QCP::iRangeDrag, true);
2822
2823 GUIDER_GRAPHICS = initGraph(graphicsPlot, graphicsPlot->yAxis,
2824 QCPGraph::lsNone, Qt::cyan, "Guide Error");
2825 graphicsPlot->graph(GUIDER_GRAPHICS)->setScatterStyle(
2827}
2828
2829void Analyze::displayFocusGraphics(const QVector<double> &positions, const QVector<double> &hfrs, const bool useWeights,
2830 const QVector<double> &weights, const QVector<bool> &outliers, const QString &curve, const QString &title, bool success)
2831{
2832 resetGraphicsPlot();
2833 auto graph = graphicsPlot->graph(FOCUS_GRAPHICS);
2834 auto finalGraph = graphicsPlot->graph(FOCUS_GRAPHICS_FINAL);
2835 double maxHfr = -1e8, maxPosition = -1e8, minHfr = 1e8, minPosition = 1e8;
2836 QVector<double> errorData, finalErrorData;
2837 for (int i = 0; i < positions.size(); ++i)
2838 {
2839 // Yellow circle for the final point.
2840 if (success && i == positions.size() - 1)
2841 {
2842 finalGraph->addData(positions[i], hfrs[i]);
2843 if (useWeights)
2844 {
2845 // Display the error bars in Standard Deviation form = 1 / sqrt(weight)
2846 double sd = (weights[i] <= 0.0) ? 0.0 : std::pow(weights[i], -0.5);
2847 finalErrorData.push_front(sd);
2848 }
2849 }
2850 else
2851 {
2852 graph->addData(positions[i], hfrs[i]);
2853 if (useWeights)
2854 {
2855 double sd = (weights[i] <= 0.0) ? 0.0 : std::pow(weights[i], -0.5);
2856 errorData.push_front(sd);
2857 }
2858 }
2859 maxHfr = std::max(maxHfr, hfrs[i]);
2860 minHfr = std::min(minHfr, hfrs[i]);
2861 maxPosition = std::max(maxPosition, positions[i]);
2862 minPosition = std::min(minPosition, positions[i]);
2863 }
2864
2865 for (int i = 0; i < positions.size(); ++i)
2866 {
2867 QCPItemText *textLabel = new QCPItemText(graphicsPlot);
2869 textLabel->position->setType(QCPItemPosition::ptPlotCoords);
2870 textLabel->position->setCoords(positions[i], hfrs[i]);
2871 if (outliers[i])
2872 {
2873 textLabel->setText("X");
2874 textLabel->setColor(Qt::black);
2875 textLabel->setFont(QFont(font().family(), 20));
2876 }
2877 else
2878 {
2879 textLabel->setText(QString::number(i + 1));
2880 textLabel->setColor(Qt::red);
2881 textLabel->setFont(QFont(font().family(), 12));
2882 }
2883 textLabel->setPen(Qt::NoPen);
2884 }
2885
2886 // Error bars on the focus datapoints
2887 errorBars->setVisible(useWeights);
2888 finalErrorBars->setVisible(useWeights);
2889 if (useWeights)
2890 {
2891 errorBars->setData(errorData);
2892 finalErrorBars->setData(finalErrorData);
2893 }
2894
2895 const double xRange = maxPosition - minPosition;
2896 const double xPadding = hfrs.size() > 1 ? xRange / (hfrs.size() - 1.0) : 10;
2897
2898 // Draw the curve, if given.
2899 if (curve.size() > 0)
2900 {
2901 CurveFitting curveFitting(curve);
2902 const double interval = xRange / 20.0;
2903 auto curveGraph = graphicsPlot->graph(FOCUS_GRAPHICS_CURVE);
2904 for (double x = minPosition - xPadding ; x <= maxPosition + xPadding; x += interval)
2905 curveGraph->addData(x, curveFitting.f(x));
2906 }
2907
2908 auto plotTitle = new QCPItemText(graphicsPlot);
2909 plotTitle->setColor(QColor(255, 255, 255));
2910 plotTitle->setPositionAlignment(Qt::AlignTop | Qt::AlignHCenter);
2911 plotTitle->position->setType(QCPItemPosition::ptAxisRectRatio);
2912 plotTitle->position->setCoords(0.5, 0);
2913 plotTitle->setFont(QFont(font().family(), 10));
2914 plotTitle->setVisible(true);
2915 plotTitle->setText(title);
2916
2917 // Set the same axes ranges as are used in focushfrvplot.cpp.
2918 const double upper = 1.5 * maxHfr;
2919 const double lower = minHfr - (0.25 * (upper - minHfr));
2920 graphicsPlot->xAxis->setRange(minPosition - xPadding, maxPosition + xPadding);
2921 graphicsPlot->yAxis->setRange(lower, upper);
2922 graphicsPlot->replot();
2923}
2924
2925void Analyze::resetGraphicsPlot()
2926{
2927 for (int i = 0; i < graphicsPlot->graphCount(); ++i)
2928 graphicsPlot->graph(i)->data()->clear();
2929 graphicsPlot->clearItems();
2930 errorBars->data().clear();
2931 finalErrorBars->data().clear();
2932}
2933
2934void Analyze::displayFITS(const QString &filename)
2935{
2936 QUrl url = QUrl::fromLocalFile(filename);
2937
2938 if (fitsViewer.isNull())
2939 {
2940 fitsViewer = KStars::Instance()->createFITSViewer();
2941 fitsViewerTabID = fitsViewer->loadFile(url);
2942 connect(fitsViewer.get(), &FITSViewer::terminated, this, [this]()
2943 {
2944 fitsViewer.clear();
2945 });
2946 }
2947 else
2948 {
2949 if (fitsViewer->tabExists(fitsViewerTabID))
2950 fitsViewer->updateFile(url, fitsViewerTabID);
2951 else
2952 fitsViewerTabID = fitsViewer->loadFile(url);
2953 }
2954
2955 fitsViewer->show();
2956}
2957
2958// This is intended for recording data to file.
2959// Don't use this when displaying data read from file, as this is not using the
2960// correct analyzeStartTime.
2961double Analyze::logTime(const QDateTime &time)
2962{
2963 if (!logInitialized)
2964 startLog();
2965 return (time.toMSecsSinceEpoch() - analyzeStartTime.toMSecsSinceEpoch()) / 1000.0;
2966}
2967
2968// The logTime using clock = now.
2969// This is intended for recording data to file.
2970// Don't use this When displaying data read from file.
2971double Analyze::logTime()
2972{
2973 return logTime(QDateTime::currentDateTime());
2974}
2975
2976// Goes back to clock time from seconds into the log.
2977// Appropriate for both displaying data from files as well as when displaying live data.
2978QDateTime Analyze::clockTime(double logSeconds)
2979{
2980 return displayStartTime.addMSecs(logSeconds * 1000.0);
2981}
2982
2983
2984// Write the command name, a timestamp and the message with comma separation to a .analyze file.
2985void Analyze::saveMessage(const QString &type, const QString &message)
2986{
2987 QString line(QString("%1,%2%3%4\n")
2988 .arg(type)
2989 .arg(QString::number(logTime(), 'f', 3))
2990 .arg(message.size() > 0 ? "," : "", message));
2991 appendToLog(line);
2992}
2993
2994// Start writing a new .analyze file and reset the graphics to start from "now".
2995void Analyze::restart()
2996{
2997 qCDebug(KSTARS_EKOS_ANALYZE) << "(Re)starting Analyze";
2998
2999 // Setup the new .analyze file
3000 startLog();
3001
3002 // Reset the graphics so that it ignore any old data.
3003 reset();
3004 inputCombo->setCurrentIndex(0);
3005 inputValue->setText("");
3006 maxXValue = readDataFromFile(logFilename);
3007 runtimeDisplay = true;
3008 fullWidthCB->setChecked(true);
3009 fullWidthCB->setVisible(true);
3010 fullWidthCB->setDisabled(false);
3011 replot();
3012}
3013
3014// Start writing a .analyze file.
3015void Analyze::startLog()
3016{
3017 analyzeStartTime = QDateTime::currentDateTime();
3018 startTimeInitialized = true;
3019 if (runtimeDisplay)
3020 displayStartTime = analyzeStartTime;
3021
3022 QDir dir = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/analyze");
3023 dir.mkpath(".");
3024
3025 logFile.reset(new QFile);
3026 logFilename = dir.filePath("ekos-" + QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss") + ".analyze");
3027 logFile->setFileName(logFilename);
3028 logFile->open(QIODevice::WriteOnly | QIODevice::Text);
3029
3030 // This must happen before the below appendToLog() call.
3031 logInitialized = true;
3032
3033 appendToLog(QString("#KStars version %1. Analyze log version 1.0.\n\n")
3034 .arg(KSTARS_VERSION));
3035 appendToLog(QString("%1,%2,%3\n")
3036 .arg("AnalyzeStartTime", analyzeStartTime.toString(timeFormat), analyzeStartTime.timeZoneAbbreviation()));
3037}
3038
3039void Analyze::appendToLog(const QString &lines)
3040{
3041 if (!logInitialized)
3042 startLog();
3043 QTextStream out(logFile.data());
3044 out << lines;
3045 out.flush();
3046}
3047
3048// maxXValue is the largest time value we have seen so far for this data.
3049void Analyze::updateMaxX(double time)
3050{
3051 maxXValue = std::max(time, maxXValue);
3052}
3053
3054// Manage temporary sessions displayed on the Timeline.
3055// Those are ongoing sessions that will ultimately be replaced when the session is complete.
3056// This only happens with live data, not with data read from .analyze files.
3057
3058// Remove the graphic element.
3059void Analyze::removeTemporarySession(Session * session)
3060{
3061 if (session->rect != nullptr)
3062 timelinePlot->removeItem(session->rect);
3063 session->rect = nullptr;
3064 session->start = 0;
3065 session->end = 0;
3066}
3067
3068// Remove all temporary sessions (i.e. from all lines in the Timeline).
3069void Analyze::removeTemporarySessions()
3070{
3071 removeTemporarySession(&temporaryCaptureSession);
3072 removeTemporarySession(&temporaryMountFlipSession);
3073 removeTemporarySession(&temporaryFocusSession);
3074 removeTemporarySession(&temporaryGuideSession);
3075 removeTemporarySession(&temporaryMountSession);
3076 removeTemporarySession(&temporaryAlignSession);
3077 removeTemporarySession(&temporarySchedulerJobSession);
3078}
3079
3080// Add a new temporary session.
3081void Analyze::addTemporarySession(Session * session, double time, double duration,
3082 int y_offset, const QBrush &brush)
3083{
3084 if (time < 0) return;
3085 removeTemporarySession(session);
3086 session->rect = addSession(time, time + duration, y_offset, brush);
3087 session->start = time;
3088 session->end = time + duration;
3089 session->offset = y_offset;
3090 session->temporaryBrush = brush;
3091 updateMaxX(time + duration);
3092}
3093
3094// Extend a temporary session. That is, we don't know how long the session will last,
3095// so when new data arrives (from any module, not necessarily the one with the temporary
3096// session) we must extend that temporary session.
3097void Analyze::adjustTemporarySession(Session * session)
3098{
3099 if (session->rect != nullptr && session->end < maxXValue)
3100 {
3101 QBrush brush = session->temporaryBrush;
3102 double start = session->start;
3103 int offset = session->offset;
3104 addTemporarySession(session, start, maxXValue - start, offset, brush);
3105 }
3106}
3107
3108// Extend all temporary sessions.
3109void Analyze::adjustTemporarySessions()
3110{
3111 adjustTemporarySession(&temporaryCaptureSession);
3112 adjustTemporarySession(&temporaryMountFlipSession);
3113 adjustTemporarySession(&temporaryFocusSession);
3114 adjustTemporarySession(&temporaryGuideSession);
3115 adjustTemporarySession(&temporaryMountSession);
3116 adjustTemporarySession(&temporaryAlignSession);
3117 adjustTemporarySession(&temporarySchedulerJobSession);
3118}
3119
3120// Called when the captureStarting slot receives a signal.
3121// Saves the message to disk, and calls processCaptureStarting.
3122void Analyze::captureStarting(double exposureSeconds, const QString &filter)
3123{
3124 saveMessage("CaptureStarting",
3125 QString("%1,%2").arg(QString::number(exposureSeconds, 'f', 3), filter));
3126 processCaptureStarting(logTime(), exposureSeconds, filter);
3127}
3128
3129// Called by either the above (when live data is received), or reading from file.
3130// BatchMode would be true when reading from file.
3131void Analyze::processCaptureStarting(double time, double exposureSeconds, const QString &filter)
3132{
3133 captureStartedTime = time;
3134 captureStartedFilter = filter;
3135 updateMaxX(time);
3136
3137 addTemporarySession(&temporaryCaptureSession, time, 1, CAPTURE_Y, temporaryBrush);
3138 temporaryCaptureSession.duration = exposureSeconds;
3139 temporaryCaptureSession.filter = filter;
3140}
3141
3142// Called when the captureComplete slot receives a signal.
3143void Analyze::captureComplete(const QVariantMap &metadata)
3144{
3145 auto filename = metadata["filename"].toString();
3146 auto exposure = metadata["exposure"].toDouble();
3147 auto filter = metadata["filter"].toString();
3148 auto hfr = metadata["hfr"].toDouble();
3149 auto starCount = metadata["starCount"].toInt();
3150 auto median = metadata["median"].toDouble();
3151 auto eccentricity = metadata["eccentricity"].toDouble();
3152
3153 saveMessage("CaptureComplete",
3154 QString("%1,%2,%3,%4,%5,%6,%7")
3155 .arg(QString::number(exposure, 'f', 3), filter, QString::number(hfr, 'f', 3), filename)
3156 .arg(starCount)
3157 .arg(median)
3158 .arg(QString::number(eccentricity, 'f', 3)));
3159 if (runtimeDisplay && captureStartedTime >= 0)
3160 processCaptureComplete(logTime(), filename, exposure, filter, hfr, starCount, median, eccentricity);
3161}
3162
3163void Analyze::processCaptureComplete(double time, const QString &filename,
3164 double exposureSeconds, const QString &filter, double hfr,
3165 int numStars, int median, double eccentricity, bool batchMode)
3166{
3167 removeTemporarySession(&temporaryCaptureSession);
3168 QBrush stripe;
3169 if (captureStartedTime < 0)
3170 return;
3171
3172 if (filterStripeBrush(filter, &stripe))
3173 addSession(captureStartedTime, time, CAPTURE_Y, successBrush, &stripe);
3174 else
3175 addSession(captureStartedTime, time, CAPTURE_Y, successBrush, nullptr);
3176 auto session = CaptureSession(captureStartedTime, time, nullptr, false,
3177 filename, exposureSeconds, filter);
3178 captureSessions.add(session);
3179 addHFR(hfr, numStars, median, eccentricity, time, captureStartedTime);
3180 updateMaxX(time);
3181 if (!batchMode)
3182 {
3183 if (runtimeDisplay && keepCurrentCB->isChecked() && statsCursor == nullptr)
3184 captureSessionClicked(session, false);
3185 replot();
3186 }
3187 previousCaptureStartedTime = captureStartedTime;
3188 previousCaptureCompletedTime = time;
3189 captureStartedTime = -1;
3190}
3191
3192void Analyze::captureAborted(double exposureSeconds)
3193{
3194 saveMessage("CaptureAborted",
3195 QString("%1").arg(QString::number(exposureSeconds, 'f', 3)));
3196 if (runtimeDisplay && captureStartedTime >= 0)
3197 processCaptureAborted(logTime(), exposureSeconds);
3198}
3199
3200void Analyze::processCaptureAborted(double time, double exposureSeconds, bool batchMode)
3201{
3202 removeTemporarySession(&temporaryCaptureSession);
3203 double duration = time - captureStartedTime;
3204 if (captureStartedTime >= 0 &&
3205 duration < (exposureSeconds + 30) &&
3206 duration < 3600)
3207 {
3208 // You can get a captureAborted without a captureStarting,
3209 // so make sure this associates with a real start.
3210 addSession(captureStartedTime, time, CAPTURE_Y, failureBrush);
3211 auto session = CaptureSession(captureStartedTime, time, nullptr, true, "",
3212 exposureSeconds, captureStartedFilter);
3213 captureSessions.add(session);
3214 updateMaxX(time);
3215 if (!batchMode)
3216 {
3217 if (runtimeDisplay && keepCurrentCB->isChecked() && statsCursor == nullptr)
3218 captureSessionClicked(session, false);
3219 replot();
3220 }
3221 captureStartedTime = -1;
3222 }
3223 previousCaptureStartedTime = -1;
3224 previousCaptureCompletedTime = -1;
3225}
3226
3227void Analyze::resetCaptureState()
3228{
3229 captureStartedTime = -1;
3230 captureStartedFilter = "";
3231 medianMax = 1;
3232 numCaptureStarsMax = 1;
3233 previousCaptureStartedTime = -1;
3234 previousCaptureCompletedTime = -1;
3235}
3236
3237void Analyze::autofocusStarting(double temperature, const QString &filter, const AutofocusReason reason,
3238 const QString &reasonInfo)
3239{
3240 saveMessage("AutofocusStarting",
3241 QString("%1,%2,%3,%4")
3242 .arg(filter)
3243 .arg(QString::number(temperature, 'f', 1))
3244 .arg(QString::number(reason))
3245 .arg(reasonInfo));
3246 processAutofocusStarting(logTime(), temperature, filter, reason, reasonInfo);
3247}
3248
3249void Analyze::processAutofocusStarting(double time, double temperature, const QString &filter, const AutofocusReason reason,
3250 const QString &reasonInfo)
3251{
3252 autofocusStartedTime = time;
3253 autofocusStartedFilter = filter;
3254 autofocusStartedTemperature = temperature;
3255 autofocusStartedReason = reason;
3256 autofocusStartedReasonInfo = reasonInfo;
3257
3258 addTemperature(temperature, time);
3259 updateMaxX(time);
3260
3261 addTemporarySession(&temporaryFocusSession, time, 1, FOCUS_Y, temporaryBrush);
3262 temporaryFocusSession.temperature = temperature;
3263 temporaryFocusSession.filter = filter;
3264 temporaryFocusSession.reason = reason;
3265}
3266
3267void Analyze::adaptiveFocusComplete(const QString &filter, double temperature, double tempTicks,
3268 double altitude, double altTicks, int prevPosError, int thisPosError,
3269 int totalTicks, int position, bool focuserMoved)
3270{
3271 saveMessage("AdaptiveFocusComplete", QString("%1,%2,%3,%4,%5,%6,%7,%8,%9,%10").arg(filter).arg(temperature, 0, 'f', 2)
3272 .arg(tempTicks, 0, 'f', 2).arg(altitude, 0, 'f', 2).arg(altTicks, 0, 'f', 2).arg(prevPosError)
3273 .arg(thisPosError).arg(totalTicks).arg(position).arg(focuserMoved ? 1 : 0));
3274
3275 if (runtimeDisplay)
3276 processAdaptiveFocusComplete(logTime(), filter, temperature, tempTicks, altitude, altTicks, prevPosError, thisPosError,
3277 totalTicks, position, focuserMoved);
3278}
3279
3280void Analyze::processAdaptiveFocusComplete(double time, const QString &filter, double temperature, double tempTicks,
3281 double altitude, double altTicks, int prevPosError, int thisPosError, int totalTicks, int position,
3282 bool focuserMoved, bool batchMode)
3283{
3284 removeTemporarySession(&temporaryFocusSession);
3285
3286 addFocusPosition(position, time);
3287 updateMaxX(time);
3288
3289 // In general if nothing happened we won't plot a value. This means there won't be lots of points with zeros in them.
3290 // However, we need to cover the situation of offsetting movements that overall don't move the focuser but still have non-zero detail
3291 if (!focuserMoved || (abs(tempTicks) < 1.00 && abs(altTicks) < 1.0 && prevPosError == 0 && thisPosError == 0))
3292 return;
3293
3294 // Add a dot on the timeline.
3295 timelinePlot->graph(ADAPTIVE_FOCUS_GRAPH)->addData(time, FOCUS_Y);
3296
3297 // Add mouse sensitivity on the timeline.
3298 constexpr int artificialInterval = 10;
3299 auto session = FocusSession(time - artificialInterval, time + artificialInterval, nullptr,
3300 filter, temperature, tempTicks, altitude, altTicks, prevPosError, thisPosError, totalTicks,
3301 position);
3302 focusSessions.add(session);
3303
3304 if (!batchMode)
3305 replot();
3306
3307 autofocusStartedTime = -1;
3308}
3309
3310void Analyze::autofocusComplete(const double temperature, const QString &filter, const QString &points,
3311 const bool useWeights, const QString &curve, const QString &rawTitle)
3312{
3313 // Remove commas from the title as they're used as separators in the .analyze file.
3314 QString title = rawTitle;
3315 title.replace(",", " ");
3316
3317 // Version 1 message structure is now deprecated, leaving code commented out in case old files need debugging
3318 /*if (curve.size() == 0)
3319 saveMessage("AutofocusComplete", QString("%1,%2").arg(filter, points));
3320 else if (title.size() == 0)
3321 saveMessage("AutofocusComplete", QString("%1,%2,%3").arg(filter, points, curve));
3322 else
3323 saveMessage("AutofocusComplete", QString("%1,%2,%3,%4").arg(filter, points, curve, title));*/
3324
3325 QString temp = QString::number(temperature, 'f', 1);
3326 QVariant reasonV = autofocusStartedReason;
3327 QString reason = reasonV.toString();
3328 QString reasonInfo = autofocusStartedReasonInfo;
3329 QString weights = QString::number(useWeights);
3330 if (curve.size() == 0)
3331 saveMessage("AutofocusComplete", QString("%1,%2,%3,%4,%5,%6").arg(temp, reason, reasonInfo, filter, points, weights));
3332 else if (title.size() == 0)
3333 saveMessage("AutofocusComplete", QString("%1,%2,%3,%4,%5,%6,%7").arg(temp, reason, reasonInfo, filter, points, weights,
3334 curve));
3335 else
3336 saveMessage("AutofocusComplete", QString("%1,%2,%3,%4,%5,%6,%7,%8").arg(temp, reason, reasonInfo, filter, points, weights,
3337 curve, title));
3338
3339 if (runtimeDisplay && autofocusStartedTime >= 0)
3340 processAutofocusCompleteV2(logTime(), temperature, filter, autofocusStartedReason, reasonInfo, points, useWeights, curve,
3341 title);
3342}
3343
3344// Version 2 of processAutofocusComplete to process weights, outliers and reason codes.
3345void Analyze::processAutofocusCompleteV2(double time, const double temperature, const QString &filter,
3346 const AutofocusReason reason, const QString &reasonInfo,
3347 const QString &points, const bool useWeights, const QString &curve, const QString &title, bool batchMode)
3348{
3349 removeTemporarySession(&temporaryFocusSession);
3350 updateMaxX(time);
3351 if (autofocusStartedTime >= 0)
3352 {
3353 QBrush stripe;
3354 if (filterStripeBrush(filter, &stripe))
3355 addSession(autofocusStartedTime, time, FOCUS_Y, successBrush, &stripe);
3356 else
3357 addSession(autofocusStartedTime, time, FOCUS_Y, successBrush, nullptr);
3358 // Use the focus complete temperature (rather than focus start temperature) for consistency with Focus
3359 auto session = FocusSession(autofocusStartedTime, time, nullptr, true, temperature, filter, reason, reasonInfo, points,
3360 useWeights, curve, title, AutofocusFailReason::FOCUS_FAIL_NONE, "");
3361 focusSessions.add(session);
3362 addFocusPosition(session.focusPosition(), autofocusStartedTime);
3363 if (!batchMode)
3364 {
3365 if (runtimeDisplay && keepCurrentCB->isChecked() && statsCursor == nullptr)
3366 focusSessionClicked(session, false);
3367 replot();
3368 }
3369 }
3370 autofocusStartedTime = -1;
3371}
3372
3373// Older version of processAutofocusComplete to process analyze files created before version 2.
3374void Analyze::processAutofocusComplete(double time, const QString &filter, const QString &points,
3375 const QString &curve, const QString &title, bool batchMode)
3376{
3377 removeTemporarySession(&temporaryFocusSession);
3378 if (autofocusStartedTime < 0)
3379 return;
3380
3381 QBrush stripe;
3382 if (filterStripeBrush(filter, &stripe))
3383 addSession(autofocusStartedTime, time, FOCUS_Y, successBrush, &stripe);
3384 else
3385 addSession(autofocusStartedTime, time, FOCUS_Y, successBrush, nullptr);
3386 auto session = FocusSession(autofocusStartedTime, time, nullptr, true,
3387 autofocusStartedTemperature, filter, points, curve, title);
3388 focusSessions.add(session);
3389 addFocusPosition(session.focusPosition(), autofocusStartedTime);
3390 updateMaxX(time);
3391 if (!batchMode)
3392 {
3393 if (runtimeDisplay && keepCurrentCB->isChecked() && statsCursor == nullptr)
3394 focusSessionClicked(session, false);
3395 replot();
3396 }
3397 autofocusStartedTime = -1;
3398}
3399
3400void Analyze::autofocusAborted(const QString &filter, const QString &points, const bool useWeights,
3401 const AutofocusFailReason failCode, const QString failCodeInfo)
3402{
3403 QString temperature = QString::number(autofocusStartedTemperature, 'f', 1);
3404 QVariant reasonV = autofocusStartedReason;
3405 QString reason = reasonV.toString();
3406 QString reasonInfo = autofocusStartedReasonInfo;
3407 QString weights = QString::number(useWeights);
3408 QVariant failReasonV = static_cast<int>(failCode);
3409 QString failReason = failReasonV.toString();
3410 saveMessage("AutofocusAborted", QString("%1,%2,%3,%4,%5,%6,%7,%8").arg(temperature, reason, reasonInfo, filter, points,
3411 weights, failReason, failCodeInfo));
3412 if (runtimeDisplay && autofocusStartedTime >= 0)
3413 processAutofocusAbortedV2(logTime(), autofocusStartedTemperature, filter, autofocusStartedReason, reasonInfo, points,
3414 useWeights, failCode, failCodeInfo);
3415}
3416
3417// Version 2 of processAutofocusAborted added weights, outliers and reason codes.
3418void Analyze::processAutofocusAbortedV2(double time, double temperature, const QString &filter,
3419 const AutofocusReason reason, const QString &reasonInfo, const QString &points, const bool useWeights,
3420 const AutofocusFailReason failCode, const QString failCodeInfo, bool batchMode)
3421{
3422 Q_UNUSED(temperature);
3423 removeTemporarySession(&temporaryFocusSession);
3424 double duration = time - autofocusStartedTime;
3425 if (autofocusStartedTime >= 0 && duration < 1000)
3426 {
3427 // Just in case..
3428 addSession(autofocusStartedTime, time, FOCUS_Y, failureBrush);
3429 auto session = FocusSession(autofocusStartedTime, time, nullptr, false, autofocusStartedTemperature, filter, reason,
3430 reasonInfo, points, useWeights, "", "", failCode, failCodeInfo);
3431 focusSessions.add(session);
3432 updateMaxX(time);
3433 if (!batchMode)
3434 {
3435 if (runtimeDisplay && keepCurrentCB->isChecked() && statsCursor == nullptr)
3436 focusSessionClicked(session, false);
3437 replot();
3438 }
3439 autofocusStartedTime = -1;
3440 }
3441}
3442
3443// Older version processAutofocusAborted to support processing analyze files created before V2
3444void Analyze::processAutofocusAborted(double time, const QString &filter, const QString &points, bool batchMode)
3445{
3446 removeTemporarySession(&temporaryFocusSession);
3447 double duration = time - autofocusStartedTime;
3448 if (autofocusStartedTime >= 0 && duration < 1000)
3449 {
3450 // Just in case..
3451 addSession(autofocusStartedTime, time, FOCUS_Y, failureBrush);
3452 auto session = FocusSession(autofocusStartedTime, time, nullptr, false,
3453 autofocusStartedTemperature, filter, points, "", "");
3454 focusSessions.add(session);
3455 updateMaxX(time);
3456 if (!batchMode)
3457 {
3458 if (runtimeDisplay && keepCurrentCB->isChecked() && statsCursor == nullptr)
3459 focusSessionClicked(session, false);
3460 replot();
3461 }
3462 autofocusStartedTime = -1;
3463 }
3464}
3465
3466void Analyze::resetAutofocusState()
3467{
3468 autofocusStartedTime = -1;
3469 autofocusStartedFilter = "";
3470 autofocusStartedTemperature = 0;
3471 autofocusStartedReason = AutofocusReason::FOCUS_NONE;
3472 autofocusStartedReasonInfo = "";
3473}
3474
3475namespace
3476{
3477
3478// TODO: move to ekos.h/cpp?
3479Ekos::GuideState stringToGuideState(const QString &str)
3480{
3481 if (str == i18n("Idle"))
3482 return GUIDE_IDLE;
3483 else if (str == i18n("Aborted"))
3484 return GUIDE_ABORTED;
3485 else if (str == i18n("Connected"))
3486 return GUIDE_CONNECTED;
3487 else if (str == i18n("Disconnected"))
3488 return GUIDE_DISCONNECTED;
3489 else if (str == i18n("Capturing"))
3490 return GUIDE_CAPTURE;
3491 else if (str == i18n("Looping"))
3492 return GUIDE_LOOPING;
3493 else if (str == i18n("Subtracting"))
3494 return GUIDE_DARK;
3495 else if (str == i18n("Subframing"))
3496 return GUIDE_SUBFRAME;
3497 else if (str == i18n("Selecting star"))
3498 return GUIDE_STAR_SELECT;
3499 else if (str == i18n("Calibrating"))
3500 return GUIDE_CALIBRATING;
3501 else if (str == i18n("Calibration error"))
3502 return GUIDE_CALIBRATION_ERROR;
3503 else if (str == i18n("Calibrated"))
3504 return GUIDE_CALIBRATION_SUCCESS;
3505 else if (str == i18n("Guiding"))
3506 return GUIDE_GUIDING;
3507 else if (str == i18n("Suspended"))
3508 return GUIDE_SUSPENDED;
3509 else if (str == i18n("Reacquiring"))
3510 return GUIDE_REACQUIRE;
3511 else if (str == i18n("Dithering"))
3512 return GUIDE_DITHERING;
3513 else if (str == i18n("Manual Dithering"))
3514 return GUIDE_MANUAL_DITHERING;
3515 else if (str == i18n("Dithering error"))
3516 return GUIDE_DITHERING_ERROR;
3517 else if (str == i18n("Dithering successful"))
3518 return GUIDE_DITHERING_SUCCESS;
3519 else if (str == i18n("Settling"))
3520 return GUIDE_DITHERING_SETTLE;
3521 else
3522 return GUIDE_IDLE;
3523}
3524
3525Analyze::SimpleGuideState convertGuideState(Ekos::GuideState state)
3526{
3527 switch (state)
3528 {
3529 case GUIDE_IDLE:
3530 case GUIDE_ABORTED:
3531 case GUIDE_CONNECTED:
3532 case GUIDE_DISCONNECTED:
3533 case GUIDE_LOOPING:
3534 return Analyze::G_IDLE;
3535 case GUIDE_GUIDING:
3536 return Analyze::G_GUIDING;
3537 case GUIDE_CAPTURE:
3538 case GUIDE_DARK:
3539 case GUIDE_SUBFRAME:
3540 case GUIDE_STAR_SELECT:
3541 return Analyze::G_IGNORE;
3542 case GUIDE_CALIBRATING:
3543 case GUIDE_CALIBRATION_ERROR:
3544 case GUIDE_CALIBRATION_SUCCESS:
3545 return Analyze::G_CALIBRATING;
3546 case GUIDE_SUSPENDED:
3547 case GUIDE_REACQUIRE:
3548 return Analyze::G_SUSPENDED;
3549 case GUIDE_DITHERING:
3550 case GUIDE_MANUAL_DITHERING:
3551 case GUIDE_DITHERING_ERROR:
3552 case GUIDE_DITHERING_SUCCESS:
3553 case GUIDE_DITHERING_SETTLE:
3554 return Analyze::G_DITHERING;
3555 }
3556 // Shouldn't get here--would get compile error, I believe with a missing case.
3557 return Analyze::G_IDLE;
3558}
3559
3560const QBrush guideBrush(Analyze::SimpleGuideState simpleState)
3561{
3562 switch (simpleState)
3563 {
3564 case Analyze::G_IDLE:
3565 case Analyze::G_IGNORE:
3566 // don't actually render these, so don't care.
3567 return offBrush;
3568 case Analyze::G_GUIDING:
3569 return successBrush;
3570 case Analyze::G_CALIBRATING:
3571 return progressBrush;
3572 case Analyze::G_SUSPENDED:
3573 return stoppedBrush;
3574 case Analyze::G_DITHERING:
3575 return progress2Brush;
3576 }
3577 // Shouldn't get here.
3578 return offBrush;
3579}
3580
3581} // namespace
3582
3583void Analyze::guideState(Ekos::GuideState state)
3584{
3585 QString str = getGuideStatusString(state);
3586 saveMessage("GuideState", str);
3587 if (runtimeDisplay)
3588 processGuideState(logTime(), str);
3589}
3590
3591void Analyze::processGuideState(double time, const QString &stateStr, bool batchMode)
3592{
3593 Ekos::GuideState gstate = stringToGuideState(stateStr);
3594 SimpleGuideState state = convertGuideState(gstate);
3595 if (state == G_IGNORE)
3596 return;
3597 if (state == lastGuideStateStarted)
3598 return;
3599 // End the previous guide session and start the new one.
3600 if (guideStateStartedTime >= 0)
3601 {
3602 if (lastGuideStateStarted != G_IDLE)
3603 {
3604 // Don't render the idle guiding
3605 addSession(guideStateStartedTime, time, GUIDE_Y, guideBrush(lastGuideStateStarted));
3606 guideSessions.add(GuideSession(guideStateStartedTime, time, nullptr, lastGuideStateStarted));
3607 }
3608 }
3609 if (state == G_GUIDING)
3610 {
3611 addTemporarySession(&temporaryGuideSession, time, 1, GUIDE_Y, successBrush);
3612 temporaryGuideSession.simpleState = state;
3613 }
3614 else
3615 removeTemporarySession(&temporaryGuideSession);
3616
3617 guideStateStartedTime = time;
3618 lastGuideStateStarted = state;
3619 updateMaxX(time);
3620 if (!batchMode)
3621 replot();
3622}
3623
3624void Analyze::resetGuideState()
3625{
3626 lastGuideStateStarted = G_IDLE;
3627 guideStateStartedTime = -1;
3628}
3629
3630void Analyze::newTemperature(double temperatureDelta, double temperature)
3631{
3632 Q_UNUSED(temperatureDelta);
3633 if (temperature > -200 && temperature != lastTemperature)
3634 {
3635 saveMessage("Temperature", QString("%1").arg(QString::number(temperature, 'f', 3)));
3636 lastTemperature = temperature;
3637 if (runtimeDisplay)
3638 processTemperature(logTime(), temperature);
3639 }
3640}
3641
3642void Analyze::processTemperature(double time, double temperature, bool batchMode)
3643{
3644 addTemperature(temperature, time);
3645 updateMaxX(time);
3646 if (!batchMode)
3647 replot();
3648}
3649
3650void Analyze::resetTemperature()
3651{
3652 lastTemperature = -1000;
3653}
3654
3655void Analyze::newTargetDistance(double targetDistance)
3656{
3657 saveMessage("TargetDistance", QString("%1").arg(QString::number(targetDistance, 'f', 0)));
3658 if (runtimeDisplay)
3659 processTargetDistance(logTime(), targetDistance);
3660}
3661
3662void Analyze::processTargetDistance(double time, double targetDistance, bool batchMode)
3663{
3664 addTargetDistance(targetDistance, time);
3665 updateMaxX(time);
3666 if (!batchMode)
3667 replot();
3668}
3669
3670void Analyze::guideStats(double raError, double decError, int raPulse, int decPulse,
3671 double snr, double skyBg, int numStars)
3672{
3673 saveMessage("GuideStats", QString("%1,%2,%3,%4,%5,%6,%7")
3674 .arg(QString::number(raError, 'f', 3), QString::number(decError, 'f', 3))
3675 .arg(raPulse)
3676 .arg(decPulse)
3677 .arg(QString::number(snr, 'f', 3), QString::number(skyBg, 'f', 3))
3678 .arg(numStars));
3679
3680 if (runtimeDisplay)
3681 processGuideStats(logTime(), raError, decError, raPulse, decPulse, snr, skyBg, numStars);
3682}
3683
3684void Analyze::processGuideStats(double time, double raError, double decError,
3685 int raPulse, int decPulse, double snr, double skyBg, int numStars, bool batchMode)
3686{
3687 addGuideStats(raError, decError, raPulse, decPulse, snr, numStars, skyBg, time);
3688 updateMaxX(time);
3689 if (!batchMode)
3690 replot();
3691}
3692
3693void Analyze::resetGuideStats()
3694{
3695 lastGuideStatsTime = -1;
3696 lastCaptureRmsTime = -1;
3697 numStarsMax = 0;
3698 snrMax = 0;
3699 skyBgMax = 0;
3700}
3701
3702namespace
3703{
3704
3705// TODO: move to ekos.h/cpp
3706AlignState convertAlignState(const QString &str)
3707{
3708 for (int i = 0; i < alignStates.size(); ++i)
3709 {
3710 if (str == alignStates[i].toString())
3711 return static_cast<AlignState>(i);
3712 }
3713 return ALIGN_IDLE;
3714}
3715
3716const QBrush alignBrush(AlignState state)
3717{
3718 switch (state)
3719 {
3720 case ALIGN_IDLE:
3721 return offBrush;
3722 case ALIGN_COMPLETE:
3723 case ALIGN_SUCCESSFUL:
3724 return successBrush;
3725 case ALIGN_FAILED:
3726 return failureBrush;
3727 case ALIGN_PROGRESS:
3728 return progress3Brush;
3729 case ALIGN_SYNCING:
3730 return progress2Brush;
3731 case ALIGN_SLEWING:
3732 return progressBrush;
3733 case ALIGN_ROTATING:
3734 return progress4Brush;
3735 case ALIGN_ABORTED:
3736 return failureBrush;
3737 case ALIGN_SUSPENDED:
3738 return offBrush;
3739 }
3740 // Shouldn't get here.
3741 return offBrush;
3742}
3743} // namespace
3744
3745void Analyze::alignState(AlignState state)
3746{
3747 if (state == lastAlignStateReceived)
3748 return;
3749 lastAlignStateReceived = state;
3750
3751 QString stateStr = getAlignStatusString(state);
3752 saveMessage("AlignState", stateStr);
3753 if (runtimeDisplay)
3754 processAlignState(logTime(), stateStr);
3755}
3756
3757//ALIGN_IDLE, ALIGN_COMPLETE, ALIGN_FAILED, ALIGN_ABORTED,ALIGN_PROGRESS,ALIGN_SYNCING,ALIGN_SLEWING
3758void Analyze::processAlignState(double time, const QString &statusString, bool batchMode)
3759{
3760 AlignState state = convertAlignState(statusString);
3761
3762 if (state == lastAlignStateStarted)
3763 return;
3764
3765 bool lastStateInteresting = (lastAlignStateStarted == ALIGN_PROGRESS ||
3766 lastAlignStateStarted == ALIGN_SYNCING ||
3767 lastAlignStateStarted == ALIGN_SLEWING);
3768 if (lastAlignStateStartedTime >= 0 && lastStateInteresting)
3769 {
3770 if (state == ALIGN_COMPLETE || state == ALIGN_FAILED || state == ALIGN_ABORTED)
3771 {
3772 // These states are really commetaries on the previous states.
3773 addSession(lastAlignStateStartedTime, time, ALIGN_Y, alignBrush(state));
3774 alignSessions.add(AlignSession(lastAlignStateStartedTime, time, nullptr, state));
3775 }
3776 else
3777 {
3778 addSession(lastAlignStateStartedTime, time, ALIGN_Y, alignBrush(lastAlignStateStarted));
3779 alignSessions.add(AlignSession(lastAlignStateStartedTime, time, nullptr, lastAlignStateStarted));
3780 }
3781 }
3782 bool stateInteresting = (state == ALIGN_PROGRESS || state == ALIGN_SYNCING ||
3783 state == ALIGN_SLEWING);
3784 if (stateInteresting)
3785 {
3786 addTemporarySession(&temporaryAlignSession, time, 1, ALIGN_Y, temporaryBrush);
3787 temporaryAlignSession.state = state;
3788 }
3789 else
3790 removeTemporarySession(&temporaryAlignSession);
3791
3792 lastAlignStateStartedTime = time;
3793 lastAlignStateStarted = state;
3794 updateMaxX(time);
3795 if (!batchMode)
3796 replot();
3797
3798}
3799
3800void Analyze::resetAlignState()
3801{
3802 lastAlignStateReceived = ALIGN_IDLE;
3803 lastAlignStateStarted = ALIGN_IDLE;
3804 lastAlignStateStartedTime = -1;
3805}
3806
3807namespace
3808{
3809
3810const QBrush mountBrush(ISD::Mount::Status state)
3811{
3812 switch (state)
3813 {
3814 case ISD::Mount::MOUNT_IDLE:
3815 return offBrush;
3816 case ISD::Mount::MOUNT_ERROR:
3817 return failureBrush;
3818 case ISD::Mount::MOUNT_MOVING:
3819 case ISD::Mount::MOUNT_SLEWING:
3820 return progressBrush;
3821 case ISD::Mount::MOUNT_TRACKING:
3822 return successBrush;
3823 case ISD::Mount::MOUNT_PARKING:
3824 return stoppedBrush;
3825 case ISD::Mount::MOUNT_PARKED:
3826 return stopped2Brush;
3827 }
3828 // Shouldn't get here.
3829 return offBrush;
3830}
3831
3832} // namespace
3833
3834// Mount status can be:
3835// MOUNT_IDLE, MOUNT_MOVING, MOUNT_SLEWING, MOUNT_TRACKING, MOUNT_PARKING, MOUNT_PARKED, MOUNT_ERROR
3836void Analyze::mountState(ISD::Mount::Status state)
3837{
3838 QString statusString = mountStatusString(state);
3839 saveMessage("MountState", statusString);
3840 if (runtimeDisplay)
3841 processMountState(logTime(), statusString);
3842}
3843
3844void Analyze::processMountState(double time, const QString &statusString, bool batchMode)
3845{
3846 ISD::Mount::Status state = toMountStatus(statusString);
3847 if (mountStateStartedTime >= 0 && lastMountState != ISD::Mount::MOUNT_IDLE)
3848 {
3849 addSession(mountStateStartedTime, time, MOUNT_Y, mountBrush(lastMountState));
3850 mountSessions.add(MountSession(mountStateStartedTime, time, nullptr, lastMountState));
3851 }
3852
3853 if (state != ISD::Mount::MOUNT_IDLE)
3854 {
3855 addTemporarySession(&temporaryMountSession, time, 1, MOUNT_Y,
3856 (state == ISD::Mount::MOUNT_TRACKING) ? successBrush : temporaryBrush);
3857 temporaryMountSession.state = state;
3858 }
3859 else
3860 removeTemporarySession(&temporaryMountSession);
3861
3862 mountStateStartedTime = time;
3863 lastMountState = state;
3864 updateMaxX(time);
3865 if (!batchMode)
3866 replot();
3867}
3868
3869void Analyze::resetMountState()
3870{
3871 mountStateStartedTime = -1;
3872 lastMountState = ISD::Mount::Status::MOUNT_IDLE;
3873}
3874
3875// This message comes from the mount module
3876void Analyze::mountCoords(const SkyPoint &position, ISD::Mount::PierSide pierSide, const dms &haValue)
3877{
3878 double ra = position.ra().Degrees();
3879 double dec = position.dec().Degrees();
3880 double ha = haValue.Degrees();
3881 double az = position.az().Degrees();
3882 double alt = position.alt().Degrees();
3883
3884 // Only process the message if something's changed by 1/4 degree or more.
3885 constexpr double MIN_DEGREES_CHANGE = 0.25;
3886 if ((fabs(ra - lastMountRa) > MIN_DEGREES_CHANGE) ||
3887 (fabs(dec - lastMountDec) > MIN_DEGREES_CHANGE) ||
3888 (fabs(ha - lastMountHa) > MIN_DEGREES_CHANGE) ||
3889 (fabs(az - lastMountAz) > MIN_DEGREES_CHANGE) ||
3890 (fabs(alt - lastMountAlt) > MIN_DEGREES_CHANGE) ||
3891 (pierSide != lastMountPierSide))
3892 {
3893 saveMessage("MountCoords", QString("%1,%2,%3,%4,%5,%6")
3894 .arg(QString::number(ra, 'f', 4), QString::number(dec, 'f', 4),
3895 QString::number(az, 'f', 4), QString::number(alt, 'f', 4))
3896 .arg(pierSide)
3897 .arg(QString::number(ha, 'f', 4)));
3898
3899 if (runtimeDisplay)
3900 processMountCoords(logTime(), ra, dec, az, alt, pierSide, ha);
3901
3902 lastMountRa = ra;
3903 lastMountDec = dec;
3904 lastMountHa = ha;
3905 lastMountAz = az;
3906 lastMountAlt = alt;
3907 lastMountPierSide = pierSide;
3908 }
3909}
3910
3911void Analyze::processMountCoords(double time, double ra, double dec, double az,
3912 double alt, int pierSide, double ha, bool batchMode)
3913{
3914 addMountCoords(ra, dec, az, alt, pierSide, ha, time);
3915 updateMaxX(time);
3916 if (!batchMode)
3917 replot();
3918}
3919
3920void Analyze::resetMountCoords()
3921{
3922 lastMountRa = -1;
3923 lastMountDec = -1;
3924 lastMountHa = -1;
3925 lastMountAz = -1;
3926 lastMountAlt = -1;
3927 lastMountPierSide = -1;
3928}
3929
3930namespace
3931{
3932
3933// TODO: Move to mount.h/cpp?
3934MeridianFlipState::MeridianFlipMountState convertMountFlipState(const QString &statusStr)
3935{
3936 if (statusStr == "MOUNT_FLIP_NONE")
3937 return MeridianFlipState::MOUNT_FLIP_NONE;
3938 else if (statusStr == "MOUNT_FLIP_PLANNED")
3939 return MeridianFlipState::MOUNT_FLIP_PLANNED;
3940 else if (statusStr == "MOUNT_FLIP_WAITING")
3941 return MeridianFlipState::MOUNT_FLIP_WAITING;
3942 else if (statusStr == "MOUNT_FLIP_ACCEPTED")
3943 return MeridianFlipState::MOUNT_FLIP_ACCEPTED;
3944 else if (statusStr == "MOUNT_FLIP_RUNNING")
3945 return MeridianFlipState::MOUNT_FLIP_RUNNING;
3946 else if (statusStr == "MOUNT_FLIP_COMPLETED")
3947 return MeridianFlipState::MOUNT_FLIP_COMPLETED;
3948 else if (statusStr == "MOUNT_FLIP_ERROR")
3949 return MeridianFlipState::MOUNT_FLIP_ERROR;
3950 return MeridianFlipState::MOUNT_FLIP_ERROR;
3951}
3952
3953QBrush mountFlipStateBrush(MeridianFlipState::MeridianFlipMountState state)
3954{
3955 switch (state)
3956 {
3957 case MeridianFlipState::MOUNT_FLIP_NONE:
3958 return offBrush;
3959 case MeridianFlipState::MOUNT_FLIP_PLANNED:
3960 return stoppedBrush;
3961 case MeridianFlipState::MOUNT_FLIP_WAITING:
3962 return stopped2Brush;
3963 case MeridianFlipState::MOUNT_FLIP_ACCEPTED:
3964 return progressBrush;
3965 case MeridianFlipState::MOUNT_FLIP_RUNNING:
3966 return progress2Brush;
3967 case MeridianFlipState::MOUNT_FLIP_COMPLETED:
3968 return successBrush;
3969 case MeridianFlipState::MOUNT_FLIP_ERROR:
3970 return failureBrush;
3971 }
3972 // Shouldn't get here.
3973 return offBrush;
3974}
3975} // namespace
3976
3977void Analyze::mountFlipStatus(MeridianFlipState::MeridianFlipMountState state)
3978{
3979 if (state == lastMountFlipStateReceived)
3980 return;
3981 lastMountFlipStateReceived = state;
3982
3983 QString stateStr = MeridianFlipState::meridianFlipStatusString(state);
3984 saveMessage("MeridianFlipState", stateStr);
3985 if (runtimeDisplay)
3986 processMountFlipState(logTime(), stateStr);
3987
3988}
3989
3990// MeridianFlipState::MOUNT_FLIP_NONE MeridianFlipState::MOUNT_FLIP_PLANNED MeridianFlipState::MOUNT_FLIP_WAITING MeridianFlipState::MOUNT_FLIP_ACCEPTED MeridianFlipState::MOUNT_FLIP_RUNNING MeridianFlipState::MOUNT_FLIP_COMPLETED MeridianFlipState::MOUNT_FLIP_ERROR
3991void Analyze::processMountFlipState(double time, const QString &statusString, bool batchMode)
3992{
3993 MeridianFlipState::MeridianFlipMountState state = convertMountFlipState(statusString);
3994 if (state == lastMountFlipStateStarted)
3995 return;
3996
3997 bool lastStateInteresting =
3998 (lastMountFlipStateStarted == MeridianFlipState::MOUNT_FLIP_PLANNED ||
3999 lastMountFlipStateStarted == MeridianFlipState::MOUNT_FLIP_WAITING ||
4000 lastMountFlipStateStarted == MeridianFlipState::MOUNT_FLIP_ACCEPTED ||
4001 lastMountFlipStateStarted == MeridianFlipState::MOUNT_FLIP_RUNNING);
4002 if (mountFlipStateStartedTime >= 0 && lastStateInteresting)
4003 {
4004 if (state == MeridianFlipState::MOUNT_FLIP_COMPLETED || state == MeridianFlipState::MOUNT_FLIP_ERROR)
4005 {
4006 // These states are really commentaries on the previous states.
4007 addSession(mountFlipStateStartedTime, time, MERIDIAN_MOUNT_FLIP_Y, mountFlipStateBrush(state));
4008 mountFlipSessions.add(MountFlipSession(mountFlipStateStartedTime, time, nullptr, state));
4009 }
4010 else
4011 {
4012 addSession(mountFlipStateStartedTime, time, MERIDIAN_MOUNT_FLIP_Y, mountFlipStateBrush(lastMountFlipStateStarted));
4013 mountFlipSessions.add(MountFlipSession(mountFlipStateStartedTime, time, nullptr, lastMountFlipStateStarted));
4014 }
4015 }
4016 bool stateInteresting =
4017 (state == MeridianFlipState::MOUNT_FLIP_PLANNED ||
4018 state == MeridianFlipState::MOUNT_FLIP_WAITING ||
4019 state == MeridianFlipState::MOUNT_FLIP_ACCEPTED ||
4020 state == MeridianFlipState::MOUNT_FLIP_RUNNING);
4021 if (stateInteresting)
4022 {
4023 addTemporarySession(&temporaryMountFlipSession, time, 1, MERIDIAN_MOUNT_FLIP_Y, temporaryBrush);
4024 temporaryMountFlipSession.state = state;
4025 }
4026 else
4027 removeTemporarySession(&temporaryMountFlipSession);
4028
4029 mountFlipStateStartedTime = time;
4030 lastMountFlipStateStarted = state;
4031 updateMaxX(time);
4032 if (!batchMode)
4033 replot();
4034}
4035
4036void Analyze::resetMountFlipState()
4037{
4038 lastMountFlipStateReceived = MeridianFlipState::MOUNT_FLIP_NONE;
4039 lastMountFlipStateStarted = MeridianFlipState::MOUNT_FLIP_NONE;
4040 mountFlipStateStartedTime = -1;
4041}
4042
4043QBrush Analyze::schedulerJobBrush(const QString &jobName, bool temporary)
4044{
4045 QList<QColor> colors =
4046 {
4047 {110, 120, 150}, {150, 180, 180}, {180, 165, 130}, {180, 200, 140}, {250, 180, 130},
4048 {190, 170, 160}, {140, 110, 160}, {250, 240, 190}, {250, 200, 220}, {150, 125, 175}
4049 };
4050
4051 Qt::BrushStyle pattern = temporary ? Qt::Dense4Pattern : Qt::SolidPattern;
4052 auto it = schedulerJobColors.constFind(jobName);
4053 if (it == schedulerJobColors.constEnd())
4054 {
4055 const int numSoFar = schedulerJobColors.size();
4056 auto color = colors[numSoFar % colors.size()];
4057 schedulerJobColors[jobName] = color;
4058 return QBrush(color, pattern);
4059 }
4060 else
4061 {
4062 return QBrush(*it, pattern);
4063 }
4064}
4065
4066void Analyze::schedulerJobStarted(const QString &jobName)
4067{
4068 saveMessage("SchedulerJobStart", jobName);
4069 if (runtimeDisplay)
4070 processSchedulerJobStarted(logTime(), jobName);
4071
4072}
4073
4074void Analyze::schedulerJobEnded(const QString &jobName, const QString &reason)
4075{
4076 saveMessage("SchedulerJobEnd", QString("%1,%2").arg(jobName, reason));
4077 if (runtimeDisplay)
4078 processSchedulerJobEnded(logTime(), jobName, reason);
4079}
4080
4081
4082// Called by either the above (when live data is received), or reading from file.
4083// BatchMode would be true when reading from file.
4084void Analyze::processSchedulerJobStarted(double time, const QString &jobName)
4085{
4086 checkForMissingSchedulerJobEnd(time - 1);
4087 schedulerJobStartedTime = time;
4088 schedulerJobStartedJobName = jobName;
4089 updateMaxX(time);
4090
4091 addTemporarySession(&temporarySchedulerJobSession, time, 1, SCHEDULER_Y, schedulerJobBrush(jobName, true));
4092 temporarySchedulerJobSession.jobName = jobName;
4093}
4094
4095// Called when the captureComplete slot receives a signal.
4096void Analyze::processSchedulerJobEnded(double time, const QString &jobName, const QString &reason, bool batchMode)
4097{
4098 removeTemporarySession(&temporarySchedulerJobSession);
4099
4100 if (schedulerJobStartedTime < 0)
4101 {
4102 replot();
4103 return;
4104 }
4105
4106 addSession(schedulerJobStartedTime, time, SCHEDULER_Y, schedulerJobBrush(jobName, false));
4107 auto session = SchedulerJobSession(schedulerJobStartedTime, time, nullptr, jobName, reason);
4108 schedulerJobSessions.add(session);
4109 updateMaxX(time);
4110 resetSchedulerJob();
4111 if (!batchMode)
4112 replot();
4113}
4114
4115// Just called in batch mode, in case the processSchedulerJobEnded was never called.
4116void Analyze::checkForMissingSchedulerJobEnd(double time)
4117{
4118 if (schedulerJobStartedTime < 0)
4119 return;
4120 removeTemporarySession(&temporarySchedulerJobSession);
4121 addSession(schedulerJobStartedTime, time, SCHEDULER_Y, schedulerJobBrush(schedulerJobStartedJobName, false));
4122 auto session = SchedulerJobSession(schedulerJobStartedTime, time, nullptr, schedulerJobStartedJobName, "missing job end");
4123 schedulerJobSessions.add(session);
4124 updateMaxX(time);
4125 resetSchedulerJob();
4126}
4127
4128void Analyze::resetSchedulerJob()
4129{
4130 schedulerJobStartedTime = -1;
4131 schedulerJobStartedJobName = "";
4132}
4133
4134void Analyze::appendLogText(const QString &text)
4135{
4136 m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
4137 KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text));
4138
4139 qCInfo(KSTARS_EKOS_ANALYZE) << text;
4140
4141 emit newLog(text);
4142}
4143
4144void Analyze::clearLog()
4145{
4146 m_LogText.clear();
4147 emit newLog(QString());
4148}
4149
4150} // namespace Ekos
static KStars * Instance()
Definition kstars.h:122
The abstract base class for all entries in a QCPLegend.
virtual int findBegin(double sortKey, bool expandedRange=true) const override
bool removeFromLegend(QCPLegend *legend) const
bool addToLegend(QCPLegend *legend)
void setPen(const QPen &pen)
void setName(const QString &name)
QCPAxis * addAxis(QCPAxis::AxisType type, QCPAxis *axis=nullptr)
void setRangeZoomAxes(QCPAxis *horizontal, QCPAxis *vertical)
Specialized axis ticker for calendar dates and times as axis ticks.
static QDateTime keyToDateTime(double key)
Specialized axis ticker which allows arbitrary labels at specified coordinates.
Manages a single axis inside a QCustomPlot.
void rangeChanged(const QCPRange &newRange)
void scaleRange(double factor)
void setLabel(const QString &str)
void setTickLabelColor(const QColor &color)
void rescale(bool onlyVisiblePlottables=false)
double pixelToCoord(double value) const
QCPGrid * grid() const
void setLabelColor(const QColor &color)
void setBasePen(const QPen &pen)
void setTickPen(const QPen &pen)
@ atLeft
0x01 Axis is vertical and on the left side of the axis rect
Q_SLOT void setRange(const QCPRange &range)
void setSubTickPen(const QPen &pen)
A plottable that adds a set of error bars to other plottables.
A plottable representing a graph in a plot.
QSharedPointer< QCPGraphDataContainer > data() const
void setLineStyle(LineStyle ls)
@ lsLine
data points are connected by a straight line
@ lsStepRight
line is drawn as steps where the step height is the value of the right data point
@ lsStepLeft
line is drawn as steps where the step height is the value of the left data point
@ lsNone
data points are not connected with any lines (e.g.
void addData(const QVector< double > &keys, const QVector< double > &values, bool alreadySorted=false)
void setZeroLinePen(const QPen &pen)
void setSubGridPen(const QPen &pen)
void setPen(const QPen &pen)
An ellipse.
void setPen(const QPen &pen)
A line from one point to another.
void setPen(const QPen &pen)
void setType(PositionType type)
void setCoords(double key, double value)
@ ptAxisRectRatio
Static positioning given by a fraction of the axis rect size (see setAxisRect).
@ ptPlotCoords
Dynamic positioning at a plot coordinate defined by two axes (see setAxes).
A rectangle.
void setPen(const QPen &pen)
void setSelectedPen(const QPen &pen)
void setBrush(const QBrush &brush)
void setSelectedBrush(const QBrush &brush)
A text label.
void setText(const QString &text)
void setPositionAlignment(Qt::Alignment alignment)
void setFont(const QFont &font)
void setPen(const QPen &pen)
void setColor(const QColor &color)
void setVisible(bool on)
@ foRowsFirst
Rows are filled first, and a new element is wrapped to the next column if the row count would exceed ...
Manages a legend inside a QCustomPlot.
@ spLegendBox
0x001 The legend box (frame)
Represents the range an axis is encompassing.
double center() const
Represents the visual appearance of scatter points.
@ ssDisc
\enumimage{ssDisc.png} a circle which is filled with the pen's color (not the brush as with ssCircle)
@ ssStar
\enumimage{ssStar.png} a star with eight arms, i.e. a combination of cross and plus
@ ssCircle
\enumimage{ssCircle.png} a circle
The central class of the library. This is the QWidget which displays the plot and interacts with the ...
void setBackground(const QPixmap &pm)
QCPGraph * addGraph(QCPAxis *keyAxis=nullptr, QCPAxis *valueAxis=nullptr)
int graphCount() const
QCPGraph * graph(int index) const
void mouseMove(QMouseEvent *event)
void legendClick(QCPLegend *legend, QCPAbstractLegendItem *item, QMouseEvent *event)
QCPAxis * xAxis
void mouseDoubleClick(QMouseEvent *event)
void mouseWheel(QWheelEvent *event)
void mousePress(QMouseEvent *event)
QCPAxis * yAxis
The sky coordinates of a point in the sky.
Definition skypoint.h:45
const CachingDms & dec() const
Definition skypoint.h:269
const CachingDms & ra() const
Definition skypoint.h:263
const dms & az() const
Definition skypoint.h:275
const dms & alt() const
Definition skypoint.h:281
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
double Hours() const
Definition dms.h:168
virtual void setH(const double &x)
Sets floating-point value of angle, in hours.
Definition dms.h:210
int second() const
Definition dms.cpp:231
int minute() const
Definition dms.cpp:221
int hour() const
Definition dms.h:147
virtual void setD(const double &x)
Sets floating-point value of angle, in degrees.
Definition dms.h:179
const double & Degrees() const
Definition dms.h:141
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
char * toString(const EngineQuery &query)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:83
@ ALIGN_FAILED
Alignment failed.
Definition ekos.h:148
@ ALIGN_PROGRESS
Alignment operation in progress.
Definition ekos.h:150
@ ALIGN_SUCCESSFUL
Alignment Astrometry solver successfully solved the image.
Definition ekos.h:151
@ ALIGN_SLEWING
Slewing mount to target coordinates.
Definition ekos.h:153
@ ALIGN_ABORTED
Alignment aborted by user or agent.
Definition ekos.h:149
@ ALIGN_SYNCING
Syncing mount to solution coordinates.
Definition ekos.h:152
@ ALIGN_IDLE
No ongoing operations.
Definition ekos.h:146
@ ALIGN_COMPLETE
Alignment successfully completed.
Definition ekos.h:147
@ ALIGN_SUSPENDED
Alignment operations suspended.
Definition ekos.h:155
@ ALIGN_ROTATING
Rotating (Automatic or Manual) to target position angle.
Definition ekos.h:154
bool fileExists(const QUrl &path)
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
KIOCORE_EXPORT QString dir(const QString &fileClass)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
KIOCORE_EXPORT void add(const QString &fileClass, const QString &directory)
QString name(StandardAction id)
KGuiItem reset()
KGuiItem clear()
const QList< QKeySequence > & begin()
const QList< QKeySequence > & zoomIn()
const QList< QKeySequence > & zoomOut()
const QList< QKeySequence > & next()
const QList< QKeySequence > & find()
const QList< QKeySequence > & findNext()
@ iRangeDrag
0x001 Axis ranges are draggable (see QCPAxisRect::setRangeDrag, QCPAxisRect::setRangeDragAxes)
@ iRangeZoom
0x002 Axis ranges are zoomable with the mouse wheel (see QCPAxisRect::setRangeZoom,...
bool isChecked() const const
void clicked(bool checked)
void toggled(bool checked)
void valueChanged(int value)
void stateChanged(int state)
void activated(int index)
QDateTime addMSecs(qint64 msecs) const const
QDateTime currentDateTime()
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
qint64 toMSecsSinceEpoch() const const
QString toString(QStringView format, QCalendar cal) const const
MouseButtonDblClick
Type type() const const
QUrl getOpenFileUrl(QWidget *parent, const QString &caption, const QUrl &dir, const QString &filter, QString *selectedFilter, Options options, const QStringList &supportedSchemes)
QString absoluteFilePath() const const
QString fileName() const const
void setPointSizeF(qreal pointSize)
void clear()
void setText(const QString &)
void append(QList< T > &&value)
bool contains(const AT &value) const const
T & last()
QList< T > mid(qsizetype pos, qsizetype length) const const
void push_back(parameter_type value)
void push_front(parameter_type value)
qsizetype size() const const
QString toString(QDate date, FormatType format) const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
T qobject_cast(QObject *object)
void setColor(ColorGroup group, ColorRole role, const QColor &color)
void activated()
QString writableLocation(StandardLocation type)
QString & append(QChar ch)
QString arg(Args &&... args) const const
bool isEmpty() const const
qsizetype lastIndexOf(QChar ch, Qt::CaseSensitivity cs) const const
QString number(double n, char format, int precision)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QString right(qsizetype n) const const
qsizetype size() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
double toDouble(bool *ok) const const
int toInt(bool *ok, int base) const const
AlignLeft
DiagCrossPattern
Unchecked
RightButton
SolidLine
QTextStream & dec(QTextStream &stream)
QTextStream & left(QTextStream &stream)
void setSpan(int row, int column, int rowSpanCount, int columnSpanCount)
void setItem(int row, int column, QTableWidgetItem *item)
void setRowCount(int rows)
QFont font() const const
void setFont(const QFont &font)
void setForeground(const QBrush &brush)
void setText(const QString &text)
void setTextAlignment(Qt::Alignment alignment)
QFuture< void > filter(QThreadPool *pool, Sequence &sequence, KeepFunctor &&filterFunction)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
RemoveFilename
QString fileName(ComponentFormattingOptions options) const const
QUrl fromLocalFile(const QString &localFile)
bool isEmpty() const const
QString toLocalFile() const const
QString url(FormattingOptions options) const const
int toInt(bool *ok) const const
QString toString() const const
void setDisabled(bool disable)
Used to keep track of the various Y-axes and connect them to the QLineEdits.
Definition yaxistool.h:25
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 25 2025 11:58:34 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.