Kstars

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

KDE's Doxygen guidelines are available online.