diff --git a/kstars/ekos/align/align.cpp b/kstars/ekos/align/align.cpp index 3efd60361..d583b1aec 100644 --- a/kstars/ekos/align/align.cpp +++ b/kstars/ekos/align/align.cpp @@ -1,6143 +1,6143 @@ /* Ekos Alignment Module Copyright (C) 2013 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "align.h" #include "alignadaptor.h" #include "alignview.h" #include "flagcomponent.h" #include "fov.h" #include "kstars.h" #include "kstarsdata.h" #include "ksuserdb.h" #include "offlineastrometryparser.h" #include "onlineastrometryparser.h" #include "astapastrometryparser.h" #include "opsalign.h" #include "opsastap.h" #include "opsastrometry.h" #include "opsastrometrycfg.h" #include "opsastrometryindexfiles.h" #include "Options.h" #include "remoteastrometryparser.h" #include "skymap.h" #include "skymapcomposite.h" #include "starobject.h" #include "auxiliary/QProgressIndicator.h" #include "auxiliary/ksmessagebox.h" #include "dialogs/finddialog.h" #include "ekos/manager.h" #include "ekos/auxiliary/darklibrary.h" #include "fitsviewer/fitsdata.h" #include "fitsviewer/fitstab.h" #include "indi/clientmanager.h" #include "indi/driverinfo.h" #include "indi/indifilter.h" #include "profileinfo.h" #include "ksnotification.h" #include #include #include #include #include #include #define PAH_CUTOFF_FOV 10 // Minimum FOV width in arcminutes for PAH to work #define MAXIMUM_SOLVER_ITERATIONS 10 #define CAPTURE_RETRY_DELAY 10000 #define AL_FORMAT_VERSION 1.0 namespace Ekos { // 30 arcminutes RA movement const double Align::RAMotion = 0.5; // Sidereal rate, degrees/s const double Align::SIDRATE = 0.004178; const QMap Align::PAHStages = { {PAH_IDLE, I18N_NOOP("Idle")}, {PAH_FIRST_CAPTURE, I18N_NOOP("First Capture"}), {PAH_FIND_CP, I18N_NOOP("Finding CP"}), {PAH_FIRST_ROTATE, I18N_NOOP("First Rotation"}), {PAH_SECOND_CAPTURE, I18N_NOOP("Second Capture"}), {PAH_SECOND_ROTATE, I18N_NOOP("Second Rotation"}), {PAH_THIRD_CAPTURE, I18N_NOOP("Third Capture"}), {PAH_STAR_SELECT, I18N_NOOP("Select Star"}), {PAH_PRE_REFRESH, I18N_NOOP("Select Refresh"}), {PAH_REFRESH, I18N_NOOP("Refreshing"}), {PAH_ERROR, I18N_NOOP("Error")}, }; Align::Align(ProfileInfo *activeProfile) : m_ActiveProfile(activeProfile) { setupUi(this); qRegisterMetaType("Ekos::AlignState"); qDBusRegisterMetaType(); new AlignAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Align", this); dirPath = QDir::homePath(); //loadSlewMode = false; solverFOV.reset(new FOV()); solverFOV->setName(i18n("Solver FOV")); solverFOV->setLockCelestialPole(true); solverFOV->setColor(KStars::Instance()->data()->colorScheme()->colorNamed("SolverFOVColor").name()); sensorFOV.reset(new FOV()); sensorFOV->setLockCelestialPole(true); QAction *a = KStars::Instance()->actionCollection()->action("show_sensor_fov"); if (a) a->setEnabled(true); showFITSViewerB->setIcon( QIcon::fromTheme("kstars_fitsviewer")); showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Align::showFITSViewer); toggleFullScreenB->setIcon( QIcon::fromTheme("view-fullscreen")); toggleFullScreenB->setShortcut(Qt::Key_F4); toggleFullScreenB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(toggleFullScreenB, &QPushButton::clicked, this, &Ekos::Align::toggleAlignWidgetFullScreen); alignView = new AlignView(alignWidget, FITS_ALIGN); alignView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); alignView->setBaseSize(alignWidget->size()); alignView->createFloatingToolBar(); QVBoxLayout *vlayout = new QVBoxLayout(); vlayout->addWidget(alignView); alignWidget->setLayout(vlayout); connect(solveB, &QPushButton::clicked, this, &Ekos::Align::captureAndSolve); connect(stopB, &QPushButton::clicked, this, &Ekos::Align::abort); connect(measureAltB, &QPushButton::clicked, this, &Ekos::Align::measureAltError); connect(measureAzB, &QPushButton::clicked, this, &Ekos::Align::measureAzError); // Effective FOV Edit connect(FOVOut, &QLineEdit::editingFinished, this, &Align::syncFOV); connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Align::setDefaultCCD); connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Align::checkCCD); connect(correctAltB, &QPushButton::clicked, this, &Ekos::Align::correctAltError); connect(correctAzB, &QPushButton::clicked, this, &Ekos::Align::correctAzError); connect(loadSlewB, &QPushButton::clicked, [&]() { loadAndSlew(); }); FilterDevicesCombo->addItem("--"); connect(FilterDevicesCombo, static_cast(&QComboBox::activated), [ = ](const QString & text) { syncSettings(); Options::setDefaultAlignFW(text); }); connect(FilterDevicesCombo, static_cast(&QComboBox::activated), this, &Ekos::Align::checkFilter); connect(FilterPosCombo, static_cast(&QComboBox::activated), [ = ](int index) { syncSettings(); Options::setLockAlignFilterIndex(index); } ); connect(PAHSlewRateCombo, static_cast(&QComboBox::activated), [&](int index) { Options::setPAHMountSpeedIndex(index); }); gotoModeButtonGroup->setId(syncR, GOTO_SYNC); gotoModeButtonGroup->setId(slewR, GOTO_SLEW); gotoModeButtonGroup->setId(nothingR, GOTO_NOTHING); connect(gotoModeButtonGroup, static_cast(&QButtonGroup::buttonClicked), this, [ = ](int id) { this->currentGotoMode = static_cast(id); }); m_CaptureTimer.setSingleShot(true); m_CaptureTimer.setInterval(CAPTURE_RETRY_DELAY); connect(&m_CaptureTimer, &QTimer::timeout, [&]() { if (m_CaptureTimeoutCounter++ > 3) { appendLogText(i18n("Capture timed out.")); abort(); } else { ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (targetChip->isCapturing()) { appendLogText(i18n("Capturing still running, Retrying in %1 seconds...", m_CaptureTimer.interval() / 500)); targetChip->abortExposure(); m_CaptureTimer.start( m_CaptureTimer.interval() * 2); } else captureAndSolve(); } }); m_AlignTimer.setSingleShot(true); m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000); connect(&m_AlignTimer, &QTimer::timeout, this, &Ekos::Align::checkAlignmentTimeout); currentGotoMode = static_cast(Options::solverGotoOption()); gotoModeButtonGroup->button(currentGotoMode)->setChecked(true); editOptionsB->setIcon(QIcon::fromTheme("document-edit")); editOptionsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); KConfigDialog *dialog = new KConfigDialog(this, "alignsettings", Options::self()); #ifdef Q_OS_OSX dialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); #endif opsAlign = new OpsAlign(this); connect(opsAlign, &OpsAlign::settingsUpdated, this, &Ekos::Align::refreshAlignOptions); KPageWidgetItem *page = dialog->addPage(opsAlign, i18n("Astrometry.net")); page->setIcon(QIcon(":/icons/astrometry.svg")); opsAstrometry = new OpsAstrometry(this); page = dialog->addPage(opsAstrometry, i18n("Solver Options")); page->setIcon(QIcon::fromTheme("configure")); #ifndef Q_OS_WIN opsAstrometryCfg = new OpsAstrometryCfg(this); page = dialog->addPage(opsAstrometryCfg, i18n("Astrometry.cfg")); page->setIcon(QIcon::fromTheme("document-edit")); opsAstrometryIndexFiles = new OpsAstrometryIndexFiles(this); page = dialog->addPage(opsAstrometryIndexFiles, i18n("Index Files")); page->setIcon(QIcon::fromTheme("map-flat")); #endif opsASTAP = new OpsASTAP(this); page = dialog->addPage(opsASTAP, i18n("ASTAP")); page->setIcon(QIcon(":/icons/astap.ico")); connect(editOptionsB, &QPushButton::clicked, dialog, &QDialog::show); appendLogText(i18n("Idle.")); pi.reset(new QProgressIndicator(this)); stopLayout->addWidget(pi.get()); exposureIN->setValue(Options::alignExposure()); connect(exposureIN, static_cast(&QDoubleSpinBox::valueChanged), [&]() { syncSettings(); }); altStage = ALT_INIT; azStage = AZ_INIT; rememberSolverWCS = Options::astrometrySolverWCS(); rememberAutoWCS = Options::autoWCS(); solverBackendGroup->setId(astapSolverR, SOLVER_ASTAP); solverBackendGroup->setId(astrometrySolverR, SOLVER_ASTROMETRYNET); // JM 2019-11-10: solver type was 3 in previous version (online, offline, remote) // But they are now two choices (ASTAP and ASTROMETERY.NET) so we need to accommodate that. if (Options::solverBackend() > SOLVER_ASTROMETRYNET) { Options::setSolverBackend(SOLVER_ASTROMETRYNET); } solverBackendGroup->button(Options::solverBackend())->setChecked(true); connect(solverBackendGroup, static_cast(&QButtonGroup::buttonClicked), this, &Align::setSolverBackend); astrometryTypeCombo->addItem(i18n("Online")); #ifndef Q_OS_WIN astrometryTypeCombo->addItem(i18n("Offline")); #endif astrometryTypeCombo->addItem(i18n("Remote")); astrometryTypeCombo->setCurrentIndex(Options::astrometrySolverType()); connect(astrometryTypeCombo, static_cast(&QComboBox::activated), this, &Ekos::Align::setAstrometrySolverType); setSolverBackend(solverBackendGroup->checkedId()); // Which telescope info to use for FOV calculations FOVScopeCombo->setCurrentIndex(Options::solverScopeType()); connect(FOVScopeCombo, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Align::updateTelescopeType); accuracySpin->setValue(Options::solverAccuracyThreshold()); alignDarkFrameCheck->setChecked(Options::alignDarkFrame()); delaySpin->setValue(Options::settlingTime()); connect(delaySpin, &QSpinBox::editingFinished, this, &Ekos::Align::saveSettleTime); connect(binningCombo, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Align::setBinningIndex); // PAH Connections connect(this, &Align::PAHEnabled, [&](bool enabled) { PAHStartB->setEnabled(enabled); directionLabel->setEnabled(enabled); PAHDirectionCombo->setEnabled(enabled); PAHRotationSpin->setEnabled(enabled); PAHSlewRateCombo->setEnabled(enabled); PAHManual->setEnabled(enabled); }); connect(PAHStartB, &QPushButton::clicked, this, &Ekos::Align::startPAHProcess); // PAH StopB is just a shortcut for the regular stop connect(PAHStopB, &QPushButton::clicked, this, &Align::stopPAHProcess); connect(PAHCorrectionsNextB, &QPushButton::clicked, this, &Ekos::Align::setPAHCorrectionSelectionComplete); connect(PAHRefreshB, &QPushButton::clicked, this, &Ekos::Align::startPAHRefreshProcess); connect(PAHDoneB, &QPushButton::clicked, this, &Ekos::Align::setPAHRefreshComplete); // done buttons for manual slewing during polar alignment: connect(PAHfirstDone, &QPushButton::clicked, this, &Ekos::Align::setPAHSlewDone); connect(PAHsecondDone, &QPushButton::clicked, this, &Ekos::Align::setPAHSlewDone); if (solverOptions->text().contains("no-fits2fits")) appendLogText(i18n( "Warning: If using astrometry.net v0.68 or above, remove the --no-fits2fits from the astrometry options.")); hemisphere = KStarsData::Instance()->geo()->lat()->Degrees() > 0 ? NORTH_HEMISPHERE : SOUTH_HEMISPHERE; double accuracyRadius = accuracySpin->value(); alignPlot->setBackground(QBrush(Qt::black)); alignPlot->setSelectionTolerance(10); alignPlot->xAxis->setBasePen(QPen(Qt::white, 1)); alignPlot->yAxis->setBasePen(QPen(Qt::white, 1)); alignPlot->xAxis->setTickPen(QPen(Qt::white, 1)); alignPlot->yAxis->setTickPen(QPen(Qt::white, 1)); alignPlot->xAxis->setSubTickPen(QPen(Qt::white, 1)); alignPlot->yAxis->setSubTickPen(QPen(Qt::white, 1)); alignPlot->xAxis->setTickLabelColor(Qt::white); alignPlot->yAxis->setTickLabelColor(Qt::white); alignPlot->xAxis->setLabelColor(Qt::white); alignPlot->yAxis->setLabelColor(Qt::white); alignPlot->xAxis->setLabelFont(QFont(font().family(), 10)); alignPlot->yAxis->setLabelFont(QFont(font().family(), 10)); alignPlot->xAxis->setLabelPadding(2); alignPlot->yAxis->setLabelPadding(2); alignPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); alignPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); alignPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); alignPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); alignPlot->xAxis->grid()->setZeroLinePen(QPen(Qt::yellow)); alignPlot->yAxis->grid()->setZeroLinePen(QPen(Qt::yellow)); alignPlot->xAxis->setLabel(i18n("dRA (arcsec)")); alignPlot->yAxis->setLabel(i18n("dDE (arcsec)")); alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); alignPlot->setInteractions(QCP::iRangeZoom); alignPlot->setInteraction(QCP::iRangeDrag, true); alignPlot->addGraph(); alignPlot->graph(0)->setLineStyle(QCPGraph::lsNone); alignPlot->graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, Qt::white, 15)); buildTarget(); connect(alignPlot, &QCustomPlot::mouseMove, this, &Ekos::Align::handlePointTooltip); connect(rightLayout, &QSplitter::splitterMoved, this, &Ekos::Align::handleVerticalPlotSizeChange); connect(alignSplitter, &QSplitter::splitterMoved, this, &Ekos::Align::handleHorizontalPlotSizeChange); connect(accuracySpin, static_cast(&QSpinBox::valueChanged), this, &Ekos::Align::buildTarget); alignPlot->resize(190, 190); alignPlot->replot(); solutionTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); clearAllSolutionsB->setIcon( QIcon::fromTheme("application-exit")); clearAllSolutionsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); removeSolutionB->setIcon(QIcon::fromTheme("list-remove")); removeSolutionB->setAttribute(Qt::WA_LayoutUsesWidgetRect); exportSolutionsCSV->setIcon( QIcon::fromTheme("document-save-as")); exportSolutionsCSV->setAttribute(Qt::WA_LayoutUsesWidgetRect); autoScaleGraphB->setIcon(QIcon::fromTheme("zoom-fit-best")); autoScaleGraphB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.setupUi(&mountModelDialog); mountModelDialog.setWindowTitle("Mount Model Tool"); mountModelDialog.setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); mountModel.alignTable->setColumnWidth(0, 70); mountModel.alignTable->setColumnWidth(1, 75); mountModel.alignTable->setColumnWidth(2, 130); mountModel.alignTable->setColumnWidth(3, 30); mountModel.wizardAlignB->setIcon( QIcon::fromTheme("tools-wizard")); mountModel.wizardAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.clearAllAlignB->setIcon( QIcon::fromTheme("application-exit")); mountModel.clearAllAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.removeAlignB->setIcon(QIcon::fromTheme("list-remove")); mountModel.removeAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.addAlignB->setIcon(QIcon::fromTheme("list-add")); mountModel.addAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.findAlignB->setIcon(QIcon::fromTheme("edit-find")); mountModel.findAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.alignTable->verticalHeader()->setDragDropOverwriteMode(false); mountModel.alignTable->verticalHeader()->setSectionsMovable(true); mountModel.alignTable->verticalHeader()->setDragEnabled(true); mountModel.alignTable->verticalHeader()->setDragDropMode(QAbstractItemView::InternalMove); connect(mountModel.alignTable->verticalHeader(), SIGNAL(sectionMoved(int, int, int)), this, SLOT(moveAlignPoint(int, int, int))); mountModel.loadAlignB->setIcon( QIcon::fromTheme("document-open")); mountModel.loadAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.saveAsAlignB->setIcon( QIcon::fromTheme("document-save-as")); mountModel.saveAsAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.saveAlignB->setIcon( QIcon::fromTheme("document-save")); mountModel.saveAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.previewB->setIcon(QIcon::fromTheme("kstars_grid")); mountModel.previewB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.previewB->setCheckable(true); mountModel.sortAlignB->setIcon(QIcon::fromTheme("svn-update")); mountModel.sortAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.stopAlignB->setIcon( QIcon::fromTheme("media-playback-stop")); mountModel.stopAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.startAlignB->setIcon( QIcon::fromTheme("media-playback-start")); mountModel.startAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(clearAllSolutionsB, &QPushButton::clicked, this, &Ekos::Align::slotClearAllSolutionPoints); connect(removeSolutionB, &QPushButton::clicked, this, &Ekos::Align::slotRemoveSolutionPoint); connect(exportSolutionsCSV, &QPushButton::clicked, this, &Ekos::Align::exportSolutionPoints); connect(autoScaleGraphB, &QPushButton::clicked, this, &Ekos::Align::slotAutoScaleGraph); connect(mountModelB, &QPushButton::clicked, this, &Ekos::Align::slotMountModel); connect(solutionTable, &QTableWidget::cellClicked, this, &Ekos::Align::selectSolutionTableRow); connect(mountModel.wizardAlignB, &QPushButton::clicked, this, &Ekos::Align::slotWizardAlignmentPoints); connect(mountModel.alignTypeBox, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Align::alignTypeChanged); connect(mountModel.starListBox, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Align::slotStarSelected); connect(mountModel.greekStarListBox, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Align::slotStarSelected); connect(mountModel.loadAlignB, &QPushButton::clicked, this, &Ekos::Align::slotLoadAlignmentPoints); connect(mountModel.saveAsAlignB, &QPushButton::clicked, this, &Ekos::Align::slotSaveAsAlignmentPoints); connect(mountModel.saveAlignB, &QPushButton::clicked, this, &Ekos::Align::slotSaveAlignmentPoints); connect(mountModel.clearAllAlignB, &QPushButton::clicked, this, &Ekos::Align::slotClearAllAlignPoints); connect(mountModel.removeAlignB, &QPushButton::clicked, this, &Ekos::Align::slotRemoveAlignPoint); connect(mountModel.addAlignB, &QPushButton::clicked, this, &Ekos::Align::slotAddAlignPoint); connect(mountModel.findAlignB, &QPushButton::clicked, this, &Ekos::Align::slotFindAlignObject); connect(mountModel.sortAlignB, &QPushButton::clicked, this, &Ekos::Align::slotSortAlignmentPoints); connect(mountModel.previewB, &QPushButton::clicked, this, &Ekos::Align::togglePreviewAlignPoints); connect(mountModel.stopAlignB, &QPushButton::clicked, this, &Ekos::Align::resetAlignmentProcedure); connect(mountModel.startAlignB, &QPushButton::clicked, this, &Ekos::Align::startStopAlignmentProcedure); //Note: This is to prevent a button from being called the default button //and then executing when the user hits the enter key such as when on a Text Box QList qButtons = findChildren(); for (auto &button : qButtons) button->setAutoDefault(false); } Align::~Align() { if (alignWidget->parent() == nullptr) toggleAlignWidgetFullScreen(); // Remove temporary FITS files left before by the solver QDir dir(QDir::tempPath()); dir.setNameFilters(QStringList() << "fits*" << "tmp.*"); dir.setFilter(QDir::Files); for (auto &dirFile : dir.entryList()) dir.remove(dirFile); } void Align::selectSolutionTableRow(int row, int column) { Q_UNUSED(column) solutionTable->selectRow(row); for (int i = 0; i < alignPlot->itemCount(); i++) { QCPAbstractItem *abstractItem = alignPlot->item(i); if (abstractItem) { QCPItemText *item = qobject_cast(abstractItem); if (item) { if (i == row) { item->setColor(Qt::black); item->setBrush(Qt::yellow); } else { item->setColor(Qt::red); item->setBrush(Qt::white); } } } } alignPlot->replot(); } void Align::handleHorizontalPlotSizeChange() { alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0); alignPlot->replot(); } void Align::handleVerticalPlotSizeChange() { alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0); alignPlot->replot(); } void Align::resizeEvent(QResizeEvent *event) { if (event->oldSize().width() != -1) { if (event->oldSize().width() != size().width()) handleHorizontalPlotSizeChange(); else if (event->oldSize().height() != size().height()) handleVerticalPlotSizeChange(); } else { QTimer::singleShot(10, this, &Ekos::Align::handleHorizontalPlotSizeChange); } } void Align::handlePointTooltip(QMouseEvent *event) { QCPAbstractItem *item = alignPlot->itemAt(event->localPos()); if (item) { QCPItemText *label = qobject_cast(item); if (label) { QString labelText = label->text(); int point = labelText.toInt() - 1; if (point < 0) return; QToolTip::showText(event->globalPos(), tr("" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "
Object %L1: %L2
RA:%L3
DE:%L4
dRA:%L5
dDE:%L6
") .arg(point + 1) .arg(solutionTable->item(point, 2)->text(), solutionTable->item(point, 0)->text(), solutionTable->item(point, 1)->text(), solutionTable->item(point, 4)->text(), solutionTable->item(point, 5)->text()), alignPlot, alignPlot->rect()); } } } void Align::buildTarget() { double accuracyRadius = accuracySpin->value(); if (centralTarget) { concentricRings->data()->clear(); redTarget->data()->clear(); yellowTarget->data()->clear(); centralTarget->data()->clear(); } else { concentricRings = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis); redTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis); yellowTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis); centralTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis); } const int pointCount = 200; QVector circleRings( pointCount * (5)); //Have to multiply by the number of rings, Rings at : 25%, 50%, 75%, 125%, 175% QVector circleCentral(pointCount); QVector circleYellow(pointCount); QVector circleRed(pointCount); int circleRingPt = 0; for (int i = 0; i < pointCount; i++) { double theta = i / static_cast(pointCount) * 2 * M_PI; for (double ring = 1; ring < 8; ring++) { if (ring != 4 && ring != 6) { if (i % (9 - static_cast(ring)) == 0) //This causes fewer points to draw on the inner circles. { circleRings[circleRingPt] = QCPCurveData(circleRingPt, accuracyRadius * ring * 0.25 * qCos(theta), accuracyRadius * ring * 0.25 * qSin(theta)); circleRingPt++; } } } circleCentral[i] = QCPCurveData(i, accuracyRadius * qCos(theta), accuracyRadius * qSin(theta)); circleYellow[i] = QCPCurveData(i, accuracyRadius * 1.5 * qCos(theta), accuracyRadius * 1.5 * qSin(theta)); circleRed[i] = QCPCurveData(i, accuracyRadius * 2 * qCos(theta), accuracyRadius * 2 * qSin(theta)); } concentricRings->setLineStyle(QCPCurve::lsNone); concentricRings->setScatterSkip(0); concentricRings->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, QColor(255, 255, 255, 150), 1)); concentricRings->data()->set(circleRings, true); redTarget->data()->set(circleRed, true); yellowTarget->data()->set(circleYellow, true); centralTarget->data()->set(circleCentral, true); concentricRings->setPen(QPen(Qt::white)); redTarget->setPen(QPen(Qt::red)); yellowTarget->setPen(QPen(Qt::yellow)); centralTarget->setPen(QPen(Qt::green)); concentricRings->setBrush(Qt::NoBrush); redTarget->setBrush(QBrush(QColor(255, 0, 0, 50))); yellowTarget->setBrush( QBrush(QColor(0, 255, 0, 50))); //Note this is actually yellow. It is green on top of red with equal opacity. centralTarget->setBrush(QBrush(QColor(0, 255, 0, 50))); if (alignPlot->size().width() > 0) alignPlot->replot(); } void Align::slotAutoScaleGraph() { double accuracyRadius = accuracySpin->value(); alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0); alignPlot->replot(); } void Align::slotWizardAlignmentPoints() { int points = mountModel.alignPtNum->value(); if (points < 2) //The minimum is 2 because the wizard calculations require the calculation of an angle between points. return; //It should not be less than 2 because the minimum in the spin box is 2. int minAlt = mountModel.minAltBox->value(); KStarsData *data = KStarsData::Instance(); GeoLocation *geo = data->geo(); double lat = geo->lat()->Degrees(); if (mountModel.alignTypeBox->currentIndex() == OBJECT_FIXED_DEC) { double decAngle = mountModel.alignDec->value(); //Dec that never rises. if (lat > 0) { if (decAngle < lat - 90 + minAlt) //Min altitude possible at minAlt deg above horizon { KSNotification::sorry(i18n("DEC is below the altitude limit")); return; } } else { if (decAngle > lat + 90 - minAlt) //Max altitude possible at minAlt deg above horizon { KSNotification::sorry(i18n("DEC is below the altitude limit")); return; } } } //If there are less than 6 points, keep them all in the same DEC, //any more, set the num per row to be the sqrt of the points to evenly distribute in RA and DEC int numRAperDEC = 5; if (points > 5) numRAperDEC = qSqrt(points); //These calculations rely on modulus and int division counting beginning at 0, but the #s start at 1. int decPoints = (points - 1) / numRAperDEC + 1; int lastSetRAPoints = (points - 1) % numRAperDEC + 1; double decIncrement = -1; double initDEC = -1; SkyPoint spTest; if (mountModel.alignTypeBox->currentIndex() == OBJECT_FIXED_DEC) { decPoints = 1; initDEC = mountModel.alignDec->value(); decIncrement = 0; } else if (decPoints == 1) { decIncrement = 0; spTest.setAlt( minAlt); //The goal here is to get the point exactly West at the minAlt so that we can use that DEC spTest.setAz(270); spTest.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); initDEC = spTest.dec().Degrees(); } else { spTest.setAlt( minAlt + 10); //We don't want to be right at the minAlt because there would be only 1 point on the dec circle above the alt. spTest.setAz(180); spTest.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); initDEC = spTest.dec().Degrees(); if (lat > 0) decIncrement = (80 - initDEC) / (decPoints); //Don't quite want to reach NCP else decIncrement = (initDEC - 80) / (decPoints); //Don't quite want to reach SCP } for (int d = 0; d < decPoints; d++) { double initRA = -1; double raPoints = -1; double raIncrement = -1; double dec; if (lat > 0) dec = initDEC + d * decIncrement; else dec = initDEC - d * decIncrement; if (mountModel.alignTypeBox->currentIndex() == OBJECT_FIXED_DEC) { raPoints = points; } else if (d == decPoints - 1) { raPoints = lastSetRAPoints; } else { raPoints = numRAperDEC; } //This computes both the initRA and the raIncrement. calculateAngleForRALine(raIncrement, initRA, dec, lat, raPoints, minAlt); if (raIncrement == -1 || decIncrement == -1) { KSNotification::sorry(i18n("Point calculation error.")); return; } for (int i = 0; i < raPoints; i++) { double ra = initRA + i * raIncrement; const SkyObject *original = getWizardAlignObject(ra, dec); QString ra_report, dec_report, name; if (original) { SkyObject *o = original->clone(); o->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false); getFormattedCoords(o->ra0().Hours(), o->dec0().Degrees(), ra_report, dec_report); name = o->longname(); } else { getFormattedCoords(dms(ra).Hours(), dec, ra_report, dec_report); name = i18n("Sky Point"); } int currentRow = mountModel.alignTable->rowCount(); mountModel.alignTable->insertRow(currentRow); QTableWidgetItem *RAReport = new QTableWidgetItem(); RAReport->setText(ra_report); RAReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 0, RAReport); QTableWidgetItem *DECReport = new QTableWidgetItem(); DECReport->setText(dec_report); DECReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 1, DECReport); QTableWidgetItem *ObjNameReport = new QTableWidgetItem(); ObjNameReport->setText(name); ObjNameReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 2, ObjNameReport); QTableWidgetItem *disabledBox = new QTableWidgetItem(); disabledBox->setFlags(Qt::ItemIsSelectable); mountModel.alignTable->setItem(currentRow, 3, disabledBox); } } if (previewShowing) updatePreviewAlignPoints(); } void Align::calculateAngleForRALine(double &raIncrement, double &initRA, double initDEC, double lat, double raPoints, double minAlt) { SkyPoint spEast; SkyPoint spWest; //Circumpolar dec if (fabs(initDEC) > (90 - fabs(lat) + minAlt)) { if (raPoints > 1) raIncrement = 360 / (raPoints - 1); else raIncrement = 0; initRA = 0; } else { dms AZEast, AZWest; calculateAZPointsForDEC(dms(initDEC), dms(minAlt), AZEast, AZWest); spEast.setAlt(minAlt); spEast.setAz(AZEast.Degrees()); spEast.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); spWest.setAlt(minAlt); spWest.setAz(AZWest.Degrees()); spWest.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); dms angleSep = spEast.ra().deltaAngle(spWest.ra()); initRA = spWest.ra().Degrees(); if (raPoints > 1) raIncrement = fabs(angleSep.Degrees() / (raPoints - 1)); else raIncrement = 0; } } void Align::calculateAZPointsForDEC(dms dec, dms alt, dms &AZEast, dms &AZWest) { KStarsData *data = KStarsData::Instance(); GeoLocation *geo = data->geo(); double AZRad; double sindec, cosdec, sinlat, coslat; double sinAlt, cosAlt; geo->lat()->SinCos(sinlat, coslat); dec.SinCos(sindec, cosdec); alt.SinCos(sinAlt, cosAlt); double arg = (sindec - sinlat * sinAlt) / (coslat * cosAlt); AZRad = acos(arg); AZEast.setRadians(AZRad); AZWest.setRadians(2.0 * dms::PI - AZRad); } const SkyObject *Align::getWizardAlignObject(double ra, double dec) { double maxSearch = 5.0; switch (mountModel.alignTypeBox->currentIndex()) { case OBJECT_ANY_OBJECT: return KStarsData::Instance()->skyComposite()->objectNearest(new SkyPoint(dms(ra), dms(dec)), maxSearch); case OBJECT_FIXED_DEC: case OBJECT_FIXED_GRID: return nullptr; case OBJECT_ANY_STAR: return KStarsData::Instance()->skyComposite()->starNearest(new SkyPoint(dms(ra), dms(dec)), maxSearch); } //If they want named stars, then try to search for and return the closest Align Star to the requested location dms bestDiff = dms(360); double index = -1; for (int i = 0; i < alignStars.size(); i++) { const StarObject *star = alignStars.value(i); if (star) { if (star->hasName()) { SkyPoint thisPt(ra / 15.0, dec); dms thisDiff = thisPt.angularDistanceTo(star); if (thisDiff.Degrees() < bestDiff.Degrees()) { index = i; bestDiff = thisDiff; } } } } if (index == -1) return KStarsData::Instance()->skyComposite()->starNearest(new SkyPoint(dms(ra), dms(dec)), maxSearch); return alignStars.value(index); } void Align::alignTypeChanged(int alignType) { if (alignType == OBJECT_FIXED_DEC) mountModel.alignDec->setEnabled(true); else mountModel.alignDec->setEnabled(false); } void Align::slotStarSelected(const QString selectedStar) { for (int i = 0; i < alignStars.size(); i++) { const StarObject *star = alignStars.value(i); if (star) { if (star->name() == selectedStar || star->gname().simplified() == selectedStar) { int currentRow = mountModel.alignTable->rowCount(); mountModel.alignTable->insertRow(currentRow); QString ra_report, dec_report; getFormattedCoords(star->ra0().Hours(), star->dec0().Degrees(), ra_report, dec_report); QTableWidgetItem *RAReport = new QTableWidgetItem(); RAReport->setText(ra_report); RAReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 0, RAReport); QTableWidgetItem *DECReport = new QTableWidgetItem(); DECReport->setText(dec_report); DECReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 1, DECReport); QTableWidgetItem *ObjNameReport = new QTableWidgetItem(); ObjNameReport->setText(star->longname()); ObjNameReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 2, ObjNameReport); QTableWidgetItem *disabledBox = new QTableWidgetItem(); disabledBox->setFlags(Qt::ItemIsSelectable); mountModel.alignTable->setItem(currentRow, 3, disabledBox); mountModel.starListBox->setCurrentIndex(0); mountModel.greekStarListBox->setCurrentIndex(0); return; } } } if (previewShowing) updatePreviewAlignPoints(); } void Align::generateAlignStarList() { alignStars.clear(); mountModel.starListBox->clear(); mountModel.greekStarListBox->clear(); KStarsData *data = KStarsData::Instance(); QVector> listStars; listStars.append(data->skyComposite()->objectLists(SkyObject::STAR)); for (int i = 0; i < listStars.size(); i++) { QPair pair = listStars.value(i); const StarObject *star = dynamic_cast(pair.second); if (star) { StarObject *alignStar = star->clone(); alignStar->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false); alignStars.append(alignStar); } } QStringList boxNames; QStringList greekBoxNames; for (int i = 0; i < alignStars.size(); i++) { const StarObject *star = alignStars.value(i); if (star) { if (!isVisible(star)) { alignStars.remove(i); i--; } else { if (star->hasLatinName()) boxNames << star->name(); else { if (!star->gname().isEmpty()) greekBoxNames << star->gname().simplified(); } } } } boxNames.sort(Qt::CaseInsensitive); boxNames.removeDuplicates(); greekBoxNames.removeDuplicates(); - qSort(greekBoxNames.begin(), greekBoxNames.end(), [](const QString & a, const QString & b) + std::sort(greekBoxNames.begin(), greekBoxNames.end(), [](const QString & a, const QString & b) { QStringList aParts = a.split(' '); QStringList bParts = b.split(' '); if (aParts.length() < 2 || bParts.length() < 2) return a < b; //This should not happen, they should all have 2 words in the string. if (aParts[1] == bParts[1]) { return aParts[0] < bParts[0]; //This compares the greek letter when the constellation is the same } else return aParts[1] < bParts[1]; //This compares the constellation names }); mountModel.starListBox->addItem("Select one:"); mountModel.greekStarListBox->addItem("Select one:"); for (int i = 0; i < boxNames.size(); i++) mountModel.starListBox->addItem(boxNames.at(i)); for (int i = 0; i < greekBoxNames.size(); i++) mountModel.greekStarListBox->addItem(greekBoxNames.at(i)); } bool Align::isVisible(const SkyObject *so) { return (getAltitude(so) > 30); } double Align::getAltitude(const SkyObject *so) { KStarsData *data = KStarsData::Instance(); SkyPoint sp = so->recomputeCoords(data->ut(), data->geo()); //check altitude of object at this time. sp.EquatorialToHorizontal(data->lst(), data->geo()->lat()); return sp.alt().Degrees(); } void Align::togglePreviewAlignPoints() { previewShowing = !previewShowing; mountModel.previewB->setChecked(previewShowing); updatePreviewAlignPoints(); } void Align::updatePreviewAlignPoints() { FlagComponent *flags = KStarsData::Instance()->skyComposite()->flags(); for (int i = 0; i < flags->size(); i++) { if (flags->label(i).startsWith(QLatin1String("Align"))) { flags->remove(i); i--; } } if (previewShowing) { for (int i = 0; i < mountModel.alignTable->rowCount(); i++) { QTableWidgetItem *raCell = mountModel.alignTable->item(i, 0); QTableWidgetItem *deCell = mountModel.alignTable->item(i, 1); QTableWidgetItem *objNameCell = mountModel.alignTable->item(i, 2); if (raCell && deCell && objNameCell) { QString raString = raCell->text(); QString deString = deCell->text(); dms raDMS = dms::fromString(raString, false); dms decDMS = dms::fromString(deString, true); QString objString = objNameCell->text(); SkyPoint flagPoint(raDMS, decDMS); flags->add(flagPoint, "J2000", "Default", "Align " + QString::number(i + 1) + ' ' + objString, "white"); } } } KStars::Instance()->map()->forceUpdate(true); } void Align::slotLoadAlignmentPoints() { QUrl fileURL = QFileDialog::getOpenFileUrl(&mountModelDialog, i18n("Open Ekos Alignment List"), alignURLPath, "Ekos AlignmentList (*.eal)"); if (fileURL.isEmpty()) return; if (fileURL.isValid() == false) { QString message = i18n("Invalid URL: %1", fileURL.toLocalFile()); KSNotification::sorry(message, i18n("Invalid URL")); return; } alignURLPath = QUrl(fileURL.url(QUrl::RemoveFilename)); loadAlignmentPoints(fileURL.toLocalFile()); if (previewShowing) updatePreviewAlignPoints(); } bool Align::loadAlignmentPoints(const QString &fileURL) { QFile sFile; sFile.setFileName(fileURL); if (!sFile.open(QIODevice::ReadOnly)) { QString message = i18n("Unable to open file %1", fileURL); KSNotification::sorry(message, i18n("Could Not Open File")); return false; } mountModel.alignTable->setRowCount(0); LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; char c; while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) { double sqVersion = atof(findXMLAttValu(root, "version")); if (sqVersion < AL_FORMAT_VERSION) { appendLogText(i18n("Deprecated sequence file format version %1. Please construct a new sequence file.", sqVersion)); return false; } XMLEle *ep = nullptr; XMLEle *subEP = nullptr; int currentRow = 0; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "AlignmentPoint")) { mountModel.alignTable->insertRow(currentRow); subEP = findXMLEle(ep, "RA"); if (subEP) { QTableWidgetItem *RAReport = new QTableWidgetItem(); RAReport->setText(pcdataXMLEle(subEP)); RAReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 0, RAReport); } else return false; subEP = findXMLEle(ep, "DE"); if (subEP) { QTableWidgetItem *DEReport = new QTableWidgetItem(); DEReport->setText(pcdataXMLEle(subEP)); DEReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 1, DEReport); } else return false; subEP = findXMLEle(ep, "NAME"); if (subEP) { QTableWidgetItem *ObjReport = new QTableWidgetItem(); ObjReport->setText(pcdataXMLEle(subEP)); ObjReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 2, ObjReport); } else return false; } currentRow++; } return true; } } return false; } void Align::slotSaveAsAlignmentPoints() { alignURL.clear(); slotSaveAlignmentPoints(); } void Align::slotSaveAlignmentPoints() { QUrl backupCurrent = alignURL; if (alignURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || alignURL.toLocalFile().contains("/Temp")) alignURL.clear(); if (alignURL.isEmpty()) { alignURL = QFileDialog::getSaveFileUrl(&mountModelDialog, i18n("Save Ekos Alignment List"), alignURLPath, "Ekos Alignment List (*.eal)"); // if user presses cancel if (alignURL.isEmpty()) { alignURL = backupCurrent; return; } alignURLPath = QUrl(alignURL.url(QUrl::RemoveFilename)); if (alignURL.toLocalFile().endsWith(QLatin1String(".eal")) == false) alignURL.setPath(alignURL.toLocalFile() + ".eal"); if (QFile::exists(alignURL.toLocalFile())) { int r = KMessageBox::warningContinueCancel(nullptr, i18n("A file named \"%1\" already exists. " "Overwrite it?", alignURL.fileName()), i18n("Overwrite File?"), KStandardGuiItem::overwrite()); if (r == KMessageBox::Cancel) return; } } if (alignURL.isValid()) { if ((saveAlignmentPoints(alignURL.toLocalFile())) == false) { KSNotification::error(i18n("Failed to save alignment list"), i18n("Save")); return; } } else { QString message = i18n("Invalid URL: %1", alignURL.url()); KSNotification::sorry(message, i18n("Invalid URL")); } } bool Align::saveAlignmentPoints(const QString &path) { QFile file; file.setFileName(path); if (!file.open(QIODevice::WriteOnly)) { QString message = i18n("Unable to write to file %1", path); KSNotification::sorry(message, i18n("Could Not Open File")); return false; } QTextStream outstream(&file); outstream << "" << endl; outstream << "" << endl; for (int i = 0; i < mountModel.alignTable->rowCount(); i++) { QTableWidgetItem *raCell = mountModel.alignTable->item(i, 0); QTableWidgetItem *deCell = mountModel.alignTable->item(i, 1); QTableWidgetItem *objNameCell = mountModel.alignTable->item(i, 2); if (!raCell || !deCell || !objNameCell) return false; QString raString = raCell->text(); QString deString = deCell->text(); QString objString = objNameCell->text(); outstream << "" << endl; outstream << "" << raString << "" << endl; outstream << "" << deString << "" << endl; outstream << "" << objString << "" << endl; outstream << "" << endl; } outstream << "" << endl; appendLogText(i18n("Alignment List saved to %1", path)); file.close(); return true; } void Align::slotSortAlignmentPoints() { int firstAlignmentPt = findClosestAlignmentPointToTelescope(); if (firstAlignmentPt != -1) { swapAlignPoints(firstAlignmentPt, 0); } for (int i = 0; i < mountModel.alignTable->rowCount() - 1; i++) { int nextAlignmentPoint = findNextAlignmentPointAfter(i); if (nextAlignmentPoint != -1) { swapAlignPoints(nextAlignmentPoint, i + 1); } } if (previewShowing) updatePreviewAlignPoints(); } int Align::findClosestAlignmentPointToTelescope() { dms bestDiff = dms(360); double index = -1; for (int i = 0; i < mountModel.alignTable->rowCount(); i++) { QTableWidgetItem *raCell = mountModel.alignTable->item(i, 0); QTableWidgetItem *deCell = mountModel.alignTable->item(i, 1); if (raCell && deCell) { dms raDMS = dms::fromString(raCell->text(), false); dms deDMS = dms::fromString(deCell->text(), true); dms thisDiff = telescopeCoord.angularDistanceTo(new SkyPoint(raDMS, deDMS)); if (thisDiff.Degrees() < bestDiff.Degrees()) { index = i; bestDiff = thisDiff; } } } return index; } int Align::findNextAlignmentPointAfter(int currentSpot) { QTableWidgetItem *currentRACell = mountModel.alignTable->item(currentSpot, 0); QTableWidgetItem *currentDECell = mountModel.alignTable->item(currentSpot, 1); if (currentRACell && currentDECell) { dms thisRADMS = dms::fromString(currentRACell->text(), false); dms thisDEDMS = dms::fromString(currentDECell->text(), true); SkyPoint thisPt(thisRADMS, thisDEDMS); dms bestDiff = dms(360); double index = -1; for (int i = currentSpot + 1; i < mountModel.alignTable->rowCount(); i++) { QTableWidgetItem *raCell = mountModel.alignTable->item(i, 0); QTableWidgetItem *deCell = mountModel.alignTable->item(i, 1); if (raCell && deCell) { dms raDMS = dms::fromString(raCell->text(), false); dms deDMS = dms::fromString(deCell->text(), true); SkyPoint point(raDMS, deDMS); dms thisDiff = thisPt.angularDistanceTo(&point); if (thisDiff.Degrees() < bestDiff.Degrees()) { index = i; bestDiff = thisDiff; } } } return index; } else return -1; } void Align::exportSolutionPoints() { if (solutionTable->rowCount() == 0) return; QUrl exportFile = QFileDialog::getSaveFileUrl(KStars::Instance(), i18n("Export Solution Points"), alignURLPath, "CSV File (*.csv)"); if (exportFile.isEmpty()) // if user presses cancel return; if (exportFile.toLocalFile().endsWith(QLatin1String(".csv")) == false) exportFile.setPath(exportFile.toLocalFile() + ".csv"); QString path = exportFile.toLocalFile(); if (QFile::exists(path)) { int r = KMessageBox::warningContinueCancel(nullptr, i18n("A file named \"%1\" already exists. " "Overwrite it?", exportFile.fileName()), i18n("Overwrite File?"), KStandardGuiItem::overwrite()); if (r == KMessageBox::Cancel) return; } if (!exportFile.isValid()) { QString message = i18n("Invalid URL: %1", exportFile.url()); KSNotification::sorry(message, i18n("Invalid URL")); return; } QFile file; file.setFileName(path); if (!file.open(QIODevice::WriteOnly)) { QString message = i18n("Unable to write to file %1", path); KSNotification::sorry(message, i18n("Could Not Open File")); return; } QTextStream outstream(&file); QString epoch = QString::number(KStarsDateTime::currentDateTime().epoch()); outstream << "RA (J" << epoch << "),DE (J" << epoch << "),RA (degrees),DE (degrees),Name,RA Error (arcsec),DE Error (arcsec)" << endl; for (int i = 0; i < solutionTable->rowCount(); i++) { QTableWidgetItem *raCell = solutionTable->item(i, 0); QTableWidgetItem *deCell = solutionTable->item(i, 1); QTableWidgetItem *objNameCell = solutionTable->item(i, 2); QTableWidgetItem *raErrorCell = solutionTable->item(i, 4); QTableWidgetItem *deErrorCell = solutionTable->item(i, 5); if (!raCell || !deCell || !objNameCell || !raErrorCell || !deErrorCell) { KSNotification::sorry(i18n("Error in table structure.")); return; } dms raDMS = dms::fromString(raCell->text(), false); dms deDMS = dms::fromString(deCell->text(), true); outstream << raDMS.toHMSString() << ',' << deDMS.toDMSString() << ',' << raDMS.Degrees() << ',' << deDMS.Degrees() << ',' << objNameCell->text() << ',' << raErrorCell->text().remove('\"') << ',' << deErrorCell->text().remove('\"') << endl; } appendLogText(i18n("Solution Points Saved as: %1", path)); file.close(); } void Align::slotClearAllSolutionPoints() { if (solutionTable->rowCount() == 0) return; connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); solutionTable->setRowCount(0); alignPlot->graph(0)->data()->clear(); alignPlot->clearItems(); buildTarget(); slotAutoScaleGraph(); }); KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to clear all of the solution points?"), i18n("Clear Solution Points"), 60); } void Align::slotClearAllAlignPoints() { if (mountModel.alignTable->rowCount() == 0) return; if (KMessageBox::questionYesNo(&mountModelDialog, i18n("Are you sure you want to clear all the alignment points?"), i18n("Clear Align Points")) == KMessageBox::Yes) mountModel.alignTable->setRowCount(0); if (previewShowing) updatePreviewAlignPoints(); } void Align::slotRemoveSolutionPoint() { QCPAbstractItem *abstractItem = alignPlot->item(solutionTable->currentRow()); if (abstractItem) { QCPItemText *item = qobject_cast(abstractItem); if (item) { double point = item->position->key(); alignPlot->graph(0)->data()->remove(point); } } alignPlot->removeItem(solutionTable->currentRow()); for (int i = 0; i < alignPlot->itemCount(); i++) { QCPAbstractItem *abstractItem = alignPlot->item(i); if (abstractItem) { QCPItemText *item = qobject_cast(abstractItem); if (item) item->setText(QString::number(i + 1)); } } solutionTable->removeRow(solutionTable->currentRow()); alignPlot->replot(); } void Align::slotRemoveAlignPoint() { mountModel.alignTable->removeRow(mountModel.alignTable->currentRow()); if (previewShowing) updatePreviewAlignPoints(); } void Align::moveAlignPoint(int logicalIndex, int oldVisualIndex, int newVisualIndex) { Q_UNUSED(logicalIndex) for (int i = 0; i < mountModel.alignTable->columnCount(); i++) { QTableWidgetItem *oldItem = mountModel.alignTable->takeItem(oldVisualIndex, i); QTableWidgetItem *newItem = mountModel.alignTable->takeItem(newVisualIndex, i); mountModel.alignTable->setItem(newVisualIndex, i, oldItem); mountModel.alignTable->setItem(oldVisualIndex, i, newItem); } mountModel.alignTable->verticalHeader()->blockSignals(true); mountModel.alignTable->verticalHeader()->moveSection(newVisualIndex, oldVisualIndex); mountModel.alignTable->verticalHeader()->blockSignals(false); if (previewShowing) updatePreviewAlignPoints(); } void Align::swapAlignPoints(int firstPt, int secondPt) { for (int i = 0; i < mountModel.alignTable->columnCount(); i++) { QTableWidgetItem *firstPtItem = mountModel.alignTable->takeItem(firstPt, i); QTableWidgetItem *secondPtItem = mountModel.alignTable->takeItem(secondPt, i); mountModel.alignTable->setItem(firstPt, i, secondPtItem); mountModel.alignTable->setItem(secondPt, i, firstPtItem); } } void Align::slotMountModel() { generateAlignStarList(); SkyPoint spWest; spWest.setAlt(30); spWest.setAz(270); spWest.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); mountModel.alignDec->setValue(static_cast(spWest.dec().Degrees())); mountModelDialog.show(); } void Align::slotAddAlignPoint() { int currentRow = mountModel.alignTable->rowCount(); mountModel.alignTable->insertRow(currentRow); QTableWidgetItem *disabledBox = new QTableWidgetItem(); disabledBox->setFlags(Qt::ItemIsSelectable); mountModel.alignTable->setItem(currentRow, 3, disabledBox); } void Align::slotFindAlignObject() { KStarsData *data = KStarsData::Instance(); if (FindDialog::Instance()->exec() == QDialog::Accepted) { SkyObject *object = FindDialog::Instance()->targetObject(); if (object != nullptr) { SkyObject *o = object->clone(); o->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false); int currentRow = mountModel.alignTable->rowCount(); mountModel.alignTable->insertRow(currentRow); QString ra_report, dec_report; getFormattedCoords(o->ra0().Hours(), o->dec0().Degrees(), ra_report, dec_report); QTableWidgetItem *RAReport = new QTableWidgetItem(); RAReport->setText(ra_report); RAReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 0, RAReport); QTableWidgetItem *DECReport = new QTableWidgetItem(); DECReport->setText(dec_report); DECReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 1, DECReport); QTableWidgetItem *ObjNameReport = new QTableWidgetItem(); ObjNameReport->setText(o->longname()); ObjNameReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 2, ObjNameReport); QTableWidgetItem *disabledBox = new QTableWidgetItem(); disabledBox->setFlags(Qt::ItemIsSelectable); mountModel.alignTable->setItem(currentRow, 3, disabledBox); } } if (previewShowing) updatePreviewAlignPoints(); } void Align::resetAlignmentProcedure() { mountModel.alignTable->setCellWidget(currentAlignmentPoint, 3, new QWidget()); QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setFlags(Qt::ItemIsSelectable); statusReport->setIcon(QIcon(":/icons/AlignWarning.svg")); mountModel.alignTable->setItem(currentAlignmentPoint, 3, statusReport); appendLogText(i18n("The Mount Model Tool is Reset.")); mountModel.startAlignB->setIcon( QIcon::fromTheme("media-playback-start")); mountModelRunning = false; currentAlignmentPoint = 0; abort(); } bool Align::alignmentPointsAreBad() { for (int i = 0; i < mountModel.alignTable->rowCount(); i++) { QTableWidgetItem *raCell = mountModel.alignTable->item(i, 0); if (!raCell) return true; QString raString = raCell->text(); if (dms().setFromString(raString, false) == false) return true; QTableWidgetItem *decCell = mountModel.alignTable->item(i, 1); if (!decCell) return true; QString decString = decCell->text(); if (dms().setFromString(decString, true) == false) return true; } return false; } void Align::startStopAlignmentProcedure() { if (!mountModelRunning) { if (mountModel.alignTable->rowCount() > 0) { if (alignmentPointsAreBad()) { KSNotification::error(i18n("Please Check the Alignment Points.")); return; } if (currentGotoMode == GOTO_NOTHING) { int r = KMessageBox::warningContinueCancel( nullptr, i18n("In the Align Module, \"Nothing\" is Selected for the Solver Action. This means that the " "mount model tool will not sync/align your mount but will only report the pointing model " "errors. Do you wish to continue?"), i18n("Pointing Model Report Only?"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "nothing_selected_warning"); if (r == KMessageBox::Cancel) return; } if (currentAlignmentPoint == 0) { for (int row = 0; row < mountModel.alignTable->rowCount(); row++) { QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setIcon(QIcon()); mountModel.alignTable->setItem(row, 3, statusReport); } } mountModel.startAlignB->setIcon( QIcon::fromTheme("media-playback-pause")); mountModelRunning = true; appendLogText(i18n("The Mount Model Tool is Starting.")); startAlignmentPoint(); } } else { mountModel.startAlignB->setIcon( QIcon::fromTheme("media-playback-start")); mountModel.alignTable->setCellWidget(currentAlignmentPoint, 3, new QWidget()); appendLogText(i18n("The Mount Model Tool is Paused.")); abort(); mountModelRunning = false; QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setFlags(Qt::ItemIsSelectable); statusReport->setIcon(QIcon(":/icons/AlignWarning.svg")); mountModel.alignTable->setItem(currentAlignmentPoint, 3, statusReport); } } void Align::startAlignmentPoint() { if (mountModelRunning && currentAlignmentPoint >= 0 && currentAlignmentPoint < mountModel.alignTable->rowCount()) { QTableWidgetItem *raCell = mountModel.alignTable->item(currentAlignmentPoint, 0); QString raString = raCell->text(); dms raDMS = dms::fromString(raString, false); double ra = raDMS.Hours(); QTableWidgetItem *decCell = mountModel.alignTable->item(currentAlignmentPoint, 1); QString decString = decCell->text(); dms decDMS = dms::fromString(decString, true); double dec = decDMS.Degrees(); QProgressIndicator *alignIndicator = new QProgressIndicator(this); mountModel.alignTable->setCellWidget(currentAlignmentPoint, 3, alignIndicator); alignIndicator->startAnimation(); targetCoord.setRA0(ra); targetCoord.setDec0(dec); targetCoord.updateCoordsNow(KStarsData::Instance()->updateNum()); Slew(); } } void Align::finishAlignmentPoint(bool solverSucceeded) { if (mountModelRunning && currentAlignmentPoint >= 0 && currentAlignmentPoint < mountModel.alignTable->rowCount()) { mountModel.alignTable->setCellWidget(currentAlignmentPoint, 3, new QWidget()); QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setFlags(Qt::ItemIsSelectable); if (solverSucceeded) statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg")); else statusReport->setIcon(QIcon(":/icons/AlignFailure.svg")); mountModel.alignTable->setItem(currentAlignmentPoint, 3, statusReport); currentAlignmentPoint++; if (currentAlignmentPoint < mountModel.alignTable->rowCount()) { startAlignmentPoint(); } else { mountModelRunning = false; mountModel.startAlignB->setIcon( QIcon::fromTheme("media-playback-start")); appendLogText(i18n("The Mount Model Tool is Finished.")); currentAlignmentPoint = 0; } } } bool Align::isParserOK() { Q_ASSERT_X(parser, __FUNCTION__, "Astrometry parser is not valid."); bool rc = parser->init(); if (rc) { connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection); connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection); } return rc; } void Align::checkAlignmentTimeout() { if (loadSlewState != IPS_IDLE || ++solverIterations == MAXIMUM_SOLVER_ITERATIONS) abort(); else if (loadSlewState == IPS_IDLE) { appendLogText(i18n("Solver timed out.")); parser->stopSolver(); int currentRow = solutionTable->rowCount() - 1; solutionTable->setCellWidget(currentRow, 3, new QWidget()); QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setIcon(QIcon(":/icons/timedout.svg")); statusReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 3, statusReport); captureAndSolve(); } // TODO must also account for loadAndSlew. Retain file name } void Align::setSolverBackend(int type) { if (sender() == nullptr && type >= 0 && type <= 1) { solverBackendGroup->button(type)->setChecked(true); } // Astrometry solver if (type == SOLVER_ASTROMETRYNET) { astrometryTypeCombo->setEnabled(true); setAstrometrySolverType(Options::astrometrySolverType()); } // ASTAP solver else { if (!QFile::exists(Options::aSTAPExecutable())) { KSMessageBox::Instance()->error(i18n("No valid ASTAP installation found. Install ASTAP and select the path to ASTAP executable in options.")); KConfigDialog::showDialog("alignsettings"); return; } if (astapParser.get() != nullptr) parser = astapParser.get(); else { astapParser.reset(new Ekos::ASTAPAstrometryParser()); parser = astapParser.get(); } parser->setAlign(this); if (parser->init()) { connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection); connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection); } else parser->disconnect(); astrometryTypeCombo->setEnabled(false); } Options::setSolverBackend(type); generateArgs(); } void Align::setAstrometrySolverType(int type) { if (sender() == nullptr && type >= 0 && type <= 2) { astrometryTypeCombo->setCurrentIndex(type); } // For Windows, we only have two items in the combo box (Online & Remote) // When Remote is clicked, type = 1 is sent which is SOLVER_OFFLINE // We need to change that to SOLVER_REMOTE. #ifdef Q_OS_WIN if (type == SOLVER_OFFLINE) type = SOLVER_REMOTE; #endif syncSettings(); Options::setAstrometrySolverType(type); switch (type) { case SOLVER_ONLINE: loadSlewB->setEnabled(true); if (onlineParser.get() != nullptr) { parser = onlineParser.get(); return; } onlineParser.reset(new Ekos::OnlineAstrometryParser()); parser = onlineParser.get(); break; case SOLVER_OFFLINE: loadSlewB->setEnabled(true); if (offlineParser.get() != nullptr) { parser = offlineParser.get(); return; } offlineParser.reset(new Ekos::OfflineAstrometryParser()); parser = offlineParser.get(); break; case SOLVER_REMOTE: loadSlewB->setEnabled(true); if (remoteParser.get() != nullptr && remoteParserDevice != nullptr) { parser = remoteParser.get(); (dynamic_cast(parser))->setAstrometryDevice(remoteParserDevice); return; } remoteParser.reset(new Ekos::RemoteAstrometryParser()); parser = remoteParser.get(); (dynamic_cast(parser))->setAstrometryDevice(remoteParserDevice); if (currentCCD) (dynamic_cast(parser))->setCCD(currentCCD->getDeviceName()); break; } parser->setAlign(this); if (parser->init()) { connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection); connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection); } else parser->disconnect(); } bool Align::setCamera(const QString &device) { for (int i = 0; i < CCDCaptureCombo->count(); i++) if (device == CCDCaptureCombo->itemText(i)) { CCDCaptureCombo->setCurrentIndex(i); checkCCD(i); return true; } return false; } QString Align::camera() { if (currentCCD) return currentCCD->getDeviceName(); return QString(); } void Align::setDefaultCCD(QString ccd) { syncSettings(); Options::setDefaultAlignCCD(ccd); } void Align::checkCCD(int ccdNum) { if (ccdNum == -1 || ccdNum >= CCDs.count()) { ccdNum = CCDCaptureCombo->currentIndex(); if (ccdNum == -1) return; } currentCCD = CCDs.at(ccdNum); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); if (targetChip && targetChip->isCapturing()) return; if (solverBackendGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr) (dynamic_cast(remoteParser.get()))->setCCD(currentCCD->getDeviceName()); syncCCDInfo(); syncTelescopeInfo(); } void Align::addCCD(ISD::GDInterface *newCCD) { if (CCDs.contains(static_cast(newCCD))) { syncCCDInfo(); return; } CCDs.append(static_cast(newCCD)); CCDCaptureCombo->addItem(newCCD->getDeviceName()); checkCCD(); syncSettings(); } void Align::setTelescope(ISD::GDInterface *newTelescope) { currentTelescope = static_cast(newTelescope); currentTelescope->disconnect(this); connect(currentTelescope, &ISD::GDInterface::numberUpdated, this, &Ekos::Align::processNumber, Qt::UniqueConnection); connect(currentTelescope, &ISD::GDInterface::switchUpdated, this, &Ekos::Align::processSwitch, Qt::UniqueConnection); connect(currentTelescope, &ISD::GDInterface::Disconnected, this, [this]() { m_isRateSynced = false; }); if (m_isRateSynced == false) { PAHSlewRateCombo->blockSignals(true); PAHSlewRateCombo->clear(); PAHSlewRateCombo->addItems(currentTelescope->slewRates()); if (Options::pAHMountSpeedIndex() >= 0) PAHSlewRateCombo->setCurrentIndex(Options::pAHMountSpeedIndex()); else PAHSlewRateCombo->setCurrentIndex(currentTelescope->getSlewRate()); PAHSlewRateCombo->blockSignals(false); m_isRateSynced = !currentTelescope->slewRates().empty(); } syncTelescopeInfo(); } void Align::setDome(ISD::GDInterface *newDome) { currentDome = static_cast(newDome); connect(currentDome, &ISD::GDInterface::switchUpdated, this, &Ekos::Align::processSwitch, Qt::UniqueConnection); } void Align::removeDevice(ISD::GDInterface *device) { device->disconnect(this); if (currentTelescope && !strcmp(currentTelescope->getDeviceName(), device->getDeviceName())) { currentTelescope = nullptr; m_isRateSynced = false; } else if (currentDome && !strcmp(currentDome->getDeviceName(), device->getDeviceName())) { currentDome = nullptr; } else if (currentRotator && !strcmp(currentRotator->getDeviceName(), device->getDeviceName())) { currentRotator = nullptr; } if (CCDs.contains(static_cast(device))) { CCDs.removeAll(static_cast(device)); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(device->getDeviceName())); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(device->getDeviceName() + QString(" Guider"))); if (CCDs.empty()) currentCCD = nullptr; checkCCD(); } if (Filters.contains(static_cast(device))) { Filters.removeAll(static_cast(device)); filterManager->removeDevice(device); FilterDevicesCombo->removeItem(FilterDevicesCombo->findText(device->getDeviceName())); if (Filters.empty()) currentFilter = nullptr; checkFilter(); } } bool Align::syncTelescopeInfo() { if (currentTelescope == nullptr || currentTelescope->isConnected() == false) return false; canSync = currentTelescope->canSync(); if (canSync == false && syncR->isEnabled()) { slewR->setChecked(true); appendLogText(i18n("Mount does not support syncing.")); } syncR->setEnabled(canSync); INumberVectorProperty *nvp = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO"); if (nvp) { INumber *np = IUFindNumber(nvp, "TELESCOPE_APERTURE"); if (np && np->value > 0) primaryAperture = np->value; np = IUFindNumber(nvp, "GUIDER_APERTURE"); if (np && np->value > 0) guideAperture = np->value; aperture = primaryAperture; //if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE) if (FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_GUIDE) aperture = guideAperture; np = IUFindNumber(nvp, "TELESCOPE_FOCAL_LENGTH"); if (np && np->value > 0) primaryFL = np->value; np = IUFindNumber(nvp, "GUIDER_FOCAL_LENGTH"); if (np && np->value > 0) guideFL = np->value; focal_length = primaryFL; //if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE) if (FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_GUIDE) focal_length = guideFL; } if (focal_length == -1 || aperture == -1) return false; if (ccd_hor_pixel != -1 && ccd_ver_pixel != -1 && focal_length != -1 && aperture != -1) { FOVScopeCombo->setItemData( ISD::CCD::TELESCOPE_PRIMARY, i18nc("F-Number, Focal Length, Aperture", "F%1 Focal Length: %2 mm Aperture: %3 mm2", QString::number(primaryFL / primaryAperture, 'f', 1), QString::number(primaryFL, 'f', 2), QString::number(primaryAperture, 'f', 2)), Qt::ToolTipRole); FOVScopeCombo->setItemData( ISD::CCD::TELESCOPE_GUIDE, i18nc("F-Number, Focal Length, Aperture", "F%1 Focal Length: %2 mm Aperture: %3 mm2", QString::number(guideFL / guideAperture, 'f', 1), QString::number(guideFL, 'f', 2), QString::number(guideAperture, 'f', 2)), Qt::ToolTipRole); calculateFOV(); generateArgs(); return true; } return false; } void Align::setTelescopeInfo(double primaryFocalLength, double primaryAperture, double guideFocalLength, double guideAperture) { if (primaryFocalLength > 0) primaryFL = primaryFocalLength; if (guideFocalLength > 0) guideFL = guideFocalLength; if (primaryAperture > 0) this->primaryAperture = primaryAperture; if (guideAperture > 0) this->guideAperture = guideAperture; focal_length = primaryFL; if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE) focal_length = guideFL; aperture = primaryAperture; if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE) aperture = guideAperture; syncTelescopeInfo(); } void Align::syncCCDInfo() { INumberVectorProperty *nvp = nullptr; if (currentCCD == nullptr) return; if (useGuideHead) nvp = currentCCD->getBaseDevice()->getNumber("GUIDER_INFO"); else nvp = currentCCD->getBaseDevice()->getNumber("CCD_INFO"); if (nvp) { INumber *np = IUFindNumber(nvp, "CCD_PIXEL_SIZE_X"); if (np && np->value > 0) ccd_hor_pixel = ccd_ver_pixel = np->value; np = IUFindNumber(nvp, "CCD_PIXEL_SIZE_Y"); if (np && np->value > 0) ccd_ver_pixel = np->value; np = IUFindNumber(nvp, "CCD_PIXEL_SIZE_Y"); if (np && np->value > 0) ccd_ver_pixel = np->value; } ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); ISwitchVectorProperty *svp = currentCCD->getBaseDevice()->getSwitch("WCS_CONTROL"); if (svp) setWCSEnabled(Options::astrometrySolverWCS()); targetChip->setImageView(alignView, FITS_ALIGN); targetChip->getFrameMinMax(nullptr, nullptr, nullptr, nullptr, nullptr, &ccd_width, nullptr, &ccd_height); binningCombo->setEnabled(targetChip->canBin()); if (targetChip->canBin()) { binningCombo->blockSignals(true); int binx = 1, biny = 1; targetChip->getMaxBin(&binx, &biny); binningCombo->clear(); for (int i = 0; i < binx; i++) binningCombo->addItem(QString("%1x%2").arg(i + 1).arg(i + 1)); // By default, set to maximum binning since the solver behaves better this way // solverBinningIndex is set by default to 4, but as soon as the user changes the binning, it changes // to whatever value the user selected. if (Options::solverBinningIndex() == 4 && binningCombo->count() <= 4) { binningCombo->setCurrentIndex(binningCombo->count() - 1); Options::setSolverBinningIndex(binningCombo->count() - 1); } else binningCombo->setCurrentIndex(Options::solverBinningIndex()); binningCombo->blockSignals(false); } if (ccd_hor_pixel == -1 || ccd_ver_pixel == -1) return; if (ccd_hor_pixel != -1 && ccd_ver_pixel != -1 && focal_length != -1 && aperture != -1) { calculateFOV(); generateArgs(); } } void Align::getFOVScale(double &fov_w, double &fov_h, double &fov_scale) { fov_w = fov_x; fov_h = fov_y; fov_scale = fov_pixscale; } QList Align::fov() { QList result; result << fov_x << fov_y << fov_pixscale; return result; } QList Align::cameraInfo() { QList result; result << ccd_width << ccd_height << ccd_hor_pixel << ccd_ver_pixel; return result; } QList Align::telescopeInfo() { QList result; result << focal_length << aperture; return result; } void Align::getCalculatedFOVScale(double &fov_w, double &fov_h, double &fov_scale) { // FOV in arcsecs fov_w = 206264.8062470963552 * ccd_width * ccd_hor_pixel / 1000.0 / focal_length; fov_h = 206264.8062470963552 * ccd_height * ccd_ver_pixel / 1000.0 / focal_length; // Pix Scale fov_scale = (fov_w * (Options::solverBinningIndex() + 1)) / ccd_width; // FOV in arcmins fov_w /= 60.0; fov_h /= 60.0; } void Align::calculateFOV() { // Calculate FOV // FOV in arcsecs fov_x = 206264.8062470963552 * ccd_width * ccd_hor_pixel / 1000.0 / focal_length; fov_y = 206264.8062470963552 * ccd_height * ccd_ver_pixel / 1000.0 / focal_length; // Pix Scale fov_pixscale = (fov_x * (Options::solverBinningIndex() + 1)) / ccd_width; // FOV in arcmins fov_x /= 60.0; fov_y /= 60.0; double calculated_fov_x = fov_x; double calculated_fov_y = fov_y; QString calculatedFOV = (QString("%1' x %2'").arg(fov_x, 0, 'g', 3).arg(fov_y, 0, 'g', 3)); // JM 2018-04-20 Above calculations are for RAW FOV. Starting from 2.9.5, we are using EFFECTIVE FOV // Which is the real FOV as measured from the plate solution. The effective FOVs are stored in the database and are unique // per profile/pixel_size/focal_length combinations. It defaults to 0' x 0' and gets updated after the first successful solver is complete. getEffectiveFOV(); if (fov_x == 0) { //FOVOut->setReadOnly(false); FOVOut->setToolTip(i18n("

Effective field of view size in arcminutes.

Please capture and solve once to measure the effective FOV or enter the values manually.

Calculated FOV: %1

", calculatedFOV)); fov_x = calculated_fov_x; fov_y = calculated_fov_y; m_EffectiveFOVPending = true; } else { m_EffectiveFOVPending = false; FOVOut->setToolTip(i18n("

Effective field of view size in arcminutes.

")); } solverFOV->setSize(fov_x, fov_y); sensorFOV->setSize(fov_x, fov_y); if (currentCCD) sensorFOV->setName(currentCCD->getDeviceName()); FOVOut->setText(QString("%1' x %2'").arg(fov_x, 0, 'g', 3).arg(fov_y, 0, 'g', 3)); if (((fov_x + fov_y) / 2.0) > PAH_CUTOFF_FOV) { if (isPAHReady == false) { PAHWidgets->setEnabled(true); isPAHReady = true; emit PAHEnabled(true); PAHWidgets->setToolTip(QString()); FOVDisabledLabel->hide(); } } else if (PAHWidgets->isEnabled()) { PAHWidgets->setEnabled(false); isPAHReady = false; emit PAHEnabled(false); PAHWidgets->setToolTip(i18n( "

Polar Alignment Helper tool requires the following:

1. German Equatorial Mount

2. FOV >" " 0.5 degrees

For small FOVs, use the Legacy Polar Alignment Tool.

")); FOVDisabledLabel->show(); } if (opsAstrometry->kcfg_AstrometryUseImageScale->isChecked()) { int unitType = opsAstrometry->kcfg_AstrometryImageScaleUnits->currentIndex(); // Degrees if (unitType == 0) { double fov_low = qMin(fov_x / 60, fov_y / 60); double fov_high = qMax(fov_x / 60, fov_y / 60); opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low); opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high); Options::setAstrometryImageScaleLow(fov_low); Options::setAstrometryImageScaleHigh(fov_high); } // Arcmins else if (unitType == 1) { double fov_low = qMin(fov_x, fov_y); double fov_high = qMax(fov_x, fov_y); opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low); opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high); Options::setAstrometryImageScaleLow(fov_low); Options::setAstrometryImageScaleHigh(fov_high); } // Arcsec per pixel else { opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_pixscale * 0.9); opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_pixscale * 1.1); // 10% boundary Options::setAstrometryImageScaleLow(fov_pixscale * 0.9); Options::setAstrometryImageScaleHigh(fov_pixscale * 1.1); } } } QStringList Align::generateOptions(const QVariantMap &optionsMap, uint8_t solverType) { QStringList solver_args; // -O overwrite // -3 Expected RA // -4 Expected DEC // -5 Radius (deg) // -L lower scale of image in arcminutes // -H upper scale of image in arcminutes // -u aw set scale to be in arcminutes // -W solution.wcs name of solution file // apog1.jpg name of target file to analyze //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg if (solverType == SOLVER_ASTROMETRYNET) { // Start with always-used arguments solver_args << "-O" << "--no-plots"; // Now go over boolean options // noverify if (optionsMap.contains("noverify")) solver_args << "--no-verify"; // noresort if (optionsMap.contains("resort")) solver_args << "--resort"; // fits2fits if (optionsMap.contains("nofits2fits")) solver_args << "--no-fits2fits"; // downsample if (optionsMap.contains("downsample")) solver_args << "--downsample" << QString::number(optionsMap.value("downsample", 2).toInt()); // image scale low if (optionsMap.contains("scaleL")) solver_args << "-L" << QString::number(optionsMap.value("scaleL").toDouble()); // image scale high if (optionsMap.contains("scaleH")) solver_args << "-H" << QString::number(optionsMap.value("scaleH").toDouble()); // image scale units if (optionsMap.contains("scaleUnits")) solver_args << "-u" << optionsMap.value("scaleUnits").toString(); // RA if (optionsMap.contains("ra")) solver_args << "-3" << QString::number(optionsMap.value("ra").toDouble()); // DE if (optionsMap.contains("de")) solver_args << "-4" << QString::number(optionsMap.value("de").toDouble()); // Radius if (optionsMap.contains("radius")) solver_args << "-5" << QString::number(optionsMap.value("radius").toDouble()); // Custom if (optionsMap.contains("custom")) solver_args << optionsMap.value("custom").toString(); } else { // Radius if (optionsMap.contains("radius")) solver_args << "-r" << QString::number(optionsMap.value("radius").toDouble()); // downsample if (optionsMap.contains("downsample")) solver_args << "-z" << QString::number(optionsMap.value("downsample", 0).toInt()); // Speed if (optionsMap.contains("speed")) solver_args << "-speed" << optionsMap.value("speed").toString(); if (optionsMap.contains("update")) solver_args << "-update"; } return solver_args; } //This will generate the high and low scale of the imager field size based on the stated units. void Align::generateFOVBounds(double fov_h, QString &fov_low, QString &fov_high, double tolerance) { // This sets the percentage we search outside the lower and upper boundary limits // by default, we stretch the limits by 5% (tolerance = 0.05) double lower_boundary = 1.0 - tolerance; double upper_boundary = 1.0 + tolerance; // let's stretch the boundaries by 5% // fov_lower = ((fov_h < fov_v) ? (fov_h * lower_boundary) : (fov_v * lower_boundary)); // fov_upper = ((fov_h > fov_v) ? (fov_h * upper_boundary) : (fov_v * upper_boundary)); // JM 2019-10-20: The bounds consider image width only, not height. double fov_lower = fov_h * lower_boundary; double fov_upper = fov_h * upper_boundary; //No need to do anything if they are aw, since that is the default fov_low = QString::number(fov_lower); fov_high = QString::number(fov_upper); } void Align::generateArgs() { QVariantMap optionsMap; if (solverBackendGroup->checkedId() == SOLVER_ASTROMETRYNET) { // -O overwrite // -3 Expected RA // -4 Expected DEC // -5 Radius (deg) // -L lower scale of image in arcminutes // -H upper scale of image in arcminutes // -u aw set scale to be in arcminutes // -W solution.wcs name of solution file // apog1.jpg name of target file to analyze //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg if (Options::astrometryUseNoVerify()) optionsMap["noverify"] = true; if (Options::astrometryUseResort()) optionsMap["resort"] = true; if (Options::astrometryUseNoFITS2FITS()) optionsMap["nofits2fits"] = true; if (Options::astrometryUseDownsample()) { if (Options::astrometryAutoDownsample() && ccd_width && ccd_height) { uint8_t bin = qMax(Options::solverBinningIndex() + 1, 1u); uint16_t w = ccd_width / bin; optionsMap["downsample"] = getSolverDownsample(w); } else optionsMap["downsample"] = Options::astrometryDownsample(); } if (Options::astrometryUseImageScale() && fov_x > 0 && fov_y > 0) { QString units = ImageScales[Options::astrometryImageScaleUnits()]; if (Options::astrometryAutoUpdateImageScale()) { QString fov_low, fov_high; double fov_w = fov_x; double fov_h = fov_y; if (units == "dw") { fov_w /= 60; fov_h /= 60; } else if (units == "app") { fov_w = fov_pixscale; fov_h = fov_pixscale; } // If effective FOV is pending, let's set a wider tolerance range generateFOVBounds(fov_w, fov_low, fov_high, m_EffectiveFOVPending ? 0.3 : 0.05); optionsMap["scaleL"] = fov_low; optionsMap["scaleH"] = fov_high; optionsMap["scaleUnits"] = units; } else { optionsMap["scaleL"] = Options::astrometryImageScaleLow(); optionsMap["scaleH"] = Options::astrometryImageScaleHigh(); optionsMap["scaleUnits"] = units; } } if (Options::astrometryUsePosition() && currentTelescope != nullptr) { double ra = 0, dec = 0; currentTelescope->getEqCoords(&ra, &dec); optionsMap["ra"] = ra * 15.0; optionsMap["de"] = dec; optionsMap["radius"] = Options::astrometryRadius(); } if (Options::astrometryCustomOptions().isEmpty() == false) optionsMap["custom"] = Options::astrometryCustomOptions(); } // ASTAP else { if (Options::aSTAPSearchRadius()) optionsMap["radius"] = Options::aSTAPSearchRadiusValue(); if (Options::aSTAPDownSample() && Options::aSTAPDownSampleValue() > 0) optionsMap["downsample"] = Options::aSTAPDownSampleValue(); optionsMap["speed"] = Options::aSTAPLargeSearchWindow() ? "slow" : "auto"; if (Options::aSTAPUpdateFITS()) optionsMap["update"] = true; } QStringList solverArgs = generateOptions(optionsMap, solverBackendGroup->checkedId()); QString options = solverArgs.join(" "); solverOptions->setText(options); solverOptions->setToolTip(options); } bool Align::captureAndSolve() { m_AlignTimer.stop(); m_CaptureTimer.stop(); #ifdef Q_OS_OSX if(solverBackendGroup->checkedId() == SOLVER_OFFLINE) { if(Options::useDefaultPython()) { if( !opsAlign->astropyInstalled() || !opsAlign->pythonInstalled() ) { KSNotification::error(i18n("Astrometry.net uses python3 and the astropy package for plate solving images offline. These were not detected on your system. Please go into the Align Options and either click the setup button to install them or uncheck the default button and enter the path to python3 on your system and manually install astropy.")); return false; } } } #endif if (currentCCD == nullptr) return false; if (currentCCD->isConnected() == false) { appendLogText(i18n("Error: lost connection to CCD.")); KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed"), KSNotification::EVENT_ALERT); return false; } if (currentCCD->isBLOBEnabled() == false) { currentCCD->setBLOBEnabled(true); } // If CCD Telescope Type does not match desired scope type, change it // but remember current value so that it can be reset once capture is complete or is aborted. if (currentCCD->getTelescopeType() != FOVScopeCombo->currentIndex()) { rememberTelescopeType = currentCCD->getTelescopeType(); currentCCD->setTelescopeType(static_cast(FOVScopeCombo->currentIndex())); } if (parser->init() == false) return false; if (focal_length == -1 || aperture == -1) { KSNotification::error(i18n("Telescope aperture and focal length are missing. Please check your driver settings and try again.")); return false; } if (ccd_hor_pixel == -1 || ccd_ver_pixel == -1) { KSNotification::error(i18n("CCD pixel size is missing. Please check your driver settings and try again.")); return false; } if (currentFilter != nullptr) { if (currentFilter->isConnected() == false) { appendLogText(i18n("Error: lost connection to filter wheel.")); return false; } int targetPosition = FilterPosCombo->currentIndex() + 1; if (targetPosition > 0 && targetPosition != currentFilterPosition) { filterPositionPending = true; // Disabling the autofocus policy for align. filterManager->setFilterPosition( targetPosition, FilterManager::NO_AUTOFOCUS_POLICY); state = ALIGN_PROGRESS; return true; } } if (currentCCD->getDriverInfo()->getClientManager()->getBLOBMode(currentCCD->getDeviceName(), "CCD1") == B_NEVER) { if (KMessageBox::questionYesNo( nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) == KMessageBox::Yes) { currentCCD->getDriverInfo()->getClientManager()->setBLOBMode(B_ONLY, currentCCD->getDeviceName(), "CCD1"); currentCCD->getDriverInfo()->getClientManager()->setBLOBMode(B_ONLY, currentCCD->getDeviceName(), "CCD2"); } else { return false; } } double seqExpose = exposureIN->value(); ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (focusState >= FOCUS_PROGRESS) { appendLogText(i18n("Cannot capture while focus module is busy. Retrying in %1 seconds...", CAPTURE_RETRY_DELAY / 1000)); m_CaptureTimer.start(CAPTURE_RETRY_DELAY); return false; } if (targetChip->isCapturing()) { appendLogText(i18n("Cannot capture while CCD exposure is in progress. Retrying in %1 seconds...", CAPTURE_RETRY_DELAY / 1000)); m_CaptureTimer.start(CAPTURE_RETRY_DELAY); return false; } alignView->setBaseSize(alignWidget->size()); connect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Align::newFITS); connect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Align::checkCCDExposureProgress); // In case of remote solver, check if we need to update active CCD if (solverBackendGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr) { // Update ACTIVE_CCD of the remote astrometry driver so it listens to BLOB emitted by the CCD ITextVectorProperty *activeDevices = remoteParserDevice->getBaseDevice()->getText("ACTIVE_DEVICES"); if (activeDevices) { IText *activeCCD = IUFindText(activeDevices, "ACTIVE_CCD"); if (QString(activeCCD->text) != CCDCaptureCombo->currentText()) { IUSaveText(activeCCD, CCDCaptureCombo->currentText().toLatin1().data()); remoteParserDevice->getDriverInfo()->getClientManager()->sendNewText(activeDevices); } } // Enable remote parse dynamic_cast(remoteParser.get())->setEnabled(true); QString options = solverOptions->text().simplified(); QStringList solverArgs = options.split(' '); dynamic_cast(remoteParser.get())->sendArgs(solverArgs); // If mount model was reset, we do not update targetCoord // since the RA/DE is now different immediately after the reset // so we still try to lock for the coordinates before the reset. if (solverIterations == 0 && mountModelReset == false) { double ra, dec; currentTelescope->getEqCoords(&ra, &dec); targetCoord.setRA(ra); targetCoord.setDec(dec); } mountModelReset = false; solverTimer.start(); } if (currentCCD->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) { rememberUploadMode = ISD::CCD::UPLOAD_LOCAL; currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT); } rememberCCDExposureLooping = currentCCD->isLooping(); if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(false); // Remove temporary FITS files left before by the solver QDir dir(QDir::tempPath()); dir.setNameFilters(QStringList() << "fits*" << "tmp.*"); dir.setFilter(QDir::Files); for (auto &dirFile : dir.entryList()) dir.remove(dirFile); currentCCD->setTransformFormat(ISD::CCD::FORMAT_FITS); targetChip->resetFrame(); targetChip->setBatchMode(false); targetChip->setCaptureMode(FITS_ALIGN); targetChip->setFrameType(FRAME_LIGHT); int bin = Options::solverBinningIndex() + 1; targetChip->setBinning(bin, bin); // In case we're in refresh phase of the polar alignment helper then we use capture value from there if (pahStage == PAH_REFRESH) targetChip->capture(PAHExposure->value()); else targetChip->capture(seqExpose); Options::setAlignExposure(seqExpose); solveB->setEnabled(false); stopB->setEnabled(true); pi->startAnimation(); differentialSlewingActivated = false; state = ALIGN_PROGRESS; emit newStatus(state); // If we're just refreshing, then we're done if (pahStage == PAH_REFRESH) return true; appendLogText(i18n("Capturing image...")); //This block of code will create the row in the solution table and populate RA, DE, and object name. //It also starts the progress indicator. double ra, dec; currentTelescope->getEqCoords(&ra, &dec); if (loadSlewState == IPS_IDLE) { int currentRow = solutionTable->rowCount(); solutionTable->insertRow(currentRow); for (int i = 4; i < 6; i++) { QTableWidgetItem *disabledBox = new QTableWidgetItem(); disabledBox->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, i, disabledBox); } QTableWidgetItem *RAReport = new QTableWidgetItem(); RAReport->setText(ScopeRAOut->text()); RAReport->setTextAlignment(Qt::AlignHCenter); RAReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 0, RAReport); QTableWidgetItem *DECReport = new QTableWidgetItem(); DECReport->setText(ScopeDecOut->text()); DECReport->setTextAlignment(Qt::AlignHCenter); DECReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 1, DECReport); double maxrad = 1.0; SkyObject *so = KStarsData::Instance()->skyComposite()->objectNearest(new SkyPoint(dms(ra * 15), dms(dec)), maxrad); QString name; if (so) { name = so->longname(); } else { name = "None"; } QTableWidgetItem *ObjNameReport = new QTableWidgetItem(); ObjNameReport->setText(name); ObjNameReport->setTextAlignment(Qt::AlignHCenter); ObjNameReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 2, ObjNameReport); #ifdef Q_OS_OSX repaint(); //This is a band-aid for a bug in QT 5.10.0 #endif QProgressIndicator *alignIndicator = new QProgressIndicator(this); solutionTable->setCellWidget(currentRow, 3, alignIndicator); alignIndicator->startAnimation(); #ifdef Q_OS_OSX repaint(); //This is a band-aid for a bug in QT 5.10.0 #endif } return true; } void Align::newFITS(IBLOB *bp) { // Ignore guide head if there is any. if (!strcmp(bp->name, "CCD2")) return; disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Align::newFITS); disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Align::checkCCDExposureProgress); blobType = *(static_cast(bp->aux1)); blobFileName = QString(static_cast(bp->aux2)); // If it's Refresh, we're done if (pahStage == PAH_REFRESH) { setCaptureComplete(); return; } appendLogText(i18n("Image received.")); if (solverBackendGroup->checkedId() != SOLVER_REMOTE) { if (blobType == ISD::CCD::BLOB_FITS) { ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (alignDarkFrameCheck->isChecked()) { int x, y, w, h, binx = 1, biny = 1; targetChip->getFrame(&x, &y, &w, &h); targetChip->getBinning(&binx, &biny); uint16_t offsetX = x / binx; uint16_t offsetY = y / biny; FITSData *darkData = DarkLibrary::Instance()->getDarkFrame(targetChip, exposureIN->value()); connect(DarkLibrary::Instance(), &DarkLibrary::darkFrameCompleted, this, [&](bool completed) { DarkLibrary::Instance()->disconnect(this); alignDarkFrameCheck->setChecked(completed); if (completed) setCaptureComplete(); else abort(); }); connect(DarkLibrary::Instance(), &DarkLibrary::newLog, this, &Ekos::Align::appendLogText); if (darkData) DarkLibrary::Instance()->subtract(darkData, alignView, FITS_NONE, offsetX, offsetY); else { DarkLibrary::Instance()->captureAndSubtract(targetChip, alignView, exposureIN->value(), offsetX, offsetY); } return; } } setCaptureComplete(); } } void Align::setCaptureComplete() { DarkLibrary::Instance()->disconnect(this); if (pahStage == PAH_REFRESH) { newFrame(alignView); captureAndSolve(); return; } emit newImage(alignView); if (solverBackendGroup->checkedId() == SOLVER_ASTROMETRYNET && astrometryTypeCombo->currentIndex() == SOLVER_ONLINE && Options::astrometryUseJPEG()) { ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (targetChip) { QString jpegFile = blobFileName + ".jpg"; bool rc = alignView->getDisplayImage().save(jpegFile, "JPG"); if (rc) blobFileName = jpegFile; } } if (getSolverFOV()) getSolverFOV()->setImage(alignView->getDisplayImage()); startSolving(blobFileName); } void Align::setSolverAction(int mode) { gotoModeButtonGroup->button(mode)->setChecked(true); currentGotoMode = static_cast(mode); } void Align::startSolving(const QString &filename, bool isGenerated) { QStringList solverArgs; QString options = solverOptions->text().simplified(); if (isGenerated) { solverArgs = options.split(' '); // Replace RA and DE with LST & 90/-90 pole if (pahStage == PAH_FIRST_CAPTURE) { for (int i = 0; i < solverArgs.count(); i++) { // RA if (solverArgs[i] == "-3") solverArgs[i + 1] = QString::number(KStarsData::Instance()->lst()->Degrees()); // DE. +90 for Northern hemisphere. -90 for southern hemisphere else if (solverArgs[i] == "-4") solverArgs[i + 1] = QString::number(hemisphere == NORTH_HEMISPHERE ? 90 : -90); } } } else if (filename.endsWith(QLatin1String("fits")) || filename.endsWith(QLatin1String("fit"))) { solverArgs = getSolverOptionsFromFITS(filename); appendLogText(i18n("Using solver options: %1", solverArgs.join(' '))); } else { KGuiItem blindItem(i18n("Blind solver"), QString(), i18n("Blind solver takes a very long time to solve but can reliably solve any image any " "where in the sky given enough time.")); KGuiItem existingItem(i18n("Use existing settings"), QString(), i18n("Mount must be pointing close to the target location and current field of view must " "match the image's field of view.")); int rc = KMessageBox::questionYesNoCancel(nullptr, i18n("No metadata is available in this image. Do you want to use the " "blind solver or the existing solver settings?"), i18n("Astrometry solver"), blindItem, existingItem, KStandardGuiItem::cancel(), "blind_solver_or_existing_solver_option"); if (rc == KMessageBox::Yes) { QVariantMap optionsMap; if (Options::astrometryUseNoVerify()) optionsMap["noverify"] = true; if (Options::astrometryUseResort()) optionsMap["resort"] = true; if (Options::astrometryUseNoFITS2FITS()) optionsMap["nofits2fits"] = true; if (Options::astrometryUseDownsample()) optionsMap["downsample"] = Options::astrometryDownsample(); solverArgs = generateOptions(optionsMap, solverBackendGroup->checkedId()); } else if (rc == KMessageBox::No) solverArgs = options.split(' '); else { abort(); return; } } if (solverIterations == 0 && mountModelReset == false) { double ra, dec; currentTelescope->getEqCoords(&ra, &dec); targetCoord.setRA(ra); targetCoord.setDec(dec); } mountModelReset = false; Options::setSolverAccuracyThreshold(accuracySpin->value()); Options::setAlignDarkFrame(alignDarkFrameCheck->isChecked()); Options::setSolverGotoOption(currentGotoMode); if (fov_x > 0) parser->verifyIndexFiles(fov_x, fov_y); solverTimer.start(); m_AlignTimer.start(); if (currentGotoMode == GOTO_SLEW) appendLogText(i18n("Solver iteration #%1", solverIterations + 1)); state = ALIGN_PROGRESS; emit newStatus(state); parser->startSovler(filename, solverArgs, isGenerated); } void Align::solverFinished(double orientation, double ra, double dec, double pixscale) { pi->stopAnimation(); stopB->setEnabled(false); solveB->setEnabled(true); sOrientation = orientation; sRA = ra; sDEC = dec; // Reset Telescope Type to remembered value if (rememberTelescopeType != ISD::CCD::TELESCOPE_UNKNOWN) { currentCCD->setTelescopeType(rememberTelescopeType); rememberTelescopeType = ISD::CCD::TELESCOPE_UNKNOWN; } m_AlignTimer.stop(); if (solverBackendGroup->checkedId() == SOLVER_ASTROMETRYNET && astrometryTypeCombo->currentIndex() == SOLVER_REMOTE && remoteParser.get() != nullptr) { // Disable remote parse dynamic_cast(remoteParser.get())->setEnabled(false); } int binx, biny; ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); targetChip->getBinning(&binx, &biny); if (Options::alignmentLogging()) appendLogText(i18n("Solver RA (%1) DEC (%2) Orientation (%3) Pixel Scale (%4)", QString::number(ra, 'g', 5), QString::number(dec, 'g', 5), QString::number(orientation, 'g', 5), QString::number(pixscale, 'g', 5))); if ( (fov_x == 0 || m_EffectiveFOVPending) && pixscale > 0) { double newFOVW = ccd_width * pixscale / binx / 60.0; double newFOVH = ccd_height * pixscale / biny / 60.0; saveNewEffectiveFOV(newFOVW, newFOVH); m_EffectiveFOVPending = false; } alignCoord.setRA0(ra / 15.0); alignCoord.setDec0(dec); RotOut->setText(QString::number(orientation, 'g', 5)); // Convert to JNow alignCoord.apparentCoord(static_cast(J2000), KStars::Instance()->data()->ut().djd()); // Get horizontal coords alignCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); double raDiff = (alignCoord.ra().deltaAngle(targetCoord.ra())).Degrees() * 3600; double deDiff = (alignCoord.dec().deltaAngle(targetCoord.dec())).Degrees() * 3600; dms RADiff(fabs(raDiff) / 3600.0), DEDiff(deDiff / 3600.0); QString dRAText = QString("%1%2").arg((raDiff > 0 ? "+" : "-"), RADiff.toHMSString()); QString dDEText = DEDiff.toDMSString(true); pixScaleOut->setText(QString::number(pixscale, 'f', 2)); targetDiff = sqrt(raDiff * raDiff + deDiff * deDiff); errOut->setText(QString("%1 arcsec. RA:%2 DE:%3").arg( QString::number(targetDiff, 'f', 0), QString::number(raDiff, 'f', 0), QString::number(deDiff, 'f', 0))); if (targetDiff <= static_cast(accuracySpin->value())) errOut->setStyleSheet("color:green"); else if (targetDiff < 1.5 * accuracySpin->value()) errOut->setStyleSheet("color:yellow"); else errOut->setStyleSheet("color:red"); double solverPA = orientation; // TODO 2019-11-06 JM: KStars needs to support "upside-down" displays since this is a hack. // Because astrometry reads image upside-down (bottom to top), the orientation is rotated 180 degrees when compared to PA // PA = Orientation + 180 double solverFlippedPA = orientation + 180; // Limit PA to -180 to +180 if (solverFlippedPA > 180) solverFlippedPA -= 360; if (solverFlippedPA < -180) solverFlippedPA += 360; solverFOV->setCenter(alignCoord); solverFOV->setPA(solverFlippedPA); solverFOV->setImageDisplay(Options::astrometrySolverOverlay()); // Sensor FOV as well sensorFOV->setPA(solverFlippedPA); QString ra_dms, dec_dms; getFormattedCoords(alignCoord.ra().Hours(), alignCoord.dec().Degrees(), ra_dms, dec_dms); SolverRAOut->setText(ra_dms); SolverDecOut->setText(dec_dms); //This block of code will write the result into the solution table and plot it on the graph. int currentRow = solutionTable->rowCount() - 1; if (loadSlewState == IPS_IDLE) { QTableWidgetItem *dRAReport = new QTableWidgetItem(); if (dRAReport) { dRAReport->setText(QString::number(raDiff, 'f', 3) + "\""); dRAReport->setTextAlignment(Qt::AlignHCenter); dRAReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 4, dRAReport); } QTableWidgetItem *dDECReport = new QTableWidgetItem(); if (dDECReport) { dDECReport->setText(QString::number(deDiff, 'f', 3) + "\""); dDECReport->setTextAlignment(Qt::AlignHCenter); dDECReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 5, dDECReport); } double raPlot = raDiff; double decPlot = deDiff; alignPlot->graph(0)->addData(raPlot, decPlot); QCPItemText *textLabel = new QCPItemText(alignPlot); textLabel->setPositionAlignment(Qt::AlignVCenter | Qt::AlignHCenter); textLabel->position->setType(QCPItemPosition::ptPlotCoords); textLabel->position->setCoords(raPlot, decPlot); textLabel->setColor(Qt::red); textLabel->setPadding(QMargins(0, 0, 0, 0)); textLabel->setBrush(Qt::white); textLabel->setPen(Qt::NoPen); textLabel->setText(' ' + QString::number(solutionTable->rowCount()) + ' '); textLabel->setFont(QFont(font().family(), 8)); if (!alignPlot->xAxis->range().contains(raDiff)) { alignPlot->graph(0)->rescaleKeyAxis(true); alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0); } if (!alignPlot->yAxis->range().contains(deDiff)) { alignPlot->graph(0)->rescaleValueAxis(true); alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0); } alignPlot->replot(); } if (Options::astrometrySolverWCS()) { INumberVectorProperty *ccdRotation = currentCCD->getBaseDevice()->getNumber("CCD_ROTATION"); if (ccdRotation) { INumber *rotation = IUFindNumber(ccdRotation, "CCD_ROTATION_VALUE"); if (rotation) { ClientManager *clientManager = currentCCD->getDriverInfo()->getClientManager(); rotation->value = orientation; clientManager->sendNewNumber(ccdRotation); if (m_wcsSynced == false) { appendLogText( i18n("WCS information updated. Images captured from this point forward shall have valid WCS.")); // Just send telescope info in case the CCD driver did not pick up before. INumberVectorProperty *telescopeInfo = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO"); if (telescopeInfo) clientManager->sendNewNumber(telescopeInfo); m_wcsSynced = true; } } } } m_CaptureErrorCounter = 0; m_SlewErrorCounter = 0; m_CaptureTimeoutCounter = 0; appendLogText(i18n("Solution coordinates: RA (%1) DEC (%2) Telescope Coordinates: RA (%3) DEC (%4)", alignCoord.ra().toHMSString(), alignCoord.dec().toDMSString(), telescopeCoord.ra().toHMSString(), telescopeCoord.dec().toDMSString())); if (loadSlewState == IPS_IDLE && currentGotoMode == GOTO_SLEW) { dms diffDeg(targetDiff / 3600.0); appendLogText(i18n("Target is within %1 degrees of solution coordinates.", diffDeg.toDMSString())); } if (rememberUploadMode != currentCCD->getUploadMode()) currentCCD->setUploadMode(rememberUploadMode); if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(true); //This block of code along with some sections in the switch below will set the status report in the solution table for this item. std::unique_ptr statusReport(new QTableWidgetItem()); if (loadSlewState == IPS_IDLE) { solutionTable->setCellWidget(currentRow, 3, new QWidget()); statusReport->setFlags(Qt::ItemIsSelectable); } // Update Rotator offsets if (currentRotator != nullptr) { // When Load&Slew image is solved, we check if we need to rotate the rotator to match the position angle of the image if (loadSlewState == IPS_BUSY && Options::astrometryUseRotator()) { loadSlewTargetPA = solverPA; qCDebug(KSTARS_EKOS_ALIGN) << "loaSlewTargetPA:" << loadSlewTargetPA; } else { INumberVectorProperty *absAngle = currentRotator->getBaseDevice()->getNumber("ABS_ROTATOR_ANGLE"); if (absAngle) { // PA = RawAngle * Multiplier + Offset currentRotatorPA = solverPA; double rawAngle = absAngle->np[0].value; double offset = range360(solverPA - (rawAngle * Options::pAMultiplier())); qCDebug(KSTARS_EKOS_ALIGN) << "Raw Rotator Angle:" << rawAngle << "Rotator PA:" << currentRotatorPA << "Rotator Offset:" << offset; Options::setPAOffset(offset); } if (absAngle && std::isnan(loadSlewTargetPA) == false && fabs(currentRotatorPA - loadSlewTargetPA) * 60 > Options::astrometryRotatorThreshold()) { double rawAngle = range360((loadSlewTargetPA - Options::pAOffset()) / Options::pAMultiplier()); // if (rawAngle < 0) // rawAngle += 360; // else if (rawAngle > 360) // rawAngle -= 360; absAngle->np[0].value = rawAngle; ClientManager *clientManager = currentRotator->getDriverInfo()->getClientManager(); clientManager->sendNewNumber(absAngle); appendLogText(i18n("Setting position angle to %1 degrees E of N...", loadSlewTargetPA)); return; } } } emit newSolverResults(orientation, ra, dec, pixscale); QJsonObject solution = { {"ra", SolverRAOut->text()}, {"de", SolverDecOut->text()}, {"dRA", dRAText}, {"dDE", dDEText}, {"pix", pixscale}, {"rot", orientation}, {"fov", FOVOut->text()}, }; emit newSolution(solution.toVariantMap()); switch (currentGotoMode) { case GOTO_SYNC: executeGOTO(); if (loadSlewState == IPS_IDLE) { statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg")); solutionTable->setItem(currentRow, 3, statusReport.release()); } return; case GOTO_SLEW: if (loadSlewState == IPS_BUSY || targetDiff > static_cast(accuracySpin->value())) { if (loadSlewState == IPS_IDLE && ++solverIterations == MAXIMUM_SOLVER_ITERATIONS) { appendLogText(i18n("Maximum number of iterations reached. Solver failed.")); if (loadSlewState == IPS_IDLE) { statusReport->setIcon(QIcon(":/icons/AlignFailure.svg")); solutionTable->setItem(currentRow, 3, statusReport.release()); } solverFailed(); if (mountModelRunning) finishAlignmentPoint(false); return; } targetAccuracyNotMet = true; if (loadSlewState == IPS_IDLE) { statusReport->setIcon(QIcon(":/icons/AlignWarning.svg")); solutionTable->setItem(currentRow, 3, statusReport.release()); } executeGOTO(); return; } if (loadSlewState == IPS_IDLE) { statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg")); solutionTable->setItem(currentRow, 3, statusReport.release()); } appendLogText(i18n("Target is within acceptable range. Astrometric solver is successful.")); if (mountModelRunning) { finishAlignmentPoint(true); if (mountModelRunning) return; } break; case GOTO_NOTHING: if (loadSlewState == IPS_IDLE) { statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg")); solutionTable->setItem(currentRow, 3, statusReport.release()); } if (mountModelRunning) { finishAlignmentPoint(true); if (mountModelRunning) return; } break; } KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully")); state = ALIGN_COMPLETE; emit newStatus(state); solverIterations = 0; if (pahStage != PAH_IDLE) processPAHStage(orientation, ra, dec, pixscale); else if (azStage > AZ_INIT || altStage > ALT_INIT) executePolarAlign(); else { solveB->setEnabled(true); loadSlewB->setEnabled(true); } } void Align::solverFailed() { KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed with errors"), KSNotification::EVENT_ALERT); pi->stopAnimation(); stopB->setEnabled(false); solveB->setEnabled(true); m_AlignTimer.stop(); azStage = AZ_INIT; altStage = ALT_INIT; loadSlewState = IPS_IDLE; solverIterations = 0; m_CaptureErrorCounter = 0; m_CaptureTimeoutCounter = 0; m_SlewErrorCounter = 0; state = ALIGN_FAILED; emit newStatus(state); int currentRow = solutionTable->rowCount() - 1; solutionTable->setCellWidget(currentRow, 3, new QWidget()); QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setIcon(QIcon(":/icons/AlignFailure.svg")); statusReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 3, statusReport); } void Align::abort() { m_CaptureTimer.stop(); parser->stopSolver(); pi->stopAnimation(); stopB->setEnabled(false); solveB->setEnabled(true); loadSlewB->setEnabled(true); // Reset Telescope Type to remembered value if (rememberTelescopeType != ISD::CCD::TELESCOPE_UNKNOWN) { currentCCD->setTelescopeType(rememberTelescopeType); rememberTelescopeType = ISD::CCD::TELESCOPE_UNKNOWN; } azStage = AZ_INIT; altStage = ALT_INIT; loadSlewState = IPS_IDLE; solverIterations = 0; m_CaptureErrorCounter = 0; m_CaptureTimeoutCounter = 0; m_SlewErrorCounter = 0; m_AlignTimer.stop(); disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Align::newFITS); disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Align::checkCCDExposureProgress); if (rememberUploadMode != currentCCD->getUploadMode()) currentCCD->setUploadMode(rememberUploadMode); if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(true); ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); // If capture is still in progress, let's stop that. if (pahStage == PAH_REFRESH) { if (targetChip->isCapturing()) targetChip->abortExposure(); appendLogText(i18n("Refresh is complete.")); } else { if (targetChip->isCapturing()) { targetChip->abortExposure(); appendLogText(i18n("Capture aborted.")); } else { int elapsed = static_cast(round(solverTimer.elapsed() / 1000.0)); appendLogText(i18np("Solver aborted after %1 second.", "Solver aborted after %1 seconds", elapsed)); } } state = ALIGN_ABORTED; emit newStatus(state); int currentRow = solutionTable->rowCount() - 1; solutionTable->setCellWidget(currentRow, 3, new QWidget()); QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setIcon(QIcon(":/icons/AlignFailure.svg")); statusReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 3, statusReport); } QList Align::getSolutionResult() { QList result; result << sOrientation << sRA << sDEC; return result; } void Align::appendLogText(const QString &text) { m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_ALIGN) << text; emit newLog(text); } void Align::clearLog() { m_LogText.clear(); emit newLog(QString()); } void Align::processSwitch(ISwitchVectorProperty *svp) { if (!strcmp(svp->name, "DOME_MOTION")) { // If dome is not ready and state is now if (domeReady == false && svp->s == IPS_OK) { domeReady = true; // trigger process number for mount so that it proceeds with normal workflow since // it was stopped by dome not being ready handleMountStatus(); } } else if ((!strcmp(svp->name, "TELESCOPE_MOTION_NS") || !strcmp(svp->name, "TELESCOPE_MOTION_WE"))) switch (svp->s) { case IPS_BUSY: // react upon mount motion handleMountMotion(); m_wasSlewStarted = true; break; default: qCDebug(KSTARS_EKOS_ALIGN) << "Mount motion finished."; handleMountStatus(); break; } } void Align::processNumber(INumberVectorProperty *nvp) { if (!strcmp(nvp->name, "EQUATORIAL_EOD_COORD") || !strcmp(nvp->name, "EQUATORIAL_COORD")) { QString ra_dms, dec_dms; if (!strcmp(nvp->name, "EQUATORIAL_COORD")) { telescopeCoord.setRA0(nvp->np[0].value); telescopeCoord.setDec0(nvp->np[1].value); // Get JNow as well telescopeCoord.apparentCoord(static_cast(J2000), KStars::Instance()->data()->ut().djd()); } else { telescopeCoord.setRA(nvp->np[0].value); telescopeCoord.setDec(nvp->np[1].value); } getFormattedCoords(telescopeCoord.ra().Hours(), telescopeCoord.dec().Degrees(), ra_dms, dec_dms); telescopeCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); ScopeRAOut->setText(ra_dms); ScopeDecOut->setText(dec_dms); qCDebug(KSTARS_EKOS_ALIGN) << "## RA" << ra_dms << "DE" << dec_dms << "state:" << pstateStr(nvp->s) << "slewStarted?" << m_wasSlewStarted; switch (nvp->s) { // Idle --> Mount not tracking or slewing case IPS_IDLE: m_wasSlewStarted = false; qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_IDLE --> setting slewStarted to FALSE"; break; // Ok --> Mount Tracking. If m_wasSlewStarted is true // then it just finished slewing case IPS_OK: { // Update the boxes as the mount just finished slewing if (m_wasSlewStarted && Options::astrometryAutoUpdatePosition()) { qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_OK --> Auto Update Position..."; opsAstrometry->estRA->setText(ra_dms); opsAstrometry->estDec->setText(dec_dms); Options::setAstrometryPositionRA(nvp->np[0].value * 15); Options::setAstrometryPositionDE(nvp->np[1].value); generateArgs(); } // If dome is syncing, wait until it stops if (currentDome && currentDome->isMoving()) { domeReady = false; return; } // If we are looking for celestial pole if (m_wasSlewStarted && pahStage == PAH_FIND_CP) { //qCDebug(KSTARS_EKOS_ALIGN) << "## PAH_FIND_CP--> setting slewStarted to FALSE"; m_wasSlewStarted = false; appendLogText(i18n("Mount completed slewing near celestial pole. Capture again to verify.")); setSolverAction(GOTO_NOTHING); pahStage = PAH_FIRST_CAPTURE; emit newPAHStage(pahStage); return; } switch (state) { case ALIGN_PROGRESS: break; case ALIGN_SYNCING: { m_wasSlewStarted = false; qCDebug(KSTARS_EKOS_ALIGN) << "## ALIGN_SYNCING --> setting slewStarted to FALSE"; if (currentGotoMode == GOTO_SLEW) { Slew(); return; } else { appendLogText(i18n("Mount is synced to solution coordinates. Astrometric solver is successful.")); KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully")); state = ALIGN_COMPLETE; emit newStatus(state); solverIterations = 0; if (mountModelRunning) finishAlignmentPoint(true); } } break; case ALIGN_SLEWING: if (m_wasSlewStarted == false) { // If mount has not started slewing yet, then skip qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew planned, but not started slewing yet..."; break; } qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew completed."; m_wasSlewStarted = false; if (loadSlewState == IPS_BUSY) { loadSlewState = IPS_IDLE; qCDebug(KSTARS_EKOS_ALIGN) << "loadSlewState is IDLE."; state = ALIGN_PROGRESS; emit newStatus(state); if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); m_CaptureTimer.start(delaySpin->value()); return; } else if (differentialSlewingActivated) { appendLogText(i18n("Differential slewing complete. Astrometric solver is successful.")); KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully")); state = ALIGN_COMPLETE; emit newStatus(state); solverIterations = 0; if (mountModelRunning) finishAlignmentPoint(true); } else if (currentGotoMode == GOTO_SLEW || mountModelRunning) { if (targetAccuracyNotMet) appendLogText(i18n("Slew complete. Target accuracy is not met, running solver again...")); else appendLogText(i18n("Slew complete. Solving Alignment Point. . .")); targetAccuracyNotMet = false; state = ALIGN_PROGRESS; emit newStatus(state); if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); m_CaptureTimer.start(delaySpin->value()); return; } break; default: { //qCDebug(KSTARS_EKOS_ALIGN) << "## Align State " << state << "--> setting slewStarted to FALSE"; m_wasSlewStarted = false; } break; } } break; // Busy --> Mount Slewing or Moving (NSWE buttons) case IPS_BUSY: { qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew running."; m_wasSlewStarted = true; handleMountMotion(); } break; // Alert --> Mount has problem moving or communicating. case IPS_ALERT: { qCDebug(KSTARS_EKOS_ALIGN) << "IPS_ALERT --> setting slewStarted to FALSE"; m_wasSlewStarted = false; if (state == ALIGN_SYNCING || state == ALIGN_SLEWING) { if (state == ALIGN_SYNCING) appendLogText(i18n("Syncing failed.")); else appendLogText(i18n("Slewing failed.")); if (++m_SlewErrorCounter == 3) { abort(); return; } else { if (currentGotoMode == GOTO_SLEW) Slew(); else Sync(); } } return; } } if (pahStage == PAH_FIRST_ROTATE) { // only wait for telescope to slew to new position if manual slewing is switched off if(!PAHManual->isChecked()) { double deltaAngle = fabs(telescopeCoord.ra().deltaAngle(targetPAH.ra()).Degrees()); qCDebug(KSTARS_EKOS_ALIGN) << "First mount rotation remaining degrees:" << deltaAngle; if (deltaAngle <= PAH_ROTATION_THRESHOLD) { currentTelescope->StopWE(); appendLogText(i18n("Mount first rotation is complete.")); pahStage = PAH_SECOND_CAPTURE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHSecondCapturePage); emit newPAHMessage(secondCaptureText->text()); if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); m_CaptureTimer.start(delaySpin->value()); } // If for some reason we didn't stop, let's stop if we get too far else if (deltaAngle > PAHRotationSpin->value() * 1.25) { currentTelescope->Abort(); appendLogText(i18n("Mount aborted. Please restart the process and reduce the speed.")); stopPAHProcess(); } return; } // endif not manual slew } else if (pahStage == PAH_SECOND_ROTATE) { // only wait for telescope to slew to new position if manual slewing is switched off if(!PAHManual->isChecked()) { double deltaAngle = fabs(telescopeCoord.ra().deltaAngle(targetPAH.ra()).Degrees()); qCDebug(KSTARS_EKOS_ALIGN) << "Second mount rotation remaining degrees:" << deltaAngle; if (deltaAngle <= PAH_ROTATION_THRESHOLD) { currentTelescope->StopWE(); appendLogText(i18n("Mount second rotation is complete.")); pahStage = PAH_THIRD_CAPTURE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHThirdCapturePage); emit newPAHMessage(thirdCaptureText->text()); if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); m_CaptureTimer.start(delaySpin->value()); } // If for some reason we didn't stop, let's stop if we get too far else if (deltaAngle > PAHRotationSpin->value() * 1.25) { currentTelescope->Abort(); appendLogText(i18n("Mount aborted. Please restart the process and reduce the speed.")); stopPAHProcess(); } return; } // endif not manual slew } switch (azStage) { case AZ_SYNCING: if (currentTelescope->isSlewing()) azStage = AZ_SLEWING; break; case AZ_SLEWING: if (currentTelescope->isSlewing() == false) { azStage = AZ_SECOND_TARGET; measureAzError(); } break; case AZ_CORRECTING: if (currentTelescope->isSlewing() == false) { appendLogText(i18n( "Slew complete. Please adjust azimuth knob until the target is in the center of the view.")); azStage = AZ_INIT; } break; default: break; } switch (altStage) { case ALT_SYNCING: if (currentTelescope->isSlewing()) altStage = ALT_SLEWING; break; case ALT_SLEWING: if (currentTelescope->isSlewing() == false) { altStage = ALT_SECOND_TARGET; measureAltError(); } break; case ALT_CORRECTING: if (currentTelescope->isSlewing() == false) { appendLogText(i18n( "Slew complete. Please adjust altitude knob until the target is in the center of the view.")); altStage = ALT_INIT; } break; default: break; } } else if (!strcmp(nvp->name, "ABS_ROTATOR_ANGLE")) { // PA = RawAngle * Multiplier + Offset currentRotatorPA = (nvp->np[0].value * Options::pAMultiplier()) + Options::pAOffset(); if (currentRotatorPA > 180) currentRotatorPA -= 360; if (currentRotatorPA < -180) currentRotatorPA += 360; if (std::isnan(loadSlewTargetPA) == false && fabs(currentRotatorPA - loadSlewTargetPA) * 60 <= Options::astrometryRotatorThreshold()) { appendLogText(i18n("Rotator reached target position angle.")); targetAccuracyNotMet = true; loadSlewTargetPA = std::numeric_limits::quiet_NaN(); QTimer::singleShot(Options::settlingTime(), this, &Ekos::Align::executeGOTO); } } // N.B. Ekos::Manager already manages TELESCOPE_INFO, why here again? //if (!strcmp(coord->name, "TELESCOPE_INFO")) //syncTelescopeInfo(); } void Align::handleMountMotion() { if (state == ALIGN_PROGRESS) { // whoops, mount slews during alignment appendLogText(i18n("Slew detected, aborting solving...")); abort(); // reset the state to busy so that solving restarts after slewing finishes loadSlewState = IPS_BUSY; // if mount model is running, retry the current alignment point if (mountModelRunning) { appendLogText(i18n("Restarting alignment point %1", currentAlignmentPoint + 1)); if (currentAlignmentPoint > 0) currentAlignmentPoint--; } state = ALIGN_SLEWING; } } void Align::handleMountStatus() { INumberVectorProperty *nvp = nullptr; if (currentTelescope->isJ2000()) nvp = currentTelescope->getBaseDevice()->getNumber("EQUATORIAL_COORD"); else nvp = currentTelescope->getBaseDevice()->getNumber("EQUATORIAL_EOD_COORD"); if (nvp) processNumber(nvp); } void Align::executeGOTO() { if (loadSlewState == IPS_BUSY) { targetCoord = alignCoord; SlewToTarget(); } else if (currentGotoMode == GOTO_SYNC) Sync(); else if (currentGotoMode == GOTO_SLEW) SlewToTarget(); } void Align::Sync() { state = ALIGN_SYNCING; if (currentTelescope->Sync(&alignCoord)) { emit newStatus(state); appendLogText( i18n("Syncing to RA (%1) DEC (%2)", alignCoord.ra().toHMSString(), alignCoord.dec().toDMSString())); } else { state = ALIGN_IDLE; emit newStatus(state); appendLogText(i18n("Syncing failed.")); } } void Align::Slew() { state = ALIGN_SLEWING; emit newStatus(state); //qCDebug(KSTARS_EKOS_ALIGN) << "## Before SLEW command: wasSlewStarted -->" << m_wasSlewStarted; //m_wasSlewStarted = currentTelescope->Slew(&targetCoord); //qCDebug(KSTARS_EKOS_ALIGN) << "## After SLEW command: wasSlewStarted -->" << m_wasSlewStarted; // JM 2019-08-23: Do not assume that slew was started immediately. Wait until IPS_BUSY state is triggered // from Goto currentTelescope->Slew(&targetCoord); appendLogText(i18n("Slewing to target coordinates: RA (%1) DEC (%2).", targetCoord.ra().toHMSString(), targetCoord.dec().toDMSString())); } void Align::SlewToTarget() { if (canSync && loadSlewState == IPS_IDLE) { // 2018-01-24 JM: This is ugly. Maybe use DBus? Signal/Slots? Ekos Manager usage like this should be avoided if (KStars::Instance()->ekosManager() && !KStars::Instance()->ekosManager()->getCurrentJobName().isEmpty()) { KSNotification::event(QLatin1String("EkosSchedulerTelescopeSynced"), i18n("Ekos job (%1) - Telescope synced", KStars::Instance()->ekosManager()->getCurrentJobName())); } // Do we perform a regular sync or use differential slewing? if (Options::astrometryDifferentialSlewing()) { dms raDiff = alignCoord.ra().deltaAngle(targetCoord.ra()); dms deDiff = alignCoord.dec().deltaAngle(targetCoord.dec()); targetCoord.setRA(targetCoord.ra() - raDiff); targetCoord.setDec(targetCoord.dec() - deDiff); differentialSlewingActivated = true; qCDebug(KSTARS_EKOS_ALIGN) << "Using differential slewing..."; Slew(); } else Sync(); return; } Slew(); } void Align::executePolarAlign() { appendLogText(i18n("Processing solution for polar alignment...")); switch (azStage) { case AZ_FIRST_TARGET: case AZ_FINISHED: measureAzError(); break; default: break; } switch (altStage) { case ALT_FIRST_TARGET: case ALT_FINISHED: measureAltError(); break; default: break; } } void Align::measureAzError() { static double initRA = 0, initDEC = 0, finalRA = 0, finalDEC = 0, initAz = 0; if (pahStage != PAH_IDLE && (KMessageBox::warningContinueCancel(KStars::Instance(), i18n("Polar Alignment Helper is still active. Do you want to continue " "using legacy polar alignment tool?")) != KMessageBox::Continue)) return; pahStage = PAH_IDLE; emit newPAHStage(pahStage); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Measuring Azimuth Error..."; switch (azStage) { case AZ_INIT: // Display message box confirming user point scope near meridian and south // N.B. This action cannot be automated. if (KMessageBox::warningContinueCancel( nullptr, hemisphere == NORTH_HEMISPHERE ? i18n("Point the telescope at the southern meridian. Press Continue when ready.") : i18n("Point the telescope at the northern meridian. Press Continue when ready."), i18n("Polar Alignment Measurement"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "ekos_measure_az_error") != KMessageBox::Continue) return; appendLogText(i18n("Solving first frame near the meridian.")); azStage = AZ_FIRST_TARGET; captureAndSolve(); break; case AZ_FIRST_TARGET: // start solving there, find RA/DEC initRA = alignCoord.ra().Degrees(); initDEC = alignCoord.dec().Degrees(); initAz = alignCoord.az().Degrees(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar initRA " << alignCoord.ra().toHMSString() << " initDEC " << alignCoord.dec().toDMSString() << " initlAz " << alignCoord.az().toDMSString() << " initAlt " << alignCoord.alt().toDMSString(); // Now move 30 arcminutes in RA if (canSync) { azStage = AZ_SYNCING; currentTelescope->Sync(initRA / 15.0, initDEC); currentTelescope->Slew((initRA - RAMotion) / 15.0, initDEC); } // If telescope doesn't sync, we slew relative to its current coordinates else { azStage = AZ_SLEWING; currentTelescope->Slew(telescopeCoord.ra().Hours() - RAMotion / 15.0, telescopeCoord.dec().Degrees()); } appendLogText(i18n("Slewing 30 arcminutes in RA...")); break; case AZ_SECOND_TARGET: // We reached second target now // Let now solver for RA/DEC appendLogText(i18n("Solving second frame near the meridian.")); azStage = AZ_FINISHED; captureAndSolve(); break; case AZ_FINISHED: // Measure deviation in DEC // Call function to report error // set stage to AZ_FIRST_TARGET again appendLogText(i18n("Calculating azimuth alignment error...")); finalRA = alignCoord.ra().Degrees(); finalDEC = alignCoord.dec().Degrees(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar finalRA " << alignCoord.ra().toHMSString() << " finalDEC " << alignCoord.dec().toDMSString() << " finalAz " << alignCoord.az().toDMSString() << " finalAlt " << alignCoord.alt().toDMSString(); // Slew back to original position if (canSync) currentTelescope->Slew(initRA / 15.0, initDEC); else { currentTelescope->Slew(telescopeCoord.ra().Hours() + RAMotion / 15.0, telescopeCoord.dec().Degrees()); } appendLogText(i18n("Slewing back to original position...")); calculatePolarError(initRA, initDEC, finalRA, finalDEC, initAz); azStage = AZ_INIT; break; default: break; } } void Align::measureAltError() { static double initRA = 0, initDEC = 0, finalRA = 0, finalDEC = 0, initAz = 0; if (pahStage != PAH_IDLE && (KMessageBox::warningContinueCancel(KStars::Instance(), i18n("Polar Alignment Helper is still active. Do you want to continue " "using legacy polar alignment tool?")) != KMessageBox::Continue)) return; pahStage = PAH_IDLE; emit newPAHStage(pahStage); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Measuring Altitude Error..."; switch (altStage) { case ALT_INIT: // Display message box confirming user point scope near meridian and south // N.B. This action cannot be automated. if (KMessageBox::warningContinueCancel(nullptr, i18n("Point the telescope to the eastern or western horizon with a " "minimum altitude of 20 degrees. Press continue when ready."), i18n("Polar Alignment Measurement"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "ekos_measure_alt_error") != KMessageBox::Continue) return; appendLogText(i18n("Solving first frame.")); altStage = ALT_FIRST_TARGET; if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); m_CaptureTimer.start(delaySpin->value()); break; case ALT_FIRST_TARGET: // start solving there, find RA/DEC initRA = alignCoord.ra().Degrees(); initDEC = alignCoord.dec().Degrees(); initAz = alignCoord.az().Degrees(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar initRA " << alignCoord.ra().toHMSString() << " initDEC " << alignCoord.dec().toDMSString() << " initlAz " << alignCoord.az().toDMSString() << " initAlt " << alignCoord.alt().toDMSString(); // Now move 30 arcminutes in RA if (canSync) { altStage = ALT_SYNCING; currentTelescope->Sync(initRA / 15.0, initDEC); currentTelescope->Slew((initRA - RAMotion) / 15.0, initDEC); } // If telescope doesn't sync, we slew relative to its current coordinates else { altStage = ALT_SLEWING; currentTelescope->Slew(telescopeCoord.ra().Hours() - RAMotion / 15.0, telescopeCoord.dec().Degrees()); } appendLogText(i18n("Slewing 30 arcminutes in RA...")); break; case ALT_SECOND_TARGET: // We reached second target now // Let now solver for RA/DEC appendLogText(i18n("Solving second frame.")); altStage = ALT_FINISHED; if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); m_CaptureTimer.start(delaySpin->value()); break; case ALT_FINISHED: // Measure deviation in DEC // Call function to report error appendLogText(i18n("Calculating altitude alignment error...")); finalRA = alignCoord.ra().Degrees(); finalDEC = alignCoord.dec().Degrees(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar finalRA " << alignCoord.ra().toHMSString() << " finalDEC " << alignCoord.dec().toDMSString() << " finalAz " << alignCoord.az().toDMSString() << " finalAlt " << alignCoord.alt().toDMSString(); // Slew back to original position if (canSync) currentTelescope->Slew(initRA / 15.0, initDEC); // If telescope doesn't sync, we slew relative to its current coordinates else { currentTelescope->Slew(telescopeCoord.ra().Hours() + RAMotion / 15.0, telescopeCoord.dec().Degrees()); } appendLogText(i18n("Slewing back to original position...")); calculatePolarError(initRA, initDEC, finalRA, finalDEC, initAz); altStage = ALT_INIT; break; default: break; } } void Align::calculatePolarError(double initRA, double initDEC, double finalRA, double finalDEC, double initAz) { double raMotion = finalRA - initRA; decDeviation = finalDEC - initDEC; // East/West of meridian int horizon = (initAz > 0 && initAz <= 180) ? 0 : 1; // How much time passed siderrally form initRA to finalRA? //double RATime = fabs(raMotion / SIDRATE) / 60.0; // 2016-03-30: Diff in RA is sufficient for time difference // raMotion in degrees. RATime in minutes. double RATime = fabs(raMotion) * 60.0; // Equation by Frank Berret (Measuring Polar Axis Alignment Error, page 4) // In degrees double deviation = (3.81 * (decDeviation * 3600)) / (RATime * cos(initDEC * dms::DegToRad)) / 60.0; dms devDMS(fabs(deviation)); KLocalizedString deviationDirection; switch (hemisphere) { // Northern hemisphere case NORTH_HEMISPHERE: if (azStage == AZ_FINISHED) { if (decDeviation > 0) deviationDirection = ki18n("%1 too far east"); else deviationDirection = ki18n("%1 too far west"); } else if (altStage == ALT_FINISHED) { switch (horizon) { // East case 0: if (decDeviation > 0) deviationDirection = ki18n("%1 too far high"); else deviationDirection = ki18n("%1 too far low"); break; // West case 1: if (decDeviation > 0) deviationDirection = ki18n("%1 too far low"); else deviationDirection = ki18n("%1 too far high"); break; default: break; } } break; // Southern hemisphere case SOUTH_HEMISPHERE: if (azStage == AZ_FINISHED) { if (decDeviation > 0) deviationDirection = ki18n("%1 too far west"); else deviationDirection = ki18n("%1 too far east"); } else if (altStage == ALT_FINISHED) { switch (horizon) { // East case 0: if (decDeviation > 0) deviationDirection = ki18n("%1 too far low"); else deviationDirection = ki18n("%1 too far high"); break; // West case 1: if (decDeviation > 0) deviationDirection = ki18n("%1 too far high"); else deviationDirection = ki18n("%1 too far low"); break; default: break; } } break; } qCDebug(KSTARS_EKOS_ALIGN) << "Polar Hemisphere is " << ((hemisphere == NORTH_HEMISPHERE) ? "North" : "South") << " --- initAz " << initAz; qCDebug(KSTARS_EKOS_ALIGN) << "Polar initRA " << initRA << " initDEC " << initDEC << " finalRA " << finalRA << " finalDEC " << finalDEC; qCDebug(KSTARS_EKOS_ALIGN) << "Polar decDeviation " << decDeviation * 3600 << " arcsec " << " RATime " << RATime << " minutes"; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Raw Deviation " << deviation << " degrees."; if (azStage == AZ_FINISHED) { azError->setText(deviationDirection.subs(QString("%1").arg(devDMS.toDMSString())).toString()); azDeviation = deviation * (decDeviation > 0 ? 1 : -1); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Azimuth Deviation " << azDeviation << " degrees."; correctAzB->setEnabled(true); } if (altStage == ALT_FINISHED) { altError->setText(deviationDirection.subs(QString("%1").arg(devDMS.toDMSString())).toString()); altDeviation = deviation * (decDeviation > 0 ? 1 : -1); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Altitude Deviation " << altDeviation << " degrees."; correctAltB->setEnabled(true); } } void Align::correctAltError() { double newRA, newDEC; SkyPoint currentCoord(telescopeCoord); dms targetLat; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Correcting Altitude Error..."; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Current Mount RA " << currentCoord.ra().toHMSString() << " DEC " << currentCoord.dec().toDMSString() << "Az " << currentCoord.az().toDMSString() << " Alt " << currentCoord.alt().toDMSString(); // An error in polar alignment altitude reflects a deviation in the latitude of the mount from actual latitude of the site // Calculating the latitude accounting for the altitude deviation. This is the latitude at which the altitude deviation should be zero. targetLat.setD(KStars::Instance()->data()->geo()->lat()->Degrees() + altDeviation); // Calculate the Az/Alt of the mount if it were located at the corrected latitude currentCoord.EquatorialToHorizontal(KStars::Instance()->data()->lst(), &targetLat); // Convert corrected Az/Alt to RA/DEC given the local sideral time and current (not corrected) latitude currentCoord.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); // New RA/DEC should reflect the position in the sky at which the polar alignment altitude error is minimal. newRA = currentCoord.ra().Hours(); newDEC = currentCoord.dec().Degrees(); altStage = ALT_CORRECTING; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Target Latitude = Latitude " << KStars::Instance()->data()->geo()->lat()->Degrees() << " + Altitude Deviation " << altDeviation << " = " << targetLat.Degrees(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Slewing to calibration position..."; currentTelescope->Slew(newRA, newDEC); appendLogText(i18n("Slewing to calibration position, please wait until telescope completes slewing.")); } void Align::correctAzError() { double newRA, newDEC, currentAlt, currentAz; SkyPoint currentCoord(telescopeCoord); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Correcting Azimuth Error..."; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Current Mount RA " << currentCoord.ra().toHMSString() << " DEC " << currentCoord.dec().toDMSString() << "Az " << currentCoord.az().toDMSString() << " Alt " << currentCoord.alt().toDMSString(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Target Azimuth = Current Azimuth " << currentCoord.az().Degrees() << " + Azimuth Deviation " << azDeviation << " = " << currentCoord.az().Degrees() + azDeviation; // Get current horizontal coordinates of the mount currentCoord.EquatorialToHorizontal(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); // Keep Altitude as it is and change Azimuth to account for the azimuth deviation // The new sky position should be where the polar alignment azimuth error is minimal currentAlt = currentCoord.alt().Degrees(); currentAz = currentCoord.az().Degrees() + azDeviation; // Update current Alt and Azimuth to new values currentCoord.setAlt(currentAlt); currentCoord.setAz(currentAz); // Convert Alt/Az back to equatorial coordinates currentCoord.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); // Get new RA and DEC newRA = currentCoord.ra().Hours(); newDEC = currentCoord.dec().Degrees(); azStage = AZ_CORRECTING; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Slewing to calibration position..."; currentTelescope->Slew(newRA, newDEC); appendLogText(i18n("Slewing to calibration position, please wait until telescope completes slewing.")); } void Align::getFormattedCoords(double ra, double dec, QString &ra_str, QString &dec_str) { dms ra_s, dec_s; ra_s.setH(ra); dec_s.setD(dec); ra_str = QString("%1:%2:%3") .arg(ra_s.hour(), 2, 10, QChar('0')) .arg(ra_s.minute(), 2, 10, QChar('0')) .arg(ra_s.second(), 2, 10, QChar('0')); if (dec_s.Degrees() < 0) dec_str = QString("-%1:%2:%3") .arg(abs(dec_s.degree()), 2, 10, QChar('0')) .arg(abs(dec_s.arcmin()), 2, 10, QChar('0')) .arg(dec_s.arcsec(), 2, 10, QChar('0')); else dec_str = QString("%1:%2:%3") .arg(dec_s.degree(), 2, 10, QChar('0')) .arg(dec_s.arcmin(), 2, 10, QChar('0')) .arg(dec_s.arcsec(), 2, 10, QChar('0')); } bool Align::loadAndSlew(QString fileURL) { #ifdef Q_OS_OSX if(solverBackendGroup->checkedId() == SOLVER_OFFLINE) { if(Options::useDefaultPython()) { if( !opsAlign->astropyInstalled() || !opsAlign->pythonInstalled() ) { KSNotification::error(i18n("Astrometry.net uses python3 and the astropy package for plate solving images offline. These were not detected on your system. Please go into the Align Options and either click the setup button to install them or uncheck the default button and enter the path to python3 on your system and manually install astropy.")); return false; } } } #endif if (fileURL.isEmpty()) fileURL = QFileDialog::getOpenFileName(KStars::Instance(), i18n("Load Image"), dirPath, "Images (*.fits *.fit *.jpg *.jpeg)"); if (fileURL.isEmpty()) return false; QFileInfo fileInfo(fileURL); dirPath = fileInfo.absolutePath(); differentialSlewingActivated = false; loadSlewState = IPS_BUSY; stopPAHProcess(); slewR->setChecked(true); currentGotoMode = GOTO_SLEW; solveB->setEnabled(false); stopB->setEnabled(true); pi->startAnimation(); startSolving(fileURL, false); return true; } void Align::setExposure(double value) { exposureIN->setValue(value); } void Align::setBinningIndex(int binIndex) { syncSettings(); Options::setSolverBinningIndex(binIndex); // If sender is not our combo box, then we need to update the combobox itself if (dynamic_cast(sender()) != binningCombo) { binningCombo->blockSignals(true); binningCombo->setCurrentIndex(binIndex); binningCombo->blockSignals(false); } // Need to calculate FOV and args for APP if (Options::astrometryImageScaleUnits() == OpsAstrometry::SCALE_ARCSECPERPIX) { calculateFOV(); generateArgs(); } } void Align::setSolverArguments(const QString &value) { solverOptions->setText(value); } QString Align::solverArguments() { return solverOptions->text(); } void Align::setFOVTelescopeType(int index) { FOVScopeCombo->setCurrentIndex(index); } FOV *Align::getSolverFOV() { if (sOrientation == -1) return nullptr; else return solverFOV.get(); } void Align::addFilter(ISD::GDInterface *newFilter) { for (auto filter : Filters) { if (!strcmp(filter->getDeviceName(), newFilter->getDeviceName())) return; } FilterCaptureLabel->setEnabled(true); FilterDevicesCombo->setEnabled(true); FilterPosLabel->setEnabled(true); FilterPosCombo->setEnabled(true); FilterDevicesCombo->addItem(newFilter->getDeviceName()); Filters.append(static_cast(newFilter)); checkFilter(1); FilterDevicesCombo->setCurrentIndex(1); } bool Align::setFilterWheel(const QString &device) { bool deviceFound = false; for (int i = 1; i < FilterDevicesCombo->count(); i++) if (device == FilterDevicesCombo->itemText(i)) { checkFilter(i); deviceFound = true; break; } if (deviceFound == false) return false; return true; } QString Align::filterWheel() { if (FilterDevicesCombo->currentIndex() >= 1) return FilterDevicesCombo->currentText(); return QString(); } bool Align::setFilter(const QString &filter) { if (FilterDevicesCombo->currentIndex() >= 1) { FilterPosCombo->setCurrentText(filter); return true; } return false; } QString Align::filter() { return FilterPosCombo->currentText(); } void Align::checkFilter(int filterNum) { if (filterNum == -1) { filterNum = FilterDevicesCombo->currentIndex(); if (filterNum == -1) return; } // "--" is no filter if (filterNum == 0) { currentFilter = nullptr; currentFilterPosition = -1; FilterPosCombo->clear(); return; } if (filterNum <= Filters.count()) currentFilter = Filters.at(filterNum - 1); FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); currentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(Options::lockAlignFilterIndex()); syncSettings(); } void Align::setWCSEnabled(bool enable) { if (currentCCD == nullptr) return; ISwitchVectorProperty *wcsControl = currentCCD->getBaseDevice()->getSwitch("WCS_CONTROL"); ISwitch *wcs_enable = IUFindSwitch(wcsControl, "WCS_ENABLE"); ISwitch *wcs_disable = IUFindSwitch(wcsControl, "WCS_DISABLE"); if (!wcs_enable || !wcs_disable) return; if ((wcs_enable->s == ISS_ON && enable) || (wcs_disable->s == ISS_ON && !enable)) return; IUResetSwitch(wcsControl); if (enable) { appendLogText(i18n("World Coordinate System (WCS) is enabled. CCD rotation must be set either manually in the " "CCD driver or by solving an image before proceeding to capture any further images, " "otherwise the WCS information may be invalid.")); wcs_enable->s = ISS_ON; } else { wcs_disable->s = ISS_ON; m_wcsSynced = false; appendLogText(i18n("World Coordinate System (WCS) is disabled.")); } ClientManager *clientManager = currentCCD->getDriverInfo()->getClientManager(); clientManager->sendNewSwitch(wcsControl); } void Align::checkCCDExposureProgress(ISD::CCDChip *targetChip, double remaining, IPState state) { INDI_UNUSED(targetChip); INDI_UNUSED(remaining); if (state == IPS_ALERT) { if (++m_CaptureErrorCounter == 3 && pahStage != PAH_REFRESH) { appendLogText(i18n("Capture error. Aborting...")); abort(); return; } appendLogText(i18n("Restarting capture attempt #%1", m_CaptureErrorCounter)); int currentRow = solutionTable->rowCount() - 1; solutionTable->setCellWidget(currentRow, 3, new QWidget()); QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setIcon(QIcon(":/icons/AlignFailure.svg")); statusReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 3, statusReport); captureAndSolve(); } } void Align::setFocusStatus(Ekos::FocusState state) { focusState = state; } QStringList Align::getSolverOptionsFromFITS(const QString &filename) { QVariantMap optionsMap; // For ASTAP, we just default settings if (solverBackendGroup->checkedId() == SOLVER_ASTAP) { if (Options::aSTAPSearchRadius()) optionsMap["radius"] = Options::aSTAPSearchRadiusValue(); if (Options::aSTAPDownSample() && Options::aSTAPDownSampleValue() > 0) optionsMap["downsample"] = Options::aSTAPDownSampleValue(); optionsMap["speed"] = Options::aSTAPLargeSearchWindow() ? "slow" : "auto"; if (Options::aSTAPUpdateFITS()) optionsMap["update"] = true; return generateOptions(optionsMap, solverBackendGroup->checkedId()); } int status = 0, fits_ccd_width, fits_ccd_height, fits_binx = 1, fits_biny = 1; char comment[128], error_status[512]; fitsfile *fptr = nullptr; double ra = 0, dec = 0, fits_fov_x, fits_fov_y, fov_lower, fov_upper, fits_ccd_hor_pixel = -1, fits_ccd_ver_pixel = -1, fits_focal_length = -1; QString fov_low, fov_high; QStringList solver_args; if (Options::astrometryUseNoVerify()) optionsMap["noverify"] = true; if (Options::astrometryUseResort()) optionsMap["resort"] = true; if (Options::astrometryUseNoFITS2FITS()) optionsMap["nofits2fits"] = true; if (Options::astrometryUseDownsample()) optionsMap["downsample"] = Options::astrometryDownsample(); if (Options::astrometryCustomOptions().isEmpty() == false) optionsMap["custom"] = Options::astrometryCustomOptions(); solver_args = generateOptions(optionsMap, solverBackendGroup->checkedId()); status = 0; // Use open diskfile as it does not use extended file names which has problems opening // files with [ ] or ( ) in their names. if (fits_open_diskfile(&fptr, filename.toLatin1(), READONLY, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); qCCritical(KSTARS_EKOS_ALIGN) << QString::fromUtf8(error_status); return solver_args; } status = 0; if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); qCCritical(KSTARS_EKOS_ALIGN) << QString::fromUtf8(error_status); return solver_args; } status = 0; if (fits_read_key(fptr, TINT, "NAXIS1", &fits_ccd_width, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find NAXIS1.")); return solver_args; } status = 0; if (fits_read_key(fptr, TINT, "NAXIS2", &fits_ccd_height, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find NAXIS2.")); return solver_args; } // If we need to auto downsample, let us figure out the scale and regenerate options if (Options::astrometryAutoDownsample()) { optionsMap["downsample"] = getSolverDownsample(fits_ccd_width); solver_args = generateOptions(optionsMap, SOLVER_ASTROMETRYNET); } bool coord_ok = true; status = 0; char objectra_str[32]; if (fits_read_key(fptr, TSTRING, "OBJCTRA", objectra_str, comment, &status)) { if (fits_read_key(fptr, TDOUBLE, "RA", &ra, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); coord_ok = false; appendLogText(i18n("FITS header: cannot find OBJCTRA (%1).", QString(error_status))); } else // Degrees to hours ra /= 15; } else { dms raDMS = dms::fromString(objectra_str, false); ra = raDMS.Hours(); } status = 0; char objectde_str[32]; if (coord_ok && fits_read_key(fptr, TSTRING, "OBJCTDEC", objectde_str, comment, &status)) { if (fits_read_key(fptr, TDOUBLE, "DEC", &dec, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); coord_ok = false; appendLogText(i18n("FITS header: cannot find OBJCTDEC (%1).", QString(error_status))); } } else { dms deDMS = dms::fromString(objectde_str, true); dec = deDMS.Degrees(); } if (coord_ok && Options::astrometryUsePosition()) solver_args << "-3" << QString::number(ra * 15.0) << "-4" << QString::number(dec) << "-5" << "15"; status = 0; double pixelScale = 0; // If we have pixel scale in arcsecs per pixel then lets use that directly // instead of calculating it from FOCAL length and other information if (fits_read_key(fptr, TDOUBLE, "SCALE", &pixelScale, comment, &status) == 0) { fov_low = QString::number(0.9 * pixelScale); fov_high = QString::number(1.1 * pixelScale); if (Options::astrometryUseImageScale()) solver_args << "-L" << fov_low << "-H" << fov_high << "-u" << "app"; return solver_args; } if (fits_read_key(fptr, TDOUBLE, "FOCALLEN", &fits_focal_length, comment, &status)) { int integer_focal_length = -1; if (fits_read_key(fptr, TINT, "FOCALLEN", &integer_focal_length, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find FOCALLEN (%1).", QString(error_status))); return solver_args; } else fits_focal_length = integer_focal_length; } status = 0; if (fits_read_key(fptr, TDOUBLE, "PIXSIZE1", &fits_ccd_hor_pixel, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find PIXSIZE1 (%1).", QString(error_status))); return solver_args; } status = 0; if (fits_read_key(fptr, TDOUBLE, "PIXSIZE2", &fits_ccd_ver_pixel, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find PIXSIZE2 (%1).", QString(error_status))); return solver_args; } status = 0; fits_read_key(fptr, TINT, "XBINNING", &fits_binx, comment, &status); status = 0; fits_read_key(fptr, TINT, "YBINNING", &fits_biny, comment, &status); // Calculate FOV fits_fov_x = 206264.8062470963552 * fits_ccd_width * fits_ccd_hor_pixel / 1000.0 / fits_focal_length * fits_binx; fits_fov_y = 206264.8062470963552 * fits_ccd_height * fits_ccd_ver_pixel / 1000.0 / fits_focal_length * fits_biny; fits_fov_x /= 60.0; fits_fov_y /= 60.0; // let's stretch the boundaries by 10% fov_lower = qMin(fits_fov_x, fits_fov_y); fov_upper = qMax(fits_fov_x, fits_fov_y); fov_lower *= 0.90; fov_upper *= 1.10; fov_low = QString::number(fov_lower); fov_high = QString::number(fov_upper); if (Options::astrometryUseImageScale()) solver_args << "-L" << fov_low << "-H" << fov_high << "-u" << "aw"; return solver_args; } uint8_t Align::getSolverDownsample(uint16_t binnedW) { uint8_t downsample = Options::astrometryDownsample(); if (!Options::astrometryAutoDownsample()) return downsample; while (downsample < 8) { if (binnedW / downsample <= 1024) break; downsample += 2; } return downsample; } void Align::saveSettleTime() { Options::setSettlingTime(delaySpin->value()); } void Align::setCaptureStatus(CaptureState newState) { switch (newState) { case CAPTURE_ALIGNING: if (currentTelescope && currentTelescope->hasAlignmentModel() && Options::resetMountModelAfterMeridian()) { mountModelReset = currentTelescope->clearAlignmentModel(); qCDebug(KSTARS_EKOS_ALIGN) << "Post meridian flip mount model reset" << (mountModelReset ? "successful." : "failed."); } m_CaptureTimer.start(Options::settlingTime()); break; default: break; } } void Align::showFITSViewer() { FITSData *data = alignView->getImageData(); if (data) { QUrl url = QUrl::fromLocalFile(data->filename()); if (fv.isNull()) { if (Options::singleWindowCapturedFITS()) fv = KStars::Instance()->genericFITSViewer(); else { fv = new FITSViewer(Options::independentWindowFITS() ? nullptr : KStars::Instance()); KStars::Instance()->addFITSViewer(fv); } fv->addFITS(url); FITSView *currentView = fv->getCurrentView(); if (currentView) currentView->getImageData()->setAutoRemoveTemporaryFITS(false); } else fv->updateFITS(url, 0); fv->show(); } } void Align::toggleAlignWidgetFullScreen() { if (alignWidget->parent() == nullptr) { alignWidget->setParent(this); rightLayout->insertWidget(0, alignWidget); alignWidget->showNormal(); } else { alignWidget->setParent(nullptr); alignWidget->setWindowTitle(i18n("Align Frame")); alignWidget->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint); alignWidget->showMaximized(); alignWidget->show(); } } void Align::startPAHProcess() { qCInfo(KSTARS_EKOS_ALIGN) << "Starting Polar Alignment Assistant process..."; pahStage = PAH_FIRST_CAPTURE; emit newPAHStage(pahStage); nothingR->setChecked(true); currentGotoMode = GOTO_NOTHING; loadSlewB->setEnabled(false); rememberSolverWCS = Options::astrometrySolverWCS(); rememberAutoWCS = Options::autoWCS(); Options::setAutoWCS(false); Options::setAstrometrySolverWCS(true); if (Options::limitedResourcesMode()) appendLogText(i18n("Warning: Equatorial Grid Lines will not be drawn due to limited resources mode.")); if (currentTelescope->hasAlignmentModel()) { appendLogText(i18n("Clearing mount Alignment Model...")); mountModelReset = currentTelescope->clearAlignmentModel(); } // Unpark currentTelescope->UnPark(); // Set tracking ON if not already if (currentTelescope->canControlTrack() && currentTelescope->isTracking() == false) currentTelescope->setTrackEnabled(true); PAHStartB->setEnabled(false); PAHStopB->setEnabled(true); PAHWidgets->setCurrentWidget(PAHFirstCapturePage); emit newPAHMessage(firstCaptureText->text()); captureAndSolve(); } void Align::stopPAHProcess() { if (pahStage == PAH_IDLE) return; qCInfo(KSTARS_EKOS_ALIGN) << "Stopping Polar Alignment Assistant process..."; // Only display dialog if user explicitly restarts if ((static_cast(sender()) == PAHStopB) && KMessageBox::questionYesNo(KStars::Instance(), i18n("Are you sure you want to stop the polar alignment process?"), i18n("Polar Alignment Assistant"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "restart_PAA_process_dialog") == KMessageBox::No) return; stopB->click(); if (currentTelescope && currentTelescope->isInMotion()) currentTelescope->Abort(); pahStage = PAH_IDLE; emit newPAHStage(pahStage); PAHStartB->setEnabled(true); PAHStopB->setEnabled(false); PAHRefreshB->setEnabled(true); PAHWidgets->setCurrentWidget(PAHIntroPage); emit newPAHMessage(introText->text()); qDeleteAll(pahImageInfos); pahImageInfos.clear(); correctionVector = QLineF(); correctionOffset = QPointF(); alignView->setCorrectionParams(correctionVector); alignView->setCorrectionOffset(correctionOffset); alignView->setRACircle(QVector3D()); alignView->setRefreshEnabled(false); emit newFrame(alignView); disconnect(alignView, &AlignView::trackingStarSelected, this, &Ekos::Align::setPAHCorrectionOffset); disconnect(alignView, &AlignView::newCorrectionVector, this, &Ekos::Align::newCorrectionVector); if (Options::pAHAutoPark()) { currentTelescope->Park(); appendLogText(i18n("Parking the mount...")); } state = ALIGN_IDLE; emit newStatus(state); } void Align::rotatePAH() { double raDiff = PAHRotationSpin->value(); bool westMeridian = PAHDirectionCombo->currentIndex() == 0; // West if (westMeridian) raDiff *= -1; // East else raDiff *= 1; // JM 2018-05-03: Hemispheres shouldn't affect rotation direction in RA // if Manual slewing is selected, don't move the mount if (PAHManual->isChecked()) { appendLogText(i18n("Please rotate your mount about %1deg in RA", raDiff )); return; } // raDiff is in degrees dms newTelescopeRA = (telescopeCoord.ra() + dms(raDiff)).reduce(); targetPAH.setRA(newTelescopeRA); targetPAH.setDec(telescopeCoord.dec()); //currentTelescope->Slew(&targetPAH); // Set Selected Speed currentTelescope->setSlewRate(PAHSlewRateCombo->currentIndex()); // Go to direction currentTelescope->MoveWE(westMeridian ? ISD::Telescope::MOTION_WEST : ISD::Telescope::MOTION_EAST, ISD::Telescope::MOTION_START); appendLogText(i18n("Please wait until mount completes rotating to RA (%1) DE (%2)", targetPAH.ra().toHMSString(), targetPAH.dec().toDMSString())); } void Align::calculatePAHError() { QVector3D RACircle; bool rc = findRACircle(RACircle); if (rc == false) { appendLogText(i18n("Failed to find a solution. Try again.")); stopPAHProcess(); return; } if (alignView->isEQGridShown() == false) alignView->toggleEQGrid(); alignView->setRACircle(RACircle); FITSData *imageData = alignView->getImageData(); QPointF RACenterPoint(RACircle.x(), RACircle.y()); SkyPoint RACenter; rc = imageData->pixelToWCS(RACenterPoint, RACenter); if (rc == false) { appendLogText(i18n("Failed to find RA Axis center: %1.", imageData->getLastError())); return; } SkyPoint CP(0, (hemisphere == NORTH_HEMISPHERE) ? 90 : -90); RACenter.setRA(RACenter.ra0()); RACenter.setDec(RACenter.dec0()); double PA = 0; dms polarError = RACenter.angularDistanceTo(&CP, &PA); if (Options::alignmentLogging()) { qCDebug(KSTARS_EKOS_ALIGN) << "RA Axis Circle X: " << RACircle.x() << " Y: " << RACircle.y() << " Radius: " << RACircle.z(); qCDebug(KSTARS_EKOS_ALIGN) << "RA Axis Location RA: " << RACenter.ra0().toHMSString() << "DE: " << RACenter.dec0().toDMSString(); qCDebug(KSTARS_EKOS_ALIGN) << "RA Axis Offset: " << polarError.toDMSString() << "PA:" << PA; qCDebug(KSTARS_EKOS_ALIGN) << "CP Axis Location X:" << celestialPolePoint.x() << "Y:" << celestialPolePoint.y(); } RACenter.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); QString azDirection = RACenter.az().Degrees() < 30 ? "Right" : "Left"; QString atDirection = RACenter.alt().Degrees() < KStarsData::Instance()->geo()->lat()->Degrees() ? "Bottom" : "Top"; // FIXME should this be reversed for southern hemisphere? appendLogText(i18n("Mount axis is to the %1 %2 of the celestial pole", atDirection, azDirection)); PAHErrorLabel->setText(polarError.toDMSString()); // JM 2019-08-17: Flip for southern hemisphere. // Possible fix for: https://indilib.org/forum/ekos/5558-ekos-polar-alignment-vector-backwards.html correctionVector.setP1((hemisphere == NORTH_HEMISPHERE) ? celestialPolePoint : RACenterPoint); correctionVector.setP2((hemisphere == NORTH_HEMISPHERE) ? RACenterPoint : celestialPolePoint); connect(alignView, &AlignView::trackingStarSelected, this, &Ekos::Align::setPAHCorrectionOffset); emit polarResultUpdated(correctionVector, polarError.toDMSString()); connect(alignView, &AlignView::newCorrectionVector, this, &Ekos::Align::newCorrectionVector, Qt::UniqueConnection); emit newCorrectionVector(correctionVector); alignView->setCorrectionParams(correctionVector); emit newFrame(alignView); } void Align::setPAHCorrectionOffsetPercentage(double dx, double dy) { double x = dx * alignView->zoomedWidth() * (alignView->getCurrentZoom() / 100); double y = dy * alignView->zoomedHeight() * (alignView->getCurrentZoom() / 100); setPAHCorrectionOffset(static_cast(round(x)), static_cast(round(y))); } void Align::setPAHCorrectionOffset(int x, int y) { correctionOffset.setX(x); correctionOffset.setY(y); alignView->setCorrectionOffset(correctionOffset); emit newFrame(alignView); } void Align::setPAHCorrectionSelectionComplete() { pahStage = PAH_PRE_REFRESH; emit newPAHStage(pahStage); // If user stops here, we restore the settings, if not we // disable again in the refresh process // and restore when refresh is complete Options::setAstrometrySolverWCS(rememberSolverWCS); Options::setAutoWCS(rememberAutoWCS); PAHWidgets->setCurrentWidget(PAHRefreshPage); emit newPAHMessage(refreshText->text()); } void Align::setPAHSlewDone() { emit newPAHMessage("Manual slew done."); switch(pahStage) { case PAH_FIRST_ROTATE : pahStage = PAH_SECOND_CAPTURE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHSecondCapturePage); appendLogText(i18n("First manual rotation done.")); break; case PAH_SECOND_ROTATE : pahStage = PAH_THIRD_CAPTURE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHThirdCapturePage); appendLogText(i18n("Second manual rotation done.")); break; default : return; // no other stage should be able to trigger this event } if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); m_CaptureTimer.start(delaySpin->value()); } void Align::startPAHRefreshProcess() { qCInfo(KSTARS_EKOS_ALIGN) << "Starting Polar Alignment Assistant refreshing..."; pahStage = PAH_REFRESH; emit newPAHStage(pahStage); PAHRefreshB->setEnabled(false); // Hide EQ Grids if shown if (alignView->isEQGridShown()) alignView->toggleEQGrid(); alignView->setRefreshEnabled(true); Options::setAstrometrySolverWCS(false); Options::setAutoWCS(false); // We for refresh, just capture really captureAndSolve(); } void Align::setPAHRefreshComplete() { abort(); Options::setAstrometrySolverWCS(rememberSolverWCS); Options::setAutoWCS(rememberAutoWCS); stopPAHProcess(); } void Align::processPAHStage(double orientation, double ra, double dec, double pixscale) { //QString newWCSFile = QDir::tempPath() + QString("/fitswcs%1").arg(QUuid::createUuid().toString().remove(QRegularExpression("[-{}]"))); if (pahStage == PAH_FIND_CP) { setSolverAction(GOTO_NOTHING); appendLogText( i18n("Mount is synced to celestial pole. You can now continue Polar Alignment Assistant procedure.")); pahStage = PAH_FIRST_CAPTURE; emit newPAHStage(pahStage); return; } if (pahStage == PAH_FIRST_CAPTURE) { // Set First PAH Center PAHImageInfo *solution = new PAHImageInfo(); solution->skyCenter.setRA0(alignCoord.ra0()); solution->skyCenter.setDec0(alignCoord.dec0()); solution->orientation = orientation; solution->pixelScale = pixscale; pahImageInfos.append(solution); // Only invoke this if limited resource mode is false since we want to use CPU heavy WCS if (Options::limitedResourcesMode() == false) { appendLogText(i18n("Please wait while WCS data is processed...")); connect(alignView, &AlignView::wcsToggled, this, &Ekos::Align::setWCSToggled, Qt::UniqueConnection); alignView->injectWCS(orientation, ra, dec, pixscale); return; } pahStage = PAH_FIRST_ROTATE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHFirstRotatePage); emit newPAHMessage(firstRotateText->text()); rotatePAH(); } else if (pahStage == PAH_SECOND_CAPTURE) { // Set 2nd PAH Center PAHImageInfo *solution = new PAHImageInfo(); solution->skyCenter.setRA0(alignCoord.ra0()); solution->skyCenter.setDec0(alignCoord.dec0()); solution->orientation = orientation; solution->pixelScale = pixscale; pahImageInfos.append(solution); // Only invoke this if limited resource mode is false since we want to use CPU heavy WCS if (Options::limitedResourcesMode() == false) { appendLogText(i18n("Please wait while WCS data is processed...")); connect(alignView, &AlignView::wcsToggled, this, &Ekos::Align::setWCSToggled, Qt::UniqueConnection); alignView->injectWCS(orientation, ra, dec, pixscale); return; } pahStage = PAH_SECOND_ROTATE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHSecondRotatePage); emit newPAHMessage(secondRotateText->text()); rotatePAH(); } else if (pahStage == PAH_THIRD_CAPTURE) { // Set Third PAH Center PAHImageInfo *solution = new PAHImageInfo(); solution->skyCenter.setRA0(alignCoord.ra0()); solution->skyCenter.setDec0(alignCoord.dec0()); solution->orientation = orientation; solution->pixelScale = pixscale; pahImageInfos.append(solution); appendLogText(i18n("Please wait while WCS data is processed...")); connect(alignView, &AlignView::wcsToggled, this, &Ekos::Align::setWCSToggled, Qt::UniqueConnection); alignView->injectWCS(orientation, ra, dec, pixscale); return; } } void Align::setWCSToggled(bool result) { appendLogText(i18n("WCS data processing is complete.")); //alignView->disconnect(this); disconnect(alignView, &AlignView::wcsToggled, this, &Ekos::Align::setWCSToggled); if (pahStage == PAH_FIRST_CAPTURE) { // We need WCS to be synced first if (result == false && m_wcsSynced == true) { appendLogText(i18n("WCS info is now valid. Capturing next frame...")); pahImageInfos.clear(); captureAndSolve(); return; } // Find Celestial pole location SkyPoint CP(0, (hemisphere == NORTH_HEMISPHERE) ? 90 : -90); FITSData *imageData = alignView->getImageData(); QPointF pixelPoint, imagePoint; bool rc = imageData->wcsToPixel(CP, pixelPoint, imagePoint); pahImageInfos[0]->celestialPole = pixelPoint; // TODO check if pixelPoint is located TOO far from the current position as well // i.e. if X > Width * 2..etc if (rc == false) { appendLogText(i18n("Failed to process World Coordinate System: %1. Try again.", imageData->getLastError())); return; } // If celestial pole out of range, ask the user if they want to move to it if (pixelPoint.x() < (-1 * imageData->width()) || pixelPoint.x() > (imageData->width() * 2) || pixelPoint.y() < (-1 * imageData->height()) || pixelPoint.y() > (imageData->height() * 2)) { // JM 2019-11-15: This creates more problems at times, better leave it off #if 0 if (currentTelescope->canSync() && KMessageBox::questionYesNo( nullptr, i18n("Celestial pole is located outside of the field of view. Would you like to sync and slew " "the telescope to the celestial pole? WARNING: Slewing near poles may cause your mount to " "end up in unsafe position. Proceed with caution.")) == KMessageBox::Yes) { pahStage = PAH_FIND_CP; emit newPAHStage(pahStage); targetCoord.setRA(KStarsData::Instance()->lst()->Hours()); targetCoord.setDec(CP.dec().Degrees() > 0 ? 89.5 : -89.5); qDeleteAll(pahImageInfos); pahImageInfos.clear(); setSolverAction(GOTO_SLEW); Sync(); return; } else #endif appendLogText(i18n("Warning: Celestial pole is located outside the field of view. Move the mount closer to the celestial pole.")); } pahStage = PAH_FIRST_ROTATE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHFirstRotatePage); emit newPAHMessage(firstRotateText->text()); rotatePAH(); } else if (pahStage == PAH_SECOND_CAPTURE) { // Find Celestial pole location SkyPoint CP(0, (hemisphere == NORTH_HEMISPHERE) ? 90 : -90); FITSData *imageData = alignView->getImageData(); QPointF pixelPoint, imagePoint; imageData->wcsToPixel(CP, pixelPoint, imagePoint); pahImageInfos[1]->celestialPole = pixelPoint; pahStage = PAH_SECOND_ROTATE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHSecondRotatePage); emit newPAHMessage(secondRotateText->text()); rotatePAH(); } else if (pahStage == PAH_THIRD_CAPTURE) { FITSData *imageData = alignView->getImageData(); // Critical error if (result == false) { appendLogText(i18n("Failed to process World Coordinate System: %1. Try again.", imageData->getLastError())); return; } // Find Celestial pole location SkyPoint CP(0, (hemisphere == NORTH_HEMISPHERE) ? 90 : -90); QPointF imagePoint; imageData->wcsToPixel(CP, celestialPolePoint, imagePoint); pahImageInfos[2]->celestialPole = celestialPolePoint; // Now find pixel locations for all recorded center coordinates in the 3rd frame reference imageData->wcsToPixel(pahImageInfos[0]->skyCenter, pahImageInfos[0]->pixelCenter, imagePoint); imageData->wcsToPixel(pahImageInfos[1]->skyCenter, pahImageInfos[1]->pixelCenter, imagePoint); imageData->wcsToPixel(pahImageInfos[2]->skyCenter, pahImageInfos[2]->pixelCenter, imagePoint); qCDebug(KSTARS_EKOS_ALIGN) << "P1 RA: " << pahImageInfos[0]->skyCenter.ra0().toHMSString() << "DE: " << pahImageInfos[0]->skyCenter.dec0().toDMSString(); qCDebug(KSTARS_EKOS_ALIGN) << "P2 RA: " << pahImageInfos[1]->skyCenter.ra0().toHMSString() << "DE: " << pahImageInfos[1]->skyCenter.dec0().toDMSString(); qCDebug(KSTARS_EKOS_ALIGN) << "P3 RA: " << pahImageInfos[2]->skyCenter.ra0().toHMSString() << "DE: " << pahImageInfos[2]->skyCenter.dec0().toDMSString(); qCDebug(KSTARS_EKOS_ALIGN) << "P1 X: " << pahImageInfos[0]->pixelCenter.x() << "Y: " << pahImageInfos[0]->pixelCenter.y(); qCDebug(KSTARS_EKOS_ALIGN) << "P2 X: " << pahImageInfos[1]->pixelCenter.x() << "Y: " << pahImageInfos[1]->pixelCenter.y(); qCDebug(KSTARS_EKOS_ALIGN) << "P3 X: " << pahImageInfos[2]->pixelCenter.x() << "Y: " << pahImageInfos[2]->pixelCenter.y(); qCDebug(KSTARS_EKOS_ALIGN) << "P1 CP X: " << pahImageInfos[0]->celestialPole.x() << "CP Y: " << pahImageInfos[0]->celestialPole.y(); qCDebug(KSTARS_EKOS_ALIGN) << "P2 CP X: " << pahImageInfos[1]->celestialPole.x() << "CP Y: " << pahImageInfos[1]->celestialPole.y(); qCDebug(KSTARS_EKOS_ALIGN) << "P3 CP X: " << pahImageInfos[2]->celestialPole.x() << "CP Y: " << pahImageInfos[2]->celestialPole.y(); // We have 3 points which uniquely defines a circle with its center representing the RA Axis // We have celestial pole location. So correction vector is just the vector between these two points calculatePAHError(); pahStage = PAH_STAR_SELECT; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHCorrectionPage); emit newPAHMessage(correctionText->text()); } } void Align::updateTelescopeType(int index) { if (currentCCD == nullptr) return; syncSettings(); focal_length = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryFL : guideFL; aperture = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryAperture : guideAperture; Options::setSolverScopeType(index); syncTelescopeInfo(); } // Function adapted from https://rosettacode.org/wiki/Circles_of_given_radius_through_two_points Align::CircleSolution Align::findCircleSolutions(const QPointF &p1, const QPointF p2, double angle, QPair &circleSolutions) { QPointF solutionOne(1, 1), solutionTwo(1, 1); double radius = distance(p1, p2) / (dms::DegToRad * angle); if (p1 == p2) { if (angle == 0) { circleSolutions = qMakePair(p1, p2); appendLogText(i18n("Only one solution is found.")); return ONE_CIRCLE_SOLUTION; } else { circleSolutions = qMakePair(solutionOne, solutionTwo); appendLogText(i18n("Infinite number of solutions found.")); return INFINITE_CIRCLE_SOLUTION; } } QPointF center(p1.x() / 2 + p2.x() / 2, p1.y() / 2 + p2.y() / 2); double halfDistance = distance(center, p1); if (halfDistance > radius) { circleSolutions = qMakePair(solutionOne, solutionTwo); appendLogText(i18n("No solution is found. Points are too far away")); return NO_CIRCLE_SOLUTION; } if (halfDistance - radius == 0) { circleSolutions = qMakePair(center, solutionTwo); appendLogText(i18n("Only one solution is found.")); return ONE_CIRCLE_SOLUTION; } double root = std::hypotf(radius, halfDistance) / distance(p1, p2); solutionOne.setX(center.x() + root * (p1.y() - p2.y())); solutionOne.setY(center.y() + root * (p2.x() - p1.x())); solutionTwo.setX(center.x() - root * (p1.y() - p2.y())); solutionTwo.setY(center.y() - root * (p2.x() - p1.x())); circleSolutions = qMakePair(solutionOne, solutionTwo); return TWO_CIRCLE_SOLUTION; } double Align::distance(const QPointF &p1, const QPointF &p2) { return std::hypotf(p2.x() - p1.x(), p2.y() - p1.y()); } bool Align::findRACircle(QVector3D &RACircle) { bool rc = false; QPointF p1 = pahImageInfos[0]->pixelCenter; QPointF p2 = pahImageInfos[1]->pixelCenter; QPointF p3 = pahImageInfos[2]->pixelCenter; if (!isPerpendicular(p1, p2, p3)) rc = calcCircle(p1, p2, p3, RACircle); else if (!isPerpendicular(p1, p3, p2)) rc = calcCircle(p1, p3, p2, RACircle); else if (!isPerpendicular(p2, p1, p3)) rc = calcCircle(p2, p1, p3, RACircle); else if (!isPerpendicular(p2, p3, p1)) rc = calcCircle(p2, p3, p1, RACircle); else if (!isPerpendicular(p3, p2, p1)) rc = calcCircle(p3, p2, p1, RACircle); else if (!isPerpendicular(p3, p1, p2)) rc = calcCircle(p3, p1, p2, RACircle); else { //TRACE("\nThe three pts are perpendicular to axis\n"); return false; } return rc; } bool Align::isPerpendicular(const QPointF &p1, const QPointF &p2, const QPointF &p3) // Check the given point are perpendicular to x or y axis { double yDelta_a = p2.y() - p1.y(); double xDelta_a = p2.x() - p1.x(); double yDelta_b = p3.y() - p2.y(); double xDelta_b = p3.x() - p2.x(); // checking whether the line of the two pts are vertical if (fabs(xDelta_a) <= 0.000000001 && fabs(yDelta_b) <= 0.000000001) { //TRACE("The points are perpendicular and parallel to x-y axis\n"); return false; } if (fabs(yDelta_a) <= 0.0000001) { //TRACE(" A line of two point are perpendicular to x-axis 1\n"); return true; } else if (fabs(yDelta_b) <= 0.0000001) { //TRACE(" A line of two point are perpendicular to x-axis 2\n"); return true; } else if (fabs(xDelta_a) <= 0.000000001) { //TRACE(" A line of two point are perpendicular to y-axis 1\n"); return true; } else if (fabs(xDelta_b) <= 0.000000001) { //TRACE(" A line of two point are perpendicular to y-axis 2\n"); return true; } else return false; } bool Align::calcCircle(const QPointF &p1, const QPointF &p2, const QPointF &p3, QVector3D &RACircle) { double yDelta_a = p2.y() - p1.y(); double xDelta_a = p2.x() - p1.x(); double yDelta_b = p3.y() - p2.y(); double xDelta_b = p3.x() - p2.x(); if (fabs(xDelta_a) <= 0.000000001 && fabs(yDelta_b) <= 0.000000001) { RACircle.setX(0.5 * (p2.x() + p3.x())); RACircle.setY(0.5 * (p1.y() + p2.y())); QPointF center(RACircle.x(), RACircle.y()); RACircle.setZ(distance(center, p1)); return true; } // IsPerpendicular() assure that xDelta(s) are not zero double aSlope = yDelta_a / xDelta_a; // double bSlope = yDelta_b / xDelta_b; if (fabs(aSlope - bSlope) <= 0.000000001) { // checking whether the given points are colinear. //TRACE("The three ps are colinear\n"); return false; } // calc center RACircle.setX((aSlope * bSlope * (p1.y() - p3.y()) + bSlope * (p1.x() + p2.x()) - aSlope * (p2.x() + p3.x())) / (2 * (bSlope - aSlope))); RACircle.setY(-1 * (RACircle.x() - (p1.x() + p2.x()) / 2) / aSlope + (p1.y() + p2.y()) / 2); QPointF center(RACircle.x(), RACircle.y()); RACircle.setZ(distance(center, p1)); return true; } void Align::setMountStatus(ISD::Telescope::Status newState) { switch (newState) { case ISD::Telescope::MOUNT_PARKING: case ISD::Telescope::MOUNT_SLEWING: case ISD::Telescope::MOUNT_MOVING: solveB->setEnabled(false); loadSlewB->setEnabled(false); PAHStartB->setEnabled(false); break; default: if (state != ALIGN_PROGRESS) { solveB->setEnabled(true); if (pahStage == PAH_IDLE) { PAHStartB->setEnabled(true); loadSlewB->setEnabled(true); } } break; } } void Align::setAstrometryDevice(ISD::GDInterface *newAstrometry) { remoteParserDevice = newAstrometry; if (remoteParser.get() != nullptr) { remoteParser->setAstrometryDevice(remoteParserDevice); connect(remoteParser.get(), &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection); connect(remoteParser.get(), &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection); } } void Align::setRotator(ISD::GDInterface *newRotator) { currentRotator = newRotator; connect(currentRotator, &ISD::GDInterface::numberUpdated, this, &Ekos::Align::processNumber, Qt::UniqueConnection); } void Align::refreshAlignOptions() { if (getSolverFOV()) getSolverFOV()->setImageDisplay(Options::astrometrySolverWCS()); m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000); } void Align::setFilterManager(const QSharedPointer &manager) { filterManager = manager; connect(filterManager.data(), &FilterManager::ready, [this]() { if (filterPositionPending) { focusState = FOCUS_IDLE; filterPositionPending = false; captureAndSolve(); } } ); connect(filterManager.data(), &FilterManager::failed, [this]() { appendLogText(i18n("Filter operation failed.")); abort(); } ); connect(filterManager.data(), &FilterManager::newStatus, [this](Ekos::FilterState filterState) { if (filterPositionPending) { switch (filterState) { case FILTER_OFFSET: appendLogText(i18n("Changing focus offset by %1 steps...", filterManager->getTargetFilterOffset())); break; case FILTER_CHANGE: appendLogText(i18n("Changing filter to %1...", FilterPosCombo->itemText(filterManager->getTargetFilterPosition() - 1))); break; case FILTER_AUTOFOCUS: appendLogText(i18n("Auto focus on filter change...")); break; default: break; } } }); connect(filterManager.data(), &FilterManager::labelsChanged, this, [this]() { checkFilter(); }); connect(filterManager.data(), &FilterManager::positionChanged, this, [this]() { checkFilter(); }); } QVariantMap Align::getEffectiveFOV() { KStarsData::Instance()->userdb()->GetAllEffectiveFOVs(effectiveFOVs); fov_x = fov_y = 0; for (auto &map : effectiveFOVs) { if (map["Profile"].toString() == m_ActiveProfile->name) { if (map["Width"].toInt() == ccd_width && map["Height"].toInt() == ccd_height && map["PixelW"].toDouble() == ccd_hor_pixel && map["PixelH"].toDouble() == ccd_ver_pixel && map["FocalLength"].toDouble() == focal_length) { fov_x = map["FovW"].toDouble(); fov_y = map["FovH"].toDouble(); return map; } } } return QVariantMap(); } void Align::saveNewEffectiveFOV(double newFOVW, double newFOVH) { if (newFOVW < 0 || newFOVH < 0 || (newFOVW == fov_x && newFOVH == fov_y)) return; QVariantMap effectiveMap = getEffectiveFOV(); // If ID exists, delete it first. if (effectiveMap.isEmpty() == false) KStarsData::Instance()->userdb()->DeleteEffectiveFOV(effectiveMap["id"].toString()); // If FOV is 0x0, then we just remove existing effective FOV if (newFOVW == 0.0 && newFOVH == 0.0) { calculateFOV(); return; } effectiveMap["Profile"] = m_ActiveProfile->name; effectiveMap["Width"] = ccd_width; effectiveMap["Height"] = ccd_height; effectiveMap["PixelW"] = ccd_hor_pixel; effectiveMap["PixelH"] = ccd_ver_pixel; effectiveMap["FocalLength"] = focal_length; effectiveMap["FovW"] = newFOVW; effectiveMap["FovH"] = newFOVH; KStarsData::Instance()->userdb()->AddEffectiveFOV(effectiveMap); calculateFOV(); } QStringList Align::getActiveSolvers() const { QStringList solvers; solvers << "Online"; #ifndef Q_OS_WIN solvers << "Offline"; #endif if (remoteParserDevice != nullptr) solvers << "Remote"; return solvers; } int Align::getActiveSolverIndex() const { return solverBackendGroup->checkedId(); } QString Align::getPAHMessage() const { switch (pahStage) { case PAH_IDLE: case PAH_FIND_CP: return introText->text(); case PAH_FIRST_CAPTURE: return firstCaptureText->text(); case PAH_FIRST_ROTATE: return firstRotateText->text(); case PAH_SECOND_CAPTURE: return secondCaptureText->text(); case PAH_SECOND_ROTATE: return secondRotateText->text(); case PAH_THIRD_CAPTURE: return thirdCaptureText->text(); case PAH_STAR_SELECT: return correctionText->text(); case PAH_PRE_REFRESH: case PAH_REFRESH: return refreshText->text(); case PAH_ERROR: return PAHErrorDescriptionLabel->text(); } return QString(); } void Align::zoomAlignView() { alignView->ZoomDefault(); emit newFrame(alignView); } QJsonObject Align::getSettings() const { QJsonObject settings; settings.insert("camera", CCDCaptureCombo->currentText()); settings.insert("fw", FilterDevicesCombo->currentText()); settings.insert("filter", FilterPosCombo->currentText()); settings.insert("exp", exposureIN->value()); settings.insert("bin", qMax(1, binningCombo->currentIndex() + 1)); settings.insert("solverAction", gotoModeButtonGroup->checkedId()); settings.insert("solverBackend", solverBackendGroup->checkedId()); settings.insert("solverType", astrometryTypeCombo->currentIndex()); settings.insert("scopeType", FOVScopeCombo->currentIndex()); return settings; } void Align::setSettings(const QJsonObject &settings) { CCDCaptureCombo->setCurrentText(settings["camera"].toString()); FilterDevicesCombo->setCurrentText(settings["fw"].toString()); FilterPosCombo->setCurrentText(settings["filter"].toString()); Options::setLockAlignFilterIndex(FilterPosCombo->currentIndex()); exposureIN->setValue(settings["exp"].toDouble(1)); binningCombo->setCurrentIndex(settings["bin"].toInt() - 1); gotoModeButtonGroup->button(settings["solverAction"].toInt(1))->click(); int solverBackend = settings["solverBackend"].toInt(1); int solverType = settings["solverType"].toInt(1); if (solverBackend == SOLVER_ASTROMETRYNET) { Options::setAstrometrySolverType(solverType); astrometryTypeCombo->setCurrentIndex(solverType); solverBackendGroup->button(SOLVER_ASTROMETRYNET)->animateClick(); } else { solverBackendGroup->button(SOLVER_ASTAP)->animateClick(); } FOVScopeCombo->setCurrentIndex(settings["scopeType"].toInt(0)); } void Align::syncSettings() { emit settingsUpdated(getSettings()); } QJsonObject Align::getPAHSettings() const { QJsonObject settings = getSettings(); settings.insert("mountDirection", PAHDirectionCombo->currentIndex()); settings.insert("mountSpeed", PAHSlewRateCombo->currentIndex()); settings.insert("mountRotation", PAHRotationSpin->value()); settings.insert("refresh", PAHExposure->value()); settings.insert("manualslew", PAHManual->isChecked()); return settings; } void Align::setPAHSettings(const QJsonObject &settings) { setSettings(settings); PAHDirectionCombo->setCurrentIndex(settings["mountDirection"].toInt(0)); PAHRotationSpin->setValue(settings["mountRotation"].toInt(30)); PAHExposure->setValue(settings["refresh"].toDouble(1)); if (settings.contains("mountSpeed")) PAHSlewRateCombo->setCurrentIndex(settings["mountSpeed"].toInt(0)); PAHManual->setChecked(settings["manualslew"].toBool(false)); } void Align::syncFOV() { QString newFOV = FOVOut->text(); QRegularExpression re("(\\d+\\.*\\d*)\\D*x\\D*(\\d+\\.*\\d*)"); QRegularExpressionMatch match = re.match(newFOV); if (match.hasMatch()) { double newFOVW = match.captured(1).toDouble(); double newFOVH = match.captured(2).toDouble(); //if (newFOVW > 0 && newFOVH > 0) saveNewEffectiveFOV(newFOVW, newFOVH); FOVOut->setStyleSheet(QString()); } else { KSNotification::error(i18n("Invalid FOV.")); FOVOut->setStyleSheet("background-color:red"); } } } diff --git a/kstars/ekos/guide/internalguide/internalguider.cpp b/kstars/ekos/guide/internalguide/internalguider.cpp index 0bda02a9f..04be02658 100644 --- a/kstars/ekos/guide/internalguide/internalguider.cpp +++ b/kstars/ekos/guide/internalguide/internalguider.cpp @@ -1,1329 +1,1329 @@ /* Ekos Internal Guider Class Copyright (C) 2016 Jasem Mutlaq . Based on lin_guider This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "internalguider.h" #include "ekos_guide_debug.h" #include "gmath.h" #include "Options.h" #include "auxiliary/kspaths.h" #include "fitsviewer/fitsdata.h" #include "fitsviewer/fitsview.h" #include "ksnotification.h" #include #include #include #include #define MAX_GUIDE_STARS 10 namespace Ekos { InternalGuider::InternalGuider() { // Create math object pmath.reset(new cgmath()); connect(pmath.get(), SIGNAL(newStarPosition(QVector3D,bool)), this, SIGNAL(newStarPosition(QVector3D,bool))); state = GUIDE_IDLE; } bool InternalGuider::guide() { if (state == GUIDE_SUSPENDED) return true; if (state >= GUIDE_GUIDING) { if (m_ImageGuideEnabled) return processImageGuiding(); else return processGuiding(); } guideFrame->disconnect(this); pmath->start(); m_starLostCounter = 0; m_highRMSCounter= 0; // TODO re-enable rapid check later on #if 0 m_isStarted = true; m_useRapidGuide = ui.rapidGuideCheck->isChecked(); if (m_useRapidGuide) guideModule->startRapidGuide(); emit newStatus(Ekos::GUIDE_GUIDING); guideModule->setSuspended(false); first_frame = true; if (ui.subFrameCheck->isEnabled() && ui.subFrameCheck->isChecked() && m_isSubFramed == false) first_subframe = true; capture(); #endif m_isFirstFrame = true; state = GUIDE_GUIDING; emit newStatus(state); emit frameCaptureRequested(); return true; } bool InternalGuider::abort() { calibrationStage = CAL_IDLE; logFile.close(); if (state == GUIDE_CALIBRATING || state == GUIDE_GUIDING || state == GUIDE_DITHERING || state == GUIDE_MANUAL_DITHERING) { if (state == GUIDE_DITHERING || state == GUIDE_MANUAL_DITHERING) emit newStatus(GUIDE_DITHERING_ERROR); emit newStatus(GUIDE_ABORTED); qCDebug(KSTARS_EKOS_GUIDE) << "Aborting" << getGuideStatusString(state); } else { emit newStatus(GUIDE_IDLE); qCDebug(KSTARS_EKOS_GUIDE) << "Stopping internal guider."; } m_ProgressiveDither.clear(); m_starLostCounter=0; m_highRMSCounter=0; accumulator.first = accumulator.second = 0; pmath->suspend(false); state = GUIDE_IDLE; return true; } bool InternalGuider::suspend() { state = GUIDE_SUSPENDED; emit newStatus(state); pmath->suspend(true); return true; } bool InternalGuider::resume() { state = GUIDE_GUIDING; emit newStatus(state); pmath->suspend(false); emit frameCaptureRequested(); return true; } bool InternalGuider::ditherXY(double x, double y) { m_ProgressiveDither.clear(); m_DitherRetries=0; double cur_x, cur_y, ret_angle; pmath->getReticleParameters(&cur_x, &cur_y, &ret_angle); // Find out how many "jumps" we need to perform in order to get to target. // The current limit is now 1/4 of the box size to make sure the star stays within detection // threashold inside the window. double oneJump = (guideBoxSize/4.0); double targetX = cur_x, targetY = cur_y; int xSign = (x >= cur_x) ? 1 : -1; int ySign = (y >= cur_y) ? 1 : -1; do { if (fabs(targetX - x) > oneJump) targetX += oneJump * xSign; else if (fabs(targetX - x) < oneJump) targetX = x; if (fabs(targetY - y) > oneJump) targetY += oneJump * ySign; else if (fabs(targetY - y) < oneJump) targetY = y; m_ProgressiveDither.enqueue(Vector(targetX, targetY, ret_angle)); } while (targetX != x || targetY != y); m_DitherTargetPosition = m_ProgressiveDither.dequeue(); pmath->setReticleParameters(m_DitherTargetPosition.x, m_DitherTargetPosition.y, m_DitherTargetPosition.z); state = GUIDE_MANUAL_DITHERING; emit newStatus(state); processGuiding(); return true; } bool InternalGuider::dither(double pixels) { double cur_x, cur_y, ret_angle; pmath->getReticleParameters(&cur_x, &cur_y, &ret_angle); pmath->getStarScreenPosition(&cur_x, &cur_y); Ekos::Matrix ROT_Z = pmath->getROTZ(); if (state != GUIDE_DITHERING) { m_DitherRetries = 0; auto seed = std::chrono::system_clock::now().time_since_epoch().count(); std::default_random_engine generator(seed); std::uniform_real_distribution angleMagnitude(0, 360); double angle = angleMagnitude(generator) * dms::DegToRad; double diff_x = pixels * cos(angle); double diff_y = pixels * sin(angle); if (pmath->declinationSwapEnabled()) diff_y *= -1; if (fabs(diff_x + accumulator.first) > MAX_DITHER_TRAVEL) diff_x *= -1; accumulator.first += diff_x; if (fabs(diff_y + accumulator.second) > MAX_DITHER_TRAVEL) diff_y *= -1; accumulator.second += diff_y; m_DitherTargetPosition = Vector(cur_x, cur_y, 0) + Vector(diff_x, diff_y, 0); qCDebug(KSTARS_EKOS_GUIDE) << "Dithering process started.. Reticle Target Pos X " << m_DitherTargetPosition.x << " Y " << m_DitherTargetPosition.y; pmath->setReticleParameters(m_DitherTargetPosition.x, m_DitherTargetPosition.y, ret_angle); state = GUIDE_DITHERING; emit newStatus(state); processGuiding(); return true; } Vector star_pos = Vector(cur_x, cur_y, 0) - Vector(m_DitherTargetPosition.x, m_DitherTargetPosition.y, 0); star_pos.y = -star_pos.y; star_pos = star_pos * ROT_Z; qCDebug(KSTARS_EKOS_GUIDE) << "Dithering in progress. Diff star X:" << star_pos.x << "Y:" << star_pos.y; if (fabs(star_pos.x) < 1 && fabs(star_pos.y) < 1) { pmath->setReticleParameters(cur_x, cur_y, ret_angle); qCDebug(KSTARS_EKOS_GUIDE) << "Dither complete."; if (Options::ditherSettle() > 0) { state = GUIDE_DITHERING_SETTLE; emit newStatus(state); } QTimer::singleShot(Options::ditherSettle()* 1000, this, SLOT(setDitherSettled())); } else { if (++m_DitherRetries > Options::ditherMaxIterations()) { if (Options::ditherFailAbortsAutoGuide()) { emit newStatus(Ekos::GUIDE_DITHERING_ERROR); abort(); return false; } else { emit newLog(i18n("Warning: Dithering failed. Autoguiding shall continue as set in the options in case " "of dither failure.")); if (Options::ditherSettle() > 0) { state = GUIDE_DITHERING_SETTLE; emit newStatus(state); } QTimer::singleShot(Options::ditherSettle()* 1000, this, SLOT(setDitherSettled())); return true; } } processGuiding(); } return true; } bool InternalGuider::processManualDithering() { double cur_x, cur_y, ret_angle; pmath->getReticleParameters(&cur_x, &cur_y, &ret_angle); pmath->getStarScreenPosition(&cur_x, &cur_y); Ekos::Matrix ROT_Z = pmath->getROTZ(); Vector star_pos = Vector(cur_x, cur_y, 0) - Vector(m_DitherTargetPosition.x, m_DitherTargetPosition.y, 0); star_pos.y = -star_pos.y; star_pos = star_pos * ROT_Z; qCDebug(KSTARS_EKOS_GUIDE) << "Manual Dithering in progress. Diff star X:" << star_pos.x << "Y:" << star_pos.y; if (fabs(star_pos.x) < guideBoxSize/5.0 && fabs(star_pos.y) < guideBoxSize/5.0) { if (m_ProgressiveDither.empty() == false) { m_DitherTargetPosition = m_ProgressiveDither.dequeue(); pmath->setReticleParameters(m_DitherTargetPosition.x, m_DitherTargetPosition.y, m_DitherTargetPosition.z); qCDebug(KSTARS_EKOS_GUIDE) << "Next Dither Jump X:" << m_DitherTargetPosition.x << "Jump Y:" << m_DitherTargetPosition.y; m_DitherRetries=0; processGuiding(); return true; } if (fabs(star_pos.x) < 1 && fabs(star_pos.y) < 1) { pmath->setReticleParameters(cur_x, cur_y, ret_angle); qCDebug(KSTARS_EKOS_GUIDE) << "Manual Dither complete."; if (Options::ditherSettle() > 0) { state = GUIDE_DITHERING_SETTLE; emit newStatus(state); } QTimer::singleShot(Options::ditherSettle()* 1000, this, SLOT(setDitherSettled())); } else { processGuiding(); } } else { if (++m_DitherRetries > Options::ditherMaxIterations()) { emit newLog(i18n("Warning: Manual Dithering failed.")); if (Options::ditherSettle() > 0) { state = GUIDE_DITHERING_SETTLE; emit newStatus(state); } QTimer::singleShot(Options::ditherSettle()* 1000, this, SLOT(setDitherSettled())); return true; } processGuiding(); } return true; } void InternalGuider::setDitherSettled() { emit newStatus(Ekos::GUIDE_DITHERING_SUCCESS); // Back to guiding state = GUIDE_GUIDING; } bool InternalGuider::calibrate() { bool ccdInfo = true, scopeInfo = true; QString errMsg; if (subW == 0 || subH == 0) { errMsg = "CCD"; ccdInfo = false; } if (mountAperture == 0.0 || mountFocalLength == 0.0) { scopeInfo = false; if (ccdInfo == false) errMsg += " & Telescope"; else errMsg += "Telescope"; } if (ccdInfo == false || scopeInfo == false) { KSNotification::error(i18n("%1 info are missing. Please set the values in INDI Control Panel.", errMsg), i18n("Missing Information")); return false; } if (state != GUIDE_CALIBRATING) { calibrationStage = CAL_IDLE; state = GUIDE_CALIBRATING; emit newStatus(GUIDE_CALIBRATING); } if (calibrationStage > CAL_START) { processCalibration(); return true; } guideFrame->disconnect(this); // Must reset dec swap before we run any calibration procedure! emit DESwapChanged(false); pmath->setDeclinationSwapEnabled(false); pmath->setLostStar(false); calibrationStage = CAL_START; // automatic // If two axies (RA/DEC) are required if (Options::twoAxisEnabled()) calibrateRADECRecticle(false); else // Just RA calibrateRADECRecticle(true); return true; } void InternalGuider::processCalibration() { pmath->performProcessing(); if (pmath->isStarLost()) { emit newLog(i18n("Lost track of the guide star. Try increasing the square size or reducing pulse duration.")); reset(); calibrationStage = CAL_ERROR; emit newStatus(Ekos::GUIDE_CALIBRATION_ERROR); return; } switch (calibrationType) { case CAL_NONE: break; case CAL_RA_AUTO: calibrateRADECRecticle(true); break; case CAL_RA_DEC_AUTO: calibrateRADECRecticle(false); break; } } void InternalGuider::setGuideView(FITSView *guideView) { guideFrame = guideView; pmath->setGuideView(guideFrame); } void InternalGuider::reset() { state = GUIDE_IDLE; //calibrationStage = CAL_IDLE; connect(guideFrame, SIGNAL(trackingStarSelected(int,int)), this, SLOT(trackingStarSelected(int,int)), Qt::UniqueConnection); } void InternalGuider::calibrateRADECRecticle(bool ra_only) { bool axis_calibration_complete = false; Q_ASSERT(pmath); //int totalPulse = pulseDuration * Options::autoModeIterations(); if (ra_only) calibrationType = CAL_RA_AUTO; else calibrationType = CAL_RA_DEC_AUTO; switch (calibrationStage) { case CAL_START: //----- automatic mode ----- m_CalibrationParams.auto_drift_time = Options::autoModeIterations(); m_CalibrationParams.turn_back_time = m_CalibrationParams.auto_drift_time * 7; m_CalibrationParams.ra_iterations = 0; m_CalibrationParams.dec_iterations = 0; m_CalibrationParams.ra_total_pulse = m_CalibrationParams.de_total_pulse = 0; emit newLog(i18n("RA drifting forward...")); pmath->getReticleParameters(&m_CalibrationCoords.start_x1, &m_CalibrationCoords.start_y1, nullptr); m_CalibrationParams.last_pulse = Options::calibrationPulseDuration(); qCDebug(KSTARS_EKOS_GUIDE) << "Auto Iteration #" << m_CalibrationParams.auto_drift_time << "Default pulse:" << m_CalibrationParams.last_pulse; qCDebug(KSTARS_EKOS_GUIDE) << "Start X1 " << m_CalibrationCoords.start_x1 << " Start Y1 " << m_CalibrationCoords.start_y1; axis_calibration_complete = false; m_CalibrationCoords.last_x = m_CalibrationCoords.start_x1; m_CalibrationCoords.last_y = m_CalibrationCoords.start_x2; emit newPulse(RA_INC_DIR, m_CalibrationParams.last_pulse); m_CalibrationParams.ra_iterations++; calibrationStage = CAL_RA_INC; break; case CAL_RA_INC: { // Star position resulting from LAST guiding pulse to mount double cur_x, cur_y; pmath->getStarScreenPosition(&cur_x, &cur_y); qCDebug(KSTARS_EKOS_GUIDE) << "Iteration #" << m_CalibrationParams.ra_iterations << ": STAR " << cur_x << "," << cur_y; qCDebug(KSTARS_EKOS_GUIDE) << "Iteration " << m_CalibrationParams.ra_iterations << " Direction: RA_INC_DIR" << " Duration: " << m_CalibrationParams.last_pulse << " ms."; // Must pass at least 1.5 pixels to move on to the next stage if (m_CalibrationParams.ra_iterations >= m_CalibrationParams.auto_drift_time && (fabs(cur_x-m_CalibrationCoords.start_x1) > 1.5 || fabs(cur_y-m_CalibrationCoords.start_y1) > 1.5)) { m_CalibrationParams.ra_total_pulse += m_CalibrationParams.last_pulse; calibrationStage = CAL_RA_DEC; m_CalibrationCoords.end_x1 = cur_x; m_CalibrationCoords.end_y1 = cur_y; m_CalibrationCoords.last_x = cur_x; m_CalibrationCoords.last_y = cur_y; qCDebug(KSTARS_EKOS_GUIDE) << "End X1 " << m_CalibrationCoords.end_x1 << " End Y1 " << m_CalibrationCoords.end_y1; m_CalibrationParams.phi = pmath->calculatePhi(m_CalibrationCoords.start_x1, m_CalibrationCoords.start_y1, m_CalibrationCoords.end_x1, m_CalibrationCoords.end_y1); ROT_Z = RotateZ(-M_PI * m_CalibrationParams.phi / 180.0); // derotates... m_CalibrationCoords.ra_distance = 0; m_CalibrationParams.backlash = 0; emit newPulse(RA_DEC_DIR, m_CalibrationParams.last_pulse); m_CalibrationParams.ra_iterations++; emit newLog(i18n("RA drifting reverse...")); } else if (m_CalibrationParams.ra_iterations > m_CalibrationParams.turn_back_time) { emit newLog(i18n("Calibration rejected. Star drift is too short. Check for mount, cable, or backlash problems.")); calibrationStage = CAL_ERROR; emit newStatus(Ekos::GUIDE_CALIBRATION_ERROR); KSNotification::event(QLatin1String("CalibrationFailed"), i18n("Guiding calibration failed with errors"), KSNotification::EVENT_ALERT); reset(); } else { // Aggressive pulse in case we're going slow if (fabs(cur_x-m_CalibrationCoords.last_x) < 0.5 && fabs(cur_y-m_CalibrationCoords.last_y) < 0.5) { // 200% m_CalibrationParams.last_pulse = Options::calibrationPulseDuration() *2; } else { m_CalibrationParams.ra_total_pulse += m_CalibrationParams.last_pulse; m_CalibrationParams.last_pulse = Options::calibrationPulseDuration(); } m_CalibrationCoords.last_x = cur_x; m_CalibrationCoords.last_y = cur_y; emit newPulse(RA_INC_DIR, m_CalibrationParams.last_pulse); m_CalibrationParams.ra_iterations++; } } break; case CAL_RA_DEC: { //----- Z-check (new!) ----- double cur_x, cur_y; pmath->getStarScreenPosition(&cur_x, &cur_y); qCDebug(KSTARS_EKOS_GUIDE) << "Iteration #" << m_CalibrationParams.ra_iterations << ": STAR " << cur_x << "," << cur_y; qCDebug(KSTARS_EKOS_GUIDE) << "Iteration " << m_CalibrationParams.ra_iterations << " Direction: RA_DEC_DIR" << " Duration: " << m_CalibrationParams.last_pulse << " ms."; Vector star_pos = Vector(cur_x, cur_y, 0) - Vector(m_CalibrationCoords.start_x1, m_CalibrationCoords.start_y1, 0); star_pos.y = -star_pos.y; star_pos = star_pos * ROT_Z; qCDebug(KSTARS_EKOS_GUIDE) << "Star x pos is " << star_pos.x << " from original point."; if (m_CalibrationCoords.ra_distance == 0.0) m_CalibrationCoords.ra_distance = star_pos.x; // start point reached... so exit if (star_pos.x < 1.5) { pmath->performProcessing(); m_CalibrationParams.ra_total_pulse += m_CalibrationParams.last_pulse; m_CalibrationParams.last_pulse = Options::calibrationPulseDuration(); axis_calibration_complete = true; } // If we'not moving much, try increasing pulse to 200% to clear any backlash // Also increase pulse width if we are going FARTHER and not back to our original position else if ( (fabs(cur_x-m_CalibrationCoords.last_x) < 0.5 && fabs(cur_y-m_CalibrationCoords.last_y) < 0.5) || star_pos.x > m_CalibrationCoords.ra_distance) { m_CalibrationParams.backlash++; // Increase pulse to 200% after we tried to fight against backlash 2 times at least if (m_CalibrationParams.backlash > 2) m_CalibrationParams.last_pulse = Options::calibrationPulseDuration()*2; else m_CalibrationParams.last_pulse = Options::calibrationPulseDuration(); } else { m_CalibrationParams.ra_total_pulse += m_CalibrationParams.last_pulse; m_CalibrationParams.last_pulse = Options::calibrationPulseDuration(); m_CalibrationParams.backlash = 0; } m_CalibrationCoords.last_x = cur_x; m_CalibrationCoords.last_y = cur_y; //----- Z-check end ----- if (axis_calibration_complete == false) { if (m_CalibrationParams.ra_iterations < m_CalibrationParams.turn_back_time) { emit newPulse(RA_DEC_DIR, m_CalibrationParams.last_pulse); m_CalibrationParams.ra_iterations++; break; } calibrationStage = CAL_ERROR; emit newStatus(Ekos::GUIDE_CALIBRATION_ERROR); emit newLog(i18np("Guide RA: Scope cannot reach the start point after %1 iteration. Possible mount or " "backlash problems...", "GUIDE_RA: Scope cannot reach the start point after %1 iterations. Possible mount or " "backlash problems...", m_CalibrationParams.ra_iterations)); KSNotification::event(QLatin1String("CalibrationFailed"), i18n("Guiding calibration failed with errors"), KSNotification::EVENT_ALERT); reset(); break; } if (ra_only == false) { calibrationStage = CAL_DEC_INC; m_CalibrationCoords.start_x2 = cur_x; m_CalibrationCoords.start_y2 = cur_y; m_CalibrationCoords.last_x = cur_x; m_CalibrationCoords.last_y = cur_y; qCDebug(KSTARS_EKOS_GUIDE) << "Start X2 " << m_CalibrationCoords.start_x2 << " start Y2 " << m_CalibrationCoords.start_y2; emit newPulse(DEC_INC_DIR, Options::calibrationPulseDuration()); m_CalibrationParams.dec_iterations++; emit newLog(i18n("DEC drifting forward...")); break; } // calc orientation if (pmath->calculateAndSetReticle1D(m_CalibrationCoords.start_x1, m_CalibrationCoords.start_y1, m_CalibrationCoords.end_x1, m_CalibrationCoords.end_y1, m_CalibrationParams.ra_total_pulse)) { calibrationStage = CAL_IDLE; emit newStatus(Ekos::GUIDE_CALIBRATION_SUCESS); KSNotification::event(QLatin1String("CalibrationSuccessful"), i18n("Guiding calibration completed successfully")); } else { emit newLog(i18n("Calibration rejected. Star drift is too short. Check for mount, cable, or backlash problems.")); calibrationStage = CAL_ERROR; emit newStatus(Ekos::GUIDE_CALIBRATION_ERROR); KSNotification::event(QLatin1String("CalibrationFailed"), i18n("Guiding calibration failed with errors"), KSNotification::EVENT_ALERT); } reset(); break; } case CAL_DEC_INC: { // Star position resulting from LAST guiding pulse to mount double cur_x, cur_y; pmath->getStarScreenPosition(&cur_x, &cur_y); qCDebug(KSTARS_EKOS_GUIDE) << "Iteration #" << m_CalibrationParams.dec_iterations << ": STAR " << cur_x << "," << cur_y; qCDebug(KSTARS_EKOS_GUIDE) << "Iteration " << m_CalibrationParams.dec_iterations << " Direction: DEC_INC_DIR" << " Duration: " << m_CalibrationParams.last_pulse << " ms."; if (m_CalibrationParams.dec_iterations >= m_CalibrationParams.auto_drift_time && (fabs(cur_x-m_CalibrationCoords.start_x2) > 1.5 || fabs(cur_y-m_CalibrationCoords.start_y2) > 1.5)) { calibrationStage = CAL_DEC_DEC; m_CalibrationParams.de_total_pulse += m_CalibrationParams.last_pulse; m_CalibrationCoords.end_x2 = cur_x; m_CalibrationCoords.end_y2 = cur_y; m_CalibrationCoords.last_x = cur_x; m_CalibrationCoords.last_y = cur_y; axis_calibration_complete = false; qCDebug(KSTARS_EKOS_GUIDE) << "End X2 " << m_CalibrationCoords.end_x2 << " End Y2 " << m_CalibrationCoords.end_y2; m_CalibrationParams.phi = pmath->calculatePhi(m_CalibrationCoords.start_x2, m_CalibrationCoords.start_y2, m_CalibrationCoords.end_x2, m_CalibrationCoords.end_y2); ROT_Z = RotateZ(-M_PI * m_CalibrationParams.phi / 180.0); // derotates... m_CalibrationCoords.de_distance = 0; emit newPulse(DEC_DEC_DIR, m_CalibrationParams.last_pulse); emit newLog(i18n("DEC drifting reverse...")); m_CalibrationParams.dec_iterations++; } else if (m_CalibrationParams.dec_iterations > m_CalibrationParams.turn_back_time) { calibrationStage = CAL_ERROR; emit newStatus(Ekos::GUIDE_CALIBRATION_ERROR); emit newLog(i18np("Guide DEC: Scope cannot reach the start point after %1 iteration.\nPossible mount " "or backlash problems...", "GUIDE DEC: Scope cannot reach the start point after %1 iterations.\nPossible mount " "or backlash problems...", m_CalibrationParams.dec_iterations)); KSNotification::event(QLatin1String("CalibrationFailed"), i18n("Guiding calibration failed with errors"), KSNotification::EVENT_ALERT); reset(); } else { if (fabs(cur_x-m_CalibrationCoords.last_x) < 0.5 && fabs(cur_y-m_CalibrationCoords.last_y) < 0.5) { // Increase pulse by 200% m_CalibrationParams.last_pulse = Options::calibrationPulseDuration()*2; } else { m_CalibrationParams.de_total_pulse += m_CalibrationParams.last_pulse; m_CalibrationParams.last_pulse = Options::calibrationPulseDuration(); } m_CalibrationCoords.last_x = cur_x; m_CalibrationCoords.last_y = cur_y; emit newPulse(DEC_INC_DIR, m_CalibrationParams.last_pulse); m_CalibrationParams.dec_iterations++; } } break; case CAL_DEC_DEC: { //----- Z-check (new!) ----- double cur_x, cur_y; pmath->getStarScreenPosition(&cur_x, &cur_y); // Star position resulting from LAST guiding pulse to mount qCDebug(KSTARS_EKOS_GUIDE) << "Iteration #" << m_CalibrationParams.dec_iterations << ": STAR " << cur_x << "," << cur_y; qCDebug(KSTARS_EKOS_GUIDE) << "Iteration " << m_CalibrationParams.dec_iterations << " Direction: DEC_DEC_DIR" << " Duration: " << m_CalibrationParams.last_pulse << " ms."; Vector star_pos = Vector(cur_x, cur_y, 0) - Vector(m_CalibrationCoords.start_x2, m_CalibrationCoords.start_y2, 0); star_pos.y = -star_pos.y; star_pos = star_pos * ROT_Z; qCDebug(KSTARS_EKOS_GUIDE) << "start Pos X " << star_pos.x << " from original point."; // Keep track of distance if (m_CalibrationCoords.de_distance == 0.0) m_CalibrationCoords.de_distance = star_pos.x; // start point reached... so exit if (star_pos.x < 1.5) { pmath->performProcessing(); m_CalibrationParams.de_total_pulse += m_CalibrationParams.last_pulse; m_CalibrationParams.last_pulse = Options::calibrationPulseDuration(); axis_calibration_complete = true; } // Increase pulse if we're not moving much or if we are moving _away_ from target. else if ( (fabs(cur_x-m_CalibrationCoords.last_x) < 0.5 && fabs(cur_y-m_CalibrationCoords.last_y) < 0.5) || star_pos.x > m_CalibrationCoords.de_distance) { // Increase pulse by 200% m_CalibrationParams.last_pulse = Options::calibrationPulseDuration()*2; } else { m_CalibrationParams.de_total_pulse += m_CalibrationParams.last_pulse; m_CalibrationParams.last_pulse = Options::calibrationPulseDuration(); } if (axis_calibration_complete == false) { if (m_CalibrationParams.dec_iterations < m_CalibrationParams.turn_back_time) { emit newPulse(DEC_DEC_DIR, m_CalibrationParams.last_pulse); m_CalibrationParams.dec_iterations++; break; } calibrationStage = CAL_ERROR; emit newStatus(Ekos::GUIDE_CALIBRATION_ERROR); emit newLog(i18np("Guide DEC: Scope cannot reach the start point after %1 iteration.\nPossible mount " "or backlash problems...", "Guide DEC: Scope cannot reach the start point after %1 iterations.\nPossible mount " "or backlash problems...", m_CalibrationParams.dec_iterations)); KSNotification::event(QLatin1String("CalibrationFailed"), i18n("Guiding calibration failed with errors"),KSNotification::EVENT_ALERT); reset(); break; } bool swap_dec = false; // calc orientation if (pmath->calculateAndSetReticle2D(m_CalibrationCoords.start_x1, m_CalibrationCoords.start_y1, m_CalibrationCoords.end_x1, m_CalibrationCoords.end_y1, m_CalibrationCoords.start_x2, m_CalibrationCoords.start_y2, m_CalibrationCoords.end_x2, m_CalibrationCoords.end_y2, &swap_dec, m_CalibrationParams.ra_total_pulse, m_CalibrationParams.de_total_pulse)) { calibrationStage = CAL_IDLE; //fillInterface(); if (swap_dec) emit newLog(i18n("DEC swap enabled.")); else emit newLog(i18n("DEC swap disabled.")); emit newStatus(Ekos::GUIDE_CALIBRATION_SUCESS); emit DESwapChanged(swap_dec); KSNotification::event(QLatin1String("CalibrationSuccessful"), i18n("Guiding calibration completed successfully")); //if (ui.autoStarCheck->isChecked()) //guideModule->selectAutoStar(); } else { emit newLog(i18n("Calibration rejected. Star drift is too short. Check for mount, cable, or backlash problems.")); emit newStatus(Ekos::GUIDE_CALIBRATION_ERROR); //ui.startCalibrationLED->setColor(alertColor); calibrationStage = CAL_ERROR; KSNotification::event(QLatin1String("CalibrationFailed"), i18n("Guiding calibration failed with errors"),KSNotification::EVENT_ALERT); } reset(); break; } default: break; } } void InternalGuider::setStarPosition(QVector3D &starCenter) { pmath->setReticleParameters(starCenter.x(), starCenter.y(), -1); } void InternalGuider::trackingStarSelected(int x, int y) { if (calibrationStage == CAL_IDLE) return; //int square_size = guide_squares[pmath->getSquareIndex()].size; pmath->setReticleParameters(x, y, -1); //pmath->moveSquare(x-square_size/(2*pmath->getBinX()), y-square_size/(2*pmath->getBinY())); //update_reticle_pos(x, y); //ui.selectStarLED->setColor(okColor); calibrationStage = CAL_START; //ui.pushButton_StartCalibration->setEnabled(true); /*QVector3D starCenter; starCenter.setX(x); starCenter.setY(y); emit newStarPosition(starCenter, true);*/ //if (ui.autoStarCheck->isChecked()) //if (Options::autoStarEnabled()) //calibrate(); } void InternalGuider::setDECSwap(bool enable) { pmath->setDeclinationSwapEnabled(enable); } void InternalGuider::setSquareAlgorithm(int index) { pmath->setSquareAlgorithm(index); } void InternalGuider::setReticleParameters(double x, double y, double angle) { pmath->setReticleParameters(x, y, angle); } bool InternalGuider::getReticleParameters(double *x, double *y, double *angle) { return pmath->getReticleParameters(x, y, angle); } bool InternalGuider::setGuiderParams(double ccdPixelSizeX, double ccdPixelSizeY, double mountAperture, double mountFocalLength) { this->ccdPixelSizeX = ccdPixelSizeX; this->ccdPixelSizeY = ccdPixelSizeY; this->mountAperture = mountAperture; this->mountFocalLength = mountFocalLength; return pmath->setGuiderParameters(ccdPixelSizeX, ccdPixelSizeY, mountAperture, mountFocalLength); } bool InternalGuider::setFrameParams(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t binX, uint16_t binY) { if (w <= 0 || h <= 0) return false; subX = x; subY = y; subW = w; subH = h; subBinX = binX; subBinY = binY; pmath->setVideoParameters(w, h, subBinX, subBinY); return true; } bool InternalGuider::processGuiding() { const cproc_out_params *out; uint32_t tick = 0; // On first frame, center the box (reticle) around the star so we do not start with an offset the results in // unnecessary guiding pulses. if (m_isFirstFrame) { if (state == GUIDE_GUIDING) { Vector star_pos = pmath->findLocalStarPosition(); pmath->setReticleParameters(star_pos.x, star_pos.y, -1); } m_isFirstFrame = false; } // calc math. it tracks square pmath->performProcessing(); if (pmath->isStarLost()) m_starLostCounter++; else m_starLostCounter=0; // do pulse out = pmath->getOutputParameters(); bool sendPulses = true; // If within 95% of max pulse repeatedly, let's abort // if (out->pulse_length[GUIDE_RA] >= (0.95 * Options::rAMaximumPulse()) || // out->pulse_length[GUIDE_DEC] >= (0.95 * Options::dECMaximumPulse())) // { // // Stop sending pulses in case we are guiding and we already sent one high pulse before // // since we do not want to stray too much off the target to purse the guiding star // if (state == GUIDE_GUIDING && m_highPulseCounter > 0) // sendPulses = false; // m_highPulseCounter++; // } // else // m_highPulseCounter=0; double delta_rms = sqrt(out->delta[GUIDE_RA]*out->delta[GUIDE_RA] + out->delta[GUIDE_DEC]*out->delta[GUIDE_DEC]); if (delta_rms > Options::guideMaxDeltaRMS()) { // Stop sending pulses on the 3rd time the delta RMS is high // so that we don't stray too far off the main target. if (state == GUIDE_GUIDING && m_highRMSCounter > 2) sendPulses = false; m_highRMSCounter++; } else m_highRMSCounter=0; uint8_t abortStarLostThreshold = (state == GUIDE_DITHERING || state == GUIDE_MANUAL_DITHERING) ? MAX_LOST_STAR_THRESHOLD * 3 : MAX_LOST_STAR_THRESHOLD; uint8_t abortRMSThreshold = (state == GUIDE_DITHERING || state == GUIDE_MANUAL_DITHERING) ? MAX_RMS_THRESHOLD * 3 : MAX_RMS_THRESHOLD; if (m_starLostCounter > abortStarLostThreshold || m_highRMSCounter > abortRMSThreshold) { qCDebug(KSTARS_EKOS_GUIDE) << "m_starLostCounter" << m_starLostCounter << "m_highRMSCounter" << m_highRMSCounter << "delta_rms" << delta_rms; if (m_starLostCounter > abortStarLostThreshold) emit newLog(i18n("Lost track of the guide star. Searching for guide stars...")); else emit newLog(i18n("Delta RMS threshold value exceeded. Searching for guide stars...")); reacquireTimer.start(); rememberState = state; state = GUIDE_REACQUIRE; emit newStatus(state); return true; } if (sendPulses) { emit newPulse(out->pulse_dir[GUIDE_RA] , out->pulse_length[GUIDE_RA], out->pulse_dir[GUIDE_DEC], out->pulse_length[GUIDE_DEC]); // Wait until pulse is over before capturing an image const int waitMS = qMax(out->pulse_length[GUIDE_RA], out->pulse_length[GUIDE_DEC]); // If less than MAX_IMMEDIATE_CAPTURE ms, then capture immediately if (waitMS > MAX_IMMEDIATE_CAPTURE) // Issue frame requests MAX_IMMEDIATE_CAPTURE ms before timeout to account for // propagation delays QTimer::singleShot(waitMS - PROPAGATION_DELAY, [&]() { emit frameCaptureRequested(); }); else emit frameCaptureRequested(); } else emit frameCaptureRequested(); if (state == GUIDE_DITHERING || state == GUIDE_MANUAL_DITHERING) return true; tick = pmath->getTicks(); emit newAxisDelta(out->delta[GUIDE_RA], out->delta[GUIDE_DEC]); double raPulse = out->pulse_length[GUIDE_RA]; double dePulse = out->pulse_length[GUIDE_DEC]; //If the pulse was not sent to the mount, it should have 0 value if(out->pulse_dir[GUIDE_RA]==NO_DIR) raPulse = 0; //If the pulse was not sent to the mount, it should have 0 value if(out->pulse_dir[GUIDE_DEC]==NO_DIR) dePulse = 0; //If the pulse was in the Negative direction, it should have a negative sign. if(out->pulse_dir[GUIDE_RA]==RA_INC_DIR) raPulse = -raPulse; //If the pulse was in the Negative direction, it should have a negative sign. if(out->pulse_dir[GUIDE_DEC]==DEC_INC_DIR) dePulse = -dePulse; emit newAxisPulse(raPulse, dePulse); emit newAxisSigma(out->sigma[GUIDE_RA], out->sigma[GUIDE_DEC]); return true; } bool InternalGuider::processImageGuiding() { static int maxPulseCounter = 0; const cproc_out_params *out; uint32_t tick = 0; // calc math. it tracks square pmath->performProcessing(); if (pmath->isStarLost() && ++m_starLostCounter > 2) { emit newLog(i18n("Lost track of phase shift.")); abort(); return false; } else m_starLostCounter = 0; // do pulse out = pmath->getOutputParameters(); // If within 90% of max pulse repeatedly, let's abort if (out->pulse_length[GUIDE_RA] >= (0.9 * Options::rAMaximumPulse()) || out->pulse_length[GUIDE_DEC] >= (0.9 * Options::dECMaximumPulse())) maxPulseCounter++; else maxPulseCounter = 0; if (maxPulseCounter >= 3) { emit newLog(i18n("Lost track of phase shift. Aborting guiding...")); abort(); maxPulseCounter = 0; return false; } emit newPulse(out->pulse_dir[GUIDE_RA], out->pulse_length[GUIDE_RA], out->pulse_dir[GUIDE_DEC], out->pulse_length[GUIDE_DEC]); emit frameCaptureRequested(); if (state == GUIDE_DITHERING || state == GUIDE_MANUAL_DITHERING) return true; tick = pmath->getTicks(); if (tick & 1) { // draw some params in window emit newAxisDelta(out->delta[GUIDE_RA], out->delta[GUIDE_DEC]); emit newAxisPulse(out->pulse_length[GUIDE_RA], out->pulse_length[GUIDE_DEC]); emit newAxisSigma(out->sigma[GUIDE_RA], out->sigma[GUIDE_DEC]); } return true; } bool InternalGuider::isImageGuideEnabled() const { return m_ImageGuideEnabled; } void InternalGuider::setImageGuideEnabled(bool value) { m_ImageGuideEnabled = value; pmath->setImageGuideEnabled(value); } void InternalGuider::setRegionAxis(uint32_t value) { pmath->setRegionAxis(value); } QList InternalGuider::getGuideStars() { return pmath->PSFAutoFind(); } bool InternalGuider::selectAutoStar() { FITSData *imageData = guideFrame->getImageData(); if (imageData == nullptr) return false; bool useNativeDetection = false; QList starCenters; if (Options::guideAlgorithm() != SEP_THRESHOLD) starCenters = pmath->PSFAutoFind(); if (starCenters.empty()) { if (Options::guideAlgorithm() == SEP_THRESHOLD) imageData->findStars(ALGORITHM_SEP); else imageData->findStars(); starCenters = imageData->getStarCenters(); if (starCenters.empty()) return false; useNativeDetection = true; // For SEP, prefer flux total if (Options::guideAlgorithm() == SEP_THRESHOLD) - qSort(starCenters.begin(), starCenters.end(), [](const Edge *a, const Edge *b) { return a->val > b->val; }); + std::sort(starCenters.begin(), starCenters.end(), [](const Edge *a, const Edge *b) { return a->val > b->val; }); else - qSort(starCenters.begin(), starCenters.end(), [](const Edge *a, const Edge *b) { return a->width > b->width; }); + std::sort(starCenters.begin(), starCenters.end(), [](const Edge *a, const Edge *b) { return a->width > b->width; }); guideFrame->setStarsEnabled(true); guideFrame->updateFrame(); } int maxX = imageData->width(); int maxY = imageData->height(); int scores[MAX_GUIDE_STARS]; int maxIndex = MAX_GUIDE_STARS < starCenters.count() ? MAX_GUIDE_STARS : starCenters.count(); for (int i = 0; i < maxIndex; i++) { int score = 100; Edge *center = starCenters.at(i); if (useNativeDetection) { // Severely reject stars close to edges if (center->x < (center->width * 5) || center->y < (center->width * 5) || center->x > (maxX - center->width * 5) || center->y > (maxY - center->width * 5)) score -= 1000; // Reject stars bigger than square if (center->width > float(guideBoxSize) / subBinX) score -= 1000; else { if (Options::guideAlgorithm() == SEP_THRESHOLD) score += sqrt(center->val); else // Moderately favor brighter stars score += center->width * center->width; } // Moderately reject stars close to other stars foreach (Edge *edge, starCenters) { if (edge == center) continue; if (fabs(center->x - edge->x) < center->width * 2 && fabs(center->y - edge->y) < center->width * 2) { score -= 15; break; } } } else { score = center->val; } scores[i] = score; } int maxScore = -1; int maxScoreIndex = -1; for (int i = 0; i < maxIndex; i++) { if (scores[i] > maxScore) { maxScore = scores[i]; maxScoreIndex = i; } } if (maxScoreIndex < 0) { qCDebug(KSTARS_EKOS_GUIDE) << "No suitable star detected."; return false; } /*if (ui.autoSquareSizeCheck->isEnabled() && ui.autoSquareSizeCheck->isChecked()) { // Select appropriate square size int idealSize = ceil(starCenters[maxScoreIndex]->width * 1.5); if (Options::guideLogging()) qDebug() << "Guide: Ideal calibration box size for star width: " << starCenters[maxScoreIndex]->width << " is " << idealSize << " pixels"; // TODO Set square size in GuideModule }*/ QVector3D newStarCenter(starCenters[maxScoreIndex]->x, starCenters[maxScoreIndex]->y, 0); if (useNativeDetection == false) qDeleteAll(starCenters); emit newStarPosition(newStarCenter, true); return true; } bool InternalGuider::reacquire() { bool rc = selectAutoStar(); if (rc) { m_highRMSCounter=m_starLostCounter=0; m_isFirstFrame = true; pmath->reset(); // If we were in the process of dithering, wait until settle and resume if (rememberState == GUIDE_DITHERING || state == GUIDE_MANUAL_DITHERING) { if (Options::ditherSettle() > 0) { state = GUIDE_DITHERING_SETTLE; emit newStatus(state); } QTimer::singleShot(Options::ditherSettle()* 1000, this, SLOT(setDitherSettled())); } else { state = GUIDE_GUIDING; emit newStatus(state); } } else if (reacquireTimer.elapsed() > static_cast(Options::guideLostStarTimeout()*1000)) { emit newLog(i18n("Failed to find any suitable guide stars. Aborting...")); abort(); return false; } emit frameCaptureRequested(); return rc; } } diff --git a/kstars/ekos/scheduler/scheduler.cpp b/kstars/ekos/scheduler/scheduler.cpp index 403693406..e8bb0371c 100644 --- a/kstars/ekos/scheduler/scheduler.cpp +++ b/kstars/ekos/scheduler/scheduler.cpp @@ -1,7068 +1,7076 @@ /* Ekos Scheduler Module Copyright (C) 2015 Jasem Mutlaq DBus calls from GSoC 2015 Ekos Scheduler project by Daniel Leu This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "scheduler.h" #include "ksalmanac.h" #include "ksnotification.h" #include "kstars.h" #include "kstarsdata.h" #include "ksutils.h" #include "skymap.h" #include "mosaic.h" #include "Options.h" #include "scheduleradaptor.h" #include "schedulerjob.h" #include "skymapcomposite.h" #include "auxiliary/QProgressIndicator.h" #include "dialogs/finddialog.h" #include "ekos/manager.h" #include "ekos/capture/sequencejob.h" #include "skyobjects/starobject.h" #include #include #include #include #define BAD_SCORE -1000 #define MAX_FAILURE_ATTEMPTS 5 #define UPDATE_PERIOD_MS 1000 #define RESTART_GUIDING_DELAY_MS 5000 #define DEFAULT_CULMINATION_TIME -60 #define DEFAULT_MIN_ALTITUDE 15 #define DEFAULT_MIN_MOON_SEPARATION 0 namespace Ekos { Scheduler::Scheduler() { setupUi(this); qRegisterMetaType("Ekos::SchedulerState"); qDBusRegisterMetaType(); new SchedulerAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Scheduler", this); dirPath = QUrl::fromLocalFile(QDir::homePath()); // Get current KStars time and set seconds to zero QDateTime currentDateTime = KStarsData::Instance()->lt(); QTime currentTime = currentDateTime.time(); currentTime.setHMS(currentTime.hour(), currentTime.minute(), 0); currentDateTime.setTime(currentTime); // Set initial time for startup and completion times startupTimeEdit->setDateTime(currentDateTime); completionTimeEdit->setDateTime(currentDateTime); // Set up DBus interfaces QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Scheduler", this); ekosInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", QDBusConnection::sessionBus(), this); // Example of connecting DBus signals //connect(ekosInterface, SIGNAL(indiStatusChanged(Ekos::CommunicationStatus)), this, SLOT(setINDICommunicationStatus(Ekos::CommunicationStatus))); //connect(ekosInterface, SIGNAL(ekosStatusChanged(Ekos::CommunicationStatus)), this, SLOT(setEkosCommunicationStatus(Ekos::CommunicationStatus))); //connect(ekosInterface, SIGNAL(newModule(QString)), this, SLOT(registerNewModule(QString))); QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "newModule", this, SLOT(registerNewModule(QString))); QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "indiStatusChanged", this, SLOT(setINDICommunicationStatus(Ekos::CommunicationStatus))); QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "ekosStatusChanged", this, SLOT(setEkosCommunicationStatus(Ekos::CommunicationStatus))); sleepLabel->setPixmap( QIcon::fromTheme("chronometer").pixmap(QSize(32, 32))); sleepLabel->hide(); connect(&sleepTimer, &QTimer::timeout, this, &Scheduler::wakeUpScheduler); schedulerTimer.setInterval(UPDATE_PERIOD_MS); jobTimer.setInterval(UPDATE_PERIOD_MS); connect(&schedulerTimer, &QTimer::timeout, this, &Scheduler::checkStatus); connect(&jobTimer, &QTimer::timeout, this, &Scheduler::checkJobStage); restartGuidingTimer.setSingleShot(true); restartGuidingTimer.setInterval(RESTART_GUIDING_DELAY_MS); connect(&restartGuidingTimer, &QTimer::timeout, this, [this]() { startGuiding(true); }); pi = new QProgressIndicator(this); bottomLayout->addWidget(pi, 0, nullptr); geo = KStarsData::Instance()->geo(); raBox->setDegType(false); //RA box should be HMS-style /* FIXME: Find a way to have multi-line tooltips in the .ui file, then move the widget configuration there - what about i18n? */ queueTable->setToolTip(i18n("Job scheduler list.\nClick to select a job in the list.\nDouble click to edit a job with the left-hand fields.")); /* Set first button mode to add observation job from left-hand fields */ setJobAddApply(true); removeFromQueueB->setIcon(QIcon::fromTheme("list-remove")); removeFromQueueB->setToolTip(i18n("Remove selected job from the observation list.\nJob properties are copied in the edition fields before removal.")); removeFromQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueUpB->setIcon(QIcon::fromTheme("go-up")); queueUpB->setToolTip(i18n("Move selected job one line up in the list.\n" "Order only affect observation jobs that are scheduled to start at the same time.\n" "Not available if option \"Sort jobs by Altitude and Priority\" is set.")); queueUpB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueDownB->setIcon(QIcon::fromTheme("go-down")); queueDownB->setToolTip(i18n("Move selected job one line down in the list.\n" "Order only affect observation jobs that are scheduled to start at the same time.\n" "Not available if option \"Sort jobs by Altitude and Priority\" is set.")); queueDownB->setAttribute(Qt::WA_LayoutUsesWidgetRect); evaluateOnlyB->setIcon(QIcon::fromTheme("system-reboot")); evaluateOnlyB->setToolTip(i18n("Reset state and force reevaluation of all observation jobs.")); evaluateOnlyB->setAttribute(Qt::WA_LayoutUsesWidgetRect); sortJobsB->setIcon(QIcon::fromTheme("transform-move-vertical")); sortJobsB->setToolTip(i18n("Reset state and sort observation jobs per altitude and movement in sky, using the start time of the first job.\n" "This action sorts setting targets before rising targets, and may help scheduling when starting your observation.\n" "Option \"Sort Jobs by Altitude and Priority\" keeps the job list sorted this way, but with current time as reference.\n" "Note the algorithm first calculates all altitudes using the same time, then evaluates jobs.")); sortJobsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mosaicB->setIcon(QIcon::fromTheme("zoom-draw")); mosaicB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueSaveAsB->setIcon(QIcon::fromTheme("document-save-as")); queueSaveAsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueSaveB->setIcon(QIcon::fromTheme("document-save")); queueSaveB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueLoadB->setIcon(QIcon::fromTheme("document-open")); queueLoadB->setAttribute(Qt::WA_LayoutUsesWidgetRect); loadSequenceB->setIcon(QIcon::fromTheme("document-open")); loadSequenceB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectStartupScriptB->setIcon(QIcon::fromTheme("document-open")); selectStartupScriptB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectShutdownScriptB->setIcon( QIcon::fromTheme("document-open")); selectShutdownScriptB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectFITSB->setIcon(QIcon::fromTheme("document-open")); selectFITSB->setAttribute(Qt::WA_LayoutUsesWidgetRect); startupB->setIcon( QIcon::fromTheme("media-playback-start")); startupB->setAttribute(Qt::WA_LayoutUsesWidgetRect); shutdownB->setIcon( QIcon::fromTheme("media-playback-start")); shutdownB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(startupB, &QPushButton::clicked, this, &Scheduler::runStartupProcedure); connect(shutdownB, &QPushButton::clicked, this, &Scheduler::runShutdownProcedure); connect(selectObjectB, &QPushButton::clicked, this, &Scheduler::selectObject); connect(selectFITSB, &QPushButton::clicked, this, &Scheduler::selectFITS); connect(loadSequenceB, &QPushButton::clicked, this, &Scheduler::selectSequence); connect(selectStartupScriptB, &QPushButton::clicked, this, &Scheduler::selectStartupScript); connect(selectShutdownScriptB, &QPushButton::clicked, this, &Scheduler::selectShutdownScript); connect(mosaicB, &QPushButton::clicked, this, &Scheduler::startMosaicTool); connect(addToQueueB, &QPushButton::clicked, this, &Scheduler::addJob); connect(removeFromQueueB, &QPushButton::clicked, this, &Scheduler::removeJob); connect(queueUpB, &QPushButton::clicked, this, &Scheduler::moveJobUp); connect(queueDownB, &QPushButton::clicked, this, &Scheduler::moveJobDown); connect(evaluateOnlyB, &QPushButton::clicked, this, &Scheduler::startJobEvaluation); connect(sortJobsB, &QPushButton::clicked, this, &Scheduler::sortJobsPerAltitude); connect(queueTable->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &Scheduler::queueTableSelectionChanged); connect(queueTable, &QAbstractItemView::clicked, this, &Scheduler::clickQueueTable); connect(queueTable, &QAbstractItemView::doubleClicked, this, &Scheduler::loadJob); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setAttribute(Qt::WA_LayoutUsesWidgetRect); pauseB->setIcon(QIcon::fromTheme("media-playback-pause")); pauseB->setAttribute(Qt::WA_LayoutUsesWidgetRect); pauseB->setCheckable(false); connect(startB, &QPushButton::clicked, this, &Scheduler::toggleScheduler); connect(pauseB, &QPushButton::clicked, this, &Scheduler::pause); connect(queueSaveAsB, &QPushButton::clicked, this, &Scheduler::saveAs); connect(queueSaveB, &QPushButton::clicked, this, &Scheduler::save); connect(queueLoadB, &QPushButton::clicked, this, &Scheduler::load); connect(twilightCheck, &QCheckBox::toggled, this, &Scheduler::checkTwilightWarning); // Connect simulation clock scale connect(KStarsData::Instance()->clock(), &SimClock::scaleChanged, this, &Scheduler::simClockScaleChanged); // restore default values for error handling strategy setErrorHandlingStrategy(static_cast(Options::errorHandlingStrategy())); errorHandlingRescheduleErrorsCB->setChecked(Options::rescheduleErrors()); errorHandlingDelaySB->setValue(Options::errorHandlingStrategyDelay()); // save new default values for error handling strategy connect(errorHandlingRescheduleErrorsCB, &QPushButton::clicked, [](bool checked) { Options::setRescheduleErrors(checked); }); connect(errorHandlingButtonGroup, static_cast(&QButtonGroup::buttonClicked), [this](QAbstractButton * button) { Q_UNUSED(button) Options::setErrorHandlingStrategy(getErrorHandlingStrategy()); }); connect(errorHandlingDelaySB, static_cast(&QSpinBox::valueChanged), [](int value) { Options::setErrorHandlingStrategyDelay(value); }); connect(copySkyCenterB, &QPushButton::clicked, this, [this]() { SkyPoint center = SkyMap::Instance()->getCenterPoint(); center.deprecess(KStarsData::Instance()->updateNum()); raBox->setDMS(center.ra0().toHMSString()); decBox->setDMS(center.dec0().toDMSString()); }); connect(KConfigDialog::exists("settings"), &KConfigDialog::settingsChanged, this, &Scheduler::applyConfig); calculateDawnDusk(); loadProfiles(); watchJobChanges(true); } QString Scheduler::getCurrentJobName() { return (currentJob != nullptr ? currentJob->getName() : ""); } void Scheduler::watchJobChanges(bool enable) { /* Don't double watch, this will cause multiple signals to be connected */ if (enable == jobChangesAreWatched) return; /* These are the widgets we want to connect, per signal function, to listen for modifications */ QLineEdit * const lineEdits[] = { nameEdit, raBox, decBox, fitsEdit, sequenceEdit, startupScript, shutdownScript }; QDateTimeEdit * const dateEdits[] = { startupTimeEdit, completionTimeEdit }; QComboBox * const comboBoxes[] = { schedulerProfileCombo }; QButtonGroup * const buttonGroups[] = { stepsButtonGroup, errorHandlingButtonGroup, startupButtonGroup, constraintButtonGroup, completionButtonGroup, startupProcedureButtonGroup, shutdownProcedureGroup }; QAbstractButton * const buttons[] = { errorHandlingRescheduleErrorsCB }; QSpinBox * const spinBoxes[] = { culminationOffset, repeatsSpin, prioritySpin, errorHandlingDelaySB }; QDoubleSpinBox * const dspinBoxes[] = { minMoonSeparation, minAltitude }; if (enable) { /* Connect the relevant signal to setDirty. Note that we are not keeping the connection object: we will * only use that signal once, and there will be no leaks. If we were connecting multiple receiver functions * to the same signal, we would have to be selective when disconnecting. We also use a lambda to absorb the * excess arguments which cannot be passed to setDirty, and limit captured arguments to 'this'. * The main problem with this implementation compared to the macro method is that it is now possible to * stack signal connections. That is, multiple calls to WatchJobChanges will cause multiple signal-to-slot * instances to be registered. As a result, one click will produce N signals, with N*=2 for each call to * WatchJobChanges(true) missing its WatchJobChanges(false) counterpart. */ for (auto * const control : lineEdits) connect(control, &QLineEdit::editingFinished, this, [this]() { setDirty(); }); for (auto * const control : dateEdits) connect(control, &QDateTimeEdit::editingFinished, this, [this]() { setDirty(); }); for (auto * const control : comboBoxes) connect(control, static_cast(&QComboBox::currentIndexChanged), this, [this]() { setDirty(); }); for (auto * const control : buttonGroups) connect(control, static_cast(&QButtonGroup::buttonToggled), this, [this](int, bool) { setDirty(); }); for (auto * const control : buttons) connect(control, static_cast(&QAbstractButton::clicked), this, [this](bool) { setDirty(); }); for (auto * const control : spinBoxes) connect(control, static_cast(&QSpinBox::valueChanged), this, [this]() { setDirty(); }); for (auto * const control : dspinBoxes) connect(control, static_cast(&QDoubleSpinBox::valueChanged), this, [this](double) { setDirty(); }); } else { /* Disconnect the relevant signal from each widget. Actually, this method removes all signals from the widgets, * because we did not take care to keep the connection object when connecting. No problem in our case, we do not * expect other signals to be connected. Because we used a lambda, we cannot use the same function object to * disconnect selectively. */ for (auto * const control : lineEdits) disconnect(control, &QLineEdit::editingFinished, this, nullptr); for (auto * const control : dateEdits) disconnect(control, &QDateTimeEdit::editingFinished, this, nullptr); for (auto * const control : comboBoxes) disconnect(control, static_cast(&QComboBox::currentIndexChanged), this, nullptr); for (auto * const control : buttons) disconnect(control, static_cast(&QAbstractButton::clicked), this, nullptr); for (auto * const control : buttonGroups) disconnect(control, static_cast(&QButtonGroup::buttonToggled), this, nullptr); for (auto * const control : spinBoxes) disconnect(control, static_cast(&QSpinBox::valueChanged), this, nullptr); for (auto * const control : dspinBoxes) disconnect(control, static_cast(&QDoubleSpinBox::valueChanged), this, nullptr); } jobChangesAreWatched = enable; } void Scheduler::appendLogText(const QString &text) { /* FIXME: user settings for log length */ int const max_log_count = 2000; if (m_LogText.size() > max_log_count) m_LogText.removeLast(); m_LogText.prepend(i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_SCHEDULER) << text; emit newLog(text); } void Scheduler::clearLog() { m_LogText.clear(); emit newLog(QString()); } void Scheduler::applyConfig() { calculateDawnDusk(); if (SCHEDULER_RUNNING != state) { jobEvaluationOnly = true; evaluateJobs(); } } void Scheduler::selectObject() { if (FindDialog::Instance()->exec() == QDialog::Accepted) { SkyObject *object = FindDialog::Instance()->targetObject(); addObject(object); } } void Scheduler::addObject(SkyObject *object) { if (object != nullptr) { QString finalObjectName(object->name()); if (object->name() == "star") { StarObject *s = dynamic_cast(object); if (s->getHDIndex() != 0) finalObjectName = QString("HD %1").arg(s->getHDIndex()); } nameEdit->setText(finalObjectName); raBox->showInHours(object->ra0()); decBox->showInDegrees(object->dec0()); addToQueueB->setEnabled(sequenceEdit->text().isEmpty() == false); mosaicB->setEnabled(sequenceEdit->text().isEmpty() == false); setDirty(); } } void Scheduler::selectFITS() { fitsURL = QFileDialog::getOpenFileUrl(this, i18n("Select FITS Image"), dirPath, "FITS (*.fits *.fit)"); if (fitsURL.isEmpty()) return; dirPath = QUrl(fitsURL.url(QUrl::RemoveFilename)); fitsEdit->setText(fitsURL.toLocalFile()); if (nameEdit->text().isEmpty()) nameEdit->setText(fitsURL.fileName()); addToQueueB->setEnabled(sequenceEdit->text().isEmpty() == false); mosaicB->setEnabled(sequenceEdit->text().isEmpty() == false); processFITSSelection(); setDirty(); } void Scheduler::processFITSSelection() { const QString filename = fitsEdit->text(); int status = 0; double ra = 0, dec = 0; dms raDMS, deDMS; char comment[128], error_status[512]; fitsfile *fptr = nullptr; if (fits_open_diskfile(&fptr, filename.toLatin1(), READONLY, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); qCCritical(KSTARS_EKOS_SCHEDULER) << QString::fromUtf8(error_status); return; } status = 0; if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); qCCritical(KSTARS_EKOS_SCHEDULER) << QString::fromUtf8(error_status); return; } status = 0; char objectra_str[32] = {0}; if (fits_read_key(fptr, TSTRING, "OBJCTRA", objectra_str, comment, &status)) { if (fits_read_key(fptr, TDOUBLE, "RA", &ra, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find OBJCTRA (%1).", QString(error_status))); return; } raDMS.setD(ra); } else { raDMS = dms::fromString(objectra_str, false); } status = 0; char objectde_str[32] = {0}; if (fits_read_key(fptr, TSTRING, "OBJCTDEC", objectde_str, comment, &status)) { if (fits_read_key(fptr, TDOUBLE, "DEC", &dec, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find OBJCTDEC (%1).", QString(error_status))); return; } deDMS.setD(dec); } else { deDMS = dms::fromString(objectde_str, true); } raBox->setDMS(raDMS.toHMSString()); decBox->setDMS(deDMS.toDMSString()); char object_str[256] = {0}; if (fits_read_key(fptr, TSTRING, "OBJECT", object_str, comment, &status)) { QFileInfo info(filename); nameEdit->setText(info.baseName()); } else { nameEdit->setText(object_str); } } void Scheduler::selectSequence() { sequenceURL = QFileDialog::getOpenFileUrl(this, i18n("Select Sequence Queue"), dirPath, i18n("Ekos Sequence Queue (*.esq)")); if (sequenceURL.isEmpty()) return; dirPath = QUrl(sequenceURL.url(QUrl::RemoveFilename)); sequenceEdit->setText(sequenceURL.toLocalFile()); // For object selection, all fields must be filled if ((raBox->isEmpty() == false && decBox->isEmpty() == false && nameEdit->text().isEmpty() == false) // For FITS selection, only the name and fits URL should be filled. || (nameEdit->text().isEmpty() == false && fitsURL.isEmpty() == false)) { addToQueueB->setEnabled(true); mosaicB->setEnabled(true); } setDirty(); } void Scheduler::selectStartupScript() { startupScriptURL = QFileDialog::getOpenFileUrl(this, i18n("Select Startup Script"), dirPath, i18n("Script (*)")); if (startupScriptURL.isEmpty()) return; dirPath = QUrl(startupScriptURL.url(QUrl::RemoveFilename)); mDirty = true; startupScript->setText(startupScriptURL.toLocalFile()); } void Scheduler::selectShutdownScript() { shutdownScriptURL = QFileDialog::getOpenFileUrl(this, i18n("Select Shutdown Script"), dirPath, i18n("Script (*)")); if (shutdownScriptURL.isEmpty()) return; dirPath = QUrl(shutdownScriptURL.url(QUrl::RemoveFilename)); mDirty = true; shutdownScript->setText(shutdownScriptURL.toLocalFile()); } void Scheduler::addJob() { if (0 <= jobUnderEdit) { /* If a job is being edited, reset edition mode as all fields are already transferred to the job */ resetJobEdit(); } else { /* If a job is being added, save fields into a new job */ saveJob(); /* There is now an evaluation for each change, so don't duplicate the evaluation now */ // jobEvaluationOnly = true; // evaluateJobs(); } } void Scheduler::saveJob() { if (state == SCHEDULER_RUNNING) { appendLogText(i18n("Warning: You cannot add or modify a job while the scheduler is running.")); return; } if (nameEdit->text().isEmpty()) { appendLogText(i18n("Warning: Target name is required.")); return; } if (sequenceEdit->text().isEmpty()) { appendLogText(i18n("Warning: Sequence file is required.")); return; } // Coordinates are required unless it is a FITS file if ((raBox->isEmpty() || decBox->isEmpty()) && fitsURL.isEmpty()) { appendLogText(i18n("Warning: Target coordinates are required.")); return; } bool raOk = false, decOk = false; dms /*const*/ ra(raBox->createDms(false, &raOk)); //false means expressed in hours dms /*const*/ dec(decBox->createDms(true, &decOk)); if (raOk == false) { appendLogText(i18n("Warning: RA value %1 is invalid.", raBox->text())); return; } if (decOk == false) { appendLogText(i18n("Warning: DEC value %1 is invalid.", decBox->text())); return; } watchJobChanges(false); /* Create or Update a scheduler job */ int currentRow = queueTable->currentRow(); SchedulerJob * job = nullptr; /* If no row is selected for insertion, append at end of list. */ if (currentRow < 0) currentRow = queueTable->rowCount(); /* Add job to queue only if it is new, else reuse current row. * Make sure job is added at the right index, now that queueTable may have a line selected without being edited. */ if (0 <= jobUnderEdit) { /* FIXME: jobUnderEdit is a parallel variable that may cause issues if it desyncs from queueTable->currentRow(). */ if (jobUnderEdit != currentRow) qCWarning(KSTARS_EKOS_SCHEDULER) << "BUG: the observation job under edit does not match the selected row in the job table."; /* Use the job in the row currently edited */ job = jobs.at(currentRow); } else { /* Instantiate a new job, insert it in the job list and add a row in the table for it just after the row currently selected. */ job = new SchedulerJob(); jobs.insert(currentRow, job); queueTable->insertRow(currentRow); } /* Configure or reconfigure the observation job */ job->setName(nameEdit->text()); job->setPriority(prioritySpin->value()); job->setTargetCoords(ra, dec); job->setDateTimeDisplayFormat(startupTimeEdit->displayFormat()); /* Consider sequence file is new, and clear captured frames map */ job->setCapturedFramesMap(SchedulerJob::CapturedFramesMap()); job->setSequenceFile(sequenceURL); fitsURL = QUrl::fromLocalFile(fitsEdit->text()); job->setFITSFile(fitsURL); // #1 Startup conditions if (asapConditionR->isChecked()) { job->setStartupCondition(SchedulerJob::START_ASAP); } else if (culminationConditionR->isChecked()) { job->setStartupCondition(SchedulerJob::START_CULMINATION); job->setCulminationOffset(culminationOffset->value()); } else { job->setStartupCondition(SchedulerJob::START_AT); job->setStartupTime(startupTimeEdit->dateTime()); } /* Store the original startup condition */ job->setFileStartupCondition(job->getStartupCondition()); job->setFileStartupTime(job->getStartupTime()); // #2 Constraints // Do we have minimum altitude constraint? if (altConstraintCheck->isChecked()) job->setMinAltitude(minAltitude->value()); else job->setMinAltitude(-90); // Do we have minimum moon separation constraint? if (moonSeparationCheck->isChecked()) job->setMinMoonSeparation(minMoonSeparation->value()); else job->setMinMoonSeparation(-1); // Check enforce weather constraints job->setEnforceWeather(weatherCheck->isChecked()); // twilight constraints job->setEnforceTwilight(twilightCheck->isChecked()); /* Verifications */ /* FIXME: perhaps use a method more visible to the end-user */ if (SchedulerJob::START_AT == job->getFileStartupCondition()) { /* Warn if appending a job which startup time doesn't allow proper score */ if (calculateJobScore(job, job->getStartupTime()) < 0) appendLogText(i18n("Warning: job '%1' has startup time %2 resulting in a negative score, and will be marked invalid when processed.", job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()))); } // #3 Completion conditions if (sequenceCompletionR->isChecked()) { job->setCompletionCondition(SchedulerJob::FINISH_SEQUENCE); } else if (repeatCompletionR->isChecked()) { job->setCompletionCondition(SchedulerJob::FINISH_REPEAT); job->setRepeatsRequired(repeatsSpin->value()); job->setRepeatsRemaining(repeatsSpin->value()); } else if (loopCompletionR->isChecked()) { job->setCompletionCondition(SchedulerJob::FINISH_LOOP); } else { job->setCompletionCondition(SchedulerJob::FINISH_AT); job->setCompletionTime(completionTimeEdit->dateTime()); } // Job steps job->setStepPipeline(SchedulerJob::USE_NONE); if (trackStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_TRACK)); if (focusStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_FOCUS)); if (alignStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_ALIGN)); if (guideStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_GUIDE)); /* Reset job state to evaluate the changes */ job->reset(); // Warn user if a duplicated job is in the list - same target, same sequence // FIXME: Those duplicated jobs are not necessarily processed in the order they appear in the list! foreach (SchedulerJob *a_job, jobs) { if (a_job == job) { break; } else if (a_job->getName() == job->getName()) { int const a_job_row = a_job->getNameCell() ? a_job->getNameCell()->row() + 1 : 0; /* FIXME: Warning about duplicate jobs only checks the target name, doing it properly would require checking storage for each sequence job of each scheduler job. */ appendLogText(i18n("Warning: job '%1' at row %2 has a duplicate target at row %3, " "the scheduler may consider the same storage for captures.", job->getName(), currentRow, a_job_row)); /* Warn the user in case the two jobs are really identical */ if (a_job->getSequenceFile() == job->getSequenceFile()) { if (a_job->getRepeatsRequired() == job->getRepeatsRequired() && Options::rememberJobProgress()) appendLogText(i18n("Warning: jobs '%1' at row %2 and %3 probably require a different repeat count " "as currently they will complete simultaneously after %4 batches (or disable option 'Remember job progress')", job->getName(), currentRow, a_job_row, job->getRepeatsRequired())); if (a_job->getStartupTime() == a_job->getStartupTime() && a_job->getPriority() == job->getPriority()) appendLogText(i18n("Warning: job '%1' at row %2 might require a specific startup time or a different priority, " "as currently they will start in order of insertion in the table", job->getName(), currentRow)); } } } if (-1 == jobUnderEdit) { QTableWidgetItem *nameCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_NAME), nameCell); nameCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); nameCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *statusCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_STATUS), statusCell); statusCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); statusCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *captureCount = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_CAPTURES), captureCount); captureCount->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); captureCount->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *scoreValue = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_SCORE), scoreValue); scoreValue->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); scoreValue->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *startupCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_STARTTIME), startupCell); startupCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); startupCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *altitudeCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_ALTITUDE), altitudeCell); altitudeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); altitudeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *completionCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_ENDTIME), completionCell); completionCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); completionCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *estimatedTimeCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_DURATION), estimatedTimeCell); estimatedTimeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); estimatedTimeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *leadTimeCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_LEADTIME), leadTimeCell); leadTimeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); leadTimeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); } setJobStatusCells(currentRow); /* We just added or saved a job, so we have a job in the list - enable relevant buttons */ queueSaveAsB->setEnabled(true); queueSaveB->setEnabled(true); startB->setEnabled(true); evaluateOnlyB->setEnabled(true); setJobManipulation(!Options::sortSchedulerJobs(), true); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 was saved.").arg(job->getName()).arg(currentRow + 1); watchJobChanges(true); if (SCHEDULER_LOADING != state) { jobEvaluationOnly = true; evaluateJobs(); } } void Scheduler::syncGUIToJob(SchedulerJob *job) { nameEdit->setText(job->getName()); prioritySpin->setValue(job->getPriority()); raBox->showInHours(job->getTargetCoords().ra0()); decBox->showInDegrees(job->getTargetCoords().dec0()); if (job->getFITSFile().isEmpty() == false) fitsEdit->setText(job->getFITSFile().toLocalFile()); else fitsEdit->clear(); sequenceEdit->setText(job->getSequenceFile().toLocalFile()); trackStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_TRACK); focusStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_FOCUS); alignStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_ALIGN); guideStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_GUIDE); switch (job->getFileStartupCondition()) { case SchedulerJob::START_ASAP: asapConditionR->setChecked(true); culminationOffset->setValue(DEFAULT_CULMINATION_TIME); break; case SchedulerJob::START_CULMINATION: culminationConditionR->setChecked(true); culminationOffset->setValue(job->getCulminationOffset()); break; case SchedulerJob::START_AT: startupTimeConditionR->setChecked(true); startupTimeEdit->setDateTime(job->getStartupTime()); culminationOffset->setValue(DEFAULT_CULMINATION_TIME); break; } if (-90 < job->getMinAltitude()) { altConstraintCheck->setChecked(true); minAltitude->setValue(job->getMinAltitude()); } else { altConstraintCheck->setChecked(false); minAltitude->setValue(DEFAULT_MIN_ALTITUDE); } if (job->getMinMoonSeparation() >= 0) { moonSeparationCheck->setChecked(true); minMoonSeparation->setValue(job->getMinMoonSeparation()); } else { moonSeparationCheck->setChecked(false); minMoonSeparation->setValue(DEFAULT_MIN_MOON_SEPARATION); } weatherCheck->setChecked(job->getEnforceWeather()); twilightCheck->blockSignals(true); twilightCheck->setChecked(job->getEnforceTwilight()); twilightCheck->blockSignals(false); switch (job->getCompletionCondition()) { case SchedulerJob::FINISH_SEQUENCE: sequenceCompletionR->setChecked(true); break; case SchedulerJob::FINISH_REPEAT: repeatCompletionR->setChecked(true); repeatsSpin->setValue(job->getRepeatsRequired()); break; case SchedulerJob::FINISH_LOOP: loopCompletionR->setChecked(true); break; case SchedulerJob::FINISH_AT: timeCompletionR->setChecked(true); completionTimeEdit->setDateTime(job->getCompletionTime()); break; } setJobManipulation(!Options::sortSchedulerJobs(), true); } void Scheduler::loadJob(QModelIndex i) { if (jobUnderEdit == i.row()) return; if (state == SCHEDULER_RUNNING) { appendLogText(i18n("Warning: you cannot add or modify a job while the scheduler is running.")); return; } SchedulerJob * const job = jobs.at(i.row()); if (job == nullptr) return; watchJobChanges(false); //job->setState(SchedulerJob::JOB_IDLE); //job->setStage(SchedulerJob::STAGE_IDLE); syncGUIToJob(job); if (job->getFITSFile().isEmpty() == false) fitsURL = job->getFITSFile(); else fitsURL = QUrl(); sequenceURL = job->getSequenceFile(); /* Turn the add button into an apply button */ setJobAddApply(false); /* Disable scheduler start/evaluate buttons */ startB->setEnabled(false); evaluateOnlyB->setEnabled(false); /* Don't let the end-user remove a job being edited */ setJobManipulation(false, false); jobUnderEdit = i.row(); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is currently edited.").arg(job->getName()).arg(jobUnderEdit + 1); watchJobChanges(true); } void Scheduler::queueTableSelectionChanged(QModelIndex current, QModelIndex previous) { Q_UNUSED(previous); // prevent selection when not idle if (state != SCHEDULER_IDLE) return; if (current.row() < 0 || (current.row() + 1) > jobs.size()) return; SchedulerJob * const job = jobs.at(current.row()); if (job == nullptr) return; resetJobEdit(); syncGUIToJob(job); } void Scheduler::clickQueueTable(QModelIndex index) { setJobManipulation(!Options::sortSchedulerJobs() && index.isValid(), index.isValid()); } void Scheduler::setJobAddApply(bool add_mode) { if (add_mode) { addToQueueB->setIcon(QIcon::fromTheme("list-add")); addToQueueB->setToolTip(i18n("Use edition fields to create a new job in the observation list.")); //addToQueueB->setStyleSheet(QString()); addToQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); } else { addToQueueB->setIcon(QIcon::fromTheme("dialog-ok-apply")); addToQueueB->setToolTip(i18n("Apply job changes.")); //addToQueueB->setStyleSheet("background-color:orange;}"); addToQueueB->setEnabled(true); } } void Scheduler::setJobManipulation(bool can_reorder, bool can_delete) { bool can_edit = (state == SCHEDULER_IDLE); if (can_reorder) { int const currentRow = queueTable->currentRow(); queueUpB->setEnabled(can_edit && 0 < currentRow); queueDownB->setEnabled(can_edit && currentRow < queueTable->rowCount() - 1); } else { queueUpB->setEnabled(false); queueDownB->setEnabled(false); } sortJobsB->setEnabled(can_edit && can_reorder); removeFromQueueB->setEnabled(can_edit && can_delete); } bool Scheduler::reorderJobs(QList reordered_sublist) { /* Add jobs not reordered at the end of the list, in initial order */ foreach (SchedulerJob* job, jobs) if (!reordered_sublist.contains(job)) reordered_sublist.append(job); if (jobs != reordered_sublist) { /* Remember job currently selected */ int const selectedRow = queueTable->currentRow(); SchedulerJob * const selectedJob = 0 <= selectedRow ? jobs.at(selectedRow) : nullptr; /* Reassign list */ jobs = reordered_sublist; /* Reassign status cells for all jobs, and reset them */ for (int row = 0; row < jobs.size(); row++) setJobStatusCells(row); /* Reselect previously selected job */ if (nullptr != selectedJob) queueTable->selectRow(jobs.indexOf(selectedJob)); return true; } else return false; } void Scheduler::moveJobUp() { /* No move if jobs are sorted automatically */ if (Options::sortSchedulerJobs()) return; int const rowCount = queueTable->rowCount(); int const currentRow = queueTable->currentRow(); int const destinationRow = currentRow - 1; /* No move if no job selected, if table has one line or less or if destination is out of table */ if (currentRow < 0 || rowCount <= 1 || destinationRow < 0) return; /* Swap jobs in the list */ + #if QT_VERSION >= QT_VERSION_CHECK(5,13,0) + jobs.swapItemsAt(currentRow, destinationRow); + #else jobs.swap(currentRow, destinationRow); + #endif /* Reassign status cells */ setJobStatusCells(currentRow); setJobStatusCells(destinationRow); /* Move selection to destination row */ queueTable->selectRow(destinationRow); setJobManipulation(!Options::sortSchedulerJobs(), true); /* Jobs are now sorted, so reset all later jobs */ for (int row = destinationRow; row < jobs.size(); row++) jobs.at(row)->reset(); /* Make list modified and evaluate jobs */ mDirty = true; jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::moveJobDown() { /* No move if jobs are sorted automatically */ if (Options::sortSchedulerJobs()) return; int const rowCount = queueTable->rowCount(); int const currentRow = queueTable->currentRow(); int const destinationRow = currentRow + 1; /* No move if no job selected, if table has one line or less or if destination is out of table */ if (currentRow < 0 || rowCount <= 1 || destinationRow == rowCount) return; /* Swap jobs in the list */ + #if QT_VERSION >= QT_VERSION_CHECK(5,13,0) + jobs.swapItemsAt(currentRow, destinationRow); + #else jobs.swap(currentRow, destinationRow); + #endif /* Reassign status cells */ setJobStatusCells(currentRow); setJobStatusCells(destinationRow); /* Move selection to destination row */ queueTable->selectRow(destinationRow); setJobManipulation(!Options::sortSchedulerJobs(), true); /* Jobs are now sorted, so reset all later jobs */ for (int row = currentRow; row < jobs.size(); row++) jobs.at(row)->reset(); /* Make list modified and evaluate jobs */ mDirty = true; jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::setJobStatusCells(int row) { if (row < 0 || jobs.size() <= row) return; SchedulerJob * const job = jobs.at(row); job->setNameCell(queueTable->item(row, static_cast(SCHEDCOL_NAME))); job->setStatusCell(queueTable->item(row, static_cast(SCHEDCOL_STATUS))); job->setCaptureCountCell(queueTable->item(row, static_cast(SCHEDCOL_CAPTURES))); job->setScoreCell(queueTable->item(row, static_cast(SCHEDCOL_SCORE))); job->setAltitudeCell(queueTable->item(row, static_cast(SCHEDCOL_ALTITUDE))); job->setStartupCell(queueTable->item(row, static_cast(SCHEDCOL_STARTTIME))); job->setCompletionCell(queueTable->item(row, static_cast(SCHEDCOL_ENDTIME))); job->setEstimatedTimeCell(queueTable->item(row, static_cast(SCHEDCOL_DURATION))); job->setLeadTimeCell(queueTable->item(row, static_cast(SCHEDCOL_LEADTIME))); job->updateJobCells(); } void Scheduler::resetJobEdit() { if (jobUnderEdit < 0) return; SchedulerJob * const job = jobs.at(jobUnderEdit); Q_ASSERT_X(job != nullptr, __FUNCTION__, "Edited job must be valid"); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is not longer edited.").arg(job->getName()).arg(jobUnderEdit + 1); jobUnderEdit = -1; watchJobChanges(false); /* Revert apply button to add */ setJobAddApply(true); /* Refresh state of job manipulation buttons */ setJobManipulation(!Options::sortSchedulerJobs(), true); /* Restore scheduler operation buttons */ evaluateOnlyB->setEnabled(true); startB->setEnabled(true); Q_ASSERT_X(jobUnderEdit == -1, __FUNCTION__, "No more edited/selected job after exiting edit mode"); } void Scheduler::removeJob() { int currentRow = queueTable->currentRow(); /* Don't remove a row that is not selected */ if (currentRow < 0) return; /* Grab the job currently selected */ SchedulerJob * const job = jobs.at(currentRow); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is being deleted.").arg(job->getName()).arg(currentRow + 1); /* Remove the job from the table */ queueTable->removeRow(currentRow); /* If there are no job rows left, update UI buttons */ if (queueTable->rowCount() == 0) { setJobManipulation(false, false); evaluateOnlyB->setEnabled(false); queueSaveAsB->setEnabled(false); queueSaveB->setEnabled(false); startB->setEnabled(false); pauseB->setEnabled(false); } /* Else update the selection */ else { if (currentRow > queueTable->rowCount()) currentRow = queueTable->rowCount() - 1; loadJob(queueTable->currentIndex()); queueTable->selectRow(currentRow); } /* If needed, reset edit mode to clean up UI */ if (jobUnderEdit >= 0) resetJobEdit(); /* And remove the job object */ jobs.removeOne(job); delete (job); mDirty = true; jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::toggleScheduler() { if (state == SCHEDULER_RUNNING) { preemptiveShutdown = false; stop(); } else start(); } void Scheduler::stop() { if (state != SCHEDULER_RUNNING) return; qCInfo(KSTARS_EKOS_SCHEDULER) << "Scheduler is stopping..."; // Stop running job and abort all others // in case of soft shutdown we skip this if (preemptiveShutdown == false) { bool wasAborted = false; foreach (SchedulerJob *job, jobs) { if (job == currentJob) stopCurrentJobAction(); if (job->getState() <= SchedulerJob::JOB_BUSY) { appendLogText(i18n("Job '%1' has not been processed upon scheduler stop, marking aborted.", job->getName())); job->setState(SchedulerJob::JOB_ABORTED); wasAborted = true; } } if (wasAborted) KNotification::event(QLatin1String("SchedulerAborted"), i18n("Scheduler aborted.")); } schedulerTimer.stop(); jobTimer.stop(); restartGuidingTimer.stop(); state = SCHEDULER_IDLE; emit newStatus(state); ekosState = EKOS_IDLE; indiState = INDI_IDLE; parkWaitState = PARKWAIT_IDLE; // Only reset startup state to idle if the startup procedure was interrupted before it had the chance to complete. // Or if we're doing a soft shutdown if (startupState != STARTUP_COMPLETE || preemptiveShutdown) { if (startupState == STARTUP_SCRIPT) { scriptProcess.disconnect(); scriptProcess.terminate(); } startupState = STARTUP_IDLE; } // Reset startup state to unparking phase (dome -> mount -> cap) // We do not want to run the startup script again but unparking should be checked // whenever the scheduler is running again. else if (startupState == STARTUP_COMPLETE) { if (unparkDomeCheck->isChecked()) startupState = STARTUP_UNPARK_DOME; else if (unparkMountCheck->isChecked()) startupState = STARTUP_UNPARK_MOUNT; else if (uncapCheck->isChecked()) startupState = STARTUP_UNPARK_CAP; } shutdownState = SHUTDOWN_IDLE; setCurrentJob(nullptr); captureBatch = 0; indiConnectFailureCount = 0; ekosConnectFailureCount = 0; focusFailureCount = 0; guideFailureCount = 0; alignFailureCount = 0; captureFailureCount = 0; jobEvaluationOnly = false; loadAndSlewProgress = false; autofocusCompleted = false; startupB->setEnabled(true); shutdownB->setEnabled(true); // If soft shutdown, we return for now if (preemptiveShutdown) { sleepLabel->setToolTip(i18n("Scheduler is in shutdown until next job is ready")); sleepLabel->show(); return; } // Clear target name in capture interface upon stopping if (captureInterface.isNull() == false) captureInterface->setProperty("targetName", QString()); if (scriptProcess.state() == QProcess::Running) scriptProcess.terminate(); sleepTimer.stop(); //sleepTimer.disconnect(); sleepLabel->hide(); pi->stopAnimation(); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setToolTip(i18n("Start Scheduler")); pauseB->setEnabled(false); //startB->setText("Start Scheduler"); queueLoadB->setEnabled(true); addToQueueB->setEnabled(true); setJobManipulation(false, false); mosaicB->setEnabled(true); evaluateOnlyB->setEnabled(true); } void Scheduler::start() { switch (state) { case SCHEDULER_IDLE: /* FIXME: Manage the non-validity of the startup script earlier, and make it a warning only when the scheduler starts */ startupScriptURL = QUrl::fromUserInput(startupScript->text()); if (!startupScript->text().isEmpty() && !startupScriptURL.isValid()) { appendLogText(i18n("Warning: startup script URL %1 is not valid.", startupScript->text())); return; } /* FIXME: Manage the non-validity of the shutdown script earlier, and make it a warning only when the scheduler starts */ shutdownScriptURL = QUrl::fromUserInput(shutdownScript->text()); if (!shutdownScript->text().isEmpty() && !shutdownScriptURL.isValid()) { appendLogText(i18n("Warning: shutdown script URL %1 is not valid.", shutdownScript->text())); return; } qCInfo(KSTARS_EKOS_SCHEDULER) << "Scheduler is starting..."; /* Update UI to reflect startup */ pi->startAnimation(); sleepLabel->hide(); startB->setIcon(QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop Scheduler")); pauseB->setEnabled(true); pauseB->setChecked(false); /* Disable edit-related buttons */ queueLoadB->setEnabled(false); addToQueueB->setEnabled(false); setJobManipulation(false, false); mosaicB->setEnabled(false); evaluateOnlyB->setEnabled(false); startupB->setEnabled(false); shutdownB->setEnabled(false); state = SCHEDULER_RUNNING; emit newStatus(state); schedulerTimer.start(); appendLogText(i18n("Scheduler started.")); qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler started."; break; case SCHEDULER_PAUSED: /* Update UI to reflect resume */ startB->setIcon(QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop Scheduler")); pauseB->setEnabled(true); pauseB->setCheckable(false); pauseB->setChecked(false); /* Edit-related buttons are still disabled */ /* The end-user cannot update the schedule, don't re-evaluate jobs. Timer schedulerTimer is already running. */ state = SCHEDULER_RUNNING; emit newStatus(state); schedulerTimer.start(); appendLogText(i18n("Scheduler resuming.")); qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler resuming."; break; default: break; } } void Scheduler::pause() { state = SCHEDULER_PAUSED; emit newStatus(state); appendLogText(i18n("Scheduler pause planned...")); pauseB->setEnabled(false); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setToolTip(i18n("Resume Scheduler")); } void Scheduler::setPaused() { pauseB->setCheckable(true); pauseB->setChecked(true); schedulerTimer.stop(); appendLogText(i18n("Scheduler paused.")); } void Scheduler::setCurrentJob(SchedulerJob *job) { /* Reset job widgets */ if (currentJob) { currentJob->setStageLabel(nullptr); } /* Set current job */ currentJob = job; /* Reassign job widgets, or reset to defaults */ if (currentJob) { currentJob->setStageLabel(jobStatus); queueTable->selectRow(jobs.indexOf(currentJob)); } else { jobStatus->setText(i18n("No job running")); //queueTable->clearSelection(); } } void Scheduler::evaluateJobs() { /* Don't evaluate if list is empty */ if (jobs.isEmpty()) return; /* FIXME: it is possible to evaluate jobs while KStars has a time offset, so warn the user about this */ QDateTime const now = KStarsData::Instance()->lt(); /* Start by refreshing the number of captures already present - unneeded if not remembering job progress */ if (Options::rememberJobProgress()) updateCompletedJobsCount(); /* Update dawn and dusk astronomical times - unconditionally in case date changed */ calculateDawnDusk(); /* First, filter out non-schedulable jobs */ /* FIXME: jobs in state JOB_ERROR should not be in the list, reorder states */ QList sortedJobs = jobs; /* Then enumerate SchedulerJobs to consolidate imaging time */ foreach (SchedulerJob *job, sortedJobs) { /* Let aborted jobs be rescheduled later instead of forgetting them */ switch (job->getState()) { case SchedulerJob::JOB_SCHEDULED: /* If job is scheduled, keep it for evaluation against others */ break; case SchedulerJob::JOB_INVALID: case SchedulerJob::JOB_COMPLETE: /* If job is invalid or complete, bypass evaluation */ continue; case SchedulerJob::JOB_BUSY: /* If job is busy, edge case, bypass evaluation */ continue; case SchedulerJob::JOB_ERROR: case SchedulerJob::JOB_ABORTED: /* If job is in error or aborted and we're running, keep its evaluation until there is nothing else to do */ if (state == SCHEDULER_RUNNING) continue; /* Fall through */ case SchedulerJob::JOB_IDLE: case SchedulerJob::JOB_EVALUATION: default: /* If job is idle, re-evaluate completely */ job->setEstimatedTime(-1); break; } switch (job->getCompletionCondition()) { case SchedulerJob::FINISH_AT: /* If planned finishing time has passed, the job is set to IDLE waiting for a next chance to run */ if (job->getCompletionTime().isValid() && job->getCompletionTime() < now) { job->setState(SchedulerJob::JOB_IDLE); continue; } break; case SchedulerJob::FINISH_REPEAT: // In case of a repeating jobs, let's make sure we have more runs left to go // If we don't, re-estimate imaging time for the scheduler job before concluding if (job->getRepeatsRemaining() == 0) { appendLogText(i18n("Job '%1' has no more batches remaining.", job->getName())); if (Options::rememberJobProgress()) { job->setEstimatedTime(-1); } else { job->setState(SchedulerJob::JOB_COMPLETE); job->setEstimatedTime(0); continue; } } break; default: break; } // -1 = Job is not estimated yet // -2 = Job is estimated but time is unknown // > 0 Job is estimated and time is known if (job->getEstimatedTime() == -1) { if (estimateJobTime(job) == false) { job->setState(SchedulerJob::JOB_INVALID); continue; } } if (job->getEstimatedTime() == 0) { job->setRepeatsRemaining(0); job->setState(SchedulerJob::JOB_COMPLETE); continue; } // In any other case, evaluate job->setState(SchedulerJob::JOB_EVALUATION); } /* * At this step, we prepare scheduling of jobs. * We filter out jobs that won't run now, and make sure jobs are not all starting at the same time. */ updatePreDawn(); /* This predicate matches jobs not being evaluated and not aborted */ auto neither_evaluated_nor_aborted = [](SchedulerJob const * const job) { SchedulerJob::JOBStatus const s = job->getState(); return SchedulerJob::JOB_EVALUATION != s && SchedulerJob::JOB_ABORTED != s; }; /* This predicate matches jobs neither being evaluated nor aborted nor in error state */ auto neither_evaluated_nor_aborted_nor_error = [](SchedulerJob const * const job) { SchedulerJob::JOBStatus const s = job->getState(); return SchedulerJob::JOB_EVALUATION != s && SchedulerJob::JOB_ABORTED != s && SchedulerJob::JOB_ERROR != s; }; /* This predicate matches jobs that aborted, or completed for whatever reason */ auto finished_or_aborted = [](SchedulerJob const * const job) { SchedulerJob::JOBStatus const s = job->getState(); return SchedulerJob::JOB_ERROR <= s || SchedulerJob::JOB_ABORTED == s; }; bool nea = std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_evaluated_nor_aborted); bool neae = std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_evaluated_nor_aborted_nor_error); /* If there are no jobs left to run in the filtered list, stop evaluation */ if (sortedJobs.isEmpty() || (!errorHandlingRescheduleErrorsCB->isChecked() && nea) || (errorHandlingRescheduleErrorsCB->isChecked() && neae)) { appendLogText(i18n("No jobs left in the scheduler queue.")); setCurrentJob(nullptr); jobEvaluationOnly = false; return; } /* If there are only aborted jobs that can run, reschedule those */ if (std::all_of(sortedJobs.begin(), sortedJobs.end(), finished_or_aborted) && errorHandlingDontRestartButton->isChecked() == false) { appendLogText(i18n("Only %1 jobs left in the scheduler queue, rescheduling those.", errorHandlingRescheduleErrorsCB->isChecked() ? "aborted or error" : "aborted")); // set aborted and error jobs to evaluation state for (int index = 0; index < sortedJobs.size(); index++) { SchedulerJob * const job = sortedJobs.at(index); if (SchedulerJob::JOB_ABORTED == job->getState() || (errorHandlingRescheduleErrorsCB->isChecked() && SchedulerJob::JOB_ERROR == job->getState())) job->setState(SchedulerJob::JOB_EVALUATION); } if (errorHandlingRestartAfterAllButton->isChecked()) { // interrupt regular status checks during the sleep time schedulerTimer.stop(); // but before we restart them, we wait for the given delay. appendLogText(i18n("All jobs aborted. Waiting %1 seconds to re-schedule.", errorHandlingDelaySB->value())); // wait the given delay until the jobs will be evaluated again sleepTimer.setInterval(std::lround((errorHandlingDelaySB->value() * 1000) / KStarsData::Instance()->clock()->scale())); sleepTimer.start(); sleepLabel->setToolTip(i18n("Scheduler waits for a retry.")); sleepLabel->show(); // we continue to determine which job should be running, when the delay is over } } /* If option says so, reorder by altitude and priority before sequencing */ /* FIXME: refactor so all sorts are using the same predicates */ /* FIXME: use std::stable_sort as qStableSort is deprecated */ /* FIXME: dissociate altitude and priority, it's difficult to choose which predicate to use first */ qCInfo(KSTARS_EKOS_SCHEDULER) << "Option to sort jobs based on priority and altitude is" << Options::sortSchedulerJobs(); if (Options::sortSchedulerJobs()) { using namespace std::placeholders; std::stable_sort(sortedJobs.begin(), sortedJobs.end(), std::bind(SchedulerJob::decreasingAltitudeOrder, _1, _2, KStarsData::Instance()->lt())); std::stable_sort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingPriorityOrder); } /* The first reordered job has no lead time - this could also be the delay from now to startup */ sortedJobs.first()->setLeadTime(0); /* The objective of the following block is to make sure jobs are sequential in the list filtered previously. * * The algorithm manages overlap between jobs by stating that scheduled jobs that start sooner are non-movable. * If the completion time of the previous job overlaps the current job, we offset the startup of the current job. * Jobs that have no valid startup time when evaluated (ASAP jobs) are assigned an immediate startup time. * The lead time from the Options registry is used as a buffer between jobs. * * Note about the situation where the current job overlaps the next job, and the next job is not movable: * - If we mark the current job invalid, it will not be processed at all. Dropping is not satisfactory. * - If we move the current job after the fixed job, we need to restart evaluation with a new list, and risk an * infinite loop eventually. This means swapping schedules, and is incompatible with altitude/priority sort. * - If we mark the current job aborted, it will be re-evaluated each time a job is complete to see if it can fit. * Although puzzling for the end-user, this solution is dynamic: the aborted job might or might not be scheduled * at the planned time slot. But as the end-user did not enforce the start time, this is acceptable. Moreover, the * schedule will be altered by external events during the execution. * * Here are the constraints that have an effect on the job being examined, and indirectly on all subsequent jobs: * - Twilight constraint moves jobs to the next dark sky interval. * - Altitude constraint, currently linked with Moon separation, moves jobs to the next acceptable altitude time. * - Culmination constraint moves jobs to the next transit time, with arbitrary offset. * - Fixed startup time moves jobs to a fixed time, essentially making them non-movable, or invalid if in the past. * * Here are the constraints that have an effect on jobs following the job being examined: * - Repeats requirement increases the duration of the current job, pushing subsequent jobs. * - Looping requirement causes subsequent jobs to become invalid (until dynamic priority is implemented). * - Fixed completion makes subsequent jobs start after that boundary time. * * However, we need a way to inform the end-user about failed schedules clearly in the UI. * The message to get through is that if jobs are not sorted by altitude/priority, the aborted or invalid jobs * should be modified or manually moved to a better position. If jobs are sorted automatically, aborted jobs will * be processed when possible, probably not at the expected moment. */ // Make sure no two jobs have the same scheduled time or overlap with other jobs for (int index = 0; index < sortedJobs.size(); index++) { SchedulerJob * const currentJob = sortedJobs.at(index); // Bypass jobs that are not marked for evaluation - we did not remove them to preserve schedule order if (SchedulerJob::JOB_EVALUATION != currentJob->getState()) continue; // At this point, a job with no valid start date is a problem, so consider invalid startup time is now if (!currentJob->getStartupTime().isValid()) currentJob->setStartupTime(now); // Locate the previous scheduled job, so that a full schedule plan may be actually consolidated SchedulerJob const * previousJob = nullptr; for (int i = index - 1; 0 <= i; i--) { SchedulerJob const * const a_job = sortedJobs.at(i); if (SchedulerJob::JOB_SCHEDULED == a_job->getState()) { previousJob = a_job; break; } } Q_ASSERT_X(nullptr == previousJob || previousJob != currentJob, __FUNCTION__, "Previous job considered for schedule is either undefined or not equal to current."); // Locate the next job - nothing special required except end of list check SchedulerJob const * const nextJob = index + 1 < sortedJobs.size() ? sortedJobs.at(index + 1) : nullptr; Q_ASSERT_X(nullptr == nextJob || nextJob != currentJob, __FUNCTION__, "Next job considered for schedule is either undefined or not equal to current."); // We're attempting to schedule the job 10 times before making it invalid for (int attempt = 1; attempt < 11; attempt++) { qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Schedule attempt #%1 for %2-second job '%3' on row #%4 starting at %5, completing at %6.") .arg(attempt) .arg(static_cast(currentJob->getEstimatedTime())) .arg(currentJob->getName()) .arg(index + 1) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())) .arg(currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat())); // ----- #1 Should we reject the current job because of its fixed startup time? // // A job with fixed startup time must be processed at the time of startup, and may be late up to leadTime. // When such a job repeats, its startup time is reinitialized to prevent abort - see completion algorithm. // If such a job requires night time, minimum altitude or Moon separation, the consolidated startup time is checked for errors. // If all restrictions are complied with, we bypass the rest of the verifications as the job cannot be moved. if (SchedulerJob::START_AT == currentJob->getFileStartupCondition()) { // Check whether the current job is too far in the past to be processed - if job is repeating, its startup time is already now if (currentJob->getStartupTime().addSecs(static_cast (ceil(Options::leadTime() * 60))) < now) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has fixed startup time %2 set in the past, marking invalid.", currentJob->getName(), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); break; } // Check whether the current job has a positive dark sky score at the time of startup else if (true == currentJob->getEnforceTwilight() && getDarkSkyScore(currentJob->getStartupTime()) < 0) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has a fixed start time incompatible with its twilight restriction, marking invalid.", currentJob->getName())); break; } // Check whether the current job has a positive altitude score at the time of startup else if (-90 < currentJob->getMinAltitude() && currentJob->getAltitudeScore(currentJob->getStartupTime()) < 0) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has a fixed start time incompatible with its altitude restriction, marking invalid.", currentJob->getName())); break; } // Check whether the current job has a positive Moon separation score at the time of startup else if (0 < currentJob->getMinMoonSeparation() && currentJob->getMoonSeparationScore(currentJob->getStartupTime()) < 0) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has a fixed start time incompatible with its Moon separation restriction, marking invalid.", currentJob->getName())); break; } // Check whether a previous job overlaps the current job if (nullptr != previousJob && previousJob->getCompletionTime().isValid()) { // Calculate time we should be at after finishing the previous job QDateTime const previousCompletionTime = previousJob->getCompletionTime().addSecs(static_cast (ceil(Options::leadTime() * 60.0))); // Make this job invalid if startup time is not achievable because a START_AT job is non-movable if (currentJob->getStartupTime() < previousCompletionTime) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has fixed startup time %2 unachievable due to the completion time of its previous sibling, marking invalid.", currentJob->getName(), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); break; } currentJob->setLeadTime(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime())); } // This job is non-movable, we're done currentJob->setScore(calculateJobScore(currentJob, now)); currentJob->setState(SchedulerJob::JOB_SCHEDULED); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with fixed startup time requirement.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); break; } // ----- #2 Should we delay the current job because it overlaps the previous job? // // The previous job is considered non-movable, and its completion, plus lead time, is the origin for the current job. // If no previous job exists, or if all prior jobs in the list are rejected, there is no overlap. // If there is a previous job, the current job is simply delayed to avoid an eventual overlap. // IF there is a previous job but it never finishes, the current job is rejected. // This scheduling obviously relies on imaging time estimation: because errors stack up, future startup times are less and less reliable. if (nullptr != previousJob) { if (previousJob->getCompletionTime().isValid()) { // Calculate time we should be at after finishing the previous job QDateTime const previousCompletionTime = previousJob->getCompletionTime().addSecs(static_cast (ceil(Options::leadTime() * 60.0))); // Delay the current job to completion of its previous sibling if needed - this updates the completion time automatically if (currentJob->getStartupTime() < previousCompletionTime) { currentJob->setStartupTime(previousCompletionTime); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, %3 seconds after %4, in compliance with previous job completion requirement.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())) .arg(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime())) .arg(previousJob->getCompletionTime().toString(previousJob->getDateTimeDisplayFormat())); // If the job is repeating or looping, re-estimate imaging duration - error case may be a bug if (SchedulerJob::FINISH_SEQUENCE != currentJob->getCompletionCondition()) if (false == estimateJobTime(currentJob)) currentJob->setState(SchedulerJob::JOB_INVALID); continue; } } else { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: Job '%1' cannot start because its previous sibling has no completion time, marking invalid.", currentJob->getName())); break; } currentJob->setLeadTime(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime())); // Lead time can be zero, so completion may equal startup Q_ASSERT_X(previousJob->getCompletionTime() <= currentJob->getStartupTime(), __FUNCTION__, "Previous and current jobs do not overlap."); } // ----- #3 Should we delay the current job because it overlaps daylight? // // Pre-dawn time rules whether a job may be started before dawn, or delayed to next night. // Note that the case of START_AT jobs is considered earlier in the algorithm, thus may be omitted here. // In addition to be hardcoded currently, the imaging duration is not reliable enough to start a short job during pre-dawn. // However, completion time during daylight only causes a warning, as this case will be processed as the job runs. if (currentJob->getEnforceTwilight()) { // During that check, we don't verify the current job can actually complete before dawn. // If the job is interrupted while running, it will be aborted and rescheduled at a later time. // We wouldn't start observation 30 mins (default) before dawn. // FIXME: Refactor duplicated dawn/dusk calculations double const earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); // Compute dawn time for the startup date of the job // FIXME: Use KAlmanac to find the real dawn/dusk time for the day the job is supposed to be processed QDateTime const dawnDateTime(currentJob->getStartupTime().date(), QTime(0, 0).addSecs(earlyDawn * 24 * 3600)); // Check if the job starts after dawn if (dawnDateTime < currentJob->getStartupTime()) { // Compute dusk time for the startup date of the job - no lead time on dusk QDateTime duskDateTime(currentJob->getStartupTime().date(), QTime(0, 0).addSecs(Dusk * 24 * 3600)); // Near summer solstice, dusk may happen before dawn on the same day, shift dusk by one day in that case if (duskDateTime < dawnDateTime) duskDateTime = duskDateTime.addDays(1); // Check if the job starts before dusk if (currentJob->getStartupTime() < duskDateTime) { // Delay job to next dusk - we will check other requirements later on currentJob->setStartupTime(duskDateTime); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with night time requirement.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); continue; } } // Compute dawn time for the day following the startup time, but disregard the pre-dawn offset as we'll consider completion // FIXME: Use KAlmanac to find the real dawn/dusk time for the day next to the day the job is supposed to be processed QDateTime const nextDawnDateTime(currentJob->getStartupTime().date().addDays(1), QTime(0, 0).addSecs(Dawn * 24 * 3600)); // Check if the completion date overlaps the next dawn, and issue a warning if so if (nextDawnDateTime < currentJob->getCompletionTime()) { appendLogText(i18n("Warning: job '%1' execution overlaps daylight, it will be interrupted at dawn and rescheduled on next night time.", currentJob->getName())); } Q_ASSERT_X(0 <= getDarkSkyScore(currentJob->getStartupTime()), __FUNCTION__, "Consolidated startup time results in a positive dark sky score."); } // ----- #4 Should we delay the current job because of its target culmination? // // Culmination uses the transit time, and fixes the startup time of the job to a particular offset around this transit time. // This restriction may be used to start a job at the least air mass, or after a meridian flip. // Culmination is scheduled before altitude restriction because it is normally more restrictive for the resulting startup time. // It may happen that a target cannot rise enough to comply with the altitude restriction, but a culmination time is always valid. if (SchedulerJob::START_CULMINATION == currentJob->getFileStartupCondition()) { // Consolidate the culmination time, with offset, of the current job QDateTime const nextCulminationTime = currentJob->calculateCulmination(currentJob->getStartupTime()); if (nextCulminationTime.isValid()) // Guaranteed { if (currentJob->getStartupTime() < nextCulminationTime) { currentJob->setStartupTime(nextCulminationTime); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with culmination requirements.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); continue; } } else { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' requires culmination offset of %2 minutes, not achievable, marking invalid.", currentJob->getName(), QString("%L1").arg(currentJob->getCulminationOffset()))); break; } // Don't test altitude here, because we will push the job during the next check step // Q_ASSERT_X(0 <= getAltitudeScore(currentJob, currentJob->getStartupTime()), __FUNCTION__, "Consolidated altitude time results in a positive altitude score."); } // ----- #5 Should we delay the current job because its altitude is incorrect? // // Altitude time ensures the job is assigned a startup time when its target is high enough. // As other restrictions, the altitude is only considered for startup time, completion time is managed while the job is running. // Because a target setting down is a problem for the schedule, a cutoff altitude is added in the case the job target is past the meridian at startup time. // FIXME: though arguable, Moon separation is also considered in that restriction check - move it to a separate case. if (-90 < currentJob->getMinAltitude()) { // Consolidate a new altitude time from the startup time of the current job QDateTime const nextAltitudeTime = currentJob->calculateAltitudeTime(currentJob->getStartupTime()); if (nextAltitudeTime.isValid()) { if (currentJob->getStartupTime() < nextAltitudeTime) { currentJob->setStartupTime(nextAltitudeTime); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with altitude and Moon separation requirements.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); continue; } } else { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' requires minimum altitude %2 and Moon separation %3, not achievable, marking invalid.", currentJob->getName(), QString("%L1").arg(static_cast(currentJob->getMinAltitude()), 0, 'f', minAltitude->decimals()), 0.0 < currentJob->getMinMoonSeparation() ? QString("%L1").arg(static_cast(currentJob->getMinMoonSeparation()), 0, 'f', minMoonSeparation->decimals()) : QString("-"))); break; } Q_ASSERT_X(0 <= currentJob->getAltitudeScore(currentJob->getStartupTime()), __FUNCTION__, "Consolidated altitude time results in a positive altitude score."); } // ----- #6 Should we reject the current job because it overlaps the next job and that next job is not movable? // // If we have a blocker next to the current job, we compare the completion time of the current job and the startup time of this next job, taking lead time into account. // This verification obviously relies on the imaging time to be reliable, but there's not much we can do at this stage of the implementation. if (nullptr != nextJob && SchedulerJob::START_AT == nextJob->getFileStartupCondition()) { // In the current implementation, it is not possible to abort a running job when the next job is supposed to start. // Movable jobs after this one will be delayed, but non-movable jobs are considered blockers. // Calculate time we have between the end of the current job and the next job double const timeToNext = static_cast (currentJob->getCompletionTime().secsTo(nextJob->getStartupTime())); // If that time is overlapping the next job, abort the current job if (timeToNext < Options::leadTime() * 60) { currentJob->setState(SchedulerJob::JOB_ABORTED); appendLogText(i18n("Warning: job '%1' is constrained by the start time of the next job, and cannot finish in time, marking aborted.", currentJob->getName())); break; } Q_ASSERT_X(currentJob->getCompletionTime().addSecs(Options::leadTime() * 60) < nextJob->getStartupTime(), __FUNCTION__, "No overlap "); } // ----- #7 Should we reject the current job because it exceeded its fixed completion time? // // This verification simply checks that because of previous jobs, the startup time of the current job doesn't exceed its fixed completion time. // Its main objective is to catch wrong dates in the FINISH_AT configuration. if (SchedulerJob::FINISH_AT == currentJob->getCompletionCondition()) { if (currentJob->getCompletionTime() < currentJob->getStartupTime()) { appendLogText(i18n("Job '%1' completion time (%2) could not be achieved before start up time (%3)", currentJob->getName(), currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat()), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); currentJob->setState(SchedulerJob::JOB_INVALID); break; } } // ----- #8 Should we reject the current job because of weather? // // That verification is left for runtime // // if (false == isWeatherOK(currentJob)) //{ // currentJob->setState(SchedulerJob::JOB_ABORTED); // // appendLogText(i18n("Job '%1' cannot run now because of bad weather, marking aborted.", currentJob->getName())); //} // ----- #9 Update score for current time and mark evaluating jobs as scheduled currentJob->setScore(calculateJobScore(currentJob, now)); currentJob->setState(SchedulerJob::JOB_SCHEDULED); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' on row #%2 passed all checks after %3 attempts, will proceed at %4 for approximately %5 seconds, marking scheduled") .arg(currentJob->getName()) .arg(index + 1) .arg(attempt) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())) .arg(currentJob->getEstimatedTime()); break; } // Check if job was successfully scheduled, else reject it if (SchedulerJob::JOB_EVALUATION == currentJob->getState()) { currentJob->setState(SchedulerJob::JOB_INVALID); //appendLogText(i18n("Warning: job '%1' on row #%2 could not be scheduled during evaluation and is marked invalid, please review your plan.", // currentJob->getName(), // index + 1)); } } /* Apply sorting to queue table, and mark it for saving if it changes */ mDirty = reorderJobs(sortedJobs) | mDirty; if (jobEvaluationOnly || state != SCHEDULER_RUNNING) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos finished evaluating jobs, no job selection required."; jobEvaluationOnly = false; return; } /* * At this step, we finished evaluating jobs. * We select the first job that has to be run, per schedule. */ /* This predicate matches jobs that are neither scheduled to run nor aborted */ auto neither_scheduled_nor_aborted = [](SchedulerJob const * const job) { SchedulerJob::JOBStatus const s = job->getState(); return SchedulerJob::JOB_SCHEDULED != s && SchedulerJob::JOB_ABORTED != s; }; /* If there are no jobs left to run in the filtered list, stop evaluation */ if (sortedJobs.isEmpty() || std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_scheduled_nor_aborted)) { appendLogText(i18n("No jobs left in the scheduler queue after evaluating.")); setCurrentJob(nullptr); jobEvaluationOnly = false; return; } /* If there are only aborted jobs that can run, reschedule those and let Scheduler restart one loop */ else if (std::all_of(sortedJobs.begin(), sortedJobs.end(), finished_or_aborted) && errorHandlingDontRestartButton->isChecked() == false) { appendLogText(i18n("Only aborted jobs left in the scheduler queue after evaluating, rescheduling those.")); std::for_each(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * job) { if (SchedulerJob::JOB_ABORTED == job->getState()) job->setState(SchedulerJob::JOB_EVALUATION); }); jobEvaluationOnly = false; return; } /* The job to run is the first scheduled, locate it in the list */ QList::iterator job_to_execute_iterator = std::find_if(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * const job) { return SchedulerJob::JOB_SCHEDULED == job->getState(); }); /* If there is no scheduled job anymore (because the restriction loop made them invalid, for instance), bail out */ if (sortedJobs.end() == job_to_execute_iterator) { appendLogText(i18n("No jobs left in the scheduler queue after schedule cleanup.")); setCurrentJob(nullptr); jobEvaluationOnly = false; return; } /* Check if job can be processed right now */ SchedulerJob * const job_to_execute = *job_to_execute_iterator; if (job_to_execute->getFileStartupCondition() == SchedulerJob::START_ASAP) if( 0 <= calculateJobScore(job_to_execute, now)) job_to_execute->setStartupTime(now); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is selected for next observation with priority #%2 and score %3.") .arg(job_to_execute->getName()) .arg(job_to_execute->getPriority()) .arg(job_to_execute->getScore()); // Set the current job, and let the status timer execute it when ready setCurrentJob(job_to_execute); } void Scheduler::wakeUpScheduler() { sleepLabel->hide(); sleepTimer.stop(); if (preemptiveShutdown) { preemptiveShutdown = false; appendLogText(i18n("Scheduler is awake.")); start(); } else { if (state == SCHEDULER_RUNNING) appendLogText(i18n("Scheduler is awake. Jobs shall be started when ready...")); else appendLogText(i18n("Scheduler is awake. Jobs shall be started when scheduler is resumed.")); schedulerTimer.start(); } } int16_t Scheduler::getWeatherScore() const { if (weatherCheck->isEnabled() == false || weatherCheck->isChecked() == false) return 0; if (weatherStatus == ISD::Weather::WEATHER_WARNING) return BAD_SCORE / 2; else if (weatherStatus == ISD::Weather::WEATHER_ALERT) return BAD_SCORE; return 0; } int16_t Scheduler::getDarkSkyScore(QDateTime const &when) const { double const secsPerDay = 24.0 * 3600.0; double const minsPerDay = 24.0 * 60.0; // Dark sky score is calculated based on distance to today's dawn and next dusk. // Option "Pre-dawn Time" avoids executing a job when dawn is approaching, and is a value in minutes. // - If observation is between option "Pre-dawn Time" and dawn, score is BAD_SCORE/50. // - If observation is before dawn today, score is fraction of the day from beginning of observation to dawn time, as percentage. // - If observation is after dusk, score is fraction of the day from dusk to beginning of observation, as percentage. // - If observation is between dawn and dusk, score is BAD_SCORE. // // If observation time is invalid, the score is calculated for the current day time. // Note exact dusk time is considered valid in terms of night time, and will return a positive, albeit null, score. // FIXME: Dark sky score should consider the middle of the local night as best value. // FIXME: Current algorithm uses the dawn and dusk of today, instead of the day of the observation. int const earlyDawnSecs = static_cast ((Dawn - static_cast (Options::preDawnTime()) / minsPerDay) * secsPerDay); int const dawnSecs = static_cast (Dawn * secsPerDay); int const duskSecs = static_cast (Dusk * secsPerDay); int const obsSecs = (when.isValid() ? when : KStarsData::Instance()->lt()).time().msecsSinceStartOfDay() / 1000; int16_t score = 0; if (earlyDawnSecs <= obsSecs && obsSecs < dawnSecs) { score = BAD_SCORE / 50; //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (between pre-dawn and dawn).") // .arg(observationDateTime.toString()) // .arg(QString::asprintf("%+d", score)); } else if (obsSecs < dawnSecs) { score = static_cast ((dawnSecs - obsSecs) / secsPerDay) * 100; //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (before dawn).") // .arg(observationDateTime.toString()) // .arg(QString::asprintf("%+d", score)); } else if (duskSecs <= obsSecs) { score = static_cast ((obsSecs - duskSecs) / secsPerDay) * 100; //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (after dusk).") // .arg(observationDateTime.toString()) // .arg(QString::asprintf("%+d", score)); } else { score = BAD_SCORE; //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (during daylight).") // .arg(observationDateTime.toString()) // .arg(QString::asprintf("%+d", score)); } return score; } int16_t Scheduler::calculateJobScore(SchedulerJob const *job, QDateTime const &when) const { if (nullptr == job) return BAD_SCORE; /* Only consolidate the score if light frames are required, calibration frames can run whenever needed */ if (!job->getLightFramesRequired()) return 1000; int16_t total = 0; /* As soon as one score is negative, it's a no-go and other scores are unneeded */ if (job->getEnforceTwilight()) { int16_t const darkSkyScore = getDarkSkyScore(when); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' dark sky score is %2 at %3") .arg(job->getName()) .arg(QString::asprintf("%+d", darkSkyScore)) .arg(when.toString(job->getDateTimeDisplayFormat())); total += darkSkyScore; } /* We still enforce altitude if the job is neither required to track nor guide, because this is too confusing for the end-user. * If we bypass calculation here, it must also be bypassed when checking job constraints in checkJobStage. */ if (0 <= total /*&& ((job->getStepPipeline() & SchedulerJob::USE_TRACK) || (job->getStepPipeline() & SchedulerJob::USE_GUIDE))*/) { int16_t const altitudeScore = job->getAltitudeScore(when); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' altitude score is %2 at %3") .arg(job->getName()) .arg(QString::asprintf("%+d", altitudeScore)) .arg(when.toString(job->getDateTimeDisplayFormat())); total += altitudeScore; } if (0 <= total) { int16_t const moonSeparationScore = job->getMoonSeparationScore(when); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' Moon separation score is %2 at %3") .arg(job->getName()) .arg(QString::asprintf("%+d", moonSeparationScore)) .arg(when.toString(job->getDateTimeDisplayFormat())); total += moonSeparationScore; } qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a total score of %2 at %3.") .arg(job->getName()) .arg(QString::asprintf("%+d", total)) .arg(when.toString(job->getDateTimeDisplayFormat())); return total; } void Scheduler::calculateDawnDusk() { KSAlmanac ksal; Dawn = ksal.getDawnAstronomicalTwilight() + Options::dawnOffset() / 24.0; Dusk = ksal.getDuskAstronomicalTwilight() + Options::duskOffset() / 24.0; QTime const dawn = QTime(0, 0, 0).addSecs(Dawn * 24 * 3600); QTime const dusk = QTime(0, 0, 0).addSecs(Dusk * 24 * 3600); duskDateTime.setDate(KStars::Instance()->data()->lt().date()); duskDateTime.setTime(dusk); nightTime->setText(i18n("%1 - %2", dusk.toString("hh:mm"), dawn.toString("hh:mm"))); } void Scheduler::executeJob(SchedulerJob *job) { // Some states have executeJob called after current job is cancelled - checkStatus does this if (job == nullptr) return; // Don't execute the current job if it is already busy if (currentJob == job && SchedulerJob::JOB_BUSY == currentJob->getState()) return; setCurrentJob(job); int index = jobs.indexOf(job); if (index >= 0) queueTable->selectRow(index); QDateTime const now = KStarsData::Instance()->lt(); // If we already started, we check when the next object is scheduled at. // If it is more than 30 minutes in the future, we park the mount if that is supported // and we unpark when it is due to start. //int const nextObservationTime = now.secsTo(currentJob->getStartupTime()); // If the time to wait is greater than the lead time (5 minutes by default) // then we sleep, otherwise we wait. It's the same thing, just different labels. if (shouldSchedulerSleep(currentJob)) return; // If job schedule isn't now, wait - continuing to execute would cancel a parking attempt else if (0 < KStarsData::Instance()->lt().secsTo(currentJob->getStartupTime())) return; // From this point job can be executed now if (job->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE && Options::rememberJobProgress()) { captureInterface->setProperty("targetName", job->getName().replace(' ', "")); } updatePreDawn(); // Reset autofocus so that focus step is applied properly when checked // When the focus step is not checked, the capture module will eventually run focus periodically autofocusCompleted = false; qCInfo(KSTARS_EKOS_SCHEDULER) << "Executing Job " << currentJob->getName(); currentJob->setState(SchedulerJob::JOB_BUSY); KNotification::event(QLatin1String("EkosSchedulerJobStart"), i18n("Ekos job started (%1)", currentJob->getName())); // No need to continue evaluating jobs as we already have one. schedulerTimer.stop(); jobTimer.start(); } bool Scheduler::checkEkosState() { if (state == SCHEDULER_PAUSED) return false; switch (ekosState) { case EKOS_IDLE: { if (m_EkosCommunicationStatus == Ekos::Success) { ekosState = EKOS_READY; return true; } else { ekosInterface->call(QDBus::AutoDetect, "start"); ekosState = EKOS_STARTING; currentOperationTime.start(); qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos communication status is" << m_EkosCommunicationStatus << "Starting Ekos..."; return false; } } case EKOS_STARTING: { if (m_EkosCommunicationStatus == Ekos::Success) { appendLogText(i18n("Ekos started.")); ekosConnectFailureCount = 0; ekosState = EKOS_READY; return true; } else if (m_EkosCommunicationStatus == Ekos::Error) { if (ekosConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Starting Ekos failed. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "start"); return false; } appendLogText(i18n("Starting Ekos failed.")); stop(); return false; } else if (m_EkosCommunicationStatus == Ekos::Idle) return false; // If a minute passed, give up else if (currentOperationTime.elapsed() > (60 * 1000)) { if (ekosConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Starting Ekos timed out. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "stop"); QTimer::singleShot(1000, this, [&]() { ekosInterface->call(QDBus::AutoDetect, "start"); currentOperationTime.restart(); }); return false; } appendLogText(i18n("Starting Ekos timed out.")); stop(); return false; } } break; case EKOS_STOPPING: { if (m_EkosCommunicationStatus == Ekos::Idle) { appendLogText(i18n("Ekos stopped.")); ekosState = EKOS_IDLE; return true; } } break; case EKOS_READY: return true; } return false; } bool Scheduler::isINDIConnected() { return (m_INDICommunicationStatus == Ekos::Success); } bool Scheduler::checkINDIState() { if (state == SCHEDULER_PAUSED) return false; //qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI State" << indiState; switch (indiState) { case INDI_IDLE: { if (m_INDICommunicationStatus == Ekos::Success) { indiState = INDI_PROPERTY_CHECK; indiConnectFailureCount = 0; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI Properties..."; } else { qCDebug(KSTARS_EKOS_SCHEDULER) << "Connecting INDI devices..."; ekosInterface->call(QDBus::AutoDetect, "connectDevices"); indiState = INDI_CONNECTING; currentOperationTime.start(); } } break; case INDI_CONNECTING: { if (m_INDICommunicationStatus == Ekos::Success) { appendLogText(i18n("INDI devices connected.")); indiState = INDI_PROPERTY_CHECK; } else if (m_INDICommunicationStatus == Ekos::Error) { if (indiConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("One or more INDI devices failed to connect. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "connectDevices"); } else { appendLogText(i18n("One or more INDI devices failed to connect. Check INDI control panel for details.")); stop(); } } // If 30 seconds passed, we retry else if (currentOperationTime.elapsed() > (30 * 1000)) { if (indiConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("One or more INDI devices timed out. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "connectDevices"); currentOperationTime.restart(); } else { appendLogText(i18n("One or more INDI devices timed out. Check INDI control panel for details.")); stop(); } } } break; case INDI_DISCONNECTING: { if (m_INDICommunicationStatus == Ekos::Idle) { appendLogText(i18n("INDI devices disconnected.")); indiState = INDI_IDLE; return true; } } break; case INDI_PROPERTY_CHECK: { qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI properties."; // If dome unparking is required then we wait for dome interface if (unparkDomeCheck->isChecked() && m_DomeReady == false) { if (currentOperationTime.elapsed() > (30 * 1000)) { currentOperationTime.restart(); appendLogText(i18n("Warning: dome device not ready after timeout, attempting to recover...")); disconnectINDI(); stopEkos(); } qCDebug(KSTARS_EKOS_SCHEDULER) << "Dome unpark required but dome is not yet ready."; return false; } // If mount unparking is required then we wait for mount interface if (unparkMountCheck->isChecked() && m_MountReady == false) { if (currentOperationTime.elapsed() > (30 * 1000)) { currentOperationTime.restart(); appendLogText(i18n("Warning: mount device not ready after timeout, attempting to recover...")); disconnectINDI(); stopEkos(); } qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount unpark required but mount is not yet ready."; return false; } // If cap unparking is required then we wait for cap interface if (uncapCheck->isChecked() && m_CapReady == false) { if (currentOperationTime.elapsed() > (30 * 1000)) { currentOperationTime.restart(); appendLogText(i18n("Warning: cap device not ready after timeout, attempting to recover...")); disconnectINDI(); stopEkos(); } qCDebug(KSTARS_EKOS_SCHEDULER) << "Cap unpark required but cap is not yet ready."; return false; } // capture interface is required at all times to proceed. if (captureInterface.isNull()) return false; if (m_CaptureReady == false) { QVariant hasCoolerControl = captureInterface->property("coolerControl"); if (hasCoolerControl.isValid()) { warmCCDCheck->setEnabled(hasCoolerControl.toBool()); m_CaptureReady = true; } else qCWarning(KSTARS_EKOS_SCHEDULER) << "Capture module is not ready yet..."; } indiState = INDI_READY; indiConnectFailureCount = 0; return true; } case INDI_READY: return true; } return false; } bool Scheduler::checkStartupState() { if (state == SCHEDULER_PAUSED) return false; qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Checking Startup State (%1)...").arg(startupState); switch (startupState) { case STARTUP_IDLE: { KNotification::event(QLatin1String("ObservatoryStartup"), i18n("Observatory is in the startup process")); qCDebug(KSTARS_EKOS_SCHEDULER) << "Startup Idle. Starting startup process..."; // If Ekos is already started, we skip the script and move on to dome unpark step // unless we do not have light frames, then we skip all //QDBusReply isEkosStarted; //isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus"); //if (isEkosStarted.value() == Ekos::Success) if (m_EkosCommunicationStatus == Ekos::Success) { if (startupScriptURL.isEmpty() == false) appendLogText(i18n("Ekos is already started, skipping startup script...")); if (currentJob->getLightFramesRequired()) startupState = STARTUP_UNPARK_DOME; else startupState = STARTUP_COMPLETE; return true; } if (schedulerProfileCombo->currentText() != i18n("Default")) { QList profile; profile.append(schedulerProfileCombo->currentText()); ekosInterface->callWithArgumentList(QDBus::AutoDetect, "setProfile", profile); } if (startupScriptURL.isEmpty() == false) { startupState = STARTUP_SCRIPT; executeScript(startupScriptURL.toString(QUrl::PreferLocalFile)); return false; } startupState = STARTUP_UNPARK_DOME; return false; } case STARTUP_SCRIPT: return false; case STARTUP_UNPARK_DOME: // If there is no job in case of manual startup procedure, // or if the job requires light frames, let's proceed with // unparking the dome, otherwise startup process is complete. if (currentJob == nullptr || currentJob->getLightFramesRequired()) { if (unparkDomeCheck->isEnabled() && unparkDomeCheck->isChecked()) unParkDome(); else startupState = STARTUP_UNPARK_MOUNT; } else { startupState = STARTUP_COMPLETE; return true; } break; case STARTUP_UNPARKING_DOME: checkDomeParkingStatus(); break; case STARTUP_UNPARK_MOUNT: if (unparkMountCheck->isEnabled() && unparkMountCheck->isChecked()) unParkMount(); else startupState = STARTUP_UNPARK_CAP; break; case STARTUP_UNPARKING_MOUNT: checkMountParkingStatus(); break; case STARTUP_UNPARK_CAP: if (uncapCheck->isEnabled() && uncapCheck->isChecked()) unParkCap(); else startupState = STARTUP_COMPLETE; break; case STARTUP_UNPARKING_CAP: checkCapParkingStatus(); break; case STARTUP_COMPLETE: return true; case STARTUP_ERROR: stop(); return true; } return false; } bool Scheduler::checkShutdownState() { if (state == SCHEDULER_PAUSED) return false; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking shutdown state..."; switch (shutdownState) { case SHUTDOWN_IDLE: KNotification::event(QLatin1String("ObservatoryShutdown"), i18n("Observatory is in the shutdown process")); qCInfo(KSTARS_EKOS_SCHEDULER) << "Starting shutdown process..."; // weatherTimer.stop(); // weatherTimer.disconnect(); weatherLabel->hide(); jobTimer.stop(); setCurrentJob(nullptr); if (state == SCHEDULER_RUNNING) schedulerTimer.start(); if (preemptiveShutdown == false) { sleepTimer.stop(); //sleepTimer.disconnect(); } if (warmCCDCheck->isEnabled() && warmCCDCheck->isChecked()) { appendLogText(i18n("Warming up CCD...")); // Turn it off //QVariant arg(false); //captureInterface->call(QDBus::AutoDetect, "setCoolerControl", arg); captureInterface->setProperty("coolerControl", false); } // The following steps require a connection to the INDI server if (isINDIConnected()) { if (capCheck->isEnabled() && capCheck->isChecked()) { shutdownState = SHUTDOWN_PARK_CAP; return false; } if (parkMountCheck->isEnabled() && parkMountCheck->isChecked()) { shutdownState = SHUTDOWN_PARK_MOUNT; return false; } if (parkDomeCheck->isEnabled() && parkDomeCheck->isChecked()) { shutdownState = SHUTDOWN_PARK_DOME; return false; } } else appendLogText(i18n("Warning: Bypassing parking procedures, no INDI connection.")); if (shutdownScriptURL.isEmpty() == false) { shutdownState = SHUTDOWN_SCRIPT; return false; } shutdownState = SHUTDOWN_COMPLETE; return true; case SHUTDOWN_PARK_CAP: if (!isINDIConnected()) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection."; shutdownState = SHUTDOWN_SCRIPT; } else if (capCheck->isEnabled() && capCheck->isChecked()) parkCap(); else shutdownState = SHUTDOWN_PARK_MOUNT; break; case SHUTDOWN_PARKING_CAP: checkCapParkingStatus(); break; case SHUTDOWN_PARK_MOUNT: if (!isINDIConnected()) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection."; shutdownState = SHUTDOWN_SCRIPT; } else if (parkMountCheck->isEnabled() && parkMountCheck->isChecked()) parkMount(); else shutdownState = SHUTDOWN_PARK_DOME; break; case SHUTDOWN_PARKING_MOUNT: checkMountParkingStatus(); break; case SHUTDOWN_PARK_DOME: if (!isINDIConnected()) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection."; shutdownState = SHUTDOWN_SCRIPT; } else if (parkDomeCheck->isEnabled() && parkDomeCheck->isChecked()) parkDome(); else shutdownState = SHUTDOWN_SCRIPT; break; case SHUTDOWN_PARKING_DOME: checkDomeParkingStatus(); break; case SHUTDOWN_SCRIPT: if (shutdownScriptURL.isEmpty() == false) { // Need to stop Ekos now before executing script if it happens to stop INDI if (ekosState != EKOS_IDLE && Options::shutdownScriptTerminatesINDI()) { stopEkos(); return false; } shutdownState = SHUTDOWN_SCRIPT_RUNNING; executeScript(shutdownScriptURL.toString(QUrl::PreferLocalFile)); } else shutdownState = SHUTDOWN_COMPLETE; break; case SHUTDOWN_SCRIPT_RUNNING: return false; case SHUTDOWN_COMPLETE: return true; case SHUTDOWN_ERROR: stop(); return true; } return false; } bool Scheduler::checkParkWaitState() { if (state == SCHEDULER_PAUSED) return false; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking Park Wait State..."; switch (parkWaitState) { case PARKWAIT_IDLE: return true; case PARKWAIT_PARK: parkMount(); break; case PARKWAIT_PARKING: checkMountParkingStatus(); break; case PARKWAIT_PARKED: return true; case PARKWAIT_UNPARK: unParkMount(); break; case PARKWAIT_UNPARKING: checkMountParkingStatus(); break; case PARKWAIT_UNPARKED: return true; case PARKWAIT_ERROR: appendLogText(i18n("park/unpark wait procedure failed, aborting...")); stop(); return true; } return false; } void Scheduler::executeScript(const QString &filename) { appendLogText(i18n("Executing script %1...", filename)); connect(&scriptProcess, &QProcess::readyReadStandardOutput, this, &Scheduler::readProcessOutput); connect(&scriptProcess, static_cast(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus) { checkProcessExit(exitCode); }); scriptProcess.start(filename); } void Scheduler::readProcessOutput() { appendLogText(scriptProcess.readAllStandardOutput().simplified()); } void Scheduler::checkProcessExit(int exitCode) { scriptProcess.disconnect(); if (exitCode == 0) { if (startupState == STARTUP_SCRIPT) startupState = STARTUP_UNPARK_DOME; else if (shutdownState == SHUTDOWN_SCRIPT_RUNNING) shutdownState = SHUTDOWN_COMPLETE; return; } if (startupState == STARTUP_SCRIPT) { appendLogText(i18n("Startup script failed, aborting...")); startupState = STARTUP_ERROR; } else if (shutdownState == SHUTDOWN_SCRIPT_RUNNING) { appendLogText(i18n("Shutdown script failed, aborting...")); shutdownState = SHUTDOWN_ERROR; } } bool Scheduler::checkStatus() { for (auto job : jobs) job->updateJobCells(); if (state == SCHEDULER_PAUSED) { if (currentJob == nullptr) { setPaused(); return false; } switch (currentJob->getState()) { case SchedulerJob::JOB_BUSY: // do nothing break; case SchedulerJob::JOB_COMPLETE: // start finding next job before pausing break; default: // in all other cases pause setPaused(); break; } } // #1 If no current job selected, let's check if we need to shutdown or evaluate jobs if (currentJob == nullptr) { // #2.1 If shutdown is already complete or in error, we need to stop if (shutdownState == SHUTDOWN_COMPLETE || shutdownState == SHUTDOWN_ERROR) { // If INDI is not done disconnecting, try again later if (indiState == INDI_DISCONNECTING && checkINDIState() == false) return false; // Disconnect INDI if required first if (indiState != INDI_IDLE && Options::stopEkosAfterShutdown()) { disconnectINDI(); return false; } // If Ekos is not done stopping, try again later if (ekosState == EKOS_STOPPING && checkEkosState() == false) return false; // Stop Ekos if required. if (ekosState != EKOS_IDLE && Options::stopEkosAfterShutdown()) { stopEkos(); return false; } if (shutdownState == SHUTDOWN_COMPLETE) appendLogText(i18n("Shutdown complete.")); else appendLogText(i18n("Shutdown procedure failed, aborting...")); // Stop Scheduler stop(); return true; } // #2.2 Check if shutdown is in progress if (shutdownState > SHUTDOWN_IDLE) { // If Ekos is not done stopping, try again later if (ekosState == EKOS_STOPPING && checkEkosState() == false) return false; checkShutdownState(); return false; } // #2.3 Check if park wait procedure is in progress if (checkParkWaitState() == false) return false; // #2.4 If not in shutdown state, evaluate the jobs evaluateJobs(); // #2.5 If there is no current job after evaluation, shutdown if (nullptr == currentJob) { checkShutdownState(); return false; } } // JM 2018-12-07: Check if we need to sleep else if (shouldSchedulerSleep(currentJob) == false) { // #3 Check if startup procedure has failed. if (startupState == STARTUP_ERROR) { // Stop Scheduler stop(); return true; } // #4 Check if startup procedure Phase #1 is complete (Startup script) if ((startupState == STARTUP_IDLE && checkStartupState() == false) || startupState == STARTUP_SCRIPT) return false; // #5 Check if Ekos is started if (checkEkosState() == false) return false; // #6 Check if INDI devices are connected. if (checkINDIState() == false) return false; // #6.1 Check if park wait procedure is in progress - in the case we're waiting for a distant job if (checkParkWaitState() == false) return false; // #7 Check if startup procedure Phase #2 is complete (Unparking phase) if (startupState > STARTUP_SCRIPT && startupState < STARTUP_ERROR && checkStartupState() == false) return false; // #8 Check it it already completed (should only happen starting a paused job) // Find the next job in this case, otherwise execute the current one if (currentJob->getState() == SchedulerJob::JOB_COMPLETE) findNextJob(); else executeJob(currentJob); } return true; } void Scheduler::checkJobStage() { Q_ASSERT_X(currentJob, __FUNCTION__, "Actual current job is required to check job stage"); if (!currentJob) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking job stage for" << currentJob->getName() << "startup" << currentJob->getStartupCondition() << currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()) << "state" << currentJob->getState(); QDateTime const now = KStarsData::Instance()->lt(); /* Refresh the score of the current job */ /* currentJob->setScore(calculateJobScore(currentJob, now)); */ /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) if (now < currentJob->getStartupTime()) return; // #1 Check if we need to stop at some point if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_AT && currentJob->getState() == SchedulerJob::JOB_BUSY) { // If the job reached it COMPLETION time, we stop it. if (now.secsTo(currentJob->getCompletionTime()) <= 0) { appendLogText(i18n("Job '%1' reached completion time %2, stopping.", currentJob->getName(), currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat()))); currentJob->setState(SchedulerJob::JOB_COMPLETE); stopCurrentJobAction(); findNextJob(); return; } } // #2 Check if altitude restriction still holds true if (-90 < currentJob->getMinAltitude()) { SkyPoint p = currentJob->getTargetCoords(); p.EquatorialToHorizontal(KStarsData::Instance()->lst(), geo->lat()); /* FIXME: find a way to use altitude cutoff here, because the job can be scheduled when evaluating, then aborted when running */ if (p.alt().Degrees() < currentJob->getMinAltitude()) { // Only terminate job due to altitude limitation if mount is NOT parked. if (isMountParked() == false) { appendLogText(i18n("Job '%1' current altitude (%2 degrees) crossed minimum constraint altitude (%3 degrees), " "marking idle.", currentJob->getName(), QString("%L1").arg(p.alt().Degrees(), 0, 'f', minAltitude->decimals()), QString("%L1").arg(currentJob->getMinAltitude(), 0, 'f', minAltitude->decimals()))); currentJob->setState(SchedulerJob::JOB_IDLE); stopCurrentJobAction(); findNextJob(); return; } } } // #3 Check if moon separation is still valid if (currentJob->getMinMoonSeparation() > 0) { SkyPoint p = currentJob->getTargetCoords(); p.EquatorialToHorizontal(KStarsData::Instance()->lst(), geo->lat()); double moonSeparation = currentJob->getCurrentMoonSeparation(); if (moonSeparation < currentJob->getMinMoonSeparation()) { // Only terminate job due to moon separation limitation if mount is NOT parked. if (isMountParked() == false) { appendLogText(i18n("Job '%2' current moon separation (%1 degrees) is lower than minimum constraint (%3 " "degrees), marking idle.", moonSeparation, currentJob->getName(), currentJob->getMinMoonSeparation())); currentJob->setState(SchedulerJob::JOB_IDLE); stopCurrentJobAction(); findNextJob(); return; } } } // #4 Check if we're not at dawn if (currentJob->getEnforceTwilight() && now > KStarsDateTime(preDawnDateTime)) { // If either mount or dome are not parked, we shutdown if we approach dawn if (isMountParked() == false || (parkDomeCheck->isEnabled() && isDomeParked() == false)) { // Minute is a DOUBLE value, do not use i18np appendLogText(i18n( "Job '%3' is now approaching astronomical twilight rise limit at %1 (%2 minutes safety margin), marking idle.", preDawnDateTime.toString(), Options::preDawnTime(), currentJob->getName())); currentJob->setState(SchedulerJob::JOB_IDLE); stopCurrentJobAction(); findNextJob(); return; } } // #5 Check system status to improve robustness // This handles external events such as disconnections or end-user manipulating INDI panel if (!checkStatus()) return; // #6 Check each stage is processing properly // FIXME: Vanishing property should trigger a call to its event callback switch (currentJob->getStage()) { case SchedulerJob::STAGE_IDLE: getNextAction(); break; case SchedulerJob::STAGE_ALIGNING: // Let's make sure align module does not become unresponsive if (currentOperationTime.elapsed() > ALIGN_INACTIVITY_TIMEOUT) { QVariant const status = alignInterface->property("status"); Ekos::AlignState alignStatus = static_cast(status.toInt()); if (alignStatus == Ekos::ALIGN_IDLE) { if (alignFailureCount++ < MAX_FAILURE_ATTEMPTS) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Align module timed out. Restarting request..."; startAstrometry(); } else { appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else currentOperationTime.restart(); } break; case SchedulerJob::STAGE_CAPTURING: // Let's make sure capture module does not become unresponsive if (currentOperationTime.elapsed() > CAPTURE_INACTIVITY_TIMEOUT) { QVariant const status = captureInterface->property("status"); Ekos::CaptureState captureStatus = static_cast(status.toInt()); if (captureStatus == Ekos::CAPTURE_IDLE) { if (captureFailureCount++ < MAX_FAILURE_ATTEMPTS) { qCDebug(KSTARS_EKOS_SCHEDULER) << "capture module timed out. Restarting request..."; startCapture(); } else { appendLogText(i18n("Warning: job '%1' capture procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else currentOperationTime.restart(); } break; case SchedulerJob::STAGE_FOCUSING: // Let's make sure focus module does not become unresponsive if (currentOperationTime.elapsed() > FOCUS_INACTIVITY_TIMEOUT) { QVariant const status = focusInterface->property("status"); Ekos::FocusState focusStatus = static_cast(status.toInt()); if (focusStatus == Ekos::FOCUS_IDLE || focusStatus == Ekos::FOCUS_WAITING) { if (focusFailureCount++ < MAX_FAILURE_ATTEMPTS) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus module timed out. Restarting request..."; startFocusing(); } else { appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else currentOperationTime.restart(); } break; case SchedulerJob::STAGE_GUIDING: // Let's make sure guide module does not become unresponsive if (currentOperationTime.elapsed() > GUIDE_INACTIVITY_TIMEOUT) { GuideState guideStatus = getGuidingStatus(); if (guideStatus == Ekos::GUIDE_IDLE || guideStatus == Ekos::GUIDE_CONNECTED || guideStatus == Ekos::GUIDE_DISCONNECTED) { if (guideFailureCount++ < MAX_FAILURE_ATTEMPTS) { qCDebug(KSTARS_EKOS_SCHEDULER) << "guide module timed out. Restarting request..."; startGuiding(); } else { appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else currentOperationTime.restart(); } break; case SchedulerJob::STAGE_SLEWING: case SchedulerJob::STAGE_RESLEWING: // While slewing or re-slewing, check slew status can still be obtained { QVariant const slewStatus = mountInterface->property("status"); if (slewStatus.isValid()) { // Send the slew status periodically to avoid the situation where the mount is already at location and does not send any event // FIXME: in that case, filter TRACKING events only? ISD::Telescope::Status const status = static_cast(slewStatus.toInt()); setMountStatus(status); } else { appendLogText(i18n("Warning: job '%1' lost connection to the mount, attempting to reconnect.", currentJob->getName())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } } break; case SchedulerJob::STAGE_SLEW_COMPLETE: case SchedulerJob::STAGE_RESLEWING_COMPLETE: // When done slewing or re-slewing and we use a dome, only shift to the next action when the dome is done moving if (m_DomeReady) { QVariant const isDomeMoving = domeInterface->property("isMoving"); if (!isDomeMoving.isValid()) { appendLogText(i18n("Warning: job '%1' lost connection to the dome, attempting to reconnect.", currentJob->getName())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } if (!isDomeMoving.value()) getNextAction(); } else getNextAction(); break; default: break; } } void Scheduler::getNextAction() { qCDebug(KSTARS_EKOS_SCHEDULER) << "Get next action..."; switch (currentJob->getStage()) { case SchedulerJob::STAGE_IDLE: if (currentJob->getLightFramesRequired()) { if (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK) startSlew(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS && autofocusCompleted == false) startFocusing(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) startAstrometry(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) if (getGuidingStatus() == GUIDE_GUIDING) { appendLogText(i18n("Guiding already running, directly start capturing.")); startCapture(); } else startGuiding(); else startCapture(); } else { if (currentJob->getStepPipeline()) appendLogText( i18n("Job '%1' is proceeding directly to capture stage because only calibration frames are pending.", currentJob->getName())); startCapture(); } break; case SchedulerJob::STAGE_SLEW_COMPLETE: if (currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS && autofocusCompleted == false) startFocusing(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) startAstrometry(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_FOCUS_COMPLETE: if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) startAstrometry(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_ALIGN_COMPLETE: currentJob->setStage(SchedulerJob::STAGE_RESLEWING); break; case SchedulerJob::STAGE_RESLEWING_COMPLETE: // If we have in-sequence-focus in the sequence file then we perform post alignment focusing so that the focus // frame is ready for the capture module in-sequence-focus procedure. if ((currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS) && currentJob->getInSequenceFocus()) // Post alignment re-focusing startFocusing(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_POSTALIGN_FOCUSING_COMPLETE: if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_GUIDING_COMPLETE: startCapture(); break; default: break; } } void Scheduler::stopCurrentJobAction() { if (nullptr != currentJob) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Job '" << currentJob->getName() << "' is stopping current action..." << currentJob->getStage(); switch (currentJob->getStage()) { case SchedulerJob::STAGE_IDLE: break; case SchedulerJob::STAGE_SLEWING: mountInterface->call(QDBus::AutoDetect, "abort"); break; case SchedulerJob::STAGE_FOCUSING: focusInterface->call(QDBus::AutoDetect, "abort"); break; case SchedulerJob::STAGE_ALIGNING: alignInterface->call(QDBus::AutoDetect, "abort"); break; case SchedulerJob::STAGE_CAPTURING: captureInterface->call(QDBus::AutoDetect, "abort"); break; default: break; } /* Reset interrupted job stage */ currentJob->setStage(SchedulerJob::STAGE_IDLE); } /* Guiding being a parallel process, check to stop it */ stopGuiding(); } bool Scheduler::manageConnectionLoss() { if (SCHEDULER_RUNNING != state) return false; // Don't manage loss if Ekos is actually down in the state machine switch (ekosState) { case EKOS_IDLE: case EKOS_STOPPING: return false; default: break; } // Don't manage loss if INDI is actually down in the state machine switch (indiState) { case INDI_IDLE: case INDI_DISCONNECTING: return false; default: break; } // If Ekos is assumed to be up, check its state //QDBusReply const isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus"); if (m_EkosCommunicationStatus == Ekos::Success) { qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Ekos is currently connected, checking INDI before mitigating connection loss."); // If INDI is assumed to be up, check its state if (isINDIConnected()) { // If both Ekos and INDI are assumed up, and are actually up, no mitigation needed, this is a DBus interface error qCDebug(KSTARS_EKOS_SCHEDULER) << QString("INDI is currently connected, no connection loss mitigation needed."); return false; } } // Stop actions of the current job stopCurrentJobAction(); // Acknowledge INDI and Ekos disconnections disconnectINDI(); stopEkos(); // Let the Scheduler attempt to connect INDI again return true; } void Scheduler::load() { QUrl fileURL = QFileDialog::getOpenFileUrl(this, i18n("Open Ekos Scheduler List"), dirPath, "Ekos Scheduler List (*.esl)"); if (fileURL.isEmpty()) return; if (fileURL.isValid() == false) { QString message = i18n("Invalid URL: %1", fileURL.toLocalFile()); KSNotification::sorry(message, i18n("Invalid URL")); return; } dirPath = QUrl(fileURL.url(QUrl::RemoveFilename)); /* Run a job idle evaluation after a successful load */ if (loadScheduler(fileURL.toLocalFile())) startJobEvaluation(); } bool Scheduler::loadScheduler(const QString &fileURL) { SchedulerState const old_state = state; state = SCHEDULER_LOADING; QFile sFile; sFile.setFileName(fileURL); if (!sFile.open(QIODevice::ReadOnly)) { QString message = i18n("Unable to open file %1", fileURL); KSNotification::sorry(message, i18n("Could Not Open File")); state = old_state; return false; } if (jobUnderEdit >= 0) resetJobEdit(); while (queueTable->rowCount() > 0) queueTable->removeRow(0); qDeleteAll(jobs); jobs.clear(); LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; XMLEle *ep = nullptr; XMLEle *subEP = nullptr; char c; // We expect all data read from the XML to be in the C locale - QLocale::c() QLocale cLocale = QLocale::c(); while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *tag = tagXMLEle(ep); if (!strcmp(tag, "Job")) processJobInfo(ep); else if (!strcmp(tag, "Profile")) { schedulerProfileCombo->setCurrentText(pcdataXMLEle(ep)); } else if (!strcmp(tag, "ErrorHandlingStrategy")) { setErrorHandlingStrategy(static_cast(cLocale.toInt(findXMLAttValu(ep, "value")))); subEP = findXMLEle(ep, "delay"); if (subEP) { errorHandlingDelaySB->setValue(cLocale.toInt(pcdataXMLEle(subEP))); } subEP = findXMLEle(ep, "RescheduleErrors"); errorHandlingRescheduleErrorsCB->setChecked(subEP != nullptr); } else if (!strcmp(tag, "StartupProcedure")) { XMLEle *procedure; startupScript->clear(); unparkDomeCheck->setChecked(false); unparkMountCheck->setChecked(false); uncapCheck->setChecked(false); for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0)) { const char *proc = pcdataXMLEle(procedure); if (!strcmp(proc, "StartupScript")) { startupScript->setText(findXMLAttValu(procedure, "value")); startupScriptURL = QUrl::fromUserInput(startupScript->text()); } else if (!strcmp(proc, "UnparkDome")) unparkDomeCheck->setChecked(true); else if (!strcmp(proc, "UnparkMount")) unparkMountCheck->setChecked(true); else if (!strcmp(proc, "UnparkCap")) uncapCheck->setChecked(true); } } else if (!strcmp(tag, "ShutdownProcedure")) { XMLEle *procedure; shutdownScript->clear(); warmCCDCheck->setChecked(false); parkDomeCheck->setChecked(false); parkMountCheck->setChecked(false); capCheck->setChecked(false); for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0)) { const char *proc = pcdataXMLEle(procedure); if (!strcmp(proc, "ShutdownScript")) { shutdownScript->setText(findXMLAttValu(procedure, "value")); shutdownScriptURL = QUrl::fromUserInput(shutdownScript->text()); } else if (!strcmp(proc, "ParkDome")) parkDomeCheck->setChecked(true); else if (!strcmp(proc, "ParkMount")) parkMountCheck->setChecked(true); else if (!strcmp(proc, "ParkCap")) capCheck->setChecked(true); else if (!strcmp(proc, "WarmCCD")) warmCCDCheck->setChecked(true); } } } delXMLEle(root); } else if (errmsg[0]) { appendLogText(QString(errmsg)); delLilXML(xmlParser); state = old_state; return false; } } schedulerURL = QUrl::fromLocalFile(fileURL); mosaicB->setEnabled(true); mDirty = false; delLilXML(xmlParser); state = old_state; return true; } bool Scheduler::processJobInfo(XMLEle *root) { XMLEle *ep; XMLEle *subEP; altConstraintCheck->setChecked(false); moonSeparationCheck->setChecked(false); weatherCheck->setChecked(false); twilightCheck->blockSignals(true); twilightCheck->setChecked(false); twilightCheck->blockSignals(false); minAltitude->setValue(minAltitude->minimum()); minMoonSeparation->setValue(minMoonSeparation->minimum()); // We expect all data read from the XML to be in the C locale - QLocale::c() QLocale cLocale = QLocale::c(); for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Name")) nameEdit->setText(pcdataXMLEle(ep)); else if (!strcmp(tagXMLEle(ep), "Priority")) prioritySpin->setValue(atoi(pcdataXMLEle(ep))); else if (!strcmp(tagXMLEle(ep), "Coordinates")) { subEP = findXMLEle(ep, "J2000RA"); if (subEP) { dms ra; ra.setH(cLocale.toDouble(pcdataXMLEle(subEP))); raBox->showInHours(ra); } subEP = findXMLEle(ep, "J2000DE"); if (subEP) { dms de; de.setD(cLocale.toDouble(pcdataXMLEle(subEP))); decBox->showInDegrees(de); } } else if (!strcmp(tagXMLEle(ep), "Sequence")) { sequenceEdit->setText(pcdataXMLEle(ep)); sequenceURL = QUrl::fromUserInput(sequenceEdit->text()); } else if (!strcmp(tagXMLEle(ep), "FITS")) { fitsEdit->setText(pcdataXMLEle(ep)); fitsURL.setPath(fitsEdit->text()); } else if (!strcmp(tagXMLEle(ep), "StartupCondition")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp("ASAP", pcdataXMLEle(subEP))) asapConditionR->setChecked(true); else if (!strcmp("Culmination", pcdataXMLEle(subEP))) { culminationConditionR->setChecked(true); culminationOffset->setValue(cLocale.toDouble(findXMLAttValu(subEP, "value"))); } else if (!strcmp("At", pcdataXMLEle(subEP))) { startupTimeConditionR->setChecked(true); startupTimeEdit->setDateTime(QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate)); } } } else if (!strcmp(tagXMLEle(ep), "Constraints")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp("MinimumAltitude", pcdataXMLEle(subEP))) { altConstraintCheck->setChecked(true); minAltitude->setValue(cLocale.toDouble(findXMLAttValu(subEP, "value"))); } else if (!strcmp("MoonSeparation", pcdataXMLEle(subEP))) { moonSeparationCheck->setChecked(true); minMoonSeparation->setValue(cLocale.toDouble(findXMLAttValu(subEP, "value"))); } else if (!strcmp("EnforceWeather", pcdataXMLEle(subEP))) weatherCheck->setChecked(true); else if (!strcmp("EnforceTwilight", pcdataXMLEle(subEP))) twilightCheck->setChecked(true); } } else if (!strcmp(tagXMLEle(ep), "CompletionCondition")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp("Sequence", pcdataXMLEle(subEP))) sequenceCompletionR->setChecked(true); else if (!strcmp("Repeat", pcdataXMLEle(subEP))) { repeatCompletionR->setChecked(true); repeatsSpin->setValue(cLocale.toInt(findXMLAttValu(subEP, "value"))); } else if (!strcmp("Loop", pcdataXMLEle(subEP))) loopCompletionR->setChecked(true); else if (!strcmp("At", pcdataXMLEle(subEP))) { timeCompletionR->setChecked(true); completionTimeEdit->setDateTime(QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate)); } } } else if (!strcmp(tagXMLEle(ep), "Steps")) { XMLEle *module; trackStepCheck->setChecked(false); focusStepCheck->setChecked(false); alignStepCheck->setChecked(false); guideStepCheck->setChecked(false); for (module = nextXMLEle(ep, 1); module != nullptr; module = nextXMLEle(ep, 0)) { const char *proc = pcdataXMLEle(module); if (!strcmp(proc, "Track")) trackStepCheck->setChecked(true); else if (!strcmp(proc, "Focus")) focusStepCheck->setChecked(true); else if (!strcmp(proc, "Align")) alignStepCheck->setChecked(true); else if (!strcmp(proc, "Guide")) guideStepCheck->setChecked(true); } } } addToQueueB->setEnabled(true); saveJob(); return true; } void Scheduler::saveAs() { schedulerURL.clear(); save(); } void Scheduler::save() { QUrl backupCurrent = schedulerURL; if (schedulerURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || schedulerURL.toLocalFile().contains("/Temp")) schedulerURL.clear(); // If no changes made, return. if (mDirty == false && !schedulerURL.isEmpty()) return; if (schedulerURL.isEmpty()) { schedulerURL = QFileDialog::getSaveFileUrl(this, i18n("Save Ekos Scheduler List"), dirPath, "Ekos Scheduler List (*.esl)"); // if user presses cancel if (schedulerURL.isEmpty()) { schedulerURL = backupCurrent; return; } dirPath = QUrl(schedulerURL.url(QUrl::RemoveFilename)); if (schedulerURL.toLocalFile().contains('.') == 0) schedulerURL.setPath(schedulerURL.toLocalFile() + ".esl"); } if (schedulerURL.isValid()) { if ((saveScheduler(schedulerURL)) == false) { KSNotification::error(i18n("Failed to save scheduler list"), i18n("Save")); return; } mDirty = false; } else { QString message = i18n("Invalid URL: %1", schedulerURL.url()); KSNotification::sorry(message, i18n("Invalid URL")); } } bool Scheduler::saveScheduler(const QUrl &fileURL) { QFile file; file.setFileName(fileURL.toLocalFile()); if (!file.open(QIODevice::WriteOnly)) { QString message = i18n("Unable to write to file %1", fileURL.toLocalFile()); KSNotification::sorry(message, i18n("Could Not Open File")); return false; } QTextStream outstream(&file); // We serialize sequence data to XML using the C locale QLocale cLocale = QLocale::c(); outstream << "" << endl; outstream << "" << endl; outstream << "" << schedulerProfileCombo->currentText() << "" << endl; foreach (SchedulerJob *job, jobs) { outstream << "" << endl; outstream << "" << job->getName() << "" << endl; outstream << "" << job->getPriority() << "" << endl; outstream << "" << endl; outstream << "" << cLocale.toString(job->getTargetCoords().ra0().Hours()) << "" << endl; outstream << "" << cLocale.toString(job->getTargetCoords().dec0().Degrees()) << "" << endl; outstream << "" << endl; if (job->getFITSFile().isValid() && job->getFITSFile().isEmpty() == false) outstream << "" << job->getFITSFile().toLocalFile() << "" << endl; outstream << "" << job->getSequenceFile().toLocalFile() << "" << endl; outstream << "" << endl; if (job->getFileStartupCondition() == SchedulerJob::START_ASAP) outstream << "ASAP" << endl; else if (job->getFileStartupCondition() == SchedulerJob::START_CULMINATION) outstream << "Culmination" << endl; else if (job->getFileStartupCondition() == SchedulerJob::START_AT) outstream << "At" << endl; outstream << "" << endl; outstream << "" << endl; if (-90 < job->getMinAltitude()) outstream << "MinimumAltitude" << endl; if (job->getMinMoonSeparation() > 0) outstream << "MoonSeparation" << endl; if (job->getEnforceWeather()) outstream << "EnforceWeather" << endl; if (job->getEnforceTwilight()) outstream << "EnforceTwilight" << endl; outstream << "" << endl; outstream << "" << endl; if (job->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE) outstream << "Sequence" << endl; else if (job->getCompletionCondition() == SchedulerJob::FINISH_REPEAT) outstream << "Repeat" << endl; else if (job->getCompletionCondition() == SchedulerJob::FINISH_LOOP) outstream << "Loop" << endl; else if (job->getCompletionCondition() == SchedulerJob::FINISH_AT) outstream << "At" << endl; outstream << "" << endl; outstream << "" << endl; if (job->getStepPipeline() & SchedulerJob::USE_TRACK) outstream << "Track" << endl; if (job->getStepPipeline() & SchedulerJob::USE_FOCUS) outstream << "Focus" << endl; if (job->getStepPipeline() & SchedulerJob::USE_ALIGN) outstream << "Align" << endl; if (job->getStepPipeline() & SchedulerJob::USE_GUIDE) outstream << "Guide" << endl; outstream << "" << endl; outstream << "" << endl; } outstream << "" << endl; if (errorHandlingRescheduleErrorsCB->isChecked()) outstream << "" << endl; outstream << "" << errorHandlingDelaySB->value() << "" << endl; outstream << "" << endl; outstream << "" << endl; if (startupScript->text().isEmpty() == false) outstream << "StartupScript" << endl; if (unparkDomeCheck->isChecked()) outstream << "UnparkDome" << endl; if (unparkMountCheck->isChecked()) outstream << "UnparkMount" << endl; if (uncapCheck->isChecked()) outstream << "UnparkCap" << endl; outstream << "" << endl; outstream << "" << endl; if (warmCCDCheck->isChecked()) outstream << "WarmCCD" << endl; if (capCheck->isChecked()) outstream << "ParkCap" << endl; if (parkMountCheck->isChecked()) outstream << "ParkMount" << endl; if (parkDomeCheck->isChecked()) outstream << "ParkDome" << endl; if (shutdownScript->text().isEmpty() == false) outstream << "ShutdownScript" << endl; outstream << "" << endl; outstream << "" << endl; appendLogText(i18n("Scheduler list saved to %1", fileURL.toLocalFile())); file.close(); return true; } void Scheduler::startSlew() { Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting slewing must be valid"); // If the mount was parked by a pause or the end-user, unpark if (isMountParked()) { parkWaitState = PARKWAIT_UNPARK; return; } if (Options::resetMountModelBeforeJob()) mountInterface->call(QDBus::AutoDetect, "resetModel"); SkyPoint target = currentJob->getTargetCoords(); QList telescopeSlew; telescopeSlew.append(target.ra().Hours()); telescopeSlew.append(target.dec().Degrees()); QDBusReply const slewModeReply = mountInterface->callWithArgumentList(QDBus::AutoDetect, "slew", telescopeSlew); if (slewModeReply.error().type() != QDBusError::NoError) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' slew request received DBUS error: %2").arg(currentJob->getName(), QDBusError::errorString(slewModeReply.error().type())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); } else { currentJob->setStage(SchedulerJob::STAGE_SLEWING); appendLogText(i18n("Job '%1' is slewing to target.", currentJob->getName())); } } void Scheduler::startFocusing() { Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting focusing must be valid"); // 2017-09-30 Jasem: We're skipping post align focusing now as it can be performed // when first focus request is made in capture module if (currentJob->getStage() == SchedulerJob::STAGE_RESLEWING_COMPLETE || currentJob->getStage() == SchedulerJob::STAGE_POSTALIGN_FOCUSING) { // Clear the HFR limit value set in the capture module captureInterface->call(QDBus::AutoDetect, "clearAutoFocusHFR"); // Reset Focus frame so that next frame take a full-resolution capture first. focusInterface->call(QDBus::AutoDetect, "resetFrame"); currentJob->setStage(SchedulerJob::STAGE_POSTALIGN_FOCUSING_COMPLETE); getNextAction(); return; } // Check if autofocus is supported QDBusReply focusModeReply; focusModeReply = focusInterface->call(QDBus::AutoDetect, "canAutoFocus"); if (focusModeReply.error().type() != QDBusError::NoError) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' canAutoFocus request received DBUS error: %2").arg(currentJob->getName(), QDBusError::errorString(focusModeReply.error().type())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } if (focusModeReply.value() == false) { appendLogText(i18n("Warning: job '%1' is unable to proceed with autofocus, not supported.", currentJob->getName())); currentJob->setStepPipeline( static_cast(currentJob->getStepPipeline() & ~SchedulerJob::USE_FOCUS)); currentJob->setStage(SchedulerJob::STAGE_FOCUS_COMPLETE); getNextAction(); return; } // Clear the HFR limit value set in the capture module captureInterface->call(QDBus::AutoDetect, "clearAutoFocusHFR"); QDBusMessage reply; // We always need to reset frame first if ((reply = focusInterface->call(QDBus::AutoDetect, "resetFrame")).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' resetFrame request received DBUS error: %2").arg(currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } // Set autostar if full field option is false if (Options::focusUseFullField() == false) { QList autoStar; autoStar.append(true); if ((reply = focusInterface->callWithArgumentList(QDBus::AutoDetect, "setAutoStarEnabled", autoStar)).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' setAutoFocusStar request received DBUS error: %1").arg(currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } } // Start auto-focus if ((reply = focusInterface->call(QDBus::AutoDetect, "start")).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' startFocus request received DBUS error: %2").arg(currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } /*if (currentJob->getStage() == SchedulerJob::STAGE_RESLEWING_COMPLETE || currentJob->getStage() == SchedulerJob::STAGE_POSTALIGN_FOCUSING) { currentJob->setStage(SchedulerJob::STAGE_POSTALIGN_FOCUSING); appendLogText(i18n("Post-alignment focusing for %1 ...", currentJob->getName())); } else { currentJob->setStage(SchedulerJob::STAGE_FOCUSING); appendLogText(i18n("Focusing %1 ...", currentJob->getName())); }*/ currentJob->setStage(SchedulerJob::STAGE_FOCUSING); appendLogText(i18n("Job '%1' is focusing.", currentJob->getName())); currentOperationTime.restart(); } void Scheduler::findNextJob() { if (state == SCHEDULER_PAUSED) { // everything finished, we can pause setPaused(); return; } Q_ASSERT_X(currentJob->getState() == SchedulerJob::JOB_ERROR || currentJob->getState() == SchedulerJob::JOB_ABORTED || currentJob->getState() == SchedulerJob::JOB_COMPLETE || currentJob->getState() == SchedulerJob::JOB_IDLE, __FUNCTION__, "Finding next job requires current to be in error, aborted, idle or complete"); jobTimer.stop(); // Reset failed count alignFailureCount = guideFailureCount = focusFailureCount = captureFailureCount = 0; /* FIXME: Other debug logs in that function probably */ qCDebug(KSTARS_EKOS_SCHEDULER) << "Find next job..."; if (currentJob->getState() == SchedulerJob::JOB_ERROR || currentJob->getState() == SchedulerJob::JOB_ABORTED) { captureBatch = 0; // Stop Guiding if it was used stopGuiding(); if (currentJob->getState() == SchedulerJob::JOB_ERROR) appendLogText(i18n("Job '%1' is terminated due to errors.", currentJob->getName())); else appendLogText(i18n("Job '%1' is aborted.", currentJob->getName())); // Always reset job stage currentJob->setStage(SchedulerJob::STAGE_IDLE); // restart aborted jobs immediately, if error handling strategy is set to "restart immediately" if (errorHandlingRestartImmediatelyButton->isChecked() && (currentJob->getState() == SchedulerJob::JOB_ABORTED || (currentJob->getState() == SchedulerJob::JOB_ERROR && errorHandlingRescheduleErrorsCB->isChecked()))) { // reset the state so that it will be restarted currentJob->setState(SchedulerJob::JOB_SCHEDULED); appendLogText(i18n("Waiting %1 seconds to restart job '%2'.", errorHandlingDelaySB->value(), currentJob->getName())); // wait the given delay until the jobs will be evaluated again sleepTimer.setInterval(std::lround((errorHandlingDelaySB->value() * 1000) / KStarsData::Instance()->clock()->scale())); sleepTimer.start(); sleepLabel->setToolTip(i18n("Scheduler waits for a retry.")); sleepLabel->show(); return; } // otherwise start re-evaluation setCurrentJob(nullptr); schedulerTimer.start(); } else if (currentJob->getState() == SchedulerJob::JOB_IDLE) { // job constraints no longer valid, start re-evaluation setCurrentJob(nullptr); schedulerTimer.start(); } // Job is complete, so check completion criteria to optimize processing // In any case, we're done whether the job completed successfully or not. else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE) { /* If we remember job progress, mark the job idle as well as all its duplicates for re-evaluation */ if (Options::rememberJobProgress()) { foreach(SchedulerJob *a_job, jobs) if (a_job == currentJob || a_job->isDuplicateOf(currentJob)) a_job->setState(SchedulerJob::JOB_IDLE); } captureBatch = 0; // Stop Guiding if it was used stopGuiding(); appendLogText(i18n("Job '%1' is complete.", currentJob->getName())); // Always reset job stage currentJob->setStage(SchedulerJob::STAGE_IDLE); setCurrentJob(nullptr); schedulerTimer.start(); } else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_REPEAT) { /* If the job is about to repeat, decrease its repeat count and reset its start time */ if (0 < currentJob->getRepeatsRemaining()) { currentJob->setRepeatsRemaining(currentJob->getRepeatsRemaining() - 1); currentJob->setStartupTime(QDateTime()); } /* Mark the job idle as well as all its duplicates for re-evaluation */ foreach(SchedulerJob *a_job, jobs) if (a_job == currentJob || a_job->isDuplicateOf(currentJob)) a_job->setState(SchedulerJob::JOB_IDLE); /* Re-evaluate all jobs, without selecting a new job */ jobEvaluationOnly = true; evaluateJobs(); /* If current job is actually complete because of previous duplicates, prepare for next job */ if (currentJob == nullptr || currentJob->getRepeatsRemaining() == 0) { stopCurrentJobAction(); if (currentJob != nullptr) { appendLogText(i18np("Job '%1' is complete after #%2 batch.", "Job '%1' is complete after #%2 batches.", currentJob->getName(), currentJob->getRepeatsRequired())); setCurrentJob(nullptr); } schedulerTimer.start(); } /* If job requires more work, continue current observation */ else { /* FIXME: raise priority to allow other jobs to schedule in-between */ executeJob(currentJob); /* If we are guiding, continue capturing */ if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) { currentJob->setStage(SchedulerJob::STAGE_CAPTURING); startCapture(); } /* If we are not guiding, but using alignment, realign */ else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) { currentJob->setStage(SchedulerJob::STAGE_ALIGNING); startAstrometry(); } /* Else if we are neither guiding nor using alignment, slew back to target */ else if (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK) { currentJob->setStage(SchedulerJob::STAGE_SLEWING); startSlew(); } /* Else just start capturing */ else { currentJob->setStage(SchedulerJob::STAGE_CAPTURING); startCapture(); } appendLogText(i18np("Job '%1' is repeating, #%2 batch remaining.", "Job '%1' is repeating, #%2 batches remaining.", currentJob->getName(), currentJob->getRepeatsRemaining())); /* currentJob remains the same */ jobTimer.start(); } } else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP) { executeJob(currentJob); currentJob->setStage(SchedulerJob::STAGE_CAPTURING); captureBatch++; startCapture(); appendLogText(i18n("Job '%1' is repeating, looping indefinitely.", currentJob->getName())); /* currentJob remains the same */ jobTimer.start(); } else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_AT) { if (KStarsData::Instance()->lt().secsTo(currentJob->getCompletionTime()) <= 0) { /* Mark the job idle as well as all its duplicates for re-evaluation */ foreach(SchedulerJob *a_job, jobs) if (a_job == currentJob || a_job->isDuplicateOf(currentJob)) a_job->setState(SchedulerJob::JOB_IDLE); stopCurrentJobAction(); captureBatch = 0; appendLogText(i18np("Job '%1' stopping, reached completion time with #%2 batch done.", "Job '%1' stopping, reached completion time with #%2 batches done.", currentJob->getName(), captureBatch + 1)); // Always reset job stage currentJob->setStage(SchedulerJob::STAGE_IDLE); setCurrentJob(nullptr); schedulerTimer.start(); } else { executeJob(currentJob); currentJob->setStage(SchedulerJob::STAGE_CAPTURING); captureBatch++; startCapture(); appendLogText(i18np("Job '%1' completed #%2 batch before completion time, restarted.", "Job '%1' completed #%2 batches before completion time, restarted.", currentJob->getName(), captureBatch)); /* currentJob remains the same */ jobTimer.start(); } } else { /* Unexpected situation, mitigate by resetting the job and restarting the scheduler timer */ qCDebug(KSTARS_EKOS_SCHEDULER) << "BUGBUG! Job '" << currentJob->getName() << "' timer elapsed, but no action to be taken."; // Always reset job stage currentJob->setStage(SchedulerJob::STAGE_IDLE); setCurrentJob(nullptr); schedulerTimer.start(); } } void Scheduler::startAstrometry() { Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting aligning must be valid"); QDBusMessage reply; setSolverAction(Align::GOTO_SLEW); // Always turn update coords on //QVariant arg(true); //alignInterface->call(QDBus::AutoDetect, "setUpdateCoords", arg); // If FITS file is specified, then we use load and slew if (currentJob->getFITSFile().isEmpty() == false) { QList solveArgs; solveArgs.append(currentJob->getFITSFile().toString(QUrl::PreferLocalFile)); if ((reply = alignInterface->callWithArgumentList(QDBus::AutoDetect, "loadAndSlew", solveArgs)).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' loadAndSlew request received DBUS error: %2").arg(currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } loadAndSlewProgress = true; appendLogText(i18n("Job '%1' is plate solving %2.", currentJob->getName(), currentJob->getFITSFile().fileName())); } else { if ((reply = alignInterface->call(QDBus::AutoDetect, "captureAndSolve")).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' captureAndSolve request received DBUS error: %2").arg(currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } appendLogText(i18n("Job '%1' is capturing and plate solving.", currentJob->getName())); } /* FIXME: not supposed to modify the job */ currentJob->setStage(SchedulerJob::STAGE_ALIGNING); currentOperationTime.restart(); } void Scheduler::startGuiding(bool resetCalibration) { Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting guiding must be valid"); // avoid starting the guider twice if (resetCalibration == false && getGuidingStatus() == GUIDE_GUIDING) { appendLogText(i18n("Guiding already running for %1 ...", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_GUIDING); currentOperationTime.restart(); return; } // Connect Guider guideInterface->call(QDBus::AutoDetect, "connectGuider"); // Set Auto Star to true QVariant arg(true); guideInterface->call(QDBus::AutoDetect, "setCalibrationAutoStar", arg); // Only reset calibration on trouble // and if we are allowed to reset calibration (true by default) if (resetCalibration && Options::resetGuideCalibration()) guideInterface->call(QDBus::AutoDetect, "clearCalibration"); guideInterface->call(QDBus::AutoDetect, "guide"); currentJob->setStage(SchedulerJob::STAGE_GUIDING); appendLogText(i18n("Starting guiding procedure for %1 ...", currentJob->getName())); currentOperationTime.restart(); } void Scheduler::startCapture(bool restart) { Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting capturing must be valid"); // ensure that guiding is running before we start capturing if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE && getGuidingStatus() != GUIDE_GUIDING) { // guiding should run, but it doesn't. So start guiding first currentJob->setStage(SchedulerJob::STAGE_GUIDING); startGuiding(); return; } captureInterface->setProperty("targetName", currentJob->getName().replace(' ', "")); QString url = currentJob->getSequenceFile().toLocalFile(); if (restart == false) { QList dbusargs; dbusargs.append(url); captureInterface->callWithArgumentList(QDBus::AutoDetect, "loadSequenceQueue", dbusargs); } switch (currentJob->getCompletionCondition()) { case SchedulerJob::FINISH_LOOP: case SchedulerJob::FINISH_AT: // In these cases, we leave the captured frames map empty // to ensure, that the capture sequence is executed in any case. break; default: // Scheduler always sets captured frame map when starting a sequence - count may be different, robustness, dynamic priority // hand over the map of captured frames so that the capture // process knows about existing frames SchedulerJob::CapturedFramesMap fMap = currentJob->getCapturedFramesMap(); for (auto &e : fMap.keys()) { QList dbusargs; QDBusMessage reply; dbusargs.append(e); dbusargs.append(fMap.value(e)); if ((reply = captureInterface->callWithArgumentList(QDBus::AutoDetect, "setCapturedFramesMap", dbusargs)).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' setCapturedFramesCount request received DBUS error: %1").arg(currentJob->getName()).arg(reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } } break; } // Start capture process captureInterface->call(QDBus::AutoDetect, "start"); currentJob->setStage(SchedulerJob::STAGE_CAPTURING); KNotification::event(QLatin1String("EkosScheduledImagingStart"), i18n("Ekos job (%1) - Capture started", currentJob->getName())); if (captureBatch > 0) appendLogText(i18n("Job '%1' capture is in progress (batch #%2)...", currentJob->getName(), captureBatch + 1)); else appendLogText(i18n("Job '%1' capture is in progress...", currentJob->getName())); currentOperationTime.restart(); } void Scheduler::stopGuiding() { if (!guideInterface) return; // Tell guider to abort if the current job requires guiding - end-user may enable guiding manually before observation if (nullptr != currentJob && (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE)) { qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is stopping guiding...").arg(currentJob->getName()); guideInterface->call(QDBus::AutoDetect, "abort"); guideFailureCount = 0; } // In any case, stop the automatic guider restart if (restartGuidingTimer.isActive()) restartGuidingTimer.stop(); } void Scheduler::setSolverAction(Align::GotoMode mode) { QVariant gotoMode(static_cast(mode)); alignInterface->call(QDBus::AutoDetect, "setSolverAction", gotoMode); } void Scheduler::disconnectINDI() { qCInfo(KSTARS_EKOS_SCHEDULER) << "Disconnecting INDI..."; indiState = INDI_DISCONNECTING; ekosInterface->call(QDBus::AutoDetect, "disconnectDevices"); } void Scheduler::stopEkos() { qCInfo(KSTARS_EKOS_SCHEDULER) << "Stopping Ekos..."; ekosState = EKOS_STOPPING; ekosConnectFailureCount = 0; ekosInterface->call(QDBus::AutoDetect, "stop"); m_MountReady = m_CapReady = m_CaptureReady = m_DomeReady = false; } void Scheduler::setDirty() { mDirty = true; if (sender() == startupProcedureButtonGroup || sender() == shutdownProcedureGroup) return; if (0 <= jobUnderEdit && state != SCHEDULER_RUNNING && 0 <= queueTable->currentRow()) { // Now that jobs are sorted, reset jobs that are later than the edited one for re-evaluation for (int row = jobUnderEdit; row < jobs.size(); row++) jobs.at(row)->reset(); saveJob(); } // For object selection, all fields must be filled bool const nameSelectionOK = !raBox->isEmpty() && !decBox->isEmpty() && !nameEdit->text().isEmpty(); // For FITS selection, only the name and fits URL should be filled. bool const fitsSelectionOK = !nameEdit->text().isEmpty() && !fitsURL.isEmpty(); // Sequence selection is required bool const seqSelectionOK = !sequenceEdit->text().isEmpty(); // Finally, adding is allowed upon object/FITS and sequence selection bool const addingOK = (nameSelectionOK || fitsSelectionOK) && seqSelectionOK; addToQueueB->setEnabled(addingOK); mosaicB->setEnabled(addingOK); } void Scheduler::updateCompletedJobsCount(bool forced) { /* Use a temporary map in order to limit the number of file searches */ SchedulerJob::CapturedFramesMap newFramesCount; /* FIXME: Capture storage cache is refreshed too often, feature requires rework. */ /* Check if one job is idle or requires evaluation - if so, force refresh */ forced |= std::any_of(jobs.begin(), jobs.end(), [](SchedulerJob * oneJob) -> bool { SchedulerJob::JOBStatus const state = oneJob->getState(); return state == SchedulerJob::JOB_IDLE || state == SchedulerJob::JOB_EVALUATION;}); /* If update is forced, clear the frame map */ if (forced) capturedFramesCount.clear(); /* Enumerate SchedulerJobs to count captures that are already stored */ for (SchedulerJob *oneJob : jobs) { QList seqjobs; bool hasAutoFocus = false; //oneJob->setLightFramesRequired(false); /* Look into the sequence requirements, bypass if invalid */ if (loadSequenceQueue(oneJob->getSequenceFile().toLocalFile(), oneJob, seqjobs, hasAutoFocus) == false) { appendLogText(i18n("Warning: job '%1' has inaccessible sequence '%2', marking invalid.", oneJob->getName(), oneJob->getSequenceFile().toLocalFile())); oneJob->setState(SchedulerJob::JOB_INVALID); continue; } /* Enumerate the SchedulerJob's SequenceJobs to count captures stored for each */ for (SequenceJob *oneSeqJob : seqjobs) { /* Only consider captures stored on client (Ekos) side */ /* FIXME: ask the remote for the file count */ if (oneSeqJob->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) continue; /* FIXME: this signature path is incoherent when there is no filter wheel on the setup - bugfix should be elsewhere though */ QString const signature = oneSeqJob->getSignature(); /* If signature was processed during this run, keep it */ if (newFramesCount.constEnd() != newFramesCount.constFind(signature)) continue; /* If signature was processed during an earlier run, use the earlier count */ QMap::const_iterator const earlierRunIterator = capturedFramesCount.constFind(signature); if (capturedFramesCount.constEnd() != earlierRunIterator) { newFramesCount[signature] = earlierRunIterator.value(); continue; } /* Else recount captures already stored */ newFramesCount[signature] = getCompletedFiles(signature, oneSeqJob->getFullPrefix()); } // determine whether we need to continue capturing, depending on captured frames bool lightFramesRequired = false; switch (oneJob->getCompletionCondition()) { case SchedulerJob::FINISH_SEQUENCE: case SchedulerJob::FINISH_REPEAT: for (SequenceJob *oneSeqJob : seqjobs) { QString const signature = oneSeqJob->getSignature(); /* If frame is LIGHT, how hany do we have left? */ if (oneSeqJob->getFrameType() == FRAME_LIGHT && oneSeqJob->getCount()*oneJob->getRepeatsRequired() > newFramesCount[signature]) lightFramesRequired = true; } break; default: // in all other cases it does not depend on the number of captured frames lightFramesRequired = true; } oneJob->setLightFramesRequired(lightFramesRequired); } capturedFramesCount = newFramesCount; //if (forced) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Frame map summary:"; QMap::const_iterator it = capturedFramesCount.constBegin(); for (; it != capturedFramesCount.constEnd(); it++) qCDebug(KSTARS_EKOS_SCHEDULER) << " " << it.key() << ':' << it.value(); } } bool Scheduler::estimateJobTime(SchedulerJob *schedJob) { static SchedulerJob *jobWarned = nullptr; /* updateCompletedJobsCount(); */ // Load the sequence job associated with the argument scheduler job. QList seqJobs; bool hasAutoFocus = false; if (loadSequenceQueue(schedJob->getSequenceFile().toLocalFile(), schedJob, seqJobs, hasAutoFocus) == false) { qCWarning(KSTARS_EKOS_SCHEDULER) << QString("Warning: Failed estimating the duration of job '%1', its sequence file is invalid.").arg(schedJob->getSequenceFile().toLocalFile()); return false; } // FIXME: setting in-sequence focus should be done in XML processing. schedJob->setInSequenceFocus(hasAutoFocus); // Stop spam of log on re-evaluation. If we display the warning once, then that's it. if (schedJob != jobWarned && hasAutoFocus && !(schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS)) { appendLogText(i18n("Warning: Job '%1' has its focus step disabled, periodic and/or HFR procedures currently set in its sequence will not occur.", schedJob->getName())); jobWarned = schedJob; } /* This is the map of captured frames for this scheduler job, keyed per storage signature. * It will be forwarded to the Capture module in order to capture only what frames are required. * If option "Remember Job Progress" is disabled, this map will be empty, and the Capture module will process all requested captures unconditionally. */ SchedulerJob::CapturedFramesMap capture_map; bool const rememberJobProgress = Options::rememberJobProgress(); int totalSequenceCount = 0, totalCompletedCount = 0; double totalImagingTime = 0; // Determine number of captures in the scheduler job int capturesPerRepeat = 0; foreach (SequenceJob *seqJob, seqJobs) capturesPerRepeat += seqJob->getCount(); // Loop through sequence jobs to calculate the number of required frames and estimate duration. foreach (SequenceJob *seqJob, seqJobs) { // FIXME: find a way to actually display the filter name. QString seqName = i18n("Job '%1' %2x%3\" %4", schedJob->getName(), seqJob->getCount(), seqJob->getExposure(), seqJob->getFilterName()); if (seqJob->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) { qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 duration cannot be estimated time since the sequence saves the files remotely.").arg(seqName); schedJob->setEstimatedTime(-2); qDeleteAll(seqJobs); return true; } // Note that looping jobs will have zero repeats required. int const captures_required = seqJob->getCount() * schedJob->getRepeatsRequired(); int captures_completed = 0; if (rememberJobProgress) { /* Enumerate sequence jobs associated to this scheduler job, and assign them a completed count. * * The objective of this block is to fill the storage map of the scheduler job with completed counts for each capture storage. * * Sequence jobs capture to a storage folder, and are given a count of captures to store at that location. * The tricky part is to make sure the repeat count of the scheduler job is properly transferred to each sequence job. * * For instance, a scheduler job repeated three times must execute the full list of sequence jobs three times, thus * has to tell each sequence job it misses all captures, three times. It cannot tell the sequence job three captures are * missing, first because that's not how the sequence job is designed (completed count, not required count), and second * because this would make the single sequence job repeat three times, instead of repeating the full list of sequence * jobs three times. * * The consolidated storage map will be assigned to each sequence job based on their signature when the scheduler job executes them. * * For instance, consider a RGBL sequence of single captures. The map will store completed captures for R, G, B and L storages. * If R and G have 1 file each, and B and L have no files, map[storage(R)] = map[storage(G)] = 1 and map[storage(B)] = map[storage(L)] = 0. * When that scheduler job executes, only B and L captures will be processed. * * In the case of a RGBLRGB sequence of single captures, the second R, G and B map items will count one less capture than what is really in storage. * If R and G have 1 file each, and B and L have no files, map[storage(R1)] = map[storage(B1)] = 1, and all others will be 0. * When that scheduler job executes, B1, L, R2, G2 and B2 will be processed. * * This doesn't handle the case of duplicated scheduler jobs, that is, scheduler jobs with the same storage for capture sets. * Those scheduler jobs will all change state to completion at the same moment as they all target the same storage. * This is why it is important to manage the repeat count of the scheduler job, as stated earlier. */ // Retrieve cached count of completed captures for the output folder of this seqJob QString const signature = seqJob->getSignature(); QString const signature_path = QFileInfo(signature).path(); captures_completed = capturedFramesCount[signature]; qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 sees %2 captures in output folder '%3'.").arg(seqName).arg(captures_completed).arg(signature_path); // Enumerate sequence jobs to check how many captures are completed overall in the same storage as the current one foreach (SequenceJob *prevSeqJob, seqJobs) { // Enumerate seqJobs up to the current one if (seqJob == prevSeqJob) break; // If the previous sequence signature matches the current, reduce completion count to take duplicates into account if (!signature.compare(prevSeqJob->getLocalDir() + prevSeqJob->getDirectoryPostfix())) { // Note that looping jobs will have zero repeats required. int const previous_captures_required = prevSeqJob->getCount() * schedJob->getRepeatsRequired(); qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 has a previous duplicate sequence job requiring %2 captures.").arg(seqName).arg(previous_captures_required); captures_completed -= previous_captures_required; } // Now completed count can be needlessly negative for this job, so clamp to zero if (captures_completed < 0) captures_completed = 0; // And break if no captures remain, this job has to execute if (captures_completed == 0) break; } // Finally we're only interested in the number of captures required for this sequence item if (0 < captures_required && captures_required < captures_completed) captures_completed = captures_required; qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 has completed %2/%3 of its required captures in output folder '%4'.").arg(seqName).arg(captures_completed).arg(captures_required).arg(signature_path); // Update the completion count for this signature in the frame map if we still have captures to take. // That frame map will be transferred to the Capture module, for which the sequence is a single batch of the scheduler job. // For instance, consider a scheduler job repeated 3 times and using a 3xLum sequence, so we want 9xLum in the end. // - If no captures are already processed, the frame map contains Lum=0 // - If 1xLum are already processed, the frame map contains Lum=0 when the batch executes, so that 3xLum may be taken. // - If 3xLum are already processed, the frame map contains Lum=0 when the batch executes, as we still need more than what the sequence provides. // - If 7xLum are already processed, the frame map contains Lum=1 when the batch executes, because we now only need 2xLum to finish the job. // Therefore we need to specify a number of existing captures only for the last batch of the scheduler job. // In the last batch, we only need the remainder of frames to get to the required total. if (captures_completed < captures_required) { if (captures_required - captures_completed < seqJob->getCount()) capture_map[signature] = captures_completed % seqJob->getCount(); else capture_map[signature] = 0; } else capture_map[signature] = captures_required; // From now on, 'captures_completed' is the number of frames completed for the *current* sequence job } // Else rely on the captures done during this session else captures_completed = schedJob->getCompletedCount() / capturesPerRepeat * seqJob->getCount(); // Check if we still need any light frames. Because light frames changes the flow of the observatory startup // Without light frames, there is no need to do focusing, alignment, guiding...etc // We check if the frame type is LIGHT and if either the number of captures_completed frames is less than required // OR if the completion condition is set to LOOP so it is never complete due to looping. // Note that looping jobs will have zero repeats required. // FIXME: As it is implemented now, FINISH_LOOP may loop over a capture-complete, therefore inoperant, scheduler job. bool const areJobCapturesComplete = !(captures_completed < captures_required || 0 == captures_required); if (seqJob->getFrameType() == FRAME_LIGHT) { if(areJobCapturesComplete) { qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 completed its sequence of %2 light frames.").arg(seqName).arg(captures_required); } } else { qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 captures calibration frames.").arg(seqName); } totalSequenceCount += captures_required; totalCompletedCount += captures_completed; /* If captures are not complete, we have imaging time left */ if (!areJobCapturesComplete) { /* if looping, consider we always have one capture left */ unsigned int const captures_to_go = 0 < captures_required ? captures_required - captures_completed : 1; totalImagingTime += fabs((seqJob->getExposure() + seqJob->getDelay()) * captures_to_go); /* If we have light frames to process, add focus/dithering delay */ if (seqJob->getFrameType() == FRAME_LIGHT) { // If inSequenceFocus is true if (hasAutoFocus) { // Wild guess that each in sequence auto focus takes an average of 30 seconds. It can take any where from 2 seconds to 2+ minutes. // FIXME: estimating one focus per capture is probably not realistic. qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a focus procedure.").arg(seqName); totalImagingTime += captures_to_go * 30; } // If we're dithering after each exposure, that's another 10-20 seconds if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE && Options::ditherEnabled()) { qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a dither procedure.").arg(seqName); totalImagingTime += (captures_to_go * 15) / Options::ditherFrames(); } } } } schedJob->setCapturedFramesMap(capture_map); schedJob->setSequenceCount(totalSequenceCount); // only in case we remember the job progress, we change the completion count if (rememberJobProgress) schedJob->setCompletedCount(totalCompletedCount); qDeleteAll(seqJobs); // FIXME: Move those ifs away to the caller in order to avoid estimating in those situations! // We can't estimate times that do not finish when sequence is done if (schedJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP) { // We can't know estimated time if it is looping indefinitely schedJob->setEstimatedTime(-2); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is configured to loop until Scheduler is stopped manually, has undefined imaging time.") .arg(schedJob->getName()); } // If we know startup and finish times, we can estimate time right away else if (schedJob->getStartupCondition() == SchedulerJob::START_AT && schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT) { // FIXME: SchedulerJob is probably doing this already qint64 const diff = schedJob->getStartupTime().secsTo(schedJob->getCompletionTime()); schedJob->setEstimatedTime(diff); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a startup time and fixed completion time, will run for %2.") .arg(schedJob->getName()) .arg(dms(diff * 15.0 / 3600.0f).toHMSString()); } // If we know finish time only, we can roughly estimate the time considering the job starts now else if (schedJob->getStartupCondition() != SchedulerJob::START_AT && schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT) { qint64 const diff = KStarsData::Instance()->lt().secsTo(schedJob->getCompletionTime()); schedJob->setEstimatedTime(diff); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has no startup time but fixed completion time, will run for %2 if started now.") .arg(schedJob->getName()) .arg(dms(diff * 15.0 / 3600.0f).toHMSString()); } // Rely on the estimated imaging time to determine whether this job is complete or not - this makes the estimated time null else if (totalImagingTime <= 0) { schedJob->setEstimatedTime(0); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' will not run, complete with %2/%3 captures.") .arg(schedJob->getName()).arg(totalCompletedCount).arg(totalSequenceCount); } // Else consolidate with step durations else { if (schedJob->getLightFramesRequired()) { /* FIXME: estimation should base on actual measure of each step, eventually with preliminary data as what it used now */ // Are we doing tracking? It takes about 30 seconds if (schedJob->getStepPipeline() & SchedulerJob::USE_TRACK) totalImagingTime += 30; // Are we doing initial focusing? That can take about 2 minutes if (schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS) totalImagingTime += 120; // Are we doing astrometry? That can take about 60 seconds if (schedJob->getStepPipeline() & SchedulerJob::USE_ALIGN) { totalImagingTime += 60; } // Are we doing guiding? if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE) { // Looping, finding guide star, settling takes 15 sec totalImagingTime += 15; // Add guiding settle time from dither setting (used by phd2::guide()) totalImagingTime += Options::ditherSettle(); // Add guiding settle time from ekos sccheduler setting totalImagingTime += Options::guidingSettle(); // If calibration always cleared // then calibration process can take about 2 mins if(Options::resetGuideCalibration()) totalImagingTime += 120; } } dms const estimatedTime(totalImagingTime * 15.0 / 3600.0); schedJob->setEstimatedTime(totalImagingTime); qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' estimated to take %2 to complete.").arg(schedJob->getName(), estimatedTime.toHMSString()); } return true; } void Scheduler::parkMount() { QVariant parkingStatus = mountInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { case ISD::PARK_PARKED: if (shutdownState == SHUTDOWN_PARK_MOUNT) shutdownState = SHUTDOWN_PARK_DOME; parkWaitState = PARKWAIT_PARKED; appendLogText(i18n("Mount already parked.")); break; case ISD::PARK_UNPARKING: //case Mount::UNPARKING_BUSY: /* FIXME: Handle the situation where we request parking but an unparking procedure is running. */ // case Mount::PARKING_IDLE: // case Mount::UNPARKING_OK: case ISD::PARK_ERROR: case ISD::PARK_UNKNOWN: case ISD::PARK_UNPARKED: { QDBusReply const mountReply = mountInterface->call(QDBus::AutoDetect, "park"); if (mountReply.error().type() != QDBusError::NoError) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount park request received DBUS error: %1").arg(QDBusError::errorString(mountReply.error().type())); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } else currentOperationTime.start(); } // Fall through case ISD::PARK_PARKING: //case Mount::PARKING_BUSY: if (shutdownState == SHUTDOWN_PARK_MOUNT) shutdownState = SHUTDOWN_PARKING_MOUNT; parkWaitState = PARKWAIT_PARKING; appendLogText(i18n("Parking mount in progress...")); break; // All cases covered above so no need for default //default: // qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while parking mount.").arg(mountReply.value()); } } void Scheduler::unParkMount() { if (mountInterface.isNull()) return; QVariant parkingStatus = mountInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { //case Mount::UNPARKING_OK: case ISD::PARK_UNPARKED: if (startupState == STARTUP_UNPARK_MOUNT) startupState = STARTUP_UNPARK_CAP; parkWaitState = PARKWAIT_UNPARKED; appendLogText(i18n("Mount already unparked.")); break; //case Mount::PARKING_BUSY: case ISD::PARK_PARKING: /* FIXME: Handle the situation where we request unparking but a parking procedure is running. */ // case Mount::PARKING_IDLE: // case Mount::PARKING_OK: // case Mount::PARKING_ERROR: case ISD::PARK_ERROR: case ISD::PARK_UNKNOWN: case ISD::PARK_PARKED: { QDBusReply const mountReply = mountInterface->call(QDBus::AutoDetect, "unpark"); if (mountReply.error().type() != QDBusError::NoError) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount unpark request received DBUS error: %1").arg(QDBusError::errorString(mountReply.error().type())); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } else currentOperationTime.start(); } // Fall through //case Mount::UNPARKING_BUSY: case ISD::PARK_UNPARKING: if (startupState == STARTUP_UNPARK_MOUNT) startupState = STARTUP_UNPARKING_MOUNT; parkWaitState = PARKWAIT_UNPARKING; qCInfo(KSTARS_EKOS_SCHEDULER) << "Unparking mount in progress..."; break; // All cases covered above //default: // qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while unparking mount.").arg(mountReply.value()); } } void Scheduler::checkMountParkingStatus() { if (mountInterface.isNull()) return; static int parkingFailureCount = 0; QVariant parkingStatus = mountInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { //case Mount::PARKING_OK: case ISD::PARK_PARKED: // If we are starting up, we will unpark the mount in checkParkWaitState soon // If we are shutting down and mount is parked, proceed to next step if (shutdownState == SHUTDOWN_PARKING_MOUNT) shutdownState = SHUTDOWN_PARK_DOME; // Update parking engine state if (parkWaitState == PARKWAIT_PARKING) parkWaitState = PARKWAIT_PARKED; appendLogText(i18n("Mount parked.")); parkingFailureCount = 0; break; //case Mount::UNPARKING_OK: case ISD::PARK_UNPARKED: // If we are starting up and mount is unparked, proceed to next step // If we are shutting down, we will park the mount in checkParkWaitState soon if (startupState == STARTUP_UNPARKING_MOUNT) startupState = STARTUP_UNPARK_CAP; // Update parking engine state if (parkWaitState == PARKWAIT_UNPARKING) parkWaitState = PARKWAIT_UNPARKED; appendLogText(i18n("Mount unparked.")); parkingFailureCount = 0; break; // FIXME: Create an option for the parking/unparking timeout. //case Mount::UNPARKING_BUSY: case ISD::PARK_UNPARKING: if (currentOperationTime.elapsed() > (60 * 1000)) { if (++parkingFailureCount < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Warning: mount unpark operation timed out on attempt %1/%2. Restarting operation...", parkingFailureCount, MAX_FAILURE_ATTEMPTS)); unParkMount(); } else { appendLogText(i18n("Warning: mount unpark operation timed out on last attempt.")); parkWaitState = PARKWAIT_ERROR; } } else qCInfo(KSTARS_EKOS_SCHEDULER) << "Unparking mount in progress..."; break; //case Mount::PARKING_BUSY: case ISD::PARK_PARKING: if (currentOperationTime.elapsed() > (60 * 1000)) { if (++parkingFailureCount < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Warning: mount park operation timed out on attempt %1/%2. Restarting operation...", parkingFailureCount, MAX_FAILURE_ATTEMPTS)); parkMount(); } else { appendLogText(i18n("Warning: mount park operation timed out on last attempt.")); parkWaitState = PARKWAIT_ERROR; } } else qCInfo(KSTARS_EKOS_SCHEDULER) << "Parking mount in progress..."; break; //case Mount::PARKING_ERROR: case ISD::PARK_ERROR: if (startupState == STARTUP_UNPARKING_MOUNT) { appendLogText(i18n("Mount unparking error.")); startupState = STARTUP_ERROR; } else if (shutdownState == SHUTDOWN_PARKING_MOUNT) { appendLogText(i18n("Mount parking error.")); shutdownState = SHUTDOWN_ERROR; } else if (parkWaitState == PARKWAIT_PARKING) { appendLogText(i18n("Mount parking error.")); parkWaitState = PARKWAIT_ERROR; } else if (parkWaitState == PARKWAIT_UNPARKING) { appendLogText(i18n("Mount unparking error.")); parkWaitState = PARKWAIT_ERROR; } parkingFailureCount = 0; break; //case Mount::PARKING_IDLE: // FIXME Does this work as intended? check! case ISD::PARK_UNKNOWN: // Last parking action did not result in an action, so proceed to next step if (shutdownState == SHUTDOWN_PARKING_MOUNT) shutdownState = SHUTDOWN_PARK_DOME; // Last unparking action did not result in an action, so proceed to next step if (startupState == STARTUP_UNPARKING_MOUNT) startupState = STARTUP_UNPARK_CAP; // Update parking engine state if (parkWaitState == PARKWAIT_PARKING) parkWaitState = PARKWAIT_PARKED; else if (parkWaitState == PARKWAIT_UNPARKING) parkWaitState = PARKWAIT_UNPARKED; parkingFailureCount = 0; break; // All cases covered above //default: // qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while checking progress.").arg(mountReply.value()); } } bool Scheduler::isMountParked() { if (mountInterface.isNull()) return false; // First check if the mount is able to park - if it isn't, getParkingStatus will reply PARKING_ERROR and status won't be clear //QDBusReply const parkCapableReply = mountInterface->call(QDBus::AutoDetect, "canPark"); QVariant canPark = mountInterface->property("canPark"); if (canPark.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount canPark request received DBUS error: %1").arg(mountInterface->lastError().type()); manageConnectionLoss(); return false; } else if (canPark.toBool() == true) { // If it is able to park, obtain its current status //QDBusReply const mountReply = mountInterface->call(QDBus::AutoDetect, "getParkingStatus"); QVariant parkingStatus = mountInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parking status property is invalid %1.").arg(mountInterface->lastError().type()); manageConnectionLoss(); return false; } // Deduce state of mount - see getParkingStatus in mount.cpp switch (static_cast(parkingStatus.toInt())) { // case Mount::PARKING_OK: // INDI switch ok, and parked // case Mount::PARKING_IDLE: // INDI switch idle, and parked case ISD::PARK_PARKED: return true; // case Mount::UNPARKING_OK: // INDI switch idle or ok, and unparked // case Mount::PARKING_ERROR: // INDI switch error // case Mount::PARKING_BUSY: // INDI switch busy // case Mount::UNPARKING_BUSY: // INDI switch busy default: return false; } } // If the mount is not able to park, consider it not parked return false; } void Scheduler::parkDome() { if (domeInterface.isNull()) return; //QDBusReply const domeReply = domeInterface->call(QDBus::AutoDetect, "getParkingStatus"); //Dome::ParkingStatus status = static_cast(domeReply.value()); QVariant parkingStatus = domeInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); if (status != ISD::PARK_PARKED) { shutdownState = SHUTDOWN_PARKING_DOME; domeInterface->call(QDBus::AutoDetect, "park"); appendLogText(i18n("Parking dome...")); currentOperationTime.start(); } else { appendLogText(i18n("Dome already parked.")); shutdownState = SHUTDOWN_SCRIPT; } } void Scheduler::unParkDome() { if (domeInterface.isNull()) return; QVariant parkingStatus = domeInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } if (static_cast(parkingStatus.toInt()) != ISD::PARK_UNPARKED) { startupState = STARTUP_UNPARKING_DOME; domeInterface->call(QDBus::AutoDetect, "unpark"); appendLogText(i18n("Unparking dome...")); currentOperationTime.start(); } else { appendLogText(i18n("Dome already unparked.")); startupState = STARTUP_UNPARK_MOUNT; } } void Scheduler::checkDomeParkingStatus() { if (domeInterface.isNull()) return; /* FIXME: move this elsewhere */ static int parkingFailureCount = 0; QVariant parkingStatus = domeInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { case ISD::PARK_PARKED: if (shutdownState == SHUTDOWN_PARKING_DOME) { appendLogText(i18n("Dome parked.")); shutdownState = SHUTDOWN_SCRIPT; } parkingFailureCount = 0; break; case ISD::PARK_UNPARKED: if (startupState == STARTUP_UNPARKING_DOME) { startupState = STARTUP_UNPARK_MOUNT; appendLogText(i18n("Dome unparked.")); } parkingFailureCount = 0; break; case ISD::PARK_PARKING: case ISD::PARK_UNPARKING: // TODO make the timeouts configurable by the user if (currentOperationTime.elapsed() > (120 * 1000)) { if (parkingFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Operation timeout. Restarting operation...")); if (status == ISD::PARK_PARKING) parkDome(); else unParkDome(); break; } } break; case ISD::PARK_ERROR: if (shutdownState == SHUTDOWN_PARKING_DOME) { appendLogText(i18n("Dome parking error.")); shutdownState = SHUTDOWN_ERROR; } else if (startupState == STARTUP_UNPARKING_DOME) { appendLogText(i18n("Dome unparking error.")); startupState = STARTUP_ERROR; } parkingFailureCount = 0; break; default: break; } } bool Scheduler::isDomeParked() { if (domeInterface.isNull()) return false; QVariant parkingStatus = domeInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); return status == ISD::PARK_PARKED; } void Scheduler::parkCap() { if (capInterface.isNull()) return; QVariant parkingStatus = capInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); if (status != ISD::PARK_PARKED) { shutdownState = SHUTDOWN_PARKING_CAP; capInterface->call(QDBus::AutoDetect, "park"); appendLogText(i18n("Parking Cap...")); currentOperationTime.start(); } else { appendLogText(i18n("Cap already parked.")); shutdownState = SHUTDOWN_PARK_MOUNT; } } void Scheduler::unParkCap() { if (capInterface.isNull()) return; QVariant parkingStatus = capInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); if (status != ISD::PARK_UNPARKED) { startupState = STARTUP_UNPARKING_CAP; capInterface->call(QDBus::AutoDetect, "unpark"); appendLogText(i18n("Unparking cap...")); currentOperationTime.start(); } else { appendLogText(i18n("Cap already unparked.")); startupState = STARTUP_COMPLETE; } } void Scheduler::checkCapParkingStatus() { if (capInterface.isNull()) return; /* FIXME: move this elsewhere */ static int parkingFailureCount = 0; QVariant parkingStatus = capInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { case ISD::PARK_PARKED: if (shutdownState == SHUTDOWN_PARKING_CAP) { appendLogText(i18n("Cap parked.")); shutdownState = SHUTDOWN_PARK_MOUNT; } parkingFailureCount = 0; break; case ISD::PARK_UNPARKED: if (startupState == STARTUP_UNPARKING_CAP) { startupState = STARTUP_COMPLETE; appendLogText(i18n("Cap unparked.")); } parkingFailureCount = 0; break; case ISD::PARK_PARKING: case ISD::PARK_UNPARKING: // TODO make the timeouts configurable by the user if (currentOperationTime.elapsed() > (60 * 1000)) { if (parkingFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Operation timeout. Restarting operation...")); if (status == ISD::PARK_PARKING) parkCap(); else unParkCap(); break; } } break; case ISD::PARK_ERROR: if (shutdownState == SHUTDOWN_PARKING_CAP) { appendLogText(i18n("Cap parking error.")); shutdownState = SHUTDOWN_ERROR; } else if (startupState == STARTUP_UNPARKING_CAP) { appendLogText(i18n("Cap unparking error.")); startupState = STARTUP_ERROR; } parkingFailureCount = 0; break; default: break; } } void Scheduler::startJobEvaluation() { // Reset current job setCurrentJob(nullptr); // Reset ALL scheduler jobs to IDLE and force-reset their completed count - no effect when progress is kept for (SchedulerJob * job : jobs) { job->reset(); job->setCompletedCount(0); } // Unconditionally update the capture storage updateCompletedJobsCount(true); // And evaluate all pending jobs per the conditions set in each jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::sortJobsPerAltitude() { // We require a first job to sort, so bail out if list is empty if (jobs.isEmpty()) return; // Don't reset current job // setCurrentJob(nullptr); // Don't reset scheduler jobs startup times before sorting - we need the first job startup time // Sort by startup time, using the first job time as reference for altitude calculations using namespace std::placeholders; QList sortedJobs = jobs; std::stable_sort(sortedJobs.begin() + 1, sortedJobs.end(), std::bind(SchedulerJob::decreasingAltitudeOrder, _1, _2, jobs.first()->getStartupTime())); // If order changed, reset and re-evaluate if (reorderJobs(sortedJobs)) { for (SchedulerJob * job : jobs) job->reset(); jobEvaluationOnly = true; evaluateJobs(); } } void Scheduler::updatePreDawn() { double earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); int dayOffset = 0; QTime dawn = QTime(0, 0, 0).addSecs(Dawn * 24 * 3600); if (KStarsData::Instance()->lt().time() >= dawn) dayOffset = 1; preDawnDateTime.setDate(KStarsData::Instance()->lt().date().addDays(dayOffset)); preDawnDateTime.setTime(QTime::fromMSecsSinceStartOfDay(earlyDawn * 24 * 3600 * 1000)); } bool Scheduler::isWeatherOK(SchedulerJob *job) { if (weatherStatus == ISD::Weather::WEATHER_OK || weatherCheck->isChecked() == false) return true; else if (weatherStatus == ISD::Weather::WEATHER_IDLE) { if (indiState == INDI_READY) appendLogText(i18n("Weather information is pending...")); return true; } // Temporary BUSY is ALSO accepted for now // TODO Figure out how to exactly handle this if (weatherStatus == ISD::Weather::WEATHER_WARNING) return true; if (weatherStatus == ISD::Weather::WEATHER_ALERT) { job->setState(SchedulerJob::JOB_ABORTED); appendLogText(i18n("Job '%1' suffers from bad weather, marking aborted.", job->getName())); } /*else if (weatherStatus == IPS_BUSY) { appendLogText(i18n("%1 observation job delayed due to bad weather.", job->getName())); schedulerTimer.stop(); connect(this, &Scheduler::weatherChanged, this, &Scheduler::resumeCheckStatus); }*/ return false; } void Scheduler::resumeCheckStatus() { disconnect(this, &Scheduler::weatherChanged, this, &Scheduler::resumeCheckStatus); schedulerTimer.start(); } Scheduler::ErrorHandlingStrategy Scheduler::getErrorHandlingStrategy() { // The UI holds the state if (errorHandlingRestartAfterAllButton->isChecked()) return ERROR_RESTART_AFTER_TERMINATION; else if (errorHandlingRestartImmediatelyButton->isChecked()) return ERROR_RESTART_IMMEDIATELY; else return ERROR_DONT_RESTART; } void Scheduler::setErrorHandlingStrategy(Scheduler::ErrorHandlingStrategy strategy) { errorHandlingDelaySB->setEnabled(strategy != ERROR_DONT_RESTART); switch (strategy) { case ERROR_RESTART_AFTER_TERMINATION: errorHandlingRestartAfterAllButton->setChecked(true); break; case ERROR_RESTART_IMMEDIATELY: errorHandlingRestartImmediatelyButton->setChecked(true); break; default: errorHandlingDontRestartButton->setChecked(true); break; } } void Scheduler::startMosaicTool() { bool raOk = false, decOk = false; dms ra(raBox->createDms(false, &raOk)); //false means expressed in hours dms dec(decBox->createDms(true, &decOk)); if (raOk == false) { appendLogText(i18n("Warning: RA value %1 is invalid.", raBox->text())); return; } if (decOk == false) { appendLogText(i18n("Warning: DEC value %1 is invalid.", decBox->text())); return; } Mosaic mosaicTool; SkyPoint center; center.setRA0(ra); center.setDec0(dec); mosaicTool.setCenter(center); mosaicTool.calculateFOV(); mosaicTool.adjustSize(); if (mosaicTool.exec() == QDialog::Accepted) { // #1 Edit Sequence File ---> Not needed as of 2016-09-12 since Scheduler can send Target Name to Capture module it will append it to root dir // #1.1 Set prefix to Target-Part# // #1.2 Set directory to output/Target-Part# // #2 Save all sequence files in Jobs dir // #3 Set as current Sequence file // #4 Change Target name to Target-Part# // #5 Update J2000 coords // #6 Repeat and save Ekos Scheduler List in the output directory qCDebug(KSTARS_EKOS_SCHEDULER) << "Job accepted with # " << mosaicTool.getJobs().size() << " jobs and fits dir " << mosaicTool.getJobsDir(); QString outputDir = mosaicTool.getJobsDir(); QString targetName = nameEdit->text().simplified().remove(' '); int batchCount = 1; XMLEle *root = getSequenceJobRoot(); if (root == nullptr) return; // Delete any prior jobs before saving if (!jobs.empty()) { if (KMessageBox::questionYesNo(nullptr, i18n("Do you want to keep the existing jobs in the mosaic schedule?")) == KMessageBox::No) { qDeleteAll(jobs); jobs.clear(); while (queueTable->rowCount() > 0) queueTable->removeRow(0); } } // We do not want FITS image for mosaic job since each job has its own calculated center QString fitsFileBackup = fitsEdit->text(); fitsEdit->clear(); foreach (OneTile *oneJob, mosaicTool.getJobs()) { QString prefix = QString("%1-Part%2").arg(targetName).arg(batchCount++); prefix.replace(' ', '-'); nameEdit->setText(prefix); if (createJobSequence(root, prefix, outputDir) == false) return; QString filename = QString("%1/%2.esq").arg(outputDir, prefix); sequenceEdit->setText(filename); sequenceURL = QUrl::fromLocalFile(filename); raBox->showInHours(oneJob->skyCenter.ra0()); decBox->showInDegrees(oneJob->skyCenter.dec0()); saveJob(); } delXMLEle(root); QUrl mosaicURL = QUrl::fromLocalFile((QString("%1/%2_mosaic.esl").arg(outputDir, targetName))); if (saveScheduler(mosaicURL)) { appendLogText(i18n("Mosaic file %1 saved successfully.", mosaicURL.toLocalFile())); } else { appendLogText(i18n("Error saving mosaic file %1. Please reload job.", mosaicURL.toLocalFile())); } fitsEdit->setText(fitsFileBackup); } } XMLEle *Scheduler::getSequenceJobRoot() { QFile sFile; sFile.setFileName(sequenceURL.toLocalFile()); if (!sFile.open(QIODevice::ReadOnly)) { KSNotification::sorry(i18n("Unable to open file %1", sFile.fileName()), i18n("Could Not Open File")); return nullptr; } LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; char c; while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) break; } delLilXML(xmlParser); sFile.close(); return root; } bool Scheduler::createJobSequence(XMLEle *root, const QString &prefix, const QString &outputDir) { QFile sFile; sFile.setFileName(sequenceURL.toLocalFile()); if (!sFile.open(QIODevice::ReadOnly)) { KSNotification::sorry(i18n("Unable to open sequence file %1", sFile.fileName()), i18n("Could Not Open File")); return false; } XMLEle *ep = nullptr; XMLEle *subEP = nullptr; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Job")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp(tagXMLEle(subEP), "Prefix")) { XMLEle *rawPrefix = findXMLEle(subEP, "RawPrefix"); if (rawPrefix) { editXMLEle(rawPrefix, prefix.toLatin1().constData()); } } else if (!strcmp(tagXMLEle(subEP), "FITSDirectory")) { editXMLEle(subEP, QString("%1/%2").arg(outputDir, prefix).toLatin1().constData()); } } } } QDir().mkpath(outputDir); QString filename = QString("%1/%2.esq").arg(outputDir, prefix); FILE *outputFile = fopen(filename.toLatin1().constData(), "w"); if (outputFile == nullptr) { QString message = i18n("Unable to write to file %1", filename); KSNotification::sorry(message, i18n("Could Not Open File")); return false; } fprintf(outputFile, ""); prXMLEle(outputFile, root, 0); fclose(outputFile); return true; } void Scheduler::resetAllJobs() { if (state == SCHEDULER_RUNNING) return; // Reset capture count of all jobs before re-evaluating foreach (SchedulerJob *job, jobs) job->setCompletedCount(0); // Evaluate all jobs, this refreshes storage and resets job states startJobEvaluation(); } void Scheduler::checkTwilightWarning(bool enabled) { if (enabled) return; if (KMessageBox::warningContinueCancel( nullptr, i18n("Turning off astronomial twilight check may cause the observatory " "to run during daylight. This can cause irreversible damage to your equipment!"), i18n("Astronomial Twilight Warning"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "astronomical_twilight_warning") == KMessageBox::Cancel) { twilightCheck->setChecked(true); } } void Scheduler::checkStartupProcedure() { if (checkStartupState() == false) QTimer::singleShot(1000, this, SLOT(checkStartupProcedure())); else { if (startupState == STARTUP_COMPLETE) appendLogText(i18n("Manual startup procedure completed successfully.")); else if (startupState == STARTUP_ERROR) appendLogText(i18n("Manual startup procedure terminated due to errors.")); startupB->setIcon( QIcon::fromTheme("media-playback-start")); } } void Scheduler::runStartupProcedure() { if (startupState == STARTUP_IDLE || startupState == STARTUP_ERROR || startupState == STARTUP_COMPLETE) { /* FIXME: Probably issue a warning only, in case the user wants to run the startup script alone */ if (indiState == INDI_IDLE) { KSNotification::sorry(i18n("Cannot run startup procedure while INDI devices are not online.")); return; } if (KMessageBox::questionYesNo( nullptr, i18n("Are you sure you want to execute the startup procedure manually?")) == KMessageBox::Yes) { appendLogText(i18n("Warning: executing startup procedure manually...")); startupB->setIcon( QIcon::fromTheme("media-playback-stop")); startupState = STARTUP_IDLE; checkStartupState(); QTimer::singleShot(1000, this, SLOT(checkStartupProcedure())); } } else { switch (startupState) { case STARTUP_IDLE: break; case STARTUP_SCRIPT: scriptProcess.terminate(); break; case STARTUP_UNPARK_DOME: break; case STARTUP_UNPARKING_DOME: domeInterface->call(QDBus::AutoDetect, "abort"); break; case STARTUP_UNPARK_MOUNT: break; case STARTUP_UNPARKING_MOUNT: mountInterface->call(QDBus::AutoDetect, "abort"); break; case STARTUP_UNPARK_CAP: break; case STARTUP_UNPARKING_CAP: break; case STARTUP_COMPLETE: break; case STARTUP_ERROR: break; } startupState = STARTUP_IDLE; appendLogText(i18n("Startup procedure terminated.")); } } void Scheduler::checkShutdownProcedure() { // If shutdown procedure is not finished yet, let's check again in 1 second. if (checkShutdownState() == false) QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure())); else { if (shutdownState == SHUTDOWN_COMPLETE) { appendLogText(i18n("Manual shutdown procedure completed successfully.")); // Stop Ekos if (Options::stopEkosAfterShutdown()) stopEkos(); } else if (shutdownState == SHUTDOWN_ERROR) appendLogText(i18n("Manual shutdown procedure terminated due to errors.")); shutdownState = SHUTDOWN_IDLE; shutdownB->setIcon( QIcon::fromTheme("media-playback-start")); } } void Scheduler::runShutdownProcedure() { if (shutdownState == SHUTDOWN_IDLE || shutdownState == SHUTDOWN_ERROR || shutdownState == SHUTDOWN_COMPLETE) { if (KMessageBox::questionYesNo( nullptr, i18n("Are you sure you want to execute the shutdown procedure manually?")) == KMessageBox::Yes) { appendLogText(i18n("Warning: executing shutdown procedure manually...")); shutdownB->setIcon( QIcon::fromTheme("media-playback-stop")); shutdownState = SHUTDOWN_IDLE; checkShutdownState(); QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure())); } } else { switch (shutdownState) { case SHUTDOWN_IDLE: break; case SHUTDOWN_SCRIPT: break; case SHUTDOWN_SCRIPT_RUNNING: scriptProcess.terminate(); break; case SHUTDOWN_PARK_DOME: break; case SHUTDOWN_PARKING_DOME: domeInterface->call(QDBus::AutoDetect, "abort"); break; case SHUTDOWN_PARK_MOUNT: break; case SHUTDOWN_PARKING_MOUNT: mountInterface->call(QDBus::AutoDetect, "abort"); break; case SHUTDOWN_PARK_CAP: break; case SHUTDOWN_PARKING_CAP: break; case SHUTDOWN_COMPLETE: break; case SHUTDOWN_ERROR: break; } shutdownState = SHUTDOWN_IDLE; appendLogText(i18n("Shutdown procedure terminated.")); } } void Scheduler::loadProfiles() { QString currentProfile = schedulerProfileCombo->currentText(); QDBusReply profiles = ekosInterface->call(QDBus::AutoDetect, "getProfiles"); if (profiles.error().type() == QDBusError::NoError) { schedulerProfileCombo->blockSignals(true); schedulerProfileCombo->clear(); schedulerProfileCombo->addItem(i18n("Default")); schedulerProfileCombo->addItems(profiles); schedulerProfileCombo->setCurrentText(currentProfile); schedulerProfileCombo->blockSignals(false); } } bool Scheduler::loadSequenceQueue(const QString &fileURL, SchedulerJob *schedJob, QList &jobs, bool &hasAutoFocus) { QFile sFile; sFile.setFileName(fileURL); if (!sFile.open(QIODevice::ReadOnly)) { QString message = i18n("Unable to open sequence queue file '%1'", fileURL); KSNotification::sorry(message, i18n("Could Not Open File")); return false; } LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; XMLEle *ep = nullptr; char c; while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Autofocus")) hasAutoFocus = (!strcmp(findXMLAttValu(ep, "enabled"), "true")); else if (!strcmp(tagXMLEle(ep), "Job")) jobs.append(processJobInfo(ep, schedJob)); } delXMLEle(root); } else if (errmsg[0]) { appendLogText(QString(errmsg)); delLilXML(xmlParser); qDeleteAll(jobs); return false; } } return true; } SequenceJob *Scheduler::processJobInfo(XMLEle *root, SchedulerJob *schedJob) { XMLEle *ep = nullptr; XMLEle *subEP = nullptr; const QMap frameTypes = { { "Light", FRAME_LIGHT }, { "Dark", FRAME_DARK }, { "Bias", FRAME_BIAS }, { "Flat", FRAME_FLAT } }; SequenceJob *job = new SequenceJob(); QString rawPrefix, frameType, filterType; double exposure = 0; bool filterEnabled = false, expEnabled = false, tsEnabled = false; /* Reset light frame presence flag before enumerating */ // JM 2018-09-14: If last sequence job is not LIGHT // then scheduler job light frame is set to whatever last sequence job is // so if it was non-LIGHT, this value is set to false which is wrong. //if (nullptr != schedJob) // schedJob->setLightFramesRequired(false); for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Exposure")) { exposure = atof(pcdataXMLEle(ep)); job->setExposure(exposure); } else if (!strcmp(tagXMLEle(ep), "Filter")) { filterType = QString(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "Type")) { frameType = QString(pcdataXMLEle(ep)); /* Record frame type and mark presence of light frames for this sequence */ CCDFrameType const frameEnum = frameTypes[frameType]; job->setFrameType(frameEnum); if (FRAME_LIGHT == frameEnum && nullptr != schedJob) schedJob->setLightFramesRequired(true); } else if (!strcmp(tagXMLEle(ep), "Prefix")) { subEP = findXMLEle(ep, "RawPrefix"); if (subEP) rawPrefix = QString(pcdataXMLEle(subEP)); subEP = findXMLEle(ep, "FilterEnabled"); if (subEP) filterEnabled = !strcmp("1", pcdataXMLEle(subEP)); subEP = findXMLEle(ep, "ExpEnabled"); if (subEP) expEnabled = (!strcmp("1", pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "TimeStampEnabled"); if (subEP) tsEnabled = (!strcmp("1", pcdataXMLEle(subEP))); job->setPrefixSettings(rawPrefix, filterEnabled, expEnabled, tsEnabled); } else if (!strcmp(tagXMLEle(ep), "Count")) { job->setCount(atoi(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "Delay")) { job->setDelay(atoi(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "FITSDirectory")) { job->setLocalDir(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "RemoteDirectory")) { job->setRemoteDir(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "UploadMode")) { job->setUploadMode(static_cast(atoi(pcdataXMLEle(ep)))); } } QString targetName = schedJob->getName().remove(' '); // Because scheduler sets the target name in capture module // it would be the same as the raw prefix if (targetName.isEmpty() == false && rawPrefix.isEmpty()) rawPrefix = targetName; // Make full prefix QString imagePrefix = rawPrefix; if (imagePrefix.isEmpty() == false) imagePrefix += '_'; imagePrefix += frameType; if (filterEnabled && filterType.isEmpty() == false && (job->getFrameType() == FRAME_LIGHT || job->getFrameType() == FRAME_FLAT)) { imagePrefix += '_'; imagePrefix += filterType; } if (expEnabled) { imagePrefix += '_'; imagePrefix += QString::number(exposure, 'd', 0) + QString("_secs"); } job->setFullPrefix(imagePrefix); // Directory postfix QString directoryPostfix; /* FIXME: Refactor directoryPostfix assignment, whose code is duplicated in capture.cpp */ if (targetName.isEmpty()) directoryPostfix = QLatin1String("/") + frameType; else directoryPostfix = QLatin1String("/") + targetName + QLatin1String("/") + frameType; if ((job->getFrameType() == FRAME_LIGHT || job->getFrameType() == FRAME_FLAT) && filterType.isEmpty() == false) directoryPostfix += QLatin1String("/") + filterType; job->setDirectoryPostfix(directoryPostfix); return job; } int Scheduler::getCompletedFiles(const QString &path, const QString &seqPrefix) { int seqFileCount = 0; QFileInfo const path_info(path); QString const sig_dir(path_info.dir().path()); QString const sig_file(path_info.baseName()); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Searching in path '%1', files '%2*' for prefix '%3'...").arg(sig_dir, sig_file, seqPrefix); QDirIterator it(sig_dir, QDir::Files); /* FIXME: this counts all files with prefix in the storage location, not just captures. DSS analysis files are counted in, for instance. */ while (it.hasNext()) { QString const fileName = QFileInfo(it.next()).baseName(); if (fileName.startsWith(seqPrefix)) { qCDebug(KSTARS_EKOS_SCHEDULER) << QString("> Found '%1'").arg(fileName); seqFileCount++; } } return seqFileCount; } void Scheduler::setINDICommunicationStatus(Ekos::CommunicationStatus status) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler INDI status is" << status; m_INDICommunicationStatus = status; } void Scheduler::setEkosCommunicationStatus(Ekos::CommunicationStatus status) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler Ekos status is" << status; m_EkosCommunicationStatus = status; } void Scheduler::simClockScaleChanged(float newScale) { if (sleepTimer.isActive()) { QTime const remainingTimeMs = QTime::fromMSecsSinceStartOfDay(std::lround((double) sleepTimer.remainingTime() * KStarsData::Instance()->clock()->scale() / newScale)); appendLogText(i18n("Sleeping for %1 on simulation clock update until next observation job is ready...", remainingTimeMs.toString("hh:mm:ss"))); sleepTimer.stop(); sleepTimer.start(remainingTimeMs.msecsSinceStartOfDay()); } } void Scheduler::registerNewModule(const QString &name) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Registering new Module (" << name << ")"; if (name == "Focus") { delete focusInterface; focusInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Focus", "org.kde.kstars.Ekos.Focus", QDBusConnection::sessionBus(), this); connect(focusInterface, SIGNAL(newStatus(Ekos::FocusState)), this, SLOT(setFocusStatus(Ekos::FocusState)), Qt::UniqueConnection); } else if (name == "Capture") { delete captureInterface; captureInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Capture", "org.kde.kstars.Ekos.Capture", QDBusConnection::sessionBus(), this); connect(captureInterface, SIGNAL(ready()), this, SLOT(syncProperties())); connect(captureInterface, SIGNAL(newStatus(Ekos::CaptureState)), this, SLOT(setCaptureStatus(Ekos::CaptureState)), Qt::UniqueConnection); } else if (name == "Mount") { delete mountInterface; mountInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Mount", "org.kde.kstars.Ekos.Mount", QDBusConnection::sessionBus(), this); connect(mountInterface, SIGNAL(ready()), this, SLOT(syncProperties())); connect(mountInterface, SIGNAL(newStatus(ISD::Telescope::Status)), this, SLOT(setMountStatus(ISD::Telescope::Status)), Qt::UniqueConnection); } else if (name == "Align") { delete alignInterface; alignInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Align", "org.kde.kstars.Ekos.Align", QDBusConnection::sessionBus(), this); connect(alignInterface, SIGNAL(newStatus(Ekos::AlignState)), this, SLOT(setAlignStatus(Ekos::AlignState)), Qt::UniqueConnection); } else if (name == "Guide") { delete guideInterface; guideInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Guide", "org.kde.kstars.Ekos.Guide", QDBusConnection::sessionBus(), this); connect(guideInterface, SIGNAL(newStatus(Ekos::GuideState)), this, SLOT(setGuideStatus(Ekos::GuideState)), Qt::UniqueConnection); } else if (name == "Dome") { delete domeInterface; domeInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Dome", "org.kde.kstars.Ekos.Dome", QDBusConnection::sessionBus(), this); connect(domeInterface, SIGNAL(ready()), this, SLOT(syncProperties())); } else if (name == "Weather") { delete weatherInterface; weatherInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Weather", "org.kde.kstars.Ekos.Weather", QDBusConnection::sessionBus(), this); connect(weatherInterface, SIGNAL(ready()), this, SLOT(syncProperties())); connect(weatherInterface, SIGNAL(newStatus(ISD::Weather::Status)), this, SLOT(setWeatherStatus(ISD::Weather::Status))); } else if (name == "DustCap") { delete capInterface; capInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/DustCap", "org.kde.kstars.Ekos.DustCap", QDBusConnection::sessionBus(), this); connect(capInterface, SIGNAL(ready()), this, SLOT(syncProperties()), Qt::UniqueConnection); } } void Scheduler::syncProperties() { QDBusInterface *iface = qobject_cast(sender()); if (iface == mountInterface) { QVariant canMountPark = mountInterface->property("canPark"); unparkMountCheck->setEnabled(canMountPark.toBool()); parkMountCheck->setEnabled(canMountPark.toBool()); m_MountReady = true; } else if (iface == capInterface) { QVariant canCapPark = capInterface->property("canPark"); if (canCapPark.isValid()) { capCheck->setEnabled(canCapPark.toBool()); uncapCheck->setEnabled(canCapPark.toBool()); m_CapReady = true; } else { capCheck->setEnabled(false); uncapCheck->setEnabled(false); } } else if (iface == weatherInterface) { QVariant updatePeriod = weatherInterface->property("updatePeriod"); if (updatePeriod.isValid()) { weatherCheck->setEnabled(true); QVariant status = weatherInterface->property("status"); setWeatherStatus(static_cast(status.toInt())); // if (updatePeriod.toInt() > 0) // { // weatherTimer.setInterval(updatePeriod.toInt() * 1000); // connect(&weatherTimer, &QTimer::timeout, this, &Scheduler::checkWeather, Qt::UniqueConnection); // weatherTimer.start(); // // Check weather initially // checkWeather(); // } } else weatherCheck->setEnabled(true); } else if (iface == domeInterface) { QVariant canDomePark = domeInterface->property("canPark"); unparkDomeCheck->setEnabled(canDomePark.toBool()); parkDomeCheck->setEnabled(canDomePark.toBool()); m_DomeReady = true; } else if (iface == captureInterface) { QVariant hasCoolerControl = captureInterface->property("coolerControl"); warmCCDCheck->setEnabled(hasCoolerControl.toBool()); m_CaptureReady = true; } } void Scheduler::setAlignStatus(Ekos::AlignState status) { if (state == SCHEDULER_PAUSED || currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Align State" << Ekos::getAlignStatusString(status); /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) { QDateTime const now = KStarsData::Instance()->lt(); if (now < currentJob->getStartupTime()) return; } if (currentJob->getStage() == SchedulerJob::STAGE_ALIGNING) { // Is solver complete? if (status == Ekos::ALIGN_COMPLETE) { appendLogText(i18n("Job '%1' alignment is complete.", currentJob->getName())); alignFailureCount = 0; currentJob->setStage(SchedulerJob::STAGE_ALIGN_COMPLETE); getNextAction(); } else if (status == Ekos::ALIGN_FAILED || status == Ekos::ALIGN_ABORTED) { appendLogText(i18n("Warning: job '%1' alignment failed.", currentJob->getName())); if (alignFailureCount++ < MAX_FAILURE_ATTEMPTS) { if (Options::resetMountModelOnAlignFail() && MAX_FAILURE_ATTEMPTS - 1 < alignFailureCount) { appendLogText(i18n("Warning: job '%1' forcing mount model reset after failing alignment #%2.", currentJob->getName(), alignFailureCount)); mountInterface->call(QDBus::AutoDetect, "resetModel"); } appendLogText(i18n("Restarting %1 alignment procedure...", currentJob->getName())); startAstrometry(); } else { appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } } } void Scheduler::setGuideStatus(Ekos::GuideState status) { if (state == SCHEDULER_PAUSED || currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Guide State" << Ekos::getGuideStatusString(status); /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) { QDateTime const now = KStarsData::Instance()->lt(); if (now < currentJob->getStartupTime()) return; } if (currentJob->getStage() == SchedulerJob::STAGE_GUIDING) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Calibration & Guide stage..."; // If calibration stage complete? if (status == Ekos::GUIDE_GUIDING) { appendLogText(i18n("Job '%1' guiding is in progress.", currentJob->getName())); guideFailureCount = 0; // if guiding recovered while we are waiting, abort the restart restartGuidingTimer.stop(); currentJob->setStage(SchedulerJob::STAGE_GUIDING_COMPLETE); getNextAction(); } else if (status == Ekos::GUIDE_CALIBRATION_ERROR || status == Ekos::GUIDE_ABORTED) { if (status == Ekos::GUIDE_ABORTED) appendLogText(i18n("Warning: job '%1' guiding failed.", currentJob->getName())); else appendLogText(i18n("Warning: job '%1' calibration failed.", currentJob->getName())); // if the timer for restarting the guiding is already running, we do nothing and // wait for the action triggered by the timer. This way we avoid that a small guiding problem // abort the scheduler job if (restartGuidingTimer.isActive()) return; if (guideFailureCount++ < MAX_FAILURE_ATTEMPTS) { if (status == Ekos::GUIDE_CALIBRATION_ERROR && Options::realignAfterCalibrationFailure()) { appendLogText(i18n("Restarting %1 alignment procedure...", currentJob->getName())); // JM: We have to go back to startSlew() since if we just call startAstrometry() // It would captureAndSolve at the _current_ coords which could be way off center if the calibration // process took a wild ride search for a suitable guide star and then failed. So startSlew() would ensure // we're back on our target and then it proceed to alignment (focus is skipped since it is done if it was checked anyway). startSlew(); } else { appendLogText(i18n("Job '%1' is guiding, guiding procedure will be restarted in %2 seconds.", currentJob->getName(), (RESTART_GUIDING_DELAY_MS * guideFailureCount) / 1000)); restartGuidingTimer.start(RESTART_GUIDING_DELAY_MS * guideFailureCount); } } else { appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } } } GuideState Scheduler::getGuidingStatus() { QVariant guideStatus = guideInterface->property("status"); Ekos::GuideState gStatus = static_cast(guideStatus.toInt()); return gStatus; } void Scheduler::setCaptureStatus(Ekos::CaptureState status) { if (currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Capture State" << Ekos::getCaptureStatusString(status); /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) { QDateTime const now = KStarsData::Instance()->lt(); if (now < currentJob->getStartupTime()) return; } if (currentJob->getStage() == SchedulerJob::STAGE_CAPTURING) { if (status == Ekos::CAPTURE_ABORTED) { appendLogText(i18n("Warning: job '%1' failed to capture target.", currentJob->getName())); if (captureFailureCount++ < MAX_FAILURE_ATTEMPTS) { // If capture failed due to guiding error, let's try to restart that if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) { // Check if it is guiding related. Ekos::GuideState gStatus = getGuidingStatus(); if (gStatus == Ekos::GUIDE_ABORTED || gStatus == Ekos::GUIDE_CALIBRATION_ERROR || gStatus == GUIDE_DITHERING_ERROR) { appendLogText(i18n("Job '%1' is capturing, is restarting its guiding procedure (attempt #%2 of %3).", currentJob->getName(), captureFailureCount, MAX_FAILURE_ATTEMPTS)); startGuiding(true); return; } } /* FIXME: it's not clear whether it is actually possible to continue capturing when capture fails this way */ appendLogText(i18n("Warning: job '%1' failed its capture procedure, restarting capture.", currentJob->getName())); startCapture(true); } else { /* FIXME: it's not clear whether this situation can be recovered at all */ appendLogText(i18n("Warning: job '%1' failed its capture procedure, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else if (status == Ekos::CAPTURE_COMPLETE) { KNotification::event(QLatin1String("EkosScheduledImagingFinished"), i18n("Ekos job (%1) - Capture finished", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_COMPLETE); findNextJob(); } else if (status == Ekos::CAPTURE_IMAGE_RECEIVED) { // We received a new image, but we don't know precisely where so update the storage map and re-estimate job times. // FIXME: rework this once capture storage is reworked if (Options::rememberJobProgress()) { updateCompletedJobsCount(true); for (SchedulerJob * job : jobs) estimateJobTime(job); } // Else if we don't remember the progress on jobs, increase the completed count for the current job only - no cross-checks else currentJob->setCompletedCount(currentJob->getCompletedCount() + 1); captureFailureCount = 0; } } } void Scheduler::setFocusStatus(Ekos::FocusState status) { if (state == SCHEDULER_PAUSED || currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus State" << Ekos::getFocusStatusString(status); /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) { QDateTime const now = KStarsData::Instance()->lt(); if (now < currentJob->getStartupTime()) return; } if (currentJob->getStage() == SchedulerJob::STAGE_FOCUSING) { // Is focus complete? if (status == Ekos::FOCUS_COMPLETE) { appendLogText(i18n("Job '%1' focusing is complete.", currentJob->getName())); autofocusCompleted = true; currentJob->setStage(SchedulerJob::STAGE_FOCUS_COMPLETE); getNextAction(); } else if (status == Ekos::FOCUS_FAILED || status == Ekos::FOCUS_ABORTED) { appendLogText(i18n("Warning: job '%1' focusing failed.", currentJob->getName())); if (focusFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Job '%1' is restarting its focusing procedure.", currentJob->getName())); // Reset frame to original size. focusInterface->call(QDBus::AutoDetect, "resetFrame"); // Restart focusing startFocusing(); } else { appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } } } void Scheduler::setMountStatus(ISD::Telescope::Status status) { if (state == SCHEDULER_PAUSED || currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount State changed to" << status; /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) if (static_cast(KStarsData::Instance()->lt()) < currentJob->getStartupTime()) return; switch (currentJob->getStage()) { case SchedulerJob::STAGE_SLEWING: { qCDebug(KSTARS_EKOS_SCHEDULER) << "Slewing stage..."; if (status == ISD::Telescope::MOUNT_TRACKING) { appendLogText(i18n("Job '%1' slew is complete.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_SLEW_COMPLETE); /* getNextAction is deferred to checkJobStage for dome support */ } else if (status == ISD::Telescope::MOUNT_ERROR) { appendLogText(i18n("Warning: job '%1' slew failed, marking terminated due to errors.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ERROR); findNextJob(); } else if (status == ISD::Telescope::MOUNT_IDLE) { appendLogText(i18n("Warning: job '%1' found not slewing, restarting.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_IDLE); getNextAction(); } } break; case SchedulerJob::STAGE_RESLEWING: { qCDebug(KSTARS_EKOS_SCHEDULER) << "Re-slewing stage..."; if (status == ISD::Telescope::MOUNT_TRACKING) { appendLogText(i18n("Job '%1' repositioning is complete.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_RESLEWING_COMPLETE); /* getNextAction is deferred to checkJobStage for dome support */ } else if (status == ISD::Telescope::MOUNT_ERROR) { appendLogText(i18n("Warning: job '%1' repositioning failed, marking terminated due to errors.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ERROR); findNextJob(); } else if (status == ISD::Telescope::MOUNT_IDLE) { appendLogText(i18n("Warning: job '%1' found not repositioning, restarting.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_IDLE); getNextAction(); } } break; default: break; } } void Scheduler::setWeatherStatus(ISD::Weather::Status status) { ISD::Weather::Status newStatus = status; QString statusString; switch (newStatus) { case ISD::Weather::WEATHER_OK: statusString = i18n("Weather conditions are OK."); break; case ISD::Weather::WEATHER_WARNING: statusString = i18n("Warning: weather conditions are in the WARNING zone."); break; case ISD::Weather::WEATHER_ALERT: statusString = i18n("Caution: weather conditions are in the DANGER zone!"); break; default: break; } if (newStatus != weatherStatus) { weatherStatus = newStatus; qCDebug(KSTARS_EKOS_SCHEDULER) << statusString; if (weatherStatus == ISD::Weather::WEATHER_OK) weatherLabel->setPixmap( QIcon::fromTheme("security-high") .pixmap(QSize(32, 32))); else if (weatherStatus == ISD::Weather::WEATHER_WARNING) { weatherLabel->setPixmap( QIcon::fromTheme("security-medium") .pixmap(QSize(32, 32))); KNotification::event(QLatin1String("WeatherWarning"), i18n("Weather conditions in warning zone")); } else if (weatherStatus == ISD::Weather::WEATHER_ALERT) { weatherLabel->setPixmap( QIcon::fromTheme("security-low") .pixmap(QSize(32, 32))); KNotification::event(QLatin1String("WeatherAlert"), i18n("Weather conditions are critical. Observatory shutdown is imminent")); } else weatherLabel->setPixmap(QIcon::fromTheme("chronometer") .pixmap(QSize(32, 32))); weatherLabel->show(); weatherLabel->setToolTip(statusString); appendLogText(statusString); emit weatherChanged(weatherStatus); } // Shutdown scheduler if it was started and not already in shutdown if (weatherStatus == ISD::Weather::WEATHER_ALERT && state != Ekos::SCHEDULER_IDLE && state != Ekos::SCHEDULER_SHUTDOWN) { appendLogText(i18n("Starting shutdown procedure due to severe weather.")); if (currentJob) { currentJob->setState(SchedulerJob::JOB_ABORTED); stopCurrentJobAction(); jobTimer.stop(); } checkShutdownState(); //connect(KStars::Instance()->data()->clock(), SIGNAL(timeAdvanced()), this, SLOT(checkStatus()), &Scheduler::Qt::UniqueConnection); } } bool Scheduler::shouldSchedulerSleep(SchedulerJob *currentJob) { Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "There must be a valid current job for Scheduler to test sleep requirement"); if (currentJob->getLightFramesRequired() == false) return false; QDateTime const now = KStarsData::Instance()->lt(); int const nextObservationTime = now.secsTo(currentJob->getStartupTime()); // If start up procedure is complete and the user selected pre-emptive shutdown, let us check if the next observation time exceed // the pre-emptive shutdown time in hours (default 2). If it exceeds that, we perform complete shutdown until next job is ready if (startupState == STARTUP_COMPLETE && Options::preemptiveShutdown() && nextObservationTime > (Options::preemptiveShutdownTime() * 3600)) { appendLogText(i18n( "Job '%1' scheduled for execution at %2. " "Observatory scheduled for shutdown until next job is ready.", currentJob->getName(), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); preemptiveShutdown = true; weatherCheck->setEnabled(false); weatherLabel->hide(); checkShutdownState(); //schedulerTimer.stop(); // Wake up when job is due. // FIXME: Implement waking up periodically before job is due for weather check. // int const nextWakeup = nextObservationTime < 60 ? nextObservationTime : 60; sleepTimer.setInterval(std::lround(((nextObservationTime + 1) * 1000) / KStarsData::Instance()->clock()->scale())); sleepTimer.start(); return true; } // Otherwise, sleep until job is ready /* FIXME: if not parking, stop tracking maybe? this would prevent crashes or scheduler stops from leaving the mount to track and bump the pier */ // If start up procedure is already complete, and we didn't issue any parking commands before and parking is checked and enabled // Then we park the mount until next job is ready. But only if the job uses TRACK as its first step, otherwise we cannot get into position again. // This is also only performed if next job is due more than the default lead time (5 minutes). // If job is due sooner than that is not worth parking and we simply go into sleep or wait modes. else if (nextObservationTime > Options::leadTime() * 60 && startupState == STARTUP_COMPLETE && parkWaitState == PARKWAIT_IDLE && (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK) && parkMountCheck->isEnabled() && parkMountCheck->isChecked()) { appendLogText(i18n( "Job '%1' scheduled for execution at %2. " "Parking the mount until the job is ready.", currentJob->getName(), currentJob->getStartupTime().toString())); parkWaitState = PARKWAIT_PARK; return false; } else if (nextObservationTime > Options::leadTime() * 60) { appendLogText(i18n("Sleeping until observation job %1 is ready at %2...", currentJob->getName(), now.addSecs(nextObservationTime + 1).toString())); sleepLabel->setToolTip(i18n("Scheduler is in sleep mode")); sleepLabel->show(); // Warn the user if the next job is really far away - 60/5 = 12 times the lead time if (nextObservationTime > Options::leadTime() * 60 * 12 && !Options::preemptiveShutdown()) { dms delay(static_cast(nextObservationTime * 15.0 / 3600.0)); appendLogText(i18n( "Warning: Job '%1' is %2 away from now, you may want to enable Preemptive Shutdown.", currentJob->getName(), delay.toHMSString())); } /* FIXME: stop tracking now */ schedulerTimer.stop(); // Wake up when job is due. // FIXME: Implement waking up periodically before job is due for weather check. // int const nextWakeup = nextObservationTime < 60 ? nextObservationTime : 60; sleepTimer.setInterval(std::lround(((nextObservationTime + 1) * 1000) / KStarsData::Instance()->clock()->scale())); sleepTimer.start(); return true; } return false; } } diff --git a/kstars/hips/hipsmanager.cpp b/kstars/hips/hipsmanager.cpp index 90ea13081..201233aa1 100644 --- a/kstars/hips/hipsmanager.cpp +++ b/kstars/hips/hipsmanager.cpp @@ -1,457 +1,462 @@ /* Copyright (C) 2015-2017, Pavel Mraz Copyright (C) 2017, Jasem Mutlaq This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "hipsmanager.h" #include "auxiliary/kspaths.h" #include "auxiliary/ksuserdb.h" #include "kstars.h" #include "kstarsdata.h" #include "kstars_debug.h" #include "Options.h" #include "skymap.h" #include #include #include #include #include static QNetworkDiskCache *g_discCache = nullptr; static UrlFileDownload *g_download = nullptr; static int qHash(const pixCacheKey_t &key, uint seed) { return qHash(QString("%1_%2_%3").arg(key.level).arg(key.pix).arg(key.uid), seed); } inline bool operator==(const pixCacheKey_t &k1, const pixCacheKey_t &k2) { return (k1.uid == k2.uid) && (k1.level == k2.level) && (k1.pix == k2.pix); } HIPSManager * HIPSManager::_HIPSManager = nullptr; HIPSManager *HIPSManager::Instance() { if (_HIPSManager == nullptr) _HIPSManager = new HIPSManager(); return _HIPSManager; } HIPSManager::HIPSManager() : QObject(KStars::Instance()) { if (g_discCache == nullptr) { g_discCache = new QNetworkDiskCache(); } if (g_download == nullptr) { g_download = new UrlFileDownload(this, g_discCache); connect(g_download, SIGNAL(sigDownloadDone(QNetworkReply::NetworkError,QByteArray&,pixCacheKey_t&)), this, SLOT(slotDone(QNetworkReply::NetworkError,QByteArray&,pixCacheKey_t&))); } //g_discCache->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/cache/hips"); g_discCache->setCacheDirectory(KSPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "hips"); //g_discCache->setMaximumCacheSize(setting("hips_net_cache").toLongLong()); //m_cache.setMaxCost(setting("hips_mem_cache").toInt()); g_discCache->setMaximumCacheSize(Options::hIPSNetCache()*1024*1024); m_cache.setMaxCost(Options::hIPSMemoryCache()*1024*1024); } void HIPSManager::showSettings() { KConfigDialog *dialog = KConfigDialog::exists("hipssettings"); if (dialog == nullptr) { dialog = new KConfigDialog(KStars::Instance(), "hipssettings", Options::self()); connect(dialog->button(QDialogButtonBox::Apply), SIGNAL(clicked()), SLOT(slotApply())); connect(dialog->button(QDialogButtonBox::Ok), SIGNAL(clicked()), SLOT(slotApply())); displaySettings.reset(new OpsHIPSDisplay()); KPageWidgetItem *page = dialog->addPage(displaySettings.get(), i18n("Display")); page->setIcon(QIcon::fromTheme("computer")); cacheSettings.reset(new OpsHIPSCache()); page = dialog->addPage(cacheSettings.get(), i18n("Cache")); page->setIcon(QIcon::fromTheme("preferences-web-browser-cache")); sourceSettings.reset(new OpsHIPS()); page = dialog->addPage(sourceSettings.get(), i18n("Sources")); page->setIcon(QIcon::fromTheme("view-preview")); dialog->resize(800,600); } dialog->show(); } void HIPSManager::slotApply() { readSources(); KStars::Instance()->repopulateHIPS(); SkyMap::Instance()->forceUpdate(); } qint64 HIPSManager::getDiscCacheSize() const { return g_discCache->cacheSize(); } void HIPSManager::readSources() { KStarsData::Instance()->userdb()->GetAllHIPSSources(m_hipsSources); QString currentSourceTitle = Options::hIPSSource(); setCurrentSource(currentSourceTitle); } /*void HIPSManager::setParam(const hipsParams_t ¶m) { m_param = param; m_uid = qHash(param.url); }*/ QImage *HIPSManager::getPix(bool allsky, int level, int pix, bool &freeImage) { if (m_currentSource.isEmpty()) { qCWarning(KSTARS) << "HIPS source not available!"; return nullptr; } int origPix = pix; freeImage = false; if (allsky) { level = 0; pix = 0; } pixCacheKey_t key; key.level = level; key.pix = pix; key.uid = m_uid; pixCacheItem_t *item = getCacheItem(key); if (m_downloadMap.contains(key)) { // downloading // try render (level - 1) while downloading key.level = level - 1; key.pix = pix / 4; pixCacheItem_t *item = getCacheItem(key); if (item != nullptr) { QImage *cacheImage = item->image; int size = m_currentTileWidth >> 1; int offset = cacheImage->width() / size; QImage *image = cacheImage; int index[4] = {0, 2, 1, 3}; int ox = index[pix % 4] % offset; int oy = index[pix % 4] / offset; QImage *newImage = new QImage(image->copy(ox * size, oy * size, size, size)); freeImage = true; return newImage; } return nullptr; } if (item != nullptr) { QImage *cacheImage = item->image; Q_ASSERT(!item->image->isNull()); if (allsky && cacheImage != nullptr) { // all sky int size = 64; int offset = cacheImage->width() / size; QImage *image = cacheImage; int ox = origPix % offset; int oy = origPix / offset; QImage *newImage = new QImage(image->copy(ox * size, oy * size, size, size)); freeImage = true; return newImage; } return cacheImage; } QString path; if (!allsky) { int dir = (pix / 10000) * 10000; path = "/Norder" + QString::number(level) + "/Dir" + QString::number(dir) + "/Npix" + QString::number(pix) + '.' + m_currentFormat; } else { path = "/Norder3/Allsky." + m_currentFormat; } QUrl downloadURL(m_currentURL); downloadURL.setPath(downloadURL.path() + path); g_download->begin(downloadURL, key); m_downloadMap.insert(key); return nullptr; } #if 0 bool HIPSManager::parseProperties(hipsParams_t *param, const QString &filename, const QString &url) { QFile f(filename); if (!f.open(QFile::ReadOnly | QFile::Text)) { qDebug() << "nf" << f.fileName(); return false; } QMap map; QTextStream in(&f); while (!in.atEnd()) { QString line = in.readLine(); int index = line.indexOf("="); if (index > 0) { map[line.left(index).simplified()] = line.mid(index + 1).simplified(); } } param->url = url; qDebug() << url; int count = 0; QString tmp; if (map.contains("obs_collection")) { param->name = map["obs_collection"]; count++; } if (map.contains("hips_tile_width")) { param->tileWidth = map["hips_tile_width"].toInt(); count++; } if (map.contains("hips_order")) { param->max_level = map["hips_order"].toInt(); count++; } if (map.contains("hips_tile_format")) { tmp = map["hips_tile_format"]; QStringList list = tmp.split(" "); if (list.contains("jpeg")) { param->imageExtension = "jpg"; count++; } else if (list.contains("png")) { param->imageExtension = "png"; count++; } } if (map.contains("hips_frame") || map.contains("ohips_frame")) { if (map.contains("hips_frame")) tmp = map["hips_frame"]; else tmp = map["ohips_frame"]; if (tmp == "equatorial") { param->frame = HIPS_FRAME_EQT; count++; } else if (tmp == "galactic") { param->frame = HIPS_FRAME_GAL; count++; } } return count == 5; // all items have been loaded } #endif void HIPSManager::cancelAll() { g_download->abortAll(); } void HIPSManager::clearDiscCache() { g_discCache->clear(); } void HIPSManager::slotDone(QNetworkReply::NetworkError error, QByteArray &data, pixCacheKey_t &key) { if (error == QNetworkReply::NoError) { m_downloadMap.remove(key); auto *item = new pixCacheItem_t; item->image = new QImage(); if (item->image->loadFromData(data)) { addToMemoryCache(key, item); //SkyMap::Instance()->forceUpdate(); } else { delete item; qCWarning(KSTARS) << "no image" << data; } } else { if (error == QNetworkReply::OperationCanceledError) { m_downloadMap.remove(key); } else { auto *timer = new RemoveTimer(); timer->setKey(key); connect(timer, SIGNAL(remove(pixCacheKey_t&)), this, SLOT(removeTimer(pixCacheKey_t&))); } } } void HIPSManager::removeTimer(pixCacheKey_t &key) { m_downloadMap.remove(key); sender()->deleteLater(); emit sigRepaint(); } PixCache *HIPSManager::getCache() { return &m_cache; } void HIPSManager::addToMemoryCache(pixCacheKey_t &key, pixCacheItem_t *item) { Q_ASSERT(item); Q_ASSERT(item->image); + #if QT_VERSION >= QT_VERSION_CHECK(5,10,0) + int cost = item->image->sizeInBytes(); + #else int cost = item->image->byteCount(); + #endif + m_cache.add(key, item, cost); } pixCacheItem_t *HIPSManager::getCacheItem(pixCacheKey_t &key) { return m_cache.get(key); } bool HIPSManager::setCurrentSource(const QString &title) { if (title == "None") { Options::setShowHIPS(false); Options::setHIPSSource(title); m_currentSource.clear(); m_currentFormat.clear(); m_currentFrame = HIPS_OTHER_FRAME; m_currentURL.clear(); m_currentOrder=0; m_currentTileWidth=0; m_uid=0; return true; } for (QMap &source : m_hipsSources) { if (source.value("obs_title") == title) { m_currentSource = source; m_currentFormat = source.value("hips_tile_format"); if (m_currentFormat.contains("jpeg")) m_currentFormat = "jpg"; else if (m_currentFormat.contains("png")) m_currentFormat = "png"; else { qCWarning(KSTARS) << "FITS HIPS images are not currently supported."; return false; } m_currentOrder = source.value("hips_order").toInt(); m_currentTileWidth = source.value("hips_tile_width").toInt(); if (source.value("hips_frame") == "equatorial") m_currentFrame = HIPS_EQUATORIAL_FRAME; else if (source.value("hips_frame") == "galactic") m_currentFrame = HIPS_GALACTIC_FRAME; else m_currentFrame = HIPS_OTHER_FRAME; m_currentURL = QUrl(source.value("hips_service_url")); m_uid = qHash(m_currentURL); Options::setHIPSSource(title); Options::setShowHIPS(true); return true; } } return false; } void RemoveTimer::setKey(const pixCacheKey_t &key) { m_key = key; } diff --git a/kstars/hips/hipsrenderer.cpp b/kstars/hips/hipsrenderer.cpp index 45a83fe1e..291238bd7 100644 --- a/kstars/hips/hipsrenderer.cpp +++ b/kstars/hips/hipsrenderer.cpp @@ -1,253 +1,258 @@ /* Copyright (C) 2015-2017, Pavel Mraz Copyright (C) 2017, Jasem Mutlaq This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "hipsrenderer.h" #include "colorscheme.h" #include "kstars_debug.h" #include "Options.h" #include "skymap.h" #include "skyqpainter.h" #include "projections/projector.h" HIPSRenderer::HIPSRenderer() { m_scanRender.reset(new ScanRender()); m_HEALpix.reset(new HEALPix()); } bool HIPSRenderer::render(uint16_t w, uint16_t h, QImage *hipsImage, const Projector *m_proj) { gridColor = KStarsData::Instance()->colorScheme()->colorNamed("HIPSGridColor").name(); m_projector = m_proj; int level = 1; // Min FOV in Degrees double minfov = 58.5; double fov = m_proj->fov() * w / (double) h; // Find suitable level for current FOV while( level < HIPSManager::Instance()->getCurrentOrder() && fov < minfov) { minfov /= 2; level++; } m_renderedMap.clear(); m_rendered = 0; m_blocks = 0; m_size = 0; SkyPoint center = SkyMap::Instance()->getCenterPoint(); center.deprecess(KStarsData::Instance()->updateNum()); double ra = center.ra0().radians(); double de = center.dec0().radians(); if (std::isnan(ra) || std::isnan(de)) { qCWarning(KSTARS) << "NAN Center, HiPS draw canceled."; return false; } bool allSky; if (level < 3) { allSky = true; level = 3; } else { allSky = false; } int centerPix = m_HEALpix->getPix(level, ra, de); SkyPoint cornerSkyCoords[4]; QPointF tileLine[2]; m_HEALpix->getCornerPoints(level, centerPix, cornerSkyCoords); //qCDebug(KSTARS) << "#" << i+1 << "RA0" << cornerSkyCoords[i].ra0().toHMSString(); //qCDebug(KSTARS) << "#" << i+1 << "DE0" << cornerSkyCoords[i].dec0().toHMSString(); //qCDebug(KSTARS) << "#" << i+1 << "X" << tileLine[i].x(); //qCDebug(KSTARS) << "#" << i+1 << "Y" << tileLine[i].y(); for (int i=0; i < 2; i++) tileLine[i] = m_projector->toScreen(&cornerSkyCoords[i]); int size = std::sqrt(std::pow(tileLine[0].x()-tileLine[1].x(), 2) + std::pow(tileLine[0].y()-tileLine[1].y(), 2)); if (size < 0) size = HIPSManager::Instance()->getCurrentTileWidth(); bool old = m_scanRender->isBilinearInterpolationEnabled(); m_scanRender->setBilinearInterpolationEnabled(Options::hIPSBiLinearInterpolation() && (size >= HIPSManager::Instance()->getCurrentTileWidth() || allSky)); renderRec(allSky, level, centerPix, hipsImage); m_scanRender->setBilinearInterpolationEnabled(old); return true; } void HIPSRenderer::renderRec(bool allsky, int level, int pix, QImage *pDest) { if (m_renderedMap.contains(pix)) { return; } if (renderPix(allsky, level, pix, pDest)) { m_renderedMap.insert(pix); int dirs[8]; int nside = 1 << level; m_HEALpix->neighbours(nside, pix, dirs); renderRec(allsky, level, dirs[0], pDest); renderRec(allsky, level, dirs[2], pDest); renderRec(allsky, level, dirs[4], pDest); renderRec(allsky, level, dirs[6], pDest); } } bool HIPSRenderer::renderPix(bool allsky, int level, int pix, QImage *pDest) { SkyPoint cornerSkyCoords[4]; QPointF cornerScreenCoords[4]; bool freeImage = false; m_HEALpix->getCornerPoints(level, pix, cornerSkyCoords); bool isVisible = false; for (int i=0; i < 4; i++) { cornerScreenCoords[i] = m_projector->toScreen(&cornerSkyCoords[i]); isVisible |= m_projector->checkVisibility(&cornerSkyCoords[i]); } //if (SKPLANECheckFrustumToPolygon(trfGetFrustum(), pts, 4)) // Is the right way to do this? if (isVisible) { m_blocks++; /*for (int i = 0; i < 4; i++) { trfProjectPointNoCheck(&pts[i]); } */ QImage *image = HIPSManager::Instance()->getPix(allsky, level, pix, freeImage); if (image) { m_rendered++; + + #if QT_VERSION >= QT_VERSION_CHECK(5,10,0) + m_size += image->sizeInBytes(); + #else m_size += image->byteCount(); + #endif // UV Mapping to apply image unto the destination image // 4x4 = 16 points are mapped from the source image unto the destination image. // Starting from each grandchild pixel, each pix polygon is mapped accordingly. // For example, pixel 357 will have 4 child pixels, each of them will have 4 childs pixels and so // on. Each healpix pixel appears roughly as a diamond on the sky map. // The corners points for HealPIX moves from NORTH -> EAST -> SOUTH -> WEST // Hence first point is 0.25, 0.25 in UV coordinate system. // Depending on the selected algorithm, the mapping will either utilize nearest neighbour // or bilinear interpolation. QPointF uv[16][4] = {{QPointF(.25, .25), QPointF(0.25, 0), QPointF(0, .0),QPointF(0, .25)}, {QPointF(.25, .5), QPointF(0.25, 0.25), QPointF(0, .25),QPointF(0, .5)}, {QPointF(.5, .25), QPointF(0.5, 0), QPointF(.25, .0),QPointF(.25, .25)}, {QPointF(.5, .5), QPointF(0.5, 0.25), QPointF(.25, .25),QPointF(.25, .5)}, {QPointF(.25, .75), QPointF(0.25, 0.5), QPointF(0, 0.5), QPointF(0, .75)}, {QPointF(.25, 1), QPointF(0.25, 0.75), QPointF(0, .75),QPointF(0, 1)}, {QPointF(.5, .75), QPointF(0.5, 0.5), QPointF(.25, .5),QPointF(.25, .75)}, {QPointF(.5, 1), QPointF(0.5, 0.75), QPointF(.25, .75),QPointF(.25, 1)}, {QPointF(.75, .25), QPointF(0.75, 0), QPointF(0.5, .0),QPointF(0.5, .25)}, {QPointF(.75, .5), QPointF(0.75, 0.25), QPointF(0.5, .25),QPointF(0.5, .5)}, {QPointF(1, .25), QPointF(1, 0), QPointF(.75, .0),QPointF(.75, .25)}, {QPointF(1, .5), QPointF(1, 0.25), QPointF(.75, .25),QPointF(.75, .5)}, {QPointF(.75, .75), QPointF(0.75, 0.5), QPointF(0.5, .5),QPointF(0.5, .75)}, {QPointF(.75, 1), QPointF(0.75, 0.75), QPointF(0.5, .75),QPointF(0.5, 1)}, {QPointF(1, .75), QPointF(1, 0.5), QPointF(.75, .5),QPointF(.75, .75)}, {QPointF(1, 1), QPointF(1, 0.75), QPointF(.75, .75),QPointF(.75, 1)}, }; int childPixelID[4]; // Find all the 4 children of the current pixel m_HEALpix->getPixChilds(pix, childPixelID); int j = 0; for (int id : childPixelID) { int grandChildPixelID[4]; // Find the children of this child (i.e. grand child) // Then we have 4x4 pixels under the primary pixel // The image is interpolated and rendered over these pixels // coordinate to minimize any distortions due to the projection // system. m_HEALpix->getPixChilds(id, grandChildPixelID); QPointF fineScreenCoords[4]; for (int id2 : grandChildPixelID) { SkyPoint fineSkyPoints[4]; m_HEALpix->getCornerPoints(level + 2, id2, fineSkyPoints); for (int i = 0; i < 4; i++) fineScreenCoords[i] = m_projector->toScreen(&fineSkyPoints[i]); m_scanRender->renderPolygon(3, fineScreenCoords, pDest, image, uv[j]); j++; } } if (freeImage) { delete image; } } if (Options::hIPSShowGrid()) { QPainter p(pDest); p.setRenderHint(QPainter::Antialiasing); p.setPen(gridColor); p.drawLine(cornerScreenCoords[0].x(), cornerScreenCoords[0].y(), cornerScreenCoords[1].x(), cornerScreenCoords[1].y()); p.drawLine(cornerScreenCoords[1].x(), cornerScreenCoords[1].y(), cornerScreenCoords[2].x(), cornerScreenCoords[2].y()); p.drawLine(cornerScreenCoords[2].x(), cornerScreenCoords[2].y(), cornerScreenCoords[3].x(), cornerScreenCoords[3].y()); p.drawLine(cornerScreenCoords[3].x(), cornerScreenCoords[3].y(), cornerScreenCoords[0].x(), cornerScreenCoords[0].y()); p.drawText((cornerScreenCoords[0].x() + cornerScreenCoords[1].x() + cornerScreenCoords[2].x() + cornerScreenCoords[3].x()) / 4, (cornerScreenCoords[0].y() + cornerScreenCoords[1].y() + cornerScreenCoords[2].y() + cornerScreenCoords[3].y()) / 4, QString::number(pix) + " / " + QString::number(level)); } return true; } return false; } diff --git a/kstars/skyqpainter.cpp b/kstars/skyqpainter.cpp index c79da2664..d765694d2 100644 --- a/kstars/skyqpainter.cpp +++ b/kstars/skyqpainter.cpp @@ -1,1066 +1,1072 @@ /* (C) 2010 Henry de Valence This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more decomas. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "skyqpainter.h" #include #include "kstarsdata.h" #include "Options.h" #include "skymap.h" #include "projections/projector.h" #include "skycomponents/flagcomponent.h" #include "skycomponents/linelist.h" #include "skycomponents/linelistlabel.h" #include "skycomponents/satellitescomponent.h" #include "skycomponents/skiphashlist.h" #include "skycomponents/skymapcomposite.h" #include "skycomponents/solarsystemcomposite.h" #include "skycomponents/earthshadowcomponent.h" #include "skyobjects/constellationsart.h" #include "skyobjects/deepskyobject.h" #include "skyobjects/ksasteroid.h" #include "skyobjects/kscomet.h" #include "skyobjects/kssun.h" #include "skyobjects/satellite.h" #include "skyobjects/supernova.h" #include "skyobjects/ksearthshadow.h" #include "hips/hipsrenderer.h" namespace { // Convert spectral class to numerical index. // If spectral class is invalid return index for white star (A class) int harvardToIndex(char c) { switch (c) { case 'o': case 'O': return 0; case 'b': case 'B': return 1; case 'a': case 'A': return 2; case 'f': case 'F': return 3; case 'g': case 'G': return 4; case 'k': case 'K': return 5; case 'm': case 'M': return 6; // For unknown spectral class assume A class (white star) default: return 2; } } // Total number of sizes of stars. const int nStarSizes = 15; // Total number of spectral classes // N.B. Must be in sync with harvardToIndex const int nSPclasses = 7; // Cache for star images. // // These pixmaps are never deallocated. Not really good... QPixmap *imageCache[nSPclasses][nStarSizes] = { { nullptr } }; std::unique_ptr visibleSatPixmap, invisibleSatPixmap; } int SkyQPainter::starColorMode = 0; QColor SkyQPainter::m_starColor = QColor(); QMap SkyQPainter::ColorMap = QMap(); void SkyQPainter::releaseImageCache() { for (char &color : ColorMap.keys()) { QPixmap **pmap = imageCache[harvardToIndex(color)]; for (int size = 1; size < nStarSizes; size++) { if (pmap[size]) delete pmap[size]; pmap[size] = nullptr; } } } SkyQPainter::SkyQPainter(QPaintDevice *pd) : SkyPainter(), QPainter() { Q_ASSERT(pd); m_pd = pd; m_size = QSize(pd->width(), pd->height()); m_hipsRender = new HIPSRenderer(); } SkyQPainter::SkyQPainter(QPaintDevice *pd, const QSize &size) : SkyPainter(), QPainter() { Q_ASSERT(pd); m_pd = pd; m_size = size; m_hipsRender = new HIPSRenderer(); } SkyQPainter::SkyQPainter(QWidget *widget, QPaintDevice *pd) : SkyPainter(), QPainter() { Q_ASSERT(widget); // Set paint device pointer to pd or to the widget if pd = 0 m_pd = (pd ? pd : widget); m_size = widget->size(); m_hipsRender = new HIPSRenderer(); } SkyQPainter::~SkyQPainter() { delete (m_hipsRender); } void SkyQPainter::begin() { QPainter::begin(m_pd); bool aa = !m_sm->isSlewing() && Options::useAntialias(); setRenderHint(QPainter::Antialiasing, aa); setRenderHint(QPainter::HighQualityAntialiasing, aa); m_proj = m_sm->projector(); } void SkyQPainter::end() { QPainter::end(); } void SkyQPainter::drawSkyBackground() { //FIXME use projector fillRect(0, 0, m_size.width(), m_size.height(), KStarsData::Instance()->colorScheme()->colorNamed("SkyColor")); } void SkyQPainter::setPen(const QPen &pen) { QPainter::setPen(pen); } void SkyQPainter::setBrush(const QBrush &brush) { QPainter::setBrush(brush); } void SkyQPainter::initStarImages() { const int starColorIntensity = Options::starColorIntensity(); ColorMap.clear(); switch (Options::starColorMode()) { case 1: // Red stars. m_starColor = Qt::red; break; case 2: // Black stars. m_starColor = Qt::black; break; case 3: // White stars m_starColor = Qt::white; break; case 0: // Real color default: // And use real color for everything else m_starColor = QColor(); ColorMap.insert('O', QColor::fromRgb(0, 0, 255)); ColorMap.insert('B', QColor::fromRgb(0, 200, 255)); ColorMap.insert('A', QColor::fromRgb(0, 255, 255)); ColorMap.insert('F', QColor::fromRgb(200, 255, 100)); ColorMap.insert('G', QColor::fromRgb(255, 255, 0)); ColorMap.insert('K', QColor::fromRgb(255, 100, 0)); ColorMap.insert('M', QColor::fromRgb(255, 0, 0)); break; } if (ColorMap.isEmpty()) { ColorMap.insert('O', m_starColor); ColorMap.insert('B', m_starColor); ColorMap.insert('A', m_starColor); ColorMap.insert('F', m_starColor); ColorMap.insert('G', m_starColor); ColorMap.insert('K', m_starColor); ColorMap.insert('M', m_starColor); } for (char &color : ColorMap.keys()) { QPixmap BigImage(15, 15); BigImage.fill(Qt::transparent); QPainter p; p.begin(&BigImage); if (Options::starColorMode() == 0) { qreal h, s, v, a; p.setRenderHint(QPainter::Antialiasing, false); QColor starColor = ColorMap[color]; starColor.getHsvF(&h, &s, &v, &a); for (int i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) { qreal x = i - 7; qreal y = j - 7; qreal dist = sqrt(x * x + y * y) / 7.0; starColor.setHsvF(h, qMin(qreal(1), dist < (10 - starColorIntensity) / 10.0 ? 0 : dist), v, qMax(qreal(0), dist < (10 - starColorIntensity) / 20.0 ? 1 : 1 - dist)); p.setPen(starColor); p.drawPoint(i, j); p.drawPoint(14 - i, j); p.drawPoint(i, 14 - j); p.drawPoint(14 - i, 14 - j); } } } else { p.setRenderHint(QPainter::Antialiasing, true); p.setPen(QPen(ColorMap[color], 2.0)); p.setBrush(p.pen().color()); p.drawEllipse(QRectF(2, 2, 10, 10)); } p.end(); // Cache array slice QPixmap **pmap = imageCache[harvardToIndex(color)]; for (int size = 1; size < nStarSizes; size++) { if (!pmap[size]) pmap[size] = new QPixmap(); *pmap[size] = BigImage.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation); } } starColorMode = Options::starColorMode(); if (!visibleSatPixmap.get()) visibleSatPixmap.reset(new QPixmap(":/icons/kstars_satellites_visible.svg")); if (!invisibleSatPixmap.get()) invisibleSatPixmap.reset(new QPixmap(":/icons/kstars_satellites_invisible.svg")); } void SkyQPainter::drawSkyLine(SkyPoint *a, SkyPoint *b) { bool aVisible, bVisible; QPointF aScreen = m_proj->toScreen(a, true, &aVisible); QPointF bScreen = m_proj->toScreen(b, true, &bVisible); drawLine(aScreen, bScreen); //THREE CASES: // if (aVisible && bVisible) // { // //Both a,b visible, so paint the line normally: // drawLine(aScreen, bScreen); // } // else if (aVisible) // { // //a is visible but b isn't: // drawLine(aScreen, m_proj->clipLine(a, b)); // } // else if (bVisible) // { // //b is visible but a isn't: // drawLine(bScreen, m_proj->clipLine(b, a)); // } //FIXME: what if both are offscreen but the line isn't? } void SkyQPainter::drawSkyPolyline(LineList *list, SkipHashList *skipList, LineListLabel *label) { SkyList *points = list->points(); bool isVisible, isVisibleLast; QPointF oLast = m_proj->toScreen(points->first().get(), true, &isVisibleLast); // & with the result of checkVisibility to clip away things below horizon isVisibleLast &= m_proj->checkVisibility(points->first().get()); QPointF oThis, oThis2; for (int j = 1; j < points->size(); j++) { SkyPoint *pThis = points->at(j).get(); oThis2 = oThis = m_proj->toScreen(pThis, true, &isVisible); // & with the result of checkVisibility to clip away things below horizon isVisible &= m_proj->checkVisibility(pThis); bool doSkip = false; if (skipList) { doSkip = skipList->skip(j); } bool pointsVisible = false; //Temporary solution to avoid random lines in Gnomonic projection and draw lines up to horizon if (SkyMap::Instance()->projector()->type() == Projector::Gnomonic) { if (isVisible && isVisibleLast) pointsVisible = true; } else { if (isVisible || isVisibleLast) pointsVisible = true; } if (!doSkip) { if (pointsVisible) { drawLine(oLast, oThis); if (label) label->updateLabelCandidates(oThis.x(), oThis.y(), list, j); } } oLast = oThis2; isVisibleLast = isVisible; } } void SkyQPainter::drawSkyPolygon(LineList *list, bool forceClip) { bool isVisible = false, isVisibleLast; SkyList *points = list->points(); QPolygonF polygon; if (forceClip == false) { for (const auto &point : *points) { polygon << m_proj->toScreen(point.get(), false, &isVisibleLast); isVisible |= isVisibleLast; } // If 1+ points are visible, draw it if (polygon.size() && isVisible) drawPolygon(polygon); return; } SkyPoint *pLast = points->last().get(); QPointF oLast = m_proj->toScreen(pLast, true, &isVisibleLast); // & with the result of checkVisibility to clip away things below horizon isVisibleLast &= m_proj->checkVisibility(pLast); for (const auto &point : *points) { SkyPoint *pThis = point.get(); QPointF oThis = m_proj->toScreen(pThis, true, &isVisible); // & with the result of checkVisibility to clip away things below horizon isVisible &= m_proj->checkVisibility(pThis); if (isVisible && isVisibleLast) { polygon << oThis; } else if (isVisibleLast) { QPointF oMid = m_proj->clipLine(pLast, pThis); polygon << oMid; } else if (isVisible) { QPointF oMid = m_proj->clipLine(pThis, pLast); polygon << oMid; polygon << oThis; } pLast = pThis; oLast = oThis; isVisibleLast = isVisible; } if (polygon.size()) drawPolygon(polygon); } bool SkyQPainter::drawPlanet(KSPlanetBase *planet) { if (!m_proj->checkVisibility(planet)) return false; bool visible = false; QPointF pos = m_proj->toScreen(planet, true, &visible); if (!visible || !m_proj->onScreen(pos)) return false; float fakeStarSize = (10.0 + log10(Options::zoomFactor()) - log10(MINZOOM)) * (10 - planet->mag()) / 10; if (fakeStarSize > 15.0) fakeStarSize = 15.0; double size = planet->angSize() * dms::PI * Options::zoomFactor() / 10800.0; if (size < fakeStarSize && planet->name() != i18n("Sun") && planet->name() != i18n("Moon")) { // Draw them as bright stars of appropriate color instead of images char spType; //FIXME: do these need i18n? if (planet->name() == i18n("Mars")) { spType = 'K'; } else if (planet->name() == i18n("Jupiter") || planet->name() == i18n("Mercury") || planet->name() == i18n("Saturn")) { spType = 'F'; } else { spType = 'B'; } drawPointSource(pos, fakeStarSize, spType); } else { float sizemin = 1.0; if (planet->name() == i18n("Sun") || planet->name() == i18n("Moon")) sizemin = 8.0; if (size < sizemin) size = sizemin; if (Options::showPlanetImages() && !planet->image().isNull()) { //Because Saturn has rings, we inflate its image size by a factor 2.5 if (planet->name() == "Saturn") size = int(2.5 * size); // Scale size exponentially so it is visible at large zooms else if (planet->name() == "Pluto") size = int(size * exp(1.5 * size)); save(); translate(pos); rotate(m_proj->findPA(planet, pos.x(), pos.y())); drawImage(QRectF(-0.5 * size, -0.5 * size, size, size), planet->image()); restore(); } else //Otherwise, draw a simple circle. { drawEllipse(pos, size * .5, size * .5); } } return true; } bool SkyQPainter::drawEarthShadow(KSEarthShadow *shadow) { if (!m_proj->checkVisibility(shadow)) return false; bool visible = false; QPointF pos = m_proj->toScreen(shadow, true, &visible); if(!visible) return false; double umbra_size = shadow->getUmbraAngSize() * dms::PI * Options::zoomFactor() / 10800.0; double penumbra_size = shadow->getPenumbraAngSize() * dms::PI * Options::zoomFactor() / 10800.0; save(); setBrush(QBrush(QColor(255, 96, 38, 128))); drawEllipse(pos, umbra_size, umbra_size); setBrush(QBrush(QColor(255, 96, 38, 90))); drawEllipse(pos, penumbra_size, penumbra_size); restore(); return true; } bool SkyQPainter::drawComet(KSComet *com) { if (!m_proj->checkVisibility(com)) return false; double size = com->angSize() * dms::PI * Options::zoomFactor() / 10800.0 / 2; // Radius if (size < 1) size = 1; bool visible = false; QPointF pos = m_proj->toScreen(com, true, &visible); // Draw the coma. FIXME: Another Check?? if (visible && m_proj->onScreen(pos)) { // Draw the comet. drawEllipse(pos, size, size); double comaLength = (com->getComaAngSize().arcmin() * dms::PI * Options::zoomFactor() / 10800.0); // If coma is visible and long enough. if (Options::showCometComas() && comaLength > size) { KSSun *sun = KStarsData::Instance()->skyComposite()->solarSystemComposite()->sun(); // Find the angle to the sun. double comaAngle = m_proj->findPA(sun, pos.x(), pos.y()); const QVector coma = { QPoint(pos.x() - size, pos.y()), QPoint(pos.x() + size, pos.y()), QPoint(pos.x(), pos.y() + comaLength) }; QPolygon comaPoly(coma); comaPoly = QTransform() .translate(pos.x(), pos.y()) .rotate(comaAngle) // Already + 180 Deg, because rotated from south, not north. .translate(-pos.x(), -pos.y()) .map(comaPoly); save(); // Nice fade for the Coma. QLinearGradient linearGrad(pos, comaPoly.point(2)); linearGrad.setColorAt(0, QColor("white")); linearGrad.setColorAt(size / comaLength, QColor("white")); linearGrad.setColorAt(0.9, QColor("transparent")); setBrush(linearGrad); // Render Coma. drawConvexPolygon(comaPoly); restore(); } return true; } else { return false; } } bool SkyQPainter::drawPointSource(SkyPoint *loc, float mag, char sp) { //Check if it's even visible before doing anything if (!m_proj->checkVisibility(loc)) return false; bool visible = false; QPointF pos = m_proj->toScreen(loc, true, &visible); if (visible && m_proj->onScreen( pos)) // FIXME: onScreen here should use canvas size rather than SkyMap size, especially while printing in portrait mode! { drawPointSource(pos, starWidth(mag), sp); return true; } else { return false; } } void SkyQPainter::drawPointSource(const QPointF &pos, float size, char sp) { int isize = qMin(static_cast(size), 14); if (!m_vectorStars || starColorMode == 0) { // Draw stars as bitmaps, either because we were asked to, or because we're painting real colors QPixmap *im = imageCache[harvardToIndex(sp)][isize]; float offset = 0.5 * im->width(); drawPixmap(QPointF(pos.x() - offset, pos.y() - offset), *im); } else { // Draw stars as vectors, for better printing / SVG export etc. if (starColorMode != 4) { setPen(m_starColor); setBrush(m_starColor); } else { // Note: This is not efficient, but we use vector stars only when plotting SVG, not when drawing the skymap, so speed is not very important. QColor c = ColorMap.value(sp, Qt::white); setPen(c); setBrush(c); } // Be consistent with old raster representation if (size > 14) size = 14; if (size >= 2) drawEllipse(pos.x() - 0.5 * size, pos.y() - 0.5 * size, int(size), int(size)); else if (size >= 1) drawPoint(pos.x(), pos.y()); } } bool SkyQPainter::drawConstellationArtImage(ConstellationsArt *obj) { double zoom = Options::zoomFactor(); bool visible = false; obj->EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); QPointF constellationmidpoint = m_proj->toScreen(obj, true, &visible); if (!visible || !m_proj->onScreen(constellationmidpoint)) return false; //qDebug() << "o->pa() " << obj->pa(); float positionangle = m_proj->findPA(obj, constellationmidpoint.x(), constellationmidpoint.y()); //qDebug() << " final PA " << positionangle; float w = obj->getWidth() * 60 * dms::PI * zoom / 10800; float h = obj->getHeight() * 60 * dms::PI * zoom / 10800; save(); setRenderHint(QPainter::SmoothPixmapTransform); translate(constellationmidpoint); rotate(positionangle); setOpacity(0.7); drawImage(QRectF(-0.5 * w, -0.5 * h, w, h), obj->image()); setOpacity(1); setRenderHint(QPainter::SmoothPixmapTransform, false); restore(); return true; } bool SkyQPainter::drawHips() { int w = viewport().width(); int h = viewport().height(); QImage *hipsImage = new QImage(w, h, QImage::Format_ARGB32_Premultiplied); bool rendered = m_hipsRender->render(w, h, hipsImage, m_proj); if (rendered) drawImage(viewport(), *hipsImage); delete (hipsImage); return rendered; } bool SkyQPainter::drawDeepSkyObject(DeepSkyObject *obj, bool drawImage) { if (!m_proj->checkVisibility(obj)) return false; bool visible = false; QPointF pos = m_proj->toScreen(obj, true, &visible); if (!visible || !m_proj->onScreen(pos)) return false; // if size is 0.0 set it to 1.0, this are normally stars (type 0 and 1) // if we use size 0.0 the star wouldn't be drawn float majorAxis = obj->a(); if (majorAxis == 0.0) { majorAxis = 1.0; } float size = majorAxis * dms::PI * Options::zoomFactor() / 10800.0; //FIXME: this is probably incorrect float positionAngle = m_proj->findPA(obj, pos.x(), pos.y()); //Draw Image if (drawImage && Options::zoomFactor() > 5. * MINZOOM) drawDeepSkyImage(pos, obj, positionAngle); //Draw Symbol drawDeepSkySymbol(pos, obj->type(), size, obj->e(), positionAngle); return true; } bool SkyQPainter::drawDeepSkyImage(const QPointF &pos, DeepSkyObject *obj, float positionAngle) { double zoom = Options::zoomFactor(); double w = obj->a() * dms::PI * zoom / 10800.0; double h = obj->e() * w; save(); translate(pos); rotate(positionAngle); drawImage(QRectF(-0.5 * w, -0.5 * h, w, h), obj->image()); restore(); return true; } void SkyQPainter::drawDeepSkySymbol(const QPointF &pos, int type, float size, float e, float positionAngle) { float x = pos.x(); float y = pos.y(); float zoom = Options::zoomFactor(); int isize = int(size); float dx1 = -0.5 * size; float dx2 = 0.5 * size; float dy1 = -1.0 * e * size / 2.; float dy2 = e * size / 2.; float x1 = x + dx1; float x2 = x + dx2; float y1 = y + dy1; float y2 = y + dy2; float dxa = -size / 4.; float dxb = size / 4.; float dya = -1.0 * e * size / 4.; float dyb = e * size / 4.; float xa = x + dxa; float xb = x + dxb; float ya = y + dya; float yb = y + dyb; QString color; float psize; QBrush tempBrush; std::function lambdaDrawEllipse; std::function lambdaDrawLine; std::function lambdaDrawCross; if (Options::useAntialias()) { lambdaDrawEllipse = [this](float x, float y, float width, float height) { drawEllipse(QRectF(x, y, width, height)); }; lambdaDrawLine = [this](float x1, float y1, float x2, float y2) { drawLine(QLineF(x1, y1, x2, y2)); }; lambdaDrawCross = [this](float centerX, float centerY, float sizeX, float sizeY) { drawLine(QLineF(centerX - sizeX / 2., centerY, centerX + sizeX / 2., centerY)); drawLine(QLineF(centerX, centerY - sizeY / 2., centerX, centerY + sizeY / 2.)); }; } else { lambdaDrawEllipse = [this](float x, float y, float width, float height) { drawEllipse(QRect(x, y, width, height)); }; lambdaDrawLine = [this](float x1, float y1, float x2, float y2) { drawLine(QLine(x1, y1, x2, y2)); }; lambdaDrawCross = [this](float centerX, float centerY, float sizeX, float sizeY) { drawLine(QLine(centerX - sizeX / 2., centerY, centerX + sizeX / 2., centerY)); drawLine(QLine(centerX, centerY - sizeY / 2., centerX, centerY + sizeY / 2.)); }; } switch (type) { case 0: case 1: //catalog star //Some NGC/IC objects are stars...changed their type to 1 (was double star) if (size < 2.) size = 2.; lambdaDrawEllipse(x - size / 2., y - size / 2., size, size); break; case 2: //Planet break; case 3: //Open cluster; draw circle of points case 13: // Asterism { tempBrush = brush(); color = pen().color().name(); setBrush(pen().color()); psize = 2.; if (size > 50.) psize *= 2.; if (size > 100.) psize *= 2.; auto putDot = [psize, &lambdaDrawEllipse](float x, float y) { lambdaDrawEllipse(x - psize / 2., y - psize / 2., psize, psize); }; putDot(xa, y1); putDot(xb, y1); putDot(xa, y2); putDot(xb, y2); putDot(x1, ya); putDot(x1, yb); putDot(x2, ya); putDot(x2, yb); setBrush(tempBrush); break; } case 4: //Globular Cluster if (size < 2.) size = 2.; save(); translate(x, y); color = pen().color().name(); rotate(positionAngle); //rotate the coordinate system lambdaDrawEllipse(dx1, dy1, size, e * size); lambdaDrawCross(0, 0, size, e * size); restore(); //reset coordinate system break; case 5: //Gaseous Nebula case 15: // Dark Nebula save(); translate(x, y); rotate(positionAngle); //rotate the coordinate system color = pen().color().name(); lambdaDrawLine(dx1, dy1, dx2, dy1); lambdaDrawLine(dx2, dy1, dx2, dy2); lambdaDrawLine(dx2, dy2, dx1, dy2); lambdaDrawLine(dx1, dy2, dx1, dy1); restore(); //reset coordinate system break; case 6: //Planetary Nebula if (size < 2.) size = 2.; save(); translate(x, y); rotate(positionAngle); //rotate the coordinate system color = pen().color().name(); lambdaDrawEllipse(dx1, dy1, size, e * size); lambdaDrawLine(0., dy1, 0., dy1 - e * size / 2.); lambdaDrawLine(0., dy2, 0., dy2 + e * size / 2.); lambdaDrawLine(dx1, 0., dx1 - size / 2., 0.); lambdaDrawLine(dx2, 0., dx2 + size / 2., 0.); restore(); //reset coordinate system break; case 7: //Supernova remnant // FIXME: Why is SNR drawn different from a gaseous nebula? save(); translate(x, y); rotate(positionAngle); //rotate the coordinate system color = pen().color().name(); lambdaDrawLine(0., dy1, dx2, 0.); lambdaDrawLine(dx2, 0., 0., dy2); lambdaDrawLine(0., dy2, dx1, 0.); lambdaDrawLine(dx1, 0., 0., dy1); restore(); //reset coordinate system break; case 8: //Galaxy case 16: // Quasar color = pen().color().name(); if (size < 1. && zoom > 20 * MINZOOM) size = 3.; //force ellipse above zoomFactor 20 if (size < 1. && zoom > 5 * MINZOOM) size = 1.; //force points above zoomFactor 5 if (size > 2.) { save(); translate(x, y); rotate(positionAngle); //rotate the coordinate system lambdaDrawEllipse(dx1, dy1, size, e * size); restore(); //reset coordinate system } else if (size > 0.) { drawPoint(QPointF(x, y)); } break; case 14: // Galaxy cluster - draw a dashed circle { tempBrush = brush(); setBrush(QBrush()); color = pen().color().name(); save(); translate(x, y); rotate(positionAngle); //rotate the coordinate system QPen newPen = pen(); newPen.setStyle(Qt::DashLine); setPen(newPen); lambdaDrawEllipse(dx1, dy1, size, e * size); restore(); setBrush(tempBrush); break; } default: // Unknown object or something we don't know how to draw. Just draw an ellipse with a ?-mark color = pen().color().name(); if (size < 1. && zoom > 20 * MINZOOM) size = 3.; //force ellipse above zoomFactor 20 if (size < 1. && zoom > 5 * MINZOOM) size = 1.; //force points above zoomFactor 5 if (size > 2.) { save(); QFont f = font(); const QString qMark = " ? "; + + #if QT_VERSION >= QT_VERSION_CHECK(5,11,0) + double scaleFactor = 0.8 * size / fontMetrics().horizontalAdvance(qMark); + #else double scaleFactor = 0.8 * size / fontMetrics().width(qMark); + #endif + f.setPointSizeF(f.pointSizeF() * scaleFactor); setFont(f); translate(x, y); rotate(positionAngle); //rotate the coordinate system lambdaDrawEllipse(dx1, dy1, size, e * size); if (Options::useAntialias()) drawText(QRectF(dx1, dy1, size, e * size), Qt::AlignCenter, qMark); else { int idx1 = int(dx1); int idy1 = int(dy1); drawText(QRect(idx1, idy1, isize, int(e * size)), Qt::AlignCenter, qMark); } restore(); //reset coordinate system (and font?) } else if (size > 0.) { if (Options::useAntialias()) drawPoint(QPointF(x, y)); else drawPoint(QPoint(x, y)); } } } void SkyQPainter::drawObservingList(const QList &obs) { foreach (SkyObject *obj, obs) { bool visible = false; QPointF o = m_proj->toScreen(obj, true, &visible); if (!visible || !m_proj->onScreen(o)) continue; float size = 20.; float x1 = o.x() - 0.5 * size; float y1 = o.y() - 0.5 * size; drawArc(QRectF(x1, y1, size, size), -60 * 16, 120 * 16); drawArc(QRectF(x1, y1, size, size), 120 * 16, 120 * 16); } } void SkyQPainter::drawFlags() { KStarsData *data = KStarsData::Instance(); std::shared_ptr point; QImage image; bool visible = false; QPointF pos; for (int i = 0; i < data->skyComposite()->flags()->size(); i++) { point = data->skyComposite()->flags()->pointList().at(i); image = data->skyComposite()->flags()->image(i); // Set Horizontal coordinates point->EquatorialToHorizontal(data->lst(), data->geo()->lat()); // Get flag position on screen pos = m_proj->toScreen(point.get(), true, &visible); // Return if flag is not visible if (!visible || !m_proj->onScreen(pos)) continue; // Draw flag image drawImage(pos.x() - 0.5 * image.width(), pos.y() - 0.5 * image.height(), image); // Draw flag label setPen(data->skyComposite()->flags()->labelColor(i)); setFont(QFont("Helvetica", 10, QFont::Bold)); drawText(pos.x() + 10, pos.y() - 10, data->skyComposite()->flags()->label(i)); } } void SkyQPainter::drawHorizon(bool filled, SkyPoint *labelPoint, bool *drawLabel) { QVector ground = m_proj->groundPoly(labelPoint, drawLabel); if (ground.size()) { QPolygonF groundPoly(ground.size()); for (int i = 0; i < ground.size(); ++i) groundPoly[i] = KSUtils::vecToPoint(ground[i]); if (filled) drawPolygon(groundPoly); else { groundPoly.append(groundPoly.first()); drawPolyline(groundPoly); } } } bool SkyQPainter::drawSatellite(Satellite *sat) { if (!m_proj->checkVisibility(sat)) return false; QPointF pos; bool visible = false; //sat->HorizontalToEquatorial( data->lst(), data->geo()->lat() ); pos = m_proj->toScreen(sat, true, &visible); if (!visible || !m_proj->onScreen(pos)) return false; if (Options::drawSatellitesLikeStars()) { drawPointSource(pos, 3.5, 'B'); } else { if (sat->isVisible()) drawPixmap(QPoint(pos.x() - 15, pos.y() - 11), *visibleSatPixmap); else drawPixmap(QPoint(pos.x() - 15, pos.y() - 11), *invisibleSatPixmap); //drawPixmap(pos, *genericSatPixmap); /*drawLine( QPoint( pos.x() - 0.5, pos.y() - 0.5 ), QPoint( pos.x() + 0.5, pos.y() - 0.5 ) ); drawLine( QPoint( pos.x() + 0.5, pos.y() - 0.5 ), QPoint( pos.x() + 0.5, pos.y() + 0.5 ) ); drawLine( QPoint( pos.x() + 0.5, pos.y() + 0.5 ), QPoint( pos.x() - 0.5, pos.y() + 0.5 ) ); drawLine( QPoint( pos.x() - 0.5, pos.y() + 0.5 ), QPoint( pos.x() - 0.5, pos.y() - 0.5 ) );*/ } return true; //if ( Options::showSatellitesLabels() ) //data->skyComposite()->satellites()->drawLabel( sat, pos ); } bool SkyQPainter::drawSupernova(Supernova *sup) { KStarsData *data = KStarsData::Instance(); if (!m_proj->checkVisibility(sup)) { return false; } bool visible = false; QPointF pos = m_proj->toScreen(sup, true, &visible); //qDebug()<<"sup->ra() = "<<(sup->ra()).toHMSString()<<"sup->dec() = "<dec().toDMSString(); //qDebug()<<"pos = "<onScreen(pos) = "<onScreen(pos); if (!visible || !m_proj->onScreen(pos)) return false; setPen(data->colorScheme()->colorNamed("SupernovaColor")); //qDebug()<<"Here"< #include #include #include #include #include #include #include #include #include #include #include #include OptionsTreeViewWidget::OptionsTreeViewWidget(QWidget *p) : QFrame(p) { setupUi(this); } OptionsTreeView::OptionsTreeView(QWidget *p) : QDialog(p) { otvw.reset(new OptionsTreeViewWidget(this)); QVBoxLayout *mainLayout = new QVBoxLayout; mainLayout->addWidget(otvw.get()); setLayout(mainLayout); setWindowTitle(i18n("Options")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); mainLayout->addWidget(buttonBox); connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); setModal(false); } void OptionsTreeView::resizeColumns() { //Size each column to the maximum width of items in that column int maxwidth[3] = { 0, 0, 0 }; QFontMetrics qfm = optionsList()->fontMetrics(); for (int i = 0; i < optionsList()->topLevelItemCount(); ++i) { QTreeWidgetItem *topitem = optionsList()->topLevelItem(i); topitem->setExpanded(true); for (int j = 0; j < topitem->childCount(); ++j) { QTreeWidgetItem *child = topitem->child(j); for (int icol = 0; icol < 3; ++icol) { child->setExpanded(true); + #if QT_VERSION >= QT_VERSION_CHECK(5,11,0) + int w = qfm.horizontalAdvance(child->text(icol)) + 4; + #else int w = qfm.width(child->text(icol)) + 4; + #endif + if (icol == 0) { w += 2 * optionsList()->indentation(); } if (w > maxwidth[icol]) { maxwidth[icol] = w; } } } } for (int icol = 0; icol < 3; ++icol) { //DEBUG qDebug() << QString("max width of column %1: %2").arg(icol).arg(maxwidth[icol]) << endl; optionsList()->setColumnWidth(icol, maxwidth[icol]); } } ScriptNameWidget::ScriptNameWidget(QWidget *p) : QFrame(p) { setupUi(this); } ScriptNameDialog::ScriptNameDialog(QWidget *p) : QDialog(p) { #ifdef Q_OS_OSX setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); #endif snw = new ScriptNameWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout; mainLayout->addWidget(snw); setLayout(mainLayout); setWindowTitle(i18n("Script Data")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); mainLayout->addWidget(buttonBox); connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); okB = buttonBox->button(QDialogButtonBox::Ok); connect(snw->ScriptName, SIGNAL(textChanged(QString)), this, SLOT(slotEnableOkButton())); } ScriptNameDialog::~ScriptNameDialog() { delete snw; } void ScriptNameDialog::slotEnableOkButton() { okB->setEnabled(!snw->ScriptName->text().isEmpty()); } ScriptBuilderUI::ScriptBuilderUI(QWidget *p) : QFrame(p) { setupUi(this); } ScriptBuilder::ScriptBuilder(QWidget *parent) : QDialog(parent), UnsavedChanges(false), checkForChanges(true), currentFileURL(), currentDir(QDir::homePath()), currentScriptName(), currentAuthor() { #ifdef Q_OS_OSX setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); #endif sb = new ScriptBuilderUI(this); QVBoxLayout *mainLayout = new QVBoxLayout; mainLayout->addWidget(sb); setLayout(mainLayout); setWindowTitle(i18n("Script Builder")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); mainLayout->addWidget(buttonBox); connect(buttonBox, SIGNAL(rejected()), this, SLOT(slotClose())); sb->FuncDoc->setTextInteractionFlags(Qt::NoTextInteraction); //Initialize function templates and descriptions KStarsFunctionList.append(new ScriptFunction("lookTowards", i18n("Point the display at the specified location. %1 can be the name " "of an object, a cardinal point on the compass, or 'zenith'.", QString("dir")), false, "QString", "dir")); KStarsFunctionList.append(new ScriptFunction( "addLabel", i18n("Add a name label to the object named %1.", QString("name")), false, "QString", "name")); KStarsFunctionList.append( new ScriptFunction("removeLabel", i18n("Remove the name label from the object named %1.", QString("name")), false, "QString", "name")); KStarsFunctionList.append(new ScriptFunction( "addTrail", i18n("Add a trail to the solar system body named %1.", QString("name")), false, "QString", "name")); KStarsFunctionList.append(new ScriptFunction( "removeTrail", i18n("Remove the trail from the solar system body named %1.", QString("name")), false, "QString", "name")); KStarsFunctionList.append(new ScriptFunction("setRaDec", i18n("Point the display at the specified RA/Dec coordinates. RA is " "expressed in Hours; Dec is expressed in Degrees."), false, "double", "ra", "double", "dec")); KStarsFunctionList.append(new ScriptFunction( "setAltAz", i18n("Point the display at the specified Alt/Az coordinates. Alt and Az are expressed in Degrees."), false, "double", "alt", "double", "az")); KStarsFunctionList.append(new ScriptFunction("zoomIn", i18n("Increase the display Zoom Level."), false)); KStarsFunctionList.append(new ScriptFunction("zoomOut", i18n("Decrease the display Zoom Level."), false)); KStarsFunctionList.append( new ScriptFunction("defaultZoom", i18n("Set the display Zoom Level to its default value."), false)); KStarsFunctionList.append( new ScriptFunction("zoom", i18n("Set the display Zoom Level manually."), false, "double", "z")); KStarsFunctionList.append( new ScriptFunction("setLocalTime", i18n("Set the system clock to the specified Local Time."), false, "int", "year", "int", "month", "int", "day", "int", "hour", "int", "minute", "int", "second")); KStarsFunctionList.append(new ScriptFunction( "waitFor", i18n("Pause script execution for specified number of seconds."), false, "double", "sec")); KStarsFunctionList.append(new ScriptFunction("waitForKey", i18n("Halt script execution until the specified key is pressed. Only " "single-key strokes are possible; use 'space' for the spacebar."), false, "QString", "key")); KStarsFunctionList.append(new ScriptFunction( "setTracking", i18n("Set whether the display is tracking the current location."), false, "bool", "track")); KStarsFunctionList.append(new ScriptFunction( "changeViewOption", i18n("Change view option named %1 to value %2.", QString("opName"), QString("opValue")), false, "QString", "opName", "QString", "opValue")); KStarsFunctionList.append(new ScriptFunction( "setGeoLocation", i18n("Set the geographic location to the city specified by city, province and country."), false, "QString", "cityName", "QString", "provinceName", "QString", "countryName")); KStarsFunctionList.append(new ScriptFunction( "setColor", i18n("Set the color named %1 to the value %2.", QString("colorName"), QString("value")), false, "QString", "colorName", "QString", "value")); KStarsFunctionList.append(new ScriptFunction("loadColorScheme", i18n("Load the color scheme specified by name."), false, "QString", "name")); KStarsFunctionList.append( new ScriptFunction("exportImage", i18n("Export the sky image to the file, with specified width and height."), false, "QString", "fileName", "int", "width", "int", "height")); KStarsFunctionList.append( new ScriptFunction("printImage", i18n("Print the sky image to a printer or file. If %1 is true, it will show the print " "dialog. If %2 is true, it will use the Star Chart color scheme for printing.", QString("usePrintDialog"), QString("useChartColors")), false, "bool", "usePrintDialog", "bool", "useChartColors")); SimClockFunctionList.append(new ScriptFunction("stop", i18n("Halt the simulation clock."), true)); SimClockFunctionList.append(new ScriptFunction("start", i18n("Start the simulation clock."), true)); SimClockFunctionList.append(new ScriptFunction("setClockScale", i18n("Set the timescale of the simulation clock to specified scale. " " 1.0 means real-time; 2.0 means twice real-time; etc."), true, "double", "scale")); // JM: We're using QTreeWdiget for Qt4 now QTreeWidgetItem *kstars_tree = new QTreeWidgetItem(sb->FunctionTree, QStringList("KStars")); QTreeWidgetItem *simclock_tree = new QTreeWidgetItem(sb->FunctionTree, QStringList("SimClock")); for (auto &item : KStarsFunctionList) new QTreeWidgetItem(kstars_tree, QStringList(item->prototype())); for (auto &item : SimClockFunctionList) new QTreeWidgetItem(simclock_tree, QStringList(item->prototype())); kstars_tree->sortChildren(0, Qt::AscendingOrder); simclock_tree->sortChildren(0, Qt::AscendingOrder); sb->FunctionTree->setColumnCount(1); sb->FunctionTree->setHeaderLabels(QStringList(i18n("Functions"))); sb->FunctionTree->setSortingEnabled(false); //Add icons to Push Buttons sb->NewButton->setIcon(QIcon::fromTheme("document-new")); sb->OpenButton->setIcon(QIcon::fromTheme("document-open")); sb->SaveButton->setIcon(QIcon::fromTheme("document-save")); sb->SaveAsButton->setIcon( QIcon::fromTheme("document-save-as")); sb->RunButton->setIcon(QIcon::fromTheme("system-run")); sb->CopyButton->setIcon(QIcon::fromTheme("view-refresh")); sb->AddButton->setIcon(QIcon::fromTheme("go-previous")); sb->RemoveButton->setIcon(QIcon::fromTheme("go-next")); sb->UpButton->setIcon(QIcon::fromTheme("go-up")); sb->DownButton->setIcon(QIcon::fromTheme("go-down")); sb->NewButton->setAttribute(Qt::WA_LayoutUsesWidgetRect); sb->OpenButton->setAttribute(Qt::WA_LayoutUsesWidgetRect); sb->SaveButton->setAttribute(Qt::WA_LayoutUsesWidgetRect); sb->SaveAsButton->setAttribute(Qt::WA_LayoutUsesWidgetRect); sb->RunButton->setAttribute(Qt::WA_LayoutUsesWidgetRect); sb->CopyButton->setAttribute(Qt::WA_LayoutUsesWidgetRect); sb->AddButton->setAttribute(Qt::WA_LayoutUsesWidgetRect); sb->RemoveButton->setAttribute(Qt::WA_LayoutUsesWidgetRect); sb->UpButton->setAttribute(Qt::WA_LayoutUsesWidgetRect); sb->DownButton->setAttribute(Qt::WA_LayoutUsesWidgetRect); //Prepare the widget stack argBlank = new QWidget(); argLookToward = new ArgLookToward(sb->ArgStack); argFindObject = new ArgFindObject(sb->ArgStack); //shared by Add/RemoveLabel and Add/RemoveTrail argSetRaDec = new ArgSetRaDec(sb->ArgStack); argSetAltAz = new ArgSetAltAz(sb->ArgStack); argSetLocalTime = new ArgSetLocalTime(sb->ArgStack); argWaitFor = new ArgWaitFor(sb->ArgStack); argWaitForKey = new ArgWaitForKey(sb->ArgStack); argSetTracking = new ArgSetTrack(sb->ArgStack); argChangeViewOption = new ArgChangeViewOption(sb->ArgStack); argSetGeoLocation = new ArgSetGeoLocation(sb->ArgStack); argTimeScale = new ArgTimeScale(sb->ArgStack); argZoom = new ArgZoom(sb->ArgStack); argExportImage = new ArgExportImage(sb->ArgStack); argPrintImage = new ArgPrintImage(sb->ArgStack); argSetColor = new ArgSetColor(sb->ArgStack); argLoadColorScheme = new ArgLoadColorScheme(sb->ArgStack); sb->ArgStack->addWidget(argBlank); sb->ArgStack->addWidget(argLookToward); sb->ArgStack->addWidget(argFindObject); sb->ArgStack->addWidget(argSetRaDec); sb->ArgStack->addWidget(argSetAltAz); sb->ArgStack->addWidget(argSetLocalTime); sb->ArgStack->addWidget(argWaitFor); sb->ArgStack->addWidget(argWaitForKey); sb->ArgStack->addWidget(argSetTracking); sb->ArgStack->addWidget(argChangeViewOption); sb->ArgStack->addWidget(argSetGeoLocation); sb->ArgStack->addWidget(argTimeScale); sb->ArgStack->addWidget(argZoom); sb->ArgStack->addWidget(argExportImage); sb->ArgStack->addWidget(argPrintImage); sb->ArgStack->addWidget(argSetColor); sb->ArgStack->addWidget(argLoadColorScheme); sb->ArgStack->setCurrentIndex(0); snd = new ScriptNameDialog(KStars::Instance()); otv = new OptionsTreeView(KStars::Instance()); otv->resize(sb->width(), 0.5 * sb->height()); initViewOptions(); otv->resizeColumns(); //connect widgets in ScriptBuilderUI connect(sb->FunctionTree, SIGNAL(itemDoubleClicked(QTreeWidgetItem*, int)), this, SLOT(slotAddFunction())); connect(sb->FunctionTree, SIGNAL(itemClicked(QTreeWidgetItem*, int)), this, SLOT(slotShowDoc())); connect(sb->UpButton, SIGNAL(clicked()), this, SLOT(slotMoveFunctionUp())); connect(sb->ScriptListBox, SIGNAL(itemClicked(QListWidgetItem*)), this, SLOT(slotArgWidget())); connect(sb->DownButton, SIGNAL(clicked()), this, SLOT(slotMoveFunctionDown())); connect(sb->CopyButton, SIGNAL(clicked()), this, SLOT(slotCopyFunction())); connect(sb->RemoveButton, SIGNAL(clicked()), this, SLOT(slotRemoveFunction())); connect(sb->NewButton, SIGNAL(clicked()), this, SLOT(slotNew())); connect(sb->OpenButton, SIGNAL(clicked()), this, SLOT(slotOpen())); connect(sb->SaveButton, SIGNAL(clicked()), this, SLOT(slotSave())); connect(sb->SaveAsButton, SIGNAL(clicked()), this, SLOT(slotSaveAs())); connect(sb->AddButton, SIGNAL(clicked()), this, SLOT(slotAddFunction())); connect(sb->RunButton, SIGNAL(clicked()), this, SLOT(slotRunScript())); //Connections for Arg Widgets connect(argSetGeoLocation->FindCityButton, SIGNAL(clicked()), this, SLOT(slotFindCity())); connect(argLookToward->FindButton, SIGNAL(clicked()), this, SLOT(slotFindObject())); connect(argChangeViewOption->TreeButton, SIGNAL(clicked()), this, SLOT(slotShowOptions())); connect(argFindObject->FindButton, SIGNAL(clicked()), this, SLOT(slotFindObject())); connect(argLookToward->FocusEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotLookToward())); connect(argFindObject->NameEdit, SIGNAL(textChanged(QString)), this, SLOT(slotArgFindObject())); connect(argSetRaDec->RABox, SIGNAL(textChanged(QString)), this, SLOT(slotRa())); connect(argSetRaDec->DecBox, SIGNAL(textChanged(QString)), this, SLOT(slotDec())); connect(argSetAltAz->AltBox, SIGNAL(textChanged(QString)), this, SLOT(slotAlt())); connect(argSetAltAz->AzBox, SIGNAL(textChanged(QString)), this, SLOT(slotAz())); connect(argSetLocalTime->DateWidget, SIGNAL(dateChanged(QDate)), this, SLOT(slotChangeDate())); connect(argSetLocalTime->TimeBox, SIGNAL(timeChanged(QTime)), this, SLOT(slotChangeTime())); connect(argWaitFor->DelayBox, SIGNAL(valueChanged(int)), this, SLOT(slotWaitFor())); connect(argWaitForKey->WaitKeyEdit, SIGNAL(textChanged(QString)), this, SLOT(slotWaitForKey())); connect(argSetTracking->CheckTrack, SIGNAL(stateChanged(int)), this, SLOT(slotTracking())); connect(argChangeViewOption->OptionName, SIGNAL(activated(QString)), this, SLOT(slotViewOption())); connect(argChangeViewOption->OptionValue, SIGNAL(textChanged(QString)), this, SLOT(slotViewOption())); connect(argSetGeoLocation->CityName, SIGNAL(textChanged(QString)), this, SLOT(slotChangeCity())); connect(argSetGeoLocation->ProvinceName, SIGNAL(textChanged(QString)), this, SLOT(slotChangeProvince())); connect(argSetGeoLocation->CountryName, SIGNAL(textChanged(QString)), this, SLOT(slotChangeCountry())); connect(argTimeScale->TimeScale, SIGNAL(scaleChanged(float)), this, SLOT(slotTimeScale())); connect(argZoom->ZoomBox, SIGNAL(textChanged(QString)), this, SLOT(slotZoom())); connect(argExportImage->ExportFileName, SIGNAL(textChanged(QString)), this, SLOT(slotExportImage())); connect(argExportImage->ExportWidth, SIGNAL(valueChanged(int)), this, SLOT(slotExportImage())); connect(argExportImage->ExportHeight, SIGNAL(valueChanged(int)), this, SLOT(slotExportImage())); connect(argPrintImage->UsePrintDialog, SIGNAL(toggled(bool)), this, SLOT(slotPrintImage())); connect(argPrintImage->UseChartColors, SIGNAL(toggled(bool)), this, SLOT(slotPrintImage())); connect(argSetColor->ColorName, SIGNAL(activated(QString)), this, SLOT(slotChangeColorName())); connect(argSetColor->ColorValue, SIGNAL(changed(QColor)), this, SLOT(slotChangeColor())); connect(argLoadColorScheme->SchemeList, SIGNAL(itemClicked(QListWidgetItem*)), this, SLOT(slotLoadColorScheme())); //disable some buttons sb->CopyButton->setEnabled(false); sb->AddButton->setEnabled(false); sb->RemoveButton->setEnabled(false); sb->UpButton->setEnabled(false); sb->DownButton->setEnabled(false); sb->SaveButton->setEnabled(false); sb->SaveAsButton->setEnabled(false); sb->RunButton->setEnabled(false); } ScriptBuilder::~ScriptBuilder() { while (!KStarsFunctionList.isEmpty()) delete KStarsFunctionList.takeFirst(); while (!SimClockFunctionList.isEmpty()) delete SimClockFunctionList.takeFirst(); while (!ScriptList.isEmpty()) delete ScriptList.takeFirst(); } void ScriptBuilder::initViewOptions() { otv->optionsList()->setRootIsDecorated(true); QStringList fields; //InfoBoxes opsGUI = new QTreeWidgetItem(otv->optionsList(), QStringList(i18n("InfoBoxes"))); fields << "ShowInfoBoxes" << i18n("Toggle display of all InfoBoxes") << i18n("bool"); new QTreeWidgetItem(opsGUI, fields); fields.clear(); fields << "ShowTimeBox" << i18n("Toggle display of Time InfoBox") << i18n("bool"); new QTreeWidgetItem(opsGUI, fields); fields.clear(); fields << "ShowGeoBox" << i18n("Toggle display of Geographic InfoBox") << i18n("bool"); new QTreeWidgetItem(opsGUI, fields); fields.clear(); fields << "ShowFocusBox" << i18n("Toggle display of Focus InfoBox") << i18n("bool"); new QTreeWidgetItem(opsGUI, fields); fields.clear(); fields << "ShadeTimeBox" << i18n("(un)Shade Time InfoBox") << i18n("bool"); new QTreeWidgetItem(opsGUI, fields); fields.clear(); fields << "ShadeGeoBox" << i18n("(un)Shade Geographic InfoBox") << i18n("bool"); new QTreeWidgetItem(opsGUI, fields); fields.clear(); fields << "ShadeFocusBox" << i18n("(un)Shade Focus InfoBox") << i18n("bool"); new QTreeWidgetItem(opsGUI, fields); fields.clear(); argChangeViewOption->OptionName->addItem("ShowInfoBoxes"); argChangeViewOption->OptionName->addItem("ShowTimeBox"); argChangeViewOption->OptionName->addItem("ShowGeoBox"); argChangeViewOption->OptionName->addItem("ShowFocusBox"); argChangeViewOption->OptionName->addItem("ShadeTimeBox"); argChangeViewOption->OptionName->addItem("ShadeGeoBox"); argChangeViewOption->OptionName->addItem("ShadeFocusBox"); //Toolbars opsToolbar = new QTreeWidgetItem(otv->optionsList(), QStringList(i18n("Toolbars"))); fields << "ShowMainToolBar" << i18n("Toggle display of main toolbar") << i18n("bool"); new QTreeWidgetItem(opsToolbar, fields); fields.clear(); fields << "ShowViewToolBar" << i18n("Toggle display of view toolbar") << i18n("bool"); new QTreeWidgetItem(opsToolbar, fields); fields.clear(); argChangeViewOption->OptionName->addItem("ShowMainToolBar"); argChangeViewOption->OptionName->addItem("ShowViewToolBar"); //Show Objects opsShowObj = new QTreeWidgetItem(otv->optionsList(), QStringList(i18n("Show Objects"))); fields << "ShowStars" << i18n("Toggle display of Stars") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); fields.clear(); fields << "ShowDeepSky" << i18n("Toggle display of all deep-sky objects") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); fields.clear(); fields << "ShowMessier" << i18n("Toggle display of Messier object symbols") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); fields.clear(); fields << "ShowMessierImages" << i18n("Toggle display of Messier object images") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); fields.clear(); fields << "ShowNGC" << i18n("Toggle display of NGC objects") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); fields.clear(); fields << "ShowIC" << i18n("Toggle display of IC objects") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); fields.clear(); fields << "ShowSolarSystem" << i18n("Toggle display of all solar system bodies") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); fields.clear(); fields << "ShowSun" << i18n("Toggle display of Sun") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); fields.clear(); fields << "ShowMoon" << i18n("Toggle display of Moon") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); fields.clear(); fields << "ShowMercury" << i18n("Toggle display of Mercury") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); fields.clear(); fields << "ShowVenus" << i18n("Toggle display of Venus") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); fields.clear(); fields << "ShowMars" << i18n("Toggle display of Mars") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); fields.clear(); fields << "ShowJupiter" << i18n("Toggle display of Jupiter") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); fields.clear(); fields << "ShowSaturn" << i18n("Toggle display of Saturn") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); fields.clear(); fields << "ShowUranus" << i18n("Toggle display of Uranus") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); fields.clear(); fields << "ShowNeptune" << i18n("Toggle display of Neptune") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); //fields.clear(); //fields << "ShowPluto" << i18n( "Toggle display of Pluto" ) << i18n( "bool" ); //new QTreeWidgetItem( opsShowObj, fields ); fields.clear(); fields << "ShowAsteroids" << i18n("Toggle display of Asteroids") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); fields.clear(); fields << "ShowComets" << i18n("Toggle display of Comets") << i18n("bool"); new QTreeWidgetItem(opsShowObj, fields); fields.clear(); argChangeViewOption->OptionName->addItem("ShowStars"); argChangeViewOption->OptionName->addItem("ShowDeepSky"); argChangeViewOption->OptionName->addItem("ShowMessier"); argChangeViewOption->OptionName->addItem("ShowMessierImages"); argChangeViewOption->OptionName->addItem("ShowNGC"); argChangeViewOption->OptionName->addItem("ShowIC"); argChangeViewOption->OptionName->addItem("ShowSolarSystem"); argChangeViewOption->OptionName->addItem("ShowSun"); argChangeViewOption->OptionName->addItem("ShowMoon"); argChangeViewOption->OptionName->addItem("ShowMercury"); argChangeViewOption->OptionName->addItem("ShowVenus"); argChangeViewOption->OptionName->addItem("ShowMars"); argChangeViewOption->OptionName->addItem("ShowJupiter"); argChangeViewOption->OptionName->addItem("ShowSaturn"); argChangeViewOption->OptionName->addItem("ShowUranus"); argChangeViewOption->OptionName->addItem("ShowNeptune"); //argChangeViewOption->OptionName->addItem( "ShowPluto" ); argChangeViewOption->OptionName->addItem("ShowAsteroids"); argChangeViewOption->OptionName->addItem("ShowComets"); opsShowOther = new QTreeWidgetItem(otv->optionsList(), QStringList(i18n("Show Other"))); fields << "ShowCLines" << i18n("Toggle display of constellation lines") << i18n("bool"); new QTreeWidgetItem(opsShowOther, fields); fields.clear(); fields << "ShowCBounds" << i18n("Toggle display of constellation boundaries") << i18n("bool"); new QTreeWidgetItem(opsShowOther, fields); fields.clear(); fields << "ShowCNames" << i18n("Toggle display of constellation names") << i18n("bool"); new QTreeWidgetItem(opsShowOther, fields); fields.clear(); fields << "ShowMilkyWay" << i18n("Toggle display of Milky Way") << i18n("bool"); new QTreeWidgetItem(opsShowOther, fields); fields.clear(); fields << "ShowGrid" << i18n("Toggle display of the coordinate grid") << i18n("bool"); new QTreeWidgetItem(opsShowOther, fields); fields.clear(); fields << "ShowEquator" << i18n("Toggle display of the celestial equator") << i18n("bool"); new QTreeWidgetItem(opsShowOther, fields); fields.clear(); fields << "ShowEcliptic" << i18n("Toggle display of the ecliptic") << i18n("bool"); new QTreeWidgetItem(opsShowOther, fields); fields.clear(); fields << "ShowHorizon" << i18n("Toggle display of the horizon line") << i18n("bool"); new QTreeWidgetItem(opsShowOther, fields); fields.clear(); fields << "ShowGround" << i18n("Toggle display of the opaque ground") << i18n("bool"); new QTreeWidgetItem(opsShowOther, fields); fields.clear(); fields << "ShowStarNames" << i18n("Toggle display of star name labels") << i18n("bool"); new QTreeWidgetItem(opsShowOther, fields); fields.clear(); fields << "ShowStarMagnitudes" << i18n("Toggle display of star magnitude labels") << i18n("bool"); new QTreeWidgetItem(opsShowOther, fields); fields.clear(); fields << "ShowAsteroidNames" << i18n("Toggle display of asteroid name labels") << i18n("bool"); new QTreeWidgetItem(opsShowOther, fields); fields.clear(); fields << "ShowCometNames" << i18n("Toggle display of comet name labels") << i18n("bool"); new QTreeWidgetItem(opsShowOther, fields); fields.clear(); fields << "ShowPlanetNames" << i18n("Toggle display of planet name labels") << i18n("bool"); new QTreeWidgetItem(opsShowOther, fields); fields.clear(); fields << "ShowPlanetImages" << i18n("Toggle display of planet images") << i18n("bool"); new QTreeWidgetItem(opsShowOther, fields); fields.clear(); argChangeViewOption->OptionName->addItem("ShowCLines"); argChangeViewOption->OptionName->addItem("ShowCBounds"); argChangeViewOption->OptionName->addItem("ShowCNames"); argChangeViewOption->OptionName->addItem("ShowMilkyWay"); argChangeViewOption->OptionName->addItem("ShowGrid"); argChangeViewOption->OptionName->addItem("ShowEquator"); argChangeViewOption->OptionName->addItem("ShowEcliptic"); argChangeViewOption->OptionName->addItem("ShowHorizon"); argChangeViewOption->OptionName->addItem("ShowGround"); argChangeViewOption->OptionName->addItem("ShowStarNames"); argChangeViewOption->OptionName->addItem("ShowStarMagnitudes"); argChangeViewOption->OptionName->addItem("ShowAsteroidNames"); argChangeViewOption->OptionName->addItem("ShowCometNames"); argChangeViewOption->OptionName->addItem("ShowPlanetNames"); argChangeViewOption->OptionName->addItem("ShowPlanetImages"); opsCName = new QTreeWidgetItem(otv->optionsList(), QStringList(i18n("Constellation Names"))); fields << "UseLatinConstellNames" << i18n("Show Latin constellation names") << i18n("bool"); new QTreeWidgetItem(opsCName, fields); fields.clear(); fields << "UseLocalConstellNames" << i18n("Show constellation names in local language") << i18n("bool"); new QTreeWidgetItem(opsCName, fields); fields.clear(); fields << "UseAbbrevConstellNames" << i18n("Show IAU-standard constellation abbreviations") << i18n("bool"); new QTreeWidgetItem(opsCName, fields); fields.clear(); argChangeViewOption->OptionName->addItem("UseLatinConstellNames"); argChangeViewOption->OptionName->addItem("UseLocalConstellNames"); argChangeViewOption->OptionName->addItem("UseAbbrevConstellNames"); opsHide = new QTreeWidgetItem(otv->optionsList(), QStringList(i18n("Hide Items"))); fields << "HideOnSlew" << i18n("Toggle whether objects hidden while slewing display") << i18n("bool"); new QTreeWidgetItem(opsHide, fields); fields.clear(); fields << "SlewTimeScale" << i18n("Timestep threshold (in seconds) for hiding objects") << i18n("double"); new QTreeWidgetItem(opsHide, fields); fields.clear(); fields << "HideStars" << i18n("Hide faint stars while slewing?") << i18n("bool"); new QTreeWidgetItem(opsHide, fields); fields.clear(); fields << "HidePlanets" << i18n("Hide solar system bodies while slewing?") << i18n("bool"); new QTreeWidgetItem(opsHide, fields); fields.clear(); fields << "HideMessier" << i18n("Hide Messier objects while slewing?") << i18n("bool"); new QTreeWidgetItem(opsHide, fields); fields.clear(); fields << "HideNGC" << i18n("Hide NGC objects while slewing?") << i18n("bool"); new QTreeWidgetItem(opsHide, fields); fields.clear(); fields << "HideIC" << i18n("Hide IC objects while slewing?") << i18n("bool"); new QTreeWidgetItem(opsHide, fields); fields.clear(); fields << "HideMilkyWay" << i18n("Hide Milky Way while slewing?") << i18n("bool"); new QTreeWidgetItem(opsHide, fields); fields.clear(); fields << "HideCNames" << i18n("Hide constellation names while slewing?") << i18n("bool"); new QTreeWidgetItem(opsHide, fields); fields.clear(); fields << "HideCLines" << i18n("Hide constellation lines while slewing?") << i18n("bool"); new QTreeWidgetItem(opsHide, fields); fields.clear(); fields << "HideCBounds" << i18n("Hide constellation boundaries while slewing?") << i18n("bool"); new QTreeWidgetItem(opsHide, fields); fields.clear(); fields << "HideGrid" << i18n("Hide coordinate grid while slewing?") << i18n("bool"); new QTreeWidgetItem(opsHide, fields); fields.clear(); argChangeViewOption->OptionName->addItem("HideOnSlew"); argChangeViewOption->OptionName->addItem("SlewTimeScale"); argChangeViewOption->OptionName->addItem("HideStars"); argChangeViewOption->OptionName->addItem("HidePlanets"); argChangeViewOption->OptionName->addItem("HideMessier"); argChangeViewOption->OptionName->addItem("HideNGC"); argChangeViewOption->OptionName->addItem("HideIC"); argChangeViewOption->OptionName->addItem("HideMilkyWay"); argChangeViewOption->OptionName->addItem("HideCNames"); argChangeViewOption->OptionName->addItem("HideCLines"); argChangeViewOption->OptionName->addItem("HideCBounds"); argChangeViewOption->OptionName->addItem("HideGrid"); opsSkymap = new QTreeWidgetItem(otv->optionsList(), QStringList(i18n("Skymap Options"))); fields << "UseAltAz" << i18n("Use Horizontal coordinates? (otherwise, use Equatorial)") << i18n("bool"); new QTreeWidgetItem(opsSkymap, fields); fields.clear(); fields << "ZoomFactor" << i18n("Set the Zoom Factor") << i18n("double"); new QTreeWidgetItem(opsSkymap, fields); fields.clear(); fields << "FOVName" << i18n("Select angular size for the FOV symbol (in arcmin)") << i18n("double"); new QTreeWidgetItem(opsSkymap, fields); fields.clear(); fields << "FOVShape" << i18n("Select shape for the FOV symbol (0=Square, 1=Circle, 2=Crosshairs, 4=Bullseye)") << i18n("int"); new QTreeWidgetItem(opsSkymap, fields); fields.clear(); fields << "FOVColor" << i18n("Select color for the FOV symbol") << i18n("string"); new QTreeWidgetItem(opsSkymap, fields); fields.clear(); fields << "UseAnimatedSlewing" << i18n("Use animated slewing? (otherwise, \"snap\" to new focus)") << i18n("bool"); new QTreeWidgetItem(opsSkymap, fields); fields.clear(); fields << "UseRefraction" << i18n("Correct for atmospheric refraction?") << i18n("bool"); new QTreeWidgetItem(opsSkymap, fields); fields.clear(); fields << "UseAutoLabel" << i18n("Automatically attach name label to centered object?") << i18n("bool"); new QTreeWidgetItem(opsSkymap, fields); fields.clear(); fields << "UseHoverLabel" << i18n("Attach temporary name label when hovering mouse over an object?") << i18n("bool"); new QTreeWidgetItem(opsSkymap, fields); fields.clear(); fields << "UseAutoTrail" << i18n("Automatically add trail to centered solar system body?") << i18n("bool"); new QTreeWidgetItem(opsSkymap, fields); fields.clear(); fields << "FadePlanetTrails" << i18n("Planet trails fade to sky color? (otherwise color is constant)") << i18n("bool"); new QTreeWidgetItem(opsSkymap, fields); fields.clear(); argChangeViewOption->OptionName->addItem("UseAltAz"); argChangeViewOption->OptionName->addItem("ZoomFactor"); argChangeViewOption->OptionName->addItem("FOVName"); argChangeViewOption->OptionName->addItem("FOVSize"); argChangeViewOption->OptionName->addItem("FOVShape"); argChangeViewOption->OptionName->addItem("FOVColor"); argChangeViewOption->OptionName->addItem("UseRefraction"); argChangeViewOption->OptionName->addItem("UseAutoLabel"); argChangeViewOption->OptionName->addItem("UseHoverLabel"); argChangeViewOption->OptionName->addItem("UseAutoTrail"); argChangeViewOption->OptionName->addItem("AnimateSlewing"); argChangeViewOption->OptionName->addItem("FadePlanetTrails"); opsLimit = new QTreeWidgetItem(otv->optionsList(), QStringList(i18n("Limits"))); /* fields << "magLimitDrawStar" << i18n( "magnitude of faintest star drawn on map when zoomed in" ) << i18n( "double" ); new QTreeWidgetItem( opsLimit, fields ); fields.clear(); fields << "magLimitDrawStarZoomOut" << i18n( "magnitude of faintest star drawn on map when zoomed out" ) << i18n( "double" ); new QTreeWidgetItem( opsLimit, fields ); fields.clear(); */ // TODO: We have disabled the following two features. Enable them when feasible... /* fields << "magLimitDrawDeepSky" << i18n( "magnitude of faintest nonstellar object drawn on map when zoomed in" ) << i18n( "double" ); new QTreeWidgetItem( opsLimit, fields ); fields.clear(); fields << "magLimitDrawDeepSkyZoomOut" << i18n( "magnitude of faintest nonstellar object drawn on map when zoomed out" ) << i18n( "double" ); new QTreeWidgetItem( opsLimit, fields ); fields.clear(); */ //FIXME: This description is incorrect! Fix after strings freeze fields << "starLabelDensity" << i18n("magnitude of faintest star labeled on map") << i18n("double"); new QTreeWidgetItem(opsLimit, fields); fields.clear(); fields << "magLimitHideStar" << i18n("magnitude of brightest star hidden while slewing") << i18n("double"); new QTreeWidgetItem(opsLimit, fields); fields.clear(); fields << "magLimitAsteroid" << i18n("magnitude of faintest asteroid drawn on map") << i18n("double"); new QTreeWidgetItem(opsLimit, fields); fields.clear(); //FIXME: This description is incorrect! Fix after strings freeze fields << "asteroidLabelDensity" << i18n("magnitude of faintest asteroid labeled on map") << i18n("double"); new QTreeWidgetItem(opsLimit, fields); fields.clear(); fields << "maxRadCometName" << i18n("comets nearer to the Sun than this (in AU) are labeled on map") << i18n("double"); new QTreeWidgetItem(opsLimit, fields); fields.clear(); // argChangeViewOption->OptionName->addItem( "magLimitDrawStar" ); // argChangeViewOption->OptionName->addItem( "magLimitDrawStarZoomOut" ); argChangeViewOption->OptionName->addItem("magLimitDrawDeepSky"); argChangeViewOption->OptionName->addItem("magLimitDrawDeepSkyZoomOut"); argChangeViewOption->OptionName->addItem("starLabelDensity"); argChangeViewOption->OptionName->addItem("magLimitHideStar"); argChangeViewOption->OptionName->addItem("magLimitAsteroid"); argChangeViewOption->OptionName->addItem("asteroidLabelDensity"); argChangeViewOption->OptionName->addItem("maxRadCometName"); //init the list of color names and values for (unsigned int i = 0; i < KStarsData::Instance()->colorScheme()->numberOfColors(); ++i) { argSetColor->ColorName->addItem(KStarsData::Instance()->colorScheme()->nameAt(i)); } //init list of color scheme names argLoadColorScheme->SchemeList->addItem(i18nc("use default color scheme", "Default Colors")); argLoadColorScheme->SchemeList->addItem(i18nc("use 'star chart' color scheme", "Star Chart")); argLoadColorScheme->SchemeList->addItem(i18nc("use 'night vision' color scheme", "Night Vision")); argLoadColorScheme->SchemeList->addItem(i18nc("use 'moonless night' color scheme", "Moonless Night")); QFile file; QString line; file.setFileName(KSPaths::locate(QStandardPaths::GenericDataLocation, "colors.dat")); //determine filename in local user KDE directory tree. if (file.open(QIODevice::ReadOnly)) { QTextStream stream(&file); while (!stream.atEnd()) { line = stream.readLine(); argLoadColorScheme->SchemeList->addItem(line.left(line.indexOf(':'))); } file.close(); } } //Slots defined in ScriptBuilderUI void ScriptBuilder::slotNew() { saveWarning(); if (!UnsavedChanges) { ScriptList.clear(); sb->ScriptListBox->clear(); sb->ArgStack->setCurrentWidget(argBlank); sb->CopyButton->setEnabled(false); sb->RemoveButton->setEnabled(false); sb->RunButton->setEnabled(false); sb->SaveAsButton->setEnabled(false); currentFileURL.clear(); currentScriptName.clear(); } } void ScriptBuilder::slotOpen() { saveWarning(); QString fname; QTemporaryFile tmpfile; tmpfile.open(); if (!UnsavedChanges) { currentFileURL = QFileDialog::getOpenFileUrl( KStars::Instance(), QString(), QUrl(currentDir), "*.kstars|" + i18nc("Filter by file type: KStars Scripts.", "KStars Scripts (*.kstars)")); if (currentFileURL.isValid()) { currentDir = currentFileURL.toLocalFile(); ScriptList.clear(); sb->ScriptListBox->clear(); sb->ArgStack->setCurrentWidget(argBlank); if (currentFileURL.isLocalFile()) { fname = currentFileURL.toLocalFile(); } else { fname = tmpfile.fileName(); if (KIO::copy(currentFileURL, QUrl(fname))->exec() == false) //if ( ! KIO::NetAccess::download( currentFileURL, fname, (QWidget*) 0 ) ) KSNotification::sorry(i18n("Could not download remote file."), i18n("Download Error")); } QFile f(fname); if (!f.open(QIODevice::ReadOnly)) { KSNotification::sorry(i18n("Could not open file %1.", f.fileName()), i18n("Could Not Open File")); currentFileURL.clear(); return; } QTextStream istream(&f); readScript(istream); f.close(); } else if (!currentFileURL.url().isEmpty()) { KSNotification::sorry(i18n("Invalid URL: %1", currentFileURL.url()), i18n("Invalid URL")); currentFileURL.clear(); } } } void ScriptBuilder::slotSave() { QString fname; QTemporaryFile tmpfile; tmpfile.open(); if (currentScriptName.isEmpty()) { //Get Script Name and Author info if (snd->exec() == QDialog::Accepted) { currentScriptName = snd->scriptName(); currentAuthor = snd->authorName(); } else { return; } } bool newFilename = false; if (currentFileURL.isEmpty()) { currentFileURL = QFileDialog::getSaveFileUrl( KStars::Instance(), QString(), QUrl(currentDir), "*.kstars|" + i18nc("Filter by file type: KStars Scripts.", "KStars Scripts (*.kstars)")); newFilename = true; } if (currentFileURL.isValid()) { currentDir = currentFileURL.toLocalFile(); if (currentFileURL.isLocalFile()) { fname = currentFileURL.toLocalFile(); //Warn user if file exists if (newFilename == true && QFile::exists(currentFileURL.toLocalFile())) { int r = KMessageBox::warningContinueCancel(static_cast(parent()), i18n("A file named \"%1\" already exists. " "Overwrite it?", currentFileURL.fileName()), i18n("Overwrite File?"), KStandardGuiItem::overwrite()); if (r == KMessageBox::Cancel) return; } } else { fname = tmpfile.fileName(); } if (fname.right(7).toLower() != ".kstars") fname += ".kstars"; QFile f(fname); if (!f.open(QIODevice::WriteOnly)) { QString message = i18n("Could not open file %1.", f.fileName()); KSNotification::sorry(message, i18n("Could Not Open File")); currentFileURL.clear(); return; } QTextStream ostream(&f); writeScript(ostream); f.close(); #ifndef _WIN32 //set rwx for owner, rx for group, rx for other chmod(fname.toLatin1(), S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); #endif if (tmpfile.fileName() == fname) { //need to upload to remote location //if ( ! KIO::NetAccess::upload( tmpfile.fileName(), currentFileURL, (QWidget*) 0 ) ) if (KIO::storedHttpPost(&tmpfile, currentFileURL)->exec() == false) { QString message = i18n("Could not upload image to remote location: %1", currentFileURL.url()); KSNotification::sorry(message, i18n("Could not upload file")); } } setUnsavedChanges(false); } else { QString message = i18n("Invalid URL: %1", currentFileURL.url()); KSNotification::sorry(message, i18n("Invalid URL")); currentFileURL.clear(); } } void ScriptBuilder::slotSaveAs() { currentFileURL.clear(); currentScriptName.clear(); slotSave(); } void ScriptBuilder::saveWarning() { if (UnsavedChanges) { QString caption = i18n("Save Changes to Script?"); QString message = i18n("The current script has unsaved changes. Would you like to save before closing it?"); int ans = KMessageBox::warningYesNoCancel(nullptr, message, caption, KStandardGuiItem::save(), KStandardGuiItem::discard()); if (ans == KMessageBox::Yes) { slotSave(); setUnsavedChanges(false); } else if (ans == KMessageBox::No) { setUnsavedChanges(false); } //Do nothing if 'cancel' selected } } void ScriptBuilder::slotRunScript() { //hide window while script runs // If this is uncommented, the program hangs before the script is executed. Why? // hide(); //Save current script to a temporary file, then execute that file. //For some reason, I can't use KTempFile here! If I do, then the temporary script //is not executable. Bizarre... //KTempFile tmpfile; //QString fname = tmpfile.name(); QString fname = QDir::tempPath() + QDir::separator() + "kstars-tempscript"; QFile f(fname); if (f.exists()) f.remove(); if (!f.open(QIODevice::WriteOnly)) { QString message = i18n("Could not open file %1.", f.fileName()); KSNotification::sorry(message, i18n("Could Not Open File")); currentFileURL.clear(); return; } QTextStream ostream(&f); writeScript(ostream); f.close(); #ifndef _WIN32 //set rwx for owner, rx for group, rx for other chmod(QFile::encodeName(f.fileName()), S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); #endif QProcess p; #ifdef Q_OS_OSX QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); QString path = env.value("PATH", ""); env.insert("PATH", "/usr/local/bin:" + QCoreApplication::applicationDirPath() + ':' + path); p.setProcessEnvironment(env); #endif p.start(f.fileName()); if (!p.waitForStarted()) qDebug() << "Process did not start."; while (!p.waitForFinished(10)) { qApp->processEvents(); //otherwise tempfile may get deleted before script completes. if (p.state() != QProcess::Running) break; } //delete temp file if (f.exists()) f.remove(); //uncomment if 'hide()' is uncommented... // show(); } /* This can't work anymore and is also not protable in any way :( */ void ScriptBuilder::writeScript(QTextStream &ostream) { // FIXME Without --print-reply, the dbus-send doesn't do anything, why?? QString dbus_call = "dbus-send --dest=org.kde.kstars --print-reply "; QString main_method = "/KStars org.kde.kstars."; QString clock_method = "/KStars/SimClock org.kde.kstars.SimClock."; //Write script header ostream << "#!/bin/bash" << endl; ostream << "#KStars DBus script: " << currentScriptName << endl; ostream << "#by " << currentAuthor << endl; ostream << "#last modified: " << KStarsDateTime::currentDateTime().toString(Qt::ISODate) << endl; ostream << "#" << endl; foreach (ScriptFunction *sf, ScriptList) { if (!sf->valid()) continue; if (sf->isClockFunction()) { ostream << dbus_call << clock_method << sf->scriptLine() << endl; } else { ostream << dbus_call << main_method << sf->scriptLine() << endl; } } //Write script footer ostream << "##" << endl; } void ScriptBuilder::readScript(QTextStream &istream) { QString line; QString service_name = "org.kde.kstars."; QString fn_name; while (!istream.atEnd()) { line = istream.readLine(); //look for name of script if (line.contains("#KStars DBus script: ")) currentScriptName = line.mid(21).trimmed(); //look for author of scriptbuilder if (line.contains("#by ")) currentAuthor = line.mid(4).trimmed(); //Actual script functions if (line.left(4) == "dbus") { //is ClockFunction? if (line.contains("SimClock")) { service_name += "SimClock."; } //remove leading dbus prefix line = line.mid(line.lastIndexOf(service_name) + service_name.count()); fn_name = line.left(line.indexOf(' ')); line = line.mid(line.indexOf(' ') + 1); //construct a stringlist that is fcn name and its arg name/value pairs QStringList fn; // If the function lacks any arguments, do not attempt to split if (fn_name != line) fn = line.split(' '); if (parseFunction(fn_name, fn)) { sb->ScriptListBox->addItem(ScriptList.last()->name()); // Initially, any read script is valid! ScriptList.last()->setValid(true); } else qWarning() << i18n("Could not parse script. Line was: %1", line); } // end if left(4) == "dcop" } // end while !atEnd() //Select first item in sb->ScriptListBox if (sb->ScriptListBox->count()) { sb->ScriptListBox->setCurrentItem(nullptr); slotArgWidget(); } } bool ScriptBuilder::parseFunction(QString fn_name, QStringList &fn) { // clean up the string list first if needed // We need to perform this in case we havea quoted string "NGC 3000" because this will counted // as two arguments, and it should be counted as one. bool foundQuote(false), quoteProcessed(false); QString cur, arg; QStringList::iterator it; for (it = fn.begin(); it != fn.end(); ++it) { cur = (*it); cur = cur.mid(cur.indexOf(":") + 1).remove('\''); (*it) = cur; if (cur.startsWith('\"')) { arg += cur.rightRef(cur.length() - 1); arg += ' '; foundQuote = true; quoteProcessed = true; } else if (cur.endsWith('\"')) { arg += cur.leftRef(cur.length() - 1); arg += '\''; foundQuote = false; } else if (foundQuote) { arg += cur; arg += ' '; } else { arg += cur; arg += '\''; } } if (quoteProcessed) fn = arg.split('\'', QString::SkipEmptyParts); //loop over known functions to find a name match foreach (ScriptFunction *sf, KStarsFunctionList) { if (fn_name == sf->name()) { if (fn_name == "setGeoLocation") { QString city(fn[0]), prov, cntry(fn[1]); prov.clear(); if (fn.count() == 4) { prov = fn[1]; cntry = fn[2]; } if (fn.count() == 3 || fn.count() == 4) { ScriptList.append(new ScriptFunction(sf)); ScriptList.last()->setArg(0, city); ScriptList.last()->setArg(1, prov); ScriptList.last()->setArg(2, cntry); } else return false; } else if (fn.count() != sf->numArgs()) return false; ScriptList.append(new ScriptFunction(sf)); for (int i = 0; i < sf->numArgs(); ++i) ScriptList.last()->setArg(i, fn[i]); return true; } foreach (ScriptFunction *sf, SimClockFunctionList) { if (fn_name == sf->name()) { if (fn.count() != sf->numArgs()) return false; ScriptList.append(new ScriptFunction(sf)); for (int i = 0; i < sf->numArgs(); ++i) ScriptList.last()->setArg(i, fn[i]); return true; } } } //if we get here, no function-name match was found return false; } void ScriptBuilder::setUnsavedChanges(bool b) { if (checkForChanges) { UnsavedChanges = b; sb->SaveButton->setEnabled(b); } } void ScriptBuilder::slotCopyFunction() { if (!UnsavedChanges) setUnsavedChanges(true); int Pos = sb->ScriptListBox->currentRow() + 1; ScriptList.insert(Pos, new ScriptFunction(ScriptList[Pos - 1])); //copy ArgVals for (int i = 0; i < ScriptList[Pos - 1]->numArgs(); ++i) ScriptList[Pos]->setArg(i, ScriptList[Pos - 1]->argVal(i)); sb->ScriptListBox->insertItem(Pos, ScriptList[Pos]->name()); //sb->ScriptListBox->setSelected( Pos, true ); sb->ScriptListBox->setCurrentRow(Pos); slotArgWidget(); } void ScriptBuilder::slotRemoveFunction() { setUnsavedChanges(true); int Pos = sb->ScriptListBox->currentRow(); ScriptList.removeAt(Pos); sb->ScriptListBox->takeItem(Pos); if (sb->ScriptListBox->count() == 0) { sb->ArgStack->setCurrentWidget(argBlank); sb->CopyButton->setEnabled(false); sb->RemoveButton->setEnabled(false); sb->RunButton->setEnabled(false); sb->SaveAsButton->setEnabled(false); } else { //sb->ScriptListBox->setSelected( Pos, true ); if (Pos == sb->ScriptListBox->count()) { Pos = Pos - 1; } sb->ScriptListBox->setCurrentRow(Pos); } slotArgWidget(); } void ScriptBuilder::slotAddFunction() { ScriptFunction *found = nullptr; QTreeWidgetItem *currentItem = sb->FunctionTree->currentItem(); if (currentItem == nullptr || currentItem->parent() == nullptr) return; for (auto &sc : KStarsFunctionList) { if (sc->prototype() == currentItem->text(0)) { found = sc; break; } } for (auto &sc : SimClockFunctionList) { if (sc->prototype() == currentItem->text(0)) { found = sc; break; } } if (found == nullptr) return; setUnsavedChanges(true); int Pos = sb->ScriptListBox->currentRow() + 1; ScriptList.insert(Pos, new ScriptFunction(found)); sb->ScriptListBox->insertItem(Pos, ScriptList[Pos]->name()); sb->ScriptListBox->setCurrentRow(Pos); slotArgWidget(); } void ScriptBuilder::slotMoveFunctionUp() { if (sb->ScriptListBox->currentRow() > 0) { setUnsavedChanges(true); //QString t = sb->ScriptListBox->currentItem()->text(); QString t = sb->ScriptListBox->currentItem()->text(); unsigned int n = sb->ScriptListBox->currentRow(); ScriptFunction *tmp = ScriptList.takeAt(n); ScriptList.insert(n - 1, tmp); sb->ScriptListBox->takeItem(n); sb->ScriptListBox->insertItem(n - 1, t); sb->ScriptListBox->setCurrentRow(n - 1); slotArgWidget(); } } void ScriptBuilder::slotMoveFunctionDown() { if (sb->ScriptListBox->currentRow() > -1 && sb->ScriptListBox->currentRow() < ((int)sb->ScriptListBox->count()) - 1) { setUnsavedChanges(true); QString t = sb->ScriptListBox->currentItem()->text(); unsigned int n = sb->ScriptListBox->currentRow(); ScriptFunction *tmp = ScriptList.takeAt(n); ScriptList.insert(n + 1, tmp); sb->ScriptListBox->takeItem(n); sb->ScriptListBox->insertItem(n + 1, t); sb->ScriptListBox->setCurrentRow(n + 1); slotArgWidget(); } } void ScriptBuilder::slotArgWidget() { //First, setEnabled on buttons that act on the selected script function if (sb->ScriptListBox->currentRow() == -1) //no selection { sb->CopyButton->setEnabled(false); sb->RemoveButton->setEnabled(false); sb->UpButton->setEnabled(false); sb->DownButton->setEnabled(false); } else if (sb->ScriptListBox->count() == 1) //only one item, so disable up/down buttons { sb->CopyButton->setEnabled(true); sb->RemoveButton->setEnabled(true); sb->UpButton->setEnabled(false); sb->DownButton->setEnabled(false); } else if (sb->ScriptListBox->currentRow() == 0) //first item selected { sb->CopyButton->setEnabled(true); sb->RemoveButton->setEnabled(true); sb->UpButton->setEnabled(false); sb->DownButton->setEnabled(true); } else if (sb->ScriptListBox->currentRow() == ((int)sb->ScriptListBox->count()) - 1) //last item selected { sb->CopyButton->setEnabled(true); sb->RemoveButton->setEnabled(true); sb->UpButton->setEnabled(true); sb->DownButton->setEnabled(false); } else //other item selected { sb->CopyButton->setEnabled(true); sb->RemoveButton->setEnabled(true); sb->UpButton->setEnabled(true); sb->DownButton->setEnabled(true); } //RunButton and SaveAs button enabled when script not empty. if (sb->ScriptListBox->count()) { sb->RunButton->setEnabled(true); sb->SaveAsButton->setEnabled(true); } else { sb->RunButton->setEnabled(false); sb->SaveAsButton->setEnabled(false); setUnsavedChanges(false); } //Display the function's arguments widget if (sb->ScriptListBox->currentRow() > -1 && sb->ScriptListBox->currentRow() < ((int)sb->ScriptListBox->count())) { unsigned int n = sb->ScriptListBox->currentRow(); ScriptFunction *sf = ScriptList.at(n); checkForChanges = false; //Don't signal unsaved changes if (sf->name() == "lookTowards") { sb->ArgStack->setCurrentWidget(argLookToward); QString s = sf->argVal(0); argLookToward->FocusEdit->setEditText(s); } else if (sf->name() == "addLabel" || sf->name() == "removeLabel" || sf->name() == "addTrail" || sf->name() == "removeTrail") { sb->ArgStack->setCurrentWidget(argFindObject); QString s = sf->argVal(0); argFindObject->NameEdit->setText(s); } else if (sf->name() == "setRaDec") { bool ok(false); double r(0.0), d(0.0); dms ra(0.0); sb->ArgStack->setCurrentWidget(argSetRaDec); ok = !sf->argVal(0).isEmpty(); if (ok) r = sf->argVal(0).toDouble(&ok); else argSetRaDec->RABox->clear(); if (ok) { ra.setH(r); argSetRaDec->RABox->showInHours(ra); } ok = !sf->argVal(1).isEmpty(); if (ok) d = sf->argVal(1).toDouble(&ok); else argSetRaDec->DecBox->clear(); if (ok) argSetRaDec->DecBox->showInDegrees(dms(d)); } else if (sf->name() == "setAltAz") { bool ok(false); double x(0.0), y(0.0); sb->ArgStack->setCurrentWidget(argSetAltAz); ok = !sf->argVal(0).isEmpty(); if (ok) y = sf->argVal(0).toDouble(&ok); else argSetAltAz->AzBox->clear(); if (ok) argSetAltAz->AltBox->showInDegrees(dms(y)); else argSetAltAz->AltBox->clear(); ok = !sf->argVal(1).isEmpty(); x = sf->argVal(1).toDouble(&ok); if (ok) argSetAltAz->AzBox->showInDegrees(dms(x)); } else if (sf->name() == "zoomIn") { sb->ArgStack->setCurrentWidget(argBlank); //no Args } else if (sf->name() == "zoomOut") { sb->ArgStack->setCurrentWidget(argBlank); //no Args } else if (sf->name() == "defaultZoom") { sb->ArgStack->setCurrentWidget(argBlank); //no Args } else if (sf->name() == "zoom") { sb->ArgStack->setCurrentWidget(argZoom); bool ok(false); /*double z = */ sf->argVal(0).toDouble(&ok); if (ok) argZoom->ZoomBox->setText(sf->argVal(0)); else argZoom->ZoomBox->setText("2000."); } else if (sf->name() == "exportImage") { sb->ArgStack->setCurrentWidget(argExportImage); argExportImage->ExportFileName->setUrl(QUrl::fromUserInput(sf->argVal(0))); bool ok(false); int w = 0, h = 0; w = sf->argVal(1).toInt(&ok); if (ok) h = sf->argVal(2).toInt(&ok); if (ok) { argExportImage->ExportWidth->setValue(w); argExportImage->ExportHeight->setValue(h); } else { argExportImage->ExportWidth->setValue(SkyMap::Instance()->width()); argExportImage->ExportHeight->setValue(SkyMap::Instance()->height()); } } else if (sf->name() == "printImage") { if (sf->argVal(0) == i18n("true")) argPrintImage->UsePrintDialog->setChecked(true); else argPrintImage->UsePrintDialog->setChecked(false); if (sf->argVal(1) == i18n("true")) argPrintImage->UseChartColors->setChecked(true); else argPrintImage->UseChartColors->setChecked(false); } else if (sf->name() == "setLocalTime") { sb->ArgStack->setCurrentWidget(argSetLocalTime); bool ok(false); int year = 0, month = 0, day = 0, hour = 0, min = 0, sec = 0; year = sf->argVal(0).toInt(&ok); if (ok) month = sf->argVal(1).toInt(&ok); if (ok) day = sf->argVal(2).toInt(&ok); if (ok) argSetLocalTime->DateWidget->setDate(QDate(year, month, day)); else argSetLocalTime->DateWidget->setDate(QDate::currentDate()); hour = sf->argVal(3).toInt(&ok); if (sf->argVal(3).isEmpty()) ok = false; if (ok) min = sf->argVal(4).toInt(&ok); if (ok) sec = sf->argVal(5).toInt(&ok); if (ok) argSetLocalTime->TimeBox->setTime(QTime(hour, min, sec)); else argSetLocalTime->TimeBox->setTime(QTime(QTime::currentTime())); } else if (sf->name() == "waitFor") { sb->ArgStack->setCurrentWidget(argWaitFor); bool ok(false); int sec = sf->argVal(0).toInt(&ok); if (ok) argWaitFor->DelayBox->setValue(sec); else argWaitFor->DelayBox->setValue(0); } else if (sf->name() == "waitForKey") { sb->ArgStack->setCurrentWidget(argWaitForKey); if (sf->argVal(0).length() == 1 || sf->argVal(0).toLower() == "space") argWaitForKey->WaitKeyEdit->setText(sf->argVal(0)); else argWaitForKey->WaitKeyEdit->setText(QString()); } else if (sf->name() == "setTracking") { sb->ArgStack->setCurrentWidget(argSetTracking); if (sf->argVal(0) == i18n("true")) argSetTracking->CheckTrack->setChecked(true); else argSetTracking->CheckTrack->setChecked(false); } else if (sf->name() == "changeViewOption") { sb->ArgStack->setCurrentWidget(argChangeViewOption); argChangeViewOption->OptionName->setCurrentIndex(argChangeViewOption->OptionName->findText(sf->argVal(0))); argChangeViewOption->OptionValue->setText(sf->argVal(1)); } else if (sf->name() == "setGeoLocation") { sb->ArgStack->setCurrentWidget(argSetGeoLocation); argSetGeoLocation->CityName->setText(sf->argVal(0)); argSetGeoLocation->ProvinceName->setText(sf->argVal(1)); argSetGeoLocation->CountryName->setText(sf->argVal(2)); } else if (sf->name() == "setColor") { sb->ArgStack->setCurrentWidget(argSetColor); if (sf->argVal(0).isEmpty()) sf->setArg(0, "SkyColor"); //initialize default value argSetColor->ColorName->setCurrentIndex( argSetColor->ColorName->findText(KStarsData::Instance()->colorScheme()->nameFromKey(sf->argVal(0)))); argSetColor->ColorValue->setColor(QColor(sf->argVal(1).remove('\\'))); } else if (sf->name() == "loadColorScheme") { sb->ArgStack->setCurrentWidget(argLoadColorScheme); argLoadColorScheme->SchemeList->setCurrentItem( argLoadColorScheme->SchemeList->findItems(sf->argVal(0).remove('\"'), Qt::MatchExactly).at(0)); } else if (sf->name() == "stop") { sb->ArgStack->setCurrentWidget(argBlank); //no Args } else if (sf->name() == "start") { sb->ArgStack->setCurrentWidget(argBlank); //no Args } else if (sf->name() == "setClockScale") { sb->ArgStack->setCurrentWidget(argTimeScale); bool ok(false); double ts = sf->argVal(0).toDouble(&ok); if (ok) argTimeScale->TimeScale->tsbox()->changeScale(float(ts)); else argTimeScale->TimeScale->tsbox()->changeScale(0.0); } checkForChanges = true; //signal unsaved changes if the argument widgets are changed } } void ScriptBuilder::slotShowDoc() { ScriptFunction *found = nullptr; QTreeWidgetItem *currentItem = sb->FunctionTree->currentItem(); if (currentItem == nullptr || currentItem->parent() == nullptr) return; for (auto &sc : KStarsFunctionList) { if (sc->prototype() == currentItem->text(0)) { found = sc; break; } } for (auto &sc : SimClockFunctionList) { if (sc->prototype() == currentItem->text(0)) { found = sc; break; } } if (found == nullptr) { sb->AddButton->setEnabled(false); qWarning() << i18n("Function index out of bounds."); return; } sb->AddButton->setEnabled(true); sb->FuncDoc->setHtml(found->description()); } //Slots for Arg Widgets void ScriptBuilder::slotFindCity() { QPointer ld = new LocationDialog(this); if (ld->exec() == QDialog::Accepted) { if (ld->selectedCity()) { // set new location names argSetGeoLocation->CityName->setText(ld->selectedCityName()); if (!ld->selectedProvinceName().isEmpty()) { argSetGeoLocation->ProvinceName->setText(ld->selectedProvinceName()); } else { argSetGeoLocation->ProvinceName->clear(); } argSetGeoLocation->CountryName->setText(ld->selectedCountryName()); ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "setGeoLocation") { setUnsavedChanges(true); sf->setArg(0, ld->selectedCityName()); sf->setArg(1, ld->selectedProvinceName()); sf->setArg(2, ld->selectedCountryName()); } else { warningMismatch("setGeoLocation"); } } } delete ld; } void ScriptBuilder::slotFindObject() { if (FindDialog::Instance()->exec() == QDialog::Accepted && FindDialog::Instance()->targetObject()) { setUnsavedChanges(true); if (sender() == argLookToward->FindButton) argLookToward->FocusEdit->setEditText(FindDialog::Instance()->targetObject()->name()); else argFindObject->NameEdit->setText(FindDialog::Instance()->targetObject()->name()); } } void ScriptBuilder::slotShowOptions() { //Show tree-view of view options if (otv->exec() == QDialog::Accepted) { argChangeViewOption->OptionName->setCurrentIndex( argChangeViewOption->OptionName->findText(otv->optionsList()->currentItem()->text(0))); } } void ScriptBuilder::slotLookToward() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "lookTowards") { setUnsavedChanges(true); sf->setArg(0, argLookToward->FocusEdit->currentText()); sf->setValid(true); } else { warningMismatch("lookTowards"); } } void ScriptBuilder::slotArgFindObject() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "addLabel" || sf->name() == "removeLabel" || sf->name() == "addTrail" || sf->name() == "removeTrail") { setUnsavedChanges(true); sf->setArg(0, argFindObject->NameEdit->text()); sf->setValid(true); } else { warningMismatch(sf->name()); } } void ScriptBuilder::slotRa() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "setRaDec") { //do nothing if box is blank (because we could be clearing boxes while switching argWidgets) if (argSetRaDec->RABox->text().isEmpty()) return; bool ok(false); dms ra = argSetRaDec->RABox->createDms(false, &ok); if (ok) { setUnsavedChanges(true); sf->setArg(0, QString("%1").arg(ra.Hours())); if (!sf->argVal(1).isEmpty()) sf->setValid(true); } else { sf->setArg(0, QString()); sf->setValid(false); } } else { warningMismatch("setRaDec"); } } void ScriptBuilder::slotDec() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "setRaDec") { //do nothing if box is blank (because we could be clearing boxes while switching argWidgets) if (argSetRaDec->DecBox->text().isEmpty()) return; bool ok(false); dms dec = argSetRaDec->DecBox->createDms(true, &ok); if (ok) { setUnsavedChanges(true); sf->setArg(1, QString("%1").arg(dec.Degrees())); if (!sf->argVal(0).isEmpty()) sf->setValid(true); } else { sf->setArg(1, QString()); sf->setValid(false); } } else { warningMismatch("setRaDec"); } } void ScriptBuilder::slotAz() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "setAltAz") { //do nothing if box is blank (because we could be clearing boxes while switching argWidgets) if (argSetAltAz->AzBox->text().isEmpty()) return; bool ok(false); dms az = argSetAltAz->AzBox->createDms(true, &ok); if (ok) { setUnsavedChanges(true); sf->setArg(1, QString("%1").arg(az.Degrees())); if (!sf->argVal(0).isEmpty()) sf->setValid(true); } else { sf->setArg(1, QString()); sf->setValid(false); } } else { warningMismatch("setAltAz"); } } void ScriptBuilder::slotAlt() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "setAltAz") { //do nothing if box is blank (because we could be clearing boxes while switching argWidgets) if (argSetAltAz->AltBox->text().isEmpty()) return; bool ok(false); dms alt = argSetAltAz->AltBox->createDms(true, &ok); if (ok) { setUnsavedChanges(true); sf->setArg(0, QString("%1").arg(alt.Degrees())); if (!sf->argVal(1).isEmpty()) sf->setValid(true); } else { sf->setArg(0, QString()); sf->setValid(false); } } else { warningMismatch("setAltAz"); } } void ScriptBuilder::slotChangeDate() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "setLocalTime") { setUnsavedChanges(true); QDate date = argSetLocalTime->DateWidget->date(); sf->setArg(0, QString("%1").arg(date.year())); sf->setArg(1, QString("%1").arg(date.month())); sf->setArg(2, QString("%1").arg(date.day())); if (!sf->argVal(3).isEmpty()) sf->setValid(true); } else { warningMismatch("setLocalTime"); } } void ScriptBuilder::slotChangeTime() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "setLocalTime") { setUnsavedChanges(true); QTime time = argSetLocalTime->TimeBox->time(); sf->setArg(3, QString("%1").arg(time.hour())); sf->setArg(4, QString("%1").arg(time.minute())); sf->setArg(5, QString("%1").arg(time.second())); if (!sf->argVal(0).isEmpty()) sf->setValid(true); } else { warningMismatch("setLocalTime"); } } void ScriptBuilder::slotWaitFor() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "waitFor") { bool ok(false); int delay = argWaitFor->DelayBox->text().toInt(&ok); if (ok) { setUnsavedChanges(true); sf->setArg(0, QString("%1").arg(delay)); sf->setValid(true); } else { sf->setValid(false); } } else { warningMismatch("waitFor"); } } void ScriptBuilder::slotWaitForKey() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "waitForKey") { QString sKey = argWaitForKey->WaitKeyEdit->text().trimmed(); //DCOP script can only use single keystrokes; make sure entry is either one character, //or the word 'space' if (sKey.length() == 1 || sKey == "space") { setUnsavedChanges(true); sf->setArg(0, sKey); sf->setValid(true); } else { sf->setValid(false); } } else { warningMismatch("waitForKey"); } } void ScriptBuilder::slotTracking() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "setTracking") { setUnsavedChanges(true); sf->setArg(0, (argSetTracking->CheckTrack->isChecked() ? i18n("true") : i18n("false"))); sf->setValid(true); } else { warningMismatch("setTracking"); } } void ScriptBuilder::slotViewOption() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "changeViewOption") { if (argChangeViewOption->OptionName->currentIndex() >= 0 && argChangeViewOption->OptionValue->text().length()) { setUnsavedChanges(true); sf->setArg(0, argChangeViewOption->OptionName->currentText()); sf->setArg(1, argChangeViewOption->OptionValue->text()); sf->setValid(true); } else { sf->setValid(false); } } else { warningMismatch("changeViewOption"); } } void ScriptBuilder::slotChangeCity() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "setGeoLocation") { QString city = argSetGeoLocation->CityName->text(); if (city.length()) { setUnsavedChanges(true); sf->setArg(0, city); if (sf->argVal(2).length()) sf->setValid(true); } else { sf->setArg(0, QString()); sf->setValid(false); } } else { warningMismatch("setGeoLocation"); } } void ScriptBuilder::slotChangeProvince() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "setGeoLocation") { QString province = argSetGeoLocation->ProvinceName->text(); if (province.length()) { setUnsavedChanges(true); sf->setArg(1, province); if (sf->argVal(0).length() && sf->argVal(2).length()) sf->setValid(true); } else { sf->setArg(1, QString()); //might not be invalid } } else { warningMismatch("setGeoLocation"); } } void ScriptBuilder::slotChangeCountry() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "setGeoLocation") { QString country = argSetGeoLocation->CountryName->text(); if (country.length()) { setUnsavedChanges(true); sf->setArg(2, country); if (sf->argVal(0).length()) sf->setValid(true); } else { sf->setArg(2, QString()); sf->setValid(false); } } else { warningMismatch("setGeoLocation"); } } void ScriptBuilder::slotTimeScale() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "setClockScale") { setUnsavedChanges(true); sf->setArg(0, QString("%1").arg(argTimeScale->TimeScale->tsbox()->timeScale())); sf->setValid(true); } else { warningMismatch("setClockScale"); } } void ScriptBuilder::slotZoom() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "zoom") { setUnsavedChanges(true); bool ok(false); argZoom->ZoomBox->text().toDouble(&ok); if (ok) { sf->setArg(0, argZoom->ZoomBox->text()); sf->setValid(true); } } else { warningMismatch("zoom"); } } void ScriptBuilder::slotExportImage() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "exportImage") { setUnsavedChanges(true); sf->setArg(0, argExportImage->ExportFileName->url().url()); sf->setArg(1, QString("%1").arg(argExportImage->ExportWidth->value())); sf->setArg(2, QString("%1").arg(argExportImage->ExportHeight->value())); sf->setValid(true); } else { warningMismatch("exportImage"); } } void ScriptBuilder::slotPrintImage() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "printImage") { setUnsavedChanges(true); sf->setArg(0, (argPrintImage->UsePrintDialog->isChecked() ? i18n("true") : i18n("false"))); sf->setArg(1, (argPrintImage->UseChartColors->isChecked() ? i18n("true") : i18n("false"))); sf->setValid(true); } else { warningMismatch("exportImage"); } } void ScriptBuilder::slotChangeColorName() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "setColor") { setUnsavedChanges(true); argSetColor->ColorValue->setColor(KStarsData::Instance()->colorScheme()->colorAt(argSetColor->ColorName->currentIndex())); sf->setArg(0, KStarsData::Instance()->colorScheme()->keyAt(argSetColor->ColorName->currentIndex())); QString cname(argSetColor->ColorValue->color().name()); //if ( cname.at(0) == '#' ) cname = "\\" + cname; //prepend a "\" so bash doesn't think we have a comment sf->setArg(1, cname); sf->setValid(true); } else { warningMismatch("setColor"); } } void ScriptBuilder::slotChangeColor() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "setColor") { setUnsavedChanges(true); sf->setArg(0, KStarsData::Instance()->colorScheme()->keyAt(argSetColor->ColorName->currentIndex())); QString cname(argSetColor->ColorValue->color().name()); //if ( cname.at(0) == '#' ) cname = "\\" + cname; //prepend a "\" so bash doesn't think we have a comment sf->setArg(1, cname); sf->setValid(true); } else { warningMismatch("setColor"); } } void ScriptBuilder::slotLoadColorScheme() { ScriptFunction *sf = ScriptList[sb->ScriptListBox->currentRow()]; if (sf->name() == "loadColorScheme") { setUnsavedChanges(true); sf->setArg(0, '\"' + argLoadColorScheme->SchemeList->currentItem()->text() + '\"'); sf->setValid(true); } else { warningMismatch("loadColorScheme"); } } void ScriptBuilder::slotClose() { saveWarning(); if (!UnsavedChanges) { ScriptList.clear(); sb->ScriptListBox->clear(); sb->ArgStack->setCurrentWidget(argBlank); close(); } } //TODO JM: INDI Scripting to be included in KDE 4.1 #if 0 void ScriptBuilder::slotINDIStartDeviceName() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "startINDI" ) { setUnsavedChanges( true ); sf->setArg(0, argStartINDI->deviceName->text()); sf->setArg(1, argStartINDI->LocalButton->isChecked() ? "true" : "false"); sf->setValid(true); } else { warningMismatch( "startINDI" ); } } void ScriptBuilder::slotINDIStartDeviceMode() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "startINDI" ) { setUnsavedChanges( true ); sf->setArg(1, argStartINDI->LocalButton->isChecked() ? "true" : "false"); sf->setValid(true); } else { warningMismatch( "startINDI" ); } } void ScriptBuilder::slotINDISetDevice() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "setINDIDevice" ) { setUnsavedChanges( true ); sf->setArg(0, argSetDeviceINDI->deviceName->text()); sf->setValid(true); } else { warningMismatch( "startINDI" ); } } void ScriptBuilder::slotINDIShutdown() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "shutdownINDI" ) { if (argShutdownINDI->deviceName->text().isEmpty()) { sf->setValid(false); return; } if (sf->argVal(0) != argShutdownINDI->deviceName->text()) setUnsavedChanges( true ); sf->setArg(0, argShutdownINDI->deviceName->text()); sf->setValid(true); } else { warningMismatch( "shutdownINDI" ); } } void ScriptBuilder::slotINDISwitchDeviceConnection() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "switchINDI" ) { if (sf->argVal(0) != (argSwitchINDI->OnButton->isChecked() ? "true" : "false")) setUnsavedChanges( true ); sf->setArg(0, argSwitchINDI->OnButton->isChecked() ? "true" : "false"); sf->setValid(true); } else { warningMismatch( "switchINDI" ); } } void ScriptBuilder::slotINDISetPortDevicePort() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "setINDIPort" ) { if (argSetPortINDI->devicePort->text().isEmpty()) { sf->setValid(false); return; } if (sf->argVal(0) != argSetPortINDI->devicePort->text()) setUnsavedChanges( true ); sf->setArg(0, argSetPortINDI->devicePort->text()); sf->setValid(true); } else { warningMismatch( "setINDIPort" ); } } void ScriptBuilder::slotINDISetTargetCoordDeviceRA() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "setINDITargetCoord" ) { //do nothing if box is blank (because we could be clearing boxes while switching argWidgets) if ( argSetTargetCoordINDI->RABox->text().isEmpty() ) { sf->setValid(false); return; } bool ok(false); dms ra = argSetTargetCoordINDI->RABox->createDms(false, &ok); if ( ok ) { if (sf->argVal(0) != QString( "%1" ).arg( ra.Hours() )) setUnsavedChanges( true ); sf->setArg( 0, QString( "%1" ).arg( ra.Hours() ) ); if ( ( ! sf->argVal(1).isEmpty() )) sf->setValid( true ); else sf->setValid(false); } else { sf->setArg( 0, QString() ); sf->setValid( false ); } } else { warningMismatch( "setINDITargetCoord" ); } } void ScriptBuilder::slotINDISetTargetCoordDeviceDEC() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "setINDITargetCoord" ) { //do nothing if box is blank (because we could be clearing boxes while switching argWidgets) if ( argSetTargetCoordINDI->DecBox->text().isEmpty() ) { sf->setValid(false); return; } bool ok(false); dms dec = argSetTargetCoordINDI->DecBox->createDms(true, &ok); if ( ok ) { if (sf->argVal(1) != QString( "%1" ).arg( dec.Degrees() )) setUnsavedChanges( true ); sf->setArg( 1, QString( "%1" ).arg( dec.Degrees() ) ); if ( ( ! sf->argVal(0).isEmpty() )) sf->setValid( true ); else sf->setValid(false); } else { sf->setArg( 1, QString() ); sf->setValid( false ); } } else { warningMismatch( "setINDITargetCoord" ); } } void ScriptBuilder::slotINDISetTargetNameTargetName() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "setINDITargetName" ) { if (argSetTargetNameINDI->targetName->text().isEmpty()) { sf->setValid(false); return; } if (sf->argVal(0) != argSetTargetNameINDI->targetName->text()) setUnsavedChanges( true ); sf->setArg(0, argSetTargetNameINDI->targetName->text()); sf->setValid(true); } else { warningMismatch( "setINDITargetName" ); } } void ScriptBuilder::slotINDISetActionName() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "setINDIAction" ) { if (argSetActionINDI->actionName->text().isEmpty()) { sf->setValid(false); return; } if (sf->argVal(0) != argSetActionINDI->actionName->text()) setUnsavedChanges( true ); sf->setArg(0, argSetActionINDI->actionName->text()); sf->setValid(true); } else { warningMismatch( "setINDIAction" ); } } void ScriptBuilder::slotINDIWaitForActionName() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "waitForINDIAction" ) { if (argWaitForActionINDI->actionName->text().isEmpty()) { sf->setValid(false); return; } if (sf->argVal(0) != argWaitForActionINDI->actionName->text()) setUnsavedChanges( true ); sf->setArg(0, argWaitForActionINDI->actionName->text()); sf->setValid(true); } else { warningMismatch( "waitForINDIAction" ); } } void ScriptBuilder::slotINDISetFocusSpeed() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "setINDIFocusSpeed" ) { if (sf->argVal(0).toInt() != argSetFocusSpeedINDI->speedIN->value()) setUnsavedChanges( true ); sf->setArg(0, QString("%1").arg(argSetFocusSpeedINDI->speedIN->value())); sf->setValid(true); } else { warningMismatch( "setINDIFocusSpeed" ); } } void ScriptBuilder::slotINDIStartFocusDirection() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "startINDIFocus" ) { if (sf->argVal(0) != argStartFocusINDI->directionCombo->currentText()) setUnsavedChanges( true ); sf->setArg(0, argStartFocusINDI->directionCombo->currentText()); sf->setValid(true); } else { warningMismatch( "startINDIFocus" ); } } void ScriptBuilder::slotINDISetFocusTimeout() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "setINDIFocusTimeout" ) { if (sf->argVal(0).toInt() != argSetFocusTimeoutINDI->timeOut->value()) setUnsavedChanges( true ); sf->setArg(0, QString("%1").arg(argSetFocusTimeoutINDI->timeOut->value())); sf->setValid(true); } else { warningMismatch( "setINDIFocusTimeout" ); } } void ScriptBuilder::slotINDISetGeoLocationDeviceLong() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "setINDIGeoLocation" ) { //do nothing if box is blank (because we could be clearing boxes while switching argWidgets) if ( argSetGeoLocationINDI->longBox->text().isEmpty()) { sf->setValid(false); return; } bool ok(false); dms longitude = argSetGeoLocationINDI->longBox->createDms(true, &ok); if ( ok ) { if (sf->argVal(0) != QString( "%1" ).arg( longitude.Degrees())) setUnsavedChanges( true ); sf->setArg( 0, QString( "%1" ).arg( longitude.Degrees() ) ); if ( ! sf->argVal(1).isEmpty() ) sf->setValid( true ); else sf->setValid(false); } else { sf->setArg( 0, QString() ); sf->setValid( false ); } } else { warningMismatch( "setINDIGeoLocation" ); } } void ScriptBuilder::slotINDISetGeoLocationDeviceLat() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "setINDIGeoLocation" ) { //do nothing if box is blank (because we could be clearing boxes while switching argWidgets) if ( argSetGeoLocationINDI->latBox->text().isEmpty() ) { sf->setValid(false); return; } bool ok(false); dms latitude = argSetGeoLocationINDI->latBox->createDms(true, &ok); if ( ok ) { if (sf->argVal(1) != QString( "%1" ).arg( latitude.Degrees())) setUnsavedChanges( true ); sf->setArg( 1, QString( "%1" ).arg( latitude.Degrees() ) ); if ( ! sf->argVal(0).isEmpty() ) sf->setValid( true ); else sf->setValid(false); } else { sf->setArg( 1, QString() ); sf->setValid( false ); } } else { warningMismatch( "setINDIGeoLocation" ); } } void ScriptBuilder::slotINDIStartExposureTimeout() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "startINDIExposure" ) { if (sf->argVal(0).toInt() != argStartExposureINDI->timeOut->value()) setUnsavedChanges( true ); sf->setArg(0, QString("%1").arg(argStartExposureINDI->timeOut->value())); sf->setValid(true); } else { warningMismatch( "startINDIExposure" ); } } void ScriptBuilder::slotINDISetUTC() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "setINDIUTC" ) { if (argSetUTCINDI->UTC->text().isEmpty()) { sf->setValid(false); return; } if (sf->argVal(0) != argSetUTCINDI->UTC->text()) setUnsavedChanges( true ); sf->setArg(0, argSetUTCINDI->UTC->text()); sf->setValid(true); } else { warningMismatch( "setINDIUTC" ); } } void ScriptBuilder::slotINDISetScopeAction() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "setINDIScopeAction" ) { if (sf->argVal(0) != argSetScopeActionINDI->actionCombo->currentText()) setUnsavedChanges( true ); sf->setArg(0, argSetScopeActionINDI->actionCombo->currentText()); sf->setINDIProperty("CHECK"); sf->setValid(true); } else { warningMismatch( "setINDIScopeAction" ); } } void ScriptBuilder::slotINDISetFrameType() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "setINDIFrameType" ) { if (sf->argVal(0) != argSetFrameTypeINDI->typeCombo->currentText()) setUnsavedChanges( true ); sf->setArg(0, argSetFrameTypeINDI->typeCombo->currentText()); sf->setValid(true); } else { warningMismatch( "setINDIFrameType" ); } } void ScriptBuilder::slotINDISetCCDTemp() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "setINDICCDTemp" ) { if (sf->argVal(0).toInt() != argSetCCDTempINDI->temp->value()) setUnsavedChanges( true ); sf->setArg(0, QString("%1").arg(argSetCCDTempINDI->temp->value())); sf->setValid(true); } else { warningMismatch( "setINDICCDTemp" ); } } void ScriptBuilder::slotINDISetFilterNum() { ScriptFunction * sf = ScriptList[ sb->ScriptListBox->currentRow() ]; if ( sf->name() == "setINDIFilterNum" ) { if (sf->argVal(0).toInt() != argSetFilterNumINDI->filter_num->value()) setUnsavedChanges( true ); sf->setArg(0, QString("%1").arg(argSetFilterNumINDI->filter_num->value())); sf->setValid(true); } else { warningMismatch( "setINDIFilterNum" ); } } #endif void ScriptBuilder::warningMismatch(const QString &expected) const { qWarning() << i18n("Mismatch between function and Arg widget (expected %1.)", QString(expected)); } diff --git a/kstars/widgets/infoboxwidget.cpp b/kstars/widgets/infoboxwidget.cpp index ee5a0dbee..f3c875b98 100644 --- a/kstars/widgets/infoboxwidget.cpp +++ b/kstars/widgets/infoboxwidget.cpp @@ -1,259 +1,265 @@ /*************************************************************************** fovwidget.cpp - description ------------------- begin : 20 Aug 2009 copyright : (C) 2009 by Khudyakov Alexey email : alexey.skladnoy@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "infoboxwidget.h" #include "colorscheme.h" #include "kstarsdata.h" #include "ksutils.h" #include #include #include #include #include const int InfoBoxWidget::padX = 6; const int InfoBoxWidget::padY = 2; InfoBoxes::InfoBoxes(QWidget *parent) : QWidget(parent) { setMouseTracking(true); } void InfoBoxes::addInfoBox(InfoBoxWidget *ibox) { ibox->setParent(this); m_boxes.append(ibox); } void InfoBoxes::resizeEvent(QResizeEvent *) { foreach (InfoBoxWidget *w, m_boxes) w->adjust(); } /* ================================================================ */ InfoBoxWidget::InfoBoxWidget(bool shade, const QPoint &pos, int anchor, const QStringList &str, QWidget *parent) : QWidget(parent), m_strings(str), m_adjusted(false), m_grabbed(false), m_shaded(shade), m_anchor(anchor) { move(pos); updateSize(); } void InfoBoxWidget::updateSize() { QFontMetrics fm(font()); int w = 0; - foreach (const QString &str, m_strings) + foreach (const QString &str, m_strings) { + #if QT_VERSION >= QT_VERSION_CHECK(5,11,0) + w = qMax(w, fm.horizontalAdvance(str)); + #else w = qMax(w, fm.width(str)); + #endif + } + int h = fm.height() * (m_shaded ? 1 : m_strings.size()); // Add padding resize(w + 2 * padX, h + 2 * padY + 2); adjust(); } void InfoBoxWidget::slotTimeChanged() { KStarsData *data = KStarsData::Instance(); m_strings.clear(); m_strings << i18nc("Local Time", "LT: ") + data->lt().time().toString(QLocale().timeFormat().remove('t')) + " " + // Remove timezone, as timezone of geolocation in KStars might not be same as system locale timezone QLocale().toString(data->lt().date()); m_strings << i18nc("Universal Time", "UT: ") + data->ut().time().toString("HH:mm:ss") + " " + QLocale().toString(data->ut().date()); // Do not format UTC according to locale QString STString; STString = STString.sprintf("%02d:%02d:%02d ", data->lst()->hour(), data->lst()->minute(), data->lst()->second()); //Don't use KLocale::formatNumber() for Julian Day because we don't want //thousands-place separators QString JDString = QString::number(data->ut().djd(), 'f', 2); JDString.replace('.', QLocale().decimalPoint()); m_strings << i18nc("Sidereal Time", "ST: ") + STString + i18nc("Julian Day", "JD: ") + JDString; updateSize(); update(); } void InfoBoxWidget::slotGeoChanged() { GeoLocation *geo = KStarsData::Instance()->geo(); m_strings.clear(); m_strings << geo->fullName(); //m_strings << i18nc("Longitude", "Long:") + ' ' + QLocale().toString(geo->lng()->Degrees(), 3) + " " + // i18nc("Latitude", "Lat:") + ' ' + QLocale().toString(geo->lat()->Degrees(), 3); m_strings << i18nc("Longitude", "Long:") + ' ' + geo->lng()->toDMSString(true) + ' ' + i18nc("Latitude", "Lat:") + ' ' + geo->lat()->toDMSString(true); updateSize(); update(); } void InfoBoxWidget::slotObjectChanged(SkyObject *obj) { setPoint(obj->translatedLongName(), obj); } void InfoBoxWidget::slotPointChanged(SkyPoint *p) { setPoint(i18n("nothing"), p); } void InfoBoxWidget::setPoint(QString name, SkyPoint *p) { m_strings.clear(); m_strings << name; m_strings << i18nc("Right Ascension", "RA") + ": " + p->ra().toHMSString() + " " + i18nc("Declination", "Dec") + ": " + p->dec().toDMSString(true); m_strings << i18nc("Azimuth", "Az") + ": " + p->az().toDMSString(true) + " " + i18nc("Altitude", "Alt") + ": " + p->alt().toDMSString(true); updateSize(); update(); } void InfoBoxWidget::adjust() { if (!isVisible()) return; // X axis int newX = x(); int maxX = parentWidget()->width() - width(); if (m_anchor & AnchorRight) { newX = maxX; } else { newX = KSUtils::clamp(newX, 0, maxX); if (newX == maxX) m_anchor |= AnchorRight; } // Y axis int newY = y(); int maxY = parentWidget()->height() - height(); if (m_anchor & AnchorBottom) { newY = maxY; } else { newY = KSUtils::clamp(newY, 0, maxY); if (newY == maxY) m_anchor |= AnchorBottom; } // Do move m_adjusted = true; move(newX, newY); } void InfoBoxWidget::paintEvent(QPaintEvent *) { // If widget contain no strings return if (m_strings.empty()) return; // Start with painting ColorScheme *cs = KStarsData::Instance()->colorScheme(); QPainter p; p.begin(this); // Draw background QColor colBG = cs->colorNamed("BoxBGColor"); colBG.setAlpha(127); p.fillRect(contentsRect(), colBG); // Draw border if (m_grabbed) { p.setPen(cs->colorNamed("BoxGrabColor")); p.drawRect(0, 0, width() - 1, height() - 1); } // Draw text int h = QFontMetrics(font()).height(); int y = 0; p.setPen(cs->colorNamed("BoxTextColor")); foreach (const QString &str, m_strings) { y += h; p.drawText(padX, padY + y, str); } // Done p.end(); } void InfoBoxWidget::mouseMoveEvent(QMouseEvent *event) { m_grabbed = true; // X axis int newX = x() + event->x(); int maxX = parentWidget()->width() - width(); if (newX > maxX) { newX = maxX; m_anchor |= AnchorRight; } else { if (newX < 0) newX = 0; m_anchor &= ~AnchorRight; } // Y axis int newY = y() + event->y(); int maxY = parentWidget()->height() - height(); if (newY > maxY) { newY = maxY; m_anchor |= AnchorBottom; } else { if (newY < 0) newY = 0; m_anchor &= ~AnchorBottom; } // Do move m_adjusted = true; move(newX, newY); } void InfoBoxWidget::mousePressEvent(QMouseEvent *) { emit clicked(); } void InfoBoxWidget::showEvent(QShowEvent *) { if (!m_adjusted) adjust(); } void InfoBoxWidget::mouseDoubleClickEvent(QMouseEvent *) { m_shaded = !m_shaded; updateSize(); update(); } void InfoBoxWidget::mouseReleaseEvent(QMouseEvent *) { m_grabbed = false; } diff --git a/kstars/widgets/timespinbox.cpp b/kstars/widgets/timespinbox.cpp index efbdf5868..74914e9da 100644 --- a/kstars/widgets/timespinbox.cpp +++ b/kstars/widgets/timespinbox.cpp @@ -1,256 +1,261 @@ /*************************************************************************** timespinbox.cpp - description ------------------- begin : Sun Mar 31 2002 copyright : (C) 2002 by Jason Harris email : kstars@30doradus.org ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "timespinbox.h" #include "kstars_debug.h" #include #include #include #include #define SECS_PER_DAY 86400. #define SIDEREAL_YEAR 31558149.77 //Time steps: //0-9: 0 sec, 0.1, 0.25, 0.5, 1, 2, 5, 10, 20, 30 sec //10-14: 1 minute, 2, 5, 10, 15, 30 min //15-19: 1 hour, 2, 4, 6, 12 hours //20-23: 1 day, 2, 3, 5 days //24-26: 1 week, 2, 3 weeks //27-32: 1 month, 2, 3, 4, 6, 9 months //33-41: 1 year, 2, 3, 4, 5, 10, 25, 50, 100 years TimeSpinBox::TimeSpinBox(QWidget *parent, bool _daysonly) : QSpinBox(parent) { setDaysOnly(_daysonly); setMinimum(-41); setMaximum(41); setSingleStep(1); setButtonSymbols(QSpinBox::PlusMinus); lineEdit()->setReadOnly(true); setValue(4); //1 second (real time) //Set width: QFontMetrics fm(font()); int extra = width() - lineEdit()->width(); uint wmax = 0; for (int i = 0; i < maximum(); ++i) { + #if QT_VERSION >= QT_VERSION_CHECK(5,11,0) + uint w = fm.horizontalAdvance('-' + TimeString[i]); + #else uint w = fm.width('-' + TimeString[i]); + #endif + if (w > wmax) wmax = w; } setFixedWidth(wmax + extra); connect(this, SIGNAL(valueChanged(int)), this, SLOT(reportChange())); // updateDisplay(); } void TimeSpinBox::setDaysOnly(bool daysonly) { DaysOnly = daysonly; int i = 0; //index for TimeScale values TimeScale[0] = 0.0; // 0.0 sec if (!daysOnly()) { TimeScale[1] = 0.1; // 0.1 sec TimeScale[2] = 0.25; // 0.25 sec TimeScale[3] = 0.5; // 0.5 sec TimeScale[4] = 1.0; // 1 sec (real-time) TimeScale[5] = 2.0; // 2 sec TimeScale[6] = 5.0; // 5 sec TimeScale[7] = 10.0; // 10 sec TimeScale[8] = 20.0; // 20 sec TimeScale[9] = 30.0; // 30 sec TimeScale[10] = 60.0; // 1 min TimeScale[11] = 120.0; // 2 min TimeScale[12] = 300.0; // 5 min TimeScale[13] = 600.0; // 10 min TimeScale[14] = 900.0; // 15 min TimeScale[15] = 1800.0; // 30 min TimeScale[16] = 3600.0; // 1 hr TimeScale[17] = 7200.0; // 2 hr TimeScale[18] = 10800.0; // 3 hr TimeScale[19] = 21600.0; // 6 hr TimeScale[20] = 43200.0; // 12 hr i = 20; } TimeScale[i + 1] = 86164.1; // 23 hr 56 min TimeScale[i + 2] = SECS_PER_DAY; // 1 day TimeScale[i + 3] = 2. * SECS_PER_DAY; // 2 days TimeScale[i + 4] = 3. * SECS_PER_DAY; // 3 days TimeScale[i + 5] = 5. * SECS_PER_DAY; // 5 days TimeScale[i + 6] = 7. * SECS_PER_DAY; // 1 week TimeScale[i + 7] = 14. * SECS_PER_DAY; //2 weeks TimeScale[i + 8] = 21. * SECS_PER_DAY; //3 weeks //Months aren't a simple measurement of time; I'll just use fractions of a year TimeScale[i + 9] = SIDEREAL_YEAR / 12.0; // 1 month TimeScale[i + 10] = SIDEREAL_YEAR / 6.0; // 2 months TimeScale[i + 11] = 0.25 * SIDEREAL_YEAR; // 3 months TimeScale[i + 12] = SIDEREAL_YEAR / 3.0; // 4 months TimeScale[i + 13] = 0.5 * SIDEREAL_YEAR; // 6 months TimeScale[i + 14] = 0.75 * SIDEREAL_YEAR; // 9 months TimeScale[i + 15] = SIDEREAL_YEAR; // 1 year TimeScale[i + 16] = 2.0 * SIDEREAL_YEAR; // 2 years TimeScale[i + 17] = 3.0 * SIDEREAL_YEAR; // 3 years TimeScale[i + 18] = 5.0 * SIDEREAL_YEAR; // 5 years TimeScale[i + 19] = 10.0 * SIDEREAL_YEAR; // 10 years TimeScale[i + 20] = 25.0 * SIDEREAL_YEAR; // 25 years TimeScale[i + 21] = 50.0 * SIDEREAL_YEAR; // 50 years TimeScale[i + 22] = 100.0 * SIDEREAL_YEAR; // 100 years TimeString.clear(); if (!daysOnly()) { TimeString.append("0 " + i18nc("seconds", "secs")); TimeString.append("0.1 " + i18nc("seconds", "secs")); TimeString.append("0.25 " + i18nc("seconds", "secs")); TimeString.append("0.5 " + i18nc("seconds", "secs")); TimeString.append("1 " + i18nc("second", "sec")); TimeString.append("2 " + i18nc("seconds", "secs")); TimeString.append("5 " + i18nc("seconds", "secs")); TimeString.append("10 " + i18nc("seconds", "secs")); TimeString.append("20 " + i18nc("seconds", "secs")); TimeString.append("30 " + i18nc("seconds", "secs")); TimeString.append("1 " + i18nc("minute", "min")); TimeString.append("2 " + i18nc("minutes", "mins")); TimeString.append("5 " + i18nc("minutes", "mins")); TimeString.append("10 " + i18nc("minutes", "mins")); TimeString.append("15 " + i18nc("minutes", "mins")); TimeString.append("30 " + i18nc("minutes", "mins")); TimeString.append("1 " + i18n("hour")); TimeString.append("2 " + i18nc("hours", "hrs")); TimeString.append("3 " + i18nc("hours", "hrs")); TimeString.append("6 " + i18nc("hours", "hrs")); TimeString.append("12 " + i18nc("hours", "hrs")); } else { TimeString.append("0 " + i18n("days")); } TimeString.append("1 " + i18nc("sidereal day", "sid day")); TimeString.append("1 " + i18n("day")); TimeString.append("2 " + i18n("days")); TimeString.append("3 " + i18n("days")); TimeString.append("5 " + i18n("days")); TimeString.append("1 " + i18n("week")); TimeString.append("2 " + i18nc("weeks", "wks")); TimeString.append("3 " + i18nc("weeks", "wks")); TimeString.append("1 " + i18n("month")); TimeString.append("2 " + i18nc("months", "mths")); TimeString.append("3 " + i18nc("months", "mths")); TimeString.append("4 " + i18nc("months", "mths")); TimeString.append("6 " + i18nc("months", "mths")); TimeString.append("9 " + i18nc("months", "mths")); TimeString.append("1 " + i18n("year")); TimeString.append("2 " + i18nc("years", "yrs")); TimeString.append("3 " + i18nc("years", "yrs")); TimeString.append("5 " + i18nc("years", "yrs")); TimeString.append("10 " + i18nc("years", "yrs")); TimeString.append("25 " + i18nc("years", "yrs")); TimeString.append("50 " + i18nc("years", "yrs")); TimeString.append("100 " + i18nc("years", "yrs")); if (!daysOnly()) { setMinimum(-41); setMaximum(41); } else { setMinimum(-21); setMaximum(21); } } int TimeSpinBox::valueFromText(const QString &text) const { for (int i = 0; i < TimeString.size(); i++) { if (text == TimeString[i]) { return i; } if (text.mid(1, text.length()) == TimeString[i]) { return -1 * i; } } return 0; } QString TimeSpinBox::textFromValue(int value) const { QString result; int posval(abs(value)); if (posval > TimeString.size() - 1) posval = 4; result = TimeString[posval]; if (value < 0) { result = '-' + result; } return result; } void TimeSpinBox::changeScale(float x) { //Pick the closest value int imin = 0; float dxlast(10000000000.0), dxmin(10000000000.0); for (unsigned int i = 0; i < 42; ++i) { float dx = fabs(TimeScale[i] - fabs(x)); if (dx < dxmin) { imin = i; dxmin = dx; } if (dx > dxlast) break; //we have passed the minimum dx dxlast = dx; } if (x < 0.0) imin *= -1; setValue(imin); } float TimeSpinBox::timeScale(void) const { return value() > 0 ? TimeScale[value()] : -1. * TimeScale[abs(value())]; } void TimeSpinBox::reportChange() { qCDebug(KSTARS) << "Reporting new timestep value: " << timeScale(); emit scaleChanged(timeScale()); }