diff --git a/kstars/ekos/align/align.cpp b/kstars/ekos/align/align.cpp index 0dbe64aa8..a8139798c 100644 --- a/kstars/ekos/align/align.cpp +++ b/kstars/ekos/align/align.cpp @@ -1,5717 +1,5717 @@ /* 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 "opsalign.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 "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 #define PAH_CUTOFF_FOV 30 // Minimum FOV width in arcminutes for PAH to work #define MAXIMUM_SOLVER_ITERATIONS 10 #define AL_FORMAT_VERSION 1.0 namespace Ekos { // 30 arcmiutes 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); } ); 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); }); alignTimer.setSingleShot(true); alignTimer.setInterval(Options::astrometryTimeout() * 1000); connect(&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")); #ifdef Q_OS_OSX opsAstrometryCfg = new OpsAstrometryCfg(this); page = dialog->addPage(opsAstrometryCfg, i18n("Astrometry.cfg")); page->setIcon(QIcon::fromTheme("document-edit")); #endif #ifndef Q_OS_WIN opsAstrometryIndexFiles = new OpsAstrometryIndexFiles(this); page = dialog->addPage(opsAstrometryIndexFiles, i18n("Index Files")); page->setIcon(QIcon::fromTheme("map-flat")); #endif 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(); // Online/Offline/Remote solver check solverTypeGroup->setId(onlineSolverR, SOLVER_ONLINE); solverTypeGroup->setId(offlineSolverR, SOLVER_OFFLINE); solverTypeGroup->setId(remoteSolverR, SOLVER_REMOTE); #ifdef Q_OS_WIN offlineSolverR->setEnabled(false); offlineSolverR->setToolTip( i18n("Offline solver is not supported under Windows. Please use either the Online or Remote solvers.")); #endif solverTypeGroup->button(Options::solverType())->setChecked(true); connect(solverTypeGroup, SIGNAL(buttonClicked(int)), SLOT(setSolverType(int))); switch (solverTypeGroup->checkedId()) { case SOLVER_ONLINE: onlineParser.reset(new Ekos::OnlineAstrometryParser()); parser = onlineParser.get(); break; case SOLVER_OFFLINE: offlineParser.reset(new OfflineAstrometryParser()); parser = offlineParser.get(); break; case SOLVER_REMOTE: remoteParser.reset(new RemoteAstrometryParser()); parser = remoteParser.get(); break; } parser->setAlign(this); if (parser->init() == false) setEnabled(false); else { connect(parser, &Ekos::AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection); connect(parser, &Ekos::AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection); } //solverOptions->setText(Options::solverOptions()); // Which telescope info to use for FOV calculations //kcfg_solverOTA->setChecked(Options::solverOTA()); //guideScopeCCDs = Options::guideScopeCCDs(); FOVScopeCombo->setCurrentIndex(Options::solverScopeType()); connect(FOVScopeCombo, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Align::updateTelescopeType); //connect(FOVScopeCombo, SIGNAL(currentIndexChanged(int)), this, SIGNAL(newFOVTelescopeType(int))); 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); }); 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); 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->setColumnWidth(0, 70); solutionTable->setColumnWidth(1, 75); solutionTable->setColumnWidth(2, 80); solutionTable->setColumnWidth(3, 30); solutionTable->setColumnWidth(4, 100); solutionTable->setColumnWidth(5, 100); 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->currentText() == "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 { KMessageBox::sorry(nullptr, i18n("DEC is below the altitude limit")); return; } } else { if (decAngle > lat + 90 - minAlt) //Max altitude possible at minAlt deg above horizon { KMessageBox::sorry(nullptr, 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->currentText() == "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->currentText() == "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) { KMessageBox::sorry(nullptr, 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 = "None"; } 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; if (spEast.ra().Degrees() > spWest.ra().Degrees()) angleSep = spEast.ra() - spWest.ra(); else angleSep = spEast.ra() + dms(360) - spWest.ra(); initRA = spWest.ra().Degrees(); if (raPoints > 1) raIncrement = 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; if (mountModel.alignTypeBox->currentText() == "Any Object") return KStarsData::Instance()->skyComposite()->objectNearest(new SkyPoint(dms(ra), dms(dec)), maxSearch); else if (mountModel.alignTypeBox->currentText() == "Fixed DEC" || mountModel.alignTypeBox->currentText() == "Fixed Grid") return nullptr; else if (mountModel.alignTypeBox->currentText() == "Any Stars") 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(const QString alignType) { if (alignType == "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) { 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(); GeoLocation *geo = data->geo(); CachingDms *lst = data->lst(); KStarsDateTime ut = geo->LTtoUT(KStarsDateTime(QDateTime::currentDateTime().toLocalTime())); SkyPoint sp = so->recomputeCoords(ut, geo); //check altitude of object at this time. sp.EquatorialToHorizontal(lst, 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()); KMessageBox::sorry(nullptr, 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); KMessageBox::sorry(nullptr, 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) { KMessageBox::error(KStars::Instance(), i18n("Failed to save alignment list"), i18n("Save")); return; } } else { QString message = i18n("Invalid URL: %1", alignURL.url()); KMessageBox::sorry(KStars::Instance(), 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); KMessageBox::sorry(nullptr, 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()); KMessageBox::sorry(KStars::Instance(), 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); KMessageBox::sorry(nullptr, 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) { KMessageBox::sorry(nullptr, 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; if (KMessageBox::questionYesNo( KStars::Instance(), i18n("Are you sure you want to clear all of the solution points?"), i18n("Clear Solution Points"), KStandardGuiItem::yes(), KStandardGuiItem::no()) == KMessageBox::Yes) { solutionTable->setRowCount(0); alignPlot->graph(0)->data()->clear(); alignPlot->clearItems(); buildTarget(); slotAutoScaleGraph(); } } 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(); QPointer fd = new FindDialog(KStars::Instance()); if (fd->exec() == QDialog::Accepted) { SkyObject *object = fd->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); } } delete fd; 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()) { KMessageBox::error(nullptr, 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() { 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(); captureAndSolve(); } // TODO must also account for loadAndSlew. Retain file name } void Align::setSolverType(int type) { if (sender() == nullptr && type >= 0 && type <= 2) solverTypeGroup->button(type)->setChecked(true); syncSettings(); Options::setSolverType(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); if (solverTypeGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr) (dynamic_cast(remoteParser.get()))->setCCD(currentCCD->getDeviceName()); syncCCDInfo(); /* FOVScopeCombo->blockSignals(true); ISD::CCD::TelescopeType type = currentCCD->getTelescopeType(); FOVScopeCombo->setCurrentIndex(type == ISD::CCD::TELESCOPE_UNKNOWN ? 0 : type); FOVScopeCombo->blockSignals(false); */ 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); connect(currentTelescope, &ISD::GDInterface::numberUpdated, this, &Ekos::Align::processNumber, Qt::UniqueConnection); syncTelescopeInfo(); } void Align::setDome(ISD::GDInterface *newDome) { currentDome = static_cast(newDome); connect(currentDome, &ISD::GDInterface::switchUpdated, this, &Ekos::Align::processSwitch, Qt::UniqueConnection); } void Align::syncTelescopeInfo() { if (currentTelescope == nullptr || currentTelescope->isConnected() == false) return; 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; 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(); } } 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); //targetChip->getFrame(&x,&y,&ccd_width,&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; } 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; 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)); } else { FOVOut->setToolTip(i18n("

Effective field of view size in arcminutes.

