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