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

KDE's Doxygen guidelines are available online.