")); //FOVOut->setReadOnly(true); } 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) { // -O overwrite // -3 Expected RA // -4 Expected DEC // -5 Radius (deg) // -L lower scale of image in arcminutes // -H upper scale of image in arcmiutes // -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 QStringList solver_args; // 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(); 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, double fov_v, QString &fov_low, QString &fov_high) { double fov_lower, fov_upper; // let's stretch the boundaries by 5% fov_lower = ((fov_h < fov_v) ? (fov_h * 0.95) : (fov_v * 0.95)); fov_upper = ((fov_h > fov_v) ? (fov_h * 1.05) : (fov_v * 1.05)); //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() { // -O overwrite // -3 Expected RA // -4 Expected DEC // -5 Radius (deg) // -L lower scale of image in arcminutes // -H upper scale of image in arcmiutes // -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 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(); 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; } generateFOVBounds(fov_w, fov_h, fov_low, fov_high); 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(); QStringList solverArgs = generateOptions(optionsMap); QString options = solverArgs.join(" "); solverOptions->setText(options); solverOptions->setToolTip(options); } bool Align::captureAndSolve() { alignTimer.stop(); 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) { KMessageBox::error( nullptr, 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) { KMessageBox::error(nullptr, 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; filterManager->setFilterPosition(targetPosition); 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 10 seconds...")); alignTimer.start(); return false; } if (targetChip->isCapturing()) { appendLogText(i18n("Cannot capture while CCD exposure is in progress. Retrying in 10 seconds...")); alignTimer.start(); 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 (solverTypeGroup->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(); } //else //{ 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 (solverTypeGroup->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, &Ekos::Align::setCaptureComplete); connect(DarkLibrary::Instance(), &DarkLibrary::newLog, this, &Ekos::Align::appendLogText); if (darkData) DarkLibrary::Instance()->subtract(darkData, alignView, FITS_NONE, offsetX, offsetY); else { bool rc = DarkLibrary::Instance()->captureAndSubtract(targetChip, alignView, exposureIN->value(), offsetX, offsetY); alignDarkFrameCheck->setChecked(rc); } return; } } setCaptureComplete(); } } void Align::setCaptureComplete() { DarkLibrary::Instance()->disconnect(this); if (pahStage == PAH_REFRESH) { newFrame(alignView); captureAndSolve(); return; } emit newImage(alignView); if (solverTypeGroup->checkedId() == 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()->setImageDisplay(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); } 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::setSolverOptions(solverOptions->text()); //Options::setGuideScopeCCDs(guideScopeCCDs); Options::setSolverAccuracyThreshold(accuracySpin->value()); Options::setAlignDarkFrame(alignDarkFrameCheck->isChecked()); Options::setSolverGotoOption(currentGotoMode); //m_isSolverComplete = false; //m_isSolverSuccessful = false; if (fov_x > 0) parser->verifyIndexFiles(fov_x, fov_y); solverTimer.start(); 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; } alignTimer.stop(); if (solverTypeGroup->checkedId() == 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::astrometrySolverVerbose()) + 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 0 if (pixscale > 0 && loadSlewState == IPS_IDLE) { double solver_focal_length = (206.264 * ccd_hor_pixel) / pixscale * binx; if (fabs(focal_length - solver_focal_length) > 1) appendLogText(i18n("Current focal length is %1 mm while computed focal length from the solver is %2 mm. " "Please update the mount focal length to obtain accurate results.", QString::number(focal_length, 'g', 5), QString::number(solver_focal_length, 'g', 5))); } #endif if (fov_x == 0 && pixscale > 0) { double newFOVW = ccd_width * pixscale / binx / 60.0; double newFOVH = ccd_height * pixscale / biny / 60.0; saveNewEffectiveFOV(newFOVW, newFOVH); } 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().Degrees() - targetCoord.ra().Degrees()) * 3600; double deDiff = (alignCoord.dec().Degrees() - targetCoord.dec().Degrees()) * 3600; dms RADiff(fabs(raDiff) / 3600.0), DEDiff(deDiff / 3600.0); dRAOut->setText(QString("%1%2").arg((raDiff > 0 ? "+" : "-"), RADiff.toHMSString())); dDEOut->setText(DEDiff.toDMSString(true)); pixScaleOut->setText(QString::number(pixscale, 'f', 2)); //emit newSolutionDeviation(raDiff, deDiff); targetDiff = sqrt(raDiff * raDiff + deDiff * deDiff); // Because astrometry reads image upside-down (bottom to top), the orientation is rotated 180 degrees when compared to PA // PA = Orientation + 180 double solverPA = orientation + 180; // Limit PA to -180 to +180 if (solverPA > 180) solverPA -= 360; if (solverPA < -180) solverPA += 360; solverFOV->setCenter(alignCoord); solverFOV->setPA(solverPA); solverFOV->setImageDisplay(Options::astrometrySolverOverlay()); sensorFOV->setPA(solverPA); 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->setBrush(Qt::NoBrush); 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; } } } } retries = 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); //if (syncR->isChecked() || nothingR->isChecked() || targetDiff <= accuracySpin->value()) // CONTINUE HERE //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 Rotater 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 = 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 = (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", dRAOut->text()}, {"dDE", dDEOut->text()}, {"pix", pixscale}, {"rot", orientation}, {"fov", FOVOut->text()}, }; emit newSolution(solution); switch (currentGotoMode) { case GOTO_SYNC: executeGOTO(); if (loadSlewState == IPS_IDLE) { statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg")); solutionTable->setItem(currentRow, 3, statusReport.release()); } return; break; 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); alignTimer.stop(); azStage = AZ_INIT; altStage = ALT_INIT; //loadSlewMode = false; loadSlewState = IPS_IDLE; solverIterations = 0; retries = 0; //emit solverComplete(false); 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() { 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; //loadSlewMode = false; loadSlewState = IPS_IDLE; solverIterations = 0; retries = 0; alignTimer.stop(); //currentCCD->disconnect(this); 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", QDateTime::currentDateTime().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 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::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); switch (nvp->s) { case IPS_OK: { // Update the boxes as the mount just finished slewing if (isSlewDirty && Options::astrometryAutoUpdatePosition()) { 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 (isSlewDirty && pahStage == PAH_FIND_CP) { isSlewDirty = false; appendLogText(i18n("Mount completed slewing near celestial pole. Capture again to verify.")); setSolverAction(GOTO_NOTHING); pahStage = PAH_FIRST_CAPTURE; emit newPAHStage(pahStage); return; } if (isSlewDirty && pahStage == PAH_FIRST_ROTATE) { isSlewDirty = false; appendLogText(i18n("Mount first rotation is complete.")); pahStage = PAH_SECOND_CAPTURE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHSecondCapturePage); emit newPAHMessage(secondCaptureText->text()); captureAndSolve(); } else if (isSlewDirty && pahStage == PAH_SECOND_ROTATE) { isSlewDirty = false; appendLogText(i18n("Mount second rotation is complete.")); pahStage = PAH_THIRD_CAPTURE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHThirdCapturePage); emit newPAHMessage(thirdCaptureText->text()); captureAndSolve(); } switch (state) { case ALIGN_PROGRESS: break; case ALIGN_SYNCING: { isSlewDirty = 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 (isSlewDirty == false) break; isSlewDirty = false; if (loadSlewState == IPS_BUSY) { loadSlewState = IPS_IDLE; qCDebug(KSTARS_EKOS_ALIGN) << "loadSlewState is IDLE."; state = ALIGN_PROGRESS; emit newStatus(state); QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); 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); QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); return; } break; default: { isSlewDirty = false; } break; } } break; case IPS_BUSY: { isSlewDirty = true; } break; case IPS_ALERT: { if (state == ALIGN_SYNCING || state == ALIGN_SLEWING) { if (state == ALIGN_SYNCING) appendLogText(i18n("Syncing failed.")); else appendLogText(i18n("Slewing failed.")); if (++retries == 3) { abort(); return; } else { if (currentGotoMode == GOTO_SLEW) Slew(); else Sync(); } } return; } break; default: break; } /*if (Options::alignmentLogging()) qDebug() << "State is " << Ekos::getAlignStatusString(state) << " isSlewing? " << currentTelescope->isSlewing() << " slew Dirty? " << slew_dirty << " Current GOTO Mode? " << currentGotoMode << " LoadSlewState? " << pstateStr(loadSlewState);*/ 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 mananges TELESCOPE_INFO, why here again? //if (!strcmp(coord->name, "TELESCOPE_INFO")) //syncTelescopeInfo(); } void Align::executeGOTO() { if (loadSlewState == IPS_BUSY) { //if (loadSlewIterations == loadSlewIterationsSpin->value()) //loadSlewCoord = alignCoord; //targetCoord = loadSlewCoord; 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); isSlewDirty = 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()) { double raDiff = alignCoord.ra().Degrees() - targetCoord.ra().Degrees(); double deDiff = alignCoord.dec().Degrees() - targetCoord.dec().Degrees(); targetCoord.setRA((targetCoord.ra().Degrees()-raDiff)/15.0); targetCoord.setDec(targetCoord.dec().Degrees()-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 Measureing Azimuth Error..."; switch (azStage) { case AZ_INIT: // Display message box confirming user point scope near meridian and south 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 Measureing Altitude Error..."; switch (altStage) { case ALT_INIT: // Display message box confirming user point scope near meridian and south 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; captureAndSolve(); 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; captureAndSolve(); 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; default: 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 Deviaiton " << deviation << " degrees."; if (azStage == AZ_FINISHED) { azError->setText(deviationDirection.subs(QString("%1").arg(devDMS.toDMSString())).toString()); //azError->setText(deviationDirection.subs(QString("%1")azDMS.toDMSString()); 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(fabs(deviation), 0, 'g', 3)).toString()); 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) { /*if (solverTypeGroup->checkedId() == SOLVER_REMOTE) { appendLogText(i18n("Load and Slew is not supported in remote solver mode.")); loadSlewB->setEnabled(false); return; }*/ 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) { foreach (ISD::GDInterface *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 (++retries == 3 && pahStage != PAH_REFRESH) { appendLogText(i18n("Capture error. Aborting...")); abort(); return; } appendLogText(i18n("Restarting capture attempt #%1", retries)); 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) { 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; 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(); if (Options::astrometryCustomOptions().isEmpty() == false) optionsMap["custom"] = Options::astrometryCustomOptions(); solver_args = generateOptions(optionsMap); status = 0; #if 0 if (fits_open_image(&fptr, filename.toLatin1(), READONLY, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); qCritical(KSTARS_EKOS_ALIGN) << "Could not open file " << filename << " Error: " << QString::fromUtf8(error_status); return solver_args; } #endif // 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; } 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 == false) { ra = telescopeCoord.ra0().Hours(); dec = telescopeCoord.dec0().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; } 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."); } QTimer::singleShot(Options::settlingTime(), this, &Ekos::Align::captureAndSolve); 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); //rightLayout->setStretch(0, 2); // rightLayout->setStretch(1, 1); 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(); } // 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); 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 0 // North if (hemisphere == NORTH_HEMISPHERE) { // West if (westMeridian) raDiff *= -1; // East else raDiff *= 1; } // South else { // West if (westMeridian) raDiff *= 1; // East else raDiff *= -1; } #endif SkyPoint targetPAH; double newTelescopeRA = (telescopeCoord.ra().Degrees() + raDiff) / 15.0; targetPAH.setRA(newTelescopeRA); targetPAH.setDec(telescopeCoord.dec()); // Convert to JNow //targetPAH.apparentCoord((long double) J2000, KStars::Instance()->data()->ut().djd()); // Get horizontal coords //targetPAH.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); currentTelescope->Slew(&targetPAH); 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()); correctionVector.setP1(celestialPolePoint); correctionVector.setP2(RACenterPoint); /* bool RAAxisInside = imageData->contains(RACenterPoint); bool CPPointInside= imageData->contains(celestialPolePoint); if (RAAxisInside == false && CPPointInside == false) appendLogText(i18n("Warning: Mount axis and celestial pole are outside the field of view. Correction vector may be inaccurate.")); */ 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::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) { // Create temporary file to hold all WCS data // QTemporaryFile tmpFile(QDir::tempPath() + "/fitswcsXXXXXX"); // tmpFile.setAutoRemove(false); // tmpFile.open(); // QString newWCSFile = tmpFile.fileName(); // tmpFile.close(); QString newWCSFile = QDir::tempPath() + QString("/fitswcs%1").arg(QUuid::createUuid().toString().remove(QRegularExpression("[-{}]"))); //alignView->setLoadWCSEnabled(true); 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->createWCSFile(newWCSFile, 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) { // Second capture WCS is not important. Since it consumes quite a bit of resources, skip it #if 0 if (Options::limitedResourcesMode() == false) { rc = alignView->createWCSFile(newWCSFile, orientation, ra, dec, pixscale); if (rc == false) { appendLogText(i18n("Error creating WCS file: %1", alignView->getImageData()->getLastError())); // Not critical error //return; } } #endif // 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); 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->createWCSFile(newWCSFile, 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; } // Not critical error /* if (result == false) { appendLogText( i18n("Warning: failed to load WCS data in file: %1", alignView->getImageData()->getLastError())); pahStage = PAH_FIRST_ROTATE; PAHWidgets->setCurrentWidget(PAHFirstRotatePage); 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); // 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)) { 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 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_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 Celstial pole location SkyPoint CP(0, (hemisphere == NORTH_HEMISPHERE) ? 90 : -90); QPointF imagePoint; imageData->wcsToPixel(CP, celestialPolePoint, imagePoint); // 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(); // 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(); /* bool rc = currentCCD->setTelescopeType(static_cast(index)); // If false, try to set it to existing known telescope if (rc == false) { focal_length = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryFL : guideFL; aperture = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryAperture : guideAperture; syncTelescopeInfo(); }*/ 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 pependicular 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; remoteSolverR->setEnabled(true); 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()); 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 solverTypeGroup->checkedId(); } QString Align::getPAHMessage() const { switch (pahStage) { case PAH_IDLE: case PAH_FIND_CP: default: 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(); } } 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", binningCombo->currentIndex()+1); settings.insert("solverAction", gotoModeButtonGroup->checkedId()); settings.insert("solverType", solverTypeGroup->checkedId()); 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))->setChecked(true); solverTypeGroup->button(settings["solverType"].toInt(1))->setChecked(true); 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("mountRotation", PAHRotationSpin->value()); settings.insert("refresh", PAHExposure->value()); 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)); } 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 { KMessageBox::error(nullptr, i18n("Invalid FOV.")); FOVOut->setStyleSheet("background-color:red"); } } } diff --git a/kstars/ekos/align/offlineastrometryparser.cpp b/kstars/ekos/align/offlineastrometryparser.cpp index bea8c68bf..9eb826bbb 100644 --- a/kstars/ekos/align/offlineastrometryparser.cpp +++ b/kstars/ekos/align/offlineastrometryparser.cpp @@ -1,482 +1,482 @@ /* Astrometry.net Parser Copyright (C) 2012 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 "offlineastrometryparser.h" #include "align.h" #include "ekos_align_debug.h" #include "ksutils.h" #include "Options.h" #include "kspaths.h" #include namespace Ekos { OfflineAstrometryParser::OfflineAstrometryParser() : AstrometryParser() { astrometryIndex[2.8] = "index-4200"; astrometryIndex[4.0] = "index-4201"; astrometryIndex[5.6] = "index-4202"; astrometryIndex[8] = "index-4203"; astrometryIndex[11] = "index-4204"; astrometryIndex[16] = "index-4205"; astrometryIndex[22] = "index-4206"; astrometryIndex[30] = "index-4207"; astrometryIndex[42] = "index-4208"; astrometryIndex[60] = "index-4209"; astrometryIndex[85] = "index-4210"; astrometryIndex[120] = "index-4211"; astrometryIndex[170] = "index-4212"; astrometryIndex[240] = "index-4213"; astrometryIndex[340] = "index-4214"; astrometryIndex[480] = "index-4215"; astrometryIndex[680] = "index-4216"; astrometryIndex[1000] = "index-4217"; astrometryIndex[1400] = "index-4218"; astrometryIndex[2000] = "index-4219"; // Reset parity on solver failure connect(this, &OfflineAstrometryParser::solverFailed, this, [&]() { parity = QString(); }); } bool OfflineAstrometryParser::init() { #ifdef Q_OS_OSX if (Options::astrometryConfFileIsInternal()) KSUtils::configureDefaultAstrometry(); #endif if (astrometryFilesOK) return true; if (astrometryNetOK() == false) { if (align && align->isEnabled()) KMessageBox::information( nullptr, i18n( "Failed to find astrometry.net binaries. Please ensure astrometry.net is installed and try again."), i18n("Missing astrometry files"), "missing_astrometry_binaries_warning"); return false; } astrometryFilesOK = true; QString solverPath; if (Options::astrometrySolverIsInternal()) solverPath = QCoreApplication::applicationDirPath() + "/astrometry/bin/solve-field"; else solverPath = Options::astrometrySolverBinary(); QProcess solveField; solveField.start("bash", QStringList() << "-c" << (solverPath + " --help | grep Revision")); solveField.waitForFinished(); QString output = solveField.readAllStandardOutput(); qCDebug(KSTARS_EKOS_ALIGN) << "solve-field Revision" << output; if (output.isEmpty() == false) { QString version = output.mid(9, 4); align->appendLogText(i18n("Detected Astrometry.net version %1", version)); if (version <= "0.67" && Options::astrometryUseNoFITS2FITS() == false) { Options::setAstrometryUseNoFITS2FITS(true); align->appendLogText(i18n("Setting astrometry option --no-fits2fits")); } else if (version > "0.67" && Options::astrometryUseNoFITS2FITS()) { Options::setAstrometryUseNoFITS2FITS(false); align->appendLogText(i18n("Turning off option --no-fits2fits")); } } return true; } bool OfflineAstrometryParser::astrometryNetOK() { bool solverOK = false, wcsinfoOK = false; if (Options::astrometrySolverIsInternal()) { QFileInfo solverFileInfo(QCoreApplication::applicationDirPath() + "/astrometry/bin/solve-field"); solverOK = solverFileInfo.exists() && solverFileInfo.isFile(); } else { QFileInfo solverFileInfo(Options::astrometrySolverBinary()); solverOK = solverFileInfo.exists() && solverFileInfo.isFile(); #ifdef Q_OS_LINUX QString confPath = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry")+ QLatin1Literal("/astrometry.cfg"); if (QFileInfo(confPath).exists() == false) createLocalAstrometryConf(); #endif } if (Options::astrometryWCSIsInternal()) { QFileInfo wcsFileInfo(QCoreApplication::applicationDirPath() + "/astrometry/bin/wcsinfo"); wcsinfoOK = wcsFileInfo.exists() && wcsFileInfo.isFile(); } else { QFileInfo wcsFileInfo(Options::astrometryWCSInfo()); wcsinfoOK = wcsFileInfo.exists() && wcsFileInfo.isFile(); } return (solverOK && wcsinfoOK); } void OfflineAstrometryParser::verifyIndexFiles(double fov_x, double fov_y) { static double last_fov_x = 0, last_fov_y = 0; if (last_fov_x == fov_x && last_fov_y == fov_y) return; last_fov_x = fov_x; last_fov_y = fov_y; double fov_lower = 0.10 * fov_x; double fov_upper = fov_x; QStringList indexFiles; QString astrometryDataDir; bool indexesOK = true; if (getAstrometryDataDir(astrometryDataDir) == false) return; QStringList nameFilter("*.fits"); QDir directory(astrometryDataDir); QStringList indexList = directory.entryList(nameFilter); // JM 2018-09-26: Also add locally stored indexes. #ifdef Q_OS_LINUX QDir localAstrometry(KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry")); indexList << localAstrometry.entryList(nameFilter); #endif QString indexSearch = indexList.join(" "); QString startIndex, lastIndex; unsigned int missingIndexes = 0; foreach (float skymarksize, astrometryIndex.keys()) { if (skymarksize >= fov_lower && skymarksize <= fov_upper) { indexFiles << astrometryIndex.value(skymarksize); if (indexSearch.contains(astrometryIndex.value(skymarksize)) == false) { if (startIndex.isEmpty()) startIndex = astrometryIndex.value(skymarksize); lastIndex = astrometryIndex.value(skymarksize); indexesOK = false; missingIndexes++; } } } if (indexesOK == false) { if (missingIndexes == 1) align->appendLogText( i18n("Index file %1 is missing. Astrometry.net would not be able to adequately solve plates until you " "install the missing index files. Download the index files from http://www.astrometry.net", startIndex)); else align->appendLogText(i18n("Index files %1 to %2 are missing. Astrometry.net would not be able to " "adequately solve plates until you install the missing index files. Download the " "index files from http://www.astrometry.net", startIndex, lastIndex)); } } bool OfflineAstrometryParser::getAstrometryDataDir(QString &dataDir) { QString confPath; if (Options::astrometryConfFileIsInternal()) confPath = QCoreApplication::applicationDirPath() + "/astrometry/bin/astrometry.cfg"; else confPath = Options::astrometryConfFile(); QFile confFile(confPath); if (confFile.open(QIODevice::ReadOnly) == false) { KMessageBox::error(nullptr, i18n("Astrometry configuration file corrupted or missing: %1\nPlease set the " "configuration file full path in INDI options.", Options::astrometryConfFile())); return false; } QTextStream in(&confFile); QString line; while (!in.atEnd()) { line = in.readLine(); if (line.isEmpty() || line.startsWith('#')) continue; line = line.trimmed(); if (line.startsWith(QLatin1String("add_path"))) { dataDir = line.mid(9).trimmed(); return true; } } KMessageBox::error(nullptr, i18n("Unable to find data dir in astrometry configuration file.")); return false; } bool OfflineAstrometryParser::startSovler(const QString &filename, const QStringList &args, bool generated) { INDI_UNUSED(generated); QStringList solverArgs = args; // Use parity if it is: 1. Already known from previous solve. 2. This is NOT a blind solve if (Options::astrometryDetectParity() && (parity.isEmpty() == false) && (args.contains("parity") == false) && (args.contains("-3") || args.contains("-L"))) solverArgs << "--parity" << parity; QString confPath; if (Options::astrometryConfFileIsInternal()) confPath = QCoreApplication::applicationDirPath() + "/astrometry/bin/astrometry.cfg"; else { // JM 2018-09-26: On Linux, load the local config file. #ifdef Q_OS_LINUX confPath = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry")+ QLatin1Literal("/astrometry.cfg"); #else confPath = Options::astrometryConfFile(); #endif } solverArgs << "--config" << confPath; QString solutionFile = QDir::tempPath() + "/solution.wcs"; solverArgs << "-W" << solutionFile << filename; fitsFile = filename; solver.clear(); solver = new QProcess(this); #ifdef Q_OS_OSX QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); QString path = env.value("PATH", ""); if (Options::astrometrySolverIsInternal()) { env.insert("PATH", QCoreApplication::applicationDirPath() + "/netpbm/bin:" + QCoreApplication::applicationDirPath() + "/python/bin:/usr/local/bin:" + path); env.insert("PYTHONPATH", QCoreApplication::applicationDirPath() + "/python/bin/site-packages"); } else { env.insert("PATH", "/usr/local/bin:" + path); } solver->setProcessEnvironment(env); #endif connect(solver, SIGNAL(finished(int)), this, SLOT(solverComplete(int))); connect(solver, SIGNAL(readyReadStandardOutput()), this, SLOT(logSolver())); #if QT_VERSION > QT_VERSION_CHECK(5, 6, 0) connect(solver.data(), &QProcess::errorOccurred, this, [&]() { align->appendLogText(i18n("Error starting solver: %1", solver->errorString())); emit solverFailed(); }); #else connect(solver, SIGNAL(error(QProcess::ProcessError)), this, SIGNAL(solverFailed())); #endif solverTimer.start(); QString solverPath; if (Options::astrometrySolverIsInternal()) solverPath = QCoreApplication::applicationDirPath() + "/astrometry/bin/solve-field"; else solverPath = Options::astrometrySolverBinary(); solver->start(solverPath, solverArgs); align->appendLogText(i18n("Starting solver...")); - if (Options::astrometrySolverVerbose()) + if (Options::alignmentLogging()) { QString command = solverPath + ' ' + solverArgs.join(' '); align->appendLogText(command); } return true; } bool OfflineAstrometryParser::stopSolver() { if (solver.isNull() == false) { solver->terminate(); solver->disconnect(); } return true; } void OfflineAstrometryParser::solverComplete(int exist_status) { solver->disconnect(); // TODO use QTemporaryFile later QString solutionFile = QDir::tempPath() + "/solution.wcs"; QFileInfo solution(solutionFile); if (exist_status != 0 || solution.exists() == false) { align->appendLogText(i18n("Solver failed. Try again.")); emit solverFailed(); return; } connect(&wcsinfo, SIGNAL(finished(int)), this, SLOT(wcsinfoComplete(int))); QString wcsPath; if (Options::astrometryWCSIsInternal()) wcsPath = QCoreApplication::applicationDirPath() + "/astrometry/bin/wcsinfo"; else wcsPath = Options::astrometryWCSInfo(); wcsinfo.start(wcsPath, QStringList(solutionFile)); } void OfflineAstrometryParser::wcsinfoComplete(int exist_status) { wcsinfo.disconnect(); if (exist_status != 0) { align->appendLogText(i18n("WCS header missing or corrupted. Solver failed.")); emit solverFailed(); return; } QString wcsinfo_stdout = wcsinfo.readAllStandardOutput(); QStringList wcskeys = wcsinfo_stdout.split(QRegExp("[\n]")); QStringList key_value; double ra = 0, dec = 0, orientation = 0, pixscale = 0; for (auto &key : wcskeys) { key_value = key.split(' '); if (key_value.size() > 1) { if (key_value[0] == "ra_center") ra = key_value[1].toDouble(); else if (key_value[0] == "dec_center") dec = key_value[1].toDouble(); else if (key_value[0] == "orientation_center") orientation = key_value[1].toDouble(); else if (key_value[0] == "pixscale") pixscale = key_value[1].toDouble(); else if (key_value[0] == "parity") parity = (key_value[1].toInt() == 0) ? "pos" : "neg"; } } int elapsed = static_cast(round(solverTimer.elapsed() / 1000.0)); align->appendLogText(i18np("Solver completed in %1 second.", "Solver completed in %1 seconds.", elapsed)); emit solverFinished(orientation, ra, dec, pixscale); } void OfflineAstrometryParser::logSolver() { - if (Options::astrometrySolverVerbose()) + if (Options::alignmentLogging()) align->appendLogText(solver->readAll().trimmed()); } bool OfflineAstrometryParser::createLocalAstrometryConf() { bool rc = false; QString confPath = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry") + QLatin1Literal("/astrometry.cfg"); QString systemConfPath = Options::astrometryConfFile(); // Check if directory already exists, if it doesn't create one QDir writableDir(KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry")); if (writableDir.exists() == false) { rc = writableDir.mkdir(KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry")); if (rc == false) { qCCritical(KSTARS_EKOS_ALIGN) << "Failed to create local astrometry directory"; return false; } } // Now copy system astrometry.cfg to local directory rc = QFile(systemConfPath).copy(confPath); if (rc == false) { qCCritical(KSTARS_EKOS_ALIGN) << "Failed to copy" << systemConfPath << "to" << confPath; return false; } QFile localConf(confPath); // Open file and add our own path to it if (localConf.open(QFile::ReadWrite)) { QString all = localConf.readAll(); QStringList lines = all.split("\n"); for (int i=0; i < lines.count(); i++) { if (lines[i].startsWith("add_path")) { lines.insert(i+1, QString("add_path %1astrometry").arg(KSPaths::writableLocation(QStandardPaths::GenericDataLocation))); break; } } // Clear contents localConf.resize(0); // Now write back all the lines including our own inserted above QTextStream out(&localConf); for(const QString &line : lines) out << line << endl; localConf.close(); return true; } qCCritical(KSTARS_EKOS_ALIGN) << "Failed to open local astrometry config" << confPath; return false; } } diff --git a/kstars/ekos/align/onlineastrometryparser.cpp b/kstars/ekos/align/onlineastrometryparser.cpp index 31caed101..1fe984060 100644 --- a/kstars/ekos/align/onlineastrometryparser.cpp +++ b/kstars/ekos/align/onlineastrometryparser.cpp @@ -1,494 +1,494 @@ /* Astrometry.net Parser Copyright (C) 2012 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 "onlineastrometryparser.h" #include "align.h" #include "Options.h" #include #include #include #include #include #include #include #include #include #define JOB_RETRY_DURATION 2000 /* 2000 ms */ #define JOB_RETRY_ATTEMPTS 90 #define SOLVER_RETRY_DURATION 2000 /* 2000 ms */ #define INVALID_VALUE -1000 namespace Ekos { OnlineAstrometryParser::OnlineAstrometryParser() : AstrometryParser() { networkManager = new QNetworkAccessManager(this); parity = INVALID_VALUE; connect(this, SIGNAL(authenticateFinished()), this, SLOT(uploadFile())); connect(this, SIGNAL(uploadFinished()), this, SLOT(getJobID())); connect(this, SIGNAL(jobIDFinished()), this, SLOT(checkJobs())); connect(this, SIGNAL(jobFinished()), this, SLOT(checkJobCalibration())); // Reset parity on solver failure connect(this, &OnlineAstrometryParser::solverFailed, this, [&]() { parity = INVALID_VALUE; }); connect(this, SIGNAL(solverFailed()), this, SLOT(resetSolver())); connect(this, SIGNAL(solverFinished(double,double,double,double)), this, SLOT(resetSolver())); } bool OnlineAstrometryParser::init() { return true; } void OnlineAstrometryParser::verifyIndexFiles(double fov_x, double fov_y) { (void)fov_x; (void)fov_y; } bool OnlineAstrometryParser::startSovler(const QString &in_filename, const QStringList &args, bool generated) { bool ok = false; isGenerated = generated; job_retries = 0; //if (networkManager->networkAccessible() == false) // 2018-03-08 JM: Multiple users reported that network accessible is now failing if there is no internet connection // But LAN connectoin is established leading to failure of online solver with a local astrometry solver like ANSVR. // Therefore I am adding the exception below to remedy this situation for now. // 2018-07-13: Due to this bug --> https://bugreports.qt.io/browse/QTBUG-68613 // The check will be disabled until it is resolved. #if 0 if (networkManager->networkAccessible() == false && Options::astrometryAPIURL().contains("127.0.0.1") == false) { align->appendLogText(i18n("Error: no connection to the Internet.")); emit solverFailed(); return false; } #endif // Reset params center_ra = center_dec = downsample_factor = lowerScale = upperScale = INVALID_VALUE; units.clear(); useWCSCenter = false; filename = in_filename; for (int i = 0; i < args.count(); i++) { if (args[i] == "-L") lowerScale = args[i + 1].toDouble(&ok); else if (args[i] == "-H") upperScale = args[i + 1].toDouble(&ok); else if (args[i] == "-3") center_ra = args[i + 1].toDouble(&ok); else if (args[i] == "-4") center_dec = args[i + 1].toDouble(&ok); else if (args[i] == "-5") radius = args[i + 1].toDouble(&ok); else if (args[i] == "--downsample") downsample_factor = args[i + 1].toInt(&ok); else if (args[i] == "-u") { QString unitType = args[i + 1]; if (unitType == "aw") units = "arcminwidth"; else if (unitType == "dw") units = "degwidth"; else units = "arcsecperpix"; } else if (args[i] == "--crpix_center") useWCSCenter = true; else if (args[i] == "--parity") { QString arg = args[i + 1]; if (arg == "both") parity = 2; else if (arg == "pos") parity = 0; else parity = 1; } } connect(networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(onResult(QNetworkReply*))); solverTimer.start(); if (sessionKey.isEmpty()) authenticate(); else uploadFile(); return true; } void OnlineAstrometryParser::resetSolver() { stopSolver(); } bool OnlineAstrometryParser::stopSolver() { workflowStage = NO_STAGE; solver_retries = 0; disconnect(networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(onResult(QNetworkReply*))); return true; } void OnlineAstrometryParser::authenticate() { QNetworkRequest request; //request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); QUrl url(Options::astrometryAPIURL()); url.setPath("/api/login"); request.setUrl(url); QVariantMap apiReq; apiReq.insert("apikey", Options::astrometryAPIKey()); //QByteArray json = serializer.serialize(apiReq, &ok); QJsonObject json = QJsonObject::fromVariantMap(apiReq); QJsonDocument json_doc(json); QString json_request = QString("request-json=%1").arg(QString(json_doc.toJson(QJsonDocument::Compact))); workflowStage = AUTH_STAGE; networkManager->post(request, json_request.toUtf8()); } void OnlineAstrometryParser::uploadFile() { QNetworkRequest request; QFile *fitsFile = new QFile(filename); bool rc = fitsFile->open(QIODevice::ReadOnly); if (rc == false) { align->appendLogText(i18n("Failed to open the file %1: %2", filename, fitsFile->errorString())); delete (fitsFile); emit solverFailed(); return; } QUrl url(Options::astrometryAPIURL()); url.setPath("/api/upload"); request.setUrl(url); QHttpMultiPart *reqEntity = new QHttpMultiPart(QHttpMultiPart::FormDataType); QVariantMap uploadReq; uploadReq.insert("publicly_visible", "n"); uploadReq.insert("allow_modifications", "n"); uploadReq.insert("session", sessionKey); uploadReq.insert("allow_commercial_use", "n"); // Do we have the units? if (units.isEmpty() == false) { uploadReq.insert("scale_type", "ul"); uploadReq.insert("scale_units", units); uploadReq.insert("scale_lower", lowerScale); uploadReq.insert("scale_upper", upperScale); } // Do we send RA/DE? if (center_ra != INVALID_VALUE) { uploadReq.insert("center_ra", center_ra); uploadReq.insert("center_dec", center_dec); uploadReq.insert("radius", radius); } if (useWCSCenter) uploadReq.insert("crpix_center", true); if (downsample_factor != INVALID_VALUE) uploadReq.insert("downsample_factor", downsample_factor); // If we have parity and option is valid and this is NOT a blind solve (if ra or units exist then it is not a blind solve) // then we send parity if (Options::astrometryDetectParity() && parity != INVALID_VALUE && (center_ra != INVALID_VALUE || units.isEmpty() == false)) uploadReq.insert("parity", parity); QJsonObject json = QJsonObject::fromVariantMap(uploadReq); QJsonDocument json_doc(json); QHttpPart jsonPart; jsonPart.setHeader(QNetworkRequest::ContentTypeHeader, "application/text/plain"); jsonPart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"request-json\""); jsonPart.setBody(json_doc.toJson(QJsonDocument::Compact)); QHttpPart filePart; filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream"); filePart.setHeader(QNetworkRequest::ContentDispositionHeader, QString("form-data; name=\"file\"; filename=\"%1\"").arg(filename)); filePart.setBodyDevice(fitsFile); // Re-parent so that it get deleted later fitsFile->setParent(reqEntity); reqEntity->append(jsonPart); reqEntity->append(filePart); workflowStage = UPLOAD_STAGE; align->appendLogText(i18n("Uploading file...")); QNetworkReply *reply = networkManager->post(request, reqEntity); // The entity should be deleted when reply is finished reqEntity->setParent(reply); } void OnlineAstrometryParser::getJobID() { QNetworkRequest request; QUrl getJobID = QUrl(QString("%1/api/submissions/%2").arg(Options::astrometryAPIURL()).arg(subID)); request.setUrl(getJobID); workflowStage = JOB_ID_STAGE; networkManager->get(request); } void OnlineAstrometryParser::checkJobs() { //qDebug() << "with jobID " << jobID << endl; QNetworkRequest request; QUrl getJobStatus = QUrl(QString("%1/api/jobs/%2").arg(Options::astrometryAPIURL()).arg(jobID)); request.setUrl(getJobStatus); workflowStage = JOB_STATUS_STAGE; networkManager->get(request); } void OnlineAstrometryParser::checkJobCalibration() { QNetworkRequest request; QUrl getCablirationResult = QUrl(QString("%1/api/jobs/%2/calibration").arg(Options::astrometryAPIURL()).arg(jobID)); request.setUrl(getCablirationResult); workflowStage = JOB_CALIBRATION_STAGE; networkManager->get(request); } void OnlineAstrometryParser::onResult(QNetworkReply *reply) { bool ok=false; QJsonParseError parseError; QString status; QList jsonArray; uint32_t elapsed=0; if (workflowStage == NO_STAGE) { reply->abort(); return; } if (reply->error() != QNetworkReply::NoError) { align->appendLogText(reply->errorString()); emit solverFailed(); return; } QString json = (QString)reply->readAll(); QJsonDocument json_doc = QJsonDocument::fromJson(json.toUtf8(), &parseError); if (parseError.error != QJsonParseError::NoError) { align->appendLogText(i18n("JSON error during parsing (%1).", parseError.errorString())); emit solverFailed(); return; } QVariant json_result = json_doc.toVariant(); QVariantMap result = json_result.toMap(); - if (Options::astrometrySolverVerbose()) + if (Options::alignmentLogging()) align->appendLogText(json_doc.toJson(QJsonDocument::Compact)); switch (workflowStage) { case AUTH_STAGE: status = result["status"].toString(); if (status != "success") { align->appendLogText( i18n("Astrometry.net authentication failed. Check the validity of the Astrometry.net API Key.")); emit solverFailed(); return; } sessionKey = result["session"].toString(); - if (Options::astrometrySolverVerbose()) + if (Options::alignmentLogging()) align->appendLogText(i18n("Authentication to astrometry.net is successful. Session: %1", sessionKey)); emit authenticateFinished(); break; case UPLOAD_STAGE: status = result["status"].toString(); if (status != "success") { align->appendLogText(i18n("Upload failed.")); emit solverFailed(); return; } subID = result["subid"].toInt(&ok); if (ok == false) { align->appendLogText(i18n("Parsing submission ID failed.")); emit solverFailed(); return; } align->appendLogText(i18n("Upload complete. Waiting for astrometry.net solver to complete...")); emit uploadFinished(); if (isGenerated) QFile::remove(filename); break; case JOB_ID_STAGE: jsonArray = result["jobs"].toList(); if (jsonArray.isEmpty()) jobID = 0; else jobID = jsonArray.first().toInt(&ok); if (jobID == 0 || !ok) { if (job_retries++ < JOB_RETRY_ATTEMPTS) { QTimer::singleShot(JOB_RETRY_DURATION, this, SLOT(getJobID())); return; } else { align->appendLogText(i18n("Failed to retrieve job ID.")); emit solverFailed(); return; } } job_retries = 0; emit jobIDFinished(); break; case JOB_STATUS_STAGE: status = result["status"].toString(); elapsed = static_cast(round(solverTimer.elapsed() / 1000.0)); if (status == "success") emit jobFinished(); else if (status == "solving" || status == "processing") { //if (status == "solving" && solver_retries++ < SOLVER_RETRY_ATTEMPTS) if (elapsed < Options::astrometryTimeout()) { QTimer::singleShot(SOLVER_RETRY_DURATION, this, SLOT(checkJobs())); return; } else { align->appendLogText(i18n("Solver timed out.")); solver_retries = 0; emit solverFailed(); return; } } else if (status == "failure") { align->appendLogText( i18np("Solver failed after %1 second.", "Solver failed after %1 seconds.", elapsed)); emit solverFailed(); return; } break; case JOB_CALIBRATION_STAGE: parity = result["parity"].toInt(&ok); if (ok == false) { align->appendLogText(i18n("Error parsing parity.")); emit solverFailed(); return; } orientation = result["orientation"].toDouble(&ok); if (ok == false) { align->appendLogText(i18n("Error parsing orientation.")); emit solverFailed(); return; } orientation *= parity; ra = result["ra"].toDouble(&ok); if (ok == false) { align->appendLogText(i18n("Error parsing RA.")); emit solverFailed(); return; } dec = result["dec"].toDouble(&ok); if (ok == false) { align->appendLogText(i18n("Error parsing DEC.")); emit solverFailed(); return; } pixscale = result["pixscale"].toDouble(&ok); if (ok == false) { align->appendLogText(i18n("Error parsing DEC.")); emit solverFailed(); return; } elapsed = (int)round(solverTimer.elapsed() / 1000.0); align->appendLogText(i18np("Solver completed in %1 second.", "Solver completed in %1 seconds.", elapsed)); emit solverFinished(orientation, ra, dec, pixscale); break; default: break; } } } diff --git a/kstars/ekos/align/opsalign.ui b/kstars/ekos/align/opsalign.ui index 707077d33..1f47cc3b0 100644 --- a/kstars/ekos/align/opsalign.ui +++ b/kstars/ekos/align/opsalign.ui @@ -1,248 +1,235 @@ OpsAlign 0 0 422 150 Timeout in seconds to wait for astrometry solver to complete 30 600 config: solver: Astrometry.net wcsinfo binary Astrometry.net solve-field binary Astrometry.net configuration file Astrometry.net configuration file Astrometry.net configuration file Time out: API Key: API URL: wcsinfo: <html><head/><body><p>On Load &amp; Slew, solve the image and slew the mount to the target location and then rotate the camera to match the orientation of the FITS image.</p></body></html> Rotator: <html><head/><body><p>Rotator threshold in arc-minutes when using Load &amp; Slew. If the difference between measured position angle and FITS position angle is below this value, the Load &amp; Slew operation is considered successful.</p></body></html> enable World Coordinate System (WCS). WCS is used to encode RA/DEC coordinates in captured CCD images. WCS - - - - Log verbose solver output - - - - - - Verbose - - - Display received FITS images unto solver FOV rectangle in the sky map Overlay Use JPEG format, instead of FITS, to upload images to the online astrometry.net service Upload JPG true Qt::Horizontal 40 20 Qt::Vertical 20 3 diff --git a/kstars/kstars.kcfg b/kstars/kstars.kcfg index 0f2e19d5c..9fd08fcf0 100644 --- a/kstars/kstars.kcfg +++ b/kstars/kstars.kcfg @@ -1,2238 +1,2234 @@ ksutils.h The screen coordinates of the Time InfoBox. QPoint(0,0) The screen coordinates of the Focus InfoBox. QPoint(600,0) The screen coordinates of the Geographic Location InfoBox. QPoint(0,600) If true, the Time InfoBox will show only its top line of data. true If true, the Focus InfoBox will show only its top line of data. true If true, the Geographic Location InfoBox will show only its top line of data. true Toggles display of all three InfoBoxes. true Toggles display of the Time InfoBox. true Toggles display of the Focus InfoBox. true Toggles display of the Geographic Location InfoBox. true Is the Time InfoBox anchored to a window edge? 0 = not anchored; 1 = anchored to right edge; 2 = anchored to bottom edge; 3 = anchored to bottom and right edges. 0 0 3 Is the Focus InfoBox anchored to a window edge? 0 = not anchored; 1 = anchored to right edge; 2 = anchored to bottom edge; 3 = anchored to bottom and right edges. 1 0 3 Is the Geographic Location InfoBox anchored to a window edge? 0 = not anchored; 1 = anchored to right edge; 2 = anchored to bottom edge; 3 = anchored to bottom and right edges. 2 0 3 Toggle display of the status bar. true Toggle display of the Horizontal coordinates of the mouse cursor in the status bar. true Toggle display of the Equatorial coordinates of the mouse cursor at the current epoch in the status bar. true Toggle display of the Equatorial coordinates of the mouse cursor at the standard epoch in the status bar. false true 1024 768 true Black Body List of the filenames of custom object catalogs. List of integers toggling display of each custom object catalog (any nonzero value indicates the objects in that catalog will be displayed). List of names for which custom catalogs are to be displayed. Names of objects entered into the find dialog are resolved using online services and stored in the database. This option also toggles the display of such resolved objects on the sky map. true 800 600 true true false Toggle display of crosshairs centered at telescope's pointed position in the KStars sky map. true Toggle display of INDI messages in the KStars statusbar. true Show INDI messages as desktop notifications instead of dialogs. false true false false The default location of saved FITS files KSUtils::getDefaultPath("fitsDir") INDI server will attempt to bind with ports starting from this port 7624 INDI server will attempt to bind with ports ending with this port 9000 List of the aliases for filter wheel slots. PATH to indiserver binary KSUtils::getDefaultPath("indiServer") false PATH to indi drivers directory KSUtils::getDefaultPath("indiDriversDir") false 320 240 false false false false false false false false false false false The City name of the current geographic location. Greenwich The Province name of the current geographic location. This is the name of the state for locations in the U. S. The Country name of the current geographic location. United Kingdom The longitude of the current geographic location, in decimal degrees. 0.0 The latitude of the current geographic location, in decimal degrees. 51.468 -10.0 0.0 Two-letter code that determines the dates on which daylight savings time begins and ends (you can view the rules by pressing the "Explain DST Rules" button in the Geographic Location window). -- If true, focus changes will cause the sky to visibly spin to the new position. Otherwise, the display will "snap" instantly to the new position. true The names of the currently selected field-of-view indicators. The list of defined FOV indicator names is listed in the "Settings|FOV Symbols" menu. Telrad If true, trails attached to solar system bodies will fade into the background sky color. true The right ascension of the initial focus position of the sky map, in decimal hours. This value is volatile; it is reset whenever the program shuts down. 180.0 The declination of the initial focus position of the sky map, in decimal degrees. This value is volatile; it is reset whenever the program shuts down. 45.0 The name of the object that should be centered and tracked on startup. If no object should be centered, set to "nothing". This value is volatile; it is reset whenever the program shuts down. nothing True if the skymap should track on its initial position on startup. This value is volatile; it is reset whenever the program shuts down. false Toggle whether KStars should hide some objects while the display is moving, for smoother motion. true Toggle whether constellation boundaries are hidden while the display is in motion. true Toggle whether constellation lines are hidden while the display is in motion. false Choose sky culture. 11 Toggle whether constellation names are hidden while the display is in motion. false Toggle whether the coordinate grids are hidden while the display is in motion. true Toggle whether the Milky Way contour is hidden while the display is in motion. true Toggle whether IC objects are hidden while the display is in motion. true Toggle whether Messier objects are hidden while the display is in motion. true Toggle whether NGC objects are hidden while the display is in motion. true Toggle whether extra objects are hidden while the display is in motion. true Toggle whether solar system objects are hidden while the display is in motion. false Toggle whether faint stars are hidden while the display is in motion. true Toggle whether name labels are hidden while the display is in motion. true Toggle whether asteroids are drawn in the sky map. true Toggle whether asteroid name labels are drawn in the sky map. false Toggle whether comets are drawn in the sky map. true Toggle whether comet comas are drawn in the sky map. true Toggle whether comet name labels are drawn in the sky map. false Toggle whether supernovae are drawn in the sky map. false Toggle whether supernova name labels are drawn in the sky map. false Set magnitude limit for supernovae to be shown on the skymap. 16 Toggle supernova alerts. true Set magnitude limit for supernovae to be alerted. 13 Toggle whether constellation boundaries are drawn in the sky map. false Toggle whether constellation boundary containing the central focus point is highlighted in the sky map. false Toggle whether constellation lines are drawn in the sky map. false Toggle whether constellation art drawn in the sky map. false Toggle whether constellation name labels are drawn in the sky map. false Toggle whether deep-sky objects are drawn in the sky map. true Toggle whether the ecliptic line is drawn in the sky map. false Toggle whether the equator line is drawn in the sky map. false Coordinate grids will automatically change according to active coordinate system. true Toggle whether the equatorial coordinate grid is drawn in the sky map. false Toggle whether the horizontal coordinate grid is drawn in the sky map. false Toggle whether the local meridian line is drawn in the sky map. false Toggle whether the region below the horizon is opaque. true Toggle whether the horizon line is drawn in the sky map. true Toggle whether flags are drawn in the sky map. true Toggle whether IC objects are drawn in the sky map. false Toggle whether NGC objects are drawn in the sky map. true Toggle whether Messier objects are drawn in the sky map. true Toggle whether Messier objects are rendered as images in the sky map. true Toggle whether extra objects are drawn in the sky map. true Toggle whether the Milky Way contour is drawn in the sky map. true Toggle whether the Milky Way contour is filled. When this option is false, the Milky Way is shown as an outline. true Meta-option to control whether all major planets (and the Sun and Moon) are drawn in the sky map. true Toggle whether major planets (and the Sun and Moon) are rendered as images in the sky map. true Toggle whether major planets (and the Sun and Moon) are labeled in the sky map. true Toggle whether the Sun is drawn in the sky map. true Toggle whether the Moon is drawn in the sky map. true Toggle whether Mercury is drawn in the sky map. true Toggle whether Venus is drawn in the sky map. true Toggle whether Mars is drawn in the sky map. true Toggle whether Jupiter is drawn in the sky map. true Toggle whether Saturn is drawn in the sky map. true Toggle whether Uranus is drawn in the sky map. true Toggle whether Neptune is drawn in the sky map. true Toggle whether Pluto is drawn in the sky map. true Toggle whether stars are drawn in the sky map. true Toggle whether star magnitude (brightness) labels are shown in the sky map. false Toggle whether star name labels are shown in the sky map. true Toggle whether deep-sky object magnitude (brightness) labels are shown in the sky map. false Toggle whether deep-sky object name labels are shown in the sky map. false The timescale above which slewing mode is forced on at all times. 60 The background fill mode for the on-screen information boxes: 0="no BG"; 1="semi-transparent BG"; 2="opaque BG" 1 Algorithm for the mapping projection. 0 Use official IAU abbreviations for constellation names. false Use Latin constellation names. false Use localized constellation names (if localized names are not available, default to Latin names). true Display the sky with horizontal coordinates (when false, equatorial coordinates will be used). true Toggle whether a centered object automatically gets a name label attached. true Toggle whether a centered solar system object automatically gets a trail attached, as long as it remains centered. true Toggle whether the object under the mouse cursor gets a transient name label. true Toggle whether object positions are corrected for the effects of atmospheric refraction (only applies when horizontal coordinates are used). true Toggle whether corrections due to bending of light around the sun are taken into account false Toggle whether the sky is rendered using antialiasing. Lines and shapes are smoother with antialiasing, but rendering the screen will take more time. true The zoom level, measured in pixels per radian. 250. 250. 5000000. When zooming in or out, change zoom speed factor by this multiplier. 0.2 0.01 1.0 The faint magnitude limit for drawing asteroids. 15.0 The maximum magnitude (visibility) to filter the asteroid data download from JPL. 12.000 Controls the relative number of asteroid name labels drawn in the map. 4.0 The faint magnitude limit for drawing deep-sky objects, when fully zoomed in. 16.0 The faint magnitude limit for drawing deep-sky objects, when fully zoomed out. 5.0 When enabled, objects whose magnitudes are unknown, or not available to KStars, are drawn irrespective of the faint limits set. true Sets the density of stars in the field of view 5 The faint magnitude limit for drawing stars, when the map is in motion (only applicable if faint stars are set to be hidden while the map is in motion). 5.0 The relative density for drawing star name and magnitude labels. 2.0 The relative density for drawing deep-sky object name and magnitude labels. 5.0 If true, long names (common names) for deep-sky objects are shown in the labels. false The maximum solar distance for drawing comets. 3.0 Use experimental OpenGL backend (deprecated). false The state of the clock (running or not) true Objects in the observing list will be highlighted with a symbol in the map. true Objects in the observing list will be highlighted with a colored name label in the map. false The observing list will prefer DSS imagery while downloading imagery. true The observing list will prefer SDSS imagery while downloading imagery. false Check this if you use a large Dobsonian telescope. Sorting by percentage current altitude is an easy way of determining what objects are well-placed for observation. However, when using a large Dobsonian telescope, objects close to the zenith are hard to observe. Since tracking there corresponds to a rotation in azimuth, it is both counterintuitive and requires the observer to frequently move the ladder. The region around the zenith where this is particularly frustrating is called the Dobsonian hole. This checkbox makes the observing list consider objects present in the hole as unfit for observation. false This specifies the angular radius of the Dobsonian hole, i.e. the region where a large Dobsonian telescope cannot be pointed easily. 15.00 The name of the color scheme moonless-night.colors The method for rendering stars: 0="realistic colors"; 1="solid red"; 2="solid black"; 3="solid white"; 4="solid real colors" 0 4 The color saturation level of stars (only applicable when using "realistic colors" mode). 6 10 The color for the angular-distance measurement ruler. #FFF The background color of the on-screen information boxes. #000 The text color for the on-screen information boxes, when activated by a mouse click. #F00 The normal text color of the on-screen information boxes. #FFF The color for the constellation boundary lines. #222 The color for the constellation boundary lines. #222 The color for the constellation figure lines. #555 The color for the constellation names. #AA7 The color for the cardinal compass point labels. #002 The color for the ecliptic line. #663 The color for the equator line. #FFF The color for the equatorial coordinate grid lines. #456 The color for the horizontal coordinate grid lines. #5A3 The color for objects which have extra URL links available. #A00 The color for the horizon line and opaque ground. #5A3 The color for the local meridian line. #0059b3 The color for Messier object symbols. #0F0 The color for NGC object symbols. #066 The color for IC object symbols. #439 The color for the Milky Way contour. #123 The color for star name labels. #7AA The color for deep-sky object name labels. #7AA The color for solar system object labels. #439 The color for solar system object trails. #963 The color for the sky background. #002 The color for the artificial horizon region. #C82828 The color for telescope target symbols. #8B8 Color of visible satellites. #00FF00 Color of invisible satellites. #FF0000 Color of satellites labels. #640000 Color of supernova #FFA500 The color for user-added object labels. #439 The color for RA Guide Error bar in Ekos guide module. #00FF00 The color for DEC Guide Error bar in Ekos guide module. #00A5FF The color for solver FOV box in Ekos alignment module. #FFFF00 false Xplanet binary path KSUtils::getDefaultPath("XplanetPath") Option to use a FIFO file instead of saving to the hard disk true How long to wait for XPlanet before giving up in milliseconds 1000 How long to pause between frames in the XPlanet Animation 100 Width of xplanet window 640 Height of xplanet window 480 If true, display a label in the upper right corner. false Show local time. true Show GMT instead of local time. false Specify the text of the first line of the label. By default, it says something like "Looking at Earth". Any instances of %t will be replaced by the target name, and any instances of %o will be replaced by the origin name. Specify the point size. 12 Set the color for the label. #F00 Specify the format for the date/time label. This format string is passed to strftime(3). The default is "%c %Z", which shows the date, time, and time zone in the locale’s appropriate date and time representation. %c %Z false true false false Draw a glare around the sun with a radius of the specified value larger than the Sun. The default value is 28. 28 Place the observer above a random latitude and longitude false Place the observer above the specified longitude and latitude true Render the target body as seen from above the specified latitude (in degrees). The default value is 0. 0 Place the observer above the specified longitude (in degrees). Longitude is positive going east, negative going west (for the earth and moon), so for example Los Angeles is at -118 or 242. The default value is 0. 0 The default is no projection. Multiple bodies will not be shown if this option is specified, although shadows will still be drawn. 0 Use a file as the background image, with the planet to be superimposed upon it. This option is only meaningful with the -projection option. A color may also be supplied. false Use a file as the background image. false The path of the background image. Use a color as the background. true The color of the background. #000 A star of the specified magnitude will have a pixel brightness of 1. The default value is 10. Stars will be drawn more brightly if this number is larger. 10 If checked, use an arc file to be plotted against the background stars. false Specify an arc file to be plotted against the background stars. If checked, use a config file. false Use the specified configuration file. If checked, use kstars's FOV. false If checked, use the specified marker file. false Specify a file containing user-defined marker data to display against the background stars. If checked, write coordinates of the bounding box for each marker in a file. false Write coordinates of the bounding box for each marker to this file. If checked, use star map file to draw the background stars. false Star map file path This option is only used when creating JPEG images. The quality can range from 0 to 100. The default value is 80. 80 Toggle whether satellite tracks are drawn in the sky map. false Toggle whether satellite tracks are drawn in the sky map. false If selected, satellites will be draw like stars, otherwise, draw satellites as small colored square. false Toggle whether satellite labels are drawn in the sky map. false List of selected satellites. Checking this option causes recomputation of current equatorial coordinates from catalog coordinates (i.e. application of precession, nutation and aberration corrections) for every redraw of the map. This makes processing slower when there are many stars to handle, but is more likely to be bug free. There are known bugs in the rendering of stars when this recomputation is avoided. false The default size for DSS images downloaded from the Internet. 15.0 To include parts of the star field, we add some extra padding around DSS images of deep-sky objects. This option configures the total (both sides) padding added to either dimension of the field. 10.0 Checking this option causes KStars to generate verbose debug information for diagnostic purposes. This may cause slowdown of KStars. false Checking this option causes KStars to generate regular debug information. true Checking this option causes KStars to stop generating ANY debug information. false Checking this option causes KStars log debug messages to the default output used by the platform (e.g. Standard Error). true Checking this option causes KStars log debug messages to a log file as specified. false Log FITS Data activity. false Log INDI devices activity. false Log Ekos Capture Module activity. false Log Ekos Focus Module activity. false Log Ekos Guide Module activity. false Log Ekos Alignment Module activity. false Log Ekos Mount Module activity. false true Display all captured FITS images in a single tab instead of multiple tabs per image. true Display all captured FITS images in a single FITS Viewer window. By default each camera create its own FITS Viewer instance false Display all opened FITS images in a single FITS Viewer window. true false true true true false 4 false false 40.0 0 600 600 true false true false Simulators false true false 1 Minimum telescope altitude limit. If the telescope is below this limit, it will be commanded to stop. 0 Maximum telescope altitude limit. If the telescope is above this limit, it will be commanded to stop. 90.0 false false 1 0 If guide deviation exceeds this limit, the exposure will be automatically aborted and only resumed when the deviation is within this limit. 2 If HFR deviation exceeds this limit, the autofocus routine will be automatically started. 0.5 false false false Sets the time interval before forced autofocus attempts during a capture sequence. 60 If the target hour angle exceeds this value, Ekos will command a meridian flip and if successful it will resume guiding and capture operations. false false If set, Ekos will capture a few flat images to determine the optimal exposure time to achieve the desired ADU value. 0 Maximum difference between measured and target ADU values to deem the value as acceptable. 1000 0 0 0 0 0.1 0 false false 2.5 false 1 30 true true true 0 KSUtils::getDefaultPath("fitsDir") Step size of the absolute focuser. The step size TICKS should be adjusted so that when the focuser moves TICKS steps, the difference in HFR is more than 0.1 pixels. Lower the value when you are close to optimal focus. 100 Wait for this many seconds after moving the focuser before capturing the next image during AutoFocus. 0 The tolerance specifies the percentage difference between the current focusing position and the minimum obtained during the focusing run. Adjustment of this value is necessary to prevent the focusing algorithm from oscillating back and forth. 1 Set the maximum travel distance of an absolute focuser. 10000 Specifies exposure value of CCD when performing focusing. Lower this value to avoid saturation of bright stars which adversely affects HFR measurement. Increase the value if no stars are detected. 0.5 Set box size to select a focus star. 64 Set horizontal binning of CCD camera while in focus mode. 1 Set vertical binning of CCD camera while in focus mode. 1 true false false true false 0 150 0 0 1 Specifies exposure value of CCD in seconds when performing plate solving. 1 Set binning index of CCD camera while in alignment mode. Default values 0-3 corresponding to 1x1 to 4x4 binning. 4 is max binning. 4 - - Display detailed verbose messages of the astrometry solver process while in progress. - false - Use rotator when performing load and slew. false Threshold between measured and FITS position angles in arcminutes to consider the load and slew operation successful. 30 0 0 0 true false false 30 0 false 1500 true true true true true 1 true 2 true true 30 false Path to astrometry.net solver location. KSUtils::getDefaultPath("AstrometrySolverBinary") false Path to astrometry.net wcsinfo location. KSUtils::getDefaultPath("AstrometryWCSInfo") false Path to astrometry.net file location. KSUtils::getDefaultPath("AstrometryConfFile") false Key to access astrometry.net online web services. You must register with astrometry.net to obtain a key. iczikaqstszeptgs http://nova.astrometry.net true 180 1.0 0 0 localhost 4400 localhost 5656 0 1000 2 false 1 false false false 5 60 10 true false false false 2 1 0 1 45 10 500 false false false 2 true true true true true true 133.33 133.33 0 0 0 0 5000 5000 100 100 0.5 2 true true false false Log Ekos Scheduler Module activity. false Sort scheduler jobs by priority and altitude. true true false false false false true false 2 true 5 30 0 0 0 0 0 0 7624 8624 300 1000 None false false Toggle whether the HIPS sources are drawn in the sky map. false