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

KDE's Doxygen guidelines are available online.