File indexing completed on 2024-05-05 07:41:20
0001 /* 0002 SPDX-FileCopyrightText: 2020 Hy Murveit <hy@murveit.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "analyze.h" 0008 0009 #include <KNotifications/KNotification> 0010 #include <QDateTime> 0011 #include <QShortcut> 0012 #include <QtGlobal> 0013 #include <QColor> 0014 0015 #include "auxiliary/kspaths.h" 0016 #include "dms.h" 0017 #include "ekos/manager.h" 0018 #include "ekos/focus/curvefit.h" 0019 #include "fitsviewer/fitsdata.h" 0020 #include "fitsviewer/fitsviewer.h" 0021 #include "ksmessagebox.h" 0022 #include "kstars.h" 0023 #include "kstarsdata.h" 0024 #include "Options.h" 0025 #include "qcustomplot.h" 0026 0027 #include <ekos_analyze_debug.h> 0028 #include <KHelpClient> 0029 #include <version.h> 0030 0031 // Subclass QCPAxisTickerDateTime, so that times are offset from the start 0032 // of the log, instead of being offset from the UNIX 0-seconds time. 0033 class OffsetDateTimeTicker : public QCPAxisTickerDateTime 0034 { 0035 public: 0036 void setOffset(double offset) 0037 { 0038 timeOffset = offset; 0039 } 0040 QString getTickLabel(double tick, const QLocale &locale, QChar formatChar, int precision) override 0041 { 0042 Q_UNUSED(precision); 0043 Q_UNUSED(formatChar); 0044 // Seconds are offset from the unix origin by 0045 return locale.toString(keyToDateTime(tick + timeOffset).toTimeSpec(mDateTimeSpec), mDateTimeFormat); 0046 } 0047 private: 0048 double timeOffset = 0; 0049 }; 0050 0051 namespace 0052 { 0053 0054 // QDateTime is written to file with this format. 0055 QString timeFormat = "yyyy-MM-dd hh:mm:ss.zzz"; 0056 0057 // The resolution of the scroll bar. 0058 constexpr int MAX_SCROLL_VALUE = 10000; 0059 0060 // Half the height of a timeline line. 0061 // That is timeline lines are horizontal bars along y=1 or y=2 ... and their 0062 // vertical widths are from y-halfTimelineHeight to y+halfTimelineHeight. 0063 constexpr double halfTimelineHeight = 0.35; 0064 0065 // These are initialized in initStatsPlot when the graphs are added. 0066 // They index the graphs in statsPlot, e.g. statsPlot->graph(HFR_GRAPH)->addData(...) 0067 int HFR_GRAPH = -1; 0068 int TEMPERATURE_GRAPH = -1; 0069 int FOCUS_POSITION_GRAPH = -1; 0070 int NUM_CAPTURE_STARS_GRAPH = -1; 0071 int MEDIAN_GRAPH = -1; 0072 int ECCENTRICITY_GRAPH = -1; 0073 int NUMSTARS_GRAPH = -1; 0074 int SKYBG_GRAPH = -1; 0075 int SNR_GRAPH = -1; 0076 int RA_GRAPH = -1; 0077 int DEC_GRAPH = -1; 0078 int RA_PULSE_GRAPH = -1; 0079 int DEC_PULSE_GRAPH = -1; 0080 int DRIFT_GRAPH = -1; 0081 int RMS_GRAPH = -1; 0082 int CAPTURE_RMS_GRAPH = -1; 0083 int MOUNT_RA_GRAPH = -1; 0084 int MOUNT_DEC_GRAPH = -1; 0085 int MOUNT_HA_GRAPH = -1; 0086 int AZ_GRAPH = -1; 0087 int ALT_GRAPH = -1; 0088 int PIER_SIDE_GRAPH = -1; 0089 int TARGET_DISTANCE_GRAPH = -1; 0090 0091 // This one is in timelinePlot. 0092 int ADAPTIVE_FOCUS_GRAPH = -1; 0093 0094 // Initialized in initGraphicsPlot(). 0095 int FOCUS_GRAPHICS = -1; 0096 int FOCUS_GRAPHICS_FINAL = -1; 0097 int FOCUS_GRAPHICS_CURVE = -1; 0098 int GUIDER_GRAPHICS = -1; 0099 0100 // Brushes used in the timeline plot. 0101 const QBrush temporaryBrush(Qt::green, Qt::DiagCrossPattern); 0102 const QBrush timelineSelectionBrush(QColor(255, 100, 100, 150), Qt::SolidPattern); 0103 const QBrush successBrush(Qt::green, Qt::SolidPattern); 0104 const QBrush failureBrush(Qt::red, Qt::SolidPattern); 0105 const QBrush offBrush(Qt::gray, Qt::SolidPattern); 0106 const QBrush progressBrush(Qt::blue, Qt::SolidPattern); 0107 const QBrush progress2Brush(QColor(0, 165, 255), Qt::SolidPattern); 0108 const QBrush progress3Brush(Qt::cyan, Qt::SolidPattern); 0109 const QBrush progress4Brush(Qt::darkGreen, Qt::SolidPattern); 0110 const QBrush stoppedBrush(Qt::yellow, Qt::SolidPattern); 0111 const QBrush stopped2Brush(Qt::darkYellow, Qt::SolidPattern); 0112 0113 // Utility to checks if a file exists and is not a directory. 0114 bool fileExists(const QString &path) 0115 { 0116 QFileInfo info(path); 0117 return info.exists() && info.isFile(); 0118 } 0119 0120 // Utilities to go between a mount status and a string. 0121 // Move to inditelescope.h/cpp? 0122 const QString mountStatusString(ISD::Mount::Status status) 0123 { 0124 switch (status) 0125 { 0126 case ISD::Mount::MOUNT_IDLE: 0127 return i18n("Idle"); 0128 case ISD::Mount::MOUNT_PARKED: 0129 return i18n("Parked"); 0130 case ISD::Mount::MOUNT_PARKING: 0131 return i18n("Parking"); 0132 case ISD::Mount::MOUNT_SLEWING: 0133 return i18n("Slewing"); 0134 case ISD::Mount::MOUNT_MOVING: 0135 return i18n("Moving"); 0136 case ISD::Mount::MOUNT_TRACKING: 0137 return i18n("Tracking"); 0138 case ISD::Mount::MOUNT_ERROR: 0139 return i18n("Error"); 0140 } 0141 return i18n("Error"); 0142 } 0143 0144 ISD::Mount::Status toMountStatus(const QString &str) 0145 { 0146 if (str == i18n("Idle")) 0147 return ISD::Mount::MOUNT_IDLE; 0148 else if (str == i18n("Parked")) 0149 return ISD::Mount::MOUNT_PARKED; 0150 else if (str == i18n("Parking")) 0151 return ISD::Mount::MOUNT_PARKING; 0152 else if (str == i18n("Slewing")) 0153 return ISD::Mount::MOUNT_SLEWING; 0154 else if (str == i18n("Moving")) 0155 return ISD::Mount::MOUNT_MOVING; 0156 else if (str == i18n("Tracking")) 0157 return ISD::Mount::MOUNT_TRACKING; 0158 else 0159 return ISD::Mount::MOUNT_ERROR; 0160 } 0161 0162 // Returns the stripe color used when drawing the capture timeline for various filters. 0163 // TODO: Not sure how to internationalize this. 0164 bool filterStripeBrush(const QString &filter, QBrush *brush) 0165 { 0166 const QRegularExpression::PatternOption c = QRegularExpression::CaseInsensitiveOption; 0167 0168 const QString rPattern("^(red|r)$"); 0169 if (QRegularExpression(rPattern, c).match(filter).hasMatch()) 0170 { 0171 *brush = QBrush(Qt::red, Qt::SolidPattern); 0172 return true; 0173 } 0174 const QString gPattern("^(green|g)$"); 0175 if (QRegularExpression(gPattern, c).match(filter).hasMatch()) 0176 { 0177 *brush = QBrush(Qt::green, Qt::SolidPattern); 0178 return true; 0179 } 0180 const QString bPattern("^(blue|b)$"); 0181 if (QRegularExpression(bPattern, c).match(filter).hasMatch()) 0182 { 0183 *brush = QBrush(Qt::blue, Qt::SolidPattern); 0184 return true; 0185 } 0186 const QString hPattern("^(ha|h|h-a|h_a|h-alpha|hydrogen|hydrogen_alpha|hydrogen-alpha|h_alpha|halpha)$"); 0187 if (QRegularExpression(hPattern, c).match(filter).hasMatch()) 0188 { 0189 *brush = QBrush(Qt::darkRed, Qt::SolidPattern); 0190 return true; 0191 } 0192 const QString oPattern("^(oiii|oxygen|oxygen_3|oxygen-3|oxygen_iii|oxygen-iii|o_iii|o-iii|o_3|o-3|o3)$"); 0193 if (QRegularExpression(oPattern, c).match(filter).hasMatch()) 0194 { 0195 *brush = QBrush(Qt::cyan, Qt::SolidPattern); 0196 return true; 0197 } 0198 const QString 0199 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)$"); 0200 if (QRegularExpression(sPattern, c).match(filter).hasMatch()) 0201 { 0202 // Pink. 0203 *brush = QBrush(QColor(255, 182, 193), Qt::SolidPattern); 0204 return true; 0205 } 0206 const QString lPattern("^(lpr|L|UV-IR cut|UV-IR|white|monochrome|broadband|clear|focus|luminance|lum|lps|cls)$"); 0207 if (QRegularExpression(lPattern, c).match(filter).hasMatch()) 0208 { 0209 *brush = QBrush(Qt::white, Qt::SolidPattern); 0210 return true; 0211 } 0212 return false; 0213 } 0214 0215 // Used when searching for FITS files to display. 0216 // If filename isn't found as is, it tries alterateDirectory in several ways 0217 // e.g. if filename = /1/2/3/4/name is not found, then try alternateDirectory/name, 0218 // then alternateDirectory/4/name, then alternateDirectory/3/4/name, 0219 // then alternateDirectory/2/3/4/name, and so on. 0220 // If it cannot find the FITS file, it returns an empty string, otherwise it returns 0221 // the full path where the file was found. 0222 QString findFilename(const QString &filename, const QString &alternateDirectory) 0223 { 0224 // Try the origial full path. 0225 QFileInfo info(filename); 0226 if (info.exists() && info.isFile()) 0227 return filename; 0228 0229 // Try putting the filename at the end of the full path onto alternateDirectory. 0230 QString name = info.fileName(); 0231 QString temp = QString("%1/%2").arg(alternateDirectory, name); 0232 if (fileExists(temp)) 0233 return temp; 0234 0235 // Try appending the filename plus the ending directories onto alternateDirectory. 0236 int size = filename.size(); 0237 int searchBackFrom = size - name.size(); 0238 int num = 0; 0239 while (searchBackFrom >= 0) 0240 { 0241 int index = filename.lastIndexOf('/', searchBackFrom); 0242 if (index < 0) 0243 break; 0244 0245 QString temp2 = QString("%1%2").arg(alternateDirectory, filename.right(size - index)); 0246 if (fileExists(temp2)) 0247 return temp2; 0248 0249 searchBackFrom = index - 1; 0250 0251 // Paranoia 0252 if (++num > 20) 0253 break; 0254 } 0255 return ""; 0256 } 0257 0258 // This is an exhaustive search for now. 0259 // This is reasonable as the number of sessions should be limited. 0260 template <class T> 0261 class IntervalFinder 0262 { 0263 public: 0264 IntervalFinder() {} 0265 ~IntervalFinder() {} 0266 void add(T value) 0267 { 0268 intervals.append(value); 0269 } 0270 void clear() 0271 { 0272 intervals.clear(); 0273 } 0274 QList<T> find(double t) 0275 { 0276 QList<T> result; 0277 for (const auto &interval : intervals) 0278 { 0279 if (t >= interval.start && t <= interval.end) 0280 result.push_back(interval); 0281 } 0282 return result; 0283 } 0284 // Finds the interval AFTER t, not including t 0285 T *findNext(double t) 0286 { 0287 double bestStart = 1e7; 0288 T *result = nullptr; 0289 for (auto &interval : intervals) 0290 { 0291 if (interval.start > t && interval.start < bestStart) 0292 { 0293 bestStart = interval.start; 0294 result = &interval; 0295 } 0296 } 0297 return result; 0298 } 0299 // Finds the interval BEFORE t, not including t 0300 T *findPrevious(double t) 0301 { 0302 double bestStart = -1e7; 0303 T *result = nullptr; 0304 for (auto &interval : intervals) 0305 { 0306 if (interval.start < t && interval.start > bestStart) 0307 { 0308 bestStart = interval.start; 0309 result = &interval; 0310 } 0311 } 0312 return result; 0313 } 0314 private: 0315 QList<T> intervals; 0316 }; 0317 0318 IntervalFinder<Ekos::Analyze::CaptureSession> captureSessions; 0319 IntervalFinder<Ekos::Analyze::FocusSession> focusSessions; 0320 IntervalFinder<Ekos::Analyze::GuideSession> guideSessions; 0321 IntervalFinder<Ekos::Analyze::MountSession> mountSessions; 0322 IntervalFinder<Ekos::Analyze::AlignSession> alignSessions; 0323 IntervalFinder<Ekos::Analyze::MountFlipSession> mountFlipSessions; 0324 IntervalFinder<Ekos::Analyze::SchedulerJobSession> schedulerJobSessions; 0325 0326 } // namespace 0327 0328 namespace Ekos 0329 { 0330 0331 // RmsFilter computes the RMS error of a 2-D sequence. Input the x error and y error 0332 // into newSample(). It returns the sqrt of an approximate moving average of the squared 0333 // errors roughly averaged over 40 samples--implemented by a simple digital low-pass filter. 0334 // It's used to compute RMS guider errors, where x and y would be RA and DEC errors. 0335 class RmsFilter 0336 { 0337 public: 0338 RmsFilter() 0339 { 0340 constexpr double timeConstant = 40.0; 0341 alpha = 1.0 / pow(timeConstant, 0.865); 0342 } 0343 void resetFilter() 0344 { 0345 filteredRMS = 0; 0346 } 0347 double newSample(double x, double y) 0348 { 0349 const double valueSquared = x * x + y * y; 0350 filteredRMS = alpha * valueSquared + (1.0 - alpha) * filteredRMS; 0351 return sqrt(filteredRMS); 0352 } 0353 private: 0354 double alpha { 0 }; 0355 double filteredRMS { 0 }; 0356 }; 0357 0358 bool Analyze::eventFilter(QObject *obj, QEvent *ev) 0359 { 0360 // Quit if click wasn't on a QLineEdit. 0361 if (qobject_cast<QLineEdit*>(obj) == nullptr) 0362 return false; 0363 0364 // This filter only applies to single or double clicks. 0365 if (ev->type() != QEvent::MouseButtonDblClick && ev->type() != QEvent::MouseButtonPress) 0366 return false; 0367 0368 auto axisEntry = yAxisMap.find(obj); 0369 if (axisEntry == yAxisMap.end()) 0370 return false; 0371 0372 const bool isRightClick = (ev->type() == QEvent::MouseButtonPress) && 0373 (static_cast<QMouseEvent*>(ev)->button() == Qt::RightButton); 0374 const bool isControlClick = (ev->type() == QEvent::MouseButtonPress) && 0375 (static_cast<QMouseEvent*>(ev)->modifiers() & 0376 Qt::KeyboardModifier::ControlModifier); 0377 const bool isShiftClick = (ev->type() == QEvent::MouseButtonPress) && 0378 (static_cast<QMouseEvent*>(ev)->modifiers() & 0379 Qt::KeyboardModifier::ShiftModifier); 0380 0381 if (ev->type() == QEvent::MouseButtonDblClick || isRightClick || isControlClick || isShiftClick) 0382 { 0383 startYAxisTool(axisEntry->first, axisEntry->second); 0384 clickTimer.stop(); 0385 return true; 0386 } 0387 else if (ev->type() == QEvent::MouseButtonPress) 0388 { 0389 clickTimer.setSingleShot(true); 0390 clickTimer.setInterval(250); 0391 clickTimer.start(); 0392 m_ClickTimerInfo = axisEntry->second; 0393 // Wait 0.25 seconds to see if this is a double click or just a single click. 0394 connect(&clickTimer, &QTimer::timeout, this, [&]() 0395 { 0396 m_YAxisTool.reject(); 0397 if (m_ClickTimerInfo.checkBox && !m_ClickTimerInfo.checkBox->isChecked()) 0398 { 0399 // Enable the graph. 0400 m_ClickTimerInfo.checkBox->setChecked(true); 0401 statsPlot->graph(m_ClickTimerInfo.graphIndex)->setVisible(true); 0402 statsPlot->graph(m_ClickTimerInfo.graphIndex)->addToLegend(); 0403 } 0404 userSetLeftAxis(m_ClickTimerInfo.axis); 0405 }); 0406 return true; 0407 } 0408 return false; 0409 } 0410 0411 Analyze::Analyze() : m_YAxisTool(this) 0412 { 0413 setupUi(this); 0414 0415 captureRms.reset(new RmsFilter); 0416 guiderRms.reset(new RmsFilter); 0417 0418 alternateFolder = QDir::homePath(); 0419 0420 initInputSelection(); 0421 initTimelinePlot(); 0422 0423 initStatsPlot(); 0424 connect(&m_YAxisTool, &YAxisTool::axisChanged, this, &Analyze::userChangedYAxis); 0425 connect(&m_YAxisTool, &YAxisTool::leftAxisChanged, this, &Analyze::userSetLeftAxis); 0426 connect(&m_YAxisTool, &YAxisTool::axisColorChanged, this, &Analyze::userSetAxisColor); 0427 qApp->installEventFilter(this); 0428 0429 initGraphicsPlot(); 0430 fullWidthCB->setChecked(true); 0431 keepCurrentCB->setChecked(true); 0432 runtimeDisplay = true; 0433 fullWidthCB->setVisible(true); 0434 fullWidthCB->setDisabled(false); 0435 0436 // Initialize the checkboxes that allow the user to make (in)visible 0437 // each of the 4 main displays in Analyze. 0438 detailsCB->setChecked(true); 0439 statsCB->setChecked(true); 0440 graphsCB->setChecked(true); 0441 timelineCB->setChecked(true); 0442 setVisibility(); 0443 connect(timelineCB, &QCheckBox::stateChanged, this, &Analyze::setVisibility); 0444 connect(graphsCB, &QCheckBox::stateChanged, this, &Analyze::setVisibility); 0445 connect(statsCB, &QCheckBox::stateChanged, this, &Analyze::setVisibility); 0446 connect(detailsCB, &QCheckBox::stateChanged, this, &Analyze::setVisibility); 0447 0448 connect(fullWidthCB, &QCheckBox::toggled, [ = ](bool checked) 0449 { 0450 if (checked) 0451 this->replot(); 0452 }); 0453 0454 initStatsCheckboxes(); 0455 0456 connect(zoomInB, &QPushButton::clicked, this, &Analyze::zoomIn); 0457 connect(zoomOutB, &QPushButton::clicked, this, &Analyze::zoomOut); 0458 connect(prevSessionB, &QPushButton::clicked, this, &Analyze::previousTimelineItem); 0459 connect(nextSessionB, &QPushButton::clicked, this, &Analyze::nextTimelineItem); 0460 connect(timelinePlot, &QCustomPlot::mousePress, this, &Analyze::timelineMousePress); 0461 connect(timelinePlot, &QCustomPlot::mouseDoubleClick, this, &Analyze::timelineMouseDoubleClick); 0462 connect(timelinePlot, &QCustomPlot::mouseWheel, this, &Analyze::timelineMouseWheel); 0463 connect(statsPlot, &QCustomPlot::mousePress, this, &Analyze::statsMousePress); 0464 connect(statsPlot, &QCustomPlot::mouseDoubleClick, this, &Analyze::statsMouseDoubleClick); 0465 connect(statsPlot, &QCustomPlot::mouseMove, this, &Analyze::statsMouseMove); 0466 connect(analyzeSB, &QScrollBar::valueChanged, this, &Analyze::scroll); 0467 analyzeSB->setRange(0, MAX_SCROLL_VALUE); 0468 connect(helpB, &QPushButton::clicked, this, &Analyze::helpMessage); 0469 connect(keepCurrentCB, &QCheckBox::stateChanged, this, &Analyze::keepCurrent); 0470 0471 setupKeyboardShortcuts(this); 0472 0473 reset(); 0474 replot(); 0475 } 0476 0477 void Analyze::setVisibility() 0478 { 0479 detailsWidget->setVisible(detailsCB->isChecked()); 0480 statsGridWidget->setVisible(statsCB->isChecked()); 0481 timelinePlot->setVisible(timelineCB->isChecked()); 0482 statsPlot->setVisible(graphsCB->isChecked()); 0483 replot(); 0484 } 0485 0486 // Mouse wheel over the Timeline plot causes an x-axis zoom. 0487 void Analyze::timelineMouseWheel(QWheelEvent *event) 0488 { 0489 if (event->angleDelta().y() > 0) 0490 zoomIn(); 0491 else if (event->angleDelta().y() < 0) 0492 zoomOut(); 0493 } 0494 0495 // This callback is used so that when keepCurrent is checked, we replot immediately. 0496 // The actual keepCurrent work is done in replot(). 0497 void Analyze::keepCurrent(int state) 0498 { 0499 Q_UNUSED(state); 0500 if (keepCurrentCB->isChecked()) 0501 { 0502 removeStatsCursor(); 0503 replot(); 0504 } 0505 } 0506 0507 // Implements the input selection UI. User can either choose the current Ekos 0508 // session, or a file read from disk, or set the alternateDirectory variable. 0509 void Analyze::initInputSelection() 0510 { 0511 // Setup the input combo box. 0512 dirPath = QUrl::fromLocalFile(QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("analyze")); 0513 0514 inputCombo->addItem(i18n("Current Session")); 0515 inputCombo->addItem(i18n("Read from File")); 0516 inputCombo->addItem(i18n("Set alternative image-file base directory")); 0517 inputValue->setText(""); 0518 connect(inputCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, [&](int index) 0519 { 0520 if (index == 0) 0521 { 0522 // Input from current session 0523 if (!runtimeDisplay) 0524 { 0525 reset(); 0526 inputValue->setText(i18n("Current Session")); 0527 maxXValue = readDataFromFile(logFilename); 0528 runtimeDisplay = true; 0529 } 0530 fullWidthCB->setChecked(true); 0531 fullWidthCB->setVisible(true); 0532 fullWidthCB->setDisabled(false); 0533 replot(); 0534 } 0535 else if (index == 1) 0536 { 0537 // The i18n call below is broken up (and the word "analyze" is protected from it) because i18n 0538 // translates "analyze" to "analyse" for the English UK locale, but we need to keep it ".analyze" 0539 // because that's what how the files are named. 0540 QUrl inputURL = QFileDialog::getOpenFileUrl(this, i18nc("@title:window", "Select input file"), dirPath, 0541 QString("Analyze %1 (*.analyze);;%2").arg(i18n("Log")).arg(i18n("All Files (*)"))); 0542 if (inputURL.isEmpty()) 0543 return; 0544 dirPath = QUrl(inputURL.url(QUrl::RemoveFilename)); 0545 0546 reset(); 0547 inputValue->setText(inputURL.fileName()); 0548 0549 // If we do this after the readData call below, it would animate the sequence. 0550 runtimeDisplay = false; 0551 0552 maxXValue = readDataFromFile(inputURL.toLocalFile()); 0553 checkForMissingSchedulerJobEnd(maxXValue); 0554 plotStart = 0; 0555 plotWidth = maxXValue + 5; 0556 replot(); 0557 } 0558 else if (index == 2) 0559 { 0560 QString dir = QFileDialog::getExistingDirectory( 0561 this, i18n("Set an alternate base directory for your captured images"), 0562 QDir::homePath(), 0563 QFileDialog::ShowDirsOnly); 0564 if (dir.size() > 0) 0565 { 0566 // TODO: replace with an option. 0567 alternateFolder = dir; 0568 } 0569 // This is not a destiation, reset to one of the above. 0570 if (runtimeDisplay) 0571 inputCombo->setCurrentIndex(0); 0572 else 0573 inputCombo->setCurrentIndex(1); 0574 } 0575 }); 0576 } 0577 0578 void Analyze::setupKeyboardShortcuts(QWidget *plot) 0579 { 0580 // Shortcuts defined: https://doc.qt.io/archives/qt-4.8/qkeysequence.html#standard-shortcuts 0581 QShortcut *s = new QShortcut(QKeySequence(QKeySequence::ZoomIn), plot); 0582 connect(s, &QShortcut::activated, this, &Analyze::zoomIn); 0583 s = new QShortcut(QKeySequence(QKeySequence::ZoomOut), plot); 0584 connect(s, &QShortcut::activated, this, &Analyze::zoomOut); 0585 0586 s = new QShortcut(QKeySequence(QKeySequence::MoveToNextChar), plot); 0587 connect(s, &QShortcut::activated, this, &Analyze::scrollRight); 0588 s = new QShortcut(QKeySequence(QKeySequence::MoveToPreviousChar), plot); 0589 connect(s, &QShortcut::activated, this, &Analyze::scrollLeft); 0590 0591 s = new QShortcut(QKeySequence(QKeySequence::MoveToNextWord), plot); 0592 connect(s, &QShortcut::activated, this, &Analyze::nextTimelineItem); 0593 s = new QShortcut(QKeySequence(QKeySequence::MoveToPreviousWord), plot); 0594 connect(s, &QShortcut::activated, this, &Analyze::previousTimelineItem); 0595 0596 s = new QShortcut(QKeySequence(QKeySequence::SelectNextWord), plot); 0597 connect(s, &QShortcut::activated, this, &Analyze::nextTimelineItem); 0598 s = new QShortcut(QKeySequence(QKeySequence::SelectPreviousWord), plot); 0599 connect(s, &QShortcut::activated, this, &Analyze::previousTimelineItem); 0600 0601 s = new QShortcut(QKeySequence(QKeySequence::MoveToNextLine), plot); 0602 connect(s, &QShortcut::activated, this, &Analyze::statsYZoomIn); 0603 s = new QShortcut(QKeySequence(QKeySequence::MoveToPreviousLine), plot); 0604 connect(s, &QShortcut::activated, this, &Analyze::statsYZoomOut); 0605 s = new QShortcut(QKeySequence("?"), plot); 0606 connect(s, &QShortcut::activated, this, &Analyze::helpMessage); 0607 s = new QShortcut(QKeySequence("h"), plot); 0608 connect(s, &QShortcut::activated, this, &Analyze::helpMessage); 0609 s = new QShortcut(QKeySequence(QKeySequence::HelpContents), plot); 0610 connect(s, &QShortcut::activated, this, &Analyze::helpMessage); 0611 } 0612 0613 Analyze::~Analyze() 0614 { 0615 // TODO: 0616 // We should write out to disk any sessions that haven't terminated 0617 // (e.g. capture, focus, guide) 0618 } 0619 0620 void Analyze::setSelectedSession(const Session &s) 0621 { 0622 m_selectedSession = s; 0623 } 0624 0625 void Analyze::clearSelectedSession() 0626 { 0627 m_selectedSession = Session(); 0628 } 0629 0630 // When a user selects a timeline session, the previously selected one 0631 // is deselected. Note: this does not replot(). 0632 void Analyze::unhighlightTimelineItem() 0633 { 0634 clearSelectedSession(); 0635 if (selectionHighlight != nullptr) 0636 { 0637 timelinePlot->removeItem(selectionHighlight); 0638 selectionHighlight = nullptr; 0639 } 0640 detailsTable->clear(); 0641 prevSessionB->setDisabled(true); 0642 nextSessionB->setDisabled(true); 0643 } 0644 0645 // Highlight the area between start and end of the session on row y in Timeline. 0646 // Note that this doesn't replot(). 0647 void Analyze::highlightTimelineItem(const Session &session) 0648 { 0649 constexpr double halfHeight = 0.5; 0650 unhighlightTimelineItem(); 0651 0652 setSelectedSession(session); 0653 QCPItemRect *rect = new QCPItemRect(timelinePlot); 0654 rect->topLeft->setCoords(session.start, session.offset + halfHeight); 0655 rect->bottomRight->setCoords(session.end, session.offset - halfHeight); 0656 rect->setBrush(timelineSelectionBrush); 0657 selectionHighlight = rect; 0658 prevSessionB->setDisabled(false); 0659 nextSessionB->setDisabled(false); 0660 0661 } 0662 0663 // Creates a fat line-segment on the Timeline, optionally with a stripe in the middle. 0664 QCPItemRect * Analyze::addSession(double start, double end, double y, 0665 const QBrush &brush, const QBrush *stripeBrush) 0666 { 0667 QPen pen = QPen(Qt::black, 1, Qt::SolidLine); 0668 QCPItemRect *rect = new QCPItemRect(timelinePlot); 0669 rect->topLeft->setCoords(start, y + halfTimelineHeight); 0670 rect->bottomRight->setCoords(end, y - halfTimelineHeight); 0671 rect->setPen(pen); 0672 rect->setSelectedPen(pen); 0673 rect->setBrush(brush); 0674 rect->setSelectedBrush(brush); 0675 0676 if (stripeBrush != nullptr) 0677 { 0678 QCPItemRect *stripe = new QCPItemRect(timelinePlot); 0679 stripe->topLeft->setCoords(start, y + halfTimelineHeight / 2.0); 0680 stripe->bottomRight->setCoords(end, y - halfTimelineHeight / 2.0); 0681 stripe->setPen(pen); 0682 stripe->setBrush(*stripeBrush); 0683 } 0684 return rect; 0685 } 0686 0687 // Add the guide stats values to the Stats graphs. 0688 // We want to avoid drawing guide-stat values when not guiding. 0689 // That is, we have no input samples then, but the graph would connect 0690 // two points with a line. By adding NaN values into the graph, 0691 // those places are made invisible. 0692 void Analyze::addGuideStats(double raDrift, double decDrift, int raPulse, int decPulse, double snr, 0693 int numStars, double skyBackground, double time) 0694 { 0695 double MAX_GUIDE_STATS_GAP = 30; 0696 0697 if (time - lastGuideStatsTime > MAX_GUIDE_STATS_GAP && 0698 lastGuideStatsTime >= 0) 0699 { 0700 addGuideStatsInternal(qQNaN(), qQNaN(), 0, 0, qQNaN(), qQNaN(), qQNaN(), qQNaN(), qQNaN(), 0701 lastGuideStatsTime + .0001); 0702 addGuideStatsInternal(qQNaN(), qQNaN(), 0, 0, qQNaN(), qQNaN(), qQNaN(), qQNaN(), qQNaN(), time - .0001); 0703 guiderRms->resetFilter(); 0704 } 0705 0706 const double drift = std::hypot(raDrift, decDrift); 0707 0708 // To compute the RMS error, which is sqrt(sum square error / N), filter the squared 0709 // error, which effectively returns sum squared error / N, and take the sqrt. 0710 // This is done by RmsFilter::newSample(). 0711 const double rms = guiderRms->newSample(raDrift, decDrift); 0712 addGuideStatsInternal(raDrift, decDrift, double(raPulse), double(decPulse), snr, numStars, skyBackground, drift, rms, time); 0713 0714 // If capture is active, plot the capture RMS. 0715 if (captureStartedTime >= 0) 0716 { 0717 // lastCaptureRmsTime is the last time we plotted a capture RMS value. 0718 // If we have plotted values previously, and there's a gap in guiding 0719 // we must place NaN values in the graph surrounding the gap. 0720 if ((lastCaptureRmsTime >= 0) && 0721 (time - lastCaptureRmsTime > MAX_GUIDE_STATS_GAP)) 0722 { 0723 // this is the first sample in a series with a gap behind us. 0724 statsPlot->graph(CAPTURE_RMS_GRAPH)->addData(lastCaptureRmsTime + .0001, qQNaN()); 0725 statsPlot->graph(CAPTURE_RMS_GRAPH)->addData(time - .0001, qQNaN()); 0726 captureRms->resetFilter(); 0727 } 0728 const double rmsC = captureRms->newSample(raDrift, decDrift); 0729 statsPlot->graph(CAPTURE_RMS_GRAPH)->addData(time, rmsC); 0730 lastCaptureRmsTime = time; 0731 } 0732 0733 lastGuideStatsTime = time; 0734 } 0735 0736 void Analyze::addGuideStatsInternal(double raDrift, double decDrift, double raPulse, 0737 double decPulse, double snr, 0738 double numStars, double skyBackground, 0739 double drift, double rms, double time) 0740 { 0741 statsPlot->graph(RA_GRAPH)->addData(time, raDrift); 0742 statsPlot->graph(DEC_GRAPH)->addData(time, decDrift); 0743 statsPlot->graph(RA_PULSE_GRAPH)->addData(time, raPulse); 0744 statsPlot->graph(DEC_PULSE_GRAPH)->addData(time, decPulse); 0745 statsPlot->graph(DRIFT_GRAPH)->addData(time, drift); 0746 statsPlot->graph(RMS_GRAPH)->addData(time, rms); 0747 0748 // Set the SNR axis' maximum to 95% of the way up from the middle to the top. 0749 if (!qIsNaN(snr)) 0750 snrMax = std::max(snr, snrMax); 0751 if (!qIsNaN(skyBackground)) 0752 skyBgMax = std::max(skyBackground, skyBgMax); 0753 if (!qIsNaN(numStars)) 0754 numStarsMax = std::max(numStars, static_cast<double>(numStarsMax)); 0755 0756 statsPlot->graph(SNR_GRAPH)->addData(time, snr); 0757 statsPlot->graph(NUMSTARS_GRAPH)->addData(time, numStars); 0758 statsPlot->graph(SKYBG_GRAPH)->addData(time, skyBackground); 0759 } 0760 0761 void Analyze::addTemperature(double temperature, double time) 0762 { 0763 // The HFR corresponds to the last capture 0764 // If there is no temperature sensor, focus sends a large negative value. 0765 if (temperature > -200) 0766 statsPlot->graph(TEMPERATURE_GRAPH)->addData(time, temperature); 0767 } 0768 0769 void Analyze::addFocusPosition(double focusPosition, double time) 0770 { 0771 statsPlot->graph(FOCUS_POSITION_GRAPH)->addData(time, focusPosition); 0772 } 0773 0774 void Analyze::addTargetDistance(double targetDistance, double time) 0775 { 0776 // The target distance corresponds to the last capture 0777 if (previousCaptureStartedTime >= 0 && previousCaptureCompletedTime >= 0 && 0778 previousCaptureStartedTime < previousCaptureCompletedTime && 0779 previousCaptureCompletedTime <= time) 0780 { 0781 statsPlot->graph(TARGET_DISTANCE_GRAPH)->addData(previousCaptureStartedTime - .0001, qQNaN()); 0782 statsPlot->graph(TARGET_DISTANCE_GRAPH)->addData(previousCaptureStartedTime, targetDistance); 0783 statsPlot->graph(TARGET_DISTANCE_GRAPH)->addData(previousCaptureCompletedTime, targetDistance); 0784 statsPlot->graph(TARGET_DISTANCE_GRAPH)->addData(previousCaptureCompletedTime + .0001, qQNaN()); 0785 } 0786 } 0787 0788 // Add the HFR values to the Stats graph, as a constant value between startTime and time. 0789 void Analyze::addHFR(double hfr, int numCaptureStars, int median, double eccentricity, 0790 double time, double startTime) 0791 { 0792 // The HFR corresponds to the last capture 0793 statsPlot->graph(HFR_GRAPH)->addData(startTime - .0001, qQNaN()); 0794 statsPlot->graph(HFR_GRAPH)->addData(startTime, hfr); 0795 statsPlot->graph(HFR_GRAPH)->addData(time, hfr); 0796 statsPlot->graph(HFR_GRAPH)->addData(time + .0001, qQNaN()); 0797 0798 statsPlot->graph(NUM_CAPTURE_STARS_GRAPH)->addData(startTime - .0001, qQNaN()); 0799 statsPlot->graph(NUM_CAPTURE_STARS_GRAPH)->addData(startTime, numCaptureStars); 0800 statsPlot->graph(NUM_CAPTURE_STARS_GRAPH)->addData(time, numCaptureStars); 0801 statsPlot->graph(NUM_CAPTURE_STARS_GRAPH)->addData(time + .0001, qQNaN()); 0802 0803 statsPlot->graph(MEDIAN_GRAPH)->addData(startTime - .0001, qQNaN()); 0804 statsPlot->graph(MEDIAN_GRAPH)->addData(startTime, median); 0805 statsPlot->graph(MEDIAN_GRAPH)->addData(time, median); 0806 statsPlot->graph(MEDIAN_GRAPH)->addData(time + .0001, qQNaN()); 0807 0808 statsPlot->graph(ECCENTRICITY_GRAPH)->addData(startTime - .0001, qQNaN()); 0809 statsPlot->graph(ECCENTRICITY_GRAPH)->addData(startTime, eccentricity); 0810 statsPlot->graph(ECCENTRICITY_GRAPH)->addData(time, eccentricity); 0811 statsPlot->graph(ECCENTRICITY_GRAPH)->addData(time + .0001, qQNaN()); 0812 0813 medianMax = std::max(median, medianMax); 0814 numCaptureStarsMax = std::max(numCaptureStars, numCaptureStarsMax); 0815 } 0816 0817 // Add the Mount Coordinates values to the Stats graph. 0818 // All but pierSide are in double degrees. 0819 void Analyze::addMountCoords(double ra, double dec, double az, 0820 double alt, int pierSide, double ha, double time) 0821 { 0822 statsPlot->graph(MOUNT_RA_GRAPH)->addData(time, ra); 0823 statsPlot->graph(MOUNT_DEC_GRAPH)->addData(time, dec); 0824 statsPlot->graph(MOUNT_HA_GRAPH)->addData(time, ha); 0825 statsPlot->graph(AZ_GRAPH)->addData(time, az); 0826 statsPlot->graph(ALT_GRAPH)->addData(time, alt); 0827 statsPlot->graph(PIER_SIDE_GRAPH)->addData(time, double(pierSide)); 0828 } 0829 0830 // Read a .analyze file, and setup all the graphics. 0831 double Analyze::readDataFromFile(const QString &filename) 0832 { 0833 double lastTime = 10; 0834 QFile inputFile(filename); 0835 if (inputFile.open(QIODevice::ReadOnly)) 0836 { 0837 QTextStream in(&inputFile); 0838 while (!in.atEnd()) 0839 { 0840 QString line = in.readLine(); 0841 double time = processInputLine(line); 0842 if (time > lastTime) 0843 lastTime = time; 0844 } 0845 inputFile.close(); 0846 } 0847 return lastTime; 0848 } 0849 0850 // Process an input line read from a .analyze file. 0851 double Analyze::processInputLine(const QString &line) 0852 { 0853 bool ok; 0854 // Break the line into comma-separated components 0855 QStringList list = line.split(QLatin1Char(',')); 0856 // We need at least a command and a timestamp 0857 if (list.size() < 2) 0858 return 0; 0859 if (list[0].at(0).toLatin1() == '#') 0860 { 0861 // Comment character # must be at start of line. 0862 return 0; 0863 } 0864 0865 if ((list[0] == "AnalyzeStartTime") && list.size() == 3) 0866 { 0867 displayStartTime = QDateTime::fromString(list[1], timeFormat); 0868 startTimeInitialized = true; 0869 analyzeTimeZone = list[2]; 0870 return 0; 0871 } 0872 0873 // Except for comments and the above AnalyzeStartTime, the second item 0874 // in the csv line is a double which represents seconds since start of the log. 0875 const double time = QString(list[1]).toDouble(&ok); 0876 if (!ok) 0877 return 0; 0878 if (time < 0 || time > 3600 * 24 * 10) 0879 return 0; 0880 0881 if ((list[0] == "CaptureStarting") && (list.size() == 4)) 0882 { 0883 const double exposureSeconds = QString(list[2]).toDouble(&ok); 0884 if (!ok) 0885 return 0; 0886 const QString filter = list[3]; 0887 processCaptureStarting(time, exposureSeconds, filter); 0888 } 0889 else if ((list[0] == "CaptureComplete") && (list.size() >= 6) && (list.size() <= 9)) 0890 { 0891 const double exposureSeconds = QString(list[2]).toDouble(&ok); 0892 if (!ok) 0893 return 0; 0894 const QString filter = list[3]; 0895 const double hfr = QString(list[4]).toDouble(&ok); 0896 if (!ok) 0897 return 0; 0898 const QString filename = list[5]; 0899 const int numStars = (list.size() > 6) ? QString(list[6]).toInt(&ok) : 0; 0900 if (!ok) 0901 return 0; 0902 const int median = (list.size() > 7) ? QString(list[7]).toInt(&ok) : 0; 0903 if (!ok) 0904 return 0; 0905 const double eccentricity = (list.size() > 8) ? QString(list[8]).toDouble(&ok) : 0; 0906 if (!ok) 0907 return 0; 0908 processCaptureComplete(time, filename, exposureSeconds, filter, hfr, numStars, median, eccentricity, true); 0909 } 0910 else if ((list[0] == "CaptureAborted") && (list.size() == 3)) 0911 { 0912 const double exposureSeconds = QString(list[2]).toDouble(&ok); 0913 if (!ok) 0914 return 0; 0915 processCaptureAborted(time, exposureSeconds, true); 0916 } 0917 else if ((list[0] == "AutofocusStarting") && (list.size() == 4)) 0918 { 0919 QString filter = list[2]; 0920 double temperature = QString(list[3]).toDouble(&ok); 0921 if (!ok) 0922 return 0; 0923 processAutofocusStarting(time, temperature, filter); 0924 } 0925 else if ((list[0] == "AutofocusComplete") && (list.size() >= 4)) 0926 { 0927 const QString filter = list[2]; 0928 const QString samples = list[3]; 0929 const QString curve = list.size() > 4 ? list[4] : ""; 0930 const QString title = list.size() > 5 ? list[5] : ""; 0931 processAutofocusComplete(time, filter, samples, curve, title, true); 0932 } 0933 else if ((list[0] == "AdaptiveFocusComplete") && (list.size() == 12)) 0934 { 0935 // This is the second version of the AdaptiveFocusComplete message 0936 const QString filter = list[2]; 0937 double temperature = QString(list[3]).toDouble(&ok); 0938 const double tempTicks = QString(list[4]).toDouble(&ok); 0939 double altitude = QString(list[5]).toDouble(&ok); 0940 const double altTicks = QString(list[6]).toDouble(&ok); 0941 const int prevPosError = QString(list[7]).toInt(&ok); 0942 const int thisPosError = QString(list[8]).toInt(&ok); 0943 const int totalTicks = QString(list[9]).toInt(&ok); 0944 const int position = QString(list[10]).toInt(&ok); 0945 const bool focuserMoved = QString(list[11]).toInt(&ok) != 0; 0946 processAdaptiveFocusComplete(time, filter, temperature, tempTicks, altitude, altTicks, prevPosError, 0947 thisPosError, totalTicks, position, focuserMoved, true); 0948 } 0949 else if ((list[0] == "AdaptiveFocusComplete") && (list.size() >= 9)) 0950 { 0951 // This is the first version of the AdaptiveFocusComplete message - retained os Analyze can process 0952 // historical messages correctly 0953 const QString filter = list[2]; 0954 double temperature = QString(list[3]).toDouble(&ok); 0955 const int tempTicks = QString(list[4]).toInt(&ok); 0956 double altitude = QString(list[5]).toDouble(&ok); 0957 const int altTicks = QString(list[6]).toInt(&ok); 0958 const int totalTicks = QString(list[7]).toInt(&ok); 0959 const int position = QString(list[8]).toInt(&ok); 0960 const bool focuserMoved = list.size() < 10 || QString(list[9]).toInt(&ok) != 0; 0961 processAdaptiveFocusComplete(time, filter, temperature, tempTicks, 0962 altitude, altTicks, 0, 0, totalTicks, position, focuserMoved, true); 0963 } 0964 else if ((list[0] == "AutofocusAborted") && (list.size() == 4)) 0965 { 0966 QString filter = list[2]; 0967 QString samples = list[3]; 0968 processAutofocusAborted(time, filter, samples, true); 0969 } 0970 else if ((list[0] == "GuideState") && list.size() == 3) 0971 { 0972 processGuideState(time, list[2], true); 0973 } 0974 else if ((list[0] == "GuideStats") && list.size() == 9) 0975 { 0976 const double ra = QString(list[2]).toDouble(&ok); 0977 if (!ok) 0978 return 0; 0979 const double dec = QString(list[3]).toDouble(&ok); 0980 if (!ok) 0981 return 0; 0982 const double raPulse = QString(list[4]).toInt(&ok); 0983 if (!ok) 0984 return 0; 0985 const double decPulse = QString(list[5]).toInt(&ok); 0986 if (!ok) 0987 return 0; 0988 const double snr = QString(list[6]).toDouble(&ok); 0989 if (!ok) 0990 return 0; 0991 const double skyBg = QString(list[7]).toDouble(&ok); 0992 if (!ok) 0993 return 0; 0994 const double numStars = QString(list[8]).toInt(&ok); 0995 if (!ok) 0996 return 0; 0997 processGuideStats(time, ra, dec, raPulse, decPulse, snr, skyBg, numStars, true); 0998 } 0999 else if ((list[0] == "Temperature") && list.size() == 3) 1000 { 1001 const double temperature = QString(list[2]).toDouble(&ok); 1002 if (!ok) 1003 return 0; 1004 processTemperature(time, temperature, true); 1005 } 1006 else if ((list[0] == "TargetDistance") && list.size() == 3) 1007 { 1008 const double targetDistance = QString(list[2]).toDouble(&ok); 1009 if (!ok) 1010 return 0; 1011 processTargetDistance(time, targetDistance, true); 1012 } 1013 else if ((list[0] == "MountState") && list.size() == 3) 1014 { 1015 processMountState(time, list[2], true); 1016 } 1017 else if ((list[0] == "MountCoords") && (list.size() == 7 || list.size() == 8)) 1018 { 1019 const double ra = QString(list[2]).toDouble(&ok); 1020 if (!ok) 1021 return 0; 1022 const double dec = QString(list[3]).toDouble(&ok); 1023 if (!ok) 1024 return 0; 1025 const double az = QString(list[4]).toDouble(&ok); 1026 if (!ok) 1027 return 0; 1028 const double alt = QString(list[5]).toDouble(&ok); 1029 if (!ok) 1030 return 0; 1031 const int side = QString(list[6]).toInt(&ok); 1032 if (!ok) 1033 return 0; 1034 const double ha = (list.size() > 7) ? QString(list[7]).toDouble(&ok) : 0; 1035 if (!ok) 1036 return 0; 1037 processMountCoords(time, ra, dec, az, alt, side, ha, true); 1038 } 1039 else if ((list[0] == "AlignState") && list.size() == 3) 1040 { 1041 processAlignState(time, list[2], true); 1042 } 1043 else if ((list[0] == "MeridianFlipState") && list.size() == 3) 1044 { 1045 processMountFlipState(time, list[2], true); 1046 } 1047 else if ((list[0] == "SchedulerJobStart") && list.size() == 3) 1048 { 1049 QString jobName = list[2]; 1050 processSchedulerJobStarted(time, jobName); 1051 } 1052 else if ((list[0] == "SchedulerJobEnd") && list.size() == 4) 1053 { 1054 QString jobName = list[2]; 1055 QString reason = list[3]; 1056 processSchedulerJobEnded(time, jobName, reason, true); 1057 } 1058 else 1059 { 1060 return 0; 1061 } 1062 return time; 1063 } 1064 1065 namespace 1066 { 1067 void addDetailsRow(QTableWidget *table, const QString &col1, const QColor &color1, 1068 const QString &col2, const QColor &color2, 1069 const QString &col3 = "", const QColor &color3 = Qt::white) 1070 { 1071 int row = table->rowCount(); 1072 table->setRowCount(row + 1); 1073 1074 QTableWidgetItem *item = new QTableWidgetItem(); 1075 if (col1 == "Filename") 1076 { 1077 // Special case filenames--they tend to be too long and get elided. 1078 QFont ft = item->font(); 1079 ft.setPointSizeF(8.0); 1080 item->setFont(ft); 1081 item->setText(col2); 1082 item->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); 1083 item->setForeground(color2); 1084 table->setItem(row, 0, item); 1085 table->setSpan(row, 0, 1, 3); 1086 return; 1087 } 1088 1089 item->setText(col1); 1090 item->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); 1091 item->setForeground(color1); 1092 table->setItem(row, 0, item); 1093 1094 item = new QTableWidgetItem(); 1095 item->setText(col2); 1096 item->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); 1097 item->setForeground(color2); 1098 if (col1 == "Filename") 1099 { 1100 // Special Case long filenames. 1101 QFont ft = item->font(); 1102 ft.setPointSizeF(8.0); 1103 item->setFont(ft); 1104 } 1105 table->setItem(row, 1, item); 1106 1107 if (col3.size() > 0) 1108 { 1109 item = new QTableWidgetItem(); 1110 item->setText(col3); 1111 item->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); 1112 item->setForeground(color3); 1113 table->setItem(row, 2, item); 1114 } 1115 else 1116 { 1117 // Column 1 spans 2nd and 3rd columns 1118 table->setSpan(row, 1, 1, 2); 1119 } 1120 } 1121 } 1122 1123 // Helper to create tables in the details display. 1124 // Start the table, displaying the heading and timing information, common to all sessions. 1125 void Analyze::Session::setupTable(const QString &name, const QString &status, 1126 const QDateTime &startClock, const QDateTime &endClock, QTableWidget *table) 1127 { 1128 details = table; 1129 details->clear(); 1130 details->setRowCount(0); 1131 details->setEditTriggers(QAbstractItemView::NoEditTriggers); 1132 details->setColumnCount(3); 1133 details->verticalHeader()->setDefaultSectionSize(20); 1134 details->horizontalHeader()->setStretchLastSection(true); 1135 details->setColumnWidth(0, 100); 1136 details->setColumnWidth(1, 100); 1137 details->setShowGrid(false); 1138 details->setWordWrap(true); 1139 details->horizontalHeader()->hide(); 1140 details->verticalHeader()->hide(); 1141 1142 QString startDateStr = startClock.toString("dd.MM.yyyy"); 1143 QString startTimeStr = startClock.toString("hh:mm:ss"); 1144 QString endTimeStr = isTemporary() ? "Ongoing" 1145 : endClock.toString("hh:mm:ss"); 1146 1147 addDetailsRow(details, name, Qt::yellow, status, Qt::yellow); 1148 addDetailsRow(details, "Date", Qt::yellow, startDateStr, Qt::white); 1149 addDetailsRow(details, "Interval", Qt::yellow, QString::number(start, 'f', 3), Qt::white, 1150 isTemporary() ? "Ongoing" : QString::number(end, 'f', 3), Qt::white); 1151 addDetailsRow(details, "Clock", Qt::yellow, startTimeStr, Qt::white, endTimeStr, Qt::white); 1152 addDetailsRow(details, "Duration", Qt::yellow, QString::number(end - start, 'f', 1), Qt::white); 1153 } 1154 1155 // Add a new row to the table, which is specific to the particular Timeline line. 1156 void Analyze::Session::addRow(const QString &key, const QString &value) 1157 { 1158 addDetailsRow(details, key, Qt::yellow, value, Qt::white); 1159 } 1160 1161 bool Analyze::Session::isTemporary() const 1162 { 1163 return rect != nullptr; 1164 } 1165 1166 // The focus session parses the "pipe-separate-values" list of positions 1167 // and HFRs given it, eventually to be used to plot the focus v-curve. 1168 Analyze::FocusSession::FocusSession(double start_, double end_, QCPItemRect *rect, bool ok, double temperature_, 1169 const QString &filter_, const QString &points_, const QString &curve_, const QString &title_) 1170 : Session(start_, end_, FOCUS_Y, rect), success(ok), 1171 temperature(temperature_), filter(filter_), points(points_), curve(curve_), title(title_) 1172 { 1173 const QStringList list = points.split(QLatin1Char('|')); 1174 const int size = list.size(); 1175 // Size can be 1 if points_ is an empty string. 1176 if (size < 2) 1177 return; 1178 1179 for (int i = 0; i < size; ) 1180 { 1181 bool parsed1, parsed2; 1182 int position = QString(list[i++]).toInt(&parsed1); 1183 if (i >= size) 1184 break; 1185 double hfr = QString(list[i++]).toDouble(&parsed2); 1186 if (!parsed1 || !parsed2) 1187 { 1188 positions.clear(); 1189 hfrs.clear(); 1190 return; 1191 } 1192 positions.push_back(position); 1193 hfrs.push_back(hfr); 1194 } 1195 } 1196 1197 Analyze::FocusSession::FocusSession(double start_, double end_, QCPItemRect *rect, 1198 const QString &filter_, double temperature_, double tempTicks_, double altitude_, 1199 double altTicks_, int prevPosError_, int thisPosError_, int totalTicks_, int position_) 1200 : Session(start_, end_, FOCUS_Y, rect), temperature(temperature_), filter(filter_), tempTicks(tempTicks_), 1201 altitude(altitude_), altTicks(altTicks_), prevPosError(prevPosError_), thisPosError(thisPosError_), 1202 totalTicks(totalTicks_), adaptedPosition(position_) 1203 { 1204 standardSession = false; 1205 } 1206 1207 double Analyze::FocusSession::focusPosition() 1208 { 1209 if (!standardSession) 1210 return adaptedPosition; 1211 1212 if (positions.size() > 0) 1213 return positions.last(); 1214 return 0; 1215 } 1216 1217 namespace 1218 { 1219 bool isTemporaryFile(const QString &filename) 1220 { 1221 QString tempFileLocation = QStandardPaths::writableLocation(QStandardPaths::TempLocation); 1222 return filename.startsWith(tempFileLocation); 1223 } 1224 } 1225 1226 // When the user clicks on a particular capture session in the timeline, 1227 // a table is rendered in the details section, and, if it was a double click, 1228 // the fits file is displayed, if it can be found. 1229 void Analyze::captureSessionClicked(CaptureSession &c, bool doubleClick) 1230 { 1231 highlightTimelineItem(c); 1232 1233 if (c.isTemporary()) 1234 c.setupTable("Capture", "in progress", clockTime(c.start), clockTime(c.start), detailsTable); 1235 else if (c.aborted) 1236 c.setupTable("Capture", "ABORTED", clockTime(c.start), clockTime(c.end), detailsTable); 1237 else 1238 c.setupTable("Capture", "successful", clockTime(c.start), clockTime(c.end), detailsTable); 1239 1240 c.addRow("Filter", c.filter); 1241 1242 double raRMS, decRMS, totalRMS; 1243 int numSamples; 1244 displayGuideGraphics(c.start, c.end, &raRMS, &decRMS, &totalRMS, &numSamples); 1245 if (numSamples > 0) 1246 c.addRow("GuideRMS", QString::number(totalRMS, 'f', 2)); 1247 1248 c.addRow("Exposure", QString::number(c.duration, 'f', 2)); 1249 if (!c.isTemporary()) 1250 c.addRow("Filename", c.filename); 1251 1252 1253 // Don't try to display images from temporary sessions (they aren't done yet). 1254 if (doubleClick && !c.isTemporary()) 1255 { 1256 QString filename = findFilename(c.filename, alternateFolder); 1257 // Don't display temporary files from completed sessions either. 1258 bool tempImage = isTemporaryFile(c.filename); 1259 if (!tempImage && filename.size() == 0) 1260 appendLogText(i18n("Could not find image file: %1", c.filename)); 1261 else if (!tempImage) 1262 displayFITS(filename); 1263 else appendLogText(i18n("Cannot display temporary image file: %1", c.filename)); 1264 } 1265 } 1266 1267 namespace 1268 { 1269 QString getSign(int val) 1270 { 1271 if (val == 0) return ""; 1272 else if (val > 0) return "+"; 1273 else return "-"; 1274 } 1275 QString signedIntString(int val) 1276 { 1277 return QString("%1%2").arg(getSign(val)).arg(abs(val)); 1278 } 1279 } 1280 1281 1282 // When the user clicks on a focus session in the timeline, 1283 // a table is rendered in the details section, and the HFR/position plot 1284 // is displayed in the graphics plot. If focus is ongoing 1285 // the information for the graphics is not plotted as it is not yet available. 1286 void Analyze::focusSessionClicked(FocusSession &c, bool doubleClick) 1287 { 1288 Q_UNUSED(doubleClick); 1289 highlightTimelineItem(c); 1290 1291 if (!c.standardSession) 1292 { 1293 // This is an adaptive focus session 1294 c.setupTable("Focus", "Adaptive", clockTime(c.end), clockTime(c.end), detailsTable); 1295 c.addRow("Filter", c.filter); 1296 addDetailsRow(detailsTable, "Temperature", Qt::yellow, QString("%1°").arg(c.temperature, 0, 'f', 1), 1297 Qt::white, QString("%1").arg(c.tempTicks, 0, 'f', 1)); 1298 addDetailsRow(detailsTable, "Altitude", Qt::yellow, QString("%1°").arg(c.altitude, 0, 'f', 1), 1299 Qt::white, QString("%1").arg(c.altTicks, 0, 'f', 1)); 1300 addDetailsRow(detailsTable, "Pos Error", Qt::yellow, "Start / End", Qt::white, 1301 QString("%1 / %2").arg(c.prevPosError).arg(c.thisPosError)); 1302 addDetailsRow(detailsTable, "Position", Qt::yellow, QString::number(c.adaptedPosition), 1303 Qt::white, signedIntString(c.totalTicks)); 1304 return; 1305 } 1306 1307 if (c.success) 1308 c.setupTable("Focus", "successful", clockTime(c.start), clockTime(c.end), detailsTable); 1309 else if (c.isTemporary()) 1310 c.setupTable("Focus", "in progress", clockTime(c.start), clockTime(c.start), detailsTable); 1311 else 1312 c.setupTable("Focus", "FAILED", clockTime(c.start), clockTime(c.end), detailsTable); 1313 1314 if (!c.isTemporary()) 1315 { 1316 if (c.success) 1317 { 1318 if (c.hfrs.size() > 0) 1319 c.addRow("HFR", QString::number(c.hfrs.last(), 'f', 2)); 1320 if (c.positions.size() > 0) 1321 c.addRow("Solution", QString::number(c.positions.last(), 'f', 0)); 1322 } 1323 c.addRow("Iterations", QString::number(c.positions.size())); 1324 } 1325 c.addRow("Filter", c.filter); 1326 c.addRow("Temperature", QString::number(c.temperature, 'f', 1)); 1327 1328 if (c.isTemporary()) 1329 resetGraphicsPlot(); 1330 else 1331 displayFocusGraphics(c.positions, c.hfrs, c.curve, c.title, c.success); 1332 } 1333 1334 // When the user clicks on a guide session in the timeline, 1335 // a table is rendered in the details section. If it has a G_GUIDING state 1336 // then a drift plot is generated and RMS values are calculated 1337 // for the guiding session's time interval. 1338 void Analyze::guideSessionClicked(GuideSession &c, bool doubleClick) 1339 { 1340 Q_UNUSED(doubleClick); 1341 highlightTimelineItem(c); 1342 1343 QString st; 1344 if (c.simpleState == G_IDLE) 1345 st = "Idle"; 1346 else if (c.simpleState == G_GUIDING) 1347 st = "Guiding"; 1348 else if (c.simpleState == G_CALIBRATING) 1349 st = "Calibrating"; 1350 else if (c.simpleState == G_SUSPENDED) 1351 st = "Suspended"; 1352 else if (c.simpleState == G_DITHERING) 1353 st = "Dithering"; 1354 1355 c.setupTable("Guide", st, clockTime(c.start), clockTime(c.end), detailsTable); 1356 resetGraphicsPlot(); 1357 if (c.simpleState == G_GUIDING) 1358 { 1359 double raRMS, decRMS, totalRMS; 1360 int numSamples; 1361 displayGuideGraphics(c.start, c.end, &raRMS, &decRMS, &totalRMS, &numSamples); 1362 if (numSamples > 0) 1363 { 1364 c.addRow("total RMS", QString::number(totalRMS, 'f', 2)); 1365 c.addRow("ra RMS", QString::number(raRMS, 'f', 2)); 1366 c.addRow("dec RMS", QString::number(decRMS, 'f', 2)); 1367 } 1368 c.addRow("Num Samples", QString::number(numSamples)); 1369 } 1370 } 1371 1372 void Analyze::displayGuideGraphics(double start, double end, double *raRMS, 1373 double *decRMS, double *totalRMS, int *numSamples) 1374 { 1375 resetGraphicsPlot(); 1376 auto ra = statsPlot->graph(RA_GRAPH)->data()->findBegin(start); 1377 auto dec = statsPlot->graph(DEC_GRAPH)->data()->findBegin(start); 1378 auto raEnd = statsPlot->graph(RA_GRAPH)->data()->findEnd(end); 1379 auto decEnd = statsPlot->graph(DEC_GRAPH)->data()->findEnd(end); 1380 int num = 0; 1381 double raSquareErrorSum = 0, decSquareErrorSum = 0; 1382 while (ra != raEnd && dec != decEnd && 1383 ra->mainKey() < end && dec->mainKey() < end && 1384 ra != statsPlot->graph(RA_GRAPH)->data()->constEnd() && 1385 dec != statsPlot->graph(DEC_GRAPH)->data()->constEnd() && 1386 ra->mainKey() < end && dec->mainKey() < end) 1387 { 1388 const double raVal = ra->mainValue(); 1389 const double decVal = dec->mainValue(); 1390 graphicsPlot->graph(GUIDER_GRAPHICS)->addData(raVal, decVal); 1391 if (!qIsNaN(raVal) && !qIsNaN(decVal)) 1392 { 1393 raSquareErrorSum += raVal * raVal; 1394 decSquareErrorSum += decVal * decVal; 1395 num++; 1396 } 1397 ra++; 1398 dec++; 1399 } 1400 if (numSamples != nullptr) 1401 *numSamples = num; 1402 if (num > 0) 1403 { 1404 if (raRMS != nullptr) 1405 *raRMS = sqrt(raSquareErrorSum / num); 1406 if (decRMS != nullptr) 1407 *decRMS = sqrt(decSquareErrorSum / num); 1408 if (totalRMS != nullptr) 1409 *totalRMS = sqrt((raSquareErrorSum + decSquareErrorSum) / num); 1410 if (numSamples != nullptr) 1411 *numSamples = num; 1412 } 1413 QCPItemEllipse *c1 = new QCPItemEllipse(graphicsPlot); 1414 c1->bottomRight->setCoords(1.0, -1.0); 1415 c1->topLeft->setCoords(-1.0, 1.0); 1416 QCPItemEllipse *c2 = new QCPItemEllipse(graphicsPlot); 1417 c2->bottomRight->setCoords(2.0, -2.0); 1418 c2->topLeft->setCoords(-2.0, 2.0); 1419 c1->setPen(QPen(Qt::green)); 1420 c2->setPen(QPen(Qt::yellow)); 1421 1422 // Since the plot is wider than it is tall, these lines set the 1423 // vertical range to 2.5, and the horizontal range to whatever it 1424 // takes to keep the two axes' scales (number of pixels per value) 1425 // the same, so that circles stay circular (i.e. circles are not stretch 1426 // wide even though the graph area is not square). 1427 graphicsPlot->xAxis->setRange(-2.5, 2.5); 1428 graphicsPlot->yAxis->setRange(-2.5, 2.5); 1429 graphicsPlot->xAxis->setScaleRatio(graphicsPlot->yAxis); 1430 } 1431 1432 // When the user clicks on a particular mount session in the timeline, 1433 // a table is rendered in the details section. 1434 void Analyze::mountSessionClicked(MountSession &c, bool doubleClick) 1435 { 1436 Q_UNUSED(doubleClick); 1437 highlightTimelineItem(c); 1438 1439 c.setupTable("Mount", mountStatusString(c.state), clockTime(c.start), 1440 clockTime(c.isTemporary() ? c.start : c.end), detailsTable); 1441 } 1442 1443 // When the user clicks on a particular align session in the timeline, 1444 // a table is rendered in the details section. 1445 void Analyze::alignSessionClicked(AlignSession &c, bool doubleClick) 1446 { 1447 Q_UNUSED(doubleClick); 1448 highlightTimelineItem(c); 1449 c.setupTable("Align", getAlignStatusString(c.state), clockTime(c.start), 1450 clockTime(c.isTemporary() ? c.start : c.end), detailsTable); 1451 } 1452 1453 // When the user clicks on a particular meridian flip session in the timeline, 1454 // a table is rendered in the details section. 1455 void Analyze::mountFlipSessionClicked(MountFlipSession &c, bool doubleClick) 1456 { 1457 Q_UNUSED(doubleClick); 1458 highlightTimelineItem(c); 1459 c.setupTable("Meridian Flip", MeridianFlipState::meridianFlipStatusString(c.state), 1460 clockTime(c.start), clockTime(c.isTemporary() ? c.start : c.end), detailsTable); 1461 } 1462 1463 // When the user clicks on a particular scheduler session in the timeline, 1464 // a table is rendered in the details section. 1465 void Analyze::schedulerSessionClicked(SchedulerJobSession &c, bool doubleClick) 1466 { 1467 Q_UNUSED(doubleClick); 1468 highlightTimelineItem(c); 1469 c.setupTable("Scheduler Job", c.jobName, 1470 clockTime(c.start), clockTime(c.isTemporary() ? c.start : c.end), detailsTable); 1471 c.addRow("End reason", c.reason); 1472 } 1473 1474 // This method determines which timeline session (if any) was selected 1475 // when the user clicks in the Timeline plot. It also sets a cursor 1476 // in the stats plot. 1477 void Analyze::processTimelineClick(QMouseEvent *event, bool doubleClick) 1478 { 1479 unhighlightTimelineItem(); 1480 double xval = timelinePlot->xAxis->pixelToCoord(event->x()); 1481 double yval = timelinePlot->yAxis->pixelToCoord(event->y()); 1482 if (yval >= CAPTURE_Y - 0.5 && yval <= CAPTURE_Y + 0.5) 1483 { 1484 QList<CaptureSession> candidates = captureSessions.find(xval); 1485 if (candidates.size() > 0) 1486 captureSessionClicked(candidates[0], doubleClick); 1487 else if ((temporaryCaptureSession.rect != nullptr) && 1488 (xval > temporaryCaptureSession.start)) 1489 captureSessionClicked(temporaryCaptureSession, doubleClick); 1490 } 1491 else if (yval >= FOCUS_Y - 0.5 && yval <= FOCUS_Y + 0.5) 1492 { 1493 QList<FocusSession> candidates = focusSessions.find(xval); 1494 if (candidates.size() > 0) 1495 focusSessionClicked(candidates[0], doubleClick); 1496 else if ((temporaryFocusSession.rect != nullptr) && 1497 (xval > temporaryFocusSession.start)) 1498 focusSessionClicked(temporaryFocusSession, doubleClick); 1499 } 1500 else if (yval >= GUIDE_Y - 0.5 && yval <= GUIDE_Y + 0.5) 1501 { 1502 QList<GuideSession> candidates = guideSessions.find(xval); 1503 if (candidates.size() > 0) 1504 guideSessionClicked(candidates[0], doubleClick); 1505 else if ((temporaryGuideSession.rect != nullptr) && 1506 (xval > temporaryGuideSession.start)) 1507 guideSessionClicked(temporaryGuideSession, doubleClick); 1508 } 1509 else if (yval >= MOUNT_Y - 0.5 && yval <= MOUNT_Y + 0.5) 1510 { 1511 QList<MountSession> candidates = mountSessions.find(xval); 1512 if (candidates.size() > 0) 1513 mountSessionClicked(candidates[0], doubleClick); 1514 else if ((temporaryMountSession.rect != nullptr) && 1515 (xval > temporaryMountSession.start)) 1516 mountSessionClicked(temporaryMountSession, doubleClick); 1517 } 1518 else if (yval >= ALIGN_Y - 0.5 && yval <= ALIGN_Y + 0.5) 1519 { 1520 QList<AlignSession> candidates = alignSessions.find(xval); 1521 if (candidates.size() > 0) 1522 alignSessionClicked(candidates[0], doubleClick); 1523 else if ((temporaryAlignSession.rect != nullptr) && 1524 (xval > temporaryAlignSession.start)) 1525 alignSessionClicked(temporaryAlignSession, doubleClick); 1526 } 1527 else if (yval >= MERIDIAN_MOUNT_FLIP_Y - 0.5 && yval <= MERIDIAN_MOUNT_FLIP_Y + 0.5) 1528 { 1529 QList<MountFlipSession> candidates = mountFlipSessions.find(xval); 1530 if (candidates.size() > 0) 1531 mountFlipSessionClicked(candidates[0], doubleClick); 1532 else if ((temporaryMountFlipSession.rect != nullptr) && 1533 (xval > temporaryMountFlipSession.start)) 1534 mountFlipSessionClicked(temporaryMountFlipSession, doubleClick); 1535 } 1536 else if (yval >= SCHEDULER_Y - 0.5 && yval <= SCHEDULER_Y + 0.5) 1537 { 1538 QList<SchedulerJobSession> candidates = schedulerJobSessions.find(xval); 1539 if (candidates.size() > 0) 1540 schedulerSessionClicked(candidates[0], doubleClick); 1541 else if ((temporarySchedulerJobSession.rect != nullptr) && 1542 (xval > temporarySchedulerJobSession.start)) 1543 schedulerSessionClicked(temporarySchedulerJobSession, doubleClick); 1544 } 1545 setStatsCursor(xval); 1546 replot(); 1547 } 1548 1549 void Analyze::nextTimelineItem() 1550 { 1551 changeTimelineItem(true); 1552 } 1553 1554 void Analyze::previousTimelineItem() 1555 { 1556 changeTimelineItem(false); 1557 } 1558 1559 void Analyze::changeTimelineItem(bool next) 1560 { 1561 if (m_selectedSession.start == 0 && m_selectedSession.end == 0) return; 1562 switch(m_selectedSession.offset) 1563 { 1564 case CAPTURE_Y: 1565 { 1566 auto nextSession = next ? captureSessions.findNext(m_selectedSession.start) 1567 : captureSessions.findPrevious(m_selectedSession.start); 1568 1569 // Since we're displaying the images, don't want to stop at an aborted capture. 1570 // Continue searching until a good session (or no session) is found. 1571 while (nextSession && nextSession->aborted) 1572 nextSession = next ? captureSessions.findNext(nextSession->start) 1573 : captureSessions.findPrevious(nextSession->start); 1574 1575 if (nextSession) 1576 { 1577 // True because we want to display the image (so simulate a double-click on that session). 1578 captureSessionClicked(*nextSession, true); 1579 setStatsCursor((nextSession->end + nextSession->start) / 2); 1580 } 1581 break; 1582 } 1583 case FOCUS_Y: 1584 { 1585 auto nextSession = next ? focusSessions.findNext(m_selectedSession.start) 1586 : focusSessions.findPrevious(m_selectedSession.start); 1587 if (nextSession) 1588 { 1589 focusSessionClicked(*nextSession, true); 1590 setStatsCursor((nextSession->end + nextSession->start) / 2); 1591 } 1592 break; 1593 } 1594 case ALIGN_Y: 1595 { 1596 auto nextSession = next ? alignSessions.findNext(m_selectedSession.start) 1597 : alignSessions.findPrevious(m_selectedSession.start); 1598 if (nextSession) 1599 { 1600 alignSessionClicked(*nextSession, true); 1601 setStatsCursor((nextSession->end + nextSession->start) / 2); 1602 } 1603 break; 1604 } 1605 case GUIDE_Y: 1606 { 1607 auto nextSession = next ? guideSessions.findNext(m_selectedSession.start) 1608 : guideSessions.findPrevious(m_selectedSession.start); 1609 if (nextSession) 1610 { 1611 guideSessionClicked(*nextSession, true); 1612 setStatsCursor((nextSession->end + nextSession->start) / 2); 1613 } 1614 break; 1615 } 1616 case MOUNT_Y: 1617 { 1618 auto nextSession = next ? mountSessions.findNext(m_selectedSession.start) 1619 : mountSessions.findPrevious(m_selectedSession.start); 1620 if (nextSession) 1621 { 1622 mountSessionClicked(*nextSession, true); 1623 setStatsCursor((nextSession->end + nextSession->start) / 2); 1624 } 1625 break; 1626 } 1627 case SCHEDULER_Y: 1628 { 1629 auto nextSession = next ? schedulerJobSessions.findNext(m_selectedSession.start) 1630 : schedulerJobSessions.findPrevious(m_selectedSession.start); 1631 if (nextSession) 1632 { 1633 schedulerSessionClicked(*nextSession, true); 1634 setStatsCursor((nextSession->end + nextSession->start) / 2); 1635 } 1636 break; 1637 } 1638 //case MERIDIAN_MOUNT_FLIP_Y: 1639 } 1640 if (!isVisible(m_selectedSession) && !isVisible(m_selectedSession)) 1641 adjustView((m_selectedSession.start + m_selectedSession.end) / 2.0); 1642 replot(); 1643 } 1644 1645 bool Analyze::isVisible(const Session &s) const 1646 { 1647 if (fullWidthCB->isChecked()) 1648 return true; 1649 return !((s.start < plotStart && s.end < plotStart) || 1650 (s.start > (plotStart + plotWidth) && s.end > (plotStart + plotWidth))); 1651 } 1652 1653 void Analyze::adjustView(double time) 1654 { 1655 if (!fullWidthCB->isChecked()) 1656 { 1657 plotStart = time - plotWidth / 2; 1658 } 1659 } 1660 1661 void Analyze::setStatsCursor(double time) 1662 { 1663 removeStatsCursor(); 1664 1665 // Cursor on the stats graph. 1666 QCPItemLine *line = new QCPItemLine(statsPlot); 1667 line->setPen(QPen(Qt::darkGray, 1, Qt::SolidLine)); 1668 const double top = statsPlot->yAxis->range().upper; 1669 const double bottom = statsPlot->yAxis->range().lower; 1670 line->start->setCoords(time, bottom); 1671 line->end->setCoords(time, top); 1672 statsCursor = line; 1673 1674 // Cursor on the timeline. 1675 QCPItemLine *line2 = new QCPItemLine(timelinePlot); 1676 line2->setPen(QPen(Qt::darkGray, 1, Qt::SolidLine)); 1677 const double top2 = timelinePlot->yAxis->range().upper; 1678 const double bottom2 = timelinePlot->yAxis->range().lower; 1679 line2->start->setCoords(time, bottom2); 1680 line2->end->setCoords(time, top2); 1681 timelineCursor = line2; 1682 1683 cursorTimeOut->setText(QString("%1s").arg(time)); 1684 cursorClockTimeOut->setText(QString("%1") 1685 .arg(clockTime(time).toString("hh:mm:ss"))); 1686 statsCursorTime = time; 1687 keepCurrentCB->setCheckState(Qt::Unchecked); 1688 } 1689 1690 void Analyze::removeStatsCursor() 1691 { 1692 if (statsCursor != nullptr) 1693 statsPlot->removeItem(statsCursor); 1694 statsCursor = nullptr; 1695 1696 if (timelineCursor != nullptr) 1697 timelinePlot->removeItem(timelineCursor); 1698 timelineCursor = nullptr; 1699 1700 cursorTimeOut->setText(""); 1701 cursorClockTimeOut->setText(""); 1702 statsCursorTime = -1; 1703 } 1704 1705 // When the users clicks in the stats plot, the cursor is set at the corresponding time. 1706 void Analyze::processStatsClick(QMouseEvent *event, bool doubleClick) 1707 { 1708 Q_UNUSED(doubleClick); 1709 double xval = statsPlot->xAxis->pixelToCoord(event->x()); 1710 setStatsCursor(xval); 1711 replot(); 1712 } 1713 1714 void Analyze::timelineMousePress(QMouseEvent *event) 1715 { 1716 processTimelineClick(event, false); 1717 } 1718 1719 void Analyze::timelineMouseDoubleClick(QMouseEvent *event) 1720 { 1721 processTimelineClick(event, true); 1722 } 1723 1724 void Analyze::statsMousePress(QMouseEvent *event) 1725 { 1726 QCPAxis *yAxis = activeYAxis; 1727 if (!yAxis) return; 1728 1729 // If we're on the legend, adjust the y-axis. 1730 if (statsPlot->xAxis->pixelToCoord(event->x()) < plotStart) 1731 { 1732 yAxisInitialPos = yAxis->pixelToCoord(event->y()); 1733 return; 1734 } 1735 processStatsClick(event, false); 1736 } 1737 1738 void Analyze::statsMouseDoubleClick(QMouseEvent *event) 1739 { 1740 processStatsClick(event, true); 1741 } 1742 1743 // Allow the user to click and hold, causing the cursor to move in real-time. 1744 void Analyze::statsMouseMove(QMouseEvent *event) 1745 { 1746 QCPAxis *yAxis = activeYAxis; 1747 if (!yAxis) return; 1748 1749 // If we're on the legend, adjust the y-axis. 1750 if (statsPlot->xAxis->pixelToCoord(event->x()) < plotStart) 1751 { 1752 auto range = yAxis->range(); 1753 double yDiff = yAxisInitialPos - yAxis->pixelToCoord(event->y()); 1754 yAxis->setRange(range.lower + yDiff, range.upper + yDiff); 1755 replot(); 1756 if (m_YAxisTool.isVisible() && m_YAxisTool.getAxis() == yAxis) 1757 m_YAxisTool.replot(true); 1758 return; 1759 } 1760 processStatsClick(event, false); 1761 } 1762 1763 // Called by the scrollbar, to move the current view. 1764 void Analyze::scroll(int value) 1765 { 1766 double pct = static_cast<double>(value) / MAX_SCROLL_VALUE; 1767 plotStart = std::max(0.0, maxXValue * pct - plotWidth / 2.0); 1768 // Normally replot adjusts the position of the slider. 1769 // If the user has done that, we don't want replot to re-do it. 1770 replot(false); 1771 1772 } 1773 void Analyze::scrollRight() 1774 { 1775 plotStart = std::min(maxXValue - plotWidth / 5, plotStart + plotWidth / 5); 1776 fullWidthCB->setChecked(false); 1777 replot(); 1778 1779 } 1780 void Analyze::scrollLeft() 1781 { 1782 plotStart = std::max(0.0, plotStart - plotWidth / 5); 1783 fullWidthCB->setChecked(false); 1784 replot(); 1785 1786 } 1787 void Analyze::replot(bool adjustSlider) 1788 { 1789 adjustTemporarySessions(); 1790 if (fullWidthCB->isChecked()) 1791 { 1792 plotStart = 0; 1793 plotWidth = std::max(10.0, maxXValue); 1794 } 1795 else if (keepCurrentCB->isChecked()) 1796 { 1797 plotStart = std::max(0.0, maxXValue - plotWidth); 1798 } 1799 // If we're keeping to the latest values, 1800 // set the time display to the latest time. 1801 if (keepCurrentCB->isChecked() && statsCursor == nullptr) 1802 { 1803 cursorTimeOut->setText(QString("%1s").arg(maxXValue)); 1804 cursorClockTimeOut->setText(QString("%1") 1805 .arg(clockTime(maxXValue).toString("hh:mm:ss"))); 1806 } 1807 analyzeSB->setPageStep( 1808 std::min(MAX_SCROLL_VALUE, 1809 static_cast<int>(MAX_SCROLL_VALUE * plotWidth / maxXValue))); 1810 if (adjustSlider) 1811 { 1812 double sliderCenter = plotStart + plotWidth / 2.0; 1813 analyzeSB->setSliderPosition(MAX_SCROLL_VALUE * (sliderCenter / maxXValue)); 1814 } 1815 1816 timelinePlot->xAxis->setRange(plotStart, plotStart + plotWidth); 1817 timelinePlot->yAxis->setRange(0, LAST_Y); 1818 1819 statsPlot->xAxis->setRange(plotStart, plotStart + plotWidth); 1820 1821 // Rescale any automatic y-axes. 1822 if (statsPlot->isVisible()) 1823 { 1824 for (auto &pairs : yAxisMap) 1825 { 1826 const YAxisInfo &info = pairs.second; 1827 if (statsPlot->graph(info.graphIndex)->visible() && info.rescale) 1828 { 1829 QCPAxis *axis = info.axis; 1830 axis->rescale(); 1831 axis->scaleRange(1.1, axis->range().center()); 1832 } 1833 } 1834 } 1835 1836 dateTicker->setOffset(displayStartTime.toMSecsSinceEpoch() / 1000.0); 1837 1838 timelinePlot->replot(); 1839 statsPlot->replot(); 1840 graphicsPlot->replot(); 1841 1842 if (activeYAxis != nullptr) 1843 { 1844 // Adjust the statsPlot padding to align statsPlot and timelinePlot. 1845 const int widthDiff = statsPlot->axisRect()->width() - timelinePlot->axisRect()->width(); 1846 const int paddingSize = activeYAxis->padding(); 1847 constexpr int maxPadding = 100; 1848 // Don't quite following why a positive difference should INCREASE padding, but it works. 1849 const int newPad = std::min(maxPadding, std::max(0, paddingSize + widthDiff)); 1850 if (newPad != paddingSize) 1851 { 1852 activeYAxis->setPadding(newPad); 1853 statsPlot->replot(); 1854 } 1855 } 1856 updateStatsValues(); 1857 } 1858 1859 void Analyze::statsYZoom(double zoomAmount) 1860 { 1861 auto axis = activeYAxis; 1862 if (!axis) return; 1863 auto range = axis->range(); 1864 const double halfDiff = (range.upper - range.lower) / 2.0; 1865 const double middle = (range.upper + range.lower) / 2.0; 1866 axis->setRange(QCPRange(middle - halfDiff * zoomAmount, middle + halfDiff * zoomAmount)); 1867 if (m_YAxisTool.isVisible() && m_YAxisTool.getAxis() == axis) 1868 m_YAxisTool.replot(true); 1869 } 1870 void Analyze::statsYZoomIn() 1871 { 1872 statsYZoom(0.80); 1873 statsPlot->replot(); 1874 } 1875 void Analyze::statsYZoomOut() 1876 { 1877 statsYZoom(1.25); 1878 statsPlot->replot(); 1879 } 1880 1881 namespace 1882 { 1883 // Pass in a function that converts the double graph value to a string 1884 // for the value box. 1885 template<typename Func> 1886 void updateStat(double time, QLineEdit *valueBox, QCPGraph *graph, Func func, bool useLastRealVal = false) 1887 { 1888 auto begin = graph->data()->findBegin(time); 1889 double timeDiffThreshold = 10000000.0; 1890 if ((begin != graph->data()->constEnd()) && 1891 (fabs(begin->mainKey() - time) < timeDiffThreshold)) 1892 { 1893 double foundVal = begin->mainValue(); 1894 valueBox->setDisabled(false); 1895 if (qIsNaN(foundVal)) 1896 { 1897 int index = graph->findBegin(time); 1898 const double MAX_TIME_DIFF = 600; 1899 while (useLastRealVal && index >= 0) 1900 { 1901 const double val = graph->data()->at(index)->mainValue(); 1902 const double t = graph->data()->at(index)->mainKey(); 1903 if (time - t > MAX_TIME_DIFF) 1904 break; 1905 if (!qIsNaN(val)) 1906 { 1907 valueBox->setText(func(val)); 1908 return; 1909 } 1910 index--; 1911 } 1912 valueBox->clear(); 1913 } 1914 else 1915 valueBox->setText(func(foundVal)); 1916 } 1917 else valueBox->setDisabled(true); 1918 } 1919 1920 } // namespace 1921 1922 // This populates the output boxes below the stats plot with the correct statistics. 1923 void Analyze::updateStatsValues() 1924 { 1925 const double time = statsCursorTime < 0 ? maxXValue : statsCursorTime; 1926 1927 auto d2Fcn = [](double d) -> QString { return QString::number(d, 'f', 2); }; 1928 auto d1Fcn = [](double d) -> QString { return QString::number(d, 'f', 1); }; 1929 // HFR, numCaptureStars, median & eccentricity are the only ones to use the last real value, 1930 // that is, it keeps those values from the last exposure. 1931 updateStat(time, hfrOut, statsPlot->graph(HFR_GRAPH), d2Fcn, true); 1932 updateStat(time, eccentricityOut, statsPlot->graph(ECCENTRICITY_GRAPH), d2Fcn, true); 1933 updateStat(time, skyBgOut, statsPlot->graph(SKYBG_GRAPH), d1Fcn); 1934 updateStat(time, snrOut, statsPlot->graph(SNR_GRAPH), d1Fcn); 1935 updateStat(time, raOut, statsPlot->graph(RA_GRAPH), d2Fcn); 1936 updateStat(time, decOut, statsPlot->graph(DEC_GRAPH), d2Fcn); 1937 updateStat(time, driftOut, statsPlot->graph(DRIFT_GRAPH), d2Fcn); 1938 updateStat(time, rmsOut, statsPlot->graph(RMS_GRAPH), d2Fcn); 1939 updateStat(time, rmsCOut, statsPlot->graph(CAPTURE_RMS_GRAPH), d2Fcn); 1940 updateStat(time, azOut, statsPlot->graph(AZ_GRAPH), d1Fcn); 1941 updateStat(time, altOut, statsPlot->graph(ALT_GRAPH), d2Fcn); 1942 updateStat(time, temperatureOut, statsPlot->graph(TEMPERATURE_GRAPH), d2Fcn); 1943 1944 auto asFcn = [](double d) -> QString { return QString("%1\"").arg(d, 0, 'f', 0); }; 1945 updateStat(time, targetDistanceOut, statsPlot->graph(TARGET_DISTANCE_GRAPH), asFcn, true); 1946 1947 auto hmsFcn = [](double d) -> QString 1948 { 1949 dms ra; 1950 ra.setD(d); 1951 return QString("%1:%2:%3").arg(ra.hour()).arg(ra.minute()).arg(ra.second()); 1952 //return ra.toHMSString(); 1953 }; 1954 updateStat(time, mountRaOut, statsPlot->graph(MOUNT_RA_GRAPH), hmsFcn); 1955 auto dmsFcn = [](double d) -> QString { dms dec; dec.setD(d); return dec.toDMSString(); }; 1956 updateStat(time, mountDecOut, statsPlot->graph(MOUNT_DEC_GRAPH), dmsFcn); 1957 auto haFcn = [](double d) -> QString 1958 { 1959 dms ha; 1960 QChar z('0'); 1961 QChar sgn('+'); 1962 ha.setD(d); 1963 if (ha.Hours() > 12.0) 1964 { 1965 ha.setH(24.0 - ha.Hours()); 1966 sgn = '-'; 1967 } 1968 return QString("%1%2:%3").arg(sgn).arg(ha.hour(), 2, 10, z) 1969 .arg(ha.minute(), 2, 10, z); 1970 }; 1971 updateStat(time, mountHaOut, statsPlot->graph(MOUNT_HA_GRAPH), haFcn); 1972 1973 auto intFcn = [](double d) -> QString { return QString::number(d, 'f', 0); }; 1974 updateStat(time, numStarsOut, statsPlot->graph(NUMSTARS_GRAPH), intFcn); 1975 updateStat(time, raPulseOut, statsPlot->graph(RA_PULSE_GRAPH), intFcn); 1976 updateStat(time, decPulseOut, statsPlot->graph(DEC_PULSE_GRAPH), intFcn); 1977 updateStat(time, numCaptureStarsOut, statsPlot->graph(NUM_CAPTURE_STARS_GRAPH), intFcn, true); 1978 updateStat(time, medianOut, statsPlot->graph(MEDIAN_GRAPH), intFcn, true); 1979 updateStat(time, focusPositionOut, statsPlot->graph(FOCUS_POSITION_GRAPH), intFcn); 1980 1981 auto pierFcn = [](double d) -> QString 1982 { 1983 return d == 0.0 ? "W->E" : d == 1.0 ? "E->W" : "?"; 1984 }; 1985 updateStat(time, pierSideOut, statsPlot->graph(PIER_SIDE_GRAPH), pierFcn); 1986 } 1987 1988 void Analyze::initStatsCheckboxes() 1989 { 1990 hfrCB->setChecked(Options::analyzeHFR()); 1991 numCaptureStarsCB->setChecked(Options::analyzeNumCaptureStars()); 1992 medianCB->setChecked(Options::analyzeMedian()); 1993 eccentricityCB->setChecked(Options::analyzeEccentricity()); 1994 numStarsCB->setChecked(Options::analyzeNumStars()); 1995 skyBgCB->setChecked(Options::analyzeSkyBg()); 1996 snrCB->setChecked(Options::analyzeSNR()); 1997 temperatureCB->setChecked(Options::analyzeTemperature()); 1998 focusPositionCB->setChecked(Options::focusPosition()); 1999 targetDistanceCB->setChecked(Options::analyzeTargetDistance()); 2000 raCB->setChecked(Options::analyzeRA()); 2001 decCB->setChecked(Options::analyzeDEC()); 2002 raPulseCB->setChecked(Options::analyzeRAp()); 2003 decPulseCB->setChecked(Options::analyzeDECp()); 2004 driftCB->setChecked(Options::analyzeDrift()); 2005 rmsCB->setChecked(Options::analyzeRMS()); 2006 rmsCCB->setChecked(Options::analyzeRMSC()); 2007 mountRaCB->setChecked(Options::analyzeMountRA()); 2008 mountDecCB->setChecked(Options::analyzeMountDEC()); 2009 mountHaCB->setChecked(Options::analyzeMountHA()); 2010 azCB->setChecked(Options::analyzeAz()); 2011 altCB->setChecked(Options::analyzeAlt()); 2012 pierSideCB->setChecked(Options::analyzePierSide()); 2013 } 2014 2015 void Analyze::zoomIn() 2016 { 2017 if (plotWidth > 0.5) 2018 { 2019 if (keepCurrentCB->isChecked()) 2020 // If we're keeping to the end of the data, keep the end on the right. 2021 plotStart = std::max(0.0, maxXValue - plotWidth / 4.0); 2022 else if (statsCursorTime >= 0) 2023 // If there is a cursor, try to move it to the center. 2024 plotStart = std::max(0.0, statsCursorTime - plotWidth / 4.0); 2025 else 2026 // Keep the center the same. 2027 plotStart += plotWidth / 4.0; 2028 plotWidth = plotWidth / 2.0; 2029 } 2030 fullWidthCB->setChecked(false); 2031 replot(); 2032 } 2033 2034 void Analyze::zoomOut() 2035 { 2036 if (plotWidth < maxXValue) 2037 { 2038 plotStart = std::max(0.0, plotStart - plotWidth / 2.0); 2039 plotWidth = plotWidth * 2; 2040 } 2041 fullWidthCB->setChecked(false); 2042 replot(); 2043 } 2044 2045 namespace 2046 { 2047 2048 void setupAxisDefaults(QCPAxis *axis) 2049 { 2050 axis->setBasePen(QPen(Qt::white, 1)); 2051 axis->grid()->setPen(QPen(QColor(140, 140, 140, 140), 1, Qt::DotLine)); 2052 axis->grid()->setSubGridPen(QPen(QColor(40, 40, 40), 1, Qt::DotLine)); 2053 axis->grid()->setZeroLinePen(Qt::NoPen); 2054 axis->setBasePen(QPen(Qt::white, 1)); 2055 axis->setTickPen(QPen(Qt::white, 1)); 2056 axis->setSubTickPen(QPen(Qt::white, 1)); 2057 axis->setTickLabelColor(Qt::white); 2058 axis->setLabelColor(Qt::white); 2059 axis->grid()->setVisible(true); 2060 } 2061 2062 // Generic initialization of a plot, applied to all plots in this tab. 2063 void initQCP(QCustomPlot *plot) 2064 { 2065 plot->setBackground(QBrush(Qt::black)); 2066 setupAxisDefaults(plot->yAxis); 2067 setupAxisDefaults(plot->xAxis); 2068 plot->xAxis->grid()->setZeroLinePen(Qt::NoPen); 2069 } 2070 } // namespace 2071 2072 void Analyze::initTimelinePlot() 2073 { 2074 initQCP(timelinePlot); 2075 2076 // This places the labels on the left of the timeline. 2077 QSharedPointer<QCPAxisTickerText> textTicker(new QCPAxisTickerText); 2078 textTicker->addTick(CAPTURE_Y, i18n("Capture")); 2079 textTicker->addTick(FOCUS_Y, i18n("Focus")); 2080 textTicker->addTick(ALIGN_Y, i18n("Align")); 2081 textTicker->addTick(GUIDE_Y, i18n("Guide")); 2082 textTicker->addTick(MERIDIAN_MOUNT_FLIP_Y, i18n("Flip")); 2083 textTicker->addTick(MOUNT_Y, i18n("Mount")); 2084 textTicker->addTick(SCHEDULER_Y, i18n("Job")); 2085 timelinePlot->yAxis->setTicker(textTicker); 2086 2087 ADAPTIVE_FOCUS_GRAPH = initGraph(timelinePlot, timelinePlot->yAxis, QCPGraph::lsNone, Qt::red, "adaptiveFocus"); 2088 timelinePlot->graph(ADAPTIVE_FOCUS_GRAPH)->setPen(QPen(Qt::red, 2)); 2089 timelinePlot->graph(ADAPTIVE_FOCUS_GRAPH)->setScatterStyle(QCPScatterStyle::ssDisc); 2090 } 2091 2092 // Turn on and off the various statistics, adding/removing them from the legend. 2093 void Analyze::toggleGraph(int graph_id, bool show) 2094 { 2095 statsPlot->graph(graph_id)->setVisible(show); 2096 if (show) 2097 statsPlot->graph(graph_id)->addToLegend(); 2098 else 2099 statsPlot->graph(graph_id)->removeFromLegend(); 2100 replot(); 2101 } 2102 2103 int Analyze::initGraph(QCustomPlot * plot, QCPAxis * yAxis, QCPGraph::LineStyle lineStyle, 2104 const QColor &color, const QString &name) 2105 { 2106 int num = plot->graphCount(); 2107 plot->addGraph(plot->xAxis, yAxis); 2108 plot->graph(num)->setLineStyle(lineStyle); 2109 plot->graph(num)->setPen(QPen(color)); 2110 plot->graph(num)->setName(name); 2111 return num; 2112 } 2113 2114 void Analyze::updateYAxisMap(QObject * key, const YAxisInfo &axisInfo) 2115 { 2116 if (key == nullptr) return; 2117 auto axisEntry = yAxisMap.find(key); 2118 if (axisEntry == yAxisMap.end()) 2119 yAxisMap.insert(std::make_pair(key, axisInfo)); 2120 else 2121 axisEntry->second = axisInfo; 2122 } 2123 2124 template <typename Func> 2125 int Analyze::initGraphAndCB(QCustomPlot * plot, QCPAxis * yAxis, QCPGraph::LineStyle lineStyle, 2126 const QColor &color, const QString &name, const QString &shortName, 2127 QCheckBox * cb, Func setCb, QLineEdit * out) 2128 { 2129 const int num = initGraph(plot, yAxis, lineStyle, color, shortName); 2130 if (out != nullptr) 2131 { 2132 const bool autoAxis = YAxisInfo::isRescale(yAxis->range()); 2133 updateYAxisMap(out, YAxisInfo(yAxis, yAxis->range(), autoAxis, num, plot, cb, name, shortName, color)); 2134 } 2135 if (cb != nullptr) 2136 { 2137 // Don't call toggleGraph() here, as it's too early for replot(). 2138 bool show = cb->isChecked(); 2139 plot->graph(num)->setVisible(show); 2140 if (show) 2141 plot->graph(num)->addToLegend(); 2142 else 2143 plot->graph(num)->removeFromLegend(); 2144 2145 connect(cb, &QCheckBox::toggled, 2146 [ = ](bool show) 2147 { 2148 this->toggleGraph(num, show); 2149 setCb(show); 2150 }); 2151 } 2152 return num; 2153 } 2154 2155 2156 void Analyze::userSetAxisColor(QObject *key, const YAxisInfo &axisInfo, const QColor &color) 2157 { 2158 updateYAxisMap(key, axisInfo); 2159 statsPlot->graph(axisInfo.graphIndex)->setPen(QPen(color)); 2160 Options::setAnalyzeStatsYAxis(serializeYAxes()); 2161 replot(); 2162 } 2163 2164 void Analyze::userSetLeftAxis(QCPAxis *axis) 2165 { 2166 setLeftAxis(axis); 2167 Options::setAnalyzeStatsYAxis(serializeYAxes()); 2168 replot(); 2169 } 2170 2171 void Analyze::userChangedYAxis(QObject *key, const YAxisInfo &axisInfo) 2172 { 2173 updateYAxisMap(key, axisInfo); 2174 Options::setAnalyzeStatsYAxis(serializeYAxes()); 2175 replot(); 2176 } 2177 2178 // TODO: Doesn't seem like this is ever getting called. Not sure why not receiving the rangeChanged signal. 2179 void Analyze::yAxisRangeChanged(const QCPRange &newRange) 2180 { 2181 Q_UNUSED(newRange); 2182 if (m_YAxisTool.isVisible() && m_YAxisTool.getAxis() == activeYAxis) 2183 m_YAxisTool.replot(true); 2184 } 2185 2186 void Analyze::setLeftAxis(QCPAxis *axis) 2187 { 2188 if (axis != nullptr && axis != activeYAxis) 2189 { 2190 for (const auto &pair : yAxisMap) 2191 { 2192 disconnect(pair.second.axis, QOverload<const QCPRange &>::of(&QCPAxis::rangeChanged), this, 2193 QOverload<const QCPRange &>::of(&Analyze::yAxisRangeChanged)); 2194 pair.second.axis->setVisible(false); 2195 } 2196 axis->setVisible(true); 2197 activeYAxis = axis; 2198 statsPlot->axisRect()->setRangeZoomAxes(0, axis); 2199 connect(axis, QOverload<const QCPRange &>::of(&QCPAxis::rangeChanged), this, 2200 QOverload<const QCPRange &>::of(&Analyze::yAxisRangeChanged)); 2201 } 2202 } 2203 2204 void Analyze::startYAxisTool(QObject * key, const YAxisInfo &info) 2205 { 2206 if (info.checkBox && !info.checkBox->isChecked()) 2207 { 2208 // Enable the graph. 2209 info.checkBox->setChecked(true); 2210 statsPlot->graph(info.graphIndex)->setVisible(true); 2211 statsPlot->graph(info.graphIndex)->addToLegend(); 2212 } 2213 2214 m_YAxisTool.reset(key, info, info.axis == activeYAxis); 2215 m_YAxisTool.show(); 2216 } 2217 2218 QCPAxis *Analyze::newStatsYAxis(const QString &label, double lower, double upper) 2219 { 2220 QCPAxis *axis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0); // 0 means QCP creates the axis. 2221 axis->setVisible(false); 2222 axis->setRange(lower, upper); 2223 axis->setLabel(label); 2224 setupAxisDefaults(axis); 2225 return axis; 2226 } 2227 2228 bool Analyze::restoreYAxes(const QString &encoding) 2229 { 2230 constexpr int headerSize = 2; 2231 constexpr int itemSize = 5; 2232 QVector<QStringRef> items = encoding.splitRef(','); 2233 if (items.size() <= headerSize) return false; 2234 if ((items.size() - headerSize) % itemSize != 0) return false; 2235 if (items[0] != "AnalyzeStatsYAxis1.0") return false; 2236 2237 // Restore the active Y axis 2238 const QString leftID = "left="; 2239 if (!items[1].startsWith(leftID)) return false; 2240 QStringRef left = items[1].mid(leftID.size()); 2241 if (left.size() <= 0) return false; 2242 for (const auto &pair : yAxisMap) 2243 { 2244 if (pair.second.axis->label() == left) 2245 { 2246 setLeftAxis(pair.second.axis); 2247 break; 2248 } 2249 } 2250 2251 // Restore the various upper/lower/rescale axis values. 2252 for (int i = headerSize; i < items.size(); i += itemSize) 2253 { 2254 const QString shortName = items[i].toString(); 2255 const double lower = items[i + 1].toDouble(); 2256 const double upper = items[i + 2].toDouble(); 2257 const bool rescale = items[i + 3] == "T"; 2258 const QColor color(items[i + 4]); 2259 for (auto &pair : yAxisMap) 2260 { 2261 auto &info = pair.second; 2262 if (info.axis->label() == shortName) 2263 { 2264 info.color = color; 2265 statsPlot->graph(info.graphIndex)->setPen(QPen(color)); 2266 info.rescale = rescale; 2267 if (rescale) 2268 info.axis->setRange( 2269 QCPRange(YAxisInfo::LOWER_RESCALE, 2270 YAxisInfo::UPPER_RESCALE)); 2271 else 2272 info.axis->setRange(QCPRange(lower, upper)); 2273 break; 2274 } 2275 } 2276 } 2277 return true; 2278 } 2279 2280 // This would be sensitive to short names with commas in them, but we don't do that. 2281 QString Analyze::serializeYAxes() 2282 { 2283 QString encoding = QString("AnalyzeStatsYAxis1.0,left=%1").arg(activeYAxis->label()); 2284 QList<QString> savedAxes; 2285 for (const auto &pair : yAxisMap) 2286 { 2287 const YAxisInfo &info = pair.second; 2288 const bool rescale = info.rescale; 2289 2290 // Only save if something has changed. 2291 bool somethingChanged = (info.initialColor != info.color) || 2292 (rescale != YAxisInfo::isRescale(info.initialRange)) || 2293 (!rescale && info.axis->range() != info.initialRange); 2294 2295 if (!somethingChanged) continue; 2296 2297 // Don't save the same axis twice 2298 if (savedAxes.contains(info.axis->label())) continue; 2299 2300 double lower = rescale ? YAxisInfo::LOWER_RESCALE : info.axis->range().lower; 2301 double upper = rescale ? YAxisInfo::UPPER_RESCALE : info.axis->range().upper; 2302 encoding.append(QString(",%1,%2,%3,%4,%5") 2303 .arg(info.axis->label()).arg(lower).arg(upper) 2304 .arg(info.rescale ? "T" : "F").arg(info.color.name())); 2305 savedAxes.append(info.axis->label()); 2306 } 2307 return encoding; 2308 } 2309 2310 void Analyze::initStatsPlot() 2311 { 2312 initQCP(statsPlot); 2313 2314 // Setup the main y-axis 2315 statsPlot->yAxis->setVisible(true); 2316 statsPlot->yAxis->setLabel("RA/DEC"); 2317 statsPlot->yAxis->setRange(-2, 5); 2318 setLeftAxis(statsPlot->yAxis); 2319 2320 // Setup the legend 2321 statsPlot->legend->setVisible(true); 2322 statsPlot->legend->setFont(QFont("Helvetica", 6)); 2323 statsPlot->legend->setTextColor(Qt::white); 2324 // Legend background is black and ~75% opaque. 2325 statsPlot->legend->setBrush(QBrush(QColor(0, 0, 0, 190))); 2326 // Legend stacks vertically. 2327 statsPlot->legend->setFillOrder(QCPLegend::foRowsFirst); 2328 // Rows pretty tightly packed. 2329 statsPlot->legend->setRowSpacing(-3); 2330 statsPlot->axisRect()->insetLayout()->setInsetAlignment(0, Qt::AlignLeft | Qt::AlignTop); 2331 2332 // Add the graphs. 2333 QString shortName = "HFR"; 2334 QCPAxis *hfrAxis = newStatsYAxis(shortName, -2, 6); 2335 HFR_GRAPH = initGraphAndCB(statsPlot, hfrAxis, QCPGraph::lsStepRight, Qt::cyan, "Capture Image HFR", shortName, hfrCB, 2336 Options::setAnalyzeHFR, hfrOut); 2337 connect(hfrCB, &QCheckBox::clicked, 2338 [ = ](bool show) 2339 { 2340 if (show && !Options::autoHFR()) 2341 KSNotification::info( 2342 i18n("The \"Auto Compute HFR\" option in the KStars " 2343 "FITS options menu is not set. You won't get HFR values " 2344 "without it. Once you set it, newly captured images " 2345 "will have their HFRs computed.")); 2346 }); 2347 2348 shortName = "#SubStars"; 2349 QCPAxis *numCaptureStarsAxis = newStatsYAxis(shortName); 2350 NUM_CAPTURE_STARS_GRAPH = initGraphAndCB(statsPlot, numCaptureStarsAxis, QCPGraph::lsStepRight, Qt::darkGreen, 2351 "#Stars in Capture", shortName, 2352 numCaptureStarsCB, Options::setAnalyzeNumCaptureStars, numCaptureStarsOut); 2353 connect(numCaptureStarsCB, &QCheckBox::clicked, 2354 [ = ](bool show) 2355 { 2356 if (show && !Options::autoHFR()) 2357 KSNotification::info( 2358 i18n("The \"Auto Compute HFR\" option in the KStars " 2359 "FITS options menu is not set. You won't get # stars in capture image values " 2360 "without it. Once you set it, newly captured images " 2361 "will have their stars detected.")); 2362 }); 2363 2364 shortName = "median"; 2365 QCPAxis *medianAxis = newStatsYAxis(shortName); 2366 MEDIAN_GRAPH = initGraphAndCB(statsPlot, medianAxis, QCPGraph::lsStepRight, Qt::darkGray, "Median Pixel", shortName, 2367 medianCB, Options::setAnalyzeMedian, medianOut); 2368 2369 shortName = "ecc"; 2370 QCPAxis *eccAxis = newStatsYAxis(shortName, 0, 1.0); 2371 ECCENTRICITY_GRAPH = initGraphAndCB(statsPlot, eccAxis, QCPGraph::lsStepRight, Qt::darkMagenta, "Eccentricity", 2372 shortName, eccentricityCB, Options::setAnalyzeEccentricity, eccentricityOut); 2373 shortName = "#Stars"; 2374 QCPAxis *numStarsAxis = newStatsYAxis(shortName); 2375 NUMSTARS_GRAPH = initGraphAndCB(statsPlot, numStarsAxis, QCPGraph::lsStepRight, Qt::magenta, "#Stars in Guide Image", 2376 shortName, numStarsCB, Options::setAnalyzeNumStars, numStarsOut); 2377 shortName = "SkyBG"; 2378 QCPAxis *skyBgAxis = newStatsYAxis(shortName); 2379 SKYBG_GRAPH = initGraphAndCB(statsPlot, skyBgAxis, QCPGraph::lsStepRight, Qt::darkYellow, "Sky Background Brightness", 2380 shortName, skyBgCB, Options::setAnalyzeSkyBg, skyBgOut); 2381 2382 shortName = "temp"; 2383 QCPAxis *temperatureAxis = newStatsYAxis(shortName, -40, 40); 2384 TEMPERATURE_GRAPH = initGraphAndCB(statsPlot, temperatureAxis, QCPGraph::lsLine, Qt::yellow, "Temperature", shortName, 2385 temperatureCB, Options::setAnalyzeTemperature, temperatureOut); 2386 shortName = "focus"; 2387 QCPAxis *focusPositionAxis = newStatsYAxis(shortName); 2388 FOCUS_POSITION_GRAPH = initGraphAndCB(statsPlot, focusPositionAxis, QCPGraph::lsStepLeft, Qt::lightGray, "Focus", shortName, 2389 focusPositionCB, Options::setFocusPosition, focusPositionOut); 2390 shortName = "tDist"; 2391 QCPAxis *targetDistanceAxis = newStatsYAxis(shortName, 0, 60); 2392 TARGET_DISTANCE_GRAPH = initGraphAndCB(statsPlot, targetDistanceAxis, QCPGraph::lsLine, 2393 QColor(253, 185, 200), // pink 2394 "Distance to Target (arcsec)", shortName, targetDistanceCB, Options::setAnalyzeTargetDistance, targetDistanceOut); 2395 shortName = "SNR"; 2396 QCPAxis *snrAxis = newStatsYAxis(shortName, -100, 100); 2397 SNR_GRAPH = initGraphAndCB(statsPlot, snrAxis, QCPGraph::lsLine, Qt::yellow, "Guider SNR", shortName, snrCB, 2398 Options::setAnalyzeSNR, snrOut); 2399 shortName = "RA"; 2400 auto raColor = KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError"); 2401 RA_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, raColor, "Guider RA Drift", shortName, raCB, 2402 Options::setAnalyzeRA, raOut); 2403 shortName = "DEC"; 2404 auto decColor = KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError"); 2405 DEC_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, decColor, "Guider DEC Drift", shortName, decCB, 2406 Options::setAnalyzeDEC, decOut); 2407 shortName = "RAp"; 2408 auto raPulseColor = KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError"); 2409 raPulseColor.setAlpha(75); 2410 QCPAxis *pulseAxis = newStatsYAxis(shortName, -2 * 150, 5 * 150); 2411 RA_PULSE_GRAPH = initGraphAndCB(statsPlot, pulseAxis, QCPGraph::lsLine, raPulseColor, "RA Correction Pulse (ms)", shortName, 2412 raPulseCB, Options::setAnalyzeRAp, raPulseOut); 2413 statsPlot->graph(RA_PULSE_GRAPH)->setBrush(QBrush(raPulseColor, Qt::Dense4Pattern)); 2414 2415 shortName = "DECp"; 2416 auto decPulseColor = KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError"); 2417 decPulseColor.setAlpha(75); 2418 DEC_PULSE_GRAPH = initGraphAndCB(statsPlot, pulseAxis, QCPGraph::lsLine, decPulseColor, "DEC Correction Pulse (ms)", 2419 shortName, decPulseCB, Options::setAnalyzeDECp, decPulseOut); 2420 statsPlot->graph(DEC_PULSE_GRAPH)->setBrush(QBrush(decPulseColor, Qt::Dense4Pattern)); 2421 2422 shortName = "Drift"; 2423 DRIFT_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, Qt::lightGray, "Guider Instantaneous Drift", 2424 shortName, driftCB, Options::setAnalyzeDrift, driftOut); 2425 shortName = "RMS"; 2426 RMS_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, Qt::red, "Guider RMS Drift", shortName, rmsCB, 2427 Options::setAnalyzeRMS, rmsOut); 2428 shortName = "RMSc"; 2429 CAPTURE_RMS_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, Qt::red, 2430 "Guider RMS Drift (during capture)", shortName, rmsCCB, 2431 Options::setAnalyzeRMSC, rmsCOut); 2432 shortName = "MOUNT_RA"; 2433 QCPAxis *mountRaDecAxis = newStatsYAxis(shortName, -10, 370); 2434 // Colors of these two unimportant--not really plotted. 2435 MOUNT_RA_GRAPH = initGraphAndCB(statsPlot, mountRaDecAxis, QCPGraph::lsLine, Qt::red, "Mount RA Degrees", shortName, 2436 mountRaCB, Options::setAnalyzeMountRA, mountRaOut); 2437 shortName = "MOUNT_DEC"; 2438 MOUNT_DEC_GRAPH = initGraphAndCB(statsPlot, mountRaDecAxis, QCPGraph::lsLine, Qt::red, "Mount DEC Degrees", shortName, 2439 mountDecCB, Options::setAnalyzeMountDEC, mountDecOut); 2440 shortName = "MOUNT_HA"; 2441 MOUNT_HA_GRAPH = initGraphAndCB(statsPlot, mountRaDecAxis, QCPGraph::lsLine, Qt::red, "Mount Hour Angle", shortName, 2442 mountHaCB, Options::setAnalyzeMountHA, mountHaOut); 2443 shortName = "AZ"; 2444 QCPAxis *azAxis = newStatsYAxis(shortName, -10, 370); 2445 AZ_GRAPH = initGraphAndCB(statsPlot, azAxis, QCPGraph::lsLine, Qt::darkGray, "Mount Azimuth", shortName, azCB, 2446 Options::setAnalyzeAz, azOut); 2447 shortName = "ALT"; 2448 QCPAxis *altAxis = newStatsYAxis(shortName, 0, 90); 2449 ALT_GRAPH = initGraphAndCB(statsPlot, altAxis, QCPGraph::lsLine, Qt::white, "Mount Altitude", shortName, altCB, 2450 Options::setAnalyzeAlt, altOut); 2451 shortName = "PierSide"; 2452 QCPAxis *pierSideAxis = newStatsYAxis(shortName, -2, 2); 2453 PIER_SIDE_GRAPH = initGraphAndCB(statsPlot, pierSideAxis, QCPGraph::lsLine, Qt::darkRed, "Mount Pier Side", shortName, 2454 pierSideCB, Options::setAnalyzePierSide, pierSideOut); 2455 2456 // This makes mouseMove only get called when a button is pressed. 2457 statsPlot->setMouseTracking(false); 2458 2459 // Setup the clock-time labels on the x-axis of the stats plot. 2460 dateTicker.reset(new OffsetDateTimeTicker); 2461 dateTicker->setDateTimeFormat("hh:mm:ss"); 2462 statsPlot->xAxis->setTicker(dateTicker); 2463 2464 // Didn't include QCP::iRangeDrag as it interacts poorly with the curson logic. 2465 statsPlot->setInteractions(QCP::iRangeZoom); 2466 2467 restoreYAxes(Options::analyzeStatsYAxis()); 2468 } 2469 2470 // Clear the graphics and state when changing input data. 2471 void Analyze::reset() 2472 { 2473 maxXValue = 10.0; 2474 plotStart = 0.0; 2475 plotWidth = 10.0; 2476 2477 guiderRms->resetFilter(); 2478 captureRms->resetFilter(); 2479 2480 unhighlightTimelineItem(); 2481 2482 for (int i = 0; i < statsPlot->graphCount(); ++i) 2483 statsPlot->graph(i)->data()->clear(); 2484 statsPlot->clearItems(); 2485 2486 for (int i = 0; i < timelinePlot->graphCount(); ++i) 2487 timelinePlot->graph(i)->data()->clear(); 2488 timelinePlot->clearItems(); 2489 2490 resetGraphicsPlot(); 2491 2492 detailsTable->clear(); 2493 QPalette p = detailsTable->palette(); 2494 p.setColor(QPalette::Base, Qt::black); 2495 p.setColor(QPalette::Text, Qt::white); 2496 detailsTable->setPalette(p); 2497 2498 inputValue->clear(); 2499 2500 captureSessions.clear(); 2501 focusSessions.clear(); 2502 guideSessions.clear(); 2503 mountSessions.clear(); 2504 alignSessions.clear(); 2505 mountFlipSessions.clear(); 2506 schedulerJobSessions.clear(); 2507 2508 numStarsOut->setText(""); 2509 skyBgOut->setText(""); 2510 snrOut->setText(""); 2511 temperatureOut->setText(""); 2512 focusPositionOut->setText(""); 2513 targetDistanceOut->setText(""); 2514 eccentricityOut->setText(""); 2515 medianOut->setText(""); 2516 numCaptureStarsOut->setText(""); 2517 2518 raOut->setText(""); 2519 decOut->setText(""); 2520 driftOut->setText(""); 2521 rmsOut->setText(""); 2522 rmsCOut->setText(""); 2523 2524 removeStatsCursor(); 2525 removeTemporarySessions(); 2526 2527 resetCaptureState(); 2528 resetAutofocusState(); 2529 resetGuideState(); 2530 resetGuideStats(); 2531 resetAlignState(); 2532 resetMountState(); 2533 resetMountCoords(); 2534 resetMountFlipState(); 2535 resetSchedulerJob(); 2536 2537 // Note: no replot(). 2538 } 2539 2540 void Analyze::initGraphicsPlot() 2541 { 2542 initQCP(graphicsPlot); 2543 FOCUS_GRAPHICS = initGraph(graphicsPlot, graphicsPlot->yAxis, 2544 QCPGraph::lsNone, Qt::cyan, "Focus"); 2545 graphicsPlot->graph(FOCUS_GRAPHICS)->setScatterStyle( 2546 QCPScatterStyle(QCPScatterStyle::ssCircle, Qt::white, Qt::white, 14)); 2547 FOCUS_GRAPHICS_FINAL = initGraph(graphicsPlot, graphicsPlot->yAxis, 2548 QCPGraph::lsNone, Qt::cyan, "FocusBest"); 2549 graphicsPlot->graph(FOCUS_GRAPHICS_FINAL)->setScatterStyle( 2550 QCPScatterStyle(QCPScatterStyle::ssCircle, Qt::yellow, Qt::yellow, 14)); 2551 FOCUS_GRAPHICS_CURVE = initGraph(graphicsPlot, graphicsPlot->yAxis, 2552 QCPGraph::lsLine, Qt::white, "FocusCurve"); 2553 graphicsPlot->setInteractions(QCP::iRangeZoom); 2554 graphicsPlot->setInteraction(QCP::iRangeDrag, true); 2555 2556 2557 GUIDER_GRAPHICS = initGraph(graphicsPlot, graphicsPlot->yAxis, 2558 QCPGraph::lsNone, Qt::cyan, "Guide Error"); 2559 graphicsPlot->graph(GUIDER_GRAPHICS)->setScatterStyle( 2560 QCPScatterStyle(QCPScatterStyle::ssStar, Qt::gray, 5)); 2561 } 2562 2563 void Analyze::displayFocusGraphics(const QVector<double> &positions, const QVector<double> &hfrs, 2564 const QString &curve, const QString &title, bool success) 2565 { 2566 resetGraphicsPlot(); 2567 auto graph = graphicsPlot->graph(FOCUS_GRAPHICS); 2568 auto finalGraph = graphicsPlot->graph(FOCUS_GRAPHICS_FINAL); 2569 double maxHfr = -1e8, maxPosition = -1e8, minHfr = 1e8, minPosition = 1e8; 2570 for (int i = 0; i < positions.size(); ++i) 2571 { 2572 // Yellow circle for the final point. 2573 if (success && i == positions.size() - 1) 2574 finalGraph->addData(positions[i], hfrs[i]); 2575 else 2576 graph->addData(positions[i], hfrs[i]); 2577 maxHfr = std::max(maxHfr, hfrs[i]); 2578 minHfr = std::min(minHfr, hfrs[i]); 2579 maxPosition = std::max(maxPosition, positions[i]); 2580 minPosition = std::min(minPosition, positions[i]); 2581 } 2582 2583 for (int i = 0; i < positions.size(); ++i) 2584 { 2585 QCPItemText *textLabel = new QCPItemText(graphicsPlot); 2586 textLabel->setPositionAlignment(Qt::AlignCenter | Qt::AlignHCenter); 2587 textLabel->position->setType(QCPItemPosition::ptPlotCoords); 2588 textLabel->position->setCoords(positions[i], hfrs[i]); 2589 textLabel->setText(QString::number(i + 1)); 2590 textLabel->setFont(QFont(font().family(), 12)); 2591 textLabel->setPen(Qt::NoPen); 2592 textLabel->setColor(Qt::red); 2593 } 2594 2595 const double xRange = maxPosition - minPosition; 2596 2597 // Draw the curve, if given. 2598 if (curve.size() > 0) 2599 { 2600 CurveFitting curveFitting(curve); 2601 const double interval = xRange / 20.0; 2602 auto curveGraph = graphicsPlot->graph(FOCUS_GRAPHICS_CURVE); 2603 for (double x = minPosition ; x < maxPosition ; x += interval) 2604 curveGraph->addData(x, curveFitting.f(x)); 2605 } 2606 2607 auto plotTitle = new QCPItemText(graphicsPlot); 2608 plotTitle->setColor(QColor(255, 255, 255)); 2609 plotTitle->setPositionAlignment(Qt::AlignTop | Qt::AlignHCenter); 2610 plotTitle->position->setType(QCPItemPosition::ptAxisRectRatio); 2611 plotTitle->position->setCoords(0.5, 0); 2612 plotTitle->setFont(QFont(font().family(), 10)); 2613 plotTitle->setVisible(true); 2614 plotTitle->setText(title); 2615 2616 // Set the same axes ranges as are used in focushfrvplot.cpp. 2617 const double upper = 1.5 * maxHfr; 2618 const double lower = minHfr - (0.25 * (upper - minHfr)); 2619 const double xPadding = hfrs.size() > 0 ? xRange / hfrs.size() : 10; 2620 graphicsPlot->xAxis->setRange(minPosition - xPadding, maxPosition + xPadding); 2621 graphicsPlot->yAxis->setRange(lower, upper); 2622 graphicsPlot->replot(); 2623 } 2624 2625 void Analyze::resetGraphicsPlot() 2626 { 2627 for (int i = 0; i < graphicsPlot->graphCount(); ++i) 2628 graphicsPlot->graph(i)->data()->clear(); 2629 graphicsPlot->clearItems(); 2630 } 2631 2632 void Analyze::displayFITS(const QString &filename) 2633 { 2634 QUrl url = QUrl::fromLocalFile(filename); 2635 2636 if (fitsViewer.isNull()) 2637 { 2638 fitsViewer = KStars::Instance()->createFITSViewer(); 2639 fitsViewer->loadFile(url); 2640 connect(fitsViewer.get(), &FITSViewer::terminated, this, [this]() 2641 { 2642 fitsViewer.clear(); 2643 }); 2644 } 2645 else 2646 fitsViewer->updateFile(url, 0); 2647 2648 fitsViewer->show(); 2649 } 2650 2651 void Analyze::helpMessage() 2652 { 2653 #ifdef Q_OS_OSX // This is because KHelpClient doesn't seem to be working right on MacOS 2654 KStars::Instance()->appHelpActivated(); 2655 #else 2656 KHelpClient::invokeHelp(QStringLiteral("tool-ekos.html#ekos-analyze"), QStringLiteral("kstars")); 2657 #endif 2658 } 2659 2660 // This is intended for recording data to file. 2661 // Don't use this when displaying data read from file, as this is not using the 2662 // correct analyzeStartTime. 2663 double Analyze::logTime(const QDateTime &time) 2664 { 2665 if (!logInitialized) 2666 startLog(); 2667 return (time.toMSecsSinceEpoch() - analyzeStartTime.toMSecsSinceEpoch()) / 1000.0; 2668 } 2669 2670 // The logTime using clock = now. 2671 // This is intended for recording data to file. 2672 // Don't use this When displaying data read from file. 2673 double Analyze::logTime() 2674 { 2675 return logTime(QDateTime::currentDateTime()); 2676 } 2677 2678 // Goes back to clock time from seconds into the log. 2679 // Appropriate for both displaying data from files as well as when displaying live data. 2680 QDateTime Analyze::clockTime(double logSeconds) 2681 { 2682 return displayStartTime.addMSecs(logSeconds * 1000.0); 2683 } 2684 2685 2686 // Write the command name, a timestamp and the message with comma separation to a .analyze file. 2687 void Analyze::saveMessage(const QString &type, const QString &message) 2688 { 2689 QString line(QString("%1,%2%3%4\n") 2690 .arg(type) 2691 .arg(QString::number(logTime(), 'f', 3)) 2692 .arg(message.size() > 0 ? "," : "", message)); 2693 appendToLog(line); 2694 } 2695 2696 // Start writing a .analyze file. 2697 void Analyze::startLog() 2698 { 2699 analyzeStartTime = QDateTime::currentDateTime(); 2700 startTimeInitialized = true; 2701 if (runtimeDisplay) 2702 displayStartTime = analyzeStartTime; 2703 if (logInitialized) 2704 return; 2705 QDir dir = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/analyze"); 2706 dir.mkpath("."); 2707 2708 logFilename = dir.filePath("ekos-" + QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss") + ".analyze"); 2709 logFile.setFileName(logFilename); 2710 logFile.open(QIODevice::WriteOnly | QIODevice::Text); 2711 2712 // This must happen before the below appendToLog() call. 2713 logInitialized = true; 2714 2715 appendToLog(QString("#KStars version %1. Analyze log version 1.0.\n\n") 2716 .arg(KSTARS_VERSION)); 2717 appendToLog(QString("%1,%2,%3\n") 2718 .arg("AnalyzeStartTime", analyzeStartTime.toString(timeFormat), analyzeStartTime.timeZoneAbbreviation())); 2719 } 2720 2721 void Analyze::appendToLog(const QString &lines) 2722 { 2723 if (!logInitialized) 2724 startLog(); 2725 QTextStream out(&logFile); 2726 out << lines; 2727 out.flush(); 2728 } 2729 2730 // maxXValue is the largest time value we have seen so far for this data. 2731 void Analyze::updateMaxX(double time) 2732 { 2733 maxXValue = std::max(time, maxXValue); 2734 } 2735 2736 // Manage temporary sessions displayed on the Timeline. 2737 // Those are ongoing sessions that will ultimately be replaced when the session is complete. 2738 // This only happens with live data, not with data read from .analyze files. 2739 2740 // Remove the graphic element. 2741 void Analyze::removeTemporarySession(Session * session) 2742 { 2743 if (session->rect != nullptr) 2744 timelinePlot->removeItem(session->rect); 2745 session->rect = nullptr; 2746 session->start = 0; 2747 session->end = 0; 2748 } 2749 2750 // Remove all temporary sessions (i.e. from all lines in the Timeline). 2751 void Analyze::removeTemporarySessions() 2752 { 2753 removeTemporarySession(&temporaryCaptureSession); 2754 removeTemporarySession(&temporaryMountFlipSession); 2755 removeTemporarySession(&temporaryFocusSession); 2756 removeTemporarySession(&temporaryGuideSession); 2757 removeTemporarySession(&temporaryMountSession); 2758 removeTemporarySession(&temporaryAlignSession); 2759 removeTemporarySession(&temporarySchedulerJobSession); 2760 } 2761 2762 // Add a new temporary session. 2763 void Analyze::addTemporarySession(Session * session, double time, double duration, 2764 int y_offset, const QBrush &brush) 2765 { 2766 removeTemporarySession(session); 2767 session->rect = addSession(time, time + duration, y_offset, brush); 2768 session->start = time; 2769 session->end = time + duration; 2770 session->offset = y_offset; 2771 session->temporaryBrush = brush; 2772 updateMaxX(time + duration); 2773 } 2774 2775 // Extend a temporary session. That is, we don't know how long the session will last, 2776 // so when new data arrives (from any module, not necessarily the one with the temporary 2777 // session) we must extend that temporary session. 2778 void Analyze::adjustTemporarySession(Session * session) 2779 { 2780 if (session->rect != nullptr && session->end < maxXValue) 2781 { 2782 QBrush brush = session->temporaryBrush; 2783 double start = session->start; 2784 int offset = session->offset; 2785 addTemporarySession(session, start, maxXValue - start, offset, brush); 2786 } 2787 } 2788 2789 // Extend all temporary sessions. 2790 void Analyze::adjustTemporarySessions() 2791 { 2792 adjustTemporarySession(&temporaryCaptureSession); 2793 adjustTemporarySession(&temporaryMountFlipSession); 2794 adjustTemporarySession(&temporaryFocusSession); 2795 adjustTemporarySession(&temporaryGuideSession); 2796 adjustTemporarySession(&temporaryMountSession); 2797 adjustTemporarySession(&temporaryAlignSession); 2798 adjustTemporarySession(&temporarySchedulerJobSession); 2799 } 2800 2801 // Called when the captureStarting slot receives a signal. 2802 // Saves the message to disk, and calls processCaptureStarting. 2803 void Analyze::captureStarting(double exposureSeconds, const QString &filter) 2804 { 2805 saveMessage("CaptureStarting", 2806 QString("%1,%2").arg(QString::number(exposureSeconds, 'f', 3), filter)); 2807 processCaptureStarting(logTime(), exposureSeconds, filter); 2808 } 2809 2810 // Called by either the above (when live data is received), or reading from file. 2811 // BatchMode would be true when reading from file. 2812 void Analyze::processCaptureStarting(double time, double exposureSeconds, const QString &filter) 2813 { 2814 captureStartedTime = time; 2815 captureStartedFilter = filter; 2816 updateMaxX(time); 2817 2818 addTemporarySession(&temporaryCaptureSession, time, 1, CAPTURE_Y, temporaryBrush); 2819 temporaryCaptureSession.duration = exposureSeconds; 2820 temporaryCaptureSession.filter = filter; 2821 } 2822 2823 // Called when the captureComplete slot receives a signal. 2824 void Analyze::captureComplete(const QVariantMap &metadata) 2825 { 2826 auto filename = metadata["filename"].toString(); 2827 auto exposure = metadata["exposure"].toDouble(); 2828 auto filter = metadata["filter"].toString(); 2829 auto hfr = metadata["hfr"].toDouble(); 2830 auto starCount = metadata["starCount"].toInt(); 2831 auto median = metadata["median"].toDouble(); 2832 auto eccentricity = metadata["eccentricity"].toDouble(); 2833 2834 saveMessage("CaptureComplete", 2835 QString("%1,%2,%3,%4,%5,%6,%7") 2836 .arg(QString::number(exposure, 'f', 3), filter, QString::number(hfr, 'f', 3), filename) 2837 .arg(starCount) 2838 .arg(median) 2839 .arg(QString::number(eccentricity, 'f', 3))); 2840 if (runtimeDisplay && captureStartedTime >= 0) 2841 processCaptureComplete(logTime(), filename, exposure, filter, hfr, starCount, median, eccentricity); 2842 } 2843 2844 void Analyze::processCaptureComplete(double time, const QString &filename, 2845 double exposureSeconds, const QString &filter, double hfr, 2846 int numStars, int median, double eccentricity, bool batchMode) 2847 { 2848 removeTemporarySession(&temporaryCaptureSession); 2849 QBrush stripe; 2850 if (filterStripeBrush(filter, &stripe)) 2851 addSession(captureStartedTime, time, CAPTURE_Y, successBrush, &stripe); 2852 else 2853 addSession(captureStartedTime, time, CAPTURE_Y, successBrush, nullptr); 2854 auto session = CaptureSession(captureStartedTime, time, nullptr, false, 2855 filename, exposureSeconds, filter); 2856 captureSessions.add(session); 2857 addHFR(hfr, numStars, median, eccentricity, time, captureStartedTime); 2858 updateMaxX(time); 2859 if (!batchMode) 2860 { 2861 if (runtimeDisplay && keepCurrentCB->isChecked() && statsCursor == nullptr) 2862 captureSessionClicked(session, false); 2863 replot(); 2864 } 2865 previousCaptureStartedTime = captureStartedTime; 2866 previousCaptureCompletedTime = time; 2867 captureStartedTime = -1; 2868 } 2869 2870 void Analyze::captureAborted(double exposureSeconds) 2871 { 2872 saveMessage("CaptureAborted", 2873 QString("%1").arg(QString::number(exposureSeconds, 'f', 3))); 2874 if (runtimeDisplay && captureStartedTime >= 0) 2875 processCaptureAborted(logTime(), exposureSeconds); 2876 } 2877 2878 void Analyze::processCaptureAborted(double time, double exposureSeconds, bool batchMode) 2879 { 2880 removeTemporarySession(&temporaryCaptureSession); 2881 double duration = time - captureStartedTime; 2882 if (captureStartedTime >= 0 && 2883 duration < (exposureSeconds + 30) && 2884 duration < 3600) 2885 { 2886 // You can get a captureAborted without a captureStarting, 2887 // so make sure this associates with a real start. 2888 addSession(captureStartedTime, time, CAPTURE_Y, failureBrush); 2889 auto session = CaptureSession(captureStartedTime, time, nullptr, true, "", 2890 exposureSeconds, captureStartedFilter); 2891 captureSessions.add(session); 2892 updateMaxX(time); 2893 if (!batchMode) 2894 { 2895 if (runtimeDisplay && keepCurrentCB->isChecked() && statsCursor == nullptr) 2896 captureSessionClicked(session, false); 2897 replot(); 2898 } 2899 captureStartedTime = -1; 2900 } 2901 previousCaptureStartedTime = -1; 2902 previousCaptureCompletedTime = -1; 2903 } 2904 2905 void Analyze::resetCaptureState() 2906 { 2907 captureStartedTime = -1; 2908 captureStartedFilter = ""; 2909 medianMax = 1; 2910 numCaptureStarsMax = 1; 2911 previousCaptureStartedTime = -1; 2912 previousCaptureCompletedTime = -1; 2913 } 2914 2915 void Analyze::autofocusStarting(double temperature, const QString &filter) 2916 { 2917 saveMessage("AutofocusStarting", 2918 QString("%1,%2") 2919 .arg(filter) 2920 .arg(QString::number(temperature, 'f', 1))); 2921 processAutofocusStarting(logTime(), temperature, filter); 2922 } 2923 2924 void Analyze::processAutofocusStarting(double time, double temperature, const QString &filter) 2925 { 2926 autofocusStartedTime = time; 2927 autofocusStartedFilter = filter; 2928 autofocusStartedTemperature = temperature; 2929 addTemperature(temperature, time); 2930 updateMaxX(time); 2931 2932 addTemporarySession(&temporaryFocusSession, time, 1, FOCUS_Y, temporaryBrush); 2933 temporaryFocusSession.temperature = temperature; 2934 temporaryFocusSession.filter = filter; 2935 } 2936 2937 void Analyze::adaptiveFocusComplete(const QString &filter, double temperature, double tempTicks, 2938 double altitude, double altTicks, int prevPosError, int thisPosError, 2939 int totalTicks, int position, bool focuserMoved) 2940 { 2941 saveMessage("AdaptiveFocusComplete", QString("%1,%2,%3,%4,%5,%6,%7,%8,%9,%10").arg(filter).arg(temperature, 0, 'f', 2) 2942 .arg(tempTicks, 0, 'f', 2).arg(altitude, 0, 'f', 2).arg(altTicks, 0, 'f', 2).arg(prevPosError) 2943 .arg(thisPosError).arg(totalTicks).arg(position).arg(focuserMoved ? 1 : 0)); 2944 2945 if (runtimeDisplay) 2946 processAdaptiveFocusComplete(logTime(), filter, temperature, tempTicks, altitude, altTicks, prevPosError, thisPosError, 2947 totalTicks, position, focuserMoved); 2948 } 2949 2950 void Analyze::processAdaptiveFocusComplete(double time, const QString &filter, double temperature, double tempTicks, 2951 double altitude, double altTicks, int prevPosError, int thisPosError, int totalTicks, int position, 2952 bool focuserMoved, bool batchMode) 2953 { 2954 removeTemporarySession(&temporaryFocusSession); 2955 2956 addFocusPosition(position, time); 2957 updateMaxX(time); 2958 2959 // In general if nothing happened we won't plot a value. This means there won't be lots of points with zeros in them. 2960 // However, we need to cover the situation of offsetting movements that overall don't move the focuser but still have non-zero detail 2961 if (!focuserMoved || (abs(tempTicks) < 1.00 && abs(altTicks) < 1.0 && prevPosError == 0 && thisPosError == 0)) 2962 return; 2963 2964 // Add a dot on the timeline. 2965 timelinePlot->graph(ADAPTIVE_FOCUS_GRAPH)->addData(time, FOCUS_Y); 2966 2967 // Add mouse sensitivity on the timeline. 2968 constexpr int artificialInterval = 10; 2969 auto session = FocusSession(time - artificialInterval, time + artificialInterval, nullptr, 2970 filter, temperature, tempTicks, altitude, altTicks, prevPosError, thisPosError, totalTicks, position); 2971 focusSessions.add(session); 2972 2973 if (!batchMode) 2974 replot(); 2975 2976 autofocusStartedTime = -1; 2977 } 2978 2979 void Analyze::autofocusComplete(const QString &filter, const QString &points, const QString &curve, const QString &rawTitle) 2980 { 2981 // Remove commas from the title as they're used as separators in the .analyze file. 2982 QString title = rawTitle; 2983 title.replace(",", " "); 2984 2985 if (curve.size() == 0) 2986 saveMessage("AutofocusComplete", QString("%1,%2").arg(filter, points)); 2987 else if (title.size() == 0) 2988 saveMessage("AutofocusComplete", QString("%1,%2,%3").arg(filter, points, curve)); 2989 else 2990 saveMessage("AutofocusComplete", QString("%1,%2,%3,%4").arg(filter, points, curve, title)); 2991 2992 if (runtimeDisplay && autofocusStartedTime >= 0) 2993 processAutofocusComplete(logTime(), filter, points, curve, title); 2994 } 2995 2996 void Analyze::processAutofocusComplete(double time, const QString &filter, const QString &points, 2997 const QString &curve, const QString &title, bool batchMode) 2998 { 2999 removeTemporarySession(&temporaryFocusSession); 3000 QBrush stripe; 3001 if (filterStripeBrush(filter, &stripe)) 3002 addSession(autofocusStartedTime, time, FOCUS_Y, successBrush, &stripe); 3003 else 3004 addSession(autofocusStartedTime, time, FOCUS_Y, successBrush, nullptr); 3005 auto session = FocusSession(autofocusStartedTime, time, nullptr, true, 3006 autofocusStartedTemperature, filter, points, curve, title); 3007 focusSessions.add(session); 3008 addFocusPosition(session.focusPosition(), autofocusStartedTime); 3009 updateMaxX(time); 3010 if (!batchMode) 3011 { 3012 if (runtimeDisplay && keepCurrentCB->isChecked() && statsCursor == nullptr) 3013 focusSessionClicked(session, false); 3014 replot(); 3015 } 3016 autofocusStartedTime = -1; 3017 } 3018 3019 void Analyze::autofocusAborted(const QString &filter, const QString &points) 3020 { 3021 saveMessage("AutofocusAborted", QString("%1,%2").arg(filter, points)); 3022 if (runtimeDisplay && autofocusStartedTime >= 0) 3023 processAutofocusAborted(logTime(), filter, points); 3024 } 3025 3026 void Analyze::processAutofocusAborted(double time, const QString &filter, const QString &points, bool batchMode) 3027 { 3028 removeTemporarySession(&temporaryFocusSession); 3029 double duration = time - autofocusStartedTime; 3030 if (autofocusStartedTime >= 0 && duration < 1000) 3031 { 3032 // Just in case.. 3033 addSession(autofocusStartedTime, time, FOCUS_Y, failureBrush); 3034 auto session = FocusSession(autofocusStartedTime, time, nullptr, false, 3035 autofocusStartedTemperature, filter, points, "", ""); 3036 focusSessions.add(session); 3037 updateMaxX(time); 3038 if (!batchMode) 3039 { 3040 if (runtimeDisplay && keepCurrentCB->isChecked() && statsCursor == nullptr) 3041 focusSessionClicked(session, false); 3042 replot(); 3043 } 3044 autofocusStartedTime = -1; 3045 } 3046 } 3047 3048 void Analyze::resetAutofocusState() 3049 { 3050 autofocusStartedTime = -1; 3051 autofocusStartedFilter = ""; 3052 autofocusStartedTemperature = 0; 3053 } 3054 3055 namespace 3056 { 3057 3058 // TODO: move to ekos.h/cpp? 3059 Ekos::GuideState stringToGuideState(const QString &str) 3060 { 3061 if (str == i18n("Idle")) 3062 return GUIDE_IDLE; 3063 else if (str == i18n("Aborted")) 3064 return GUIDE_ABORTED; 3065 else if (str == i18n("Connected")) 3066 return GUIDE_CONNECTED; 3067 else if (str == i18n("Disconnected")) 3068 return GUIDE_DISCONNECTED; 3069 else if (str == i18n("Capturing")) 3070 return GUIDE_CAPTURE; 3071 else if (str == i18n("Looping")) 3072 return GUIDE_LOOPING; 3073 else if (str == i18n("Subtracting")) 3074 return GUIDE_DARK; 3075 else if (str == i18n("Subframing")) 3076 return GUIDE_SUBFRAME; 3077 else if (str == i18n("Selecting star")) 3078 return GUIDE_STAR_SELECT; 3079 else if (str == i18n("Calibrating")) 3080 return GUIDE_CALIBRATING; 3081 else if (str == i18n("Calibration error")) 3082 return GUIDE_CALIBRATION_ERROR; 3083 else if (str == i18n("Calibrated")) 3084 return GUIDE_CALIBRATION_SUCCESS; 3085 else if (str == i18n("Guiding")) 3086 return GUIDE_GUIDING; 3087 else if (str == i18n("Suspended")) 3088 return GUIDE_SUSPENDED; 3089 else if (str == i18n("Reacquiring")) 3090 return GUIDE_REACQUIRE; 3091 else if (str == i18n("Dithering")) 3092 return GUIDE_DITHERING; 3093 else if (str == i18n("Manual Dithering")) 3094 return GUIDE_MANUAL_DITHERING; 3095 else if (str == i18n("Dithering error")) 3096 return GUIDE_DITHERING_ERROR; 3097 else if (str == i18n("Dithering successful")) 3098 return GUIDE_DITHERING_SUCCESS; 3099 else if (str == i18n("Settling")) 3100 return GUIDE_DITHERING_SETTLE; 3101 else 3102 return GUIDE_IDLE; 3103 } 3104 3105 Analyze::SimpleGuideState convertGuideState(Ekos::GuideState state) 3106 { 3107 switch (state) 3108 { 3109 case GUIDE_IDLE: 3110 case GUIDE_ABORTED: 3111 case GUIDE_CONNECTED: 3112 case GUIDE_DISCONNECTED: 3113 case GUIDE_LOOPING: 3114 return Analyze::G_IDLE; 3115 case GUIDE_GUIDING: 3116 return Analyze::G_GUIDING; 3117 case GUIDE_CAPTURE: 3118 case GUIDE_DARK: 3119 case GUIDE_SUBFRAME: 3120 case GUIDE_STAR_SELECT: 3121 return Analyze::G_IGNORE; 3122 case GUIDE_CALIBRATING: 3123 case GUIDE_CALIBRATION_ERROR: 3124 case GUIDE_CALIBRATION_SUCCESS: 3125 return Analyze::G_CALIBRATING; 3126 case GUIDE_SUSPENDED: 3127 case GUIDE_REACQUIRE: 3128 return Analyze::G_SUSPENDED; 3129 case GUIDE_DITHERING: 3130 case GUIDE_MANUAL_DITHERING: 3131 case GUIDE_DITHERING_ERROR: 3132 case GUIDE_DITHERING_SUCCESS: 3133 case GUIDE_DITHERING_SETTLE: 3134 return Analyze::G_DITHERING; 3135 } 3136 // Shouldn't get here--would get compile error, I believe with a missing case. 3137 return Analyze::G_IDLE; 3138 } 3139 3140 const QBrush guideBrush(Analyze::SimpleGuideState simpleState) 3141 { 3142 switch (simpleState) 3143 { 3144 case Analyze::G_IDLE: 3145 case Analyze::G_IGNORE: 3146 // don't actually render these, so don't care. 3147 return offBrush; 3148 case Analyze::G_GUIDING: 3149 return successBrush; 3150 case Analyze::G_CALIBRATING: 3151 return progressBrush; 3152 case Analyze::G_SUSPENDED: 3153 return stoppedBrush; 3154 case Analyze::G_DITHERING: 3155 return progress2Brush; 3156 } 3157 // Shouldn't get here. 3158 return offBrush; 3159 } 3160 3161 } // namespace 3162 3163 void Analyze::guideState(Ekos::GuideState state) 3164 { 3165 QString str = getGuideStatusString(state); 3166 saveMessage("GuideState", str); 3167 if (runtimeDisplay) 3168 processGuideState(logTime(), str); 3169 } 3170 3171 void Analyze::processGuideState(double time, const QString &stateStr, bool batchMode) 3172 { 3173 Ekos::GuideState gstate = stringToGuideState(stateStr); 3174 SimpleGuideState state = convertGuideState(gstate); 3175 if (state == G_IGNORE) 3176 return; 3177 if (state == lastGuideStateStarted) 3178 return; 3179 // End the previous guide session and start the new one. 3180 if (guideStateStartedTime >= 0) 3181 { 3182 if (lastGuideStateStarted != G_IDLE) 3183 { 3184 // Don't render the idle guiding 3185 addSession(guideStateStartedTime, time, GUIDE_Y, guideBrush(lastGuideStateStarted)); 3186 guideSessions.add(GuideSession(guideStateStartedTime, time, nullptr, lastGuideStateStarted)); 3187 } 3188 } 3189 if (state == G_GUIDING) 3190 { 3191 addTemporarySession(&temporaryGuideSession, time, 1, GUIDE_Y, successBrush); 3192 temporaryGuideSession.simpleState = state; 3193 } 3194 else 3195 removeTemporarySession(&temporaryGuideSession); 3196 3197 guideStateStartedTime = time; 3198 lastGuideStateStarted = state; 3199 updateMaxX(time); 3200 if (!batchMode) 3201 replot(); 3202 } 3203 3204 void Analyze::resetGuideState() 3205 { 3206 lastGuideStateStarted = G_IDLE; 3207 guideStateStartedTime = -1; 3208 } 3209 3210 void Analyze::newTemperature(double temperatureDelta, double temperature) 3211 { 3212 Q_UNUSED(temperatureDelta); 3213 if (temperature > -200 && temperature != lastTemperature) 3214 { 3215 saveMessage("Temperature", QString("%1").arg(QString::number(temperature, 'f', 3))); 3216 lastTemperature = temperature; 3217 if (runtimeDisplay) 3218 processTemperature(logTime(), temperature); 3219 } 3220 } 3221 3222 void Analyze::processTemperature(double time, double temperature, bool batchMode) 3223 { 3224 addTemperature(temperature, time); 3225 updateMaxX(time); 3226 if (!batchMode) 3227 replot(); 3228 } 3229 3230 void Analyze::resetTemperature() 3231 { 3232 lastTemperature = -1000; 3233 } 3234 3235 void Analyze::newTargetDistance(double targetDistance) 3236 { 3237 saveMessage("TargetDistance", QString("%1").arg(QString::number(targetDistance, 'f', 0))); 3238 if (runtimeDisplay) 3239 processTargetDistance(logTime(), targetDistance); 3240 } 3241 3242 void Analyze::processTargetDistance(double time, double targetDistance, bool batchMode) 3243 { 3244 addTargetDistance(targetDistance, time); 3245 updateMaxX(time); 3246 if (!batchMode) 3247 replot(); 3248 } 3249 3250 void Analyze::guideStats(double raError, double decError, int raPulse, int decPulse, 3251 double snr, double skyBg, int numStars) 3252 { 3253 saveMessage("GuideStats", QString("%1,%2,%3,%4,%5,%6,%7") 3254 .arg(QString::number(raError, 'f', 3), QString::number(decError, 'f', 3)) 3255 .arg(raPulse) 3256 .arg(decPulse) 3257 .arg(QString::number(snr, 'f', 3), QString::number(skyBg, 'f', 3)) 3258 .arg(numStars)); 3259 3260 if (runtimeDisplay) 3261 processGuideStats(logTime(), raError, decError, raPulse, decPulse, snr, skyBg, numStars); 3262 } 3263 3264 void Analyze::processGuideStats(double time, double raError, double decError, 3265 int raPulse, int decPulse, double snr, double skyBg, int numStars, bool batchMode) 3266 { 3267 addGuideStats(raError, decError, raPulse, decPulse, snr, numStars, skyBg, time); 3268 updateMaxX(time); 3269 if (!batchMode) 3270 replot(); 3271 } 3272 3273 void Analyze::resetGuideStats() 3274 { 3275 lastGuideStatsTime = -1; 3276 lastCaptureRmsTime = -1; 3277 numStarsMax = 0; 3278 snrMax = 0; 3279 skyBgMax = 0; 3280 } 3281 3282 namespace 3283 { 3284 3285 // TODO: move to ekos.h/cpp 3286 AlignState convertAlignState(const QString &str) 3287 { 3288 for (int i = 0; i < alignStates.size(); ++i) 3289 { 3290 if (str == i18n(alignStates[i])) 3291 return static_cast<AlignState>(i); 3292 } 3293 return ALIGN_IDLE; 3294 } 3295 3296 const QBrush alignBrush(AlignState state) 3297 { 3298 switch (state) 3299 { 3300 case ALIGN_IDLE: 3301 return offBrush; 3302 case ALIGN_COMPLETE: 3303 case ALIGN_SUCCESSFUL: 3304 return successBrush; 3305 case ALIGN_FAILED: 3306 return failureBrush; 3307 case ALIGN_PROGRESS: 3308 return progress3Brush; 3309 case ALIGN_SYNCING: 3310 return progress2Brush; 3311 case ALIGN_SLEWING: 3312 return progressBrush; 3313 case ALIGN_ROTATING: 3314 return progress4Brush; 3315 case ALIGN_ABORTED: 3316 return failureBrush; 3317 case ALIGN_SUSPENDED: 3318 return offBrush; 3319 } 3320 // Shouldn't get here. 3321 return offBrush; 3322 } 3323 } // namespace 3324 3325 void Analyze::alignState(AlignState state) 3326 { 3327 if (state == lastAlignStateReceived) 3328 return; 3329 lastAlignStateReceived = state; 3330 3331 QString stateStr = getAlignStatusString(state); 3332 saveMessage("AlignState", stateStr); 3333 if (runtimeDisplay) 3334 processAlignState(logTime(), stateStr); 3335 } 3336 3337 //ALIGN_IDLE, ALIGN_COMPLETE, ALIGN_FAILED, ALIGN_ABORTED,ALIGN_PROGRESS,ALIGN_SYNCING,ALIGN_SLEWING 3338 void Analyze::processAlignState(double time, const QString &statusString, bool batchMode) 3339 { 3340 AlignState state = convertAlignState(statusString); 3341 3342 if (state == lastAlignStateStarted) 3343 return; 3344 3345 bool lastStateInteresting = (lastAlignStateStarted == ALIGN_PROGRESS || 3346 lastAlignStateStarted == ALIGN_SYNCING || 3347 lastAlignStateStarted == ALIGN_SLEWING); 3348 if (lastAlignStateStartedTime >= 0 && lastStateInteresting) 3349 { 3350 if (state == ALIGN_COMPLETE || state == ALIGN_FAILED || state == ALIGN_ABORTED) 3351 { 3352 // These states are really commetaries on the previous states. 3353 addSession(lastAlignStateStartedTime, time, ALIGN_Y, alignBrush(state)); 3354 alignSessions.add(AlignSession(lastAlignStateStartedTime, time, nullptr, state)); 3355 } 3356 else 3357 { 3358 addSession(lastAlignStateStartedTime, time, ALIGN_Y, alignBrush(lastAlignStateStarted)); 3359 alignSessions.add(AlignSession(lastAlignStateStartedTime, time, nullptr, lastAlignStateStarted)); 3360 } 3361 } 3362 bool stateInteresting = (state == ALIGN_PROGRESS || state == ALIGN_SYNCING || 3363 state == ALIGN_SLEWING); 3364 if (stateInteresting) 3365 { 3366 addTemporarySession(&temporaryAlignSession, time, 1, ALIGN_Y, temporaryBrush); 3367 temporaryAlignSession.state = state; 3368 } 3369 else 3370 removeTemporarySession(&temporaryAlignSession); 3371 3372 lastAlignStateStartedTime = time; 3373 lastAlignStateStarted = state; 3374 updateMaxX(time); 3375 if (!batchMode) 3376 replot(); 3377 3378 } 3379 3380 void Analyze::resetAlignState() 3381 { 3382 lastAlignStateReceived = ALIGN_IDLE; 3383 lastAlignStateStarted = ALIGN_IDLE; 3384 lastAlignStateStartedTime = -1; 3385 } 3386 3387 namespace 3388 { 3389 3390 const QBrush mountBrush(ISD::Mount::Status state) 3391 { 3392 switch (state) 3393 { 3394 case ISD::Mount::MOUNT_IDLE: 3395 return offBrush; 3396 case ISD::Mount::MOUNT_ERROR: 3397 return failureBrush; 3398 case ISD::Mount::MOUNT_MOVING: 3399 case ISD::Mount::MOUNT_SLEWING: 3400 return progressBrush; 3401 case ISD::Mount::MOUNT_TRACKING: 3402 return successBrush; 3403 case ISD::Mount::MOUNT_PARKING: 3404 return stoppedBrush; 3405 case ISD::Mount::MOUNT_PARKED: 3406 return stopped2Brush; 3407 } 3408 // Shouldn't get here. 3409 return offBrush; 3410 } 3411 3412 } // namespace 3413 3414 // Mount status can be: 3415 // MOUNT_IDLE, MOUNT_MOVING, MOUNT_SLEWING, MOUNT_TRACKING, MOUNT_PARKING, MOUNT_PARKED, MOUNT_ERROR 3416 void Analyze::mountState(ISD::Mount::Status state) 3417 { 3418 QString statusString = mountStatusString(state); 3419 saveMessage("MountState", statusString); 3420 if (runtimeDisplay) 3421 processMountState(logTime(), statusString); 3422 } 3423 3424 void Analyze::processMountState(double time, const QString &statusString, bool batchMode) 3425 { 3426 ISD::Mount::Status state = toMountStatus(statusString); 3427 if (mountStateStartedTime >= 0 && lastMountState != ISD::Mount::MOUNT_IDLE) 3428 { 3429 addSession(mountStateStartedTime, time, MOUNT_Y, mountBrush(lastMountState)); 3430 mountSessions.add(MountSession(mountStateStartedTime, time, nullptr, lastMountState)); 3431 } 3432 3433 if (state != ISD::Mount::MOUNT_IDLE) 3434 { 3435 addTemporarySession(&temporaryMountSession, time, 1, MOUNT_Y, 3436 (state == ISD::Mount::MOUNT_TRACKING) ? successBrush : temporaryBrush); 3437 temporaryMountSession.state = state; 3438 } 3439 else 3440 removeTemporarySession(&temporaryMountSession); 3441 3442 mountStateStartedTime = time; 3443 lastMountState = state; 3444 updateMaxX(time); 3445 if (!batchMode) 3446 replot(); 3447 } 3448 3449 void Analyze::resetMountState() 3450 { 3451 mountStateStartedTime = -1; 3452 lastMountState = ISD::Mount::Status::MOUNT_IDLE; 3453 } 3454 3455 // This message comes from the mount module 3456 void Analyze::mountCoords(const SkyPoint &position, ISD::Mount::PierSide pierSide, const dms &haValue) 3457 { 3458 double ra = position.ra().Degrees(); 3459 double dec = position.dec().Degrees(); 3460 double ha = haValue.Degrees(); 3461 double az = position.az().Degrees(); 3462 double alt = position.alt().Degrees(); 3463 3464 // Only process the message if something's changed by 1/4 degree or more. 3465 constexpr double MIN_DEGREES_CHANGE = 0.25; 3466 if ((fabs(ra - lastMountRa) > MIN_DEGREES_CHANGE) || 3467 (fabs(dec - lastMountDec) > MIN_DEGREES_CHANGE) || 3468 (fabs(ha - lastMountHa) > MIN_DEGREES_CHANGE) || 3469 (fabs(az - lastMountAz) > MIN_DEGREES_CHANGE) || 3470 (fabs(alt - lastMountAlt) > MIN_DEGREES_CHANGE) || 3471 (pierSide != lastMountPierSide)) 3472 { 3473 saveMessage("MountCoords", QString("%1,%2,%3,%4,%5,%6") 3474 .arg(QString::number(ra, 'f', 4), QString::number(dec, 'f', 4), 3475 QString::number(az, 'f', 4), QString::number(alt, 'f', 4)) 3476 .arg(pierSide) 3477 .arg(QString::number(ha, 'f', 4))); 3478 3479 if (runtimeDisplay) 3480 processMountCoords(logTime(), ra, dec, az, alt, pierSide, ha); 3481 3482 lastMountRa = ra; 3483 lastMountDec = dec; 3484 lastMountHa = ha; 3485 lastMountAz = az; 3486 lastMountAlt = alt; 3487 lastMountPierSide = pierSide; 3488 } 3489 } 3490 3491 void Analyze::processMountCoords(double time, double ra, double dec, double az, 3492 double alt, int pierSide, double ha, bool batchMode) 3493 { 3494 addMountCoords(ra, dec, az, alt, pierSide, ha, time); 3495 updateMaxX(time); 3496 if (!batchMode) 3497 replot(); 3498 } 3499 3500 void Analyze::resetMountCoords() 3501 { 3502 lastMountRa = -1; 3503 lastMountDec = -1; 3504 lastMountHa = -1; 3505 lastMountAz = -1; 3506 lastMountAlt = -1; 3507 lastMountPierSide = -1; 3508 } 3509 3510 namespace 3511 { 3512 3513 // TODO: Move to mount.h/cpp? 3514 MeridianFlipState::MeridianFlipMountState convertMountFlipState(const QString &statusStr) 3515 { 3516 if (statusStr == "MOUNT_FLIP_NONE") 3517 return MeridianFlipState::MOUNT_FLIP_NONE; 3518 else if (statusStr == "MOUNT_FLIP_PLANNED") 3519 return MeridianFlipState::MOUNT_FLIP_PLANNED; 3520 else if (statusStr == "MOUNT_FLIP_WAITING") 3521 return MeridianFlipState::MOUNT_FLIP_WAITING; 3522 else if (statusStr == "MOUNT_FLIP_ACCEPTED") 3523 return MeridianFlipState::MOUNT_FLIP_ACCEPTED; 3524 else if (statusStr == "MOUNT_FLIP_RUNNING") 3525 return MeridianFlipState::MOUNT_FLIP_RUNNING; 3526 else if (statusStr == "MOUNT_FLIP_COMPLETED") 3527 return MeridianFlipState::MOUNT_FLIP_COMPLETED; 3528 else if (statusStr == "MOUNT_FLIP_ERROR") 3529 return MeridianFlipState::MOUNT_FLIP_ERROR; 3530 return MeridianFlipState::MOUNT_FLIP_ERROR; 3531 } 3532 3533 QBrush mountFlipStateBrush(MeridianFlipState::MeridianFlipMountState state) 3534 { 3535 switch (state) 3536 { 3537 case MeridianFlipState::MOUNT_FLIP_NONE: 3538 return offBrush; 3539 case MeridianFlipState::MOUNT_FLIP_PLANNED: 3540 return stoppedBrush; 3541 case MeridianFlipState::MOUNT_FLIP_WAITING: 3542 return stopped2Brush; 3543 case MeridianFlipState::MOUNT_FLIP_ACCEPTED: 3544 return progressBrush; 3545 case MeridianFlipState::MOUNT_FLIP_RUNNING: 3546 return progress2Brush; 3547 case MeridianFlipState::MOUNT_FLIP_COMPLETED: 3548 return successBrush; 3549 case MeridianFlipState::MOUNT_FLIP_ERROR: 3550 return failureBrush; 3551 } 3552 // Shouldn't get here. 3553 return offBrush; 3554 } 3555 } // namespace 3556 3557 void Analyze::mountFlipStatus(MeridianFlipState::MeridianFlipMountState state) 3558 { 3559 if (state == lastMountFlipStateReceived) 3560 return; 3561 lastMountFlipStateReceived = state; 3562 3563 QString stateStr = MeridianFlipState::meridianFlipStatusString(state); 3564 saveMessage("MeridianFlipState", stateStr); 3565 if (runtimeDisplay) 3566 processMountFlipState(logTime(), stateStr); 3567 3568 } 3569 3570 // MeridianFlipState::MOUNT_FLIP_NONE MeridianFlipState::MOUNT_FLIP_PLANNED MeridianFlipState::MOUNT_FLIP_WAITING MeridianFlipState::MOUNT_FLIP_ACCEPTED MeridianFlipState::MOUNT_FLIP_RUNNING MeridianFlipState::MOUNT_FLIP_COMPLETED MeridianFlipState::MOUNT_FLIP_ERROR 3571 void Analyze::processMountFlipState(double time, const QString &statusString, bool batchMode) 3572 { 3573 MeridianFlipState::MeridianFlipMountState state = convertMountFlipState(statusString); 3574 if (state == lastMountFlipStateStarted) 3575 return; 3576 3577 bool lastStateInteresting = 3578 (lastMountFlipStateStarted == MeridianFlipState::MOUNT_FLIP_PLANNED || 3579 lastMountFlipStateStarted == MeridianFlipState::MOUNT_FLIP_WAITING || 3580 lastMountFlipStateStarted == MeridianFlipState::MOUNT_FLIP_ACCEPTED || 3581 lastMountFlipStateStarted == MeridianFlipState::MOUNT_FLIP_RUNNING); 3582 if (mountFlipStateStartedTime >= 0 && lastStateInteresting) 3583 { 3584 if (state == MeridianFlipState::MOUNT_FLIP_COMPLETED || state == MeridianFlipState::MOUNT_FLIP_ERROR) 3585 { 3586 // These states are really commentaries on the previous states. 3587 addSession(mountFlipStateStartedTime, time, MERIDIAN_MOUNT_FLIP_Y, mountFlipStateBrush(state)); 3588 mountFlipSessions.add(MountFlipSession(mountFlipStateStartedTime, time, nullptr, state)); 3589 } 3590 else 3591 { 3592 addSession(mountFlipStateStartedTime, time, MERIDIAN_MOUNT_FLIP_Y, mountFlipStateBrush(lastMountFlipStateStarted)); 3593 mountFlipSessions.add(MountFlipSession(mountFlipStateStartedTime, time, nullptr, lastMountFlipStateStarted)); 3594 } 3595 } 3596 bool stateInteresting = 3597 (state == MeridianFlipState::MOUNT_FLIP_PLANNED || 3598 state == MeridianFlipState::MOUNT_FLIP_WAITING || 3599 state == MeridianFlipState::MOUNT_FLIP_ACCEPTED || 3600 state == MeridianFlipState::MOUNT_FLIP_RUNNING); 3601 if (stateInteresting) 3602 { 3603 addTemporarySession(&temporaryMountFlipSession, time, 1, MERIDIAN_MOUNT_FLIP_Y, temporaryBrush); 3604 temporaryMountFlipSession.state = state; 3605 } 3606 else 3607 removeTemporarySession(&temporaryMountFlipSession); 3608 3609 mountFlipStateStartedTime = time; 3610 lastMountFlipStateStarted = state; 3611 updateMaxX(time); 3612 if (!batchMode) 3613 replot(); 3614 } 3615 3616 void Analyze::resetMountFlipState() 3617 { 3618 lastMountFlipStateReceived = MeridianFlipState::MOUNT_FLIP_NONE; 3619 lastMountFlipStateStarted = MeridianFlipState::MOUNT_FLIP_NONE; 3620 mountFlipStateStartedTime = -1; 3621 } 3622 3623 QBrush Analyze::schedulerJobBrush(const QString &jobName, bool temporary) 3624 { 3625 QList<QColor> colors = 3626 { 3627 {110, 120, 150}, {150, 180, 180}, {180, 165, 130}, {180, 200, 140}, {250, 180, 130}, 3628 {190, 170, 160}, {140, 110, 160}, {250, 240, 190}, {250, 200, 220}, {150, 125, 175} 3629 }; 3630 3631 Qt::BrushStyle pattern = temporary ? Qt::Dense4Pattern : Qt::SolidPattern; 3632 auto it = schedulerJobColors.constFind(jobName); 3633 if (it == schedulerJobColors.constEnd()) 3634 { 3635 const int numSoFar = schedulerJobColors.size(); 3636 auto color = colors[numSoFar % colors.size()]; 3637 schedulerJobColors[jobName] = color; 3638 return QBrush(color, pattern); 3639 } 3640 else 3641 { 3642 return QBrush(*it, pattern); 3643 } 3644 } 3645 3646 void Analyze::schedulerJobStarted(const QString &jobName) 3647 { 3648 saveMessage("SchedulerJobStart", jobName); 3649 if (runtimeDisplay) 3650 processSchedulerJobStarted(logTime(), jobName); 3651 3652 } 3653 3654 void Analyze::schedulerJobEnded(const QString &jobName, const QString &reason) 3655 { 3656 saveMessage("SchedulerJobEnd", QString("%1,%2").arg(jobName, reason)); 3657 if (runtimeDisplay) 3658 processSchedulerJobEnded(logTime(), jobName, reason); 3659 } 3660 3661 3662 // Called by either the above (when live data is received), or reading from file. 3663 // BatchMode would be true when reading from file. 3664 void Analyze::processSchedulerJobStarted(double time, const QString &jobName) 3665 { 3666 checkForMissingSchedulerJobEnd(time - 1); 3667 schedulerJobStartedTime = time; 3668 schedulerJobStartedJobName = jobName; 3669 updateMaxX(time); 3670 3671 addTemporarySession(&temporarySchedulerJobSession, time, 1, SCHEDULER_Y, schedulerJobBrush(jobName, true)); 3672 temporarySchedulerJobSession.jobName = jobName; 3673 } 3674 3675 // Called when the captureComplete slot receives a signal. 3676 void Analyze::processSchedulerJobEnded(double time, const QString &jobName, const QString &reason, bool batchMode) 3677 { 3678 removeTemporarySession(&temporarySchedulerJobSession); 3679 3680 if (schedulerJobStartedTime < 0) 3681 { 3682 replot(); 3683 return; 3684 } 3685 3686 addSession(schedulerJobStartedTime, time, SCHEDULER_Y, schedulerJobBrush(jobName, false)); 3687 auto session = SchedulerJobSession(schedulerJobStartedTime, time, nullptr, jobName, reason); 3688 schedulerJobSessions.add(session); 3689 updateMaxX(time); 3690 resetSchedulerJob(); 3691 if (!batchMode) 3692 replot(); 3693 } 3694 3695 // Just called in batch mode, in case the processSchedulerJobEnded was never called. 3696 void Analyze::checkForMissingSchedulerJobEnd(double time) 3697 { 3698 if (schedulerJobStartedTime < 0) 3699 return; 3700 removeTemporarySession(&temporarySchedulerJobSession); 3701 addSession(schedulerJobStartedTime, time, SCHEDULER_Y, schedulerJobBrush(schedulerJobStartedJobName, false)); 3702 auto session = SchedulerJobSession(schedulerJobStartedTime, time, nullptr, schedulerJobStartedJobName, "missing job end"); 3703 schedulerJobSessions.add(session); 3704 updateMaxX(time); 3705 resetSchedulerJob(); 3706 } 3707 3708 void Analyze::resetSchedulerJob() 3709 { 3710 schedulerJobStartedTime = -1; 3711 schedulerJobStartedJobName = ""; 3712 } 3713 3714 void Analyze::appendLogText(const QString &text) 3715 { 3716 m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", 3717 KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text)); 3718 3719 qCInfo(KSTARS_EKOS_ANALYZE) << text; 3720 3721 emit newLog(text); 3722 } 3723 3724 void Analyze::clearLog() 3725 { 3726 m_LogText.clear(); 3727 emit newLog(QString()); 3728 } 3729 3730 } // namespace Ekos