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

KDE's Doxygen guidelines are available online.