1 /*
2 SPDX-FileCopyrightText: 2013 Jasem Mutlaq <mutlaqja@ikarustech.com>
3 SPDX-FileCopyrightText: 2013-2021 Jasem Mutlaq <mutlaqja@ikarustech.com>
4 SPDX-FileCopyrightText: 2018-2020 Robert Lancaster <rlancaste@gmail.com>
5 SPDX-FileCopyrightText: 2019-2021 Hy Murveit <hy@murveit.com>
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8 */
9
10 #include "align.h"
11 #include "alignadaptor.h"
12 #include "alignview.h"
13 #include <ekos_align_debug.h>
14
15 // Options
16 #include "Options.h"
17 #include "opsalign.h"
18 #include "opsprograms.h"
19 #include "opsastrometry.h"
20 #include "opsastrometryindexfiles.h"
21
22 // Components
23 #include "mountmodel.h"
24 #include "polaralignmentassistant.h"
25 #include "remoteastrometryparser.h"
26 #include "polaralign.h"
27 #include "manualrotator.h"
28
29 // FITS
30 #include "fitsviewer/fitsdata.h"
31 #include "fitsviewer/fitstab.h"
32
33 // Auxiliary
34 #include "auxiliary/QProgressIndicator.h"
35 #include "auxiliary/ksmessagebox.h"
36 #include "ekos/auxiliary/stellarsolverprofileeditor.h"
37 #include "dialogs/finddialog.h"
38 #include "ksnotification.h"
39 #include "kspaths.h"
40 #include "fov.h"
41 #include "kstars.h"
42 #include "kstarsdata.h"
43 #include "skymapcomposite.h"
44
45 // INDI
46 #include "ekos/manager.h"
47 #include "indi/clientmanager.h"
48 #include "indi/driverinfo.h"
49 #include "indi/indifilter.h"
50 #include "profileinfo.h"
51
52 // System Includes
53 #include <KActionCollection>
54 #include <basedevice.h>
55 #include <indicom.h>
56 #include <memory>
57
58 #define MAXIMUM_SOLVER_ITERATIONS 10
59 #define CAPTURE_RETRY_DELAY 10000
60 #define PAH_CUTOFF_FOV 10 // Minimum FOV width in arcminutes for PAH to work
61 #define CHECK_PAH(x) \
62 m_PolarAlignmentAssistant && m_PolarAlignmentAssistant->x
63 #define RUN_PAH(x) \
64 if (m_PolarAlignmentAssistant) m_PolarAlignmentAssistant->x
65
66 namespace Ekos
67 {
68
69 using PAA = PolarAlignmentAssistant;
70
Align(ProfileInfo * activeProfile)71 Align::Align(ProfileInfo *activeProfile) : m_ActiveProfile(activeProfile)
72 {
73 setupUi(this);
74
75 qRegisterMetaType<Ekos::AlignState>("Ekos::AlignState");
76 qDBusRegisterMetaType<Ekos::AlignState>();
77
78 new AlignAdaptor(this);
79 QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Align", this);
80
81 dirPath = QDir::homePath();
82
83 KStarsData::Instance()->clearTransientFOVs();
84
85 //loadSlewMode = false;
86 solverFOV.reset(new FOV());
87 solverFOV->setName(i18n("Solver FOV"));
88 solverFOV->setObjectName("solver_fov");
89 solverFOV->setLockCelestialPole(true);
90 solverFOV->setColor(KStars::Instance()->data()->colorScheme()->colorNamed("SolverFOVColor").name());
91 solverFOV->setProperty("visible", false);
92 KStarsData::Instance()->addTransientFOV(solverFOV);
93
94 sensorFOV.reset(new FOV());
95 sensorFOV->setObjectName("sensor_fov");
96 sensorFOV->setLockCelestialPole(true);
97 sensorFOV->setProperty("visible", Options::showSensorFOV());
98 KStarsData::Instance()->addTransientFOV(sensorFOV);
99
100 QAction *a = KStars::Instance()->actionCollection()->action("show_sensor_fov");
101 if (a)
102 a->setEnabled(true);
103
104 showFITSViewerB->setIcon(
105 QIcon::fromTheme("kstars_fitsviewer"));
106 showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
107 connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Align::showFITSViewer);
108
109 toggleFullScreenB->setIcon(
110 QIcon::fromTheme("view-fullscreen"));
111 toggleFullScreenB->setShortcut(Qt::Key_F4);
112 toggleFullScreenB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
113 connect(toggleFullScreenB, &QPushButton::clicked, this, &Ekos::Align::toggleAlignWidgetFullScreen);
114
115 alignView = new AlignView(alignWidget, FITS_ALIGN);
116 alignView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
117 alignView->setBaseSize(alignWidget->size());
118 alignView->createFloatingToolBar();
119 QVBoxLayout *vlayout = new QVBoxLayout();
120
121 vlayout->addWidget(alignView);
122 alignWidget->setLayout(vlayout);
123
124 connect(solveB, &QPushButton::clicked, [this]()
125 {
126 updateTargetCoords();
127 captureAndSolve();
128 });
129 connect(stopB, &QPushButton::clicked, this, &Ekos::Align::abort);
130
131 // Effective FOV Edit
132 connect(FOVOut, &QLineEdit::editingFinished, this, &Align::syncFOV);
133
134 connect(CCDCaptureCombo, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated), this,
135 &Ekos::Align::setDefaultCCD);
136 connect(CCDCaptureCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &Ekos::Align::checkCCD);
137
138 connect(loadSlewB, &QPushButton::clicked, [&]()
139 {
140 loadAndSlew();
141 });
142
143 FilterDevicesCombo->addItem("--");
144 connect(FilterDevicesCombo, static_cast<void(QComboBox::*)(const QString &)>(&QComboBox::activated),
145 [ = ](const QString & text)
146 {
147 syncSettings();
148 Options::setDefaultAlignFilterWheel(text);
149 });
150
151 connect(FilterDevicesCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &Ekos::Align::checkFilter);
152
153 connect(FilterPosCombo, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated),
154 [ = ](int index)
155 {
156 syncSettings();
157 Options::setLockAlignFilterIndex(index);
158 }
159 );
160
161 gotoModeButtonGroup->setId(syncR, GOTO_SYNC);
162 gotoModeButtonGroup->setId(slewR, GOTO_SLEW);
163 gotoModeButtonGroup->setId(nothingR, GOTO_NOTHING);
164
165 connect(gotoModeButtonGroup, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), this,
166 [ = ](int id)
167 {
168 this->m_CurrentGotoMode = static_cast<GotoMode>(id);
169 });
170
171 m_CaptureTimer.setSingleShot(true);
172 m_CaptureTimer.setInterval(CAPTURE_RETRY_DELAY);
173 connect(&m_CaptureTimer, &QTimer::timeout, [&]()
174 {
175 if (m_CaptureTimeoutCounter++ > 3)
176 {
177 appendLogText(i18n("Capture timed out."));
178 m_CaptureTimer.stop();
179 abort();
180 }
181 else
182 {
183 ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD);
184 if (targetChip->isCapturing())
185 {
186 appendLogText(i18n("Capturing still running, Retrying in %1 seconds...", m_CaptureTimer.interval() / 500));
187 targetChip->abortExposure();
188 m_CaptureTimer.start( m_CaptureTimer.interval() * 2);
189 }
190 else
191 {
192 setAlignTableResult(ALIGN_RESULT_FAILED);
193 captureAndSolve();
194 }
195 }
196 });
197
198 m_AlignTimer.setSingleShot(true);
199 m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000);
200 connect(&m_AlignTimer, &QTimer::timeout, this, &Ekos::Align::checkAlignmentTimeout);
201
202 m_CurrentGotoMode = static_cast<GotoMode>(Options::solverGotoOption());
203 gotoModeButtonGroup->button(m_CurrentGotoMode)->setChecked(true);
204
205 KConfigDialog *dialog = new KConfigDialog(this, "alignsettings", Options::self());
206
207 #ifdef Q_OS_OSX
208 dialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
209 #endif
210
211 opsAlign = new OpsAlign(this);
212 connect(opsAlign, &OpsAlign::settingsUpdated, this, &Ekos::Align::refreshAlignOptions);
213 KPageWidgetItem *page = dialog->addPage(opsAlign, i18n("StellarSolver Options"));
214 page->setIcon(QIcon(":/icons/StellarSolverIcon.png"));
215
216 opsPrograms = new OpsPrograms(this);
217 page = dialog->addPage(opsPrograms, i18n("External & Online Programs"));
218 page->setIcon(QIcon(":/icons/astrometry.svg"));
219
220 opsAstrometry = new OpsAstrometry(this);
221 page = dialog->addPage(opsAstrometry, i18n("Scale & Position"));
222 page->setIcon(QIcon(":/icons/center_telescope_red.svg"));
223
224 optionsProfileEditor = new StellarSolverProfileEditor(this, Ekos::AlignProfiles, dialog);
225 page = dialog->addPage(optionsProfileEditor, i18n("Align Options Profiles Editor"));
226 connect(optionsProfileEditor, &StellarSolverProfileEditor::optionsProfilesUpdated, this, [this]()
227 {
228 if(QFile(savedOptionsProfiles).exists())
229 m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
230 else
231 m_StellarSolverProfiles = getDefaultAlignOptionsProfiles();
232 opsAlign->reloadOptionsProfiles();
233 });
234 page->setIcon(QIcon::fromTheme("configure"));
235
236 connect(opsAlign, &OpsAlign::needToLoadProfile, this, [this, dialog, page](int profile)
237 {
238 optionsProfileEditor->loadProfile(profile);
239 dialog->setCurrentPage(page);
240 });
241
242 opsAstrometryIndexFiles = new OpsAstrometryIndexFiles(this);
243 m_IndexFilesPage = dialog->addPage(opsAstrometryIndexFiles, i18n("Index Files"));
244 m_IndexFilesPage->setIcon(QIcon::fromTheme("map-flat"));
245
246 appendLogText(i18n("Idle."));
247
248 pi.reset(new QProgressIndicator(this));
249
250 stopLayout->addWidget(pi.get());
251
252 exposureIN->setValue(Options::alignExposure());
253 connect(exposureIN, static_cast<void(QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), [&]()
254 {
255 syncSettings();
256 });
257
258 rememberSolverWCS = Options::astrometrySolverWCS();
259 rememberAutoWCS = Options::autoWCS();
260
261 solverModeButtonGroup->setId(localSolverR, SOLVER_LOCAL);
262 solverModeButtonGroup->setId(remoteSolverR, SOLVER_REMOTE);
263
264 localSolverR->setChecked(Options::solverMode() == SOLVER_LOCAL);
265 remoteSolverR->setChecked(Options::solverMode() == SOLVER_REMOTE);
266 connect(solverModeButtonGroup, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), this,
267 &Align::setSolverMode);
268 setSolverMode(solverModeButtonGroup->checkedId());
269
270 // Which telescope info to use for FOV calculations
271 FOVScopeCombo->setCurrentIndex(Options::solverScopeType());
272 connect(FOVScopeCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
273 &Ekos::Align::updateTelescopeType);
274
275 accuracySpin->setValue(Options::solverAccuracyThreshold());
276 connect(accuracySpin, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), [this]()
277 {
278 Options::setSolverAccuracyThreshold(accuracySpin->value());
279 buildTarget();
280 });
281
282 connect(alignDarkFrameCheck, &QCheckBox::toggled, [this]()
283 {
284 Options::setAlignDarkFrame(alignDarkFrameCheck->isChecked());
285 });
286 alignDarkFrameCheck->setChecked(Options::alignDarkFrame());
287
288 delaySpin->setValue(Options::settlingTime());
289 connect(delaySpin, &QSpinBox::editingFinished, this, &Ekos::Align::saveSettleTime);
290
291 connect(binningCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
292 &Ekos::Align::setBinningIndex);
293
294 double accuracyRadius = accuracySpin->value();
295
296 alignPlot->setBackground(QBrush(Qt::black));
297 alignPlot->setSelectionTolerance(10);
298
299 alignPlot->xAxis->setBasePen(QPen(Qt::white, 1));
300 alignPlot->yAxis->setBasePen(QPen(Qt::white, 1));
301
302 alignPlot->xAxis->setTickPen(QPen(Qt::white, 1));
303 alignPlot->yAxis->setTickPen(QPen(Qt::white, 1));
304
305 alignPlot->xAxis->setSubTickPen(QPen(Qt::white, 1));
306 alignPlot->yAxis->setSubTickPen(QPen(Qt::white, 1));
307
308 alignPlot->xAxis->setTickLabelColor(Qt::white);
309 alignPlot->yAxis->setTickLabelColor(Qt::white);
310
311 alignPlot->xAxis->setLabelColor(Qt::white);
312 alignPlot->yAxis->setLabelColor(Qt::white);
313
314 alignPlot->xAxis->setLabelFont(QFont(font().family(), 10));
315 alignPlot->yAxis->setLabelFont(QFont(font().family(), 10));
316
317 alignPlot->xAxis->setLabelPadding(2);
318 alignPlot->yAxis->setLabelPadding(2);
319
320 alignPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
321 alignPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
322 alignPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
323 alignPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
324 alignPlot->xAxis->grid()->setZeroLinePen(QPen(Qt::yellow));
325 alignPlot->yAxis->grid()->setZeroLinePen(QPen(Qt::yellow));
326
327 alignPlot->xAxis->setLabel(i18n("dRA (arcsec)"));
328 alignPlot->yAxis->setLabel(i18n("dDE (arcsec)"));
329
330 alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
331 alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
332
333 alignPlot->setInteractions(QCP::iRangeZoom);
334 alignPlot->setInteraction(QCP::iRangeDrag, true);
335
336 alignPlot->addGraph();
337 alignPlot->graph(0)->setLineStyle(QCPGraph::lsNone);
338 alignPlot->graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, Qt::white, 15));
339
340 buildTarget();
341
342 connect(alignPlot, &QCustomPlot::mouseMove, this, &Ekos::Align::handlePointTooltip);
343 connect(rightLayout, &QSplitter::splitterMoved, this, &Ekos::Align::handleVerticalPlotSizeChange);
344 connect(alignSplitter, &QSplitter::splitterMoved, this, &Ekos::Align::handleHorizontalPlotSizeChange);
345
346 alignPlot->resize(190, 190);
347 alignPlot->replot();
348
349 solutionTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
350
351 clearAllSolutionsB->setIcon(
352 QIcon::fromTheme("application-exit"));
353 clearAllSolutionsB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
354
355 removeSolutionB->setIcon(QIcon::fromTheme("list-remove"));
356 removeSolutionB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
357
358 exportSolutionsCSV->setIcon(
359 QIcon::fromTheme("document-save-as"));
360 exportSolutionsCSV->setAttribute(Qt::WA_LayoutUsesWidgetRect);
361
362 autoScaleGraphB->setIcon(QIcon::fromTheme("zoom-fit-best"));
363 autoScaleGraphB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
364
365 connect(clearAllSolutionsB, &QPushButton::clicked, this, &Ekos::Align::slotClearAllSolutionPoints);
366 connect(removeSolutionB, &QPushButton::clicked, this, &Ekos::Align::slotRemoveSolutionPoint);
367 connect(exportSolutionsCSV, &QPushButton::clicked, this, &Ekos::Align::exportSolutionPoints);
368 connect(autoScaleGraphB, &QPushButton::clicked, this, &Ekos::Align::slotAutoScaleGraph);
369 connect(mountModelB, &QPushButton::clicked, this, &Ekos::Align::slotMountModel);
370 connect(solutionTable, &QTableWidget::cellClicked, this, &Ekos::Align::selectSolutionTableRow);
371
372 //Note: This is to prevent a button from being called the default button
373 //and then executing when the user hits the enter key such as when on a Text Box
374 QList<QPushButton *> qButtons = findChildren<QPushButton *>();
375 for (auto &button : qButtons)
376 button->setAutoDefault(false);
377
378 savedOptionsProfiles = QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("SavedAlignProfiles.ini");
379 if(QFile(savedOptionsProfiles).exists())
380 m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
381 else
382 m_StellarSolverProfiles = getDefaultAlignOptionsProfiles();
383
384 initPolarAlignmentAssistant();
385 initManualRotator();
386 initDarkProcessor();
387 }
388
~Align()389 Align::~Align()
390 {
391 if (alignWidget->parent() == nullptr)
392 toggleAlignWidgetFullScreen();
393
394 // Remove temporary FITS files left before by the solver
395 QDir dir(QDir::tempPath());
396 dir.setNameFilters(QStringList() << "fits*"
397 << "tmp.*");
398 dir.setFilter(QDir::Files);
399 for (auto &dirFile : dir.entryList())
400 dir.remove(dirFile);
401 }
selectSolutionTableRow(int row,int column)402 void Align::selectSolutionTableRow(int row, int column)
403 {
404 Q_UNUSED(column)
405
406 solutionTable->selectRow(row);
407 for (int i = 0; i < alignPlot->itemCount(); i++)
408 {
409 QCPAbstractItem *abstractItem = alignPlot->item(i);
410 if (abstractItem)
411 {
412 QCPItemText *item = qobject_cast<QCPItemText *>(abstractItem);
413 if (item)
414 {
415 if (i == row)
416 {
417 item->setColor(Qt::black);
418 item->setBrush(Qt::yellow);
419 }
420 else
421 {
422 item->setColor(Qt::red);
423 item->setBrush(Qt::white);
424 }
425 }
426 }
427 }
428 alignPlot->replot();
429 }
430
handleHorizontalPlotSizeChange()431 void Align::handleHorizontalPlotSizeChange()
432 {
433 alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);
434 alignPlot->replot();
435 }
436
handleVerticalPlotSizeChange()437 void Align::handleVerticalPlotSizeChange()
438 {
439 alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0);
440 alignPlot->replot();
441 }
442
resizeEvent(QResizeEvent * event)443 void Align::resizeEvent(QResizeEvent *event)
444 {
445 if (event->oldSize().width() != -1)
446 {
447 if (event->oldSize().width() != size().width())
448 handleHorizontalPlotSizeChange();
449 else if (event->oldSize().height() != size().height())
450 handleVerticalPlotSizeChange();
451 }
452 else
453 {
454 QTimer::singleShot(10, this, &Ekos::Align::handleHorizontalPlotSizeChange);
455 }
456 }
457
handlePointTooltip(QMouseEvent * event)458 void Align::handlePointTooltip(QMouseEvent *event)
459 {
460 QCPAbstractItem *item = alignPlot->itemAt(event->localPos());
461 if (item)
462 {
463 QCPItemText *label = qobject_cast<QCPItemText *>(item);
464 if (label)
465 {
466 QString labelText = label->text();
467 int point = labelText.toInt() - 1;
468
469 if (point < 0)
470 return;
471 QToolTip::showText(event->globalPos(),
472 i18n("<table>"
473 "<tr>"
474 "<th colspan=\"2\">Object %1: %2</th>"
475 "</tr>"
476 "<tr>"
477 "<td>RA:</td><td>%3</td>"
478 "</tr>"
479 "<tr>"
480 "<td>DE:</td><td>%4</td>"
481 "</tr>"
482 "<tr>"
483 "<td>dRA:</td><td>%5</td>"
484 "</tr>"
485 "<tr>"
486 "<td>dDE:</td><td>%6</td>"
487 "</tr>"
488 "</table>",
489 point + 1,
490 solutionTable->item(point, 2)->text(),
491 solutionTable->item(point, 0)->text(),
492 solutionTable->item(point, 1)->text(),
493 solutionTable->item(point, 4)->text(),
494 solutionTable->item(point, 5)->text()),
495 alignPlot, alignPlot->rect());
496 }
497 }
498 }
499
buildTarget()500 void Align::buildTarget()
501 {
502 double accuracyRadius = accuracySpin->value();
503 if (centralTarget)
504 {
505 concentricRings->data()->clear();
506 redTarget->data()->clear();
507 yellowTarget->data()->clear();
508 centralTarget->data()->clear();
509 }
510 else
511 {
512 concentricRings = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
513 redTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
514 yellowTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
515 centralTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
516 }
517 const int pointCount = 200;
518 QVector<QCPCurveData> circleRings(
519 pointCount * (5)); //Have to multiply by the number of rings, Rings at : 25%, 50%, 75%, 125%, 175%
520 QVector<QCPCurveData> circleCentral(pointCount);
521 QVector<QCPCurveData> circleYellow(pointCount);
522 QVector<QCPCurveData> circleRed(pointCount);
523
524 int circleRingPt = 0;
525 for (int i = 0; i < pointCount; i++)
526 {
527 double theta = i / static_cast<double>(pointCount) * 2 * M_PI;
528
529 for (double ring = 1; ring < 8; ring++)
530 {
531 if (ring != 4 && ring != 6)
532 {
533 if (i % (9 - static_cast<int>(ring)) == 0) //This causes fewer points to draw on the inner circles.
534 {
535 circleRings[circleRingPt] = QCPCurveData(circleRingPt, accuracyRadius * ring * 0.25 * qCos(theta),
536 accuracyRadius * ring * 0.25 * qSin(theta));
537 circleRingPt++;
538 }
539 }
540 }
541
542 circleCentral[i] = QCPCurveData(i, accuracyRadius * qCos(theta), accuracyRadius * qSin(theta));
543 circleYellow[i] = QCPCurveData(i, accuracyRadius * 1.5 * qCos(theta), accuracyRadius * 1.5 * qSin(theta));
544 circleRed[i] = QCPCurveData(i, accuracyRadius * 2 * qCos(theta), accuracyRadius * 2 * qSin(theta));
545 }
546
547 concentricRings->setLineStyle(QCPCurve::lsNone);
548 concentricRings->setScatterSkip(0);
549 concentricRings->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, QColor(255, 255, 255, 150), 1));
550
551 concentricRings->data()->set(circleRings, true);
552 redTarget->data()->set(circleRed, true);
553 yellowTarget->data()->set(circleYellow, true);
554 centralTarget->data()->set(circleCentral, true);
555
556 concentricRings->setPen(QPen(Qt::white));
557 redTarget->setPen(QPen(Qt::red));
558 yellowTarget->setPen(QPen(Qt::yellow));
559 centralTarget->setPen(QPen(Qt::green));
560
561 concentricRings->setBrush(Qt::NoBrush);
562 redTarget->setBrush(QBrush(QColor(255, 0, 0, 50)));
563 yellowTarget->setBrush(
564 QBrush(QColor(0, 255, 0, 50))); //Note this is actually yellow. It is green on top of red with equal opacity.
565 centralTarget->setBrush(QBrush(QColor(0, 255, 0, 50)));
566
567 if (alignPlot->size().width() > 0)
568 alignPlot->replot();
569 }
570
slotAutoScaleGraph()571 void Align::slotAutoScaleGraph()
572 {
573 double accuracyRadius = accuracySpin->value();
574 alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
575 alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
576
577 alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);
578
579 alignPlot->replot();
580 }
581
582
slotClearAllSolutionPoints()583 void Align::slotClearAllSolutionPoints()
584 {
585 if (solutionTable->rowCount() == 0)
586 return;
587
588 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
589 {
590 //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
591 KSMessageBox::Instance()->disconnect(this);
592
593 solutionTable->setRowCount(0);
594 alignPlot->graph(0)->data()->clear();
595 alignPlot->clearItems();
596 buildTarget();
597
598 slotAutoScaleGraph();
599
600 });
601
602 KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to clear all of the solution points?"),
603 i18n("Clear Solution Points"), 60);
604 }
605
slotRemoveSolutionPoint()606 void Align::slotRemoveSolutionPoint()
607 {
608 QCPAbstractItem *abstractItem = alignPlot->item(solutionTable->currentRow());
609 if (abstractItem)
610 {
611 QCPItemText *item = qobject_cast<QCPItemText *>(abstractItem);
612 if (item)
613 {
614 double point = item->position->key();
615 alignPlot->graph(0)->data()->remove(point);
616 }
617 }
618 alignPlot->removeItem(solutionTable->currentRow());
619 for (int i = 0; i < alignPlot->itemCount(); i++)
620 {
621 QCPAbstractItem *abstractItem = alignPlot->item(i);
622 if (abstractItem)
623 {
624 QCPItemText *item = qobject_cast<QCPItemText *>(abstractItem);
625 if (item)
626 item->setText(QString::number(i + 1));
627 }
628 }
629 solutionTable->removeRow(solutionTable->currentRow());
630 alignPlot->replot();
631 }
632
slotMountModel()633 void Align::slotMountModel()
634 {
635 if (!m_MountModel)
636 {
637 m_MountModel = new MountModel(this);
638 connect(m_MountModel, &Ekos::MountModel::newLog, this, &Ekos::Align::appendLogText, Qt::UniqueConnection);
639 connect(this, &Ekos::Align::newStatus, m_MountModel, &Ekos::MountModel::setAlignStatus, Qt::UniqueConnection);
640 }
641
642 m_MountModel->show();
643
644 // SkyPoint spWest;
645 // spWest.setAlt(30);
646 // spWest.setAz(270);
647 // spWest.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat());
648 // mountModel.alignDec->setValue(static_cast<int>(spWest.dec().Degrees()));
649
650 // mountModelDialog.show();
651 }
652
653
isParserOK()654 bool Align::isParserOK()
655 {
656 return true; //For now
657 Q_ASSERT_X(parser, __FUNCTION__, "Astrometry parser is not valid.");
658
659 bool rc = parser->init();
660
661 if (rc)
662 {
663 connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
664 connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
665 }
666
667 return rc;
668 }
669
checkAlignmentTimeout()670 void Align::checkAlignmentTimeout()
671 {
672 if (m_SolveFromFile || ++solverIterations == MAXIMUM_SOLVER_ITERATIONS)
673 abort();
674 else if (!m_SolveFromFile)
675 {
676 appendLogText(i18n("Solver timed out."));
677 parser->stopSolver();
678
679 setAlignTableResult(ALIGN_RESULT_FAILED);
680 captureAndSolve();
681 }
682 // TODO must also account for loadAndSlew. Retain file name
683 }
684
setSolverMode(int mode)685 void Align::setSolverMode(int mode)
686 {
687 if (sender() == nullptr && mode >= 0 && mode <= 1)
688 {
689 solverModeButtonGroup->button(mode)->setChecked(true);
690 }
691
692 Options::setSolverMode(mode);
693
694 if (mode == SOLVER_REMOTE)
695 {
696 if (remoteParser.get() != nullptr && remoteParserDevice != nullptr)
697 {
698 parser = remoteParser.get();
699 (dynamic_cast<RemoteAstrometryParser *>(parser))->setAstrometryDevice(remoteParserDevice);
700 return;
701 }
702
703 remoteParser.reset(new Ekos::RemoteAstrometryParser());
704 parser = remoteParser.get();
705 (dynamic_cast<RemoteAstrometryParser *>(parser))->setAstrometryDevice(remoteParserDevice);
706 if (currentCCD)
707 (dynamic_cast<RemoteAstrometryParser *>(parser))->setCCD(currentCCD->getDeviceName());
708
709 parser->setAlign(this);
710 if (parser->init())
711 {
712 connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
713 connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
714 }
715 else
716 parser->disconnect();
717 }
718 }
719
720
setCamera(const QString & device)721 bool Align::setCamera(const QString &device)
722 {
723 for (int i = 0; i < CCDCaptureCombo->count(); i++)
724 if (device == CCDCaptureCombo->itemText(i))
725 {
726 CCDCaptureCombo->setCurrentIndex(i);
727 checkCCD(i);
728 return true;
729 }
730
731 return false;
732 }
733
camera()734 QString Align::camera()
735 {
736 if (currentCCD)
737 return currentCCD->getDeviceName();
738
739 return QString();
740 }
741
setDefaultCCD(QString ccd)742 void Align::setDefaultCCD(QString ccd)
743 {
744 syncSettings();
745 Options::setDefaultAlignCCD(ccd);
746 }
747
checkCCD(int ccdNum)748 void Align::checkCCD(int ccdNum)
749 {
750 // Do NOT perform checks if align is in progress as this may result
751 // in signals/slots getting disconnected.
752 switch (state)
753 {
754 // Idle, camera change is OK.
755 case ALIGN_IDLE:
756 case ALIGN_COMPLETE:
757 case ALIGN_FAILED:
758 case ALIGN_ABORTED:
759 break;
760
761 // Busy, camera change is not OK.
762 case ALIGN_PROGRESS:
763 case ALIGN_SYNCING:
764 case ALIGN_SLEWING:
765 case ALIGN_SUSPENDED:
766 return;
767 }
768
769
770 if (ccdNum == -1 || ccdNum >= CCDs.count())
771 {
772 ccdNum = CCDCaptureCombo->currentIndex();
773
774 if (ccdNum == -1)
775 return;
776 }
777
778 currentCCD = CCDs.at(ccdNum);
779
780 ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
781 if (targetChip && targetChip->isCapturing())
782 return;
783
784 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr)
785 (dynamic_cast<RemoteAstrometryParser *>(remoteParser.get()))->setCCD(currentCCD->getDeviceName());
786
787 syncCCDInfo();
788
789 QStringList isoList = targetChip->getISOList();
790 ISOCombo->clear();
791
792 if (isoList.isEmpty())
793 {
794 ISOCombo->setEnabled(false);
795 }
796 else
797 {
798 ISOCombo->setEnabled(true);
799 ISOCombo->addItems(isoList);
800 ISOCombo->setCurrentIndex(targetChip->getISOIndex());
801 }
802
803 // Gain Check
804 if (currentCCD->hasGain())
805 {
806 double min, max, step, value;
807 currentCCD->getGainMinMaxStep(&min, &max, &step);
808
809 // Allow the possibility of no gain value at all.
810 GainSpinSpecialValue = min - step;
811 GainSpin->setRange(GainSpinSpecialValue, max);
812 GainSpin->setSpecialValueText(i18n("--"));
813 GainSpin->setEnabled(true);
814 GainSpin->setSingleStep(step);
815 currentCCD->getGain(&value);
816
817 // Set the custom gain if we have one
818 // otherwise it will not have an effect.
819 TargetCustomGainValue = Options::solverCameraGain();
820 if (TargetCustomGainValue > 0)
821 GainSpin->setValue(TargetCustomGainValue);
822 else
823 GainSpin->setValue(GainSpinSpecialValue);
824
825 GainSpin->setReadOnly(currentCCD->getGainPermission() == IP_RO);
826
827 connect(GainSpin, &QDoubleSpinBox::editingFinished, [this]()
828 {
829 if (GainSpin->value() > GainSpinSpecialValue)
830 {
831 TargetCustomGainValue = GainSpin->value();
832 // Save custom gain
833 Options::setSolverCameraGain(TargetCustomGainValue);
834 }
835 });
836 }
837 else
838 GainSpin->setEnabled(false);
839
840 syncTelescopeInfo();
841 }
842
addCCD(ISD::GDInterface * newCCD)843 void Align::addCCD(ISD::GDInterface *newCCD)
844 {
845 if (CCDs.contains(static_cast<ISD::CCD *>(newCCD)))
846 {
847 syncCCDInfo();
848 return;
849 }
850
851 CCDs.append(static_cast<ISD::CCD *>(newCCD));
852
853 CCDCaptureCombo->addItem(newCCD->getDeviceName());
854
855 checkCCD();
856
857 syncSettings();
858 }
859
setTelescope(ISD::GDInterface * newTelescope)860 void Align::setTelescope(ISD::GDInterface *newTelescope)
861 {
862 currentTelescope = static_cast<ISD::Telescope *>(newTelescope);
863 currentTelescope->disconnect(this);
864
865 RUN_PAH(setCurrentTelescope(currentTelescope));
866
867 connect(currentTelescope, &ISD::GDInterface::numberUpdated, this, &Ekos::Align::processNumber, Qt::UniqueConnection);
868 connect(currentTelescope, &ISD::GDInterface::switchUpdated, this, &Ekos::Align::processSwitch, Qt::UniqueConnection);
869 connect(currentTelescope, &ISD::GDInterface::Disconnected, this, [this]()
870 {
871 m_isRateSynced = false;
872 });
873
874
875 if (m_isRateSynced == false)
876 {
877 RUN_PAH(syncMountSpeed());
878 m_isRateSynced = !currentTelescope->slewRates().empty();
879 }
880
881 syncTelescopeInfo();
882 }
883
setDome(ISD::GDInterface * newDome)884 void Align::setDome(ISD::GDInterface *newDome)
885 {
886 currentDome = static_cast<ISD::Dome *>(newDome);
887 connect(currentDome, &ISD::GDInterface::switchUpdated, this, &Ekos::Align::processSwitch, Qt::UniqueConnection);
888 }
889
removeDevice(ISD::GDInterface * device)890 void Align::removeDevice(ISD::GDInterface *device)
891 {
892 device->disconnect(this);
893 if (currentTelescope && currentTelescope->getDeviceName() == device->getDeviceName())
894 {
895 currentTelescope = nullptr;
896 m_isRateSynced = false;
897 }
898 else if (currentDome && currentDome->getDeviceName() == device->getDeviceName())
899 {
900 currentDome = nullptr;
901 }
902 else if (currentRotator && currentRotator->getDeviceName() == device->getDeviceName())
903 {
904 currentRotator = nullptr;
905 }
906
907 if (CCDs.contains(static_cast<ISD::CCD *>(device)))
908 {
909 CCDs.removeAll(static_cast<ISD::CCD *>(device));
910 CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(device->getDeviceName()));
911 CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(device->getDeviceName() + QString(" Guider")));
912 if (CCDs.empty())
913 {
914 currentCCD = nullptr;
915 CCDCaptureCombo->setCurrentIndex(-1);
916 }
917 else
918 {
919 currentCCD = CCDs[0];
920 CCDCaptureCombo->setCurrentIndex(0);
921 }
922
923 QTimer::singleShot(1000, this, [this]()
924 {
925 checkCCD();
926 });
927 //checkCCD();
928 }
929
930 if (Filters.contains(static_cast<ISD::Filter *>(device)))
931 {
932 Filters.removeAll(static_cast<ISD::Filter *>(device));
933 filterManager->removeDevice(device);
934 FilterDevicesCombo->removeItem(FilterDevicesCombo->findText(device->getDeviceName()));
935 if (Filters.empty())
936 {
937 currentFilter = nullptr;
938 FilterDevicesCombo->setCurrentIndex(-1);
939 }
940 else
941 FilterDevicesCombo->setCurrentIndex(0);
942
943 //checkFilter();
944 QTimer::singleShot(1000, this, [this]()
945 {
946 checkFilter();
947 });
948 }
949
950 }
951
syncTelescopeInfo()952 bool Align::syncTelescopeInfo()
953 {
954 if (currentTelescope == nullptr || currentTelescope->isConnected() == false)
955 return false;
956
957 canSync = currentTelescope->canSync();
958
959 if (canSync == false && syncR->isEnabled())
960 {
961 slewR->setChecked(true);
962 appendLogText(i18n("Mount does not support syncing."));
963 }
964
965 syncR->setEnabled(canSync);
966
967 auto nvp = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO");
968
969 if (nvp)
970 {
971 auto np = nvp->findWidgetByName("TELESCOPE_APERTURE");
972
973 if (np && np->getValue() > 0)
974 primaryAperture = np->getValue();
975
976 np = nvp->findWidgetByName("GUIDER_APERTURE");
977 if (np && np->getValue() > 0)
978 guideAperture = np->getValue();
979
980 aperture = primaryAperture;
981
982 //if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE)
983 if (FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_GUIDE)
984 aperture = guideAperture;
985
986 np = nvp->findWidgetByName("TELESCOPE_FOCAL_LENGTH");
987 if (np && np->getValue() > 0)
988 primaryFL = np->getValue();
989
990 np = nvp->findWidgetByName("GUIDER_FOCAL_LENGTH");
991 if (np && np->getValue() > 0)
992 guideFL = np->getValue();
993
994 focal_length = primaryFL;
995
996 //if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE)
997 if (FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_GUIDE)
998 focal_length = guideFL;
999 }
1000
1001 if (focal_length == -1 || aperture == -1)
1002 return false;
1003
1004 if (ccd_hor_pixel != -1 && ccd_ver_pixel != -1 && focal_length != -1 && aperture != -1)
1005 {
1006 FOVScopeCombo->setItemData(
1007 ISD::CCD::TELESCOPE_PRIMARY,
1008 i18nc("F-Number, Focal length, Aperture",
1009 "<nobr>F<b>%1</b> Focal length: <b>%2</b> mm Aperture: <b>%3</b> mm<sup>2</sup></nobr>",
1010 QString::number(primaryFL / primaryAperture, 'f', 1), QString::number(primaryFL, 'f', 2),
1011 QString::number(primaryAperture, 'f', 2)),
1012 Qt::ToolTipRole);
1013 FOVScopeCombo->setItemData(
1014 ISD::CCD::TELESCOPE_GUIDE,
1015 i18nc("F-Number, Focal length, Aperture",
1016 "<nobr>F<b>%1</b> Focal length: <b>%2</b> mm Aperture: <b>%3</b> mm<sup>2</sup></nobr>",
1017 QString::number(guideFL / guideAperture, 'f', 1), QString::number(guideFL, 'f', 2),
1018 QString::number(guideAperture, 'f', 2)),
1019 Qt::ToolTipRole);
1020
1021 calculateFOV();
1022
1023 //generateArgs();
1024
1025 return true;
1026 }
1027
1028 return false;
1029 }
1030
setTelescopeInfo(double primaryFocalLength,double primaryAperture,double guideFocalLength,double guideAperture)1031 void Align::setTelescopeInfo(double primaryFocalLength, double primaryAperture, double guideFocalLength,
1032 double guideAperture)
1033 {
1034 if (primaryFocalLength > 0)
1035 primaryFL = primaryFocalLength;
1036 if (guideFocalLength > 0)
1037 guideFL = guideFocalLength;
1038
1039 if (primaryAperture > 0)
1040 this->primaryAperture = primaryAperture;
1041 if (guideAperture > 0)
1042 this->guideAperture = guideAperture;
1043
1044 focal_length = primaryFL;
1045
1046 if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE)
1047 focal_length = guideFL;
1048
1049 aperture = primaryAperture;
1050
1051 if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE)
1052 aperture = guideAperture;
1053
1054 syncTelescopeInfo();
1055 }
1056
syncCCDInfo()1057 void Align::syncCCDInfo()
1058 {
1059 if (!currentCCD)
1060 return;
1061
1062 auto nvp = currentCCD->getBaseDevice()->getNumber(useGuideHead ? "GUIDER_INFO" : "CCD_INFO");
1063
1064 if (nvp)
1065 {
1066 auto np = nvp->findWidgetByName("CCD_PIXEL_SIZE_X");
1067 if (np && np->getValue() > 0)
1068 ccd_hor_pixel = ccd_ver_pixel = np->getValue();
1069
1070 np = nvp->findWidgetByName("CCD_PIXEL_SIZE_Y");
1071 if (np && np->getValue() > 0)
1072 ccd_ver_pixel = np->getValue();
1073
1074 np = nvp->findWidgetByName("CCD_PIXEL_SIZE_Y");
1075 if (np && np->getValue() > 0)
1076 ccd_ver_pixel = np->getValue();
1077 }
1078
1079 ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD);
1080
1081 auto svp = currentCCD->getBaseDevice()->getSwitch("WCS_CONTROL");
1082 if (svp)
1083 setWCSEnabled(Options::astrometrySolverWCS());
1084
1085 targetChip->setImageView(alignView, FITS_ALIGN);
1086
1087 targetChip->getFrameMinMax(nullptr, nullptr, nullptr, nullptr, nullptr, &ccd_width, nullptr, &ccd_height);
1088 binningCombo->setEnabled(targetChip->canBin());
1089 if (targetChip->canBin())
1090 {
1091 binningCombo->blockSignals(true);
1092
1093 int binx = 1, biny = 1;
1094 targetChip->getMaxBin(&binx, &biny);
1095 binningCombo->clear();
1096
1097 for (int i = 0; i < binx; i++)
1098 binningCombo->addItem(QString("%1x%2").arg(i + 1).arg(i + 1));
1099
1100 binningCombo->setCurrentIndex(Options::solverBinningIndex());
1101 binningCombo->blockSignals(false);
1102 }
1103
1104 if (ccd_hor_pixel <= 0 || ccd_ver_pixel <= 0)
1105 return;
1106
1107 if (ccd_hor_pixel > 0 && ccd_ver_pixel > 0 && focal_length > 0 && aperture > 0)
1108 {
1109 calculateFOV();
1110 }
1111 }
1112
getFOVScale(double & fov_w,double & fov_h,double & fov_scale)1113 void Align::getFOVScale(double &fov_w, double &fov_h, double &fov_scale)
1114 {
1115 fov_w = fov_x;
1116 fov_h = fov_y;
1117 fov_scale = fov_pixscale;
1118 }
1119
fov()1120 QList<double> Align::fov()
1121 {
1122 QList<double> result;
1123
1124 result << fov_x << fov_y << fov_pixscale;
1125
1126 return result;
1127 }
1128
cameraInfo()1129 QList<double> Align::cameraInfo()
1130 {
1131 QList<double> result;
1132
1133 result << ccd_width << ccd_height << ccd_hor_pixel << ccd_ver_pixel;
1134
1135 return result;
1136 }
1137
telescopeInfo()1138 QList<double> Align::telescopeInfo()
1139 {
1140 QList<double> result;
1141
1142 result << focal_length << aperture;
1143
1144 return result;
1145 }
1146
getCalculatedFOVScale(double & fov_w,double & fov_h,double & fov_scale)1147 void Align::getCalculatedFOVScale(double &fov_w, double &fov_h, double &fov_scale)
1148 {
1149 // FOV in arcsecs
1150 fov_w = 206264.8062470963552 * ccd_width * ccd_hor_pixel / 1000.0 / focal_length;
1151 fov_h = 206264.8062470963552 * ccd_height * ccd_ver_pixel / 1000.0 / focal_length;
1152
1153 // Pix Scale
1154 fov_scale = (fov_w * (Options::solverBinningIndex() + 1)) / ccd_width;
1155
1156 // FOV in arcmins
1157 fov_w /= 60.0;
1158 fov_h /= 60.0;
1159 }
1160
calculateEffectiveFocalLength(double newFOVW)1161 void Align::calculateEffectiveFocalLength(double newFOVW)
1162 {
1163 if (newFOVW < 0 || newFOVW == fov_x)
1164 return;
1165
1166 double new_focal_length = ((ccd_width * ccd_hor_pixel / 1000.0) * 206264.8062470963552) / (newFOVW * 60.0);
1167 double focal_diff = std::fabs(new_focal_length - focal_length);
1168
1169 if (focal_diff > 1)
1170 {
1171 if (FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_PRIMARY)
1172 primaryEffectiveFL = new_focal_length;
1173 else
1174 guideEffectiveFL = new_focal_length;
1175 appendLogText(i18n("Effective telescope focal length is updated to %1 mm.", new_focal_length));
1176 }
1177 }
1178
calculateFOV()1179 void Align::calculateFOV()
1180 {
1181 // Calculate FOV
1182
1183 // FOV in arcsecs
1184 fov_x = 206264.8062470963552 * ccd_width * ccd_hor_pixel / 1000.0 / focal_length;
1185 fov_y = 206264.8062470963552 * ccd_height * ccd_ver_pixel / 1000.0 / focal_length;
1186
1187 // Pix Scale
1188 fov_pixscale = (fov_x * (Options::solverBinningIndex() + 1)) / ccd_width;
1189
1190 // FOV in arcmins
1191 fov_x /= 60.0;
1192 fov_y /= 60.0;
1193
1194 // Put FOV upper limit as 180 degrees
1195 if (fov_x < 1 || fov_x > 60 * 180 || fov_y < 1 || fov_y > 60 * 180)
1196 {
1197 appendLogText(
1198 i18n("Warning! The calculated field of view is out of bounds. Ensure the telescope focal length and camera pixel size are correct."));
1199 return;
1200 }
1201
1202 double calculated_fov_x = fov_x;
1203 double calculated_fov_y = fov_y;
1204
1205 QString calculatedFOV = (QString("%1' x %2'").arg(fov_x, 0, 'f', 1).arg(fov_y, 0, 'f', 1));
1206
1207 double effectiveFocalLength = FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_PRIMARY ? primaryEffectiveFL :
1208 guideEffectiveFL;
1209
1210 FocalLengthOut->setText(QString("%1 (%2)").arg(focal_length, 0, 'f', 1).
1211 arg(effectiveFocalLength > 0 ? effectiveFocalLength : focal_length, 0, 'f', 1));
1212 FocalRatioOut->setText(QString("%1 (%2)").arg(focal_length / aperture, 0, 'f', 1).
1213 arg(effectiveFocalLength > 0 ? effectiveFocalLength / aperture : focal_length / aperture, 0, 'f', 1));
1214
1215 if (effectiveFocalLength > 0)
1216 {
1217 double focal_diff = std::fabs(effectiveFocalLength - focal_length);
1218 if (focal_diff < 5)
1219 FocalLengthOut->setStyleSheet("color:green");
1220 else if (focal_diff < 15)
1221 FocalLengthOut->setStyleSheet("color:yellow");
1222 else
1223 FocalLengthOut->setStyleSheet("color:red");
1224 }
1225
1226 // JM 2018-04-20 Above calculations are for RAW FOV. Starting from 2.9.5, we are using EFFECTIVE FOV
1227 // Which is the real FOV as measured from the plate solution. The effective FOVs are stored in the database and are unique
1228 // per profile/pixel_size/focal_length combinations. It defaults to 0' x 0' and gets updated after the first successful solver is complete.
1229 getEffectiveFOV();
1230
1231 if (fov_x == 0)
1232 {
1233 //FOVOut->setReadOnly(false);
1234 FOVOut->setToolTip(
1235 i18n("<p>Effective field of view size in arcminutes.</p><p>Please capture and solve once to measure the effective FOV or enter the values manually.</p><p>Calculated FOV: %1</p>",
1236 calculatedFOV));
1237 fov_x = calculated_fov_x;
1238 fov_y = calculated_fov_y;
1239 m_EffectiveFOVPending = true;
1240 }
1241 else
1242 {
1243 m_EffectiveFOVPending = false;
1244 FOVOut->setToolTip(i18n("<p>Effective field of view size in arcminutes.</p>"));
1245 }
1246
1247 solverFOV->setSize(fov_x, fov_y);
1248 sensorFOV->setSize(fov_x, fov_y);
1249 if (currentCCD)
1250 sensorFOV->setName(currentCCD->getDeviceName());
1251
1252 FOVOut->setText(QString("%1' x %2'").arg(fov_x, 0, 'f', 1).arg(fov_y, 0, 'f', 1));
1253
1254 // Enable or Disable PAA depending on current FOV
1255 const bool fovOK = ((fov_x + fov_y) / 2.0) > PAH_CUTOFF_FOV;
1256 if (m_PolarAlignmentAssistant != nullptr)
1257 m_PolarAlignmentAssistant->setEnabled(fovOK);
1258
1259 if (opsAstrometry->kcfg_AstrometryUseImageScale->isChecked())
1260 {
1261 int unitType = opsAstrometry->kcfg_AstrometryImageScaleUnits->currentIndex();
1262
1263 // Degrees
1264 if (unitType == 0)
1265 {
1266 double fov_low = qMin(fov_x / 60, fov_y / 60);
1267 double fov_high = qMax(fov_x / 60, fov_y / 60);
1268 opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low);
1269 opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high);
1270
1271 Options::setAstrometryImageScaleLow(fov_low);
1272 Options::setAstrometryImageScaleHigh(fov_high);
1273 }
1274 // Arcmins
1275 else if (unitType == 1)
1276 {
1277 double fov_low = qMin(fov_x, fov_y);
1278 double fov_high = qMax(fov_x, fov_y);
1279 opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low);
1280 opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high);
1281
1282 Options::setAstrometryImageScaleLow(fov_low);
1283 Options::setAstrometryImageScaleHigh(fov_high);
1284 }
1285 // Arcsec per pixel
1286 else
1287 {
1288 opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_pixscale * 0.9);
1289 opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_pixscale * 1.1);
1290
1291 // 10% boundary
1292 Options::setAstrometryImageScaleLow(fov_pixscale * 0.9);
1293 Options::setAstrometryImageScaleHigh(fov_pixscale * 1.1);
1294 }
1295 }
1296 }
1297
generateRemoteOptions(const QVariantMap & optionsMap)1298 QStringList Align::generateRemoteOptions(const QVariantMap &optionsMap)
1299 {
1300 QStringList solver_args;
1301
1302 // -O overwrite
1303 // -3 Expected RA
1304 // -4 Expected DEC
1305 // -5 Radius (deg)
1306 // -L lower scale of image in arcminutes
1307 // -H upper scale of image in arcminutes
1308 // -u aw set scale to be in arcminutes
1309 // -W solution.wcs name of solution file
1310 // apog1.jpg name of target file to analyze
1311 //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg
1312
1313 // Start with always-used arguments
1314 solver_args << "-O"
1315 << "--no-plots";
1316
1317 // Now go over boolean options
1318
1319 // noverify
1320 if (optionsMap.contains("noverify"))
1321 solver_args << "--no-verify";
1322
1323 // noresort
1324 if (optionsMap.contains("resort"))
1325 solver_args << "--resort";
1326
1327 // fits2fits
1328 if (optionsMap.contains("nofits2fits"))
1329 solver_args << "--no-fits2fits";
1330
1331 // downsample
1332 if (optionsMap.contains("downsample"))
1333 solver_args << "--downsample" << QString::number(optionsMap.value("downsample", 2).toInt());
1334
1335 // image scale low
1336 if (optionsMap.contains("scaleL"))
1337 solver_args << "-L" << QString::number(optionsMap.value("scaleL").toDouble());
1338
1339 // image scale high
1340 if (optionsMap.contains("scaleH"))
1341 solver_args << "-H" << QString::number(optionsMap.value("scaleH").toDouble());
1342
1343 // image scale units
1344 if (optionsMap.contains("scaleUnits"))
1345 solver_args << "-u" << optionsMap.value("scaleUnits").toString();
1346
1347 // RA
1348 if (optionsMap.contains("ra"))
1349 solver_args << "-3" << QString::number(optionsMap.value("ra").toDouble());
1350
1351 // DE
1352 if (optionsMap.contains("de"))
1353 solver_args << "-4" << QString::number(optionsMap.value("de").toDouble());
1354
1355 // Radius
1356 if (optionsMap.contains("radius"))
1357 solver_args << "-5" << QString::number(optionsMap.value("radius").toDouble());
1358
1359 // Custom
1360 if (optionsMap.contains("custom"))
1361 solver_args << optionsMap.value("custom").toString();
1362
1363 return solver_args;
1364 }
1365
1366 //This will generate the high and low scale of the imager field size based on the stated units.
generateFOVBounds(double fov_w,QString & fov_low,QString & fov_high,double tolerance)1367 void Align::generateFOVBounds(double fov_w, QString &fov_low, QString &fov_high, double tolerance)
1368 {
1369 // This sets the percentage we search outside the lower and upper boundary limits
1370 // by default, we stretch the limits by 5% (tolerance = 0.05)
1371 double lower_boundary = 1.0 - tolerance;
1372 double upper_boundary = 1.0 + tolerance;
1373
1374 // let's stretch the boundaries by 5%
1375 // fov_lower = ((fov_h < fov_v) ? (fov_h * lower_boundary) : (fov_v * lower_boundary));
1376 // fov_upper = ((fov_h > fov_v) ? (fov_h * upper_boundary) : (fov_v * upper_boundary));
1377
1378 // JM 2019-10-20: The bounds consider image width only, not height.
1379 double fov_lower = fov_w * lower_boundary;
1380 double fov_upper = fov_w * upper_boundary;
1381
1382 //No need to do anything if they are aw, since that is the default
1383 fov_low = QString::number(fov_lower);
1384 fov_high = QString::number(fov_upper);
1385 }
1386
1387
generateRemoteArgs(const QSharedPointer<FITSData> & data)1388 QStringList Align::generateRemoteArgs(const QSharedPointer<FITSData> &data)
1389 {
1390 QVariantMap optionsMap;
1391
1392 // -O overwrite
1393 // -3 Expected RA
1394 // -4 Expected DEC
1395 // -5 Radius (deg)
1396 // -L lower scale of image in arcminutes
1397 // -H upper scale of image in arcminutes
1398 // -u aw set scale to be in arcminutes
1399 // -W solution.wcs name of solution file
1400 // apog1.jpg name of target file to analyze
1401 //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg
1402
1403 if (Options::astrometryUseNoVerify())
1404 optionsMap["noverify"] = true;
1405
1406 if (Options::astrometryUseResort())
1407 optionsMap["resort"] = true;
1408
1409 if (Options::astrometryUseNoFITS2FITS())
1410 optionsMap["nofits2fits"] = true;
1411
1412 if (data == nullptr)
1413 {
1414 if (Options::astrometryUseDownsample())
1415 {
1416 if (Options::astrometryAutoDownsample() && ccd_width && ccd_height)
1417 {
1418 uint8_t bin = qMax(Options::solverBinningIndex() + 1, 1u);
1419 uint16_t w = ccd_width / bin;
1420 optionsMap["downsample"] = getSolverDownsample(w);
1421 }
1422 else
1423 optionsMap["downsample"] = Options::astrometryDownsample();
1424 }
1425
1426 //Options needed for Sextractor
1427 int bin = Options::solverBinningIndex() + 1;
1428 optionsMap["image_width"] = ccd_width / bin;
1429 optionsMap["image_height"] = ccd_height / bin;
1430
1431 if (Options::astrometryUseImageScale() && fov_x > 0 && fov_y > 0)
1432 {
1433 QString units = "dw";
1434 if (Options::astrometryImageScaleUnits() == 1)
1435 units = "aw";
1436 else if (Options::astrometryImageScaleUnits() == 2)
1437 units = "app";
1438 if (Options::astrometryAutoUpdateImageScale())
1439 {
1440 QString fov_low, fov_high;
1441 double fov_w = fov_x;
1442 double fov_h = fov_y;
1443
1444 if (Options::astrometryImageScaleUnits() == SSolver::DEG_WIDTH)
1445 {
1446 fov_w /= 60;
1447 fov_h /= 60;
1448 }
1449 else if (Options::astrometryImageScaleUnits() == SSolver::ARCSEC_PER_PIX)
1450 {
1451 fov_w = fov_pixscale;
1452 fov_h = fov_pixscale;
1453 }
1454
1455 // If effective FOV is pending, let's set a wider tolerance range
1456 generateFOVBounds(fov_w, fov_low, fov_high, m_EffectiveFOVPending ? 0.3 : 0.05);
1457
1458 optionsMap["scaleL"] = fov_low;
1459 optionsMap["scaleH"] = fov_high;
1460 optionsMap["scaleUnits"] = units;
1461 }
1462 else
1463 {
1464 optionsMap["scaleL"] = Options::astrometryImageScaleLow();
1465 optionsMap["scaleH"] = Options::astrometryImageScaleHigh();
1466 optionsMap["scaleUnits"] = units;
1467 }
1468 }
1469
1470 if (Options::astrometryUsePosition() && currentTelescope != nullptr)
1471 {
1472 double ra = 0, dec = 0;
1473 currentTelescope->getEqCoords(&ra, &dec);
1474
1475 optionsMap["ra"] = ra * 15.0;
1476 optionsMap["de"] = dec;
1477 optionsMap["radius"] = Options::astrometryRadius();
1478 }
1479 }
1480 else
1481 {
1482 // Downsample
1483 QVariant width;
1484 data->getRecordValue("NAXIS1", width);
1485 if (width.isValid())
1486 {
1487 optionsMap["downsample"] = getSolverDownsample(width.toInt());
1488 }
1489 else
1490 optionsMap["downsample"] = Options::astrometryDownsample();
1491
1492 // Pixel Scale
1493 QVariant pixscale;
1494 data->getRecordValue("SCALE", pixscale);
1495 if (pixscale.isValid())
1496 {
1497 optionsMap["scaleL"] = 0.8 * pixscale.toDouble();
1498 optionsMap["scaleH"] = 1.2 * pixscale.toDouble();
1499 optionsMap["scaleUnits"] = "app";
1500 }
1501
1502 // Position
1503 QVariant ra, de;
1504 data->getRecordValue("RA", ra);
1505 data->getRecordValue("DEC", de);
1506 if (ra.isValid() && de.isValid())
1507 {
1508 optionsMap["ra"] = ra.toDouble();
1509 optionsMap["de"] = de.toDouble();
1510 optionsMap["radius"] = Options::astrometryRadius();
1511 }
1512 }
1513
1514 if (Options::astrometryCustomOptions().isEmpty() == false)
1515 optionsMap["custom"] = Options::astrometryCustomOptions();
1516
1517 return generateRemoteOptions(optionsMap);
1518 }
1519
captureAndSolve()1520 bool Align::captureAndSolve()
1521 {
1522 m_AlignTimer.stop();
1523 m_CaptureTimer.stop();
1524
1525 if (currentCCD == nullptr)
1526 {
1527 appendLogText(i18n("Error: No camera detected."));
1528 return false;
1529 }
1530
1531 if (currentCCD->isConnected() == false)
1532 {
1533 appendLogText(i18n("Error: lost connection to camera."));
1534 KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed"), KSNotification::EVENT_ALERT);
1535 return false;
1536 }
1537
1538 if (currentCCD->isBLOBEnabled() == false)
1539 {
1540 currentCCD->setBLOBEnabled(true);
1541 }
1542
1543 // If CCD Telescope Type does not match desired scope type, change it
1544 // but remember current value so that it can be reset once capture is complete or is aborted.
1545 if (currentCCD->getTelescopeType() != FOVScopeCombo->currentIndex())
1546 {
1547 rememberTelescopeType = currentCCD->getTelescopeType();
1548 currentCCD->setTelescopeType(static_cast<ISD::CCD::TelescopeType>(FOVScopeCombo->currentIndex()));
1549 }
1550
1551 //if (parser->init() == false)
1552 // return false;
1553
1554 if (focal_length == -1 || aperture == -1)
1555 {
1556 KSNotification::error(
1557 i18n("Telescope aperture and focal length are missing. Please check your driver settings and try again."));
1558 return false;
1559 }
1560
1561 if (ccd_hor_pixel == -1 || ccd_ver_pixel == -1)
1562 {
1563 KSNotification::error(i18n("CCD pixel size is missing. Please check your driver settings and try again."));
1564 return false;
1565 }
1566
1567 if (currentFilter != nullptr)
1568 {
1569 if (currentFilter->isConnected() == false)
1570 {
1571 appendLogText(i18n("Error: lost connection to filter wheel."));
1572 return false;
1573 }
1574
1575 int targetPosition = FilterPosCombo->currentIndex() + 1;
1576
1577 if (targetPosition > 0 && targetPosition != currentFilterPosition)
1578 {
1579 filterPositionPending = true;
1580 // Disabling the autofocus policy for align.
1581 filterManager->setFilterPosition(
1582 targetPosition, FilterManager::NO_AUTOFOCUS_POLICY);
1583 state = ALIGN_PROGRESS;
1584 return true;
1585 }
1586 }
1587
1588 if (currentCCD->getDriverInfo()->getClientManager()->getBLOBMode(currentCCD->getDeviceName().toLatin1().constData(),
1589 "CCD1") == B_NEVER)
1590 {
1591 if (KMessageBox::questionYesNo(
1592 nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) ==
1593 KMessageBox::Yes)
1594 {
1595 currentCCD->getDriverInfo()->getClientManager()->setBLOBMode(B_ONLY, currentCCD->getDeviceName().toLatin1().constData(),
1596 "CCD1");
1597 currentCCD->getDriverInfo()->getClientManager()->setBLOBMode(B_ONLY, currentCCD->getDeviceName().toLatin1().constData(),
1598 "CCD2");
1599 }
1600 else
1601 {
1602 return false;
1603 }
1604 }
1605
1606 double seqExpose = exposureIN->value();
1607
1608 ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD);
1609
1610 if (m_FocusState >= FOCUS_PROGRESS)
1611 {
1612 appendLogText(i18n("Cannot capture while focus module is busy. Retrying in %1 seconds...", CAPTURE_RETRY_DELAY / 1000));
1613 m_CaptureTimer.start(CAPTURE_RETRY_DELAY);
1614 return false;
1615 }
1616
1617 if (targetChip->isCapturing())
1618 {
1619 appendLogText(i18n("Cannot capture while CCD exposure is in progress. Retrying in %1 seconds...",
1620 CAPTURE_RETRY_DELAY / 1000));
1621 m_CaptureTimer.start(CAPTURE_RETRY_DELAY);
1622 return false;
1623 }
1624
1625 alignView->setBaseSize(alignWidget->size());
1626
1627 connect(currentCCD, &ISD::CCD::newImage, this, &Ekos::Align::processData);
1628 connect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Align::checkCCDExposureProgress);
1629
1630 // In case of remote solver, check if we need to update active CCD
1631 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr)
1632 {
1633 if (remoteParserDevice == nullptr)
1634 {
1635 appendLogText(i18n("No remote astrometry driver detected, switching to StellarSolver."));
1636 setSolverMode(SOLVER_LOCAL);
1637 }
1638 else
1639 {
1640 // Update ACTIVE_CCD of the remote astrometry driver so it listens to BLOB emitted by the CCD
1641 auto activeDevices = remoteParserDevice->getBaseDevice()->getText("ACTIVE_DEVICES");
1642 if (activeDevices)
1643 {
1644 auto activeCCD = activeDevices->findWidgetByName("ACTIVE_CCD");
1645 if (QString(activeCCD->text) != CCDCaptureCombo->currentText())
1646 {
1647 activeCCD->setText(CCDCaptureCombo->currentText().toLatin1().data());
1648
1649 remoteParserDevice->getDriverInfo()->getClientManager()->sendNewText(activeDevices);
1650 }
1651 }
1652
1653 // Enable remote parse
1654 dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->setEnabled(true);
1655 dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->sendArgs(generateRemoteArgs(QSharedPointer<FITSData>()));
1656 solverTimer.start();
1657 }
1658 }
1659
1660 // Remove temporary FITS files left before by the solver
1661 QDir dir(QDir::tempPath());
1662 dir.setNameFilters(QStringList() << "fits*" << "tmp.*");
1663 dir.setFilter(QDir::Files);
1664 for (auto &dirFile : dir.entryList())
1665 dir.remove(dirFile);
1666
1667 prepareCapture(targetChip);
1668
1669 // In case we're in refresh phase of the polar alignment helper then we use capture value from there
1670 if (matchPAHStage(PAA::PAH_REFRESH))
1671 targetChip->capture(m_PolarAlignmentAssistant->getPAHExposureDuration());
1672 else
1673 targetChip->capture(seqExpose);
1674
1675 Options::setAlignExposure(seqExpose);
1676
1677 solveB->setEnabled(false);
1678 stopB->setEnabled(true);
1679 pi->startAnimation();
1680
1681 differentialSlewingActivated = false;
1682
1683 state = ALIGN_PROGRESS;
1684 emit newStatus(state);
1685 solverFOV->setProperty("visible", true);
1686
1687 // If we're just refreshing, then we're done
1688 if (matchPAHStage(PAA::PAH_REFRESH))
1689 return true;
1690
1691 appendLogText(i18n("Capturing image..."));
1692
1693 //This block of code will create the row in the solution table and populate RA, DE, and object name.
1694 //It also starts the progress indicator.
1695 double ra, dec;
1696 currentTelescope->getEqCoords(&ra, &dec);
1697 if (!m_SolveFromFile)
1698 {
1699 int currentRow = solutionTable->rowCount();
1700 solutionTable->insertRow(currentRow);
1701 for (int i = 4; i < 6; i++)
1702 {
1703 QTableWidgetItem *disabledBox = new QTableWidgetItem();
1704 disabledBox->setFlags(Qt::ItemIsSelectable);
1705 solutionTable->setItem(currentRow, i, disabledBox);
1706 }
1707
1708 QTableWidgetItem *RAReport = new QTableWidgetItem();
1709 RAReport->setText(ScopeRAOut->text());
1710 RAReport->setTextAlignment(Qt::AlignHCenter);
1711 RAReport->setFlags(Qt::ItemIsSelectable);
1712 solutionTable->setItem(currentRow, 0, RAReport);
1713
1714 QTableWidgetItem *DECReport = new QTableWidgetItem();
1715 DECReport->setText(ScopeDecOut->text());
1716 DECReport->setTextAlignment(Qt::AlignHCenter);
1717 DECReport->setFlags(Qt::ItemIsSelectable);
1718 solutionTable->setItem(currentRow, 1, DECReport);
1719
1720 double maxrad = 1.0;
1721 SkyObject *so =
1722 KStarsData::Instance()->skyComposite()->objectNearest(new SkyPoint(dms(ra * 15), dms(dec)), maxrad);
1723 QString name;
1724 if (so)
1725 {
1726 name = so->longname();
1727 }
1728 else
1729 {
1730 name = "None";
1731 }
1732 QTableWidgetItem *ObjNameReport = new QTableWidgetItem();
1733 ObjNameReport->setText(name);
1734 ObjNameReport->setTextAlignment(Qt::AlignHCenter);
1735 ObjNameReport->setFlags(Qt::ItemIsSelectable);
1736 solutionTable->setItem(currentRow, 2, ObjNameReport);
1737 #ifdef Q_OS_OSX
1738 repaint(); //This is a band-aid for a bug in QT 5.10.0
1739 #endif
1740
1741 QProgressIndicator *alignIndicator = new QProgressIndicator(this);
1742 solutionTable->setCellWidget(currentRow, 3, alignIndicator);
1743 alignIndicator->startAnimation();
1744 #ifdef Q_OS_OSX
1745 repaint(); //This is a band-aid for a bug in QT 5.10.0
1746 #endif
1747 }
1748
1749 return true;
1750 }
1751
processData(const QSharedPointer<FITSData> & data)1752 void Align::processData(const QSharedPointer<FITSData> &data)
1753 {
1754 if (data->property("chip").toInt() == ISD::CCDChip::GUIDE_CCD)
1755 return;
1756
1757 disconnect(currentCCD, &ISD::CCD::newImage, this, &Ekos::Align::processData);
1758 disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Align::checkCCDExposureProgress);
1759
1760 if (data)
1761 m_ImageData = data;
1762 else
1763 m_ImageData.reset();
1764
1765 RUN_PAH(setImageData(m_ImageData));
1766
1767 // If it's Refresh, we're done
1768 if (matchPAHStage(PAA::PAH_REFRESH))
1769 {
1770 setCaptureComplete();
1771 return;
1772 }
1773 else
1774 appendLogText(i18n("Image received."));
1775
1776 // If Local solver, then set capture complete or perform calibration first.
1777 if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL)
1778 {
1779 // Only perform dark image subtraction on local images.
1780 if (alignDarkFrameCheck->isChecked())
1781 {
1782 int x, y, w, h, binx = 1, biny = 1;
1783 ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD);
1784 targetChip->getFrame(&x, &y, &w, &h);
1785 targetChip->getBinning(&binx, &biny);
1786
1787 uint16_t offsetX = x / binx;
1788 uint16_t offsetY = y / biny;
1789
1790 m_DarkProcessor->denoise(targetChip, m_ImageData, exposureIN->value(), offsetX, offsetY);
1791 return;
1792 }
1793
1794 setCaptureComplete();
1795 }
1796 }
1797
prepareCapture(ISD::CCDChip * targetChip)1798 void Align::prepareCapture(ISD::CCDChip *targetChip)
1799 {
1800 if (currentCCD->getUploadMode() == ISD::CCD::UPLOAD_LOCAL)
1801 {
1802 rememberUploadMode = ISD::CCD::UPLOAD_LOCAL;
1803 currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT);
1804 }
1805
1806 if (currentCCD->isFastExposureEnabled())
1807 {
1808 m_RememberCameraFastExposure = true;
1809 currentCCD->setFastExposureEnabled(false);
1810 }
1811
1812 currentCCD->setTransformFormat(ISD::CCD::FORMAT_FITS);
1813 targetChip->resetFrame();
1814 targetChip->setBatchMode(false);
1815 targetChip->setCaptureMode(FITS_ALIGN);
1816 targetChip->setFrameType(FRAME_LIGHT);
1817
1818 int bin = Options::solverBinningIndex() + 1;
1819 targetChip->setBinning(bin, bin);
1820
1821 // Set gain if applicable
1822 if (currentCCD->hasGain() && GainSpin->value() > GainSpinSpecialValue)
1823 currentCCD->setGain(GainSpin->value());
1824 // Set ISO if applicable
1825 if (ISOCombo->currentIndex() >= 0)
1826 targetChip->setISOIndex(ISOCombo->currentIndex());
1827 }
1828
1829
setCaptureComplete()1830 void Align::setCaptureComplete()
1831 {
1832 if (matchPAHStage(PAA::PAH_REFRESH))
1833 {
1834 emit newFrame(alignView);
1835 m_PolarAlignmentAssistant->processPAHRefresh();
1836 return;
1837 }
1838
1839 emit newImage(alignView);
1840
1841 solverFOV->setImage(alignView->getDisplayImage());
1842
1843 startSolving();
1844 }
1845
setSolverAction(int mode)1846 void Align::setSolverAction(int mode)
1847 {
1848 gotoModeButtonGroup->button(mode)->setChecked(true);
1849 m_CurrentGotoMode = static_cast<GotoMode>(mode);
1850 }
1851
startSolving()1852 void Align::startSolving()
1853 {
1854 //RUN_PAH(syncStage());
1855
1856 // This is needed because they might have directories stored in the config file.
1857 // So we can't just use the options folder list.
1858 QStringList astrometryDataDirs = KSUtils::getAstrometryDataDirs();
1859 disconnect(alignView, &FITSView::loaded, this, &Align::startSolving);
1860
1861 if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL)
1862 {
1863 if(Options::solverType() != SSolver::SOLVER_ASTAP) //You don't need astrometry index files to use ASTAP
1864 {
1865 bool foundAnIndex = false;
1866 for(QString dataDir : astrometryDataDirs)
1867 {
1868 QDir dir = QDir(dataDir);
1869 if(dir.exists())
1870 {
1871 dir.setNameFilters(QStringList() << "*.fits");
1872 QStringList indexList = dir.entryList();
1873 if(indexList.count() > 0)
1874 foundAnIndex = true;
1875 }
1876 }
1877 if(!foundAnIndex)
1878 {
1879 appendLogText(
1880 i18n("No index files were found on your system in the specified index file directories. Please download some index files or add the correct directory to the list."));
1881 KConfigDialog * alignSettings = KConfigDialog::exists("alignsettings");
1882 if(alignSettings && m_IndexFilesPage)
1883 {
1884 alignSettings->setCurrentPage(m_IndexFilesPage);
1885 alignSettings->show();
1886 }
1887 }
1888 }
1889 if (m_StellarSolver)
1890 {
1891 auto *solver = m_StellarSolver.release();
1892 solver->disconnect(this);
1893 if (solver->isRunning())
1894 {
1895 connect(solver, &StellarSolver::finished, solver, &StellarSolver::deleteLater);
1896 solver->abort();
1897 }
1898 else
1899 solver->deleteLater();
1900 }
1901 if (!m_ImageData)
1902 m_ImageData = alignView->imageData();
1903 m_StellarSolver.reset(new StellarSolver(SSolver::SOLVE, m_ImageData->getStatistics(), m_ImageData->getImageBuffer()));
1904 m_StellarSolver->setProperty("ExtractorType", Options::solveSextractorType());
1905 m_StellarSolver->setProperty("SolverType", Options::solverType());
1906 connect(m_StellarSolver.get(), &StellarSolver::ready, this, &Align::solverComplete);
1907 connect(m_StellarSolver.get(), &StellarSolver::logOutput, this, &Align::appendLogText);
1908 m_StellarSolver->setIndexFolderPaths(Options::astrometryIndexFolderList());
1909 m_StellarSolver->setParameters(m_StellarSolverProfiles.at(Options::solveOptionsProfile()));
1910
1911 const SSolver::SolverType type = static_cast<SSolver::SolverType>(m_StellarSolver->property("SolverType").toInt());
1912 if(type == SSolver::SOLVER_LOCALASTROMETRY || type == SSolver::SOLVER_ASTAP)
1913 {
1914 QString filename = QDir::tempPath() + QString("/solver%1.fits").arg(QUuid::createUuid().toString().remove(
1915 QRegularExpression("[-{}]")));
1916 alignView->saveImage(filename);
1917 m_StellarSolver->setProperty("FileToProcess", filename);
1918
1919 if(Options::sextractorIsInternal())
1920 m_StellarSolver->setProperty("SextractorBinaryPath", QString("%1/%2").arg(QCoreApplication::applicationDirPath())
1921 .arg("astrometry/bin/sex"));
1922 else
1923 m_StellarSolver->setProperty("SextractorBinaryPath", Options::sextractorBinary());
1924
1925 if (Options::astrometrySolverIsInternal())
1926 m_StellarSolver->setProperty("SolverPath", QString("%1/%2").arg(QCoreApplication::applicationDirPath())
1927 .arg("astrometry/bin/solve-field"));
1928 else
1929 m_StellarSolver->setProperty("SolverPath", Options::astrometrySolverBinary());
1930
1931 m_StellarSolver->setProperty("ASTAPBinaryPath", Options::aSTAPExecutable());
1932 if (Options::astrometryWCSIsInternal())
1933 m_StellarSolver->setProperty("WCSPath", QString("%1/%2").arg(QCoreApplication::applicationDirPath())
1934 .arg("astrometry/bin/wcsinfo"));
1935 else
1936 m_StellarSolver->setProperty("WCSPath", Options::astrometryWCSInfo());
1937
1938 //No need for a conf file this way.
1939 m_StellarSolver->setProperty("AutoGenerateAstroConfig", true);
1940 }
1941
1942 if(type == SSolver::SOLVER_ONLINEASTROMETRY )
1943 {
1944 QString filename = QDir::tempPath() + QString("/solver%1.jpg").arg(QUuid::createUuid().toString().remove(
1945 QRegularExpression("[-{}]")));
1946 alignView->saveImage(filename);
1947
1948 m_StellarSolver->setProperty("FileToProcess", filename);
1949 m_StellarSolver->setProperty("AstrometryAPIKey", Options::astrometryAPIKey());
1950 m_StellarSolver->setProperty("AstrometryAPIURL", Options::astrometryAPIURL());
1951 }
1952
1953 bool useImageScale = Options::astrometryUseImageScale();
1954 if (useBlindScale == BLIND_ENGAGNED)
1955 {
1956 useImageScale = false;
1957 useBlindScale = BLIND_USED;
1958 appendLogText(i18n("Solving with blind image scale..."));
1959 }
1960
1961 bool useImagePostion = Options::astrometryUsePosition();
1962 if (useBlindPosition == BLIND_ENGAGNED)
1963 {
1964 useImagePostion = false;
1965 useBlindPosition = BLIND_USED;
1966 appendLogText(i18n("Solving with blind image position..."));
1967 }
1968
1969 if (m_SolveFromFile)
1970 {
1971 FITSImage::Solution solution;
1972 m_ImageData->parseSolution(solution);
1973
1974 if (useImageScale && solution.pixscale > 0)
1975 m_StellarSolver->setSearchScale(solution.pixscale * 0.8,
1976 solution.pixscale * 1.2,
1977 SSolver::ARCSEC_PER_PIX);
1978 else
1979 m_StellarSolver->setProperty("UseScale", false);
1980
1981 if (useImagePostion && solution.ra > 0)
1982 m_StellarSolver->setSearchPositionInDegrees(solution.ra, solution.dec);
1983 else
1984 m_StellarSolver->setProperty("UsePostion", false);
1985 }
1986 else
1987 {
1988 //Setting the initial search scale settings
1989 if (useImageScale)
1990 {
1991 SSolver::ScaleUnits units = static_cast<SSolver::ScaleUnits>(Options::astrometryImageScaleUnits());
1992 // Extend search scale from 80% to 120%
1993 m_StellarSolver->setSearchScale(Options::astrometryImageScaleLow() * 0.8,
1994 Options::astrometryImageScaleHigh() * 1.2,
1995 units);
1996 }
1997 else
1998 m_StellarSolver->setProperty("UseScale", false);
1999 //Setting the initial search location settings
2000 if(useImagePostion)
2001 m_StellarSolver->setSearchPositionInDegrees(telescopeCoord.ra().Degrees(), telescopeCoord.dec().Degrees());
2002 else
2003 m_StellarSolver->setProperty("UsePostion", false);
2004 }
2005
2006 if(Options::alignmentLogging())
2007 {
2008 m_StellarSolver->setLogLevel(static_cast<SSolver::logging_level>(Options::loggerLevel()));
2009 m_StellarSolver->setSSLogLevel(SSolver::LOG_NORMAL);
2010 if(Options::astrometryLogToFile())
2011 {
2012 m_StellarSolver->setProperty("LogToFile", true);
2013 m_StellarSolver->setProperty("LogFileName", Options::astrometryLogFilepath());
2014 }
2015 }
2016 else
2017 {
2018 m_StellarSolver->setLogLevel(SSolver::LOG_NONE);
2019 m_StellarSolver->setSSLogLevel(SSolver::LOG_OFF);
2020 }
2021
2022 //Unless we decide to load the WCS Coord, let's turn it off.
2023 //Be sure to set this to true instead if we want WCS from the solve.
2024 m_StellarSolver->setLoadWCS(false);
2025
2026 // Start solving process
2027 m_StellarSolver->start();
2028 }
2029 else
2030 {
2031 // This should run only for load&slew. For regular solve, we don't get here
2032 // as the image is read and solved server-side.
2033 remoteParser->startSolver(m_ImageData->filename(), generateRemoteArgs(m_ImageData), false);
2034 }
2035
2036 // Kick off timer
2037 solverTimer.start();
2038
2039 state = ALIGN_PROGRESS;
2040 emit newStatus(state);
2041 }
2042
solverComplete()2043 void Align::solverComplete()
2044 {
2045 disconnect(m_StellarSolver.get(), &StellarSolver::ready, this, &Align::solverComplete);
2046 if(!m_StellarSolver->solvingDone() || m_StellarSolver->failed())
2047 {
2048 // If processed, we retruned. Otherwise, it is a fail
2049 if (CHECK_PAH(processSolverFailure()))
2050 return;
2051 solverFailed();
2052 return;
2053 }
2054 else
2055 {
2056 FITSImage::Solution solution = m_StellarSolver->getSolution();
2057 // Would be better if parity was a bool field instead of a QString with "pos" and "neg" as possible values.
2058 const bool eastToTheRight = solution.parity == "pos" ? false : true;
2059 solverFinished(solution.orientation, solution.ra, solution.dec, solution.pixscale, eastToTheRight);
2060 }
2061 }
2062
solverFinished(double orientation,double ra,double dec,double pixscale,bool eastToTheRight)2063 void Align::solverFinished(double orientation, double ra, double dec, double pixscale, bool eastToTheRight)
2064 {
2065 pi->stopAnimation();
2066 stopB->setEnabled(false);
2067 solveB->setEnabled(true);
2068
2069 sOrientation = orientation;
2070 sRA = ra;
2071 sDEC = dec;
2072
2073 double elapsed = solverTimer.elapsed() / 1000.0;
2074 appendLogText(i18n("Solver completed after %1 seconds.", QString::number(elapsed, 'f', 2)));
2075
2076 // Reset Telescope Type to remembered value
2077 if (rememberTelescopeType != ISD::CCD::TELESCOPE_UNKNOWN)
2078 {
2079 currentCCD->setTelescopeType(rememberTelescopeType);
2080 rememberTelescopeType = ISD::CCD::TELESCOPE_UNKNOWN;
2081 }
2082
2083 m_AlignTimer.stop();
2084 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParserDevice && remoteParser.get())
2085 {
2086 // Disable remote parse
2087 dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->setEnabled(false);
2088 }
2089
2090 int binx, biny;
2091 ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD);
2092 targetChip->getBinning(&binx, &biny);
2093
2094 if (Options::alignmentLogging())
2095 {
2096 QString parityString = eastToTheRight ? "neg" : "pos";
2097 appendLogText(i18n("Solver RA (%1) DEC (%2) Orientation (%3) Pixel Scale (%4) Parity (%5)", QString::number(ra, 'f', 5),
2098 QString::number(dec, 'f', 5), QString::number(orientation, 'f', 5),
2099 QString::number(pixscale, 'f', 5), parityString));
2100 }
2101
2102 // When solving (without Load&Slew), update effective FOV and focal length accordingly.
2103 if (!m_SolveFromFile &&
2104 (fov_x == 0 || m_EffectiveFOVPending || std::fabs(pixscale - fov_pixscale) > 0.005) &&
2105 pixscale > 0)
2106 {
2107 double newFOVW = ccd_width * pixscale / binx / 60.0;
2108 double newFOVH = ccd_height * pixscale / biny / 60.0;
2109
2110 calculateEffectiveFocalLength(newFOVW);
2111 saveNewEffectiveFOV(newFOVW, newFOVH);
2112
2113 m_EffectiveFOVPending = false;
2114 }
2115
2116 alignCoord.setRA0(ra / 15.0);
2117 alignCoord.setDec0(dec);
2118 RotOut->setText(QString::number(orientation, 'f', 5));
2119
2120 // Convert to JNow
2121 alignCoord.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd());
2122 // Get horizontal coords
2123 alignCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
2124
2125 // Do not update diff if we are performing load & slew.
2126 if (!m_SolveFromFile)
2127 {
2128 pixScaleOut->setText(QString::number(pixscale, 'f', 2));
2129 calculateAlignTargetDiff();
2130 }
2131
2132 double solverPA = orientation;
2133 // TODO 2019-11-06 JM: KStars needs to support "upside-down" displays since this is a hack.
2134 // Because astrometry reads image upside-down (bottom to top), the orientation is rotated 180 degrees when compared to PA
2135 // PA = Orientation + 180
2136 double solverFlippedPA = orientation + 180;
2137 // Limit PA to -180 to +180
2138 if (solverFlippedPA > 180)
2139 solverFlippedPA -= 360;
2140 if (solverFlippedPA < -180)
2141 solverFlippedPA += 360;
2142 solverFOV->setCenter(alignCoord);
2143 solverFOV->setPA(solverFlippedPA);
2144 solverFOV->setImageDisplay(Options::astrometrySolverOverlay());
2145 // Sensor FOV as well
2146 sensorFOV->setPA(solverFlippedPA);
2147
2148 QString ra_dms, dec_dms;
2149 getFormattedCoords(alignCoord.ra().Hours(), alignCoord.dec().Degrees(), ra_dms, dec_dms);
2150
2151 SolverRAOut->setText(ra_dms);
2152 SolverDecOut->setText(dec_dms);
2153
2154
2155 if (Options::astrometrySolverWCS())
2156 {
2157 auto ccdRotation = currentCCD->getBaseDevice()->getNumber("CCD_ROTATION");
2158 if (ccdRotation)
2159 {
2160 auto rotation = ccdRotation->findWidgetByName("CCD_ROTATION_VALUE");
2161 if (rotation)
2162 {
2163 ClientManager *clientManager = currentCCD->getDriverInfo()->getClientManager();
2164 rotation->setValue(orientation);
2165 clientManager->sendNewNumber(ccdRotation);
2166
2167 if (m_wcsSynced == false)
2168 {
2169 appendLogText(
2170 i18n("WCS information updated. Images captured from this point forward shall have valid WCS."));
2171
2172 // Just send telescope info in case the CCD driver did not pick up before.
2173 auto telescopeInfo = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO");
2174 if (telescopeInfo)
2175 clientManager->sendNewNumber(telescopeInfo);
2176
2177 m_wcsSynced = true;
2178 }
2179 }
2180 }
2181 }
2182
2183 m_CaptureErrorCounter = 0;
2184 m_SlewErrorCounter = 0;
2185 m_CaptureTimeoutCounter = 0;
2186
2187 appendLogText(i18n("Solution coordinates: RA (%1) DEC (%2) Telescope Coordinates: RA (%3) DEC (%4)",
2188 alignCoord.ra().toHMSString(), alignCoord.dec().toDMSString(), telescopeCoord.ra().toHMSString(),
2189 telescopeCoord.dec().toDMSString()));
2190 if (!m_SolveFromFile && m_CurrentGotoMode == GOTO_SLEW)
2191 {
2192 dms diffDeg(m_TargetDiffTotal / 3600.0);
2193 appendLogText(i18n("Target is within %1 degrees of solution coordinates.", diffDeg.toDMSString()));
2194 }
2195
2196 if (rememberUploadMode != currentCCD->getUploadMode())
2197 currentCCD->setUploadMode(rememberUploadMode);
2198
2199 // Remember to reset fast exposure
2200 if (m_RememberCameraFastExposure)
2201 {
2202 m_RememberCameraFastExposure = false;
2203 currentCCD->setFastExposureEnabled(true);
2204 }
2205
2206 //This block of code along with some sections in the switch below will set the status report in the solution table for this item.
2207 std::unique_ptr<QTableWidgetItem> statusReport(new QTableWidgetItem());
2208 int currentRow = solutionTable->rowCount() - 1;
2209 if (!m_SolveFromFile)
2210 {
2211 stopProgressAnimation();
2212 solutionTable->setCellWidget(currentRow, 3, new QWidget());
2213 statusReport->setFlags(Qt::ItemIsSelectable);
2214 }
2215
2216 if (m_SolveFromFile && Options::astrometryUseRotator())
2217 {
2218 loadSlewTargetPA = solverPA;
2219 qCDebug(KSTARS_EKOS_ALIGN) << "loaSlewTargetPA:" << loadSlewTargetPA;
2220 }
2221 else if (Options::astrometryUseRotator())
2222 {
2223 currentRotatorPA = solverPA;
2224
2225 // When Load&Slew image is solved, we check if we need to rotate the rotator to match the position angle of the image
2226 if (currentRotator != nullptr)
2227 {
2228 // Update Rotator offsets
2229 auto absAngle = currentRotator->getBaseDevice()->getNumber("ABS_ROTATOR_ANGLE");
2230 if (absAngle)
2231 {
2232 // PA = RawAngle * Multiplier + Offset
2233 double rawAngle = absAngle->at(0)->getValue();
2234 double offset = range360(solverPA - (rawAngle * Options::pAMultiplier()));
2235
2236 qCDebug(KSTARS_EKOS_ALIGN) << "Raw Rotator Angle:" << rawAngle << "Rotator PA:" << currentRotatorPA
2237 << "Rotator Offset:" << offset;
2238 Options::setPAOffset(offset);
2239 }
2240
2241 if (absAngle && std::isnan(loadSlewTargetPA) == false
2242 && fabs(currentRotatorPA - loadSlewTargetPA) * 60 > Options::astrometryRotatorThreshold())
2243 {
2244 double rawAngle = range360((loadSlewTargetPA - Options::pAOffset()) / Options::pAMultiplier());
2245 // if (rawAngle < 0)
2246 // rawAngle += 360;
2247 // else if (rawAngle > 360)
2248 // rawAngle -= 360;
2249 absAngle->at(0)->setValue(rawAngle);
2250 ClientManager *clientManager = currentRotator->getDriverInfo()->getClientManager();
2251 clientManager->sendNewNumber(absAngle);
2252 appendLogText(i18n("Setting position angle to %1 degrees E of N...", loadSlewTargetPA));
2253 return;
2254 }
2255 }
2256 else if (std::isnan(loadSlewTargetPA) == false)
2257 {
2258 double current = range360(currentRotatorPA);
2259 double target = range360(loadSlewTargetPA);
2260 double targetFlipped = range360(loadSlewTargetPA + 180);
2261
2262 double diff = current - target;
2263 if (fabs(current + 360.0 - target) < fabs(diff))
2264 {
2265 diff = current + 360.0 - target;
2266 }
2267
2268 if (fabs(current - targetFlipped) < fabs(diff))
2269 {
2270 diff = current - targetFlipped;
2271 target = targetFlipped;
2272 }
2273
2274 if (fabs(current + 360.0 - targetFlipped) < fabs(diff))
2275 {
2276 diff = current + 360.0 - targetFlipped;
2277 target = targetFlipped;
2278 }
2279
2280 double threshold = Options::astrometryRotatorThreshold() / 60.0;
2281
2282 appendLogText(i18n("Current Rotation is %1; Target Rotation is %2; diff: %3", current, target, diff));
2283
2284 m_ManualRotator->setRotatorDiff(current, target, diff);
2285 if (fabs(diff) > threshold)
2286 {
2287 targetAccuracyNotMet = true;
2288 m_ManualRotator->show();
2289 m_ManualRotator->raise();
2290 return;
2291 }
2292 else
2293 {
2294 loadSlewTargetPA = std::numeric_limits<double>::quiet_NaN();
2295 targetAccuracyNotMet = false;
2296 }
2297 }
2298 }
2299
2300 emit newSolverResults(orientation, ra, dec, pixscale);
2301 QJsonObject solution =
2302 {
2303 {"camera", currentCCD->getDeviceName()},
2304 {"ra", SolverRAOut->text()},
2305 {"de", SolverDecOut->text()},
2306 {"dRA", m_TargetDiffRA},
2307 {"dDE", m_TargetDiffDE},
2308 {"targetDiff", m_TargetDiffTotal},
2309 {"pix", pixscale},
2310 {"rot", orientation},
2311 {"fov", FOVOut->text()},
2312 };
2313 emit newSolution(solution.toVariantMap());
2314
2315 switch (m_CurrentGotoMode)
2316 {
2317 case GOTO_SYNC:
2318 executeGOTO();
2319
2320 if (!m_SolveFromFile)
2321 {
2322 stopProgressAnimation();
2323 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2324 solutionTable->setItem(currentRow, 3, statusReport.release());
2325 }
2326
2327 return;
2328
2329 case GOTO_SLEW:
2330 if (m_SolveFromFile || m_TargetDiffTotal > static_cast<double>(accuracySpin->value()))
2331 {
2332 if (!m_SolveFromFile && ++solverIterations == MAXIMUM_SOLVER_ITERATIONS)
2333 {
2334 appendLogText(i18n("Maximum number of iterations reached. Solver failed."));
2335
2336 if (!m_SolveFromFile)
2337 {
2338 statusReport->setIcon(QIcon(":/icons/AlignFailure.svg"));
2339 solutionTable->setItem(currentRow, 3, statusReport.release());
2340 }
2341
2342 solverFailed();
2343 return;
2344 }
2345
2346 targetAccuracyNotMet = true;
2347
2348 if (!m_SolveFromFile)
2349 {
2350 stopProgressAnimation();
2351 statusReport->setIcon(QIcon(":/icons/AlignWarning.svg"));
2352 solutionTable->setItem(currentRow, 3, statusReport.release());
2353 }
2354
2355 executeGOTO();
2356 return;
2357 }
2358
2359 if (!m_SolveFromFile)
2360 {
2361 stopProgressAnimation();
2362 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2363 solutionTable->setItem(currentRow, 3, statusReport.release());
2364 }
2365
2366 appendLogText(i18n("Target is within acceptable range. Astrometric solver is successful."));
2367
2368 // if (mountModelRunning)
2369 // {
2370 // finishAlignmentPoint(true);
2371 // if (mountModelRunning)
2372 // return;
2373 // }
2374 break;
2375
2376 case GOTO_NOTHING:
2377 if (!m_SolveFromFile)
2378 {
2379 stopProgressAnimation();
2380 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2381 solutionTable->setItem(currentRow, 3, statusReport.release());
2382 }
2383 break;
2384 }
2385
2386 KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully"));
2387 state = ALIGN_COMPLETE;
2388 emit newStatus(state);
2389 solverIterations = 0;
2390
2391 solverFOV->setProperty("visible", true);
2392
2393 if (!matchPAHStage(PAA::PAH_IDLE))
2394 m_PolarAlignmentAssistant->processPAHStage(orientation, ra, dec, pixscale, eastToTheRight);
2395 else
2396 {
2397 solveB->setEnabled(true);
2398 loadSlewB->setEnabled(true);
2399 }
2400 }
2401
solverFailed()2402 void Align::solverFailed()
2403 {
2404 if (state != ALIGN_ABORTED)
2405 {
2406 // Try to solve with scale turned off, if not turned off already
2407 if (Options::astrometryUseImageScale() && useBlindScale == BLIND_IDLE)
2408 {
2409 useBlindScale = BLIND_ENGAGNED;
2410 setAlignTableResult(ALIGN_RESULT_FAILED);
2411 captureAndSolve();
2412 return;
2413 }
2414
2415 // Try to solve with the position turned off, if not turned off already
2416 if (Options::astrometryUsePosition() && useBlindPosition == BLIND_IDLE)
2417 {
2418 useBlindPosition = BLIND_ENGAGNED;
2419 setAlignTableResult(ALIGN_RESULT_FAILED);
2420 captureAndSolve();
2421 return;
2422 }
2423
2424
2425 appendLogText(i18n("Solver Failed."));
2426 if(!Options::alignmentLogging())
2427 appendLogText(
2428 i18n("Please check you have sufficient stars in the image, the indicated FOV is correct, and the necessary index files are installed. Enable Alignment Logging in Setup Tab -> Logs to get detailed information on the failure."));
2429
2430 KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed"),
2431 KSNotification::EVENT_ALERT);
2432 }
2433
2434 pi->stopAnimation();
2435 stopB->setEnabled(false);
2436 solveB->setEnabled(true);
2437 loadSlewB->setEnabled(true);
2438
2439 m_AlignTimer.stop();
2440
2441 m_SolveFromFile = false;
2442 solverIterations = 0;
2443 m_CaptureErrorCounter = 0;
2444 m_CaptureTimeoutCounter = 0;
2445 m_SlewErrorCounter = 0;
2446
2447 state = ALIGN_FAILED;
2448 emit newStatus(state);
2449
2450 solverFOV->setProperty("visible", false);
2451
2452 setAlignTableResult(ALIGN_RESULT_FAILED);
2453 }
2454
stop(Ekos::AlignState mode)2455 void Align::stop(Ekos::AlignState mode)
2456 {
2457 m_CaptureTimer.stop();
2458 if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL && m_StellarSolver)
2459 m_StellarSolver->abort();
2460 else if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser)
2461 remoteParser->stopSolver();
2462 //parser->stopSolver();
2463 pi->stopAnimation();
2464 stopB->setEnabled(false);
2465 solveB->setEnabled(true);
2466 loadSlewB->setEnabled(true);
2467
2468 // Reset Telescope Type to remembered value
2469 if (rememberTelescopeType != ISD::CCD::TELESCOPE_UNKNOWN)
2470 {
2471 currentCCD->setTelescopeType(rememberTelescopeType);
2472 rememberTelescopeType = ISD::CCD::TELESCOPE_UNKNOWN;
2473 }
2474
2475 m_SolveFromFile = false;
2476 solverIterations = 0;
2477 m_CaptureErrorCounter = 0;
2478 m_CaptureTimeoutCounter = 0;
2479 m_SlewErrorCounter = 0;
2480 m_AlignTimer.stop();
2481
2482 disconnect(currentCCD, &ISD::CCD::newImage, this, &Ekos::Align::processData);
2483 disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Align::checkCCDExposureProgress);
2484
2485 if (rememberUploadMode != currentCCD->getUploadMode())
2486 currentCCD->setUploadMode(rememberUploadMode);
2487
2488 // Remember to reset fast exposure
2489 if (m_RememberCameraFastExposure)
2490 {
2491 m_RememberCameraFastExposure = false;
2492 currentCCD->setFastExposureEnabled(true);
2493 }
2494
2495 ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD);
2496
2497 // If capture is still in progress, let's stop that.
2498 if (matchPAHStage(PAA::PAH_POST_REFRESH))
2499 {
2500 if (targetChip->isCapturing())
2501 targetChip->abortExposure();
2502
2503 appendLogText(i18n("Refresh is complete."));
2504 }
2505 else
2506 {
2507 if (targetChip->isCapturing())
2508 {
2509 targetChip->abortExposure();
2510 appendLogText(i18n("Capture aborted."));
2511 }
2512 else
2513 {
2514 double elapsed = solverTimer.elapsed() / 1000.0;
2515 appendLogText(i18n("Solver aborted after %1 seconds.", QString::number(elapsed, 'f', 2)));
2516 }
2517 }
2518
2519 state = mode;
2520 emit newStatus(state);
2521
2522 setAlignTableResult(ALIGN_RESULT_FAILED);
2523 }
2524
getProgressStatus()2525 QProgressIndicator * Align::getProgressStatus()
2526 {
2527 int currentRow = solutionTable->rowCount() - 1;
2528
2529 // check if the current row indicates a progress state
2530 // 1. no row present
2531 if (currentRow < 0)
2532 return nullptr;
2533 // 2. indicator is not present or not a progress indicator
2534 QWidget *indicator = solutionTable->cellWidget(currentRow, 3);
2535 if (indicator == nullptr)
2536 return nullptr;
2537 return dynamic_cast<QProgressIndicator *>(indicator);
2538 }
2539
stopProgressAnimation()2540 void Align::stopProgressAnimation()
2541 {
2542 QProgressIndicator *progress_indicator = getProgressStatus();
2543 if (progress_indicator != nullptr)
2544 progress_indicator->stopAnimation();
2545 }
2546
getSolutionResult()2547 QList<double> Align::getSolutionResult()
2548 {
2549 QList<double> result;
2550
2551 result << sOrientation << sRA << sDEC;
2552
2553 return result;
2554 }
2555
appendLogText(const QString & text)2556 void Align::appendLogText(const QString &text)
2557 {
2558 m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
2559 KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text));
2560
2561 qCInfo(KSTARS_EKOS_ALIGN) << text;
2562
2563 emit newLog(text);
2564 }
2565
clearLog()2566 void Align::clearLog()
2567 {
2568 m_LogText.clear();
2569 emit newLog(QString());
2570 }
2571
processSwitch(ISwitchVectorProperty * svp)2572 void Align::processSwitch(ISwitchVectorProperty *svp)
2573 {
2574 if (!strcmp(svp->name, "DOME_MOTION"))
2575 {
2576 // If dome is not ready and state is now
2577 if (domeReady == false && svp->s == IPS_OK)
2578 {
2579 domeReady = true;
2580 // trigger process number for mount so that it proceeds with normal workflow since
2581 // it was stopped by dome not being ready
2582 handleMountStatus();
2583 }
2584 }
2585 else if ((!strcmp(svp->name, "TELESCOPE_MOTION_NS") || !strcmp(svp->name, "TELESCOPE_MOTION_WE")))
2586 switch (svp->s)
2587 {
2588 case IPS_BUSY:
2589 // react upon mount motion
2590 handleMountMotion();
2591 m_wasSlewStarted = true;
2592 break;
2593 default:
2594 qCDebug(KSTARS_EKOS_ALIGN) << "Mount motion finished.";
2595 handleMountStatus();
2596 break;
2597 }
2598
2599 }
2600
processNumber(INumberVectorProperty * nvp)2601 void Align::processNumber(INumberVectorProperty *nvp)
2602 {
2603 if (!strcmp(nvp->name, "EQUATORIAL_EOD_COORD") || !strcmp(nvp->name, "EQUATORIAL_COORD"))
2604 {
2605 QString ra_dms, dec_dms;
2606
2607 getFormattedCoords(telescopeCoord.ra().Hours(), telescopeCoord.dec().Degrees(), ra_dms, dec_dms);
2608
2609 ScopeRAOut->setText(ra_dms);
2610 ScopeDecOut->setText(dec_dms);
2611
2612 // qCDebug(KSTARS_EKOS_ALIGN) << "## RA" << ra_dms << "DE" << dec_dms
2613 // << "state:" << pstateStr(nvp->s) << "slewStarted?" << m_wasSlewStarted;
2614
2615 switch (nvp->s)
2616 {
2617 // Idle --> Mount not tracking or slewing
2618 case IPS_IDLE:
2619 m_wasSlewStarted = false;
2620 //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_IDLE --> setting slewStarted to FALSE";
2621 break;
2622
2623 // Ok --> Mount Tracking. If m_wasSlewStarted is true
2624 // then it just finished slewing
2625 case IPS_OK:
2626 {
2627 // Update the boxes as the mount just finished slewing
2628 if (m_wasSlewStarted && Options::astrometryAutoUpdatePosition())
2629 {
2630 //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_OK --> Auto Update Position...";
2631 opsAstrometry->estRA->setText(ra_dms);
2632 opsAstrometry->estDec->setText(dec_dms);
2633
2634 Options::setAstrometryPositionRA(nvp->np[0].value * 15);
2635 Options::setAstrometryPositionDE(nvp->np[1].value);
2636
2637 //generateArgs();
2638 }
2639
2640 // If dome is syncing, wait until it stops
2641 if (currentDome && currentDome->isMoving())
2642 {
2643 domeReady = false;
2644 return;
2645 }
2646
2647 // If we are looking for celestial pole
2648 if (m_wasSlewStarted && matchPAHStage(PAA::PAH_FIND_CP))
2649 {
2650 //qCDebug(KSTARS_EKOS_ALIGN) << "## PAH_FIND_CP--> setting slewStarted to FALSE";
2651 m_wasSlewStarted = false;
2652 appendLogText(i18n("Mount completed slewing near celestial pole. Capture again to verify."));
2653 setSolverAction(GOTO_NOTHING);
2654
2655 m_PolarAlignmentAssistant->setPAHStage(PAA::PAH_FIRST_CAPTURE);
2656 return;
2657 }
2658
2659 switch (state)
2660 {
2661 case ALIGN_PROGRESS:
2662 break;
2663
2664 case ALIGN_SYNCING:
2665 {
2666 m_wasSlewStarted = false;
2667 //qCDebug(KSTARS_EKOS_ALIGN) << "## ALIGN_SYNCING --> setting slewStarted to FALSE";
2668 if (m_CurrentGotoMode == GOTO_SLEW)
2669 {
2670 Slew();
2671 return;
2672 }
2673 else
2674 {
2675 appendLogText(i18n("Mount is synced to solution coordinates. Astrometric solver is successful."));
2676 KSNotification::event(QLatin1String("AlignSuccessful"),
2677 i18n("Astrometry alignment completed successfully"));
2678 state = ALIGN_COMPLETE;
2679 emit newStatus(state);
2680 solverIterations = 0;
2681 }
2682 }
2683 break;
2684
2685 case ALIGN_SLEWING:
2686
2687 if (!didSlewStart())
2688 {
2689 // If mount has not started slewing yet, then skip
2690 //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew planned, but not started slewing yet...";
2691 break;
2692 }
2693
2694 //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew completed.";
2695 m_wasSlewStarted = false;
2696 if (m_SolveFromFile)
2697 {
2698 m_SolveFromFile = false;
2699
2700 state = ALIGN_PROGRESS;
2701 emit newStatus(state);
2702
2703 if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY)
2704 appendLogText(i18n("Settling..."));
2705 m_CaptureTimer.start(delaySpin->value());
2706 return;
2707 }
2708 else if (differentialSlewingActivated)
2709 {
2710 appendLogText(i18n("Differential slewing complete. Astrometric solver is successful."));
2711 KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully"));
2712 state = ALIGN_COMPLETE;
2713 emit newStatus(state);
2714 solverIterations = 0;
2715 }
2716 else if (m_CurrentGotoMode == GOTO_SLEW || (m_MountModel && m_MountModel->isRunning()))
2717 {
2718 if (targetAccuracyNotMet)
2719 appendLogText(i18n("Slew complete. Target accuracy is not met, running solver again..."));
2720 else
2721 appendLogText(i18n("Slew complete. Solving Alignment Point. . ."));
2722
2723 targetAccuracyNotMet = false;
2724
2725 state = ALIGN_PROGRESS;
2726 emit newStatus(state);
2727
2728 if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY)
2729 appendLogText(i18n("Settling..."));
2730 m_CaptureTimer.start(delaySpin->value());
2731 return;
2732 }
2733 break;
2734
2735 default:
2736 {
2737 //qCDebug(KSTARS_EKOS_ALIGN) << "## Align State " << state << "--> setting slewStarted to FALSE";
2738 m_wasSlewStarted = false;
2739 }
2740 break;
2741 }
2742 }
2743 break;
2744
2745 // Busy --> Mount Slewing or Moving (NSWE buttons)
2746 case IPS_BUSY:
2747 {
2748 //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew running.";
2749 m_wasSlewStarted = true;
2750
2751 handleMountMotion();
2752 }
2753 break;
2754
2755 // Alert --> Mount has problem moving or communicating.
2756 case IPS_ALERT:
2757 {
2758 //qCDebug(KSTARS_EKOS_ALIGN) << "IPS_ALERT --> setting slewStarted to FALSE";
2759 m_wasSlewStarted = false;
2760
2761 if (state == ALIGN_SYNCING || state == ALIGN_SLEWING)
2762 {
2763 if (state == ALIGN_SYNCING)
2764 appendLogText(i18n("Syncing failed."));
2765 else
2766 appendLogText(i18n("Slewing failed."));
2767
2768 if (++m_SlewErrorCounter == 3)
2769 {
2770 abort();
2771 return;
2772 }
2773 else
2774 {
2775 if (m_CurrentGotoMode == GOTO_SLEW)
2776 Slew();
2777 else
2778 Sync();
2779 }
2780 }
2781
2782 return;
2783 }
2784 }
2785
2786 RUN_PAH(processMountRotation(telescopeCoord.ra(), delaySpin->value()));
2787 }
2788 else if (!strcmp(nvp->name, "ABS_ROTATOR_ANGLE"))
2789 {
2790 // PA = RawAngle * Multiplier + Offset
2791 currentRotatorPA = (nvp->np[0].value * Options::pAMultiplier()) + Options::pAOffset();
2792 if (currentRotatorPA > 180)
2793 currentRotatorPA -= 360;
2794 if (currentRotatorPA < -180)
2795 currentRotatorPA += 360;
2796 if (std::isnan(loadSlewTargetPA) == false
2797 && fabs(currentRotatorPA - loadSlewTargetPA) * 60 <= Options::astrometryRotatorThreshold())
2798 {
2799 appendLogText(i18n("Rotator reached target position angle."));
2800 targetAccuracyNotMet = true;
2801 loadSlewTargetPA = std::numeric_limits<double>::quiet_NaN();
2802 QTimer::singleShot(Options::settlingTime(), this, &Ekos::Align::executeGOTO);
2803 }
2804 }
2805
2806 // N.B. Ekos::Manager already manages TELESCOPE_INFO, why here again?
2807 //if (!strcmp(coord->name, "TELESCOPE_INFO"))
2808 //syncTelescopeInfo();
2809 }
2810
handleMountMotion()2811 void Align::handleMountMotion()
2812 {
2813 if (state == ALIGN_PROGRESS)
2814 {
2815 if (matchPAHStage(PAA::PAH_IDLE))
2816 {
2817 // whoops, mount slews during alignment
2818 appendLogText(i18n("Slew detected, suspend solving..."));
2819 suspend();
2820 // reset the state to busy so that solving restarts after slewing finishes
2821 m_SolveFromFile = true;
2822 // if mount model is running, retry the current alignment point
2823 // if (mountModelRunning)
2824 // appendLogText(i18n("Restarting alignment point %1", currentAlignmentPoint + 1));
2825 }
2826
2827 state = ALIGN_SLEWING;
2828 }
2829 }
2830
handleMountStatus()2831 void Align::handleMountStatus()
2832 {
2833 auto nvp = currentTelescope->getBaseDevice()->getNumber(currentTelescope->isJ2000() ? "EQUATORIAL_COORD" :
2834 "EQUATORIAL_EOD_COORD");
2835
2836 if (nvp)
2837 processNumber(nvp);
2838 }
2839
2840
executeGOTO()2841 void Align::executeGOTO()
2842 {
2843 if (m_SolveFromFile)
2844 {
2845 m_targetCoord = alignCoord;
2846 SlewToTarget();
2847 }
2848 else if (m_CurrentGotoMode == GOTO_SYNC)
2849 Sync();
2850 else if (m_CurrentGotoMode == GOTO_SLEW)
2851 SlewToTarget();
2852 }
2853
Sync()2854 void Align::Sync()
2855 {
2856 state = ALIGN_SYNCING;
2857
2858 if (currentTelescope->Sync(&alignCoord))
2859 {
2860 emit newStatus(state);
2861 appendLogText(
2862 i18n("Syncing to RA (%1) DEC (%2)", alignCoord.ra().toHMSString(), alignCoord.dec().toDMSString()));
2863 }
2864 else
2865 {
2866 state = ALIGN_IDLE;
2867 emit newStatus(state);
2868 appendLogText(i18n("Syncing failed."));
2869 }
2870 }
2871
Slew()2872 void Align::Slew()
2873 {
2874 state = ALIGN_SLEWING;
2875 emit newStatus(state);
2876
2877 //qCDebug(KSTARS_EKOS_ALIGN) << "## Before SLEW command: wasSlewStarted -->" << m_wasSlewStarted;
2878 //m_wasSlewStarted = currentTelescope->Slew(&m_targetCoord);
2879 //qCDebug(KSTARS_EKOS_ALIGN) << "## After SLEW command: wasSlewStarted -->" << m_wasSlewStarted;
2880
2881 // JM 2019-08-23: Do not assume that slew was started immediately. Wait until IPS_BUSY state is triggered
2882 // from Goto
2883 currentTelescope->Slew(&m_targetCoord);
2884 slewStartTimer.start();
2885 appendLogText(i18n("Slewing to target coordinates: RA (%1) DEC (%2).", m_targetCoord.ra().toHMSString(),
2886 m_targetCoord.dec().toDMSString()));
2887 }
2888
SlewToTarget()2889 void Align::SlewToTarget()
2890 {
2891 if (canSync && !m_SolveFromFile)
2892 {
2893 // 2018-01-24 JM: This is ugly. Maybe use DBus? Signal/Slots? Ekos Manager usage like this should be avoided
2894 #if 0
2895 if (Ekos::Manager::Instance()->getCurrentJobName().isEmpty())
2896 {
2897 KSNotification::event(QLatin1String("EkosSchedulerTelescopeSynced"),
2898 i18n("Ekos job (%1) - Telescope synced",
2899 Ekos::Manager::Instance()->getCurrentJobName()));
2900 }
2901 #endif
2902
2903 // Do we perform a regular sync or use differential slewing?
2904 if (Options::astrometryDifferentialSlewing())
2905 {
2906 dms m_TargetDiffRA = alignCoord.ra().deltaAngle(m_targetCoord.ra());
2907 dms m_TargetDiffDE = alignCoord.dec().deltaAngle(m_targetCoord.dec());
2908
2909 m_targetCoord.setRA(m_targetCoord.ra() - m_TargetDiffRA);
2910 m_targetCoord.setDec(m_targetCoord.dec() - m_TargetDiffDE);
2911
2912 differentialSlewingActivated = true;
2913
2914 qCDebug(KSTARS_EKOS_ALIGN) << "Using differential slewing...";
2915
2916 Slew();
2917 }
2918 else
2919 Sync();
2920
2921 return;
2922 }
2923
2924 Slew();
2925 }
2926
2927
getFormattedCoords(double ra,double dec,QString & ra_str,QString & dec_str)2928 void Align::getFormattedCoords(double ra, double dec, QString &ra_str, QString &dec_str)
2929 {
2930 dms ra_s, dec_s;
2931 ra_s.setH(ra);
2932 dec_s.setD(dec);
2933
2934 ra_str = QString("%1:%2:%3")
2935 .arg(ra_s.hour(), 2, 10, QChar('0'))
2936 .arg(ra_s.minute(), 2, 10, QChar('0'))
2937 .arg(ra_s.second(), 2, 10, QChar('0'));
2938 if (dec_s.Degrees() < 0)
2939 dec_str = QString("-%1:%2:%3")
2940 .arg(abs(dec_s.degree()), 2, 10, QChar('0'))
2941 .arg(abs(dec_s.arcmin()), 2, 10, QChar('0'))
2942 .arg(dec_s.arcsec(), 2, 10, QChar('0'));
2943 else
2944 dec_str = QString("%1:%2:%3")
2945 .arg(dec_s.degree(), 2, 10, QChar('0'))
2946 .arg(dec_s.arcmin(), 2, 10, QChar('0'))
2947 .arg(dec_s.arcsec(), 2, 10, QChar('0'));
2948 }
2949
loadAndSlew(QString fileURL)2950 bool Align::loadAndSlew(QString fileURL)
2951 {
2952 if (fileURL.isEmpty())
2953 fileURL = QFileDialog::getOpenFileName(Ekos::Manager::Instance(), i18nc("@title:window", "Load Image"), dirPath,
2954 "Images (*.fits *.fits.fz *.fit *.fts "
2955 "*.jpg *.jpeg *.png *.gif *.bmp "
2956 "*.cr2 *.cr3 *.crw *.nef *.raf *.dng *.arw *.orf)");
2957
2958 if (fileURL.isEmpty())
2959 return false;
2960
2961 QFileInfo fileInfo(fileURL);
2962
2963 dirPath = fileInfo.absolutePath();
2964
2965 differentialSlewingActivated = false;
2966
2967 m_SolveFromFile = true;
2968
2969 if (m_PolarAlignmentAssistant)
2970 m_PolarAlignmentAssistant->stopPAHProcess();
2971
2972 slewR->setChecked(true);
2973 m_CurrentGotoMode = GOTO_SLEW;
2974
2975 solveB->setEnabled(false);
2976 stopB->setEnabled(true);
2977 pi->startAnimation();
2978
2979 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParserDevice == nullptr)
2980 {
2981 appendLogText(i18n("No remote astrometry driver detected, switching to StellarSolver."));
2982 setSolverMode(SOLVER_LOCAL);
2983 }
2984
2985 m_ImageData.clear();
2986
2987 alignView->loadFile(fileURL, false);
2988 //m_FileToSolve = fileURL;
2989 connect(alignView, &FITSView::loaded, this, &Align::startSolving);
2990
2991 return true;
2992 }
2993
loadAndSlew(const QByteArray & image,const QString & extension)2994 bool Align::loadAndSlew(const QByteArray &image, const QString &extension)
2995 {
2996 differentialSlewingActivated = false;
2997 m_SolveFromFile = true;
2998 RUN_PAH(stopPAHProcess());
2999 slewR->setChecked(true);
3000 m_CurrentGotoMode = GOTO_SLEW;
3001 solveB->setEnabled(false);
3002 stopB->setEnabled(true);
3003 pi->startAnimation();
3004
3005 QSharedPointer<FITSData> data;
3006 data.reset(new FITSData(), &QObject::deleteLater);
3007 data->loadFromBuffer(image, extension);
3008 alignView->loadData(data);
3009 startSolving();
3010 return true;
3011 }
3012
setExposure(double value)3013 void Align::setExposure(double value)
3014 {
3015 exposureIN->setValue(value);
3016 }
3017
setBinningIndex(int binIndex)3018 void Align::setBinningIndex(int binIndex)
3019 {
3020 syncSettings();
3021 Options::setSolverBinningIndex(binIndex);
3022
3023 // If sender is not our combo box, then we need to update the combobox itself
3024 if (dynamic_cast<QComboBox *>(sender()) != binningCombo)
3025 {
3026 binningCombo->blockSignals(true);
3027 binningCombo->setCurrentIndex(binIndex);
3028 binningCombo->blockSignals(false);
3029 }
3030
3031 // Need to calculate FOV and args for APP
3032 if (Options::astrometryImageScaleUnits() == SSolver::ARCSEC_PER_PIX)
3033 {
3034 calculateFOV();
3035 //generateArgs();
3036 }
3037 }
3038
setFOVTelescopeType(int index)3039 void Align::setFOVTelescopeType(int index)
3040 {
3041 FOVScopeCombo->setCurrentIndex(index);
3042 }
3043
addFilter(ISD::GDInterface * newFilter)3044 void Align::addFilter(ISD::GDInterface *newFilter)
3045 {
3046 for (auto filter : Filters)
3047 {
3048 if (filter->getDeviceName() == newFilter->getDeviceName())
3049 return;
3050 }
3051
3052 FilterCaptureLabel->setEnabled(true);
3053 FilterDevicesCombo->setEnabled(true);
3054 FilterPosLabel->setEnabled(true);
3055 FilterPosCombo->setEnabled(true);
3056
3057 FilterDevicesCombo->addItem(newFilter->getDeviceName());
3058
3059 Filters.append(static_cast<ISD::Filter *>(newFilter));
3060
3061 int filterWheelIndex = 1;
3062 if (Options::defaultAlignFilterWheel().isEmpty() == false)
3063 filterWheelIndex = FilterDevicesCombo->findText(Options::defaultAlignFilterWheel());
3064
3065 if (filterWheelIndex < 1)
3066 filterWheelIndex = 1;
3067
3068 checkFilter(filterWheelIndex);
3069 FilterDevicesCombo->setCurrentIndex(filterWheelIndex);
3070
3071 emit settingsUpdated(getSettings());
3072 }
3073
setFilterWheel(const QString & device)3074 bool Align::setFilterWheel(const QString &device)
3075 {
3076 bool deviceFound = false;
3077
3078 for (int i = 1; i < FilterDevicesCombo->count(); i++)
3079 if (device == FilterDevicesCombo->itemText(i))
3080 {
3081 checkFilter(i);
3082 deviceFound = true;
3083 break;
3084 }
3085
3086 if (deviceFound == false)
3087 return false;
3088
3089 return true;
3090 }
3091
filterWheel()3092 QString Align::filterWheel()
3093 {
3094 if (FilterDevicesCombo->currentIndex() >= 1)
3095 return FilterDevicesCombo->currentText();
3096
3097 return QString();
3098 }
3099
setFilter(const QString & filter)3100 bool Align::setFilter(const QString &filter)
3101 {
3102 if (FilterDevicesCombo->currentIndex() >= 1)
3103 {
3104 FilterPosCombo->setCurrentText(filter);
3105 return true;
3106 }
3107
3108 return false;
3109 }
3110
filter()3111 QString Align::filter()
3112 {
3113 return FilterPosCombo->currentText();
3114 }
3115
3116
checkFilter(int filterNum)3117 void Align::checkFilter(int filterNum)
3118 {
3119 if (filterNum == -1)
3120 {
3121 filterNum = FilterDevicesCombo->currentIndex();
3122 if (filterNum == -1)
3123 return;
3124 }
3125
3126 // "--" is no filter
3127 if (filterNum == 0)
3128 {
3129 currentFilter = nullptr;
3130 currentFilterPosition = -1;
3131 FilterPosCombo->clear();
3132 return;
3133 }
3134
3135 if (filterNum <= Filters.count())
3136 currentFilter = Filters.at(filterNum - 1);
3137
3138 FilterPosCombo->clear();
3139
3140 FilterPosCombo->addItems(filterManager->getFilterLabels());
3141
3142 currentFilterPosition = filterManager->getFilterPosition();
3143
3144 FilterPosCombo->setCurrentIndex(Options::lockAlignFilterIndex());
3145
3146 syncSettings();
3147 }
3148
setWCSEnabled(bool enable)3149 void Align::setWCSEnabled(bool enable)
3150 {
3151 if (!currentCCD)
3152 return;
3153
3154 auto wcsControl = currentCCD->getBaseDevice()->getSwitch("WCS_CONTROL");
3155
3156 auto wcs_enable = wcsControl->findWidgetByName("WCS_ENABLE");
3157 auto wcs_disable = wcsControl->findWidgetByName("WCS_DISABLE");
3158
3159 if (!wcs_enable || !wcs_disable)
3160 return;
3161
3162 if ((wcs_enable->getState() == ISS_ON && enable) || (wcs_disable->getState() == ISS_ON && !enable))
3163 return;
3164
3165 wcsControl->reset();
3166 if (enable)
3167 {
3168 appendLogText(i18n("World Coordinate System (WCS) is enabled. CCD rotation must be set either manually in the "
3169 "CCD driver or by solving an image before proceeding to capture any further images, "
3170 "otherwise the WCS information may be invalid."));
3171 wcs_enable->setState(ISS_ON);
3172 }
3173 else
3174 {
3175 wcs_disable->setState(ISS_ON);
3176 m_wcsSynced = false;
3177 appendLogText(i18n("World Coordinate System (WCS) is disabled."));
3178 }
3179
3180 ClientManager *clientManager = currentCCD->getDriverInfo()->getClientManager();
3181 if (clientManager)
3182 clientManager->sendNewSwitch(wcsControl);
3183 }
3184
checkCCDExposureProgress(ISD::CCDChip * targetChip,double remaining,IPState state)3185 void Align::checkCCDExposureProgress(ISD::CCDChip *targetChip, double remaining, IPState state)
3186 {
3187 INDI_UNUSED(targetChip);
3188 INDI_UNUSED(remaining);
3189
3190 if (state == IPS_ALERT)
3191 {
3192 if (++m_CaptureErrorCounter == 3 && !matchPAHStage(PolarAlignmentAssistant::PAH_REFRESH))
3193 {
3194 appendLogText(i18n("Capture error. Aborting..."));
3195 abort();
3196 return;
3197 }
3198
3199 appendLogText(i18n("Restarting capture attempt #%1", m_CaptureErrorCounter));
3200 setAlignTableResult(ALIGN_RESULT_FAILED);
3201 captureAndSolve();
3202 }
3203 }
3204
setAlignTableResult(AlignResult result)3205 void Align::setAlignTableResult(AlignResult result)
3206 {
3207 // Do nothing if the progress indicator is not running.
3208 // This is necessary since it could happen that a problem occurs
3209 // before #captureAndSolve() has been started and there does not
3210 // exist a table entry for the current run.
3211 QProgressIndicator *progress_indicator = getProgressStatus();
3212 if (progress_indicator == nullptr || ! progress_indicator->isAnimated())
3213 return;
3214 stopProgressAnimation();
3215
3216 QIcon icon;
3217 switch (result)
3218 {
3219 case ALIGN_RESULT_SUCCESS:
3220 icon = QIcon(":/icons/AlignSuccess.svg");
3221 break;
3222
3223 case ALIGN_RESULT_WARNING:
3224 icon = QIcon(":/icons/AlignWarning.svg");
3225 break;
3226
3227 case ALIGN_RESULT_FAILED:
3228 default:
3229 icon = QIcon(":/icons/AlignFailure.svg");
3230 break;
3231 }
3232 int currentRow = solutionTable->rowCount() - 1;
3233 solutionTable->setCellWidget(currentRow, 3, new QWidget());
3234 QTableWidgetItem *statusReport = new QTableWidgetItem();
3235 statusReport->setIcon(icon);
3236 statusReport->setFlags(Qt::ItemIsSelectable);
3237 solutionTable->setItem(currentRow, 3, statusReport);
3238 }
3239
setFocusStatus(Ekos::FocusState state)3240 void Align::setFocusStatus(Ekos::FocusState state)
3241 {
3242 m_FocusState = state;
3243 }
3244
getSolverDownsample(uint16_t binnedW)3245 uint8_t Align::getSolverDownsample(uint16_t binnedW)
3246 {
3247 uint8_t downsample = Options::astrometryDownsample();
3248
3249 if (!Options::astrometryAutoDownsample())
3250 return downsample;
3251
3252 while (downsample < 8)
3253 {
3254 if (binnedW / downsample <= 1024)
3255 break;
3256
3257 downsample += 2;
3258 }
3259
3260 return downsample;
3261 }
3262
saveSettleTime()3263 void Align::saveSettleTime()
3264 {
3265 Options::setSettlingTime(delaySpin->value());
3266 }
3267
setCaptureStatus(CaptureState newState)3268 void Align::setCaptureStatus(CaptureState newState)
3269 {
3270 switch (newState)
3271 {
3272 case CAPTURE_PROGRESS:
3273 {
3274 // Only reset m_targetCoord if capture wasn't suspended then resumed due to error duing ongoing
3275 // capture
3276 if (currentTelescope && m_CaptureState != CAPTURE_SUSPENDED)
3277 {
3278 updateTargetCoords();
3279 }
3280
3281 }
3282 break;
3283 case CAPTURE_ALIGNING:
3284 if (currentTelescope && currentTelescope->hasAlignmentModel() && Options::resetMountModelAfterMeridian())
3285 {
3286 qCDebug(KSTARS_EKOS_ALIGN) << "Post meridian flip mount model reset" << (currentTelescope->clearAlignmentModel() ?
3287 "successful." : "failed.");
3288 }
3289
3290 m_CaptureTimer.start(Options::settlingTime());
3291 break;
3292
3293 default:
3294 break;
3295 }
3296
3297 m_CaptureState = newState;
3298 }
3299
showFITSViewer()3300 void Align::showFITSViewer()
3301 {
3302 static int lastFVTabID = -1;
3303 if (m_ImageData)
3304 {
3305 QUrl url = QUrl::fromLocalFile("align.fits");
3306 if (fv.isNull())
3307 {
3308 fv = KStars::Instance()->createFITSViewer();
3309 fv->loadData(m_ImageData, url, &lastFVTabID);
3310 }
3311 else if (fv->updateData(m_ImageData, url, lastFVTabID, &lastFVTabID) == false)
3312 fv->loadData(m_ImageData, url, &lastFVTabID);
3313
3314 fv->show();
3315 }
3316 }
3317
toggleAlignWidgetFullScreen()3318 void Align::toggleAlignWidgetFullScreen()
3319 {
3320 if (alignWidget->parent() == nullptr)
3321 {
3322 alignWidget->setParent(this);
3323 rightLayout->insertWidget(0, alignWidget);
3324 alignWidget->showNormal();
3325 }
3326 else
3327 {
3328 alignWidget->setParent(nullptr);
3329 alignWidget->setWindowTitle(i18nc("@title:window", "Align Frame"));
3330 alignWidget->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint);
3331 alignWidget->showMaximized();
3332 alignWidget->show();
3333 }
3334 }
3335
updateTelescopeType(int index)3336 void Align::updateTelescopeType(int index)
3337 {
3338 if (currentCCD == nullptr)
3339 return;
3340
3341 // Reset style sheet.
3342 FocalLengthOut->setStyleSheet(QString());
3343
3344 syncSettings();
3345
3346 focal_length = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryFL : guideFL;
3347 aperture = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryAperture : guideAperture;
3348
3349 Options::setSolverScopeType(index);
3350
3351 syncTelescopeInfo();
3352 }
3353
3354 //void Align::setMountCoords(const QString &raStr, const QString &decStr, const QString &azStr,
3355 // const QString &altStr, int pierSide, const QString &haStr)
3356 //{
3357 // mountRa = dms(raStr, false);
3358 // mountDec = dms(decStr, true);
3359 // mountHa = dms(haStr, false);
3360 // mountAz = dms(azStr, true);
3361 // mountAlt = dms(altStr, true);
3362 // mountPierSide = static_cast<ISD::Telescope::PierSide>(pierSide);
3363 //}
3364
setMountStatus(ISD::Telescope::Status newState)3365 void Align::setMountStatus(ISD::Telescope::Status newState)
3366 {
3367 switch (newState)
3368 {
3369 case ISD::Telescope::MOUNT_PARKING:
3370 case ISD::Telescope::MOUNT_SLEWING:
3371 case ISD::Telescope::MOUNT_MOVING:
3372 solveB->setEnabled(false);
3373 loadSlewB->setEnabled(false);
3374 break;
3375
3376 default:
3377 if (state != ALIGN_PROGRESS)
3378 {
3379 solveB->setEnabled(true);
3380 if (matchPAHStage(PAA::PAH_IDLE))
3381 {
3382 loadSlewB->setEnabled(true);
3383 }
3384 }
3385 break;
3386 }
3387
3388 RUN_PAH(setMountStatus(newState));
3389 }
3390
setAstrometryDevice(ISD::GDInterface * newAstrometry)3391 void Align::setAstrometryDevice(ISD::GDInterface *newAstrometry)
3392 {
3393 remoteParserDevice = newAstrometry;
3394
3395 remoteSolverR->setEnabled(true);
3396 if (remoteParser.get() != nullptr)
3397 {
3398 remoteParser->setAstrometryDevice(remoteParserDevice);
3399 connect(remoteParser.get(), &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
3400 connect(remoteParser.get(), &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
3401 }
3402 }
3403
setRotator(ISD::GDInterface * newRotator)3404 void Align::setRotator(ISD::GDInterface *newRotator)
3405 {
3406 currentRotator = newRotator;
3407 connect(currentRotator, &ISD::GDInterface::numberUpdated, this, &Ekos::Align::processNumber, Qt::UniqueConnection);
3408 }
3409
refreshAlignOptions()3410 void Align::refreshAlignOptions()
3411 {
3412 solverFOV->setImageDisplay(Options::astrometrySolverWCS());
3413 m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000);
3414 }
3415
setFilterManager(const QSharedPointer<FilterManager> & manager)3416 void Align::setFilterManager(const QSharedPointer<FilterManager> &manager)
3417 {
3418 filterManager = manager;
3419
3420 connect(filterManager.data(), &FilterManager::ready, [this]()
3421 {
3422 if (filterPositionPending)
3423 {
3424 m_FocusState = FOCUS_IDLE;
3425 filterPositionPending = false;
3426 captureAndSolve();
3427 }
3428 }
3429 );
3430
3431 connect(filterManager.data(), &FilterManager::failed, [this]()
3432 {
3433 appendLogText(i18n("Filter operation failed."));
3434 abort();
3435 }
3436 );
3437
3438 connect(filterManager.data(), &FilterManager::newStatus, [this](Ekos::FilterState filterState)
3439 {
3440 if (filterPositionPending)
3441 {
3442 switch (filterState)
3443 {
3444 case FILTER_OFFSET:
3445 appendLogText(i18n("Changing focus offset by %1 steps...", filterManager->getTargetFilterOffset()));
3446 break;
3447
3448 case FILTER_CHANGE:
3449 {
3450 const int filterComboIndex = filterManager->getTargetFilterPosition() - 1;
3451 if (filterComboIndex >= 0 && filterComboIndex < FilterPosCombo->count())
3452 appendLogText(i18n("Changing filter to %1...", FilterPosCombo->itemText(filterComboIndex)));
3453 }
3454 break;
3455
3456 case FILTER_AUTOFOCUS:
3457 appendLogText(i18n("Auto focus on filter change..."));
3458 break;
3459
3460 default:
3461 break;
3462 }
3463 }
3464 });
3465
3466 connect(filterManager.data(), &FilterManager::labelsChanged, this, [this]()
3467 {
3468 checkFilter();
3469 });
3470 connect(filterManager.data(), &FilterManager::positionChanged, this, [this]()
3471 {
3472 checkFilter();
3473 });
3474 }
3475
getEffectiveFOV()3476 QVariantMap Align::getEffectiveFOV()
3477 {
3478 KStarsData::Instance()->userdb()->GetAllEffectiveFOVs(effectiveFOVs);
3479
3480 fov_x = fov_y = 0;
3481
3482 for (auto &map : effectiveFOVs)
3483 {
3484 if (map["Profile"].toString() == m_ActiveProfile->name)
3485 {
3486 if (map["Width"].toInt() == ccd_width &&
3487 map["Height"].toInt() == ccd_height &&
3488 map["PixelW"].toDouble() == ccd_hor_pixel &&
3489 map["PixelH"].toDouble() == ccd_ver_pixel &&
3490 map["FocalLength"].toDouble() == focal_length)
3491 {
3492 fov_x = map["FovW"].toDouble();
3493 fov_y = map["FovH"].toDouble();
3494 return map;
3495 }
3496 }
3497 }
3498
3499 return QVariantMap();
3500 }
3501
saveNewEffectiveFOV(double newFOVW,double newFOVH)3502 void Align::saveNewEffectiveFOV(double newFOVW, double newFOVH)
3503 {
3504 if (newFOVW < 0 || newFOVH < 0 || (newFOVW == fov_x && newFOVH == fov_y))
3505 return;
3506
3507 QVariantMap effectiveMap = getEffectiveFOV();
3508
3509 // If ID exists, delete it first.
3510 if (effectiveMap.isEmpty() == false)
3511 KStarsData::Instance()->userdb()->DeleteEffectiveFOV(effectiveMap["id"].toString());
3512
3513 // If FOV is 0x0, then we just remove existing effective FOV
3514 if (newFOVW == 0.0 && newFOVH == 0.0)
3515 {
3516 calculateFOV();
3517 return;
3518 }
3519
3520 effectiveMap["Profile"] = m_ActiveProfile->name;
3521 effectiveMap["Width"] = ccd_width;
3522 effectiveMap["Height"] = ccd_height;
3523 effectiveMap["PixelW"] = ccd_hor_pixel;
3524 effectiveMap["PixelH"] = ccd_ver_pixel;
3525 effectiveMap["FocalLength"] = focal_length;
3526 effectiveMap["FovW"] = newFOVW;
3527 effectiveMap["FovH"] = newFOVH;
3528
3529 KStarsData::Instance()->userdb()->AddEffectiveFOV(effectiveMap);
3530
3531 calculateFOV();
3532
3533 }
3534
zoomAlignView()3535 void Align::zoomAlignView()
3536 {
3537 alignView->ZoomDefault();
3538
3539 emit newFrame(alignView);
3540 }
3541
setAlignZoom(double scale)3542 void Align::setAlignZoom(double scale)
3543 {
3544 if (scale > 1)
3545 alignView->ZoomIn();
3546 else if (scale < 1)
3547 alignView->ZoomOut();
3548
3549 emit newFrame(alignView);
3550 }
3551
getSettings() const3552 QJsonObject Align::getSettings() const
3553 {
3554 QJsonObject settings;
3555
3556 double gain = -1;
3557 if (GainSpinSpecialValue > INVALID_VALUE && GainSpin->value() > GainSpinSpecialValue)
3558 gain = GainSpin->value();
3559 else if (currentCCD && currentCCD->hasGain())
3560 currentCCD->getGain(&gain);
3561
3562 settings.insert("camera", CCDCaptureCombo->currentText());
3563 settings.insert("fw", FilterDevicesCombo->currentText());
3564 settings.insert("filter", FilterPosCombo->currentText());
3565 settings.insert("exp", exposureIN->value());
3566 settings.insert("bin", qMax(1, binningCombo->currentIndex() + 1));
3567 settings.insert("solverAction", gotoModeButtonGroup->checkedId());
3568 settings.insert("scopeType", FOVScopeCombo->currentIndex());
3569 settings.insert("gain", gain);
3570 settings.insert("iso", ISOCombo->currentIndex());
3571 settings.insert("accuracy", accuracySpin->value());
3572 settings.insert("settle", delaySpin->value());
3573 settings.insert("dark", alignDarkFrameCheck->isChecked());
3574
3575 return settings;
3576 }
3577
setSettings(const QJsonObject & settings)3578 void Align::setSettings(const QJsonObject &settings)
3579 {
3580 static bool init = false;
3581
3582 auto syncControl = [settings](const QString & key, QWidget * widget)
3583 {
3584 QSpinBox *pSB = nullptr;
3585 QDoubleSpinBox *pDSB = nullptr;
3586 QCheckBox *pCB = nullptr;
3587 QComboBox *pComboBox = nullptr;
3588
3589 if ((pSB = qobject_cast<QSpinBox *>(widget)))
3590 {
3591 const int value = settings[key].toInt(pSB->value());
3592 if (value != pSB->value())
3593 {
3594 pSB->setValue(value);
3595 return true;
3596 }
3597 }
3598 else if ((pDSB = qobject_cast<QDoubleSpinBox *>(widget)))
3599 {
3600 const double value = settings[key].toDouble(pDSB->value());
3601 if (value != pDSB->value())
3602 {
3603 pDSB->setValue(value);
3604 return true;
3605 }
3606 }
3607 else if ((pCB = qobject_cast<QCheckBox *>(widget)))
3608 {
3609 const bool value = settings[key].toBool(pCB->isChecked());
3610 if (value != pCB->isChecked())
3611 {
3612 pCB->setChecked(value);
3613 return true;
3614 }
3615 }
3616 // ONLY FOR STRINGS, not INDEX
3617 else if ((pComboBox = qobject_cast<QComboBox *>(widget)))
3618 {
3619 const QString value = settings[key].toString(pComboBox->currentText());
3620 if (value != pComboBox->currentText())
3621 {
3622 pComboBox->setCurrentText(value);
3623 return true;
3624 }
3625 }
3626
3627 return false;
3628 };
3629
3630 // Camera. If camera changed, check CCD.
3631 if (syncControl("camera", CCDCaptureCombo) || init == false)
3632 checkCCD();
3633 // Filter Wheel
3634 if (syncControl("fw", FilterDevicesCombo) || init == false)
3635 checkFilter();
3636 // Filter
3637 syncControl("filter", FilterPosCombo);
3638 Options::setLockAlignFilterIndex(FilterPosCombo->currentIndex());
3639 // Exposure
3640 syncControl("exp", exposureIN);
3641 // Binning
3642 const int bin = settings["bin"].toInt(binningCombo->currentIndex() + 1) - 1;
3643 if (bin != binningCombo->currentIndex())
3644 binningCombo->setCurrentIndex(bin);
3645 // Profiles
3646 const QString profileName = settings["sep"].toString();
3647 QStringList profiles = getStellarSolverProfiles();
3648 int profileIndex = getStellarSolverProfiles().indexOf(profileName);
3649 if (profileIndex >= 0 && profileIndex != static_cast<int>(Options::solveOptionsProfile()))
3650 Options::setSolveOptionsProfile(profileIndex);
3651
3652 int solverAction = settings["solverAction"].toInt(gotoModeButtonGroup->checkedId());
3653 if (solverAction != gotoModeButtonGroup->checkedId())
3654 {
3655 gotoModeButtonGroup->button(solverAction)->setChecked(true);
3656 m_CurrentGotoMode = static_cast<GotoMode>(solverAction);
3657 }
3658
3659 FOVScopeCombo->setCurrentIndex(settings["scopeType"].toInt(0));
3660
3661 // Gain
3662 if (currentCCD->hasGain())
3663 {
3664 syncControl("gain", GainSpin);
3665 }
3666 // ISO
3667 if (ISOCombo->count() > 1)
3668 {
3669 const int iso = settings["iso"].toInt(ISOCombo->currentIndex());
3670 if (iso != ISOCombo->currentIndex())
3671 ISOCombo->setCurrentIndex(iso);
3672 }
3673 // Dark
3674 syncControl("dark", alignDarkFrameCheck);
3675 // Accuracy
3676 syncControl("accuracy", accuracySpin);
3677 // Settle
3678 syncControl("settle", delaySpin);
3679
3680 init = true;
3681 }
3682
syncSettings()3683 void Align::syncSettings()
3684 {
3685 emit settingsUpdated(getSettings());
3686 }
3687
3688
syncFOV()3689 void Align::syncFOV()
3690 {
3691 QString newFOV = FOVOut->text();
3692 QRegularExpression re("(\\d+\\.*\\d*)\\D*x\\D*(\\d+\\.*\\d*)");
3693 QRegularExpressionMatch match = re.match(newFOV);
3694 if (match.hasMatch())
3695 {
3696 double newFOVW = match.captured(1).toDouble();
3697 double newFOVH = match.captured(2).toDouble();
3698
3699 //if (newFOVW > 0 && newFOVH > 0)
3700 saveNewEffectiveFOV(newFOVW, newFOVH);
3701
3702 FOVOut->setStyleSheet(QString());
3703 }
3704 else
3705 {
3706 KSNotification::error(i18n("Invalid FOV."));
3707 FOVOut->setStyleSheet("background-color:red");
3708 }
3709 }
3710
3711 // m_wasSlewStarted can't be false for more than 10s after a slew starts.
didSlewStart()3712 bool Align::didSlewStart()
3713 {
3714 if (m_wasSlewStarted)
3715 return true;
3716 if (slewStartTimer.isValid() && slewStartTimer.elapsed() > MAX_WAIT_FOR_SLEW_START_MSEC)
3717 {
3718 qCDebug(KSTARS_EKOS_ALIGN) << "Slew timed out...waited > 10s, it must have started already.";
3719 return true;
3720 }
3721 return false;
3722 }
3723
setTargetCoords(double ra0,double de0)3724 void Align::setTargetCoords(double ra0, double de0)
3725 {
3726 double maxrad = 1000.0 / Options::zoomFactor();
3727 SkyPoint target;
3728 target.setRA0(ra0);
3729 target.setDec0(de0);
3730 target.updateCoordsNow(KStarsData::Instance()->updateNum());
3731 setTarget(*KStarsData::Instance()->skyComposite()->objectNearest(&target, maxrad), target);
3732 }
3733
setTarget(const SkyObject & targetObject,const SkyPoint & targetCoord)3734 void Align::setTarget(const SkyObject &targetObject, const SkyPoint &targetCoord)
3735 {
3736 Q_UNUSED(targetObject);
3737 m_targetCoord = targetCoord;
3738 m_targetCoordValid = true;
3739 qCInfo(KSTARS_EKOS_ALIGN) << "Target updated to JNow RA:" << m_targetCoord.ra().toHMSString()
3740 << "DE:" << m_targetCoord.dec().toDMSString();
3741 }
3742
getTargetCoords()3743 QList<double> Align::getTargetCoords()
3744 {
3745 QList<double> coord;
3746 // if a target has been set, take the target coordinates. Otherwise, take the coordinates the scope is pointing to currently
3747 if (m_targetCoordValid)
3748 coord << m_targetCoord.ra0().Hours() << m_targetCoord.dec0().Degrees();
3749 else
3750 coord << telescopeCoord.ra0().Hours() << telescopeCoord.dec0().Degrees();
3751
3752 return coord;
3753 }
3754
setTargetRotation(double rotation)3755 void Align::setTargetRotation(double rotation)
3756 {
3757 loadSlewTargetPA = rotation;
3758
3759 qCDebug(KSTARS_EKOS_ALIGN) << "Target Rotation updated to: " << loadSlewTargetPA;
3760 }
3761
calculateAlignTargetDiff()3762 void Align::calculateAlignTargetDiff()
3763 {
3764 if (matchPAHStage(PAA::PAH_FIRST_CAPTURE) || matchPAHStage(PAA::PAH_SECOND_CAPTURE)
3765 || matchPAHStage(PAA::PAH_THIRD_CAPTURE))
3766 return;
3767 m_TargetDiffRA = (alignCoord.ra().deltaAngle(m_targetCoord.ra())).Degrees() * 3600;
3768 m_TargetDiffDE = (alignCoord.dec().deltaAngle(m_targetCoord.dec())).Degrees() * 3600;
3769
3770 dms RADiff(fabs(m_TargetDiffRA) / 3600.0), DEDiff(m_TargetDiffDE / 3600.0);
3771 QString dRAText = QString("%1%2").arg((m_TargetDiffRA > 0 ? "+" : "-"), RADiff.toHMSString());
3772 QString dDEText = DEDiff.toDMSString(true);
3773
3774 m_TargetDiffTotal = sqrt(m_TargetDiffRA * m_TargetDiffRA + m_TargetDiffDE * m_TargetDiffDE);
3775
3776 errOut->setText(QString("%1 arcsec. RA:%2 DE:%3").arg(
3777 QString::number(m_TargetDiffTotal, 'f', 0),
3778 QString::number(m_TargetDiffRA, 'f', 0),
3779 QString::number(m_TargetDiffDE, 'f', 0)));
3780 if (m_TargetDiffTotal <= static_cast<double>(accuracySpin->value()))
3781 errOut->setStyleSheet("color:green");
3782 else if (m_TargetDiffTotal < 1.5 * accuracySpin->value())
3783 errOut->setStyleSheet("color:yellow");
3784 else
3785 errOut->setStyleSheet("color:red");
3786
3787 //This block of code will write the result into the solution table and plot it on the graph.
3788 int currentRow = solutionTable->rowCount() - 1;
3789 QTableWidgetItem *dRAReport = new QTableWidgetItem();
3790 if (dRAReport)
3791 {
3792 dRAReport->setText(QString::number(m_TargetDiffRA, 'f', 3) + "\"");
3793 dRAReport->setTextAlignment(Qt::AlignHCenter);
3794 dRAReport->setFlags(Qt::ItemIsSelectable);
3795 solutionTable->setItem(currentRow, 4, dRAReport);
3796 }
3797
3798 QTableWidgetItem *dDECReport = new QTableWidgetItem();
3799 if (dDECReport)
3800 {
3801 dDECReport->setText(QString::number(m_TargetDiffDE, 'f', 3) + "\"");
3802 dDECReport->setTextAlignment(Qt::AlignHCenter);
3803 dDECReport->setFlags(Qt::ItemIsSelectable);
3804 solutionTable->setItem(currentRow, 5, dDECReport);
3805 }
3806
3807 double raPlot = m_TargetDiffRA;
3808 double decPlot = m_TargetDiffDE;
3809 alignPlot->graph(0)->addData(raPlot, decPlot);
3810
3811 QCPItemText *textLabel = new QCPItemText(alignPlot);
3812 textLabel->setPositionAlignment(Qt::AlignVCenter | Qt::AlignHCenter);
3813
3814 textLabel->position->setType(QCPItemPosition::ptPlotCoords);
3815 textLabel->position->setCoords(raPlot, decPlot);
3816 textLabel->setColor(Qt::red);
3817 textLabel->setPadding(QMargins(0, 0, 0, 0));
3818 textLabel->setBrush(Qt::white);
3819 textLabel->setPen(Qt::NoPen);
3820 textLabel->setText(' ' + QString::number(solutionTable->rowCount()) + ' ');
3821 textLabel->setFont(QFont(font().family(), 8));
3822
3823 if (!alignPlot->xAxis->range().contains(m_TargetDiffRA))
3824 {
3825 alignPlot->graph(0)->rescaleKeyAxis(true);
3826 alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0);
3827 }
3828 if (!alignPlot->yAxis->range().contains(m_TargetDiffDE))
3829 {
3830 alignPlot->graph(0)->rescaleValueAxis(true);
3831 alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);
3832 }
3833 alignPlot->replot();
3834 }
3835
updateTargetCoords()3836 void Align::updateTargetCoords()
3837 {
3838 // if target has already been set, we're done
3839 if (m_targetCoordValid)
3840 return;
3841 else
3842 {
3843 // otherwise, the best guess is the position the mount reports
3844 m_targetCoord = telescopeCoord;
3845 m_targetCoordValid = true;
3846 qCDebug(KSTARS_EKOS_ALIGN) << "No target set, using mount Coordinates JNow RA:" << telescopeCoord.ra().toHMSString()
3847 << "DE:" << telescopeCoord.dec().toDMSString();
3848 }
3849 }
3850
getStellarSolverProfiles()3851 QStringList Align::getStellarSolverProfiles()
3852 {
3853 QStringList profiles;
3854 for (auto param : m_StellarSolverProfiles)
3855 profiles << param.listName;
3856
3857 return profiles;
3858 }
3859
exportSolutionPoints()3860 void Align::exportSolutionPoints()
3861 {
3862 if (solutionTable->rowCount() == 0)
3863 return;
3864
3865 QUrl exportFile = QFileDialog::getSaveFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Export Solution Points"),
3866 alignURLPath,
3867 "CSV File (*.csv)");
3868 if (exportFile.isEmpty()) // if user presses cancel
3869 return;
3870 if (exportFile.toLocalFile().endsWith(QLatin1String(".csv")) == false)
3871 exportFile.setPath(exportFile.toLocalFile() + ".csv");
3872
3873 QString path = exportFile.toLocalFile();
3874
3875 if (QFile::exists(path))
3876 {
3877 int r = KMessageBox::warningContinueCancel(nullptr,
3878 i18n("A file named \"%1\" already exists. "
3879 "Overwrite it?",
3880 exportFile.fileName()),
3881 i18n("Overwrite File?"), KStandardGuiItem::overwrite());
3882 if (r == KMessageBox::Cancel)
3883 return;
3884 }
3885
3886 if (!exportFile.isValid())
3887 {
3888 QString message = i18n("Invalid URL: %1", exportFile.url());
3889 KSNotification::sorry(message, i18n("Invalid URL"));
3890 return;
3891 }
3892
3893 QFile file;
3894 file.setFileName(path);
3895 if (!file.open(QIODevice::WriteOnly))
3896 {
3897 QString message = i18n("Unable to write to file %1", path);
3898 KSNotification::sorry(message, i18n("Could Not Open File"));
3899 return;
3900 }
3901
3902 QTextStream outstream(&file);
3903
3904 QString epoch = QString::number(KStarsDateTime::currentDateTime().epoch());
3905
3906 outstream << "RA (J" << epoch << "),DE (J" << epoch
3907 << "),RA (degrees),DE (degrees),Name,RA Error (arcsec),DE Error (arcsec)" << endl;
3908
3909 for (int i = 0; i < solutionTable->rowCount(); i++)
3910 {
3911 QTableWidgetItem *raCell = solutionTable->item(i, 0);
3912 QTableWidgetItem *deCell = solutionTable->item(i, 1);
3913 QTableWidgetItem *objNameCell = solutionTable->item(i, 2);
3914 QTableWidgetItem *raErrorCell = solutionTable->item(i, 4);
3915 QTableWidgetItem *deErrorCell = solutionTable->item(i, 5);
3916
3917 if (!raCell || !deCell || !objNameCell || !raErrorCell || !deErrorCell)
3918 {
3919 KSNotification::sorry(i18n("Error in table structure."));
3920 return;
3921 }
3922 dms raDMS = dms::fromString(raCell->text(), false);
3923 dms deDMS = dms::fromString(deCell->text(), true);
3924 outstream << raDMS.toHMSString() << ',' << deDMS.toDMSString() << ',' << raDMS.Degrees() << ','
3925 << deDMS.Degrees() << ',' << objNameCell->text() << ',' << raErrorCell->text().remove('\"') << ','
3926 << deErrorCell->text().remove('\"') << endl;
3927 }
3928 emit newLog(i18n("Solution Points Saved as: %1", path));
3929 file.close();
3930 }
3931
initPolarAlignmentAssistant()3932 void Align::initPolarAlignmentAssistant()
3933 {
3934 // Create PAA instance
3935 m_PolarAlignmentAssistant = new PolarAlignmentAssistant(this, alignView);
3936 connect(m_PolarAlignmentAssistant, &Ekos::PAA::captureAndSolve, this, &Ekos::Align::captureAndSolve);
3937 connect(m_PolarAlignmentAssistant, &Ekos::PAA::settleStarted, [this](double duration)
3938 {
3939 m_CaptureTimer.start(duration);
3940 });
3941 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newAlignTableResult, this, &Ekos::Align::setAlignTableResult);
3942 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newFrame, this, &Ekos::Align::newFrame);
3943 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newPAHStage, this, &Ekos::Align::processPAHStage);
3944 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newLog, this, &Ekos::Align::appendLogText);
3945
3946 tabWidget->addTab(m_PolarAlignmentAssistant, i18n("Polar Alignment"));
3947 }
3948
initManualRotator()3949 void Align::initManualRotator()
3950 {
3951 if (m_ManualRotator)
3952 return;
3953
3954 m_ManualRotator = new ManualRotator(this);
3955 connect(m_ManualRotator, &Ekos::ManualRotator::captureAndSolve, this, &Ekos::Align::captureAndSolve);
3956 }
3957
initDarkProcessor()3958 void Align::initDarkProcessor()
3959 {
3960 if (m_DarkProcessor)
3961 return;
3962
3963 m_DarkProcessor = new DarkProcessor(this);
3964 connect(m_DarkProcessor, &DarkProcessor::newLog, this, &Ekos::Align::appendLogText);
3965 connect(m_DarkProcessor, &DarkProcessor::darkFrameCompleted, this, [this](bool completed)
3966 {
3967 alignDarkFrameCheck->setChecked(completed);
3968 if (completed)
3969 {
3970 alignView->rescale(ZOOM_KEEP_LEVEL);
3971 alignView->updateFrame();
3972 }
3973
3974 setCaptureComplete();
3975 });
3976 }
3977
processPAHStage(int stage)3978 void Align::processPAHStage(int stage)
3979 {
3980 switch (stage)
3981 {
3982 case PAA::PAH_POST_REFRESH:
3983 {
3984 Options::setAstrometrySolverWCS(rememberSolverWCS);
3985 Options::setAutoWCS(rememberAutoWCS);
3986 stop(ALIGN_IDLE);
3987 }
3988 break;
3989 case PAA::PAH_PRE_REFRESH:
3990 // If user stops here, we restore the settings, if not we
3991 // disable again in the refresh process
3992 // and restore when refresh is complete
3993 Options::setAstrometrySolverWCS(rememberSolverWCS);
3994 Options::setAutoWCS(rememberAutoWCS);
3995 break;
3996 case PAA::PAH_FIRST_CAPTURE:
3997 nothingR->setChecked(true);
3998 m_CurrentGotoMode = GOTO_NOTHING;
3999 loadSlewB->setEnabled(false);
4000
4001 rememberSolverWCS = Options::astrometrySolverWCS();
4002 rememberAutoWCS = Options::autoWCS();
4003
4004 Options::setAutoWCS(false);
4005 Options::setAstrometrySolverWCS(true);
4006 break;
4007 case PAA::PAH_SECOND_CAPTURE:
4008 case PAA::PAH_THIRD_CAPTURE:
4009 if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY)
4010 emit newLog(i18n("Settling..."));
4011 m_CaptureTimer.start(delaySpin->value());
4012 break;
4013
4014 default:
4015 break;
4016 }
4017 }
4018
matchPAHStage(uint32_t stage)4019 bool Align::matchPAHStage(uint32_t stage)
4020 {
4021 return m_PolarAlignmentAssistant && m_PolarAlignmentAssistant->getPAHStage() == stage;
4022 }
4023 }
4024