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 
14 #include "auxiliary/kspaths.h"
15 #include "dms.h"
16 #include "ekos/manager.h"
17 #include "fitsviewer/fitsdata.h"
18 #include "fitsviewer/fitsviewer.h"
19 #include "ksmessagebox.h"
20 #include "kstars.h"
21 #include "Options.h"
22 
23 #include <ekos_analyze_debug.h>
24 #include <KHelpClient>
25 #include <version.h>
26 
27 // Subclass QCPAxisTickerDateTime, so that times are offset from the start
28 // of the log, instead of being offset from the UNIX 0-seconds time.
29 class OffsetDateTimeTicker : public QCPAxisTickerDateTime
30 {
31     public:
setOffset(double offset)32         void setOffset(double offset)
33         {
34             timeOffset = offset;
35         }
getTickLabel(double tick,const QLocale & locale,QChar formatChar,int precision)36         QString getTickLabel(double tick, const QLocale &locale, QChar formatChar, int precision) override
37         {
38             Q_UNUSED(precision);
39             Q_UNUSED(formatChar);
40             // Seconds are offset from the unix origin by
41             return locale.toString(keyToDateTime(tick + timeOffset).toTimeSpec(mDateTimeSpec), mDateTimeFormat);
42         }
43     private:
44         double timeOffset = 0;
45 };
46 
47 namespace
48 {
49 
50 // QDateTime is written to file with this format.
51 QString timeFormat = "yyyy-MM-dd hh:mm:ss.zzz";
52 
53 // The resolution of the scroll bar.
54 constexpr int MAX_SCROLL_VALUE = 10000;
55 
56 // Half the height of a timeline line.
57 // That is timeline lines are horizontal bars along y=1 or y=2 ... and their
58 // vertical widths are from y-halfTimelineHeight to y+halfTimelineHeight.
59 constexpr double halfTimelineHeight = 0.35;
60 
61 // These are initialized in initStatsPlot when the graphs are added.
62 // They index the graphs in statsPlot, e.g. statsPlot->graph(HFR_GRAPH)->addData(...)
63 int HFR_GRAPH = -1;
64 int TEMPERATURE_GRAPH = -1;
65 int NUM_CAPTURE_STARS_GRAPH = -1;
66 int MEDIAN_GRAPH = -1;
67 int ECCENTRICITY_GRAPH = -1;
68 int NUMSTARS_GRAPH = -1;
69 int SKYBG_GRAPH = -1;
70 int SNR_GRAPH = -1;
71 int RA_GRAPH = -1;
72 int DEC_GRAPH = -1;
73 int RA_PULSE_GRAPH = -1;
74 int DEC_PULSE_GRAPH = -1;
75 int DRIFT_GRAPH = -1;
76 int RMS_GRAPH = -1;
77 int CAPTURE_RMS_GRAPH = -1;
78 int MOUNT_RA_GRAPH = -1;
79 int MOUNT_DEC_GRAPH = -1;
80 int MOUNT_HA_GRAPH = -1;
81 int AZ_GRAPH = -1;
82 int ALT_GRAPH = -1;
83 int PIER_SIDE_GRAPH = -1;
84 
85 // Initialized in initGraphicsPlot().
86 int FOCUS_GRAPHICS = -1;
87 int FOCUS_GRAPHICS_FINAL = -1;
88 int GUIDER_GRAPHICS = -1;
89 
90 // Brushes used in the timeline plot.
91 const QBrush temporaryBrush(Qt::green, Qt::DiagCrossPattern);
92 const QBrush timelineSelectionBrush(QColor(255, 100, 100, 150), Qt::SolidPattern);
93 const QBrush successBrush(Qt::green, Qt::SolidPattern);
94 const QBrush failureBrush(Qt::red, Qt::SolidPattern);
95 const QBrush offBrush(Qt::gray, Qt::SolidPattern);
96 const QBrush progressBrush(Qt::blue, Qt::SolidPattern);
97 const QBrush progress2Brush(QColor(0, 165, 255), Qt::SolidPattern);
98 const QBrush progress3Brush(Qt::cyan, Qt::SolidPattern);
99 const QBrush stoppedBrush(Qt::yellow, Qt::SolidPattern);
100 const QBrush stopped2Brush(Qt::darkYellow, Qt::SolidPattern);
101 
102 // Utility to checks if a file exists and is not a directory.
fileExists(const QString & path)103 bool fileExists(const QString &path)
104 {
105     QFileInfo info(path);
106     return info.exists() && info.isFile();
107 }
108 
109 // Utilities to go between a mount status and a string.
110 // Move to inditelescope.h/cpp?
mountStatusString(ISD::Telescope::Status status)111 const QString mountStatusString(ISD::Telescope::Status status)
112 {
113     switch (status)
114     {
115         case ISD::Telescope::MOUNT_IDLE:
116             return i18n("Idle");
117         case ISD::Telescope::MOUNT_PARKED:
118             return i18n("Parked");
119         case ISD::Telescope::MOUNT_PARKING:
120             return i18n("Parking");
121         case ISD::Telescope::MOUNT_SLEWING:
122             return i18n("Slewing");
123         case ISD::Telescope::MOUNT_MOVING:
124             return i18n("Moving");
125         case ISD::Telescope::MOUNT_TRACKING:
126             return i18n("Tracking");
127         case ISD::Telescope::MOUNT_ERROR:
128             return i18n("Error");
129     }
130     return i18n("Error");
131 }
132 
toMountStatus(const QString & str)133 ISD::Telescope::Status toMountStatus(const QString &str)
134 {
135     if (str == i18n("Idle"))
136         return ISD::Telescope::MOUNT_IDLE;
137     else if (str == i18n("Parked"))
138         return ISD::Telescope::MOUNT_PARKED;
139     else if (str == i18n("Parking"))
140         return ISD::Telescope::MOUNT_PARKING;
141     else if (str == i18n("Slewing"))
142         return ISD::Telescope::MOUNT_SLEWING;
143     else if (str == i18n("Moving"))
144         return ISD::Telescope::MOUNT_MOVING;
145     else if (str == i18n("Tracking"))
146         return ISD::Telescope::MOUNT_TRACKING;
147     else
148         return ISD::Telescope::MOUNT_ERROR;
149 }
150 
151 // Returns the stripe color used when drawing the capture timeline for various filters.
152 // TODO: Not sure how to internationalize this.
filterStripeBrush(const QString & filter,QBrush * brush)153 bool filterStripeBrush(const QString &filter, QBrush *brush)
154 {
155     const QRegularExpression::PatternOption c = QRegularExpression::CaseInsensitiveOption;
156 
157     const QString rPattern("^(red|r)$");
158     if (QRegularExpression(rPattern, c).match(filter).hasMatch())
159     {
160         *brush = QBrush(Qt::red, Qt::SolidPattern);
161         return true;
162     }
163     const QString gPattern("^(green|g)$");
164     if (QRegularExpression(gPattern, c).match(filter).hasMatch())
165     {
166         *brush = QBrush(Qt::green, Qt::SolidPattern);
167         return true;
168     }
169     const QString bPattern("^(blue|b)$");
170     if (QRegularExpression(bPattern, c).match(filter).hasMatch())
171     {
172         *brush = QBrush(Qt::blue, Qt::SolidPattern);
173         return true;
174     }
175     const QString hPattern("^(ha|h|h-a|h_a|h-alpha|hydrogen|hydrogen_alpha|hydrogen-alpha|h_alpha|halpha)$");
176     if (QRegularExpression(hPattern, c).match(filter).hasMatch())
177     {
178         *brush = QBrush(Qt::darkRed, Qt::SolidPattern);
179         return true;
180     }
181     const QString oPattern("^(oiii|oxygen|oxygen_3|oxygen-3|oxygen_iii|oxygen-iii|o_iii|o-iii|o_3|o-3|o3)$");
182     if (QRegularExpression(oPattern, c).match(filter).hasMatch())
183     {
184         *brush = QBrush(Qt::cyan, Qt::SolidPattern);
185         return true;
186     }
187     const QString
188     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)$");
189     if (QRegularExpression(sPattern, c).match(filter).hasMatch())
190     {
191         // Pink.
192         *brush = QBrush(QColor(255, 182, 193), Qt::SolidPattern);
193         return true;
194     }
195     const QString lPattern("^(lpr|L|UV-IR cut|UV-IR|white|monochrome|broadband|clear|focus|luminance|lum|lps|cls)$");
196     if (QRegularExpression(lPattern, c).match(filter).hasMatch())
197     {
198         *brush = QBrush(Qt::white, Qt::SolidPattern);
199         return true;
200     }
201     return false;
202 }
203 
204 // Used when searching for FITS files to display.
205 // If filename isn't found as is, it tries alterateDirectory in several ways
206 // e.g. if filename = /1/2/3/4/name is not found, then try alternateDirectory/name,
207 // then alternateDirectory/4/name, then alternateDirectory/3/4/name,
208 // then alternateDirectory/2/3/4/name, and so on.
209 // If it cannot find the FITS file, it returns an empty string, otherwise it returns
210 // the full path where the file was found.
findFilename(const QString & filename,const QString & alternateDirectory)211 QString findFilename(const QString &filename, const QString &alternateDirectory)
212 {
213     // Try the origial full path.
214     QFileInfo info(filename);
215     if (info.exists() && info.isFile())
216         return filename;
217 
218     // Try putting the filename at the end of the full path onto alternateDirectory.
219     QString name = info.fileName();
220     QString temp = QString("%1/%2").arg(alternateDirectory).arg(name);
221     if (fileExists(temp))
222         return temp;
223 
224     // Try appending the filename plus the ending directories onto alternateDirectory.
225     int size = filename.size();
226     int searchBackFrom = size - name.size();
227     int num = 0;
228     while (searchBackFrom >= 0)
229     {
230         int index = filename.lastIndexOf('/', searchBackFrom);
231         if (index < 0)
232             break;
233 
234         QString temp2 = QString("%1%2").arg(alternateDirectory).arg(filename.right(size - index));
235         if (fileExists(temp2))
236             return temp2;
237 
238         searchBackFrom = index - 1;
239 
240         // Paranoia
241         if (++num > 20)
242             break;
243     }
244     return "";
245 }
246 
247 // This is an exhaustive search for now.
248 // This is reasonable as the number of sessions should be limited.
249 template <class T>
250 class IntervalFinder
251 {
252     public:
IntervalFinder()253         IntervalFinder() {}
~IntervalFinder()254         ~IntervalFinder() {}
add(T value)255         void add(T value)
256         {
257             intervals.append(value);
258         }
clear()259         void clear()
260         {
261             intervals.clear();
262         }
find(double t)263         QList<T> find(double t)
264         {
265             QList<T> result;
266             for (const auto i : intervals)
267             {
268                 if (t >= i.start && t <= i.end)
269                     result.push_back(i);
270             }
271             return result;
272         }
273     private:
274         QList<T> intervals;
275 };
276 
277 IntervalFinder<Ekos::Analyze::CaptureSession> captureSessions;
278 IntervalFinder<Ekos::Analyze::FocusSession> focusSessions;
279 IntervalFinder<Ekos::Analyze::GuideSession> guideSessions;
280 IntervalFinder<Ekos::Analyze::MountSession> mountSessions;
281 IntervalFinder<Ekos::Analyze::AlignSession> alignSessions;
282 IntervalFinder<Ekos::Analyze::MountFlipSession> mountFlipSessions;
283 
284 }  // namespace
285 
286 namespace Ekos
287 {
288 
289 // RmsFilter computes the RMS error of a 2-D sequence. Input the x error and y error
290 // into newSample(). It returns the sqrt of an approximate moving average of the squared
291 // errors roughly averaged over 40 samples--implemented by a simple digital low-pass filter.
292 // It's used to compute RMS guider errors, where x and y would be RA and DEC errors.
293 class RmsFilter
294 {
295     public:
RmsFilter()296         RmsFilter()
297         {
298             constexpr double timeConstant = 40.0;
299             alpha = 1.0 / pow(timeConstant, 0.865);
300         }
resetFilter()301         void resetFilter()
302         {
303             filteredRMS = 0;
304         }
newSample(double x,double y)305         double newSample(double x, double y)
306         {
307             const double valueSquared = x * x + y * y;
308             filteredRMS = alpha * valueSquared + (1.0 - alpha) * filteredRMS;
309             return sqrt(filteredRMS);
310         }
311     private:
312         double alpha { 0 };
313         double filteredRMS { 0 };
314 };
315 
Analyze()316 Analyze::Analyze()
317 {
318     setupUi(this);
319 
320     captureRms.reset(new RmsFilter);
321     guiderRms.reset(new RmsFilter);
322 
323     alternateFolder = QDir::homePath();
324 
325     initInputSelection();
326     initTimelinePlot();
327     initStatsPlot();
328     initGraphicsPlot();
329     fullWidthCB->setChecked(true);
330     runtimeDisplay = true;
331     fullWidthCB->setVisible(true);
332     fullWidthCB->setDisabled(false);
333     connect(fullWidthCB, &QCheckBox::toggled, [ = ](bool checked)
334     {
335         if (checked)
336             this->replot();
337     });
338 
339     initStatsCheckboxes();
340 
341     connect(zoomInB, &QPushButton::clicked, this, &Ekos::Analyze::zoomIn);
342     connect(zoomOutB, &QPushButton::clicked, this, &Ekos::Analyze::zoomOut);
343     connect(timelinePlot, &QCustomPlot::mousePress, this, &Ekos::Analyze::timelineMousePress);
344     connect(timelinePlot, &QCustomPlot::mouseDoubleClick, this, &Ekos::Analyze::timelineMouseDoubleClick);
345     connect(timelinePlot, &QCustomPlot::mouseWheel, this, &Ekos::Analyze::timelineMouseWheel);
346     connect(statsPlot, &QCustomPlot::mousePress, this, &Ekos::Analyze::statsMousePress);
347     connect(statsPlot, &QCustomPlot::mouseDoubleClick, this, &Ekos::Analyze::statsMouseDoubleClick);
348     connect(statsPlot, &QCustomPlot::mouseMove, this, &Ekos::Analyze::statsMouseMove);
349     connect(analyzeSB, &QScrollBar::valueChanged, this, &Ekos::Analyze::scroll);
350     analyzeSB->setRange(0, MAX_SCROLL_VALUE);
351     connect(helpB, &QPushButton::clicked, this, &Ekos::Analyze::helpMessage);
352     connect(keepCurrentCB, &QCheckBox::stateChanged, this, &Ekos::Analyze::keepCurrent);
353 
354     setupKeyboardShortcuts(timelinePlot);
355 
356     reset();
357     replot();
358 }
359 
360 // Mouse wheel over the Timeline plot causes an x-axis zoom.
timelineMouseWheel(QWheelEvent * event)361 void Analyze::timelineMouseWheel(QWheelEvent *event)
362 {
363     if (event->angleDelta().y() > 0)
364         zoomIn();
365     else if (event->angleDelta().y() < 0)
366         zoomOut();
367 }
368 
369 // This callback is used so that when keepCurrent is checked, we replot immediately.
370 // The actual keepCurrent work is done in replot().
keepCurrent(int state)371 void Analyze::keepCurrent(int state)
372 {
373     Q_UNUSED(state);
374     if (keepCurrentCB->isChecked())
375     {
376         removeStatsCursor();
377         replot();
378     }
379 }
380 
381 // Implements the input selection UI. User can either choose the current Ekos
382 // session, or a file read from disk, or set the alternateDirectory variable.
initInputSelection()383 void Analyze::initInputSelection()
384 {
385     // Setup the input combo box.
386     dirPath = QUrl::fromLocalFile(QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("analyze"));
387 
388     inputCombo->addItem(i18n("Current Session"));
389     inputCombo->addItem(i18n("Read from File"));
390     inputCombo->addItem(i18n("Set alternative image-file base directory"));
391     inputValue->setText("");
392     connect(inputCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, [&](int index)
393     {
394         if (index == 0)
395         {
396             // Input from current session
397             if (!runtimeDisplay)
398             {
399                 reset();
400                 inputValue->setText(i18n("Current Session"));
401                 maxXValue = readDataFromFile(logFilename);
402                 runtimeDisplay = true;
403             }
404             fullWidthCB->setChecked(true);
405             fullWidthCB->setVisible(true);
406             fullWidthCB->setDisabled(false);
407             replot();
408         }
409         else if (index == 1)
410         {
411             // Input from a file.
412             QUrl inputURL = QFileDialog::getOpenFileUrl(this, i18nc("@title:window", "Select input file"), dirPath,
413                             i18n("Analyze Log (*.analyze);;All Files (*)"));
414             if (inputURL.isEmpty())
415                 return;
416             dirPath = QUrl(inputURL.url(QUrl::RemoveFilename));
417 
418             reset();
419             inputValue->setText(inputURL.fileName());
420 
421             // If we do this after the readData call below, it would animate the sequence.
422             runtimeDisplay = false;
423 
424             maxXValue = readDataFromFile(inputURL.toLocalFile());
425             plotStart = 0;
426             plotWidth = maxXValue + 5;
427             replot();
428         }
429         else if (index == 2)
430         {
431             QString dir = QFileDialog::getExistingDirectory(
432                               this, i18n("Set an alternate base directory for your captured images"),
433                               QDir::homePath(),
434                               QFileDialog::ShowDirsOnly);
435             if (dir.size() > 0)
436             {
437                 // TODO: replace with an option.
438                 alternateFolder = dir;
439             }
440             // This is not a destiation, reset to one of the above.
441             if (runtimeDisplay)
442                 inputCombo->setCurrentIndex(0);
443             else
444                 inputCombo->setCurrentIndex(1);
445         }
446     });
447 }
448 
setupKeyboardShortcuts(QCustomPlot * plot)449 void Analyze::setupKeyboardShortcuts(QCustomPlot *plot)
450 {
451     // Shortcuts defined: https://doc.qt.io/archives/qt-4.8/qkeysequence.html#standard-shortcuts
452     QShortcut *s = new QShortcut(QKeySequence(QKeySequence::ZoomIn), plot);
453     connect(s, &QShortcut::activated, this, &Ekos::Analyze::zoomIn);
454     s = new QShortcut(QKeySequence(QKeySequence::ZoomOut), plot);
455     connect(s, &QShortcut::activated, this, &Ekos::Analyze::zoomOut);
456     s = new QShortcut(QKeySequence(QKeySequence::MoveToNextChar), plot);
457     connect(s, &QShortcut::activated, this, &Ekos::Analyze::scrollRight);
458     s = new QShortcut(QKeySequence(QKeySequence::MoveToPreviousChar), plot);
459     connect(s, &QShortcut::activated, this, &Ekos::Analyze::scrollLeft);
460     s = new QShortcut(QKeySequence("?"), plot);
461     connect(s, &QShortcut::activated, this, &Ekos::Analyze::helpMessage);
462     s = new QShortcut(QKeySequence("h"), plot);
463     connect(s, &QShortcut::activated, this, &Ekos::Analyze::helpMessage);
464     s = new QShortcut(QKeySequence(QKeySequence::HelpContents), plot);
465     connect(s, &QShortcut::activated, this, &Ekos::Analyze::helpMessage);
466 }
467 
~Analyze()468 Analyze::~Analyze()
469 {
470     // TODO:
471     // We should write out to disk any sessions that haven't terminated
472     // (e.g. capture, focus, guide)
473 }
474 
475 // When a user selects a timeline session, the previously selected one
476 // is deselected.  Note: this does not replot().
unhighlightTimelineItem()477 void Analyze::unhighlightTimelineItem()
478 {
479     if (selectionHighlight != nullptr)
480     {
481         timelinePlot->removeItem(selectionHighlight);
482         selectionHighlight = nullptr;
483     }
484     detailsTable->clear();
485 }
486 
487 // Highlight the area between start and end on row y in Timeline.
488 // Note that this doesn't replot().
highlightTimelineItem(double y,double start,double end)489 void Analyze::highlightTimelineItem(double y, double start, double end)
490 {
491     constexpr double halfHeight = 0.5;
492     unhighlightTimelineItem();
493 
494     QCPItemRect *rect = new QCPItemRect(timelinePlot);
495     rect->topLeft->setCoords(start, y + halfHeight);
496     rect->bottomRight->setCoords(end, y - halfHeight);
497     rect->setBrush(timelineSelectionBrush);
498     selectionHighlight = rect;
499 }
500 
501 // Creates a fat line-segment on the Timeline, optionally with a stripe in the middle.
addSession(double start,double end,double y,const QBrush & brush,const QBrush * stripeBrush)502 QCPItemRect * Analyze::addSession(double start, double end, double y,
503                                   const QBrush &brush, const QBrush *stripeBrush)
504 {
505     QPen pen = QPen(Qt::black, 1, Qt::SolidLine);
506     QCPItemRect *rect = new QCPItemRect(timelinePlot);
507     rect->topLeft->setCoords(start, y + halfTimelineHeight);
508     rect->bottomRight->setCoords(end, y - halfTimelineHeight);
509     rect->setPen(pen);
510     rect->setSelectedPen(pen);
511     rect->setBrush(brush);
512     rect->setSelectedBrush(brush);
513 
514     if (stripeBrush != nullptr)
515     {
516         QCPItemRect *stripe = new QCPItemRect(timelinePlot);
517         stripe->topLeft->setCoords(start, y + halfTimelineHeight / 2.0);
518         stripe->bottomRight->setCoords(end, y - halfTimelineHeight / 2.0);
519         stripe->setPen(pen);
520         stripe->setBrush(*stripeBrush);
521     }
522     return rect;
523 }
524 
525 // Add the guide stats values to the Stats graphs.
526 // We want to avoid drawing guide-stat values when not guiding.
527 // That is, we have no input samples then, but the graph would connect
528 // two points with a line. By adding NaN values into the graph,
529 // those places are made invisible.
addGuideStats(double raDrift,double decDrift,int raPulse,int decPulse,double snr,int numStars,double skyBackground,double time)530 void Analyze::addGuideStats(double raDrift, double decDrift, int raPulse, int decPulse, double snr,
531                             int numStars, double skyBackground, double time)
532 {
533     double MAX_GUIDE_STATS_GAP = 30;
534 
535     if (time - lastGuideStatsTime > MAX_GUIDE_STATS_GAP &&
536             lastGuideStatsTime >= 0)
537     {
538         addGuideStatsInternal(qQNaN(), qQNaN(), 0, 0, qQNaN(), qQNaN(), qQNaN(), qQNaN(), qQNaN(),
539                               lastGuideStatsTime + .0001);
540         addGuideStatsInternal(qQNaN(), qQNaN(), 0, 0, qQNaN(), qQNaN(), qQNaN(), qQNaN(), qQNaN(), time - .0001);
541         guiderRms->resetFilter();
542     }
543 
544     const double drift = std::hypot(raDrift, decDrift);
545 
546     // To compute the RMS error, which is sqrt(sum square error / N), filter the squared
547     // error, which effectively returns sum squared error / N, and take the sqrt.
548     // This is done by RmsFilter::newSample().
549     const double rms = guiderRms->newSample(raDrift, decDrift);
550     addGuideStatsInternal(raDrift, decDrift, double(raPulse), double(decPulse), snr, numStars, skyBackground, drift, rms, time);
551 
552     // If capture is active, plot the capture RMS.
553     if (captureStartedTime >= 0)
554     {
555         // lastCaptureRmsTime is the last time we plotted a capture RMS value.
556         // If we have plotted values previously, and there's a gap in guiding
557         // we must place NaN values in the graph surrounding the gap.
558         if ((lastCaptureRmsTime >= 0) &&
559                 (time - lastCaptureRmsTime > MAX_GUIDE_STATS_GAP))
560         {
561             // this is the first sample in a series with a gap behind us.
562             statsPlot->graph(CAPTURE_RMS_GRAPH)->addData(lastCaptureRmsTime + .0001, qQNaN());
563             statsPlot->graph(CAPTURE_RMS_GRAPH)->addData(time - .0001, qQNaN());
564             // I can go either way on this. E.g. resetting the filter will start the RMS
565             // average over again, e.g. after a autofocus where the guider was suspended
566             // for a couple minutes. Not having it will average the new capture's guide
567             // errors with the previous capture's. This is, of course, a display decision.
568             // The actual guiding is not affected. I went with not resetting the RMS filter
569             // that only uses guiding samples during capture, and resetting the one that
570             // uses all guider samples (guiderRms above).
571             // captureRms->resetFilter();
572         }
573         const double rmsC = captureRms->newSample(raDrift, decDrift);
574         statsPlot->graph(CAPTURE_RMS_GRAPH)->addData(time, rmsC);
575         lastCaptureRmsTime = time;
576     }
577 
578     lastGuideStatsTime = time;
579 }
580 
addGuideStatsInternal(double raDrift,double decDrift,double raPulse,double decPulse,double snr,double numStars,double skyBackground,double drift,double rms,double time)581 void Analyze::addGuideStatsInternal(double raDrift, double decDrift, double raPulse,
582                                     double decPulse, double snr,
583                                     double numStars, double skyBackground,
584                                     double drift, double rms, double time)
585 {
586     statsPlot->graph(RA_GRAPH)->addData(time, raDrift);
587     statsPlot->graph(DEC_GRAPH)->addData(time, decDrift);
588     statsPlot->graph(RA_PULSE_GRAPH)->addData(time, raPulse);
589     statsPlot->graph(DEC_PULSE_GRAPH)->addData(time, decPulse);
590     statsPlot->graph(DRIFT_GRAPH)->addData(time, drift);
591     statsPlot->graph(RMS_GRAPH)->addData(time, rms);
592 
593     // Set the SNR axis' maximum to 95% of the way up from the middle to the top.
594     if (!qIsNaN(snr))
595         snrMax = std::max(snr, snrMax);
596     if (!qIsNaN(skyBackground))
597         skyBgMax = std::max(skyBackground, skyBgMax);
598     if (!qIsNaN(numStars))
599         numStarsMax = std::max(numStars, static_cast<double>(numStarsMax));
600 
601     snrAxis->setRange(-1.05 * snrMax, std::max(10.0, 1.05 * snrMax));
602     medianAxis->setRange(-1.35 * medianMax, std::max(10.0, 1.35 * medianMax));
603     numCaptureStarsAxis->setRange(-1.45 * numCaptureStarsMax, std::max(10.0, 1.45 * numCaptureStarsMax));
604     skyBgAxis->setRange(0, std::max(10.0, 1.15 * skyBgMax));
605     numStarsAxis->setRange(0, std::max(10.0, 1.25 * numStarsMax));
606 
607     statsPlot->graph(SNR_GRAPH)->addData(time, snr);
608     statsPlot->graph(NUMSTARS_GRAPH)->addData(time, numStars);
609     statsPlot->graph(SKYBG_GRAPH)->addData(time, skyBackground);
610 }
611 
addTemperature(double temperature,double time)612 void Analyze::addTemperature(double temperature, double time)
613 {
614     // The HFR corresponds to the last capture
615     statsPlot->graph(TEMPERATURE_GRAPH)->addData(time, temperature);
616 }
617 
618 // Add the HFR values to the Stats graph, as a constant value between startTime and time.
addHFR(double hfr,int numCaptureStars,int median,double eccentricity,double time,double startTime)619 void Analyze::addHFR(double hfr, int numCaptureStars, int median, double eccentricity,
620                      double time, double startTime)
621 {
622     // The HFR corresponds to the last capture
623     statsPlot->graph(HFR_GRAPH)->addData(startTime - .0001, qQNaN());
624     statsPlot->graph(HFR_GRAPH)->addData(startTime, hfr);
625     statsPlot->graph(HFR_GRAPH)->addData(time, hfr);
626     statsPlot->graph(HFR_GRAPH)->addData(time + .0001, qQNaN());
627 
628     statsPlot->graph(NUM_CAPTURE_STARS_GRAPH)->addData(startTime - .0001, qQNaN());
629     statsPlot->graph(NUM_CAPTURE_STARS_GRAPH)->addData(startTime, numCaptureStars);
630     statsPlot->graph(NUM_CAPTURE_STARS_GRAPH)->addData(time, numCaptureStars);
631     statsPlot->graph(NUM_CAPTURE_STARS_GRAPH)->addData(time + .0001, qQNaN());
632 
633     statsPlot->graph(MEDIAN_GRAPH)->addData(startTime - .0001, qQNaN());
634     statsPlot->graph(MEDIAN_GRAPH)->addData(startTime, median);
635     statsPlot->graph(MEDIAN_GRAPH)->addData(time, median);
636     statsPlot->graph(MEDIAN_GRAPH)->addData(time + .0001, qQNaN());
637 
638     statsPlot->graph(ECCENTRICITY_GRAPH)->addData(startTime - .0001, qQNaN());
639     statsPlot->graph(ECCENTRICITY_GRAPH)->addData(startTime, eccentricity);
640     statsPlot->graph(ECCENTRICITY_GRAPH)->addData(time, eccentricity);
641     statsPlot->graph(ECCENTRICITY_GRAPH)->addData(time + .0001, qQNaN());
642 
643     medianMax = std::max(median, medianMax);
644     numCaptureStarsMax = std::max(numCaptureStars, numCaptureStarsMax);
645 }
646 
647 // Add the Mount Coordinates values to the Stats graph.
648 // All but pierSide are in double degrees.
addMountCoords(double ra,double dec,double az,double alt,int pierSide,double ha,double time)649 void Analyze::addMountCoords(double ra, double dec, double az,
650                              double alt, int pierSide, double ha, double time)
651 {
652     statsPlot->graph(MOUNT_RA_GRAPH)->addData(time, ra);
653     statsPlot->graph(MOUNT_DEC_GRAPH)->addData(time, dec);
654     statsPlot->graph(MOUNT_HA_GRAPH)->addData(time, ha);
655     statsPlot->graph(AZ_GRAPH)->addData(time, az);
656     statsPlot->graph(ALT_GRAPH)->addData(time, alt);
657     statsPlot->graph(PIER_SIDE_GRAPH)->addData(time, double(pierSide));
658 }
659 
660 // Read a .analyze file, and setup all the graphics.
readDataFromFile(const QString & filename)661 double Analyze::readDataFromFile(const QString &filename)
662 {
663     double lastTime = 10;
664     QFile inputFile(filename);
665     if (inputFile.open(QIODevice::ReadOnly))
666     {
667         QTextStream in(&inputFile);
668         while (!in.atEnd())
669         {
670             QString line = in.readLine();
671             double time = processInputLine(line);
672             if (time > lastTime)
673                 lastTime = time;
674         }
675         inputFile.close();
676     }
677     return lastTime;
678 }
679 
680 // Process an input line read from a .analyze file.
processInputLine(const QString & line)681 double Analyze::processInputLine(const QString &line)
682 {
683     bool ok;
684     // Break the line into comma-separated components
685     QStringList list = line.split(QLatin1Char(','));
686     // We need at least a command and a timestamp
687     if (list.size() < 2)
688         return 0;
689     if (list[0].at(0).toLatin1() == '#')
690     {
691         // Comment character # must be at start of line.
692         return 0;
693     }
694 
695     if ((list[0] == "AnalyzeStartTime") && list.size() == 3)
696     {
697         displayStartTime = QDateTime::fromString(list[1], timeFormat);
698         startTimeInitialized = true;
699         analyzeTimeZone = list[2];
700         return 0;
701     }
702 
703     // Except for comments and the above AnalyzeStartTime, the second item
704     // in the csv line is a double which represents seconds since start of the log.
705     const double time = QString(list[1]).toDouble(&ok);
706     if (!ok)
707         return 0;
708     if (time < 0 || time > 3600 * 24 * 10)
709         return 0;
710 
711     if ((list[0] == "CaptureStarting") && (list.size() == 4))
712     {
713         const double exposureSeconds = QString(list[2]).toDouble(&ok);
714         if (!ok)
715             return 0;
716         const QString filter = list[3];
717         processCaptureStarting(time, exposureSeconds, filter, true);
718     }
719     else if ((list[0] == "CaptureComplete") && (list.size() >= 6) && (list.size() <= 9))
720     {
721         const double exposureSeconds = QString(list[2]).toDouble(&ok);
722         if (!ok)
723             return 0;
724         const QString filter = list[3];
725         const double hfr = QString(list[4]).toDouble(&ok);
726         if (!ok)
727             return 0;
728         const QString filename = list[5];
729         const int numStars = (list.size() > 6) ? QString(list[6]).toInt(&ok) : 0;
730         if (!ok)
731             return 0;
732         const int median = (list.size() > 7) ? QString(list[7]).toInt(&ok) : 0;
733         if (!ok)
734             return 0;
735         const double eccentricity = (list.size() > 8) ? QString(list[8]).toDouble(&ok) : 0;
736         if (!ok)
737             return 0;
738         processCaptureComplete(time, filename, exposureSeconds, filter, hfr, numStars, median, eccentricity, true);
739     }
740     else if ((list[0] == "CaptureAborted") && (list.size() == 3))
741     {
742         const double exposureSeconds = QString(list[2]).toDouble(&ok);
743         if (!ok)
744             return 0;
745         processCaptureAborted(time, exposureSeconds, true);
746     }
747     else if ((list[0] == "AutofocusStarting") && (list.size() == 4))
748     {
749         QString filter = list[2];
750         double temperature = QString(list[3]).toDouble(&ok);
751         if (!ok)
752             return 0;
753         processAutofocusStarting(time, temperature, filter, true);
754     }
755     else if ((list[0] == "AutofocusComplete") && (list.size() == 4))
756     {
757         QString filter = list[2];
758         QString samples = list[3];
759         processAutofocusComplete(time, filter, samples, true);
760     }
761     else if ((list[0] == "AutofocusAborted") && (list.size() == 4))
762     {
763         QString filter = list[2];
764         QString samples = list[3];
765         processAutofocusAborted(time, filter, samples, true);
766     }
767     else if ((list[0] == "GuideState") && list.size() == 3)
768     {
769         processGuideState(time, list[2], true);
770     }
771     else if ((list[0] == "GuideStats") && list.size() == 9)
772     {
773         const double ra = QString(list[2]).toDouble(&ok);
774         if (!ok)
775             return 0;
776         const double dec = QString(list[3]).toDouble(&ok);
777         if (!ok)
778             return 0;
779         const double raPulse = QString(list[4]).toInt(&ok);
780         if (!ok)
781             return 0;
782         const double decPulse = QString(list[5]).toInt(&ok);
783         if (!ok)
784             return 0;
785         const double snr = QString(list[6]).toDouble(&ok);
786         if (!ok)
787             return 0;
788         const double skyBg = QString(list[7]).toDouble(&ok);
789         if (!ok)
790             return 0;
791         const double numStars = QString(list[8]).toInt(&ok);
792         if (!ok)
793             return 0;
794         processGuideStats(time, ra, dec, raPulse, decPulse, snr, skyBg, numStars, true);
795     }
796     else if ((list[0] == "Temperature") && list.size() == 3)
797     {
798         const double temperature = QString(list[2]).toDouble(&ok);
799         if (!ok)
800             return 0;
801         processTemperature(time, temperature, true);
802     }
803     else if ((list[0] == "MountState") && list.size() == 3)
804     {
805         processMountState(time, list[2], true);
806     }
807     else if ((list[0] == "MountCoords") && (list.size() == 7 || list.size() == 8))
808     {
809         const double ra = QString(list[2]).toDouble(&ok);
810         if (!ok)
811             return 0;
812         const double dec = QString(list[3]).toDouble(&ok);
813         if (!ok)
814             return 0;
815         const double az = QString(list[4]).toDouble(&ok);
816         if (!ok)
817             return 0;
818         const double alt = QString(list[5]).toDouble(&ok);
819         if (!ok)
820             return 0;
821         const int side = QString(list[6]).toInt(&ok);
822         if (!ok)
823             return 0;
824         const double ha = (list.size() > 7) ? QString(list[7]).toDouble(&ok) : 0;
825         if (!ok)
826             return 0;
827         processMountCoords(time, ra, dec, az, alt, side, ha, true);
828     }
829     else if ((list[0] == "AlignState") && list.size() == 3)
830     {
831         processAlignState(time, list[2], true);
832     }
833     else if ((list[0] == "MeridianFlipState") && list.size() == 3)
834     {
835         processMountFlipState(time, list[2], true);
836     }
837     else
838     {
839         return 0;
840     }
841     return time;
842 }
843 
844 namespace
845 {
addDetailsRow(QTableWidget * table,const QString & col1,const QColor & color1,const QString & col2,const QColor & color2,const QString & col3="",const QColor & color3=Qt::white)846 void addDetailsRow(QTableWidget *table, const QString &col1, const QColor &color1,
847                    const QString &col2, const QColor &color2,
848                    const QString &col3 = "", const QColor &color3 = Qt::white)
849 {
850     int row = table->rowCount();
851     table->setRowCount(row + 1);
852 
853     QTableWidgetItem *item = new QTableWidgetItem();
854     item->setText(col1);
855     item->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
856     item->setForeground(color1);
857     table->setItem(row, 0, item);
858 
859     item = new QTableWidgetItem();
860     item->setText(col2);
861     item->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
862     item->setForeground(color2);
863     if (col1 == "Filename")
864     {
865         // Special Case long filenames.
866         QFont ft = item->font();
867         ft.setPointSizeF(8.0);
868         item->setFont(ft);
869     }
870     table->setItem(row, 1, item);
871 
872     if (col3.size() > 0)
873     {
874         item = new QTableWidgetItem();
875         item->setText(col3);
876         item->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
877         item->setForeground(color3);
878         table->setItem(row, 2, item);
879     }
880     else
881     {
882         // Column 1 spans 2nd and 3rd columns
883         table->setSpan(row, 1, 1, 2);
884     }
885 }
886 }
887 
888 // Helper to create tables in the details display.
889 // Start the table, displaying the heading and timing information, common to all sessions.
setupTable(const QString & name,const QString & status,const QDateTime & startClock,const QDateTime & endClock,QTableWidget * table)890 void Analyze::Session::setupTable(const QString &name, const QString &status,
891                                   const QDateTime &startClock, const QDateTime &endClock, QTableWidget *table)
892 {
893     details = table;
894     details->clear();
895     details->setRowCount(0);
896     details->setEditTriggers(QAbstractItemView::NoEditTriggers);
897     details->setColumnCount(3);
898     details->verticalHeader()->setDefaultSectionSize(20);
899     details->horizontalHeader()->setStretchLastSection(true);
900     details->setColumnWidth(0, 100);
901     details->setColumnWidth(1, 100);
902     details->setShowGrid(false);
903     details->setWordWrap(true);
904     details->horizontalHeader()->hide();
905     details->verticalHeader()->hide();
906 
907     QString startDateStr = startClock.toString("dd.MM.yyyy");
908     QString startTimeStr = startClock.toString("hh:mm:ss");
909     QString endTimeStr = isTemporary() ? "Ongoing"
910                          : endClock.toString("hh:mm:ss");
911 
912     addDetailsRow(details, name, Qt::yellow, status, Qt::yellow);
913     addDetailsRow(details, "Date", Qt::yellow, startDateStr, Qt::white);
914     addDetailsRow(details, "Interval", Qt::yellow, QString::number(start, 'f', 3), Qt::white,
915                   isTemporary() ? "Ongoing" : QString::number(end, 'f', 3), Qt::white);
916     addDetailsRow(details, "Clock", Qt::yellow, startTimeStr, Qt::white, endTimeStr, Qt::white);
917     addDetailsRow(details, "Duration", Qt::yellow, QString::number(end - start, 'f', 1), Qt::white);
918 }
919 
920 // Add a new row to the table, which is specific to the particular Timeline line.
addRow(const QString & key,const QString & value)921 void Analyze::Session::addRow(const QString &key, const QString &value)
922 {
923     addDetailsRow(details, key, Qt::yellow, value, Qt::white);
924 }
925 
isTemporary() const926 bool Analyze::Session::isTemporary() const
927 {
928     return rect != nullptr;
929 }
930 
931 // The focus session parses the "pipe-separate-values" list of positions
932 // and HFRs given it, eventually to be used to plot the focus v-curve.
FocusSession(double start_,double end_,QCPItemRect * rect,bool ok,double temperature_,const QString & filter_,const QString & points_)933 Analyze::FocusSession::FocusSession(double start_, double end_, QCPItemRect *rect, bool ok, double temperature_,
934                                     const QString &filter_, const QString &points_)
935     : Session(start_, end_, FOCUS_Y, rect), success(ok),
936       temperature(temperature_), filter(filter_), points(points_)
937 {
938     const QStringList list = points.split(QLatin1Char('|'));
939     const int size = list.size();
940     // Size can be 1 if points_ is an empty string.
941     if (size < 2)
942         return;
943 
944     for (int i = 0; i < size; )
945     {
946         bool parsed1, parsed2;
947         int position = QString(list[i++]).toInt(&parsed1);
948         if (i >= size)
949             break;
950         double hfr = QString(list[i++]).toDouble(&parsed2);
951         if (!parsed1 || !parsed2)
952         {
953             positions.clear();
954             hfrs.clear();
955             fprintf(stderr, "Bad focus position %d in %s\n", i - 2, points.toLatin1().data());
956             return;
957         }
958         positions.push_back(position);
959         hfrs.push_back(hfr);
960     }
961 }
962 
963 // When the user clicks on a particular capture session in the timeline,
964 // a table is rendered in the details section, and, if it was a double click,
965 // the fits file is displayed, if it can be found.
captureSessionClicked(CaptureSession & c,bool doubleClick)966 void Analyze::captureSessionClicked(CaptureSession &c, bool doubleClick)
967 {
968     highlightTimelineItem(c.offset, c.start, c.end);
969 
970     if (c.isTemporary())
971         c.setupTable("Capture", "in progress", clockTime(c.start), clockTime(c.start), detailsTable);
972     else if (c.aborted)
973         c.setupTable("Capture", "ABORTED", clockTime(c.start), clockTime(c.end), detailsTable);
974     else
975         c.setupTable("Capture", "successful", clockTime(c.start), clockTime(c.end), detailsTable);
976 
977     c.addRow("Filter", c.filter);
978 
979     double raRMS, decRMS, totalRMS;
980     int numSamples;
981     displayGuideGraphics(c.start, c.end, &raRMS, &decRMS, &totalRMS, &numSamples);
982     if (numSamples > 0)
983         c.addRow("GuideRMS", QString::number(totalRMS, 'f', 2));
984 
985     c.addRow("Exposure", QString::number(c.duration, 'f', 2));
986     if (!c.isTemporary())
987         c.addRow("Filename", c.filename);
988 
989     if (doubleClick && !c.isTemporary())
990     {
991         QString filename = findFilename(c.filename, alternateFolder);
992         if (filename.size() > 0)
993             displayFITS(filename);
994         else
995         {
996             QString message = i18n("Could not find image file: %1", c.filename);
997             KSNotification::sorry(message, i18n("Invalid URL"));
998         }
999     }
1000 }
1001 
1002 // When the user clicks on a focus session in the timeline,
1003 // a table is rendered in the details section, and the HFR/position plot
1004 // is displayed in the graphics plot. If focus is ongoing
1005 // the information for the graphics is not plotted as it is not yet available.
focusSessionClicked(FocusSession & c,bool doubleClick)1006 void Analyze::focusSessionClicked(FocusSession &c, bool doubleClick)
1007 {
1008     Q_UNUSED(doubleClick);
1009     highlightTimelineItem(c.offset, c.start, c.end);
1010 
1011     if (c.success)
1012         c.setupTable("Focus", "successful", clockTime(c.start), clockTime(c.end), detailsTable);
1013     else if (c.isTemporary())
1014         c.setupTable("Focus", "in progress", clockTime(c.start), clockTime(c.start), detailsTable);
1015     else
1016         c.setupTable("Focus", "FAILED", clockTime(c.start), clockTime(c.end), detailsTable);
1017 
1018     if (!c.isTemporary())
1019     {
1020         if (c.success)
1021         {
1022             if (c.hfrs.size() > 0)
1023                 c.addRow("HFR", QString::number(c.hfrs.last(), 'f', 2));
1024             if (c.positions.size() > 0)
1025                 c.addRow("Solution", QString::number(c.positions.last(), 'f', 0));
1026         }
1027         c.addRow("Iterations", QString::number(c.positions.size()));
1028     }
1029     c.addRow("Filter", c.filter);
1030     c.addRow("Temperature", QString::number(c.temperature, 'f', 1));
1031 
1032     if (c.isTemporary())
1033         resetGraphicsPlot();
1034     else
1035         displayFocusGraphics(c.positions, c.hfrs, c.success);
1036 }
1037 
1038 // When the user clicks on a guide session in the timeline,
1039 // a table is rendered in the details section. If it has a G_GUIDING state
1040 // then a drift plot is generated and  RMS values are calculated
1041 // for the guiding session's time interval.
guideSessionClicked(GuideSession & c,bool doubleClick)1042 void Analyze::guideSessionClicked(GuideSession &c, bool doubleClick)
1043 {
1044     Q_UNUSED(doubleClick);
1045     highlightTimelineItem(GUIDE_Y, c.start, c.end);
1046 
1047     QString st;
1048     if (c.simpleState == G_IDLE)
1049         st = "Idle";
1050     else if (c.simpleState == G_GUIDING)
1051         st = "Guiding";
1052     else if (c.simpleState == G_CALIBRATING)
1053         st = "Calibrating";
1054     else if (c.simpleState == G_SUSPENDED)
1055         st = "Suspended";
1056     else if (c.simpleState == G_DITHERING)
1057         st = "Dithering";
1058 
1059     c.setupTable("Guide", st, clockTime(c.start), clockTime(c.end), detailsTable);
1060     resetGraphicsPlot();
1061     if (c.simpleState == G_GUIDING)
1062     {
1063         double raRMS, decRMS, totalRMS;
1064         int numSamples;
1065         displayGuideGraphics(c.start, c.end, &raRMS, &decRMS, &totalRMS, &numSamples);
1066         if (numSamples > 0)
1067         {
1068             c.addRow("total RMS", QString::number(totalRMS, 'f', 2));
1069             c.addRow("ra RMS", QString::number(raRMS, 'f', 2));
1070             c.addRow("dec RMS", QString::number(decRMS, 'f', 2));
1071         }
1072         c.addRow("Num Samples", QString::number(numSamples));
1073     }
1074 }
1075 
displayGuideGraphics(double start,double end,double * raRMS,double * decRMS,double * totalRMS,int * numSamples)1076 void Analyze::displayGuideGraphics(double start, double end, double *raRMS,
1077                                    double *decRMS, double *totalRMS, int *numSamples)
1078 {
1079     resetGraphicsPlot();
1080     auto ra = statsPlot->graph(RA_GRAPH)->data()->findBegin(start);
1081     auto dec = statsPlot->graph(DEC_GRAPH)->data()->findBegin(start);
1082     auto raEnd = statsPlot->graph(RA_GRAPH)->data()->findEnd(end);
1083     auto decEnd = statsPlot->graph(DEC_GRAPH)->data()->findEnd(end);
1084     int num = 0;
1085     double raSquareErrorSum = 0, decSquareErrorSum = 0;
1086     while (ra != raEnd && dec != decEnd &&
1087             ra->mainKey() < end && dec->mainKey() < end &&
1088             ra != statsPlot->graph(RA_GRAPH)->data()->constEnd() &&
1089             dec != statsPlot->graph(DEC_GRAPH)->data()->constEnd() &&
1090             ra->mainKey() < end && dec->mainKey() < end)
1091     {
1092         const double raVal = ra->mainValue();
1093         const double decVal = dec->mainValue();
1094         graphicsPlot->graph(GUIDER_GRAPHICS)->addData(raVal, decVal);
1095         if (!qIsNaN(raVal) && !qIsNaN(decVal))
1096         {
1097             raSquareErrorSum += raVal * raVal;
1098             decSquareErrorSum += decVal * decVal;
1099             num++;
1100         }
1101         ra++;
1102         dec++;
1103     }
1104     if (numSamples != nullptr)
1105         *numSamples = num;
1106     if (num > 0)
1107     {
1108         if (raRMS != nullptr)
1109             *raRMS = sqrt(raSquareErrorSum / num);
1110         if (decRMS != nullptr)
1111             *decRMS = sqrt(decSquareErrorSum / num);
1112         if (totalRMS != nullptr)
1113             *totalRMS = sqrt((raSquareErrorSum + decSquareErrorSum) / num);
1114         if (numSamples != nullptr)
1115             *numSamples = num;
1116     }
1117     QCPItemEllipse *c1 = new QCPItemEllipse(graphicsPlot);
1118     c1->bottomRight->setCoords(1.0, -1.0);
1119     c1->topLeft->setCoords(-1.0, 1.0);
1120     QCPItemEllipse *c2 = new QCPItemEllipse(graphicsPlot);
1121     c2->bottomRight->setCoords(2.0, -2.0);
1122     c2->topLeft->setCoords(-2.0, 2.0);
1123     c1->setPen(QPen(Qt::green));
1124     c2->setPen(QPen(Qt::yellow));
1125 
1126     // Since the plot is wider than it is tall, these lines set the
1127     // vertical range to 2.5, and the horizontal range to whatever it
1128     // takes to keep the two axes' scales (number of pixels per value)
1129     // the same, so that circles stay circular (i.e. circles are not stretch
1130     // wide even though the graph area is not square).
1131     graphicsPlot->xAxis->setRange(-2.5, 2.5);
1132     graphicsPlot->yAxis->setRange(-2.5, 2.5);
1133     graphicsPlot->xAxis->setScaleRatio(graphicsPlot->yAxis);
1134 }
1135 
1136 // When the user clicks on a particular mount session in the timeline,
1137 // a table is rendered in the details section.
mountSessionClicked(MountSession & c,bool doubleClick)1138 void Analyze::mountSessionClicked(MountSession &c, bool doubleClick)
1139 {
1140     Q_UNUSED(doubleClick);
1141     highlightTimelineItem(MOUNT_Y, c.start, c.end);
1142 
1143     c.setupTable("Mount", mountStatusString(c.state), clockTime(c.start),
1144                  clockTime(c.isTemporary() ? c.start : c.end), detailsTable);
1145 }
1146 
1147 // When the user clicks on a particular align session in the timeline,
1148 // a table is rendered in the details section.
alignSessionClicked(AlignSession & c,bool doubleClick)1149 void Analyze::alignSessionClicked(AlignSession &c, bool doubleClick)
1150 {
1151     Q_UNUSED(doubleClick);
1152     highlightTimelineItem(ALIGN_Y, c.start, c.end);
1153     c.setupTable("Align", getAlignStatusString(c.state), clockTime(c.start),
1154                  clockTime(c.isTemporary() ? c.start : c.end), detailsTable);
1155 }
1156 
1157 // When the user clicks on a particular meridian flip session in the timeline,
1158 // a table is rendered in the details section.
mountFlipSessionClicked(MountFlipSession & c,bool doubleClick)1159 void Analyze::mountFlipSessionClicked(MountFlipSession &c, bool doubleClick)
1160 {
1161     Q_UNUSED(doubleClick);
1162     highlightTimelineItem(MERIDIAN_FLIP_Y, c.start, c.end);
1163     c.setupTable("Meridian Flip", Mount::meridianFlipStatusString(c.state),
1164                  clockTime(c.start), clockTime(c.isTemporary() ? c.start : c.end), detailsTable);
1165 }
1166 
1167 // This method determines which timeline session (if any) was selected
1168 // when the user clicks in the Timeline plot. It also sets a cursor
1169 // in the stats plot.
processTimelineClick(QMouseEvent * event,bool doubleClick)1170 void Analyze::processTimelineClick(QMouseEvent *event, bool doubleClick)
1171 {
1172     unhighlightTimelineItem();
1173     double xval = timelinePlot->xAxis->pixelToCoord(event->x());
1174     double yval = timelinePlot->yAxis->pixelToCoord(event->y());
1175     if (yval >= CAPTURE_Y - 0.5 && yval <= CAPTURE_Y + 0.5)
1176     {
1177         QList<CaptureSession> candidates = captureSessions.find(xval);
1178         if (candidates.size() > 0)
1179             captureSessionClicked(candidates[0], doubleClick);
1180         else if ((temporaryCaptureSession.rect != nullptr) &&
1181                  (xval > temporaryCaptureSession.start))
1182             captureSessionClicked(temporaryCaptureSession, doubleClick);
1183     }
1184     else if (yval >= FOCUS_Y - 0.5 && yval <= FOCUS_Y + 0.5)
1185     {
1186         QList<FocusSession> candidates = focusSessions.find(xval);
1187         if (candidates.size() > 0)
1188             focusSessionClicked(candidates[0], doubleClick);
1189         else if ((temporaryFocusSession.rect != nullptr) &&
1190                  (xval > temporaryFocusSession.start))
1191             focusSessionClicked(temporaryFocusSession, doubleClick);
1192     }
1193     else if (yval >= GUIDE_Y - 0.5 && yval <= GUIDE_Y + 0.5)
1194     {
1195         QList<GuideSession> candidates = guideSessions.find(xval);
1196         if (candidates.size() > 0)
1197             guideSessionClicked(candidates[0], doubleClick);
1198         else if ((temporaryGuideSession.rect != nullptr) &&
1199                  (xval > temporaryGuideSession.start))
1200             guideSessionClicked(temporaryGuideSession, doubleClick);
1201     }
1202     else if (yval >= MOUNT_Y - 0.5 && yval <= MOUNT_Y + 0.5)
1203     {
1204         QList<MountSession> candidates = mountSessions.find(xval);
1205         if (candidates.size() > 0)
1206             mountSessionClicked(candidates[0], doubleClick);
1207         else if ((temporaryMountSession.rect != nullptr) &&
1208                  (xval > temporaryMountSession.start))
1209             mountSessionClicked(temporaryMountSession, doubleClick);
1210     }
1211     else if (yval >= ALIGN_Y - 0.5 && yval <= ALIGN_Y + 0.5)
1212     {
1213         QList<AlignSession> candidates = alignSessions.find(xval);
1214         if (candidates.size() > 0)
1215             alignSessionClicked(candidates[0], doubleClick);
1216         else if ((temporaryAlignSession.rect != nullptr) &&
1217                  (xval > temporaryAlignSession.start))
1218             alignSessionClicked(temporaryAlignSession, doubleClick);
1219     }
1220     else if (yval >= MERIDIAN_FLIP_Y - 0.5 && yval <= MERIDIAN_FLIP_Y + 0.5)
1221     {
1222         QList<MountFlipSession> candidates = mountFlipSessions.find(xval);
1223         if (candidates.size() > 0)
1224             mountFlipSessionClicked(candidates[0], doubleClick);
1225         else if ((temporaryMountFlipSession.rect != nullptr) &&
1226                  (xval > temporaryMountFlipSession.start))
1227             mountFlipSessionClicked(temporaryMountFlipSession, doubleClick);
1228     }
1229     setStatsCursor(xval);
1230     replot();
1231 }
1232 
setStatsCursor(double time)1233 void Analyze::setStatsCursor(double time)
1234 {
1235     removeStatsCursor();
1236     QCPItemLine *line = new QCPItemLine(statsPlot);
1237     line->setPen(QPen(Qt::darkGray, 1, Qt::SolidLine));
1238     double top = statsPlot->yAxis->range().upper;
1239     line->start->setCoords(time, 0);
1240     line->end->setCoords(time, top);
1241     statsCursor = line;
1242     cursorTimeOut->setText(QString("%1s").arg(time));
1243     cursorClockTimeOut->setText(QString("%1")
1244                                 .arg(clockTime(time).toString("hh:mm:ss")));
1245     statsCursorTime = time;
1246     keepCurrentCB->setCheckState(Qt::Unchecked);
1247 }
1248 
removeStatsCursor()1249 void Analyze::removeStatsCursor()
1250 {
1251     if (statsCursor != nullptr)
1252         statsPlot->removeItem(statsCursor);
1253     statsCursor = nullptr;
1254     cursorTimeOut->setText("");
1255     cursorClockTimeOut->setText("");
1256     statsCursorTime = -1;
1257 }
1258 
1259 // When the users clicks in the stats plot, the cursor is set at the corresponding time.
processStatsClick(QMouseEvent * event,bool doubleClick)1260 void Analyze::processStatsClick(QMouseEvent *event, bool doubleClick)
1261 {
1262     Q_UNUSED(doubleClick);
1263     double xval = statsPlot->xAxis->pixelToCoord(event->x());
1264     if (event->button() == Qt::RightButton || event->modifiers() == Qt::ControlModifier)
1265         // Resets the range. Replot will take care of ra/dec needing negative values.
1266         statsPlot->yAxis->setRange(0, 5);
1267     else
1268         setStatsCursor(xval);
1269     replot();
1270 }
1271 
timelineMousePress(QMouseEvent * event)1272 void Analyze::timelineMousePress(QMouseEvent *event)
1273 {
1274     processTimelineClick(event, false);
1275 }
1276 
timelineMouseDoubleClick(QMouseEvent * event)1277 void Analyze::timelineMouseDoubleClick(QMouseEvent *event)
1278 {
1279     processTimelineClick(event, true);
1280 }
1281 
statsMousePress(QMouseEvent * event)1282 void Analyze::statsMousePress(QMouseEvent *event)
1283 {
1284     // If we're on the legend, adjust the y-axis.
1285     if (statsPlot->xAxis->pixelToCoord(event->x()) < plotStart)
1286     {
1287         yAxisInitialPos = statsPlot->yAxis->pixelToCoord(event->y());
1288         return;
1289     }
1290     processStatsClick(event, false);
1291 }
1292 
statsMouseDoubleClick(QMouseEvent * event)1293 void Analyze::statsMouseDoubleClick(QMouseEvent *event)
1294 {
1295     processStatsClick(event, true);
1296 }
1297 
1298 // Allow the user to click and hold, causing the cursor to move in real-time.
statsMouseMove(QMouseEvent * event)1299 void Analyze::statsMouseMove(QMouseEvent *event)
1300 {
1301     // If we're on the legend, adjust the y-axis.
1302     if (statsPlot->xAxis->pixelToCoord(event->x()) < plotStart)
1303     {
1304         auto range = statsPlot->yAxis->range();
1305         double yDiff = yAxisInitialPos - statsPlot->yAxis->pixelToCoord(event->y());
1306         statsPlot->yAxis->setRange(range.lower + yDiff, range.upper + yDiff);
1307         replot();
1308         return;
1309     }
1310     processStatsClick(event, false);
1311 }
1312 
1313 // Called by the scrollbar, to move the current view.
scroll(int value)1314 void Analyze::scroll(int value)
1315 {
1316     double pct = static_cast<double>(value) / MAX_SCROLL_VALUE;
1317     plotStart = std::max(0.0, maxXValue * pct - plotWidth / 2.0);
1318     // Normally replot adjusts the position of the slider.
1319     // If the user has done that, we don't want replot to re-do it.
1320     replot(false);
1321 
1322 }
scrollRight()1323 void Analyze::scrollRight()
1324 {
1325     plotStart = std::min(maxXValue - plotWidth / 5, plotStart + plotWidth / 5);
1326     fullWidthCB->setChecked(false);
1327     replot();
1328 
1329 }
scrollLeft()1330 void Analyze::scrollLeft()
1331 {
1332     plotStart = std::max(0.0, plotStart - plotWidth / 5);
1333     fullWidthCB->setChecked(false);
1334     replot();
1335 
1336 }
replot(bool adjustSlider)1337 void Analyze::replot(bool adjustSlider)
1338 {
1339     adjustTemporarySessions();
1340     if (fullWidthCB->isChecked())
1341     {
1342         plotStart = 0;
1343         plotWidth = std::max(10.0, maxXValue);
1344     }
1345     else if (keepCurrentCB->isChecked())
1346     {
1347         plotStart = std::max(0.0, maxXValue - plotWidth);
1348     }
1349     // If we're keeping to the latest values,
1350     // set the time display to the latest time.
1351     if (keepCurrentCB->isChecked() && statsCursor == nullptr)
1352     {
1353         cursorTimeOut->setText(QString("%1s").arg(maxXValue));
1354         cursorClockTimeOut->setText(QString("%1")
1355                                     .arg(clockTime(maxXValue).toString("hh:mm:ss")));
1356     }
1357     analyzeSB->setPageStep(
1358         std::min(MAX_SCROLL_VALUE,
1359                  static_cast<int>(MAX_SCROLL_VALUE * plotWidth / maxXValue)));
1360     if (adjustSlider)
1361     {
1362         double sliderCenter = plotStart + plotWidth / 2.0;
1363         analyzeSB->setSliderPosition(MAX_SCROLL_VALUE * (sliderCenter / maxXValue));
1364     }
1365 
1366     timelinePlot->xAxis->setRange(plotStart, plotStart + plotWidth);
1367     timelinePlot->yAxis->setRange(0, LAST_Y);
1368 
1369     statsPlot->xAxis->setRange(plotStart, plotStart + plotWidth);
1370 
1371     // Don't reset the range if the user has changed it.
1372     auto yRange = statsPlot->yAxis->range();
1373     if ((yRange.lower == 0 || yRange.lower == -2) && (yRange.upper == 5))
1374     {
1375         // Only need negative numbers on the stats plot if we're plotting RA or DEC
1376         if (raCB->isChecked() || decCB->isChecked() || raPulseCB->isChecked() || decPulseCB->isChecked())
1377             statsPlot->yAxis->setRange(-2, 5);
1378         else
1379             statsPlot->yAxis->setRange(0, 5);
1380     }
1381 
1382     dateTicker->setOffset(displayStartTime.toMSecsSinceEpoch() / 1000.0);
1383 
1384     timelinePlot->replot();
1385     statsPlot->replot();
1386     graphicsPlot->replot();
1387     updateStatsValues();
1388 }
1389 
1390 namespace
1391 {
1392 // Pass in a function that converts the double graph value to a string
1393 // for the value box.
1394 template<typename Func>
updateStat(double time,QLineEdit * valueBox,QCPGraph * graph,Func func,bool useLastRealVal=false)1395 void updateStat(double time, QLineEdit *valueBox, QCPGraph *graph, Func func, bool useLastRealVal = false)
1396 {
1397     auto begin = graph->data()->findBegin(time);
1398     double timeDiffThreshold = 10000000.0;
1399     if ((begin != graph->data()->constEnd()) &&
1400             (fabs(begin->mainKey() - time) < timeDiffThreshold))
1401     {
1402         double foundVal = begin->mainValue();
1403         valueBox->setDisabled(false);
1404         if (qIsNaN(foundVal))
1405         {
1406             int index = graph->findBegin(time);
1407             const double MAX_TIME_DIFF = 600;
1408             while (useLastRealVal && index >= 0)
1409             {
1410                 const double val = graph->data()->at(index)->mainValue();
1411                 const double t = graph->data()->at(index)->mainKey();
1412                 if (time - t > MAX_TIME_DIFF)
1413                     break;
1414                 if (!qIsNaN(val))
1415                 {
1416                     valueBox->setText(func(val));
1417                     return;
1418                 }
1419                 index--;
1420             }
1421             valueBox->clear();
1422         }
1423         else
1424             valueBox->setText(func(foundVal));
1425     }
1426     else valueBox->setDisabled(true);
1427 }
1428 
1429 }  // namespace
1430 
1431 // This populates the output boxes below the stats plot with the correct statistics.
updateStatsValues()1432 void Analyze::updateStatsValues()
1433 {
1434     const double time = statsCursorTime < 0 ? maxXValue : statsCursorTime;
1435 
1436     auto d2Fcn = [](double d) -> QString { return QString::number(d, 'f', 2); };
1437     // HFR, numCaptureStars, median & eccentricity are the only ones to use the last real value,
1438     // that is, it keeps those values from the last exposure.
1439     updateStat(time, hfrOut, statsPlot->graph(HFR_GRAPH), d2Fcn, true);
1440     updateStat(time, eccentricityOut, statsPlot->graph(ECCENTRICITY_GRAPH), d2Fcn, true);
1441     updateStat(time, skyBgOut, statsPlot->graph(SKYBG_GRAPH), d2Fcn);
1442     updateStat(time, snrOut, statsPlot->graph(SNR_GRAPH), d2Fcn);
1443     updateStat(time, raOut, statsPlot->graph(RA_GRAPH), d2Fcn);
1444     updateStat(time, decOut, statsPlot->graph(DEC_GRAPH), d2Fcn);
1445     updateStat(time, driftOut, statsPlot->graph(DRIFT_GRAPH), d2Fcn);
1446     updateStat(time, rmsOut, statsPlot->graph(RMS_GRAPH), d2Fcn);
1447     updateStat(time, rmsCOut, statsPlot->graph(CAPTURE_RMS_GRAPH), d2Fcn);
1448     updateStat(time, azOut, statsPlot->graph(AZ_GRAPH), d2Fcn);
1449     updateStat(time, altOut, statsPlot->graph(ALT_GRAPH), d2Fcn);
1450     updateStat(time, temperatureOut, statsPlot->graph(TEMPERATURE_GRAPH), d2Fcn);
1451 
1452     auto hmsFcn = [](double d) -> QString
1453     {
1454         dms ra;
1455         ra.setD(d);
1456         return QString("%1:%2:%3").arg(ra.hour()).arg(ra.minute()).arg(ra.second());
1457         //return ra.toHMSString();
1458     };
1459     updateStat(time, mountRaOut, statsPlot->graph(MOUNT_RA_GRAPH), hmsFcn);
1460     auto dmsFcn = [](double d) -> QString { dms dec; dec.setD(d); return dec.toDMSString(); };
1461     updateStat(time, mountDecOut, statsPlot->graph(MOUNT_DEC_GRAPH), dmsFcn);
1462     auto haFcn = [](double d) -> QString
1463     {
1464         dms ha;
1465         QChar z('0');
1466         QChar sgn('+');
1467         ha.setD(d);
1468         if (ha.Hours() > 12.0)
1469         {
1470             ha.setH(24.0 - ha.Hours());
1471             sgn = '-';
1472         }
1473         return QString("%1%2:%3").arg(sgn).arg(ha.hour(), 2, 10, z)
1474         .arg(ha.minute(), 2, 10, z);
1475     };
1476     updateStat(time, mountHaOut, statsPlot->graph(MOUNT_HA_GRAPH), haFcn);
1477 
1478     auto intFcn = [](double d) -> QString { return QString::number(d, 'f', 0); };
1479     updateStat(time, numStarsOut, statsPlot->graph(NUMSTARS_GRAPH), intFcn);
1480     updateStat(time, raPulseOut, statsPlot->graph(RA_PULSE_GRAPH), intFcn);
1481     updateStat(time, decPulseOut, statsPlot->graph(DEC_PULSE_GRAPH), intFcn);
1482     updateStat(time, numCaptureStarsOut, statsPlot->graph(NUM_CAPTURE_STARS_GRAPH), intFcn, true);
1483     updateStat(time, medianOut, statsPlot->graph(MEDIAN_GRAPH), intFcn, true);
1484 
1485 
1486     auto pierFcn = [](double d) -> QString
1487     {
1488         return d == 0.0 ? "W->E" : d == 1.0 ? "E->W" : "?";
1489     };
1490     updateStat(time, pierSideOut, statsPlot->graph(PIER_SIDE_GRAPH), pierFcn);
1491 }
1492 
initStatsCheckboxes()1493 void Analyze::initStatsCheckboxes()
1494 {
1495     hfrCB->setChecked(Options::analyzeHFR());
1496     numCaptureStarsCB->setChecked(Options::analyzeNumCaptureStars());
1497     medianCB->setChecked(Options::analyzeMedian());
1498     eccentricityCB->setChecked(Options::analyzeEccentricity());
1499     numStarsCB->setChecked(Options::analyzeNumStars());
1500     skyBgCB->setChecked(Options::analyzeSkyBg());
1501     snrCB->setChecked(Options::analyzeSNR());
1502     temperatureCB->setChecked(Options::analyzeTemperature());
1503     raCB->setChecked(Options::analyzeRA());
1504     decCB->setChecked(Options::analyzeDEC());
1505     raPulseCB->setChecked(Options::analyzeRAp());
1506     decPulseCB->setChecked(Options::analyzeDECp());
1507     driftCB->setChecked(Options::analyzeDrift());
1508     rmsCB->setChecked(Options::analyzeRMS());
1509     rmsCCB->setChecked(Options::analyzeRMSC());
1510     mountRaCB->setChecked(Options::analyzeMountRA());
1511     mountDecCB->setChecked(Options::analyzeMountDEC());
1512     mountHaCB->setChecked(Options::analyzeMountHA());
1513     azCB->setChecked(Options::analyzeAz());
1514     altCB->setChecked(Options::analyzeAlt());
1515     pierSideCB->setChecked(Options::analyzePierSide());
1516 }
1517 
zoomIn()1518 void Analyze::zoomIn()
1519 {
1520     if (plotWidth > 0.5)
1521     {
1522         if (keepCurrentCB->isChecked())
1523             // If we're keeping to the end of the data, keep the end on the right.
1524             plotStart = std::max(0.0, maxXValue - plotWidth / 4.0);
1525         else if (statsCursorTime >= 0)
1526             // If there is a cursor, try to move it to the center.
1527             plotStart = std::max(0.0, statsCursorTime - plotWidth / 4.0);
1528         else
1529             // Keep the center the same.
1530             plotStart += plotWidth / 4.0;
1531         plotWidth = plotWidth / 2.0;
1532     }
1533     fullWidthCB->setChecked(false);
1534     replot();
1535 }
1536 
zoomOut()1537 void Analyze::zoomOut()
1538 {
1539     if (plotWidth < maxXValue)
1540     {
1541         plotStart = std::max(0.0, plotStart - plotWidth / 2.0);
1542         plotWidth = plotWidth * 2;
1543     }
1544     fullWidthCB->setChecked(false);
1545     replot();
1546 }
1547 
1548 namespace
1549 {
1550 // Generic initialization of a plot, applied to all plots in this tab.
initQCP(QCustomPlot * plot)1551 void initQCP(QCustomPlot *plot)
1552 {
1553     plot->setBackground(QBrush(Qt::black));
1554     plot->xAxis->setBasePen(QPen(Qt::white, 1));
1555     plot->yAxis->setBasePen(QPen(Qt::white, 1));
1556     plot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140, 140), 1, Qt::DotLine));
1557     plot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140, 140), 1, Qt::DotLine));
1558     plot->xAxis->grid()->setSubGridPen(QPen(QColor(40, 40, 40), 1, Qt::DotLine));
1559     plot->yAxis->grid()->setSubGridPen(QPen(QColor(40, 40, 40), 1, Qt::DotLine));
1560     plot->xAxis->grid()->setZeroLinePen(Qt::NoPen);
1561     plot->yAxis->grid()->setZeroLinePen(QPen(Qt::white, 1));
1562     plot->xAxis->setBasePen(QPen(Qt::white, 1));
1563     plot->yAxis->setBasePen(QPen(Qt::white, 1));
1564     plot->xAxis->setTickPen(QPen(Qt::white, 1));
1565     plot->yAxis->setTickPen(QPen(Qt::white, 1));
1566     plot->xAxis->setSubTickPen(QPen(Qt::white, 1));
1567     plot->yAxis->setSubTickPen(QPen(Qt::white, 1));
1568     plot->xAxis->setTickLabelColor(Qt::white);
1569     plot->yAxis->setTickLabelColor(Qt::white);
1570     plot->xAxis->setLabelColor(Qt::white);
1571     plot->yAxis->setLabelColor(Qt::white);
1572 }
1573 }  // namespace
1574 
initTimelinePlot()1575 void Analyze::initTimelinePlot()
1576 {
1577     initQCP(timelinePlot);
1578 
1579     // This places the labels on the left of the timeline.
1580     QSharedPointer<QCPAxisTickerText> textTicker(new QCPAxisTickerText);
1581     textTicker->addTick(CAPTURE_Y, i18n("Capture"));
1582     textTicker->addTick(FOCUS_Y, i18n("Focus"));
1583     textTicker->addTick(ALIGN_Y, i18n("Align"));
1584     textTicker->addTick(GUIDE_Y, i18n("Guide"));
1585     textTicker->addTick(MERIDIAN_FLIP_Y, i18n("Flip"));
1586     textTicker->addTick(MOUNT_Y, i18n("Mount"));
1587     timelinePlot->yAxis->setTicker(textTicker);
1588 }
1589 
1590 // Turn on and off the various statistics, adding/removing them from the legend.
toggleGraph(int graph_id,bool show)1591 void Analyze::toggleGraph(int graph_id, bool show)
1592 {
1593     statsPlot->graph(graph_id)->setVisible(show);
1594     if (show)
1595         statsPlot->graph(graph_id)->addToLegend();
1596     else
1597         statsPlot->graph(graph_id)->removeFromLegend();
1598     replot();
1599 }
1600 
initGraph(QCustomPlot * plot,QCPAxis * yAxis,QCPGraph::LineStyle lineStyle,const QColor & color,const QString & name)1601 int Analyze::initGraph(QCustomPlot *plot, QCPAxis *yAxis, QCPGraph::LineStyle lineStyle,
1602                        const QColor &color, const QString &name)
1603 {
1604     int num = plot->graphCount();
1605     plot->addGraph(plot->xAxis, yAxis);
1606     plot->graph(num)->setLineStyle(lineStyle);
1607     plot->graph(num)->setPen(QPen(color));
1608     plot->graph(num)->setName(name);
1609     return num;
1610 }
1611 
1612 template <typename Func>
initGraphAndCB(QCustomPlot * plot,QCPAxis * yAxis,QCPGraph::LineStyle lineStyle,const QColor & color,const QString & name,QCheckBox * cb,Func setCb)1613 int Analyze::initGraphAndCB(QCustomPlot *plot, QCPAxis *yAxis, QCPGraph::LineStyle lineStyle,
1614                             const QColor &color, const QString &name, QCheckBox *cb, Func setCb)
1615 
1616 {
1617     const int num = initGraph(plot, yAxis, lineStyle, color, name);
1618     if (cb != nullptr)
1619     {
1620         // Don't call toggleGraph() here, as it's too early for replot().
1621         bool show = cb->isChecked();
1622         plot->graph(num)->setVisible(show);
1623         if (show)
1624             plot->graph(num)->addToLegend();
1625         else
1626             plot->graph(num)->removeFromLegend();
1627 
1628         connect(cb, &QCheckBox::toggled,
1629                 [ = ](bool show)
1630         {
1631             this->toggleGraph(num, show);
1632             setCb(show);
1633         });
1634     }
1635     return num;
1636 }
1637 
initStatsPlot()1638 void Analyze::initStatsPlot()
1639 {
1640     initQCP(statsPlot);
1641 
1642     // Setup the legend
1643     statsPlot->legend->setVisible(true);
1644     statsPlot->legend->setFont(QFont("Helvetica", 6));
1645     statsPlot->legend->setTextColor(Qt::white);
1646     // Legend background is black and ~75% opaque.
1647     statsPlot->legend->setBrush(QBrush(QColor(0, 0, 0, 190)));
1648     // Legend stacks vertically.
1649     statsPlot->legend->setFillOrder(QCPLegend::foRowsFirst);
1650     // Rows pretty tightly packed.
1651     statsPlot->legend->setRowSpacing(-3);
1652     statsPlot->axisRect()->insetLayout()->setInsetAlignment(0, Qt::AlignLeft | Qt::AlignTop);
1653 
1654     // Add the graphs.
1655 
1656     HFR_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsStepRight, Qt::cyan, "HFR", hfrCB,
1657                                Options::setAnalyzeHFR);
1658     connect(hfrCB, &QCheckBox::clicked,
1659             [ = ](bool show)
1660     {
1661         if (show && !Options::autoHFR())
1662             KSNotification::info(
1663                 i18n("The \"Auto Compute HFR\" option in the KStars "
1664                      "FITS options menu is not set. You won't get HFR values "
1665                      "without it. Once you set it, newly captured images "
1666                      "will have their HFRs computed."));
1667     });
1668 
1669     numCaptureStarsAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
1670     numCaptureStarsAxis->setVisible(false);
1671     numCaptureStarsAxis->setRange(0, 1000);  // this will be reset.
1672     NUM_CAPTURE_STARS_GRAPH = initGraphAndCB(statsPlot, numCaptureStarsAxis, QCPGraph::lsStepRight, Qt::darkGreen, "#SubStars",
1673                               numCaptureStarsCB, Options::setAnalyzeNumCaptureStars);
1674     connect(numCaptureStarsCB, &QCheckBox::clicked,
1675             [ = ](bool show)
1676     {
1677         if (show && !Options::autoHFR())
1678             KSNotification::info(
1679                 i18n("The \"Auto Compute HFR\" option in the KStars "
1680                      "FITS options menu is not set. You won't get # stars in capture image values "
1681                      "without it. Once you set it, newly captured images "
1682                      "will have their stars detected."));
1683     });
1684 
1685     medianAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
1686     medianAxis->setVisible(false);
1687     medianAxis->setRange(0, 1000);  // this will be reset.
1688     MEDIAN_GRAPH = initGraphAndCB(statsPlot, medianAxis, QCPGraph::lsStepRight, Qt::darkGray, "median",
1689                                   medianCB, Options::setAnalyzeMedian);
1690 
1691     ECCENTRICITY_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsStepRight, Qt::darkMagenta, "ecc",
1692                                         eccentricityCB, Options::setAnalyzeEccentricity);
1693 
1694     numStarsAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
1695     numStarsAxis->setVisible(false);
1696     numStarsAxis->setRange(0, 15000);
1697     NUMSTARS_GRAPH = initGraphAndCB(statsPlot, numStarsAxis, QCPGraph::lsStepRight, Qt::magenta, "#Stars", numStarsCB,
1698                                     Options::setAnalyzeNumStars);
1699 
1700     skyBgAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
1701     skyBgAxis->setVisible(false);
1702     skyBgAxis->setRange(0, 1000);
1703     SKYBG_GRAPH = initGraphAndCB(statsPlot, skyBgAxis, QCPGraph::lsStepRight, Qt::darkYellow, "SkyBG", skyBgCB,
1704                                  Options::setAnalyzeSkyBg);
1705 
1706 
1707     temperatureAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
1708     temperatureAxis->setVisible(false);
1709     temperatureAxis->setRange(-40, 40);
1710     TEMPERATURE_GRAPH = initGraphAndCB(statsPlot, temperatureAxis, QCPGraph::lsLine, Qt::yellow, "temp", temperatureCB,
1711                                        Options::setAnalyzeTemperature);
1712 
1713     snrAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
1714     snrAxis->setVisible(false);
1715     snrAxis->setRange(-100, 100);  // this will be reset.
1716     SNR_GRAPH = initGraphAndCB(statsPlot, snrAxis, QCPGraph::lsLine, Qt::yellow, "SNR", snrCB, Options::setAnalyzeSNR);
1717 
1718     auto raColor = KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError");
1719     RA_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, raColor, "RA", raCB, Options::setAnalyzeRA);
1720     auto decColor = KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError");
1721     DEC_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, decColor, "DEC", decCB, Options::setAnalyzeDEC);
1722 
1723     QCPAxis *pulseAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
1724     pulseAxis->setVisible(false);
1725     // 150 is a typical value for pulse-ms/pixel
1726     // This will roughtly co-incide with the -2,5 range for the ra/dec plots.
1727     pulseAxis->setRange(-2 * 150, 5 * 150);
1728 
1729     auto raPulseColor = KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError");
1730     raPulseColor.setAlpha(75);
1731     RA_PULSE_GRAPH = initGraphAndCB(statsPlot, pulseAxis, QCPGraph::lsLine, raPulseColor, "RAp", raPulseCB,
1732                                     Options::setAnalyzeRAp);
1733     statsPlot->graph(RA_PULSE_GRAPH)->setBrush(QBrush(raPulseColor, Qt::Dense4Pattern));
1734 
1735     auto decPulseColor = KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError");
1736     decPulseColor.setAlpha(75);
1737     DEC_PULSE_GRAPH = initGraphAndCB(statsPlot, pulseAxis, QCPGraph::lsLine, decPulseColor, "DECp", decPulseCB,
1738                                      Options::setAnalyzeDECp);
1739     statsPlot->graph(DEC_PULSE_GRAPH)->setBrush(QBrush(decPulseColor, Qt::Dense4Pattern));
1740 
1741     DRIFT_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, Qt::lightGray, "Drift", driftCB,
1742                                  Options::setAnalyzeDrift);
1743     RMS_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, Qt::red, "RMS", rmsCB, Options::setAnalyzeRMS);
1744     CAPTURE_RMS_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, Qt::red, "RMSc", rmsCCB,
1745                                        Options::setAnalyzeRMSC);
1746 
1747     QCPAxis *mountRaDecAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
1748     mountRaDecAxis->setVisible(false);
1749     mountRaDecAxis->setRange(-10, 370);
1750     // Colors of these two unimportant--not really plotted.
1751     MOUNT_RA_GRAPH = initGraphAndCB(statsPlot, mountRaDecAxis, QCPGraph::lsLine, Qt::red, "MOUNT_RA", mountRaCB,
1752                                     Options::setAnalyzeMountRA);
1753     MOUNT_DEC_GRAPH = initGraphAndCB(statsPlot, mountRaDecAxis, QCPGraph::lsLine, Qt::red, "MOUNT_DEC", mountDecCB,
1754                                      Options::setAnalyzeMountDEC);
1755     MOUNT_HA_GRAPH = initGraphAndCB(statsPlot, mountRaDecAxis, QCPGraph::lsLine, Qt::red, "MOUNT_HA", mountHaCB,
1756                                     Options::setAnalyzeMountHA);
1757 
1758     QCPAxis *azAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
1759     azAxis->setVisible(false);
1760     azAxis->setRange(-10, 370);
1761     AZ_GRAPH = initGraphAndCB(statsPlot, azAxis, QCPGraph::lsLine, Qt::darkGray, "AZ", azCB, Options::setAnalyzeAz);
1762 
1763     QCPAxis *altAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
1764     altAxis->setVisible(false);
1765     altAxis->setRange(0, 90);
1766     ALT_GRAPH = initGraphAndCB(statsPlot, altAxis, QCPGraph::lsLine, Qt::white, "ALT", altCB, Options::setAnalyzeAlt);
1767 
1768     QCPAxis *pierSideAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
1769     pierSideAxis->setVisible(false);
1770     pierSideAxis->setRange(-2, 2);
1771     PIER_SIDE_GRAPH = initGraphAndCB(statsPlot, pierSideAxis, QCPGraph::lsLine, Qt::darkRed, "PierSide", pierSideCB,
1772                                      Options::setAnalyzePierSide);
1773 
1774     // TODO: Should figure out the margin
1775     // on the timeline plot, and setting this one accordingly.
1776     // doesn't look like that's possible with current code, though.
1777     statsPlot->yAxis->setPadding(50);
1778 
1779     // This makes mouseMove only get called when a button is pressed.
1780     statsPlot->setMouseTracking(false);
1781 
1782     // Setup the clock-time labels on the x-axis of the stats plot.
1783     dateTicker.reset(new OffsetDateTimeTicker);
1784     dateTicker->setDateTimeFormat("hh:mm:ss");
1785     statsPlot->xAxis->setTicker(dateTicker);
1786 
1787     // Didn't include QCP::iRangeDrag as it  interacts poorly with the curson logic.
1788     statsPlot->setInteractions(QCP::iRangeZoom);
1789     statsPlot->axisRect()->setRangeZoomAxes(0, statsPlot->yAxis);
1790 }
1791 
1792 // Clear the graphics and state when changing input data.
reset()1793 void Analyze::reset()
1794 {
1795     maxXValue = 10.0;
1796     plotStart = 0.0;
1797     plotWidth = 10.0;
1798 
1799     guiderRms->resetFilter();
1800     captureRms->resetFilter();
1801 
1802     unhighlightTimelineItem();
1803 
1804     for (int i = 0; i < statsPlot->graphCount(); ++i)
1805         statsPlot->graph(i)->data()->clear();
1806     statsPlot->clearItems();
1807 
1808     for (int i = 0; i < timelinePlot->graphCount(); ++i)
1809         timelinePlot->graph(i)->data()->clear();
1810     timelinePlot->clearItems();
1811 
1812     resetGraphicsPlot();
1813 
1814     detailsTable->clear();
1815     QPalette p = detailsTable->palette();
1816     p.setColor(QPalette::Base, Qt::black);
1817     p.setColor(QPalette::Text, Qt::white);
1818     detailsTable->setPalette(p);
1819 
1820     inputValue->clear();
1821     captureSessions.clear();
1822     focusSessions.clear();
1823 
1824     numStarsOut->setText("");
1825     skyBgOut->setText("");
1826     snrOut->setText("");
1827     temperatureOut->setText("");
1828     eccentricityOut->setText("");
1829     medianOut->setText("");
1830     numCaptureStarsOut->setText("");
1831 
1832     raOut->setText("");
1833     decOut->setText("");
1834     driftOut->setText("");
1835     rmsOut->setText("");
1836     rmsCOut->setText("");
1837 
1838     removeStatsCursor();
1839     removeTemporarySessions();
1840 
1841     resetCaptureState();
1842     resetAutofocusState();
1843     resetGuideState();
1844     resetGuideStats();
1845     resetAlignState();
1846     resetMountState();
1847     resetMountCoords();
1848     resetMountFlipState();
1849 
1850     // Note: no replot().
1851 }
1852 
initGraphicsPlot()1853 void Analyze::initGraphicsPlot()
1854 {
1855     initQCP(graphicsPlot);
1856     FOCUS_GRAPHICS = initGraph(graphicsPlot, graphicsPlot->yAxis,
1857                                QCPGraph::lsNone, Qt::cyan, "Focus");
1858     graphicsPlot->graph(FOCUS_GRAPHICS)->setScatterStyle(
1859         QCPScatterStyle(QCPScatterStyle::ssCircle, Qt::white, Qt::white, 14));
1860     FOCUS_GRAPHICS_FINAL = initGraph(graphicsPlot, graphicsPlot->yAxis,
1861                                      QCPGraph::lsNone, Qt::cyan, "FocusBest");
1862     graphicsPlot->graph(FOCUS_GRAPHICS_FINAL)->setScatterStyle(
1863         QCPScatterStyle(QCPScatterStyle::ssCircle, Qt::yellow, Qt::yellow, 14));
1864     graphicsPlot->setInteractions(QCP::iRangeZoom);
1865     graphicsPlot->setInteraction(QCP::iRangeDrag, true);
1866 
1867 
1868     GUIDER_GRAPHICS = initGraph(graphicsPlot, graphicsPlot->yAxis,
1869                                 QCPGraph::lsNone, Qt::cyan, "Guide Error");
1870     graphicsPlot->graph(GUIDER_GRAPHICS)->setScatterStyle(
1871         QCPScatterStyle(QCPScatterStyle::ssStar, Qt::gray, 5));
1872 }
1873 
displayFocusGraphics(const QVector<double> & positions,const QVector<double> & hfrs,bool success)1874 void Analyze::displayFocusGraphics(const QVector<double> &positions, const QVector<double> &hfrs, bool success)
1875 {
1876     resetGraphicsPlot();
1877     auto graph = graphicsPlot->graph(FOCUS_GRAPHICS);
1878     auto finalGraph = graphicsPlot->graph(FOCUS_GRAPHICS_FINAL);
1879     double maxHfr = -1e8, maxPosition = -1e8, minHfr = 1e8, minPosition = 1e8;
1880     for (int i = 0; i < positions.size(); ++i)
1881     {
1882         // Yellow circle for the final point.
1883         if (success && i == positions.size() - 1)
1884             finalGraph->addData(positions[i], hfrs[i]);
1885         else
1886             graph->addData(positions[i], hfrs[i]);
1887         maxHfr = std::max(maxHfr, hfrs[i]);
1888         minHfr = std::min(minHfr, hfrs[i]);
1889         maxPosition = std::max(maxPosition, positions[i]);
1890         minPosition = std::min(minPosition, positions[i]);
1891     }
1892 
1893     for (int i = 0; i < positions.size(); ++i)
1894     {
1895         QCPItemText *textLabel = new QCPItemText(graphicsPlot);
1896         textLabel->setPositionAlignment(Qt::AlignCenter | Qt::AlignHCenter);
1897         textLabel->position->setType(QCPItemPosition::ptPlotCoords);
1898         textLabel->position->setCoords(positions[i], hfrs[i]);
1899         textLabel->setText(QString::number(i + 1));
1900         textLabel->setFont(QFont(font().family(), 12));
1901         textLabel->setPen(Qt::NoPen);
1902         textLabel->setColor(Qt::red);
1903     }
1904     const double xRange = maxPosition - minPosition;
1905     const double yRange = maxHfr - minHfr;
1906     graphicsPlot->xAxis->setRange(minPosition - xRange * .2, maxPosition + xRange * .2);
1907     graphicsPlot->yAxis->setRange(minHfr - yRange * .2, maxHfr + yRange * .2);
1908     graphicsPlot->replot();
1909 }
1910 
resetGraphicsPlot()1911 void Analyze::resetGraphicsPlot()
1912 {
1913     for (int i = 0; i < graphicsPlot->graphCount(); ++i)
1914         graphicsPlot->graph(i)->data()->clear();
1915     graphicsPlot->clearItems();
1916 }
1917 
displayFITS(const QString & filename)1918 void Analyze::displayFITS(const QString &filename)
1919 {
1920     QUrl url = QUrl::fromLocalFile(filename);
1921 
1922     if (fitsViewer.isNull())
1923     {
1924         fitsViewer = KStars::Instance()->createFITSViewer();
1925         fitsViewer->loadFile(url);
1926         //        FITSView *currentView = fitsViewer->getCurrentView();
1927         //        if (currentView)
1928         //            currentView->getImageData()->setAutoRemoveTemporaryFITS(false);
1929     }
1930     else
1931         fitsViewer->updateFile(url, 0);
1932 
1933     fitsViewer->show();
1934 }
1935 
helpMessage()1936 void Analyze::helpMessage()
1937 {
1938     KHelpClient::invokeHelp(QStringLiteral("tool-ekos.html#ekos-analyze"), QStringLiteral("kstars"));
1939 }
1940 
1941 // This is intended for recording data to file.
1942 // Don't use this when displaying data read from file, as this is not using the
1943 // correct analyzeStartTime.
logTime(const QDateTime & time)1944 double Analyze::logTime(const QDateTime &time)
1945 {
1946     if (!logInitialized)
1947         startLog();
1948     return (time.toMSecsSinceEpoch() - analyzeStartTime.toMSecsSinceEpoch()) / 1000.0;
1949 }
1950 
1951 // The logTime using clock = now.
1952 // This is intended for recording data to file.
1953 // Don't use this When displaying data read from file.
logTime()1954 double Analyze::logTime()
1955 {
1956     return logTime(QDateTime::currentDateTime());
1957 }
1958 
1959 // Goes back to clock time from seconds into the log.
1960 // Appropriate for both displaying data from files as well as when displaying live data.
clockTime(double logSeconds)1961 QDateTime Analyze::clockTime(double logSeconds)
1962 {
1963     return displayStartTime.addMSecs(logSeconds * 1000.0);
1964 }
1965 
1966 
1967 // Write the command name, a timestamp and the message with comma separation to a .analyze file.
saveMessage(const QString & type,const QString & message)1968 void Analyze::saveMessage(const QString &type, const QString &message)
1969 {
1970     QString line(QString("%1,%2%3%4\n")
1971                  .arg(type)
1972                  .arg(QString::number(logTime(), 'f', 3))
1973                  .arg(message.size() > 0 ? "," : "")
1974                  .arg(message));
1975     appendToLog(line);
1976 }
1977 
1978 // Start writing a .analyze file.
startLog()1979 void Analyze::startLog()
1980 {
1981     analyzeStartTime = QDateTime::currentDateTime();
1982     startTimeInitialized = true;
1983     if (runtimeDisplay)
1984         displayStartTime = analyzeStartTime;
1985     if (logInitialized)
1986         return;
1987     QDir dir = QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation) + "/analyze");
1988     dir.mkpath(".");
1989 
1990     logFilename = dir.filePath("ekos-" + QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss") + ".analyze");
1991     logFile.setFileName(logFilename);
1992     logFile.open(QIODevice::WriteOnly | QIODevice::Text);
1993 
1994     // This must happen before the below appendToLog() call.
1995     logInitialized = true;
1996 
1997     appendToLog(QString("#KStars version %1. Analyze log version 1.0.\n\n")
1998                 .arg(KSTARS_VERSION));
1999     appendToLog(QString("%1,%2,%3\n")
2000                 .arg("AnalyzeStartTime")
2001                 .arg(analyzeStartTime.toString(timeFormat))
2002                 .arg(analyzeStartTime.timeZoneAbbreviation()));
2003 }
2004 
appendToLog(const QString & lines)2005 void Analyze::appendToLog(const QString &lines)
2006 {
2007     if (!logInitialized)
2008         startLog();
2009     QTextStream out(&logFile);
2010     out << lines;
2011     out.flush();
2012 }
2013 
2014 // maxXValue is the largest time value we have seen so far for this data.
updateMaxX(double time)2015 void Analyze::updateMaxX(double time)
2016 {
2017     maxXValue = std::max(time, maxXValue);
2018 }
2019 
2020 // Manage temporary sessions displayed on the Timeline.
2021 // Those are ongoing sessions that will ultimately be replaced when the session is complete.
2022 // This only happens with live data, not with data read from .analyze files.
2023 
2024 // Remove the graphic element.
removeTemporarySession(Session * session)2025 void Analyze::removeTemporarySession(Session *session)
2026 {
2027     if (session->rect != nullptr)
2028         timelinePlot->removeItem(session->rect);
2029     session->rect = nullptr;
2030     session->start = 0;
2031     session->end = 0;
2032 }
2033 
2034 // Remove all temporary sessions (i.e. from all lines in the Timeline).
removeTemporarySessions()2035 void Analyze::removeTemporarySessions()
2036 {
2037     removeTemporarySession(&temporaryCaptureSession);
2038     removeTemporarySession(&temporaryMountFlipSession);
2039     removeTemporarySession(&temporaryFocusSession);
2040     removeTemporarySession(&temporaryGuideSession);
2041     removeTemporarySession(&temporaryMountSession);
2042     removeTemporarySession(&temporaryAlignSession);
2043 }
2044 
2045 // Add a new temporary session.
addTemporarySession(Session * session,double time,double duration,int y_offset,const QBrush & brush)2046 void Analyze::addTemporarySession(Session *session, double time, double duration,
2047                                   int y_offset, const QBrush &brush)
2048 {
2049     removeTemporarySession(session);
2050     session->rect = addSession(time, time + duration, y_offset, brush);
2051     session->start = time;
2052     session->end = time + duration;
2053     session->offset = y_offset;
2054     session->temporaryBrush = brush;
2055     updateMaxX(time + duration);
2056 }
2057 
2058 // Extend a temporary session. That is, we don't know how long the session will last,
2059 // so when new data arrives (from any module, not necessarily the one with the temporary
2060 // session) we must extend that temporary session.
adjustTemporarySession(Session * session)2061 void Analyze::adjustTemporarySession(Session *session)
2062 {
2063     if (session->rect != nullptr && session->end < maxXValue)
2064     {
2065         QBrush brush = session->temporaryBrush;
2066         double start = session->start;
2067         int offset = session->offset;
2068         addTemporarySession(session, start, maxXValue - start, offset, brush);
2069     }
2070 }
2071 
2072 // Extend all temporary sessions.
adjustTemporarySessions()2073 void Analyze::adjustTemporarySessions()
2074 {
2075     adjustTemporarySession(&temporaryCaptureSession);
2076     adjustTemporarySession(&temporaryMountFlipSession);
2077     adjustTemporarySession(&temporaryFocusSession);
2078     adjustTemporarySession(&temporaryGuideSession);
2079     adjustTemporarySession(&temporaryMountSession);
2080     adjustTemporarySession(&temporaryAlignSession);
2081 }
2082 
2083 // Called when the captureStarting slot receives a signal.
2084 // Saves the message to disk, and calls processCaptureStarting.
captureStarting(double exposureSeconds,const QString & filter)2085 void Analyze::captureStarting(double exposureSeconds, const QString &filter)
2086 {
2087     saveMessage("CaptureStarting",
2088                 QString("%1,%2")
2089                 .arg(QString::number(exposureSeconds, 'f', 3))
2090                 .arg(filter));
2091     processCaptureStarting(logTime(), exposureSeconds, filter);
2092 }
2093 
2094 // Called by either the above (when live data is received), or reading from file.
2095 // BatchMode would be true when reading from file.
processCaptureStarting(double time,double exposureSeconds,const QString & filter,bool batchMode)2096 void Analyze::processCaptureStarting(double time, double exposureSeconds, const QString &filter, bool batchMode)
2097 {
2098     captureStartedTime = time;
2099     captureStartedFilter = filter;
2100     updateMaxX(time);
2101 
2102     if (!batchMode)
2103     {
2104         addTemporarySession(&temporaryCaptureSession, time, 1, CAPTURE_Y, temporaryBrush);
2105         temporaryCaptureSession.duration = exposureSeconds;
2106         temporaryCaptureSession.filter = filter;
2107     }
2108 }
2109 
2110 // Called when the captureComplete slot receives a signal.
captureComplete(const QString & filename,double exposureSeconds,const QString & filter,double hfr,int numStars,int median,double eccentricity)2111 void Analyze::captureComplete(const QString &filename, double exposureSeconds, const QString &filter,
2112                               double hfr, int numStars, int median, double eccentricity)
2113 {
2114     saveMessage("CaptureComplete",
2115                 QString("%1,%2,%3,%4,%5,%6,%7")
2116                 .arg(QString::number(exposureSeconds, 'f', 3))
2117                 .arg(filter)
2118                 .arg(QString::number(hfr, 'f', 3))
2119                 .arg(filename)
2120                 .arg(numStars)
2121                 .arg(median)
2122                 .arg(QString::number(eccentricity, 'f', 3)));
2123     if (runtimeDisplay && captureStartedTime >= 0)
2124         processCaptureComplete(logTime(), filename, exposureSeconds, filter, hfr,
2125                                numStars, median, eccentricity);
2126 }
2127 
processCaptureComplete(double time,const QString & filename,double exposureSeconds,const QString & filter,double hfr,int numStars,int median,double eccentricity,bool batchMode)2128 void Analyze::processCaptureComplete(double time, const QString &filename,
2129                                      double exposureSeconds, const QString &filter, double hfr,
2130                                      int numStars, int median, double eccentricity, bool batchMode)
2131 {
2132     removeTemporarySession(&temporaryCaptureSession);
2133     QBrush stripe;
2134     if (filterStripeBrush(filter, &stripe))
2135         addSession(captureStartedTime, time, CAPTURE_Y, successBrush, &stripe);
2136     else
2137         addSession(captureStartedTime, time, CAPTURE_Y, successBrush, nullptr);
2138     captureSessions.add(CaptureSession(captureStartedTime, time, nullptr, false,
2139                                        filename, exposureSeconds, filter));
2140     addHFR(hfr, numStars, median, eccentricity, time, captureStartedTime);
2141     updateMaxX(time);
2142     if (!batchMode)
2143         replot();
2144     captureStartedTime = -1;
2145 }
2146 
captureAborted(double exposureSeconds)2147 void Analyze::captureAborted(double exposureSeconds)
2148 {
2149     saveMessage("CaptureAborted",
2150                 QString("%1").arg(QString::number(exposureSeconds, 'f', 3)));
2151     if (runtimeDisplay && captureStartedTime >= 0)
2152         processCaptureAborted(logTime(), exposureSeconds);
2153 }
2154 
processCaptureAborted(double time,double exposureSeconds,bool batchMode)2155 void Analyze::processCaptureAborted(double time, double exposureSeconds, bool batchMode)
2156 {
2157     removeTemporarySession(&temporaryCaptureSession);
2158     double duration = time - captureStartedTime;
2159     if (captureStartedTime >= 0 &&
2160             duration < (exposureSeconds + 30) &&
2161             duration < 3600)
2162     {
2163         // You can get a captureAborted without a captureStarting,
2164         // so make sure this associates with a real start.
2165         addSession(captureStartedTime, time, CAPTURE_Y, failureBrush);
2166         captureSessions.add(CaptureSession(captureStartedTime, time, nullptr, true, "",
2167                                            exposureSeconds, captureStartedFilter));
2168         updateMaxX(time);
2169         if (!batchMode)
2170             replot();
2171         captureStartedTime = -1;
2172     }
2173 }
2174 
resetCaptureState()2175 void Analyze::resetCaptureState()
2176 {
2177     captureStartedTime = -1;
2178     captureStartedFilter = "";
2179     medianMax = 1;
2180     numCaptureStarsMax = 1;
2181 }
2182 
autofocusStarting(double temperature,const QString & filter)2183 void Analyze::autofocusStarting(double temperature, const QString &filter)
2184 {
2185     saveMessage("AutofocusStarting",
2186                 QString("%1,%2")
2187                 .arg(filter)
2188                 .arg(QString::number(temperature, 'f', 1)));
2189     processAutofocusStarting(logTime(), temperature, filter);
2190 }
2191 
processAutofocusStarting(double time,double temperature,const QString & filter,bool batchMode)2192 void Analyze::processAutofocusStarting(double time, double temperature, const QString &filter, bool batchMode)
2193 {
2194     autofocusStartedTime = time;
2195     autofocusStartedFilter = filter;
2196     autofocusStartedTemperature = temperature;
2197     addTemperature(temperature, time);
2198     updateMaxX(time);
2199     if (!batchMode)
2200     {
2201         addTemporarySession(&temporaryFocusSession, time, 1, FOCUS_Y, temporaryBrush);
2202         temporaryFocusSession.temperature = temperature;
2203         temporaryFocusSession.filter = filter;
2204     }
2205 }
2206 
autofocusComplete(const QString & filter,const QString & points)2207 void Analyze::autofocusComplete(const QString &filter, const QString &points)
2208 {
2209     saveMessage("AutofocusComplete", QString("%1,%2").arg(filter).arg(points));
2210     if (runtimeDisplay && autofocusStartedTime >= 0)
2211         processAutofocusComplete(logTime(), filter, points);
2212 }
2213 
processAutofocusComplete(double time,const QString & filter,const QString & points,bool batchMode)2214 void Analyze::processAutofocusComplete(double time, const QString &filter, const QString &points, bool batchMode)
2215 {
2216     removeTemporarySession(&temporaryFocusSession);
2217     QBrush stripe;
2218     if (filterStripeBrush(filter, &stripe))
2219         addSession(autofocusStartedTime, time, FOCUS_Y, successBrush, &stripe);
2220     else
2221         addSession(autofocusStartedTime, time, FOCUS_Y, successBrush, nullptr);
2222     focusSessions.add(FocusSession(autofocusStartedTime, time, nullptr, true,
2223                                    autofocusStartedTemperature, filter, points));
2224     updateMaxX(time);
2225     if (!batchMode)
2226         replot();
2227     autofocusStartedTime = -1;
2228 }
2229 
autofocusAborted(const QString & filter,const QString & points)2230 void Analyze::autofocusAborted(const QString &filter, const QString &points)
2231 {
2232     saveMessage("AutofocusAborted", QString("%1,%2").arg(filter).arg(points));
2233     if (runtimeDisplay && autofocusStartedTime >= 0)
2234         processAutofocusAborted(logTime(), filter, points);
2235 }
2236 
processAutofocusAborted(double time,const QString & filter,const QString & points,bool batchMode)2237 void Analyze::processAutofocusAborted(double time, const QString &filter, const QString &points, bool batchMode)
2238 {
2239     removeTemporarySession(&temporaryFocusSession);
2240     double duration = time - autofocusStartedTime;
2241     if (autofocusStartedTime >= 0 && duration < 1000)
2242     {
2243         // Just in case..
2244         addSession(autofocusStartedTime, time, FOCUS_Y, failureBrush);
2245         focusSessions.add(FocusSession(autofocusStartedTime, time, nullptr, false,
2246                                        autofocusStartedTemperature, filter, points));
2247         updateMaxX(time);
2248         if (!batchMode)
2249             replot();
2250         autofocusStartedTime = -1;
2251     }
2252 }
2253 
resetAutofocusState()2254 void Analyze::resetAutofocusState()
2255 {
2256     autofocusStartedTime = -1;
2257     autofocusStartedFilter = "";
2258     autofocusStartedTemperature = 0;
2259 }
2260 
2261 namespace
2262 {
2263 
2264 // TODO: move to ekos.h/cpp?
stringToGuideState(const QString & str)2265 Ekos::GuideState stringToGuideState(const QString &str)
2266 {
2267     if (str == I18N_NOOP("Idle"))
2268         return GUIDE_IDLE;
2269     else if (str == I18N_NOOP("Aborted"))
2270         return GUIDE_ABORTED;
2271     else if (str == I18N_NOOP("Connected"))
2272         return GUIDE_CONNECTED;
2273     else if (str == I18N_NOOP("Disconnected"))
2274         return GUIDE_DISCONNECTED;
2275     else if (str == I18N_NOOP("Capturing"))
2276         return GUIDE_CAPTURE;
2277     else if (str == I18N_NOOP("Looping"))
2278         return GUIDE_LOOPING;
2279     else if (str == I18N_NOOP("Subtracting"))
2280         return GUIDE_DARK;
2281     else if (str == I18N_NOOP("Subframing"))
2282         return GUIDE_SUBFRAME;
2283     else if (str == I18N_NOOP("Selecting star"))
2284         return GUIDE_STAR_SELECT;
2285     else if (str == I18N_NOOP("Calibrating"))
2286         return GUIDE_CALIBRATING;
2287     else if (str == I18N_NOOP("Calibration error"))
2288         return GUIDE_CALIBRATION_ERROR;
2289     else if (str == I18N_NOOP("Calibrated"))
2290         return GUIDE_CALIBRATION_SUCESS;
2291     else if (str == I18N_NOOP("Guiding"))
2292         return GUIDE_GUIDING;
2293     else if (str == I18N_NOOP("Suspended"))
2294         return GUIDE_SUSPENDED;
2295     else if (str == I18N_NOOP("Reacquiring"))
2296         return GUIDE_REACQUIRE;
2297     else if (str == I18N_NOOP("Dithering"))
2298         return GUIDE_DITHERING;
2299     else if (str == I18N_NOOP("Manual Dithering"))
2300         return GUIDE_MANUAL_DITHERING;
2301     else if (str == I18N_NOOP("Dithering error"))
2302         return GUIDE_DITHERING_ERROR;
2303     else if (str == I18N_NOOP("Dithering successful"))
2304         return GUIDE_DITHERING_SUCCESS;
2305     else if (str == I18N_NOOP("Settling"))
2306         return GUIDE_DITHERING_SETTLE;
2307     else
2308         return GUIDE_IDLE;
2309 }
2310 
convertGuideState(Ekos::GuideState state)2311 Analyze::SimpleGuideState convertGuideState(Ekos::GuideState state)
2312 {
2313     switch (state)
2314     {
2315         case GUIDE_IDLE:
2316         case GUIDE_ABORTED:
2317         case GUIDE_CONNECTED:
2318         case GUIDE_DISCONNECTED:
2319         case GUIDE_LOOPING:
2320             return Analyze::G_IDLE;
2321         case GUIDE_GUIDING:
2322             return Analyze::G_GUIDING;
2323         case GUIDE_CAPTURE:
2324         case GUIDE_DARK:
2325         case GUIDE_SUBFRAME:
2326         case GUIDE_STAR_SELECT:
2327             return Analyze::G_IGNORE;
2328         case GUIDE_CALIBRATING:
2329         case GUIDE_CALIBRATION_ERROR:
2330         case GUIDE_CALIBRATION_SUCESS:
2331             return Analyze::G_CALIBRATING;
2332         case GUIDE_SUSPENDED:
2333         case GUIDE_REACQUIRE:
2334             return Analyze::G_SUSPENDED;
2335         case GUIDE_DITHERING:
2336         case GUIDE_MANUAL_DITHERING:
2337         case GUIDE_DITHERING_ERROR:
2338         case GUIDE_DITHERING_SUCCESS:
2339         case GUIDE_DITHERING_SETTLE:
2340             return Analyze::G_DITHERING;
2341     }
2342     // Shouldn't get here--would get compile error, I believe with a missing case.
2343     return Analyze::G_IDLE;
2344 }
2345 
guideBrush(Analyze::SimpleGuideState simpleState)2346 const QBrush guideBrush(Analyze::SimpleGuideState simpleState)
2347 {
2348     switch (simpleState)
2349     {
2350         case Analyze::G_IDLE:
2351         case Analyze::G_IGNORE:
2352             // don't actually render these, so don't care.
2353             return offBrush;
2354         case Analyze::G_GUIDING:
2355             return successBrush;
2356         case Analyze::G_CALIBRATING:
2357             return progressBrush;
2358         case Analyze::G_SUSPENDED:
2359             return stoppedBrush;
2360         case Analyze::G_DITHERING:
2361             return progress2Brush;
2362     }
2363     // Shouldn't get here.
2364     return offBrush;
2365 }
2366 
2367 }  // namespace
2368 
guideState(Ekos::GuideState state)2369 void Analyze::guideState(Ekos::GuideState state)
2370 {
2371     QString str = getGuideStatusString(state);
2372     saveMessage("GuideState", str);
2373     if (runtimeDisplay)
2374         processGuideState(logTime(), str);
2375 }
2376 
processGuideState(double time,const QString & stateStr,bool batchMode)2377 void Analyze::processGuideState(double time, const QString &stateStr, bool batchMode)
2378 {
2379     Ekos::GuideState gstate = stringToGuideState(stateStr);
2380     SimpleGuideState state = convertGuideState(gstate);
2381     if (state == G_IGNORE)
2382         return;
2383     if (state == lastGuideStateStarted)
2384         return;
2385     // End the previous guide session and start the new one.
2386     if (guideStateStartedTime >= 0)
2387     {
2388         if (lastGuideStateStarted != G_IDLE)
2389         {
2390             // Don't render the idle guiding
2391             addSession(guideStateStartedTime, time, GUIDE_Y, guideBrush(lastGuideStateStarted));
2392             guideSessions.add(GuideSession(guideStateStartedTime, time, nullptr, lastGuideStateStarted));
2393         }
2394     }
2395     if (state == G_GUIDING && !batchMode)
2396     {
2397         addTemporarySession(&temporaryGuideSession, time, 1, GUIDE_Y, successBrush);
2398         temporaryGuideSession.simpleState = state;
2399     }
2400     else
2401         removeTemporarySession(&temporaryGuideSession);
2402 
2403     guideStateStartedTime = time;
2404     lastGuideStateStarted = state;
2405     updateMaxX(time);
2406     if (!batchMode)
2407         replot();
2408 }
2409 
resetGuideState()2410 void Analyze::resetGuideState()
2411 {
2412     lastGuideStateStarted = G_IDLE;
2413     guideStateStartedTime = -1;
2414 }
2415 
newTemperature(double temperatureDelta,double temperature)2416 void Analyze::newTemperature(double temperatureDelta, double temperature)
2417 {
2418     Q_UNUSED(temperatureDelta);
2419     if (temperature > -200 && temperature != lastTemperature)
2420     {
2421         saveMessage("Temperature", QString("%1").arg(QString::number(temperature, 'f', 3)));
2422         lastTemperature = temperature;
2423         if (runtimeDisplay)
2424             processTemperature(logTime(), temperature);
2425     }
2426 }
2427 
processTemperature(double time,double temperature,bool batchMode)2428 void Analyze::processTemperature(double time, double temperature, bool batchMode)
2429 {
2430     addTemperature(temperature, time);
2431     updateMaxX(time);
2432     if (!batchMode)
2433         replot();
2434 }
2435 
resetTemperature()2436 void Analyze::resetTemperature()
2437 {
2438     lastTemperature = -1000;
2439 }
2440 
2441 
guideStats(double raError,double decError,int raPulse,int decPulse,double snr,double skyBg,int numStars)2442 void Analyze::guideStats(double raError, double decError, int raPulse, int decPulse,
2443                          double snr, double skyBg, int numStars)
2444 {
2445     saveMessage("GuideStats", QString("%1,%2,%3,%4,%5,%6,%7")
2446                 .arg(QString::number(raError, 'f', 3))
2447                 .arg(QString::number(decError, 'f', 3))
2448                 .arg(raPulse)
2449                 .arg(decPulse)
2450                 .arg(QString::number(snr, 'f', 3))
2451                 .arg(QString::number(skyBg, 'f', 3))
2452                 .arg(numStars));
2453 
2454     if (runtimeDisplay)
2455         processGuideStats(logTime(), raError, decError, raPulse, decPulse, snr, skyBg, numStars);
2456 }
2457 
processGuideStats(double time,double raError,double decError,int raPulse,int decPulse,double snr,double skyBg,int numStars,bool batchMode)2458 void Analyze::processGuideStats(double time, double raError, double decError,
2459                                 int raPulse, int decPulse, double snr, double skyBg, int numStars, bool batchMode)
2460 {
2461     addGuideStats(raError, decError, raPulse, decPulse, snr, numStars, skyBg, time);
2462     updateMaxX(time);
2463     if (!batchMode)
2464         replot();
2465 }
2466 
resetGuideStats()2467 void Analyze::resetGuideStats()
2468 {
2469     lastGuideStatsTime = -1;
2470     lastCaptureRmsTime = -1;
2471     numStarsMax = 0;
2472     snrMax = 0;
2473     skyBgMax = 0;
2474 }
2475 
2476 namespace
2477 {
2478 
2479 // TODO: move to ekos.h/cpp
convertAlignState(const QString & str)2480 AlignState convertAlignState(const QString &str)
2481 {
2482     for (int i = 0; i < alignStates.size(); ++i)
2483     {
2484         if (str == alignStates[i])
2485             return static_cast<AlignState>(i);
2486     }
2487     return ALIGN_IDLE;
2488 }
2489 
alignBrush(AlignState state)2490 const QBrush alignBrush(AlignState state)
2491 {
2492     switch (state)
2493     {
2494         case ALIGN_IDLE:
2495             return offBrush;
2496         case ALIGN_COMPLETE:
2497             return successBrush;
2498         case ALIGN_FAILED:
2499             return failureBrush;
2500         case ALIGN_PROGRESS:
2501             return progress3Brush;
2502         case ALIGN_SYNCING:
2503             return progress2Brush;
2504         case ALIGN_SLEWING:
2505             return progressBrush;
2506         case ALIGN_ABORTED:
2507             return failureBrush;
2508         case ALIGN_SUSPENDED:
2509             return offBrush;
2510     }
2511     // Shouldn't get here.
2512     return offBrush;
2513 }
2514 }  // namespace
2515 
alignState(AlignState state)2516 void Analyze::alignState(AlignState state)
2517 {
2518     if (state == lastAlignStateReceived)
2519         return;
2520     lastAlignStateReceived = state;
2521 
2522     QString stateStr = getAlignStatusString(state);
2523     saveMessage("AlignState", stateStr);
2524     if (runtimeDisplay)
2525         processAlignState(logTime(), stateStr);
2526 }
2527 
2528 //ALIGN_IDLE, ALIGN_COMPLETE, ALIGN_FAILED, ALIGN_ABORTED,ALIGN_PROGRESS,ALIGN_SYNCING,ALIGN_SLEWING
processAlignState(double time,const QString & statusString,bool batchMode)2529 void Analyze::processAlignState(double time, const QString &statusString, bool batchMode)
2530 {
2531     AlignState state = convertAlignState(statusString);
2532 
2533     if (state == lastAlignStateStarted)
2534         return;
2535 
2536     bool lastStateInteresting = (lastAlignStateStarted == ALIGN_PROGRESS ||
2537                                  lastAlignStateStarted == ALIGN_SYNCING ||
2538                                  lastAlignStateStarted == ALIGN_SLEWING);
2539     if (lastAlignStateStartedTime >= 0 && lastStateInteresting)
2540     {
2541         if (state == ALIGN_COMPLETE || state == ALIGN_FAILED || state == ALIGN_ABORTED)
2542         {
2543             // These states are really commetaries on the previous states.
2544             addSession(lastAlignStateStartedTime, time, ALIGN_Y, alignBrush(state));
2545             alignSessions.add(AlignSession(lastAlignStateStartedTime, time, nullptr, state));
2546         }
2547         else
2548         {
2549             addSession(lastAlignStateStartedTime, time, ALIGN_Y, alignBrush(lastAlignStateStarted));
2550             alignSessions.add(AlignSession(lastAlignStateStartedTime, time, nullptr, lastAlignStateStarted));
2551         }
2552     }
2553     bool stateInteresting = (state == ALIGN_PROGRESS || state == ALIGN_SYNCING ||
2554                              state == ALIGN_SLEWING);
2555     if (stateInteresting && !batchMode)
2556     {
2557         addTemporarySession(&temporaryAlignSession, time, 1, ALIGN_Y, temporaryBrush);
2558         temporaryAlignSession.state = state;
2559     }
2560     else
2561         removeTemporarySession(&temporaryAlignSession);
2562 
2563     lastAlignStateStartedTime = time;
2564     lastAlignStateStarted = state;
2565     updateMaxX(time);
2566     if (!batchMode)
2567         replot();
2568 
2569 }
2570 
resetAlignState()2571 void Analyze::resetAlignState()
2572 {
2573     lastAlignStateReceived = ALIGN_IDLE;
2574     lastAlignStateStarted = ALIGN_IDLE;
2575     lastAlignStateStartedTime = -1;
2576 }
2577 
2578 namespace
2579 {
2580 
mountBrush(ISD::Telescope::Status state)2581 const QBrush mountBrush(ISD::Telescope::Status state)
2582 {
2583     switch (state)
2584     {
2585         case ISD::Telescope::MOUNT_IDLE:
2586             return offBrush;
2587         case ISD::Telescope::MOUNT_ERROR:
2588             return failureBrush;
2589         case ISD::Telescope::MOUNT_MOVING:
2590         case ISD::Telescope::MOUNT_SLEWING:
2591             return progressBrush;
2592         case ISD::Telescope::MOUNT_TRACKING:
2593             return successBrush;
2594         case ISD::Telescope::MOUNT_PARKING:
2595             return stoppedBrush;
2596         case ISD::Telescope::MOUNT_PARKED:
2597             return stopped2Brush;
2598     }
2599     // Shouldn't get here.
2600     return offBrush;
2601 }
2602 
2603 }  // namespace
2604 
2605 // Mount status can be:
2606 // MOUNT_IDLE, MOUNT_MOVING, MOUNT_SLEWING, MOUNT_TRACKING, MOUNT_PARKING, MOUNT_PARKED, MOUNT_ERROR
mountState(ISD::Telescope::Status state)2607 void Analyze::mountState(ISD::Telescope::Status state)
2608 {
2609     QString statusString = mountStatusString(state);
2610     saveMessage("MountState", statusString);
2611     if (runtimeDisplay)
2612         processMountState(logTime(), statusString);
2613 }
2614 
processMountState(double time,const QString & statusString,bool batchMode)2615 void Analyze::processMountState(double time, const QString &statusString, bool batchMode)
2616 {
2617     ISD::Telescope::Status state = toMountStatus(statusString);
2618     if (mountStateStartedTime >= 0 && lastMountState != ISD::Telescope::MOUNT_IDLE)
2619     {
2620         addSession(mountStateStartedTime, time, MOUNT_Y, mountBrush(lastMountState));
2621         mountSessions.add(MountSession(mountStateStartedTime, time, nullptr, lastMountState));
2622     }
2623 
2624     if (state != ISD::Telescope::MOUNT_IDLE && !batchMode)
2625     {
2626         addTemporarySession(&temporaryMountSession, time, 1, MOUNT_Y,
2627                             (state == ISD::Telescope::MOUNT_TRACKING) ? successBrush : temporaryBrush);
2628         temporaryMountSession.state = state;
2629     }
2630     else
2631         removeTemporarySession(&temporaryMountSession);
2632 
2633     mountStateStartedTime = time;
2634     lastMountState = state;
2635     updateMaxX(time);
2636     if (!batchMode)
2637         replot();
2638 }
2639 
resetMountState()2640 void Analyze::resetMountState()
2641 {
2642     mountStateStartedTime = -1;
2643     lastMountState = ISD::Telescope::Status::MOUNT_IDLE;
2644 }
2645 
2646 // This message comes from the mount module
mountCoords(const SkyPoint & position,ISD::Telescope::PierSide pierSide,const dms & haValue)2647 void Analyze::mountCoords(const SkyPoint &position, ISD::Telescope::PierSide pierSide, const dms &haValue)
2648 {
2649     double ra = position.ra().Degrees();
2650     double dec = position.dec().Degrees();
2651     double ha = haValue.Degrees();
2652     double az = position.az().Degrees();
2653     double alt = position.alt().Degrees();
2654 
2655     // Only process the message if something's changed by 1/4 degree or more.
2656     constexpr double MIN_DEGREES_CHANGE = 0.25;
2657     if ((fabs(ra - lastMountRa) > MIN_DEGREES_CHANGE) ||
2658             (fabs(dec - lastMountDec) > MIN_DEGREES_CHANGE) ||
2659             (fabs(ha - lastMountHa) > MIN_DEGREES_CHANGE) ||
2660             (fabs(az - lastMountAz) > MIN_DEGREES_CHANGE) ||
2661             (fabs(alt - lastMountAlt) > MIN_DEGREES_CHANGE) ||
2662             (pierSide != lastMountPierSide))
2663     {
2664         saveMessage("MountCoords", QString("%1,%2,%3,%4,%5,%6")
2665                     .arg(QString::number(ra, 'f', 4))
2666                     .arg(QString::number(dec, 'f', 4))
2667                     .arg(QString::number(az, 'f', 4))
2668                     .arg(QString::number(alt, 'f', 4))
2669                     .arg(pierSide)
2670                     .arg(QString::number(ha, 'f', 4)));
2671 
2672         if (runtimeDisplay)
2673             processMountCoords(logTime(), ra, dec, az, alt, pierSide, ha);
2674 
2675         lastMountRa = ra;
2676         lastMountDec = dec;
2677         lastMountHa = ha;
2678         lastMountAz = az;
2679         lastMountAlt = alt;
2680         lastMountPierSide = pierSide;
2681     }
2682 }
2683 
processMountCoords(double time,double ra,double dec,double az,double alt,int pierSide,double ha,bool batchMode)2684 void Analyze::processMountCoords(double time, double ra, double dec, double az,
2685                                  double alt, int pierSide, double ha, bool batchMode)
2686 {
2687     addMountCoords(ra, dec, az, alt, pierSide, ha, time);
2688     updateMaxX(time);
2689     if (!batchMode)
2690         replot();
2691 }
2692 
resetMountCoords()2693 void Analyze::resetMountCoords()
2694 {
2695     lastMountRa = -1;
2696     lastMountDec = -1;
2697     lastMountHa = -1;
2698     lastMountAz = -1;
2699     lastMountAlt = -1;
2700     lastMountPierSide = -1;
2701 }
2702 
2703 namespace
2704 {
2705 
2706 // TODO: Move to mount.h/cpp?
convertMountFlipState(const QString & statusStr)2707 Mount::MeridianFlipStatus convertMountFlipState(const QString &statusStr)
2708 {
2709     if (statusStr == "FLIP_NONE")
2710         return Mount::FLIP_NONE;
2711     else if (statusStr == "FLIP_PLANNED")
2712         return Mount::FLIP_PLANNED;
2713     else if (statusStr == "FLIP_WAITING")
2714         return Mount::FLIP_WAITING;
2715     else if (statusStr == "FLIP_ACCEPTED")
2716         return Mount::FLIP_ACCEPTED;
2717     else if (statusStr == "FLIP_RUNNING")
2718         return Mount::FLIP_RUNNING;
2719     else if (statusStr == "FLIP_COMPLETED")
2720         return Mount::FLIP_COMPLETED;
2721     else if (statusStr == "FLIP_ERROR")
2722         return Mount::FLIP_ERROR;
2723     return Mount::FLIP_ERROR;
2724 }
2725 
mountFlipStateBrush(Mount::MeridianFlipStatus state)2726 QBrush mountFlipStateBrush(Mount::MeridianFlipStatus state)
2727 {
2728     switch (state)
2729     {
2730         case Mount::FLIP_NONE:
2731             return offBrush;
2732         case Mount::FLIP_PLANNED:
2733             return stoppedBrush;
2734         case Mount::FLIP_WAITING:
2735             return stopped2Brush;
2736         case Mount::FLIP_ACCEPTED:
2737             return progressBrush;
2738         case Mount::FLIP_RUNNING:
2739             return progress2Brush;
2740         case Mount::FLIP_COMPLETED:
2741             return successBrush;
2742         case Mount::FLIP_ERROR:
2743             return failureBrush;
2744     }
2745     // Shouldn't get here.
2746     return offBrush;
2747 }
2748 }  // namespace
2749 
mountFlipStatus(Mount::MeridianFlipStatus state)2750 void Analyze::mountFlipStatus(Mount::MeridianFlipStatus state)
2751 {
2752     if (state == lastMountFlipStateReceived)
2753         return;
2754     lastMountFlipStateReceived = state;
2755 
2756     QString stateStr = Mount::meridianFlipStatusString(state);
2757     saveMessage("MeridianFlipState", stateStr);
2758     if (runtimeDisplay)
2759         processMountFlipState(logTime(), stateStr);
2760 
2761 }
2762 
2763 // FLIP_NONE FLIP_PLANNED FLIP_WAITING FLIP_ACCEPTED FLIP_RUNNING FLIP_COMPLETED FLIP_ERROR
processMountFlipState(double time,const QString & statusString,bool batchMode)2764 void Analyze::processMountFlipState(double time, const QString &statusString, bool batchMode)
2765 {
2766     Mount::MeridianFlipStatus state = convertMountFlipState(statusString);
2767     if (state == lastMountFlipStateStarted)
2768         return;
2769 
2770     bool lastStateInteresting =
2771         (lastMountFlipStateStarted == Mount::FLIP_PLANNED ||
2772          lastMountFlipStateStarted == Mount::FLIP_WAITING ||
2773          lastMountFlipStateStarted == Mount::FLIP_ACCEPTED ||
2774          lastMountFlipStateStarted == Mount::FLIP_RUNNING);
2775     if (mountFlipStateStartedTime >= 0 && lastStateInteresting)
2776     {
2777         if (state == Mount::FLIP_COMPLETED || state == Mount::FLIP_ERROR)
2778         {
2779             // These states are really commentaries on the previous states.
2780             addSession(mountFlipStateStartedTime, time, MERIDIAN_FLIP_Y, mountFlipStateBrush(state));
2781             mountFlipSessions.add(MountFlipSession(mountFlipStateStartedTime, time, nullptr, state));
2782         }
2783         else
2784         {
2785             addSession(mountFlipStateStartedTime, time, MERIDIAN_FLIP_Y, mountFlipStateBrush(lastMountFlipStateStarted));
2786             mountFlipSessions.add(MountFlipSession(mountFlipStateStartedTime, time, nullptr, lastMountFlipStateStarted));
2787         }
2788     }
2789     bool stateInteresting =
2790         (state == Mount::FLIP_PLANNED ||
2791          state == Mount::FLIP_WAITING ||
2792          state == Mount::FLIP_ACCEPTED ||
2793          state == Mount::FLIP_RUNNING);
2794     if (stateInteresting && !batchMode)
2795     {
2796         addTemporarySession(&temporaryMountFlipSession, time, 1, MERIDIAN_FLIP_Y, temporaryBrush);
2797         temporaryMountFlipSession.state = state;
2798     }
2799     else
2800         removeTemporarySession(&temporaryMountFlipSession);
2801 
2802     mountFlipStateStartedTime = time;
2803     lastMountFlipStateStarted = state;
2804     updateMaxX(time);
2805     if (!batchMode)
2806         replot();
2807 }
2808 
resetMountFlipState()2809 void Analyze::resetMountFlipState()
2810 {
2811     lastMountFlipStateReceived = Mount::FLIP_NONE;
2812     lastMountFlipStateStarted = Mount::FLIP_NONE;
2813     mountFlipStateStartedTime = -1;
2814 }
2815 
2816 }  // namespace Ekos
2817