Kstars

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

KDE's Doxygen guidelines are available online.