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

KDE's Doxygen guidelines are available online.