Kstars

align.cpp
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 "manualrotator.h"
27
28// FITS
29#include "fitsviewer/fitsdata.h"
30#include "fitsviewer/fitsviewer.h"
31
32// Auxiliary
33#include "auxiliary/QProgressIndicator.h"
34#include "auxiliary/ksmessagebox.h"
35#include "ekos/auxiliary/darkprocessor.h"
36#include "ekos/auxiliary/filtermanager.h"
37#include "ekos/auxiliary/stellarsolverprofileeditor.h"
38#include "ekos/auxiliary/profilesettings.h"
39#include "ekos/auxiliary/opticaltrainmanager.h"
40#include "ekos/auxiliary/opticaltrainsettings.h"
41#include "ksnotification.h"
42#include "kspaths.h"
43#include "ksuserdb.h"
44#include "fov.h"
45#include "kstars.h"
46#include "kstarsdata.h"
47#include "skymapcomposite.h"
48#include "ekos/auxiliary/solverutils.h"
49#include "ekos/auxiliary/rotatorutils.h"
50
51// INDI
52#include "ekos/manager.h"
53#include "indi/indidome.h"
54#include "indi/clientmanager.h"
55#include "indi/driverinfo.h"
56#include "indi/indifilterwheel.h"
57#include "indi/indirotator.h"
58#include "profileinfo.h"
59
60// System Includes
61#include <KActionCollection>
62#include <basedevice.h>
63#include <indicom.h>
64#include <memory>
65
66// Qt version calming
67#include <qtendl.h>
68
69#define MAXIMUM_SOLVER_ITERATIONS 10
70#define CAPTURE_RETRY_DELAY 10000
71#define CAPTURE_ROTATOR_DELAY 5000 // After 5 seconds estimated value should be not bad
72#define PAH_CUTOFF_FOV 10 // Minimum FOV width in arcminutes for PAH to work
73#define CHECK_PAH(x) \
74 m_PolarAlignmentAssistant && m_PolarAlignmentAssistant->x
75#define RUN_PAH(x) \
76 if (m_PolarAlignmentAssistant) m_PolarAlignmentAssistant->x
77
78namespace Ekos
79{
80
82
83bool isEqual(double a, double b)
84{
85 return std::abs(a - b) < 0.01;
86}
87
88Align::Align(const QSharedPointer<ProfileInfo> &activeProfile) : m_ActiveProfile(activeProfile)
89{
90 setupUi(this);
91
92 qRegisterMetaType<Ekos::AlignState>("Ekos::AlignState");
93 qDBusRegisterMetaType<Ekos::AlignState>();
94
95 new AlignAdaptor(this);
96 QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Align", this);
97
98 dirPath = QDir::homePath();
99
100 KStarsData::Instance()->clearTransientFOVs();
101
102 solverFOV.reset(new FOV());
103 solverFOV->setName(i18n("Solver FOV"));
104 solverFOV->setObjectName("solver_fov");
105 solverFOV->setLockCelestialPole(true);
106 solverFOV->setColor(KStars::Instance()->data()->colorScheme()->colorNamed("SolverFOVColor").name());
107 solverFOV->setProperty("visible", false);
108 KStarsData::Instance()->addTransientFOV(solverFOV);
109
110 sensorFOV.reset(new FOV());
111 sensorFOV->setObjectName("sensor_fov");
112 sensorFOV->setLockCelestialPole(true);
113 sensorFOV->setProperty("visible", Options::showSensorFOV());
114 KStarsData::Instance()->addTransientFOV(sensorFOV);
115
116 QAction *a = KStars::Instance()->actionCollection()->action("show_sensor_fov");
117 if (a)
118 a->setEnabled(true);
119
120 showFITSViewerB->setIcon(
121 QIcon::fromTheme("kstars_fitsviewer"));
122 showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
123 connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Align::showFITSViewer);
124
125 toggleFullScreenB->setIcon(
126 QIcon::fromTheme("view-fullscreen"));
127 toggleFullScreenB->setShortcut(Qt::Key_F4);
128 toggleFullScreenB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
129 connect(toggleFullScreenB, &QPushButton::clicked, this, &Ekos::Align::toggleAlignWidgetFullScreen);
130
131 // rotatorB->setIcon(QIcon::fromTheme("kstars_solarsystem"));
132 rotatorB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
133
134 m_AlignView.reset(new AlignView(alignWidget, FITS_ALIGN));
135 m_AlignView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
136 m_AlignView->setBaseSize(alignWidget->size());
137 m_AlignView->createFloatingToolBar();
138 QVBoxLayout *vlayout = new QVBoxLayout();
139
140 vlayout->addWidget(m_AlignView.get());
141 alignWidget->setLayout(vlayout);
142
143 connect(solveB, &QPushButton::clicked, this, [this]()
144 {
145 captureAndSolve(true);
146 });
147
148 connect(stopB, &QPushButton::clicked, this, &Ekos::Align::abort);
149
150 // Effective FOV Edit
151 connect(FOVOut, &QLineEdit::editingFinished, this, &Align::syncFOV);
152
153 connect(loadSlewB, &QPushButton::clicked, this, [this]()
154 {
155 loadAndSlew();
156 });
157
158 gotoModeButtonGroup->setId(syncR, GOTO_SYNC);
159 gotoModeButtonGroup->setId(slewR, GOTO_SLEW);
160 gotoModeButtonGroup->setId(nothingR, GOTO_NOTHING);
161
162 // Setup Debounce timer to limit over-activation of settings changes
163 m_DebounceTimer.setInterval(500);
164 m_DebounceTimer.setSingleShot(true);
165 connect(&m_DebounceTimer, &QTimer::timeout, this, &Align::settleSettings);
166
167 m_CurrentGotoMode = GOTO_SLEW;
168 gotoModeButtonGroup->button(m_CurrentGotoMode)->setChecked(true);
169
170#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
171 connect(gotoModeButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), this,
172 [ = ](int id, bool toggled)
173#else
174 connect(gotoModeButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled), this,
175 [ = ](int id, bool toggled)
176#endif
177 {
178 if (toggled)
179 this->m_CurrentGotoMode = static_cast<GotoMode>(id);
180 });
181
182 m_CaptureTimer.setSingleShot(true);
183 m_CaptureTimer.setInterval(CAPTURE_RETRY_DELAY);
184 connect(&m_CaptureTimer, &QTimer::timeout, this, &Align::processCaptureTimeout);
185 m_AlignTimer.setSingleShot(true);
186 m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000);
187 connect(&m_AlignTimer, &QTimer::timeout, this, &Ekos::Align::checkAlignmentTimeout);
188
189 pi.reset(new QProgressIndicator(this));
190 stopLayout->addWidget(pi.get());
191
192 rememberSolverWCS = Options::astrometrySolverWCS();
193 rememberAutoWCS = Options::autoWCS();
194
195 solverModeButtonGroup->setId(localSolverR, SOLVER_LOCAL);
196 solverModeButtonGroup->setId(remoteSolverR, SOLVER_REMOTE);
197
198 setSolverMode(Options::solverMode());
199
200#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
201 connect(solverModeButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), this,
202 [ = ](int id, bool toggled)
203#else
204 connect(solverModeButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled), this,
205 [ = ](int id, bool toggled)
206#endif
207 {
208 if (toggled)
209 setSolverMode(id);
210 });
211
212 connect(alignAccuracyThreshold, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this]()
213 {
214 buildTarget();
215 });
216
217 connect(alignBinning, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
219
220 connect(this, &Align::newStatus, this, [this](AlignState state)
221 {
222 opticalTrainCombo->setEnabled(state < ALIGN_PROGRESS);
223 trainB->setEnabled(state < ALIGN_PROGRESS);
224 });
225
226 connect(alignUseCurrentFilter, &QCheckBox::toggled, this, &Align::checkFilter);
227
228 //Note: This is to prevent a button from being called the default button
229 //and then executing when the user hits the enter key such as when on a Text Box
230 QList<QPushButton *> qButtons = findChildren<QPushButton *>();
231 for (auto &button : qButtons)
232 button->setAutoDefault(false);
233
234 savedOptionsProfiles = QDir(KSPaths::writableLocation(
235 QStandardPaths::AppLocalDataLocation)).filePath("SavedAlignProfiles.ini");
236 if(QFile(savedOptionsProfiles).exists())
237 m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
238 else
239 m_StellarSolverProfiles = getDefaultAlignOptionsProfiles();
240
241 m_StellarSolver.reset(new StellarSolver());
242 connect(m_StellarSolver.get(), &StellarSolver::logOutput, this, &Align::appendLogText);
243
244 setupPolarAlignmentAssistant();
245 setupManualRotator();
246 setuptDarkProcessor();
247 setupPlot();
248 setupSolutionTable();
249 setupOptions();
250
251 // Load all settings
252 loadGlobalSettings();
253 connectSettings();
254
255 setupOpticalTrainManager();
256}
257
258Align::~Align()
259{
260 if (m_StellarSolver.get() != nullptr)
261 disconnect(m_StellarSolver.get(), &StellarSolver::logOutput, this, &Align::appendLogText);
262
263 if (alignWidget->parent() == nullptr)
264 toggleAlignWidgetFullScreen();
265
266 // Remove temporary FITS files left before by the solver
267 QDir dir(QDir::tempPath());
268 dir.setNameFilters(QStringList() << "fits*"
269 << "tmp.*");
270 dir.setFilter(QDir::Files);
271 for (auto &dirFile : dir.entryList())
272 dir.remove(dirFile);
273}
274void Align::selectSolutionTableRow(int row, int column)
275{
276 Q_UNUSED(column)
277
278 solutionTable->selectRow(row);
279 for (int i = 0; i < alignPlot->itemCount(); i++)
280 {
281 QCPAbstractItem *abstractItem = alignPlot->item(i);
282 if (abstractItem)
283 {
284 QCPItemText *item = qobject_cast<QCPItemText *>(abstractItem);
285 if (item)
286 {
287 if (i == row)
288 {
289 item->setColor(Qt::black);
290 item->setBrush(Qt::yellow);
291 }
292 else
293 {
294 item->setColor(Qt::red);
295 item->setBrush(Qt::white);
296 }
297 }
298 }
299 }
300 alignPlot->replot();
301}
302
303void Align::handleHorizontalPlotSizeChange()
304{
305 alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);
306 alignPlot->replot();
307}
308
309void Align::handleVerticalPlotSizeChange()
310{
311 alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0);
312 alignPlot->replot();
313}
314
315void Align::resizeEvent(QResizeEvent *event)
316{
317 if (event->oldSize().width() != -1)
318 {
319 if (event->oldSize().width() != size().width())
320 handleHorizontalPlotSizeChange();
321 else if (event->oldSize().height() != size().height())
322 handleVerticalPlotSizeChange();
323 }
324 else
325 {
326 QTimer::singleShot(10, this, &Ekos::Align::handleHorizontalPlotSizeChange);
327 }
328}
329
330void Align::handlePointTooltip(QMouseEvent *event)
331{
332 QCPAbstractItem *item = alignPlot->itemAt(event->localPos());
333 if (item)
334 {
336 if (label)
337 {
338 QString labelText = label->text();
339 int point = labelText.toInt() - 1;
340
341 if (point < 0)
342 return;
343 QToolTip::showText(event->globalPos(),
344 i18n("<table>"
345 "<tr>"
346 "<th colspan=\"2\">Object %1: %2</th>"
347 "</tr>"
348 "<tr>"
349 "<td>RA:</td><td>%3</td>"
350 "</tr>"
351 "<tr>"
352 "<td>DE:</td><td>%4</td>"
353 "</tr>"
354 "<tr>"
355 "<td>dRA:</td><td>%5</td>"
356 "</tr>"
357 "<tr>"
358 "<td>dDE:</td><td>%6</td>"
359 "</tr>"
360 "</table>",
361 point + 1,
362 solutionTable->item(point, 2)->text(),
363 solutionTable->item(point, 0)->text(),
364 solutionTable->item(point, 1)->text(),
365 solutionTable->item(point, 4)->text(),
366 solutionTable->item(point, 5)->text()),
367 alignPlot, alignPlot->rect());
368 }
369 }
370}
371
372void Align::buildTarget()
373{
374 double accuracyRadius = alignAccuracyThreshold->value();
375 if (centralTarget)
376 {
377 concentricRings->data()->clear();
378 redTarget->data()->clear();
379 yellowTarget->data()->clear();
380 centralTarget->data()->clear();
381 }
382 else
383 {
384 concentricRings = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
385 redTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
386 yellowTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
387 centralTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
388 }
389 const int pointCount = 200;
390 QVector<QCPCurveData> circleRings(
391 pointCount * (5)); //Have to multiply by the number of rings, Rings at : 25%, 50%, 75%, 125%, 175%
392 QVector<QCPCurveData> circleCentral(pointCount);
393 QVector<QCPCurveData> circleYellow(pointCount);
394 QVector<QCPCurveData> circleRed(pointCount);
395
396 int circleRingPt = 0;
397 for (int i = 0; i < pointCount; i++)
398 {
399 double theta = i / static_cast<double>(pointCount) * 2 * M_PI;
400
401 for (double ring = 1; ring < 8; ring++)
402 {
403 if (ring != 4 && ring != 6)
404 {
405 if (i % (9 - static_cast<int>(ring)) == 0) //This causes fewer points to draw on the inner circles.
406 {
407 circleRings[circleRingPt] = QCPCurveData(circleRingPt, accuracyRadius * ring * 0.25 * qCos(theta),
408 accuracyRadius * ring * 0.25 * qSin(theta));
409 circleRingPt++;
410 }
411 }
412 }
413
414 circleCentral[i] = QCPCurveData(i, accuracyRadius * qCos(theta), accuracyRadius * qSin(theta));
415 circleYellow[i] = QCPCurveData(i, accuracyRadius * 1.5 * qCos(theta), accuracyRadius * 1.5 * qSin(theta));
416 circleRed[i] = QCPCurveData(i, accuracyRadius * 2 * qCos(theta), accuracyRadius * 2 * qSin(theta));
417 }
418
419 concentricRings->setLineStyle(QCPCurve::lsNone);
420 concentricRings->setScatterSkip(0);
421 concentricRings->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, QColor(255, 255, 255, 150), 1));
422
423 concentricRings->data()->set(circleRings, true);
424 redTarget->data()->set(circleRed, true);
425 yellowTarget->data()->set(circleYellow, true);
426 centralTarget->data()->set(circleCentral, true);
427
428 concentricRings->setPen(QPen(Qt::white));
429 redTarget->setPen(QPen(Qt::red));
430 yellowTarget->setPen(QPen(Qt::yellow));
431 centralTarget->setPen(QPen(Qt::green));
432
433 concentricRings->setBrush(Qt::NoBrush);
434 redTarget->setBrush(QBrush(QColor(255, 0, 0, 50)));
435 yellowTarget->setBrush(
436 QBrush(QColor(0, 255, 0, 50))); //Note this is actually yellow. It is green on top of red with equal opacity.
437 centralTarget->setBrush(QBrush(QColor(0, 255, 0, 50)));
438
439 if (alignPlot->size().width() > 0)
440 alignPlot->replot();
441}
442
443void Align::slotAutoScaleGraph()
444{
445 double accuracyRadius = alignAccuracyThreshold->value();
446 alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
447 alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
448
449 alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);
450
451 alignPlot->replot();
452}
453
454
455void Align::slotClearAllSolutionPoints()
456{
457 if (solutionTable->rowCount() == 0)
458 return;
459
460 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
461 {
462 //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
463 KSMessageBox::Instance()->disconnect(this);
464
465 solutionTable->setRowCount(0);
466 alignPlot->graph(0)->data()->clear();
467 alignPlot->clearItems();
468 buildTarget();
469
470 slotAutoScaleGraph();
471
472 });
473
474 KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to clear all of the solution points?"),
475 i18n("Clear Solution Points"), 60);
476}
477
478void Align::slotRemoveSolutionPoint()
479{
480 auto abstractItem = alignPlot->item(solutionTable->currentRow());
481 if (abstractItem)
482 {
483 auto item = qobject_cast<QCPItemText *>(abstractItem);
484 if (item)
485 {
486 double point = item->position->key();
487 alignPlot->graph(0)->data()->remove(point);
488 }
489 }
490
491 alignPlot->removeItem(solutionTable->currentRow());
492
493 for (int i = 0; i < alignPlot->itemCount(); i++)
494 {
495 auto oneItem = alignPlot->item(i);
496 if (oneItem)
497 {
498 auto item = qobject_cast<QCPItemText *>(oneItem);
499 if (item)
500 item->setText(QString::number(i + 1));
501 }
502 }
503 solutionTable->removeRow(solutionTable->currentRow());
504 alignPlot->replot();
505}
506
507void Align::slotMountModel()
508{
509 if (!m_MountModel)
510 {
511 m_MountModel = new MountModel(this);
512 connect(m_MountModel, &Ekos::MountModel::newLog, this, &Ekos::Align::appendLogText, Qt::UniqueConnection);
513 connect(m_MountModel, &Ekos::MountModel::aborted, this, [this]()
514 {
515 if (m_Mount && m_Mount->isSlewing())
516 m_Mount->abort();
517 abort();
518 });
519 connect(this, &Ekos::Align::newStatus, m_MountModel, &Ekos::MountModel::setAlignStatus, Qt::UniqueConnection);
520 }
521
522 m_MountModel->show();
523}
524
525
527{
528 return true; //For now
529 // Q_ASSERT_X(parser, __FUNCTION__, "Astrometry parser is not valid.");
530
531 // bool rc = parser->init();
532
533 // if (rc)
534 // {
535 // connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
536 // connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
537 // }
538
539 // return rc;
540}
541
542void Align::checkAlignmentTimeout()
543{
544 if (m_SolveFromFile || ++solverIterations == MAXIMUM_SOLVER_ITERATIONS)
545 abort();
546 else
547 {
548 appendLogText(i18n("Solver timed out."));
549 parser->stopSolver();
550
551 setAlignTableResult(ALIGN_RESULT_FAILED);
552 captureAndSolve(false);
553 }
554 // TODO must also account for loadAndSlew. Retain file name
555}
556
558{
559 if (sender() == nullptr && mode >= 0 && mode <= 1)
560 {
561 solverModeButtonGroup->button(mode)->setChecked(true);
562 }
563
564 Options::setSolverMode(mode);
565
566 if (mode == SOLVER_REMOTE)
567 {
568 if (remoteParser.get() != nullptr && m_RemoteParserDevice != nullptr)
569 {
570 parser = remoteParser.get();
571 (dynamic_cast<RemoteAstrometryParser *>(parser))->setAstrometryDevice(m_RemoteParserDevice);
572 return;
573 }
574
575 remoteParser.reset(new Ekos::RemoteAstrometryParser());
576 parser = remoteParser.get();
577 (dynamic_cast<RemoteAstrometryParser *>(parser))->setAstrometryDevice(m_RemoteParserDevice);
578 if (m_Camera)
579 (dynamic_cast<RemoteAstrometryParser *>(parser))->setCCD(m_Camera->getDeviceName());
580
581 parser->setAlign(this);
582 if (parser->init())
583 {
584 connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
585 connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
586 }
587 else
588 parser->disconnect();
589 }
590}
591
592QString Align::camera()
593{
594 if (m_Camera)
595 return m_Camera->getDeviceName();
596
597 return QString();
598}
599
601{
602 if (!m_Camera)
603 return;
604
605 // Do NOT perform checks if align is in progress as this may result
606 // in signals/slots getting disconnected.
607 switch (state)
608 {
609 // Idle, camera change is OK.
610 case ALIGN_IDLE:
611 case ALIGN_COMPLETE:
612 case ALIGN_FAILED:
613 case ALIGN_ABORTED:
614 case ALIGN_SUCCESSFUL:
615 break;
616
617 // Busy, camera change is not OK.
618 case ALIGN_PROGRESS:
619 case ALIGN_SYNCING:
620 case ALIGN_SLEWING:
621 case ALIGN_SUSPENDED:
622 case ALIGN_ROTATING:
623 return;
624 }
625
626 auto targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
627 if (targetChip == nullptr || (targetChip && targetChip->isCapturing()))
628 return;
629
630 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr)
631 (dynamic_cast<RemoteAstrometryParser *>(remoteParser.get()))->setCCD(m_Camera->getDeviceName());
632
636}
637
639{
640 if (m_Camera && m_Camera == device)
641 {
642 checkCamera();
643 return false;
644 }
645
646 if (m_Camera)
647 m_Camera->disconnect(this);
648
649 m_Camera = device;
650
651 if (m_Camera)
652 {
653 connect(m_Camera, &ISD::ConcreteDevice::Connected, this, [this]()
654 {
655 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
656 controlBox->setEnabled(isConnected);
657 gotoBox->setEnabled(isConnected);
658 plateSolverOptionsGroup->setEnabled(isConnected);
659 tabWidget->setEnabled(isConnected);
660 });
661 connect(m_Camera, &ISD::ConcreteDevice::Disconnected, this, [this]()
662 {
663 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
664 controlBox->setEnabled(isConnected);
665 gotoBox->setEnabled(isConnected);
666 plateSolverOptionsGroup->setEnabled(isConnected);
667 tabWidget->setEnabled(isConnected);
668
669 opticalTrainCombo->setEnabled(true);
670 trainLabel->setEnabled(true);
671 });
672 }
673
674 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
675 controlBox->setEnabled(isConnected);
676 gotoBox->setEnabled(isConnected);
677 plateSolverOptionsGroup->setEnabled(isConnected);
678 tabWidget->setEnabled(isConnected);
679
680 if (!m_Camera)
681 return false;
682
683 checkCamera();
684
685 return true;
686}
687
689{
690 if (m_Mount && m_Mount == device)
691 {
693 return false;
694 }
695
696 if (m_Mount)
697 m_Mount->disconnect(this);
698
699 m_Mount = device;
700
701 if (m_Mount)
702 {
703 connect(m_Mount, &ISD::ConcreteDevice::Connected, this, [this]()
704 {
705 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
706 controlBox->setEnabled(isConnected);
707 gotoBox->setEnabled(isConnected);
708 plateSolverOptionsGroup->setEnabled(isConnected);
709 tabWidget->setEnabled(isConnected);
710 });
711 connect(m_Mount, &ISD::ConcreteDevice::Disconnected, this, [this]()
712 {
713 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
714 controlBox->setEnabled(isConnected);
715 gotoBox->setEnabled(isConnected);
716 plateSolverOptionsGroup->setEnabled(isConnected);
717 tabWidget->setEnabled(isConnected);
718
719 opticalTrainCombo->setEnabled(true);
720 trainLabel->setEnabled(true);
721 });
722 }
723
724 auto isConnected = m_Camera && m_Camera->isConnected() && m_Mount && m_Mount->isConnected();
725 controlBox->setEnabled(isConnected);
726 gotoBox->setEnabled(isConnected);
727 plateSolverOptionsGroup->setEnabled(isConnected);
728 tabWidget->setEnabled(isConnected);
729
730 if (!m_Mount)
731 return false;
732
733 RUN_PAH(setCurrentTelescope(m_Mount));
734
735 connect(m_Mount, &ISD::Mount::propertyUpdated, this, &Ekos::Align::updateProperty, Qt::UniqueConnection);
736 connect(m_Mount, &ISD::Mount::Disconnected, this, [this]()
737 {
738 m_isRateSynced = false;
739 });
740
742 return true;
743}
744
746{
747 if (m_Dome && m_Dome == device)
748 return false;
749
750 if (m_Dome)
751 m_Dome->disconnect(this);
752
753 m_Dome = device;
754
755 if (!m_Dome)
756 return false;
757
758 connect(m_Dome, &ISD::Dome::propertyUpdated, this, &Ekos::Align::updateProperty, Qt::UniqueConnection);
759 return true;
760}
761
762void Align::removeDevice(const QSharedPointer<ISD::GenericDevice> &device)
763{
764 auto name = device->getDeviceName();
765 device->disconnect(this);
766
767 // Check mounts
768 if (m_Mount && m_Mount->getDeviceName() == name)
769 {
770 m_Mount->disconnect(this);
771 m_Mount = nullptr;
772 }
773
774 // Check domes
775 if (m_Dome && m_Dome->getDeviceName() == name)
776 {
777 m_Dome->disconnect(this);
778 m_Dome = nullptr;
779 }
780
781 // Check rotators
782 if (m_Rotator && m_Rotator->getDeviceName() == name)
783 {
784 m_Rotator->disconnect(this);
785 m_Rotator = nullptr;
786 }
787
788 // Check cameras
789 if (m_Camera && m_Camera->getDeviceName() == name)
790 {
791 m_Camera->disconnect(this);
792 m_Camera = nullptr;
793
795 }
796
797 // Check Remote Astrometry
798 if (m_RemoteParserDevice && m_RemoteParserDevice->getDeviceName() == name)
799 {
800 m_RemoteParserDevice.clear();
801 }
802
803 // Check Filter Wheels
804 if (m_FilterWheel && m_FilterWheel->getDeviceName() == name)
805 {
806 m_FilterWheel->disconnect(this);
807 m_FilterWheel = nullptr;
808
810 }
811
812}
813
815{
816 if (m_Mount == nullptr || m_Mount->isConnected() == false)
817 return false;
818
819 if (m_isRateSynced == false)
820 {
821 auto speed = m_Settings["PAHMountSpeed"];
822 auto slewRates = m_Mount->slewRates();
823 if (speed.isValid())
824 {
825 RUN_PAH(syncMountSpeed(speed.toString()));
826 }
827 else if (!slewRates.isEmpty())
828 {
829 RUN_PAH(syncMountSpeed(slewRates.last()));
830 }
831
832 m_isRateSynced = !slewRates.empty();
833 }
834
835 canSync = m_Mount->canSync();
836
837 if (canSync == false && syncR->isEnabled())
838 {
839 slewR->setChecked(true);
840 appendLogText(i18n("Mount does not support syncing."));
841 }
842
843 syncR->setEnabled(canSync);
844
845 if (m_FocalLength == -1 || m_Aperture == -1)
846 return false;
847
848 if (m_CameraPixelWidth != -1 && m_CameraPixelHeight != -1)
849 {
850 calculateFOV();
851 return true;
852 }
853
854 return false;
855}
856
858{
859 if (!m_Camera)
860 return;
861
862 auto targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
863 Q_ASSERT(targetChip);
864
865 // Get Maximum resolution and pixel size
866 uint8_t bit_depth = 8;
867 targetChip->getImageInfo(m_CameraWidth, m_CameraHeight, m_CameraPixelWidth, m_CameraPixelHeight, bit_depth);
868
869 setWCSEnabled(Options::astrometrySolverWCS());
870
871 int binx = 1, biny = 1;
872 alignBinning->setEnabled(targetChip->canBin());
873 if (targetChip->canBin())
874 {
875 alignBinning->blockSignals(true);
876
877 targetChip->getMaxBin(&binx, &biny);
878 alignBinning->clear();
879
880 for (int i = 0; i < binx; i++)
881 alignBinning->addItem(QString("%1x%2").arg(i + 1).arg(i + 1));
882
883 auto binning = m_Settings["alignBinning"];
884 if (binning.isValid())
885 alignBinning->setCurrentText(binning.toString());
886
887 alignBinning->blockSignals(false);
888 }
889
890 // In case ROI is different (smaller) than maximum resolution, let's use that.
891 // N.B. 2022.08.14 JM: We must account for binning since this value is used for FOV calculations.
892 int roiW = 0, roiH = 0;
893 targetChip->getFrameMinMax(nullptr, nullptr, nullptr, nullptr, nullptr, &roiW, nullptr, &roiH);
894 roiW *= binx;
895 roiH *= biny;
896 if ( (roiW > 0 && roiW < m_CameraWidth) || (roiH > 0 && roiH < m_CameraHeight))
897 {
898 m_CameraWidth = roiW;
899 m_CameraHeight = roiH;
900 }
901
902 if (m_CameraPixelWidth > 0 && m_CameraPixelHeight > 0 && m_FocalLength > 0 && m_Aperture > 0)
903 {
904 calculateFOV();
905 }
906}
907
909{
910 if (m_Camera == nullptr)
911 return;
912
913 auto targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
914 if (targetChip == nullptr || (targetChip && targetChip->isCapturing()))
915 return;
916
917 auto isoList = targetChip->getISOList();
918 alignISO->clear();
919
920 if (isoList.isEmpty())
921 {
922 alignISO->setEnabled(false);
923 }
924 else
925 {
926 alignISO->setEnabled(true);
927 alignISO->addItems(isoList);
928 alignISO->setCurrentIndex(targetChip->getISOIndex());
929 }
930
931 // Gain Check
932 if (m_Camera->hasGain())
933 {
934 double min, max, step, value;
935 m_Camera->getGainMinMaxStep(&min, &max, &step);
936
937 // Allow the possibility of no gain value at all.
938 alignGainSpecialValue = min - step;
939 alignGain->setRange(alignGainSpecialValue, max);
940 alignGain->setSpecialValueText(i18n("--"));
941 alignGain->setEnabled(true);
942 alignGain->setSingleStep(step);
943 m_Camera->getGain(&value);
944
945 auto gain = m_Settings["alignGain"];
946 // Set the custom gain if we have one
947 // otherwise it will not have an effect.
948 if (gain.isValid())
949 TargetCustomGainValue = gain.toDouble();
950 if (TargetCustomGainValue > 0)
951 alignGain->setValue(TargetCustomGainValue);
952 else
953 alignGain->setValue(alignGainSpecialValue);
954
955 alignGain->setReadOnly(m_Camera->getGainPermission() == IP_RO);
956
957 connect(alignGain, &QDoubleSpinBox::editingFinished, this, [this]()
958 {
959 if (alignGain->value() > alignGainSpecialValue)
960 TargetCustomGainValue = alignGain->value();
961 });
962 }
963 else
964 alignGain->setEnabled(false);
965}
966
967void Align::getFOVScale(double &fov_w, double &fov_h, double &fov_scale)
968{
969 fov_w = m_FOVWidth;
970 fov_h = m_FOVHeight;
971 fov_scale = m_FOVPixelScale;
972}
973
974QList<double> Align::fov()
975{
976 QList<double> result;
977
978 result << m_FOVWidth << m_FOVHeight << m_FOVPixelScale;
979
980 return result;
981}
982
983QList<double> Align::cameraInfo()
984{
985 QList<double> result;
986
987 result << m_CameraWidth << m_CameraHeight << m_CameraPixelWidth << m_CameraPixelHeight;
988
989 return result;
990}
991
992QList<double> Align::telescopeInfo()
993{
994 QList<double> result;
995
996 result << m_FocalLength << m_Aperture << m_Reducer;
997
998 return result;
999}
1000
1001void Align::getCalculatedFOVScale(double &fov_w, double &fov_h, double &fov_scale)
1002{
1003 // FOV in arcsecs
1004 // DSLR
1005 auto reducedFocalLength = m_Reducer * m_FocalLength;
1006 if (m_FocalRatio > 0)
1007 {
1008 // The forumla is in radians, must convert to degrees.
1009 // Then to arcsecs
1010 fov_w = 3600 * 2 * atan(m_CameraWidth * (m_CameraPixelWidth / 1000.0) / (2 * reducedFocalLength)) / dms::DegToRad;
1011 fov_h = 3600 * 2 * atan(m_CameraHeight * (m_CameraPixelHeight / 1000.0) / (2 * reducedFocalLength)) / dms::DegToRad;
1012 }
1013 // Telescope
1014 else
1015 {
1016 fov_w = 206264.8062470963552 * m_CameraWidth * m_CameraPixelWidth / 1000.0 / reducedFocalLength;
1017 fov_h = 206264.8062470963552 * m_CameraHeight * m_CameraPixelHeight / 1000.0 / reducedFocalLength;
1018 }
1019
1020 // Pix Scale
1021 fov_scale = (fov_w * (alignBinning->currentIndex() + 1)) / m_CameraWidth;
1022
1023 // FOV in arcmins
1024 fov_w /= 60.0;
1025 fov_h /= 60.0;
1026}
1027
1028void Align::calculateEffectiveFocalLength(double newFOVW)
1029{
1030 if (newFOVW < 0 || isEqual(newFOVW, m_FOVWidth))
1031 return;
1032
1033 auto reducedFocalLength = m_Reducer * m_FocalLength;
1034 double new_focal_length = 0;
1035
1036 if (m_FocalRatio > 0)
1037 new_focal_length = ((m_CameraWidth * m_CameraPixelWidth / 1000.0) / tan(newFOVW / 2)) / 2;
1038 else
1039 new_focal_length = ((m_CameraWidth * m_CameraPixelWidth / 1000.0) * 206264.8062470963552) / (newFOVW * 60.0);
1040 double focal_diff = std::fabs(new_focal_length - reducedFocalLength);
1041
1042 if (focal_diff > 1)
1043 {
1044 m_EffectiveFocalLength = new_focal_length / m_Reducer;
1045 appendLogText(i18n("Effective telescope focal length is updated to %1 mm.", m_EffectiveFocalLength));
1046 }
1047}
1048
1049void Align::calculateFOV()
1050{
1051 auto reducedFocalLength = m_Reducer * m_FocalLength;
1052 auto reducecdEffectiveFocalLength = m_Reducer * m_EffectiveFocalLength;
1053 auto reducedFocalRatio = m_Reducer * m_FocalRatio;
1054
1055 if (m_FocalRatio > 0)
1056 {
1057 // The forumla is in radians, must convert to degrees.
1058 // Then to arcsecs
1059 m_FOVWidth = 3600 * 2 * atan(m_CameraWidth * (m_CameraPixelWidth / 1000.0) / (2 * reducedFocalLength)) / dms::DegToRad;
1060 m_FOVHeight = 3600 * 2 * atan(m_CameraHeight * (m_CameraPixelHeight / 1000.0) / (2 * reducedFocalLength)) / dms::DegToRad;
1061 }
1062 // Telescope
1063 else
1064 {
1065 m_FOVWidth = 206264.8062470963552 * m_CameraWidth * m_CameraPixelWidth / 1000.0 / reducedFocalLength;
1066 m_FOVHeight = 206264.8062470963552 * m_CameraHeight * m_CameraPixelHeight / 1000.0 / reducedFocalLength;
1067 }
1068
1069 // Calculate FOV
1070
1071 // Pix Scale
1072 m_FOVPixelScale = (m_FOVWidth * (alignBinning->currentIndex() + 1)) / m_CameraWidth;
1073
1074 // FOV in arcmins
1075 m_FOVWidth /= 60.0;
1076 m_FOVHeight /= 60.0;
1077
1078 double calculated_fov_x = m_FOVWidth;
1079 double calculated_fov_y = m_FOVHeight;
1080
1081 QString calculatedFOV = (QString("%1' x %2'").arg(m_FOVWidth, 0, 'f', 1).arg(m_FOVHeight, 0, 'f', 1));
1082
1083 // Put FOV upper limit as 180 degrees
1084 if (m_FOVWidth < 1 || m_FOVWidth > 60 * 180 || m_FOVHeight < 1 || m_FOVHeight > 60 * 180)
1085 {
1086 appendLogText(
1087 i18n("Warning! The calculated field of view (%1) is out of bounds. Ensure the telescope focal length and camera pixel size are correct.",
1088 calculatedFOV));
1089 return;
1090 }
1091
1092 FocalLengthOut->setText(QString("%1 (%2)").arg(reducedFocalLength, 0, 'f', 1).
1093 arg(m_EffectiveFocalLength > 0 ? reducecdEffectiveFocalLength : reducedFocalLength, 0, 'f', 1));
1094 // DSLR
1095 if (m_FocalRatio > 0)
1096 FocalRatioOut->setText(QString("%1 (%2)").arg(reducedFocalRatio, 0, 'f', 1).
1097 arg(m_EffectiveFocalLength > 0 ? reducecdEffectiveFocalLength / m_Aperture : reducedFocalRatio, 0,
1098 'f', 1));
1099 // Telescope
1100 else if (m_Aperture > 0)
1101 FocalRatioOut->setText(QString("%1 (%2)").arg(reducedFocalLength / m_Aperture, 0, 'f', 1).
1102 arg(m_EffectiveFocalLength > 0 ? reducecdEffectiveFocalLength / m_Aperture : reducedFocalLength / m_Aperture, 0,
1103 'f', 1));
1104 ReducerOut->setText(QString("%1x").arg(m_Reducer, 0, 'f', 2));
1105
1106 if (m_EffectiveFocalLength > 0)
1107 {
1108 double focal_diff = std::fabs(m_EffectiveFocalLength - m_FocalLength);
1109 if (focal_diff < 5)
1110 FocalLengthOut->setStyleSheet("color:green");
1111 else if (focal_diff < 15)
1112 FocalLengthOut->setStyleSheet("color:yellow");
1113 else
1114 FocalLengthOut->setStyleSheet("color:red");
1115 }
1116
1117 // JM 2018-04-20 Above calculations are for RAW FOV. Starting from 2.9.5, we are using EFFECTIVE FOV
1118 // Which is the real FOV as measured from the plate solution. The effective FOVs are stored in the database and are unique
1119 // per profile/pixel_size/focal_length combinations. It defaults to 0' x 0' and gets updated after the first successful solver is complete.
1120 getEffectiveFOV();
1121
1122 if (m_FOVWidth == 0)
1123 {
1124 //FOVOut->setReadOnly(false);
1125 FOVOut->setToolTip(
1126 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>",
1127 calculatedFOV));
1128 m_FOVWidth = calculated_fov_x;
1129 m_FOVHeight = calculated_fov_y;
1130 m_EffectiveFOVPending = true;
1131 }
1132 else
1133 {
1134 m_EffectiveFOVPending = false;
1135 FOVOut->setToolTip(i18n("<p>Effective field of view size in arcminutes.</p>"));
1136 }
1137
1138 solverFOV->setSize(m_FOVWidth, m_FOVHeight);
1139 sensorFOV->setSize(m_FOVWidth, m_FOVHeight);
1140 if (m_Camera)
1141 sensorFOV->setName(m_Camera->getDeviceName());
1142
1143 FOVOut->setText(QString("%1' x %2'").arg(m_FOVWidth, 0, 'f', 1).arg(m_FOVHeight, 0, 'f', 1));
1144
1145 // Enable or Disable PAA depending on current FOV
1146 const bool fovOK = ((m_FOVWidth + m_FOVHeight) / 2.0) > PAH_CUTOFF_FOV;
1147 if (m_PolarAlignmentAssistant != nullptr)
1148 m_PolarAlignmentAssistant->setEnabled(fovOK);
1149
1150 if (opsAstrometry->kcfg_AstrometryUseImageScale->isChecked())
1151 {
1152 int unitType = opsAstrometry->kcfg_AstrometryImageScaleUnits->currentIndex();
1153
1154 // Degrees
1155 if (unitType == 0)
1156 {
1157 double fov_low = qMin(m_FOVWidth / 60, m_FOVHeight / 60);
1158 double fov_high = qMax(m_FOVWidth / 60, m_FOVHeight / 60);
1159 opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low);
1160 opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high);
1161
1162 Options::setAstrometryImageScaleLow(fov_low);
1163 Options::setAstrometryImageScaleHigh(fov_high);
1164 }
1165 // Arcmins
1166 else if (unitType == 1)
1167 {
1168 double fov_low = qMin(m_FOVWidth, m_FOVHeight);
1169 double fov_high = qMax(m_FOVWidth, m_FOVHeight);
1170 opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low);
1171 opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high);
1172
1173 Options::setAstrometryImageScaleLow(fov_low);
1174 Options::setAstrometryImageScaleHigh(fov_high);
1175 }
1176 // Arcsec per pixel
1177 else
1178 {
1179 opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(m_FOVPixelScale * 0.9);
1180 opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(m_FOVPixelScale * 1.1);
1181
1182 // 10% boundary
1183 Options::setAstrometryImageScaleLow(m_FOVPixelScale * 0.9);
1184 Options::setAstrometryImageScaleHigh(m_FOVPixelScale * 1.1);
1185 }
1186 }
1187}
1188
1189QStringList Align::generateRemoteOptions(const QVariantMap &optionsMap)
1190{
1191 QStringList solver_args;
1192
1193 // -O overwrite
1194 // -3 Expected RA
1195 // -4 Expected DEC
1196 // -5 Radius (deg)
1197 // -L lower scale of image in arcminutes
1198 // -H upper scale of image in arcminutes
1199 // -u aw set scale to be in arcminutes
1200 // -W solution.wcs name of solution file
1201 // apog1.jpg name of target file to analyze
1202 //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg
1203
1204 // Start with always-used arguments
1205 solver_args << "-O"
1206 << "--no-plots";
1207
1208 // Now go over boolean options
1209
1210 // noverify
1211 if (optionsMap.contains("noverify"))
1212 solver_args << "--no-verify";
1213
1214 // noresort
1215 if (optionsMap.contains("resort"))
1216 solver_args << "--resort";
1217
1218 // fits2fits
1219 if (optionsMap.contains("nofits2fits"))
1220 solver_args << "--no-fits2fits";
1221
1222 // downsample
1223 if (optionsMap.contains("downsample"))
1224 solver_args << "--downsample" << QString::number(optionsMap.value("downsample", 2).toInt());
1225
1226 // image scale low
1227 if (optionsMap.contains("scaleL"))
1228 solver_args << "-L" << QString::number(optionsMap.value("scaleL").toDouble());
1229
1230 // image scale high
1231 if (optionsMap.contains("scaleH"))
1232 solver_args << "-H" << QString::number(optionsMap.value("scaleH").toDouble());
1233
1234 // image scale units
1235 if (optionsMap.contains("scaleUnits"))
1236 solver_args << "-u" << optionsMap.value("scaleUnits").toString();
1237
1238 // RA
1239 if (optionsMap.contains("ra"))
1240 solver_args << "-3" << QString::number(optionsMap.value("ra").toDouble());
1241
1242 // DE
1243 if (optionsMap.contains("de"))
1244 solver_args << "-4" << QString::number(optionsMap.value("de").toDouble());
1245
1246 // Radius
1247 if (optionsMap.contains("radius"))
1248 solver_args << "-5" << QString::number(optionsMap.value("radius").toDouble());
1249
1250 // Custom
1251 if (optionsMap.contains("custom"))
1252 solver_args << optionsMap.value("custom").toString();
1253
1254 return solver_args;
1255}
1256
1257//This will generate the high and low scale of the imager field size based on the stated units.
1258void Align::generateFOVBounds(double fov_w, QString &fov_low, QString &fov_high, double tolerance)
1259{
1260 // This sets the percentage we search outside the lower and upper boundary limits
1261 // by default, we stretch the limits by 5% (tolerance = 0.05)
1262 double lower_boundary = 1.0 - tolerance;
1263 double upper_boundary = 1.0 + tolerance;
1264
1265 // let's stretch the boundaries by 5%
1266 // fov_lower = ((fov_h < fov_v) ? (fov_h * lower_boundary) : (fov_v * lower_boundary));
1267 // fov_upper = ((fov_h > fov_v) ? (fov_h * upper_boundary) : (fov_v * upper_boundary));
1268
1269 // JM 2019-10-20: The bounds consider image width only, not height.
1270 double fov_lower = fov_w * lower_boundary;
1271 double fov_upper = fov_w * upper_boundary;
1272
1273 //No need to do anything if they are aw, since that is the default
1274 fov_low = QString::number(fov_lower);
1275 fov_high = QString::number(fov_upper);
1276}
1277
1278
1280{
1281 QVariantMap optionsMap;
1282
1283 // -O overwrite
1284 // -3 Expected RA
1285 // -4 Expected DEC
1286 // -5 Radius (deg)
1287 // -L lower scale of image in arcminutes
1288 // -H upper scale of image in arcminutes
1289 // -u aw set scale to be in arcminutes
1290 // -W solution.wcs name of solution file
1291 // apog1.jpg name of target file to analyze
1292 //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg
1293
1294 if (Options::astrometryUseNoVerify())
1295 optionsMap["noverify"] = true;
1296
1297 if (Options::astrometryUseResort())
1298 optionsMap["resort"] = true;
1299
1300 if (Options::astrometryUseNoFITS2FITS())
1301 optionsMap["nofits2fits"] = true;
1302
1303 if (data == nullptr)
1304 {
1305 if (Options::astrometryUseDownsample())
1306 {
1307 if (Options::astrometryAutoDownsample() && m_CameraWidth && m_CameraHeight)
1308 {
1309 uint16_t w = m_CameraWidth / (alignBinning->currentIndex() + 1);
1310 optionsMap["downsample"] = getSolverDownsample(w);
1311 }
1312 else
1313 optionsMap["downsample"] = Options::astrometryDownsample();
1314 }
1315
1316 //Options needed for Sextractor
1317 optionsMap["image_width"] = m_CameraWidth / (alignBinning->currentIndex() + 1);
1318 optionsMap["image_height"] = m_CameraHeight / (alignBinning->currentIndex() + 1);
1319
1320 if (Options::astrometryUseImageScale() && m_FOVWidth > 0 && m_FOVHeight > 0)
1321 {
1322 QString units = "dw";
1323 if (Options::astrometryImageScaleUnits() == 1)
1324 units = "aw";
1325 else if (Options::astrometryImageScaleUnits() == 2)
1326 units = "app";
1327 if (Options::astrometryAutoUpdateImageScale())
1328 {
1329 QString fov_low, fov_high;
1330 double fov_w = m_FOVWidth;
1331 //double fov_h = m_FOVHeight;
1332
1333 if (Options::astrometryImageScaleUnits() == SSolver::DEG_WIDTH)
1334 {
1335 fov_w /= 60;
1336 //fov_h /= 60;
1337 }
1338 else if (Options::astrometryImageScaleUnits() == SSolver::ARCSEC_PER_PIX)
1339 {
1340 fov_w = m_FOVPixelScale;
1341 //fov_h = m_FOVPixelScale;
1342 }
1343
1344 // If effective FOV is pending, let's set a wider tolerance range
1345 generateFOVBounds(fov_w, fov_low, fov_high, m_EffectiveFOVPending ? 0.3 : 0.05);
1346
1347 optionsMap["scaleL"] = fov_low;
1348 optionsMap["scaleH"] = fov_high;
1349 optionsMap["scaleUnits"] = units;
1350 }
1351 else
1352 {
1353 optionsMap["scaleL"] = Options::astrometryImageScaleLow();
1354 optionsMap["scaleH"] = Options::astrometryImageScaleHigh();
1355 optionsMap["scaleUnits"] = units;
1356 }
1357 }
1358
1359 if (Options::astrometryUsePosition() && m_Mount != nullptr)
1360 {
1361 double ra = 0, dec = 0;
1362 m_Mount->getEqCoords(&ra, &dec);
1363
1364 optionsMap["ra"] = ra * 15.0;
1365 optionsMap["de"] = dec;
1366 optionsMap["radius"] = Options::astrometryRadius();
1367 }
1368 }
1369 else
1370 {
1371 // Downsample
1373 data->getRecordValue("NAXIS1", width);
1374 if (width.isValid())
1375 {
1376 optionsMap["downsample"] = getSolverDownsample(width.toInt());
1377 }
1378 else
1379 optionsMap["downsample"] = Options::astrometryDownsample();
1380
1381 // Pixel Scale
1382 QVariant pixscale;
1383 data->getRecordValue("SCALE", pixscale);
1384 if (pixscale.isValid())
1385 {
1386 optionsMap["scaleL"] = 0.8 * pixscale.toDouble();
1387 optionsMap["scaleH"] = 1.2 * pixscale.toDouble();
1388 optionsMap["scaleUnits"] = "app";
1389 }
1390
1391 // Position
1392 QVariant ra, de;
1393 data->getRecordValue("RA", ra);
1394 data->getRecordValue("DEC", de);
1395 if (ra.isValid() && de.isValid())
1396 {
1397 optionsMap["ra"] = ra.toDouble();
1398 optionsMap["de"] = de.toDouble();
1399 optionsMap["radius"] = Options::astrometryRadius();
1400 }
1401 }
1402
1403 if (Options::astrometryCustomOptions().isEmpty() == false)
1404 optionsMap["custom"] = Options::astrometryCustomOptions();
1405
1406 return generateRemoteOptions(optionsMap);
1407}
1408
1409
1410// For option "differential slewing" destination coords have to be initialized, if call comes from "outside".
1411// Initialization is activated through the predefined argument "initialCall = true".
1412bool Align::captureAndSolve(bool initialCall)
1413{
1414 // Set target to current telescope position,if no object is selected yet.
1415 if (m_TargetCoord.ra().degree() < 0) // see default constructor skypoint()
1416 {
1417 m_TargetCoord = m_TelescopeCoord;
1418 appendLogText(i18n("Setting target to RA:%1 DEC:%2",
1419 m_TargetCoord.ra().toHMSString(true), m_TargetCoord.dec().toDMSString(true)));
1420 }
1421
1422 // Target coords will move the scope and in case of differential align the destination get lost.
1423 // Thus we have to save these coords for later use (-> SlewToTarget()).
1424 // This does not affect normal align, where destination is not used.
1425 if (initialCall)
1426 m_DestinationCoord = m_TargetCoord;
1427
1428 qCDebug(KSTARS_EKOS_ALIGN) << "Capture&Solve - Target RA:" << m_TargetCoord.ra().toHMSString(true)
1429 << " DE:" << m_TargetCoord.dec().toDMSString(true);
1430 qCDebug(KSTARS_EKOS_ALIGN) << "Capture&Solve - Destination RA:" << m_DestinationCoord.ra().toHMSString(true)
1431 << " DE:" << m_DestinationCoord.dec().toDMSString(true);
1432 m_AlignTimer.stop();
1433 m_CaptureTimer.stop();
1434
1435 if (m_Camera == nullptr)
1436 {
1437 appendLogText(i18n("Error: No camera detected."));
1438 return false;
1439 }
1440
1441 if (m_Camera->isConnected() == false)
1442 {
1443 appendLogText(i18n("Error: lost connection to camera."));
1444 KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed"), KSNotification::Align,
1445 KSNotification::Alert);
1446 return false;
1447 }
1448
1449 if (m_Camera->isBLOBEnabled() == false)
1450 {
1451 m_Camera->setBLOBEnabled(true);
1452 }
1453
1454 //if (parser->init() == false)
1455 // return false;
1456
1457 if (m_FocalLength == -1 || m_Aperture == -1)
1458 {
1459 KSNotification::error(
1460 i18n("Telescope aperture and focal length are missing. Please check your optical train settings and try again."));
1461 return false;
1462 }
1463
1464 if (m_CameraPixelWidth == -1 || m_CameraPixelHeight == -1)
1465 {
1466 KSNotification::error(i18n("CCD pixel size is missing. Please check your driver settings and try again."));
1467 return false;
1468 }
1469
1470 if (m_FilterWheel != nullptr)
1471 {
1472 if (m_FilterWheel->isConnected() == false)
1473 {
1474 appendLogText(i18n("Error: lost connection to filter wheel."));
1475 return false;
1476 }
1477
1478 int targetPosition = alignFilter->currentIndex() + 1;
1479
1480 if (targetPosition > 0 && targetPosition != currentFilterPosition)
1481 {
1482 filterPositionPending = true;
1483 // Disabling the autofocus policy for align.
1484 m_FilterManager->setFilterPosition(targetPosition, FilterManager::NO_AUTOFOCUS_POLICY);
1485 setState(ALIGN_PROGRESS);
1486 return true;
1487 }
1488 }
1489
1490 auto clientManager = m_Camera->getDriverInfo()->getClientManager();
1491 if (clientManager && clientManager->getBLOBMode(m_Camera->getDeviceName().toLatin1().constData(), "CCD1") == B_NEVER)
1492 {
1493 if (KMessageBox::questionYesNo(
1494 nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) ==
1495 KMessageBox::Yes)
1496 {
1497 clientManager->setBLOBMode(B_ONLY, m_Camera->getDeviceName().toLatin1().constData(), "CCD1");
1498 clientManager->setBLOBMode(B_ONLY, m_Camera->getDeviceName().toLatin1().constData(), "CCD2");
1499 }
1500 else
1501 {
1502 return false;
1503 }
1504 }
1505
1506 double seqExpose = alignExposure->value();
1507
1508 ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
1509
1510 if (m_FocusState >= FOCUS_PROGRESS)
1511 {
1512 appendLogText(i18n("Cannot capture while focus module is busy. Retrying in %1 seconds...",
1513 CAPTURE_RETRY_DELAY / 1000));
1514 m_CaptureTimer.start(CAPTURE_RETRY_DELAY);
1515 return true;
1516 }
1517
1518 if (targetChip->isCapturing())
1519 {
1520 appendLogText(i18n("Cannot capture while CCD exposure is in progress. Retrying in %1 seconds...",
1521 CAPTURE_RETRY_DELAY / 1000));
1522 m_CaptureTimer.start(CAPTURE_RETRY_DELAY);
1523 return true;
1524 }
1525
1526 // Let rotate the camera BEFORE taking a capture in [Capture & Solve]
1527 if (!m_SolveFromFile && m_Rotator && m_Rotator->absoluteAngleState() == IPS_BUSY)
1528 {
1529 int TimeOut = CAPTURE_ROTATOR_DELAY;
1530 switch (m_CaptureTimeoutCounter)
1531 {
1532 case 0:// Set start time & start angle and estimate rotator time frame during first timeout
1533 {
1534 auto absAngle = 0;
1535 if ((absAngle = m_Rotator->getNumber("ABS_ROTATOR_ANGLE")->at(0)->getValue()))
1536 {
1537 RotatorUtils::Instance()->startTimeFrame(absAngle);
1538 m_estimateRotatorTimeFrame = true;
1539 appendLogText(i18n("Cannot capture while rotator is busy: Time delay estimate started..."));
1540 }
1541 m_CaptureTimer.start(TimeOut);
1542 break;
1543 }
1544 case 1:// Use estimated time frame (in updateProperty()) for second timeout
1545 {
1546 TimeOut = m_RotatorTimeFrame * 1000;
1547 [[fallthrough]];
1548 }
1549 default:
1550 {
1551 TimeOut *= m_CaptureTimeoutCounter; // Extend Timeout in case estimated value is too short
1552 m_estimateRotatorTimeFrame = false;
1553 appendLogText(i18n("Cannot capture while rotator is busy: Retrying in %1 seconds...", TimeOut / 1000));
1554 m_CaptureTimer.start(TimeOut);
1555 }
1556 }
1557 return true; // Return value is used in 'Scheduler::startAstrometry()'
1558 }
1559
1560 m_AlignView->setBaseSize(alignWidget->size());
1561 m_AlignView->setProperty("suspended", (solverModeButtonGroup->checkedId() == SOLVER_LOCAL
1562 && alignDarkFrame->isChecked()));
1563
1564 connect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Align::processData);
1565 connect(m_Camera, &ISD::Camera::newExposureValue, this, &Ekos::Align::checkCameraExposureProgress);
1566
1567 // In case of remote solver, check if we need to update active CCD
1568 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr)
1569 {
1570 if (m_RemoteParserDevice == nullptr)
1571 {
1572 appendLogText(i18n("No remote astrometry driver detected, switching to StellarSolver."));
1573 setSolverMode(SOLVER_LOCAL);
1574 }
1575 else
1576 {
1577 // Update ACTIVE_CCD of the remote astrometry driver so it listens to BLOB emitted by the CCD
1578 auto activeDevices = m_RemoteParserDevice->getBaseDevice().getText("ACTIVE_DEVICES");
1579 if (activeDevices)
1580 {
1581 auto activeCCD = activeDevices.findWidgetByName("ACTIVE_CCD");
1582 if (QString(activeCCD->text) != m_Camera->getDeviceName())
1583 {
1584 activeCCD->setText(m_Camera->getDeviceName().toLatin1().constData());
1585 m_RemoteParserDevice->getClientManager()->sendNewProperty(activeDevices);
1586 }
1587 }
1588
1589 // Enable remote parse
1590 dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->setEnabled(true);
1591 dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->sendArgs(generateRemoteArgs(QSharedPointer<FITSData>()));
1592 solverTimer.start();
1593 }
1594 }
1595
1596 // Remove temporary FITS files left before by the solver
1597 QDir dir(QDir::tempPath());
1598 dir.setNameFilters(QStringList() << "fits*" << "tmp.*");
1599 dir.setFilter(QDir::Files);
1600 for (auto &dirFile : dir.entryList())
1601 dir.remove(dirFile);
1602
1603 prepareCapture(targetChip);
1604
1605 // In case we're in refresh phase of the polar alignment helper then we use capture value from there
1606 if (matchPAHStage(PAA::PAH_REFRESH))
1607 targetChip->capture(m_PolarAlignmentAssistant->getPAHExposureDuration());
1608 else
1609 targetChip->capture(seqExpose);
1610
1611 solveB->setEnabled(false);
1612 loadSlewB->setEnabled(false);
1613 stopB->setEnabled(true);
1614 pi->startAnimation();
1615
1616 RotatorGOTO = false;
1617
1618 setState(ALIGN_PROGRESS);
1619 emit newStatus(state);
1620 solverFOV->setProperty("visible", true);
1621
1622 // If we're just refreshing, then we're done
1623 if (matchPAHStage(PAA::PAH_REFRESH))
1624 return true;
1625
1626 appendLogText(i18n("Capturing image..."));
1627
1628 if (!m_Mount)
1629 return true;
1630
1631 //This block of code will create the row in the solution table and populate RA, DE, and object name.
1632 //It also starts the progress indicator.
1633 double ra, dec;
1634 m_Mount->getEqCoords(&ra, &dec);
1635 if (!m_SolveFromFile)
1636 {
1637 int currentRow = solutionTable->rowCount();
1638 solutionTable->insertRow(currentRow);
1639 for (int i = 4; i < 6; i++)
1640 {
1641 QTableWidgetItem *disabledBox = new QTableWidgetItem();
1642 disabledBox->setFlags(Qt::ItemIsSelectable);
1643 solutionTable->setItem(currentRow, i, disabledBox);
1644 }
1645
1646 QTableWidgetItem *RAReport = new QTableWidgetItem();
1647 RAReport->setText(ScopeRAOut->text());
1649 RAReport->setFlags(Qt::ItemIsSelectable);
1650 solutionTable->setItem(currentRow, 0, RAReport);
1651
1652 QTableWidgetItem *DECReport = new QTableWidgetItem();
1653 DECReport->setText(ScopeDecOut->text());
1655 DECReport->setFlags(Qt::ItemIsSelectable);
1656 solutionTable->setItem(currentRow, 1, DECReport);
1657
1658 double maxrad = 1.0;
1659 SkyObject *so =
1660 KStarsData::Instance()->skyComposite()->objectNearest(new SkyPoint(dms(ra * 15), dms(dec)), maxrad);
1661 QString name;
1662 if (so)
1663 {
1664 name = so->longname();
1665 }
1666 else
1667 {
1668 name = "None";
1669 }
1670 QTableWidgetItem *ObjNameReport = new QTableWidgetItem();
1671 ObjNameReport->setText(name);
1672 ObjNameReport->setTextAlignment(Qt::AlignHCenter);
1673 ObjNameReport->setFlags(Qt::ItemIsSelectable);
1674 solutionTable->setItem(currentRow, 2, ObjNameReport);
1675#ifdef Q_OS_OSX
1676 repaint(); //This is a band-aid for a bug in QT 5.10.0
1677#endif
1678
1679 QProgressIndicator *alignIndicator = new QProgressIndicator(this);
1680 solutionTable->setCellWidget(currentRow, 3, alignIndicator);
1681 alignIndicator->startAnimation();
1682#ifdef Q_OS_OSX
1683 repaint(); //This is a band-aid for a bug in QT 5.10.0
1684#endif
1685 }
1686
1687 return true;
1688}
1689
1691{
1692 auto chip = data->property("chip");
1693 if (chip.isValid() && chip.toInt() == ISD::CameraChip::GUIDE_CCD)
1694 return;
1695
1696 disconnect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Align::processData);
1697 disconnect(m_Camera, &ISD::Camera::newExposureValue, this, &Ekos::Align::checkCameraExposureProgress);
1698
1699 if (data)
1700 {
1701 m_AlignView->loadData(data);
1702 m_ImageData = data;
1703 }
1704 else
1705 m_ImageData.reset();
1706
1707 RUN_PAH(setImageData(m_ImageData));
1708
1709 // If it's Refresh, we're done
1710 if (matchPAHStage(PAA::PAH_REFRESH))
1711 {
1712 setCaptureComplete();
1713 return;
1714 }
1715 else
1716 appendLogText(i18n("Image received."));
1717
1718 // If Local solver, then set capture complete or perform calibration first.
1719 if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL)
1720 {
1721 // Only perform dark image subtraction on local images.
1722 if (alignDarkFrame->isChecked())
1723 {
1724 int x, y, w, h, binx = 1, biny = 1;
1725 ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
1726 targetChip->getFrame(&x, &y, &w, &h);
1727 targetChip->getBinning(&binx, &biny);
1728
1729 uint16_t offsetX = x / binx;
1730 uint16_t offsetY = y / biny;
1731
1732 m_DarkProcessor->denoise(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()),
1733 targetChip, m_ImageData, alignExposure->value(), offsetX, offsetY);
1734 return;
1735 }
1736
1737 setCaptureComplete();
1738 }
1739}
1740
1741void Align::prepareCapture(ISD::CameraChip *targetChip)
1742{
1743 if (m_Camera->getUploadMode() == ISD::Camera::UPLOAD_LOCAL)
1744 {
1745 rememberUploadMode = ISD::Camera::UPLOAD_LOCAL;
1746 m_Camera->setUploadMode(ISD::Camera::UPLOAD_CLIENT);
1747 }
1748
1749 if (m_Camera->isFastExposureEnabled())
1750 {
1751 m_RememberCameraFastExposure = true;
1752 m_Camera->setFastExposureEnabled(false);
1753 }
1754
1755 m_Camera->setEncodingFormat("FITS");
1756 targetChip->resetFrame();
1757 targetChip->setBatchMode(false);
1758 targetChip->setCaptureMode(FITS_ALIGN);
1759 targetChip->setFrameType(FRAME_LIGHT);
1760
1761 int bin = alignBinning->currentIndex() + 1;
1762 targetChip->setBinning(bin, bin);
1763
1764 // Set gain if applicable
1765 if (m_Camera->hasGain() && alignGain->isEnabled() && alignGain->value() > alignGainSpecialValue)
1766 m_Camera->setGain(alignGain->value());
1767 // Set ISO if applicable
1768 if (alignISO->currentIndex() >= 0)
1769 targetChip->setISOIndex(alignISO->currentIndex());
1770}
1771
1772
1773void Align::setCaptureComplete()
1774{
1775 if (matchPAHStage(PAA::PAH_REFRESH))
1776 {
1777 emit newFrame(m_AlignView);
1778 m_PolarAlignmentAssistant->processPAHRefresh();
1779 return;
1780 }
1781
1782 emit newImage(m_AlignView);
1783
1784 solverFOV->setImage(m_AlignView->getDisplayImage());
1785
1786 // If align logging is enabled, let's save the frame.
1787 if (Options::saveAlignImages())
1788 {
1789 QDir dir;
1790 QDateTime now = KStarsData::Instance()->lt();
1791 QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("align/" +
1792 now.toString("yyyy-MM-dd"));
1793 dir.mkpath(path);
1794 // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-'
1795 // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts
1796 QString name = "align_frame_" + now.toString("HH-mm-ss") + ".fits";
1797 QString filename = path + QStringLiteral("/") + name;
1798 if (m_ImageData)
1799 m_ImageData->saveImage(filename);
1800 }
1801
1802 startSolving();
1803}
1804
1806{
1807 gotoModeButtonGroup->button(mode)->setChecked(true);
1808 m_CurrentGotoMode = static_cast<GotoMode>(mode);
1809}
1810
1812{
1813 //RUN_PAH(syncStage());
1814
1815 // This is needed because they might have directories stored in the config file.
1816 // So we can't just use the options folder list.
1817 QStringList astrometryDataDirs = KSUtils::getAstrometryDataDirs();
1818 disconnect(m_AlignView.get(), &FITSView::loaded, this, &Align::startSolving);
1819
1820 m_UsedScale = false;
1821 m_UsedPosition = false;
1822 m_ScaleUsed = 0;
1823 m_RAUsed = 0;
1824 m_DECUsed = 0;
1825
1826 if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL)
1827 {
1828 if(Options::solverType() != SSolver::SOLVER_ASTAP
1829 && Options::solverType() != SSolver::SOLVER_WATNEYASTROMETRY) //You don't need astrometry index files to use ASTAP or Watney
1830 {
1831 bool foundAnIndex = false;
1832 for(auto &dataDir : astrometryDataDirs)
1833 {
1834 QDir dir = QDir(dataDir);
1835 if(dir.exists())
1836 {
1837 dir.setNameFilters(QStringList() << "*.fits");
1838 QStringList indexList = dir.entryList();
1839 if(indexList.count() > 0)
1840 foundAnIndex = true;
1841 }
1842 }
1843 if(!foundAnIndex)
1844 {
1845 appendLogText(
1846 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."));
1847 KConfigDialog * alignSettings = KConfigDialog::exists("alignsettings");
1848 if(alignSettings && m_IndexFilesPage)
1849 {
1850 alignSettings->setCurrentPage(m_IndexFilesPage);
1851 alignSettings->show();
1852 }
1853 }
1854 }
1855 if (m_StellarSolver->isRunning())
1856 m_StellarSolver->abort();
1857 if (!m_ImageData)
1858 m_ImageData = m_AlignView->imageData();
1859 m_StellarSolver->loadNewImageBuffer(m_ImageData->getStatistics(), m_ImageData->getImageBuffer());
1860 m_StellarSolver->setProperty("ProcessType", SSolver::SOLVE);
1861 m_StellarSolver->setProperty("ExtractorType", Options::solveSextractorType());
1862 m_StellarSolver->setProperty("SolverType", Options::solverType());
1863 connect(m_StellarSolver.get(), &StellarSolver::ready, this, &Align::solverComplete);
1864 m_StellarSolver->setIndexFolderPaths(Options::astrometryIndexFolderList());
1865
1866 auto params = m_StellarSolverProfiles.at(Options::solveOptionsProfile());
1867 params.partition = Options::stellarSolverPartition();
1868 m_StellarSolver->setParameters(params);
1869
1870 const SSolver::SolverType type = static_cast<SSolver::SolverType>(m_StellarSolver->property("SolverType").toInt());
1871 if(type == SSolver::SOLVER_LOCALASTROMETRY || type == SSolver::SOLVER_ASTAP || type == SSolver::SOLVER_WATNEYASTROMETRY)
1872 {
1873 QString filename = QDir::tempPath() + QString("/solver%1.fits").arg(QUuid::createUuid().toString().remove(
1874 QRegularExpression("[-{}]")));
1875 m_AlignView->saveImage(filename);
1876 m_StellarSolver->setProperty("FileToProcess", filename);
1877 ExternalProgramPaths externalPaths;
1878 externalPaths.sextractorBinaryPath = Options::sextractorBinary();
1879 externalPaths.solverPath = Options::astrometrySolverBinary();
1880 externalPaths.astapBinaryPath = Options::aSTAPExecutable();
1881 externalPaths.watneyBinaryPath = Options::watneyBinary();
1882 externalPaths.wcsPath = Options::astrometryWCSInfo();
1883 m_StellarSolver->setExternalFilePaths(externalPaths);
1884
1885 //No need for a conf file this way.
1886 m_StellarSolver->setProperty("AutoGenerateAstroConfig", true);
1887 }
1888
1889 if(type == SSolver::SOLVER_ONLINEASTROMETRY )
1890 {
1891 QString filename = QDir::tempPath() + QString("/solver%1.fits").arg(QUuid::createUuid().toString().remove(
1892 QRegularExpression("[-{}]")));
1893 m_AlignView->saveImage(filename);
1894
1895 m_StellarSolver->setProperty("FileToProcess", filename);
1896 m_StellarSolver->setProperty("AstrometryAPIKey", Options::astrometryAPIKey());
1897 m_StellarSolver->setProperty("AstrometryAPIURL", Options::astrometryAPIURL());
1898 }
1899
1900 bool useImageScale = Options::astrometryUseImageScale();
1901 if (useBlindScale == BLIND_ENGAGNED)
1902 {
1903 useImageScale = false;
1904 useBlindScale = BLIND_USED;
1905 appendLogText(i18n("Solving with blind image scale..."));
1906 }
1907
1908 bool useImagePosition = Options::astrometryUsePosition();
1909 if (useBlindPosition == BLIND_ENGAGNED)
1910 {
1911 useImagePosition = false;
1912 useBlindPosition = BLIND_USED;
1913 appendLogText(i18n("Solving with blind image position..."));
1914 }
1915
1916 if (m_SolveFromFile)
1917 {
1918 FITSImage::Solution solution;
1919 m_ImageData->parseSolution(solution);
1920
1921 if (useImageScale && solution.pixscale > 0)
1922 {
1923 m_UsedScale = true;
1924 m_ScaleUsed = solution.pixscale;
1925 m_StellarSolver->setSearchScale(solution.pixscale * 0.8,
1926 solution.pixscale * 1.2,
1927 SSolver::ARCSEC_PER_PIX);
1928 }
1929 else
1930 m_StellarSolver->setProperty("UseScale", false);
1931
1932 if (useImagePosition && solution.ra > 0)
1933 {
1934 m_UsedPosition = true;
1935 m_RAUsed = solution.ra;
1936 m_DECUsed = solution.dec;
1937 m_StellarSolver->setSearchPositionInDegrees(solution.ra, solution.dec);
1938 }
1939 else
1940 m_StellarSolver->setProperty("UsePosition", false);
1941
1942 QVariant value = "";
1943 if (!m_ImageData->getRecordValue("PIERSIDE", value))
1944 {
1945 appendLogText(i18n("Loaded image does not have pierside information"));
1946 m_TargetPierside = ISD::Mount::PIER_UNKNOWN;
1947 }
1948 else
1949 {
1950 appendLogText(i18n("Loaded image was taken on pierside %1", value.toString()));
1951 (value == "WEST") ? m_TargetPierside = ISD::Mount::PIER_WEST : m_TargetPierside = ISD::Mount::PIER_EAST;
1952 }
1953 RotatorUtils::Instance()->Instance()->setImagePierside(m_TargetPierside);
1954 }
1955 else
1956 {
1957 //Setting the initial search scale settings
1958 if (useImageScale)
1959 {
1960 m_UsedScale = true;
1961 m_ScaleUsed = Options::astrometryImageScaleLow();
1962
1963 SSolver::ScaleUnits units = static_cast<SSolver::ScaleUnits>(Options::astrometryImageScaleUnits());
1964 // Extend search scale from 80% to 120%
1965 m_StellarSolver->setSearchScale(Options::astrometryImageScaleLow() * 0.8,
1966 Options::astrometryImageScaleHigh() * 1.2,
1967 units);
1968 }
1969 else
1970 m_StellarSolver->setProperty("UseScale", false);
1971 //Setting the initial search location settings
1972 if(useImagePosition)
1973 {
1974 m_StellarSolver->setSearchPositionInDegrees(m_TelescopeCoord.ra().Degrees(), m_TelescopeCoord.dec().Degrees());
1975 m_UsedPosition = true;
1976 m_RAUsed = m_TelescopeCoord.ra().Degrees();
1977 m_DECUsed = m_TelescopeCoord.dec().Degrees();
1978 }
1979 else
1980 m_StellarSolver->setProperty("UsePosition", false);
1981 }
1982
1983 if(Options::alignmentLogging())
1984 {
1985 // Not trusting SSolver logging right now (Hy Aug 1, 2022)
1986 // m_StellarSolver->setLogLevel(static_cast<SSolver::logging_level>(Options::loggerLevel()));
1987 // m_StellarSolver->setSSLogLevel(SSolver::LOG_NORMAL);
1988 m_StellarSolver->setLogLevel(SSolver::LOG_NONE);
1989 m_StellarSolver->setSSLogLevel(SSolver::LOG_OFF);
1990 if(Options::astrometryLogToFile())
1991 {
1992 m_StellarSolver->setProperty("LogToFile", true);
1993 m_StellarSolver->setProperty("LogFileName", Options::astrometryLogFilepath());
1994 }
1995 }
1996 else
1997 {
1998 m_StellarSolver->setLogLevel(SSolver::LOG_NONE);
1999 m_StellarSolver->setSSLogLevel(SSolver::LOG_OFF);
2000 }
2001
2002 SolverUtils::patchMultiAlgorithm(m_StellarSolver.get());
2003
2004 // Start solving process
2005 m_StellarSolver->start();
2006 }
2007 else
2008 {
2009 if (m_ImageData.isNull())
2010 m_ImageData = m_AlignView->imageData();
2011 // This should run only for load&slew. For regular solve, we don't get here
2012 // as the image is read and solved server-side.
2013 remoteParser->startSolver(m_ImageData->filename(), generateRemoteArgs(m_ImageData), false);
2014 }
2015
2016 // In these cases, the error box is not used, and we don't want it polluted
2017 // from some previous operation.
2018 if (matchPAHStage(PAA::PAH_FIRST_CAPTURE) ||
2019 matchPAHStage(PAA::PAH_SECOND_CAPTURE) ||
2020 matchPAHStage(PAA::PAH_THIRD_CAPTURE) ||
2021 matchPAHStage(PAA::PAH_FIRST_SOLVE) ||
2022 matchPAHStage(PAA::PAH_SECOND_SOLVE) ||
2023 matchPAHStage(PAA::PAH_THIRD_SOLVE) ||
2024 nothingR->isChecked() ||
2025 syncR->isChecked())
2026 errOut->clear();
2027
2028 // Kick off timer
2029 solverTimer.start();
2030
2031 setState(ALIGN_PROGRESS);
2032 emit newStatus(state);
2033}
2034
2035void Align::solverComplete()
2036{
2037 disconnect(m_StellarSolver.get(), &StellarSolver::ready, this, &Align::solverComplete);
2038 if(!m_StellarSolver->solvingDone() || m_StellarSolver->failed())
2039 {
2040 if (matchPAHStage(PAA::PAH_FIRST_CAPTURE) ||
2041 matchPAHStage(PAA::PAH_SECOND_CAPTURE) ||
2042 matchPAHStage(PAA::PAH_THIRD_CAPTURE) ||
2043 matchPAHStage(PAA::PAH_FIRST_SOLVE) ||
2044 matchPAHStage(PAA::PAH_SECOND_SOLVE) ||
2045 matchPAHStage(PAA::PAH_THIRD_SOLVE))
2046 {
2047 if (CHECK_PAH(processSolverFailure()))
2048 return;
2049 else
2050 setState(ALIGN_ABORTED);
2051 }
2052 solverFailed();
2053 return;
2054 }
2055 else
2056 {
2057 FITSImage::Solution solution = m_StellarSolver->getSolution();
2058 const bool eastToTheRight = solution.parity == FITSImage::POSITIVE ? false : true;
2059 solverFinished(solution.orientation, solution.ra, solution.dec, solution.pixscale, eastToTheRight);
2060 }
2061}
2062
2063void 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 if (elapsed > 0)
2075 appendLogText(i18n("Solver completed after %1 seconds.", QString::number(elapsed, 'f', 2)));
2076
2077 m_AlignTimer.stop();
2078 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && m_RemoteParserDevice && remoteParser.get())
2079 {
2080 // Disable remote parse
2081 dynamic_cast<RemoteAstrometryParser *>(remoteParser.get())->setEnabled(false);
2082 }
2083
2084 int binx, biny;
2085 ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
2086 targetChip->getBinning(&binx, &biny);
2087
2088 if (Options::alignmentLogging())
2089 {
2090 QString parityString = eastToTheRight ? "neg" : "pos";
2091 appendLogText(i18n("Solver RA (%1) DEC (%2) Orientation (%3) Pixel Scale (%4) Parity (%5)", QString::number(ra, 'f', 5),
2092 QString::number(dec, 'f', 5), QString::number(orientation, 'f', 5),
2093 QString::number(pixscale, 'f', 5), parityString));
2094 }
2095
2096 // When solving (without Load&Slew), update effective FOV and focal length accordingly.
2097 if (!m_SolveFromFile &&
2098 (isEqual(m_FOVWidth, 0) || m_EffectiveFOVPending || std::fabs(pixscale - m_FOVPixelScale) > 0.005) &&
2099 pixscale > 0)
2100 {
2101 double newFOVW = m_CameraWidth * pixscale / binx / 60.0;
2102 double newFOVH = m_CameraHeight * pixscale / biny / 60.0;
2103
2104 calculateEffectiveFocalLength(newFOVW);
2105 saveNewEffectiveFOV(newFOVW, newFOVH);
2106
2107 m_EffectiveFOVPending = false;
2108 }
2109
2110 m_AlignCoord.setRA0(ra / 15.0); // set catalog coordinates
2111 m_AlignCoord.setDec0(dec);
2112
2113 // Convert to JNow
2114 m_AlignCoord.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd());
2115 // Get horizontal coords
2116 m_AlignCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
2117
2118 // Do not update diff if we are performing load & slew.
2119 if (!m_SolveFromFile)
2120 {
2121 pixScaleOut->setText(QString::number(pixscale, 'f', 2));
2122 calculateAlignTargetDiff();
2123 }
2124
2125 // TODO 2019-11-06 JM: KStars needs to support "upside-down" displays since this is a hack.
2126 // Because astrometry reads image upside-down (bottom to top), the orientation is rotated 180 degrees when compared to PA
2127 // PA = Orientation + 180
2128 double solverPA = KSUtils::rotationToPositionAngle(orientation);
2129 solverFOV->setCenter(m_AlignCoord);
2130 solverFOV->setPA(solverPA);
2131 solverFOV->setImageDisplay(Options::astrometrySolverOverlay());
2132 // Sensor FOV as well
2133 sensorFOV->setPA(solverPA);
2134
2135 PAOut->setText(QString::number(solverPA, 'f', 2)); // Two decimals are reasonable
2136
2137 QString ra_dms, dec_dms;
2138 getFormattedCoords(m_AlignCoord.ra().Hours(), m_AlignCoord.dec().Degrees(), ra_dms, dec_dms);
2139
2140 SolverRAOut->setText(ra_dms);
2141 SolverDecOut->setText(dec_dms);
2142
2143 if (Options::astrometrySolverWCS())
2144 {
2145 auto ccdRotation = m_Camera->getNumber("CCD_ROTATION");
2146 if (ccdRotation)
2147 {
2148 auto rotation = ccdRotation->findWidgetByName("CCD_ROTATION_VALUE");
2149 if (rotation)
2150 {
2151 auto clientManager = m_Camera->getDriverInfo()->getClientManager();
2152 rotation->setValue(orientation);
2153 clientManager->sendNewProperty(ccdRotation);
2154
2155 if (m_wcsSynced == false)
2156 {
2157 appendLogText(
2158 i18n("WCS information updated. Images captured from this point forward shall have valid WCS."));
2159
2160 // Just send telescope info in case the CCD driver did not pick up before.
2161 auto telescopeInfo = m_Mount->getNumber("TELESCOPE_INFO");
2162 if (telescopeInfo)
2163 clientManager->sendNewProperty(telescopeInfo);
2164
2165 m_wcsSynced = true;
2166 }
2167 }
2168 }
2169 }
2170
2171 m_CaptureErrorCounter = 0;
2172 m_SlewErrorCounter = 0;
2173 m_CaptureTimeoutCounter = 0;
2174
2175 appendLogText(
2176 i18n("Solution coordinates: RA (%1) DEC (%2) Telescope Coordinates: RA (%3) DEC (%4) Target Coordinates: RA (%5) DEC (%6)",
2177 m_AlignCoord.ra().toHMSString(),
2178 m_AlignCoord.dec().toDMSString(),
2179 m_TelescopeCoord.ra().toHMSString(),
2180 m_TelescopeCoord.dec().toDMSString(),
2181 m_TargetCoord.ra().toHMSString(),
2182 m_TargetCoord.dec().toDMSString()));
2183
2184 if (!m_SolveFromFile && m_CurrentGotoMode == GOTO_SLEW)
2185 {
2186 dms diffDeg(m_TargetDiffTotal / 3600.0);
2187 appendLogText(i18n("Target is within %1 degrees of solution coordinates.", diffDeg.toDMSString()));
2188 }
2189
2190 if (rememberUploadMode != m_Camera->getUploadMode())
2191 m_Camera->setUploadMode(rememberUploadMode);
2192
2193 // Remember to reset fast exposure
2194 if (m_RememberCameraFastExposure)
2195 {
2196 m_RememberCameraFastExposure = false;
2197 m_Camera->setFastExposureEnabled(true);
2198 }
2199
2200 //This block of code along with some sections in the switch below will set the status report in the solution table for this item.
2201 std::unique_ptr<QTableWidgetItem> statusReport(new QTableWidgetItem());
2202 int currentRow = solutionTable->rowCount() - 1;
2203 if (!m_SolveFromFile) // [Capture & Solve]
2204 {
2205 stopProgressAnimation();
2206 solutionTable->setCellWidget(currentRow, 3, new QWidget());
2207 statusReport->setFlags(Qt::ItemIsSelectable);
2208 // Calibration: determine camera offset from capture
2209 if (m_Rotator != nullptr && m_Rotator->isConnected())
2210 {
2211 if (auto absAngle = m_Rotator->getNumber("ABS_ROTATOR_ANGLE"))
2212 // if (absAngle && std::isnan(m_TargetPositionAngle) == true)
2213 {
2214 sRawAngle = absAngle->at(0)->getValue();
2215 double OffsetAngle = RotatorUtils::Instance()->calcOffsetAngle(sRawAngle, solverPA);
2216 RotatorUtils::Instance()->updateOffset(OffsetAngle);
2217 // Debug info
2218 auto reverseStatus = "Unknown";
2219 auto reverseProperty = m_Rotator->getSwitch("ROTATOR_REVERSE");
2220 if (reverseProperty)
2221 {
2222 if (reverseProperty->at(0)->getState() == ISS_ON)
2223 reverseStatus = "Reversed Direction";
2224 else
2225 reverseStatus = "Normal Direction";
2226 }
2227 qCDebug(KSTARS_EKOS_ALIGN) << "Raw Rotator Angle:" << sRawAngle << "Rotator PA:" << solverPA
2228 << "Rotator Offset:" << OffsetAngle << "Direction:" << reverseStatus;
2229 // Flow is: newSolverResults() -> capture: setAlignresult() -> RotatorSettings: refresh()
2230 emit newSolverResults(solverPA, ra, dec, pixscale);
2231 // appendLogText(i18n("Camera offset angle is %1 degrees.", OffsetAngle));
2232 appendLogText(i18n("Camera position angle is %1 degrees.", RotatorUtils::Instance()->calcCameraAngle(sRawAngle, false)));
2233 }
2234 }
2235 }
2236
2237 QJsonObject solution =
2238 {
2239 {"camera", m_Camera->getDeviceName()},
2240 {"ra", SolverRAOut->text()},
2241 {"de", SolverDecOut->text()},
2242 {"dRA", m_TargetDiffRA},
2243 {"dDE", m_TargetDiffDE},
2244 {"targetDiff", m_TargetDiffTotal},
2245 {"pix", pixscale},
2246 {"PA", solverPA},
2247 {"fov", FOVOut->text()},
2248 };
2249 emit newSolution(solution.toVariantMap());
2250
2251 setState(ALIGN_SUCCESSFUL);
2252 emit newStatus(state);
2253 solverIterations = 0;
2254 KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully"),
2255 KSNotification::Align);
2256
2257 switch (m_CurrentGotoMode)
2258 {
2259 case GOTO_SYNC:
2260 executeGOTO();
2261
2262 if (!m_SolveFromFile)
2263 {
2264 stopProgressAnimation();
2265 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2266 solutionTable->setItem(currentRow, 3, statusReport.release());
2267 }
2268
2269 return;
2270
2271 case GOTO_SLEW:
2272 if (m_SolveFromFile || m_TargetDiffTotal > static_cast<double>(alignAccuracyThreshold->value()))
2273 {
2274 if (!m_SolveFromFile && ++solverIterations == MAXIMUM_SOLVER_ITERATIONS)
2275 {
2276 appendLogText(i18n("Maximum number of iterations reached. Solver failed."));
2277
2278 if (!m_SolveFromFile)
2279 {
2280 statusReport->setIcon(QIcon(":/icons/AlignFailure.svg"));
2281 solutionTable->setItem(currentRow, 3, statusReport.release());
2282 }
2283
2284 solverFailed();
2285 return;
2286 }
2287
2288 targetAccuracyNotMet = true;
2289
2290 if (!m_SolveFromFile)
2291 {
2292 stopProgressAnimation();
2293 statusReport->setIcon(QIcon(":/icons/AlignWarning.svg"));
2294 solutionTable->setItem(currentRow, 3, statusReport.release());
2295 }
2296
2297 executeGOTO();
2298 return;
2299 }
2300
2301 stopProgressAnimation();
2302 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2303 solutionTable->setItem(currentRow, 3, statusReport.release());
2304
2305 appendLogText(i18n("Target is within acceptable range."));
2306 break;
2307
2308 case GOTO_NOTHING:
2309 if (!m_SolveFromFile)
2310 {
2311 stopProgressAnimation();
2312 statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
2313 solutionTable->setItem(currentRow, 3, statusReport.release());
2314 }
2315 break;
2316 }
2317
2318 solverFOV->setProperty("visible", true);
2319
2320 if (!matchPAHStage(PAA::PAH_IDLE))
2321 m_PolarAlignmentAssistant->processPAHStage(orientation, ra, dec, pixscale, eastToTheRight,
2322 m_StellarSolver->getSolutionHealpix(),
2323 m_StellarSolver->getSolutionIndexNumber());
2324 else
2325 {
2326
2328 {
2329 solveB->setEnabled(false);
2330 loadSlewB->setEnabled(false);
2331 return;
2332 }
2333
2334 // We are done!
2335 setState(ALIGN_COMPLETE);
2336 emit newStatus(state);
2337
2338 solveB->setEnabled(true);
2339 loadSlewB->setEnabled(true);
2340 }
2341}
2342
2344{
2345
2346 // If failed-align logging is enabled, let's save the frame.
2347 if (Options::saveFailedAlignImages())
2348 {
2349 QDir dir;
2350 QDateTime now = KStarsData::Instance()->lt();
2351 QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("align/failed");
2352 dir.mkpath(path);
2353 QString extraFilenameInfo;
2354 if (m_UsedScale)
2355 extraFilenameInfo.append(QString("_s%1u%2").arg(m_ScaleUsed, 0, 'f', 3)
2356 .arg(Options::astrometryImageScaleUnits()));
2357 if (m_UsedPosition)
2358 extraFilenameInfo.append(QString("_r%1_d%2").arg(m_RAUsed, 0, 'f', 5).arg(m_DECUsed, 0, 'f', 5));
2359
2360 // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-'
2361 // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts
2362 QString name = "failed_align_frame_" + now.toString("yyyy-MM-dd-HH-mm-ss") + extraFilenameInfo + ".fits";
2363 QString filename = path + QStringLiteral("/") + name;
2364 if (m_ImageData)
2365 {
2366 m_ImageData->saveImage(filename);
2367 appendLogText(i18n("Saving failed solver image to %1", filename));
2368 }
2369
2370 }
2371 if (state != ALIGN_ABORTED)
2372 {
2373 // Try to solve with scale turned off, if not turned off already
2374 if (Options::astrometryUseImageScale() && useBlindScale == BLIND_IDLE)
2375 {
2376 appendLogText(i18n("Solver failed. Retrying without scale constraint."));
2377 useBlindScale = BLIND_ENGAGNED;
2378 setAlignTableResult(ALIGN_RESULT_FAILED);
2379 captureAndSolve(false);
2380 return;
2381 }
2382
2383 // Try to solve with the position turned off, if not turned off already
2384 if (Options::astrometryUsePosition() && useBlindPosition == BLIND_IDLE)
2385 {
2386 appendLogText(i18n("Solver failed. Retrying without position constraint."));
2387 useBlindPosition = BLIND_ENGAGNED;
2388 setAlignTableResult(ALIGN_RESULT_FAILED);
2389 captureAndSolve(false);
2390 return;
2391 }
2392
2393
2394 appendLogText(i18n("Solver Failed."));
2395 if(!Options::alignmentLogging())
2396 appendLogText(
2397 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."));
2398
2399 KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed"),
2400 KSNotification::Align, KSNotification::Alert);
2401 }
2402
2403 pi->stopAnimation();
2404 stopB->setEnabled(false);
2405 solveB->setEnabled(true);
2406 loadSlewB->setEnabled(true);
2407
2408 m_AlignTimer.stop();
2409
2410 m_SolveFromFile = false;
2411 solverIterations = 0;
2412 m_CaptureErrorCounter = 0;
2413 m_CaptureTimeoutCounter = 0;
2414 m_SlewErrorCounter = 0;
2415
2416 setState(ALIGN_FAILED);
2417 emit newStatus(state);
2418
2419 solverFOV->setProperty("visible", false);
2420
2421 setAlignTableResult(ALIGN_RESULT_FAILED);
2422}
2423
2425{
2426 // Check if we need to perform any rotations.
2427 if (Options::astrometryUseRotator())
2428 {
2429 if (m_SolveFromFile) // [Load & Slew] Program flow never lands here!?
2430 {
2431 m_TargetPositionAngle = solverFOV->PA();
2432 // We are not done yet.
2433 qCDebug(KSTARS_EKOS_ALIGN) << "Solving from file: Setting target PA to:" << m_TargetPositionAngle;
2434 }
2435 else // [Capture & Solve]: "direct" or within [Load & Slew]
2436 {
2437 currentRotatorPA = solverFOV->PA();
2438 if (std::isnan(m_TargetPositionAngle) == false) // [Load & Slew] only
2439 {
2440 // If image pierside versus mount pierside is different and policy is lenient ...
2441 if (RotatorUtils::Instance()->Instance()->checkImageFlip() && (Options::astrometryFlipRotationAllowed()))
2442 {
2443 // ... calculate "flipped" PA ...
2444 sRawAngle = RotatorUtils::Instance()->calcRotatorAngle(m_TargetPositionAngle);
2445 m_TargetPositionAngle = RotatorUtils::Instance()->calcCameraAngle(sRawAngle, true);
2446 RotatorUtils::Instance()->setImagePierside(ISD::Mount::PIER_UNKNOWN); // ... once!
2447 }
2448 // Match the position angle with rotator
2449 if (m_Rotator != nullptr && m_Rotator->isConnected())
2450 {
2451 if(fabs(KSUtils::rangePA(currentRotatorPA - m_TargetPositionAngle)) * 60 >
2452 Options::astrometryRotatorThreshold())
2453 {
2454 // Signal flow: newSolverResults() -> capture: setAlignresult() -> RS: refresh()
2455 emit newSolverResults(m_TargetPositionAngle, 0, 0, 0);
2456 appendLogText(i18n("Setting camera position angle to %1 degrees ...", m_TargetPositionAngle));
2457 setState(ALIGN_ROTATING);
2458 emit newStatus(state); // Evoke 'updateProperty()' (where the same check is executed again)
2459 return true;
2460 }
2461 else
2462 {
2463 appendLogText(i18n("Camera position angle is within acceptable range."));
2464 // We're done! (Opposed to 'updateProperty()')
2465 m_TargetPositionAngle = std::numeric_limits<double>::quiet_NaN();
2466 }
2467 }
2468 // Match the position angle manually
2469 else
2470 {
2471 double current = currentRotatorPA;
2472 double target = m_TargetPositionAngle;
2473
2474 double diff = KSUtils::rangePA(current - target);
2475 double threshold = Options::astrometryRotatorThreshold() / 60.0;
2476
2477 appendLogText(i18n("Current PA is %1; Target PA is %2; diff: %3", current, target, diff));
2478
2479 emit manualRotatorChanged(current, target, threshold);
2480
2481 m_ManualRotator->setRotatorDiff(current, target, diff);
2482 if (fabs(diff) > threshold)
2483 {
2484 targetAccuracyNotMet = true;
2485 m_ManualRotator->show();
2486 m_ManualRotator->raise();
2487 setState(ALIGN_ROTATING);
2488 emit newStatus(state);
2489 return true;
2490 }
2491 else
2492 {
2493 m_TargetPositionAngle = std::numeric_limits<double>::quiet_NaN();
2494 targetAccuracyNotMet = false;
2495 }
2496 }
2497 }
2498 }
2499 }
2500 return false;
2501}
2502
2504{
2505 m_CaptureTimer.stop();
2506 if (solverModeButtonGroup->checkedId() == SOLVER_LOCAL)
2507 m_StellarSolver->abort();
2508 else if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser)
2509 remoteParser->stopSolver();
2510 //parser->stopSolver();
2511 pi->stopAnimation();
2512 stopB->setEnabled(false);
2513 solveB->setEnabled(true);
2514 loadSlewB->setEnabled(true);
2515
2516 m_SolveFromFile = false;
2517 solverIterations = 0;
2518 m_CaptureErrorCounter = 0;
2519 m_CaptureTimeoutCounter = 0;
2520 m_SlewErrorCounter = 0;
2521 m_AlignTimer.stop();
2522
2523 disconnect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Align::processData);
2524 disconnect(m_Camera, &ISD::Camera::newExposureValue, this, &Ekos::Align::checkCameraExposureProgress);
2525
2526 if (rememberUploadMode != m_Camera->getUploadMode())
2527 m_Camera->setUploadMode(rememberUploadMode);
2528
2529 // Remember to reset fast exposure
2530 if (m_RememberCameraFastExposure)
2531 {
2532 m_RememberCameraFastExposure = false;
2533 m_Camera->setFastExposureEnabled(true);
2534 }
2535
2536 auto targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
2537
2538 // If capture is still in progress, let's stop that.
2539 if (matchPAHStage(PAA::PAH_POST_REFRESH))
2540 {
2541 if (targetChip->isCapturing())
2542 targetChip->abortExposure();
2543
2544 appendLogText(i18n("Refresh is complete."));
2545 }
2546 else
2547 {
2548 if (targetChip->isCapturing())
2549 {
2550 targetChip->abortExposure();
2551 appendLogText(i18n("Capture aborted."));
2552 }
2553 else
2554 {
2555 double elapsed = solverTimer.elapsed() / 1000.0;
2556 if (elapsed > 0)
2557 appendLogText(i18n("Solver aborted after %1 seconds.", QString::number(elapsed, 'f', 2)));
2558 }
2559 }
2560
2561 setState(mode);
2562 emit newStatus(state);
2563
2564 setAlignTableResult(ALIGN_RESULT_FAILED);
2565}
2566
2567QProgressIndicator * Align::getProgressStatus()
2568{
2569 int currentRow = solutionTable->rowCount() - 1;
2570
2571 // check if the current row indicates a progress state
2572 // 1. no row present
2573 if (currentRow < 0)
2574 return nullptr;
2575 // 2. indicator is not present or not a progress indicator
2576 QWidget *indicator = solutionTable->cellWidget(currentRow, 3);
2577 if (indicator == nullptr)
2578 return nullptr;
2579 return dynamic_cast<QProgressIndicator *>(indicator);
2580}
2581
2582void Align::stopProgressAnimation()
2583{
2584 QProgressIndicator *progress_indicator = getProgressStatus();
2585 if (progress_indicator != nullptr)
2586 progress_indicator->stopAnimation();
2587}
2588
2590{
2591 QList<double> result;
2592
2593 result << sOrientation << sRA << sDEC;
2594
2595 return result;
2596}
2597
2598void Align::appendLogText(const QString &text)
2599{
2600 m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
2601 KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text));
2602
2603 qCInfo(KSTARS_EKOS_ALIGN) << text;
2604
2605 emit newLog(text);
2606}
2607
2608void Align::clearLog()
2609{
2610 m_LogText.clear();
2611 emit newLog(QString());
2612}
2613
2614void Align::updateProperty(INDI::Property prop)
2615{
2616 if (prop.isNameMatch("EQUATORIAL_EOD_COORD") || prop.isNameMatch("EQUATORIAL_COORD"))
2617 {
2618 auto nvp = prop.getNumber();
2619 QString ra_dms, dec_dms;
2620
2621 getFormattedCoords(m_TelescopeCoord.ra().Hours(), m_TelescopeCoord.dec().Degrees(), ra_dms, dec_dms);
2622
2623 ScopeRAOut->setText(ra_dms);
2624 ScopeDecOut->setText(dec_dms);
2625
2626 switch (nvp->s)
2627 {
2628 // Idle --> Mount not tracking or slewing
2629 case IPS_IDLE:
2630 m_wasSlewStarted = false;
2631 //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_IDLE --> setting slewStarted to FALSE";
2632 break;
2633
2634 // Ok --> Mount Tracking. If m_wasSlewStarted is true
2635 // then it just finished slewing
2636 case IPS_OK:
2637 {
2638 // Update the boxes as the mount just finished slewing
2639 if (m_wasSlewStarted && Options::astrometryAutoUpdatePosition())
2640 {
2641 //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_OK --> Auto Update Position...";
2642 opsAstrometry->estRA->setText(ra_dms);
2643 opsAstrometry->estDec->setText(dec_dms);
2644
2645 Options::setAstrometryPositionRA(nvp->np[0].value * 15);
2646 Options::setAstrometryPositionDE(nvp->np[1].value);
2647
2648 //generateArgs();
2649 }
2650
2651 // If dome is syncing, wait until it stops
2652 if (m_Dome && m_Dome->isMoving())
2653 {
2654 domeReady = false;
2655 return;
2656 }
2657
2658 // If we are looking for celestial pole
2659 if (m_wasSlewStarted && matchPAHStage(PAA::PAH_FIND_CP))
2660 {
2661 //qCDebug(KSTARS_EKOS_ALIGN) << "## PAH_FIND_CP--> setting slewStarted to FALSE";
2662 m_wasSlewStarted = false;
2663 appendLogText(i18n("Mount completed slewing near celestial pole. Capture again to verify."));
2664 setSolverAction(GOTO_NOTHING);
2665
2666 m_PolarAlignmentAssistant->setPAHStage(PAA::PAH_FIRST_CAPTURE);
2667 return;
2668 }
2669
2670 switch (state)
2671 {
2672 case ALIGN_PROGRESS:
2673 break;
2674
2675 case ALIGN_SYNCING:
2676 {
2677 m_wasSlewStarted = false;
2678 //qCDebug(KSTARS_EKOS_ALIGN) << "## ALIGN_SYNCING --> setting slewStarted to FALSE";
2679 if (m_CurrentGotoMode == GOTO_SLEW)
2680 {
2681 Slew();
2682 return;
2683 }
2684 else
2685 {
2686 appendLogText(i18n("Mount is synced to solution coordinates."));
2687
2688 /* if (checkIfRotationRequired())
2689 return; */
2690
2691 KSNotification::event(QLatin1String("AlignSuccessful"),
2692 i18n("Astrometry alignment completed successfully"), KSNotification::Align);
2693 setState(ALIGN_COMPLETE);
2694 emit newStatus(state);
2695 solverIterations = 0;
2696 loadSlewB->setEnabled(true);
2697 }
2698 }
2699 break;
2700
2701 case ALIGN_SLEWING:
2702
2703 if (!didSlewStart())
2704 {
2705 // If mount has not started slewing yet, then skip
2706 qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew planned, but not started slewing yet...";
2707 break;
2708 }
2709
2710 //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew completed.";
2711 m_wasSlewStarted = false;
2712 if (m_SolveFromFile)
2713 {
2714 m_SolveFromFile = false;
2715 m_TargetPositionAngle = solverFOV->PA();
2716 qCDebug(KSTARS_EKOS_ALIGN) << "Solving from file: Setting target PA to" << m_TargetPositionAngle;
2717
2718 setState(ALIGN_PROGRESS);
2719 emit newStatus(state);
2720
2721 if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY)
2722 appendLogText(i18n("Settling..."));
2723 m_resetCaptureTimeoutCounter = true; // Enable rotator time frame estimate in 'captureandsolve()'
2724 m_CaptureTimer.start(alignSettlingTime->value());
2725 return;
2726 }
2727 else if (m_CurrentGotoMode == GOTO_SLEW || (m_MountModel && m_MountModel->isRunning()))
2728 {
2729 if (targetAccuracyNotMet)
2730 appendLogText(i18n("Slew complete. Target accuracy is not met, running solver again..."));
2731 else
2732 appendLogText(i18n("Slew complete. Solving Alignment Point. . ."));
2733
2734 targetAccuracyNotMet = false;
2735
2736 setState(ALIGN_PROGRESS);
2737 emit newStatus(state);
2738
2739 if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY)
2740 appendLogText(i18n("Settling..."));
2741 m_resetCaptureTimeoutCounter = true; // Enable rotator time frame estimate in 'captureandsolve()'
2742 m_CaptureTimer.start(alignSettlingTime->value());
2743 }
2744 break;
2745
2746 default:
2747 {
2748 //qCDebug(KSTARS_EKOS_ALIGN) << "## Align State " << state << "--> setting slewStarted to FALSE";
2749 m_wasSlewStarted = false;
2750 }
2751 break;
2752 }
2753 }
2754 break;
2755
2756 // Busy --> Mount Slewing or Moving (NSWE buttons)
2757 case IPS_BUSY:
2758 {
2759 //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew running.";
2760 m_wasSlewStarted = true;
2761
2762 handleMountMotion();
2763 }
2764 break;
2765
2766 // Alert --> Mount has problem moving or communicating.
2767 case IPS_ALERT:
2768 {
2769 //qCDebug(KSTARS_EKOS_ALIGN) << "IPS_ALERT --> setting slewStarted to FALSE";
2770 m_wasSlewStarted = false;
2771
2772 if (state == ALIGN_SYNCING || state == ALIGN_SLEWING)
2773 {
2774 if (state == ALIGN_SYNCING)
2775 appendLogText(i18n("Syncing failed."));
2776 else
2777 appendLogText(i18n("Slewing failed."));
2778
2779 if (++m_SlewErrorCounter == 3)
2780 {
2781 abort();
2782 return;
2783 }
2784 else
2785 {
2786 if (m_CurrentGotoMode == GOTO_SLEW)
2787 Slew();
2788 else
2789 Sync();
2790 }
2791 }
2792
2793 return;
2794 }
2795 }
2796
2797 RUN_PAH(processMountRotation(m_TelescopeCoord.ra(), alignSettlingTime->value()));
2798 }
2799 else if (prop.isNameMatch("ABS_ROTATOR_ANGLE"))
2800 {
2801 auto nvp = prop.getNumber();
2802 double RAngle = nvp->np[0].value;
2803 currentRotatorPA = RotatorUtils::Instance()->calcCameraAngle(RAngle, false);
2804 /*QString logtext = "Alignstate: " + QString::number(state)
2805 + " IPSstate: " + QString::number(nvp->s)
2806 + " Raw Rotator Angle:" + QString::number(nvp->np[0].value)
2807 + " Current PA:" + QString::number(currentRotatorPA)
2808 + " Target PA:" + QString::number(m_TargetPositionAngle)
2809 + " Offset:" + QString::number(Options::pAOffset());
2810 appendLogText(logtext);*/
2811 // loadSlewTarget defined if activation through [Load & Slew] and rotator just reached position
2812 if (std::isnan(m_TargetPositionAngle) == false && state == ALIGN_ROTATING && nvp->s == IPS_OK)
2813 {
2814 auto diff = fabs(RotatorUtils::Instance()->DiffPA(currentRotatorPA - m_TargetPositionAngle)) * 60;
2815 qCDebug(KSTARS_EKOS_ALIGN) << "Raw Rotator Angle:" << RAngle << "Current PA:" << currentRotatorPA
2816 << "Target PA:" << m_TargetPositionAngle << "Diff (arcmin):" << diff << "Offset:"
2817 << Options::pAOffset();
2818
2819 if (diff <= Options::astrometryRotatorThreshold())
2820 {
2821 appendLogText(i18n("Rotator reached camera position angle."));
2822 // Check angle once again (no slew -> no settle time)
2823 // QTimer::singleShot(alignSettlingTime->value(), this, &Ekos::Align::executeGOTO);
2824 RotatorGOTO = true; // Flag for SlewToTarget()
2825 executeGOTO();
2826 }
2827 else
2828 {
2829 // Sometimes update of "nvp->s" is a bit slow, so check state again, if it's really ok
2830 QTimer::singleShot(300, [ = ]
2831 {
2832 if (nvp->s == IPS_OK)
2833 {
2834 appendLogText(i18n("Rotator failed to arrive at the requested position angle (Deviation %1 arcmin).", diff));
2835 setState(ALIGN_FAILED);
2836 emit newStatus(state);
2837 solveB->setEnabled(true);
2838 loadSlewB->setEnabled(true);
2839 }
2840 });
2841 }
2842 }
2843 else if (m_estimateRotatorTimeFrame) // Estimate time frame during first timeout
2844 {
2845 m_RotatorTimeFrame = RotatorUtils::Instance()->calcTimeFrame(RAngle);
2846 }
2847 }
2848 else if (prop.isNameMatch("DOME_MOTION"))
2849 {
2850 // If dome is not ready and state is now
2851 if (domeReady == false && prop.getState() == IPS_OK)
2852 {
2853 domeReady = true;
2854 // trigger process number for mount so that it proceeds with normal workflow since
2855 // it was stopped by dome not being ready
2856 handleMountStatus();
2857 }
2858 }
2859 else if (prop.isNameMatch("TELESCOPE_MOTION_NS") || prop.isNameMatch("TELESCOPE_MOTION_WE"))
2860 {
2861 switch (prop.getState())
2862 {
2863 case IPS_BUSY:
2864 // react upon mount motion
2865 handleMountMotion();
2866 m_wasSlewStarted = true;
2867 break;
2868 default:
2869 qCDebug(KSTARS_EKOS_ALIGN) << "Mount motion finished.";
2870 handleMountStatus();
2871 break;
2872 }
2873 }
2874}
2875
2876void Align::handleMountMotion()
2877{
2878 if (state == ALIGN_PROGRESS)
2879 {
2880 if (matchPAHStage(PAA::PAH_IDLE))
2881 {
2882 // whoops, mount slews during alignment
2883 appendLogText(i18n("Slew detected, suspend solving..."));
2884 suspend();
2885 }
2886
2887 setState(ALIGN_SLEWING);
2888 }
2889}
2890
2891void Align::handleMountStatus()
2892{
2893 auto nvp = m_Mount->getNumber(m_Mount->isJ2000() ? "EQUATORIAL_COORD" :
2894 "EQUATORIAL_EOD_COORD");
2895
2896 if (nvp)
2897 updateProperty(nvp);
2898}
2899
2900
2902{
2903 if (m_SolveFromFile)
2904 {
2905 // Target coords will move the scope and in case of differential align the destination get lost.
2906 // Thus we have to save these coords for later use (-> SlewToTarget()).
2907 // This does not affect normal align, where destination is not used.
2908 m_DestinationCoord = m_AlignCoord;
2909 m_TargetCoord = m_AlignCoord;
2910
2911 qCDebug(KSTARS_EKOS_ALIGN) << "Solving from file. Setting Target Coordinates align coords. RA:"
2912 << m_TargetCoord.ra().toHMSString()
2913 << "DE:" << m_TargetCoord.dec().toDMSString();
2914
2915 SlewToTarget();
2916 }
2917 else if (m_CurrentGotoMode == GOTO_SYNC)
2918 Sync();
2919 else if (m_CurrentGotoMode == GOTO_SLEW)
2920 SlewToTarget();
2921}
2922
2924{
2925 setState(ALIGN_SYNCING);
2926
2927 if (m_Mount->Sync(&m_AlignCoord))
2928 {
2929 emit newStatus(state);
2930 appendLogText(
2931 i18n("Syncing to RA (%1) DEC (%2)", m_AlignCoord.ra().toHMSString(), m_AlignCoord.dec().toDMSString()));
2932 }
2933 else
2934 {
2935 setState(ALIGN_IDLE);
2936 emit newStatus(state);
2937 appendLogText(i18n("Syncing failed."));
2938 }
2939}
2940
2942{
2943 setState(ALIGN_SLEWING);
2944 emit newStatus(state);
2945
2946 //qCDebug(KSTARS_EKOS_ALIGN) << "## Before SLEW command: wasSlewStarted -->" << m_wasSlewStarted;
2947 //m_wasSlewStarted = currentTelescope->Slew(&m_targetCoord);
2948 //qCDebug(KSTARS_EKOS_ALIGN) << "## After SLEW command: wasSlewStarted -->" << m_wasSlewStarted;
2949
2950 // JM 2019-08-23: Do not assume that slew was started immediately. Wait until IPS_BUSY state is triggered
2951 // from Goto
2952 if (m_Mount->Slew(&m_TargetCoord))
2953 {
2954 slewStartTimer.start();
2955 appendLogText(i18n("Slewing to target coordinates: RA (%1) DEC (%2).", m_TargetCoord.ra().toHMSString(),
2956 m_TargetCoord.dec().toDMSString()));
2957 }
2958 else // inform user about failure
2959 {
2960 appendLogText(i18n("Slewing to target coordinates: RA (%1) DEC (%2) is rejected. (see notification)",
2961 m_TargetCoord.ra().toHMSString(),
2962 m_TargetCoord.dec().toDMSString()));
2963 setState(ALIGN_FAILED);
2964 emit newStatus(state);
2965 solveB->setEnabled(true);
2966 loadSlewB->setEnabled(true);
2967 }
2968}
2969
2971{
2972 if (canSync && !m_SolveFromFile)
2973 {
2974 // 2018-01-24 JM: This is ugly. Maybe use DBus? Signal/Slots? Ekos Manager usage like this should be avoided
2975#if 0
2976 if (Ekos::Manager::Instance()->getCurrentJobName().isEmpty())
2977 {
2978 KSNotification::event(QLatin1String("EkosSchedulerTelescopeSynced"),
2979 i18n("Ekos job (%1) - Telescope synced",
2980 Ekos::Manager::Instance()->getCurrentJobName()));
2981 }
2982#endif
2983
2984 if (Options::astrometryDifferentialSlewing()) // Remember: Target coords will move the telescope
2985 {
2986 if (!RotatorGOTO) // Only for mount movements
2987 {
2988 m_TargetCoord.setRA(m_TargetCoord.ra() - m_AlignCoord.ra().deltaAngle(m_DestinationCoord.ra()));
2989 m_TargetCoord.setDec(m_TargetCoord.dec() - m_AlignCoord.dec().deltaAngle(m_DestinationCoord.dec()));
2990 qCDebug(KSTARS_EKOS_ALIGN) << "Differential slew - Target RA:" << m_TargetCoord.ra().toHMSString()
2991 << " DE:" << m_TargetCoord.dec().toDMSString();
2992 }
2993 Slew();
2994 }
2995 else
2996 Sync();
2997 return;
2998 }
2999 Slew();
3000}
3001
3002
3003void Align::getFormattedCoords(double ra, double dec, QString &ra_str, QString &dec_str)
3004{
3005 dms ra_s, dec_s;
3006 ra_s.setH(ra);
3007 dec_s.setD(dec);
3008
3009 ra_str = QString("%1:%2:%3")
3010 .arg(ra_s.hour(), 2, 10, QChar('0'))
3011 .arg(ra_s.minute(), 2, 10, QChar('0'))
3012 .arg(ra_s.second(), 2, 10, QChar('0'));
3013 if (dec_s.Degrees() < 0)
3014 dec_str = QString("-%1:%2:%3")
3015 .arg(abs(dec_s.degree()), 2, 10, QChar('0'))
3016 .arg(abs(dec_s.arcmin()), 2, 10, QChar('0'))
3017 .arg(dec_s.arcsec(), 2, 10, QChar('0'));
3018 else
3019 dec_str = QString("%1:%2:%3")
3020 .arg(dec_s.degree(), 2, 10, QChar('0'))
3021 .arg(dec_s.arcmin(), 2, 10, QChar('0'))
3022 .arg(dec_s.arcsec(), 2, 10, QChar('0'));
3023}
3024
3026{
3027 if (fileURL.isEmpty())
3028 fileURL = QFileDialog::getOpenFileName(Ekos::Manager::Instance(), i18nc("@title:window", "Load Image"), dirPath,
3029 "Images (*.fits *.fits.fz *.fit *.fts *.xisf "
3030 "*.jpg *.jpeg *.png *.gif *.bmp "
3031 "*.cr2 *.cr3 *.crw *.nef *.raf *.dng *.arw *.orf)");
3032
3033 if (fileURL.isEmpty())
3034 return false;
3035
3036 QFileInfo fileInfo(fileURL);
3037 if (fileInfo.exists() == false)
3038 return false;
3039
3040 dirPath = fileInfo.absolutePath();
3041
3042 RotatorGOTO = false;
3043
3044 m_SolveFromFile = true;
3045
3046 if (m_PolarAlignmentAssistant)
3047 m_PolarAlignmentAssistant->stopPAHProcess();
3048
3049 slewR->setChecked(true);
3050 m_CurrentGotoMode = GOTO_SLEW;
3051
3052 solveB->setEnabled(false);
3053 loadSlewB->setEnabled(false);
3054 stopB->setEnabled(true);
3055 pi->startAnimation();
3056
3057 if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && m_RemoteParserDevice == nullptr)
3058 {
3059 appendLogText(i18n("No remote astrometry driver detected, switching to StellarSolver."));
3060 setSolverMode(SOLVER_LOCAL);
3061 }
3062
3063 m_ImageData.clear();
3064
3065 m_AlignView->loadFile(fileURL);
3066 //m_FileToSolve = fileURL;
3067 connect(m_AlignView.get(), &FITSView::loaded, this, &Align::startSolving);
3068
3069 return true;
3070}
3071
3072bool Align::loadAndSlew(const QByteArray &image, const QString &extension)
3073{
3074 RotatorGOTO = false;
3075 m_SolveFromFile = true;
3076 RUN_PAH(stopPAHProcess());
3077 slewR->setChecked(true);
3078 m_CurrentGotoMode = GOTO_SLEW;
3079 solveB->setEnabled(false);
3080 loadSlewB->setEnabled(false);
3081 stopB->setEnabled(true);
3082 pi->startAnimation();
3083
3084 // Must clear image data so we are forced to read the
3085 // image data again from align view when solving begins.
3086 m_ImageData.clear();
3088 data.reset(new FITSData(), &QObject::deleteLater);
3089 data->setExtension(extension);
3090 data->loadFromBuffer(image);
3091 m_AlignView->loadData(data);
3092 startSolving();
3093 return true;
3094}
3095
3096void Align::setExposure(double value)
3097{
3098 alignExposure->setValue(value);
3099}
3100
3101void Align::setBinningIndex(int binIndex)
3102{
3103 // If sender is not our combo box, then we need to update the combobox itself
3104 if (dynamic_cast<QComboBox *>(sender()) != alignBinning)
3105 {
3106 alignBinning->blockSignals(true);
3107 alignBinning->setCurrentIndex(binIndex);
3108 alignBinning->blockSignals(false);
3109 }
3110
3111 // Need to calculate FOV and args for APP
3112 if (Options::astrometryImageScaleUnits() == SSolver::ARCSEC_PER_PIX)
3113 calculateFOV();
3114}
3115
3116bool Align::setFilterWheel(ISD::FilterWheel * device)
3117{
3118 if (m_FilterWheel && m_FilterWheel == device)
3119 {
3120 checkFilter();
3121 return false;
3122 }
3123
3124 if (m_FilterWheel)
3125 m_FilterWheel->disconnect(this);
3126
3127 m_FilterWheel = device;
3128
3129 if (m_FilterWheel)
3130 {
3131 connect(m_FilterWheel, &ISD::ConcreteDevice::Connected, this, [this]()
3132 {
3133 FilterPosLabel->setEnabled(true);
3134 alignFilter->setEnabled(true);
3135 });
3136 connect(m_FilterWheel, &ISD::ConcreteDevice::Disconnected, this, [this]()
3137 {
3138 FilterPosLabel->setEnabled(false);
3139 alignFilter->setEnabled(false);
3140 });
3141 }
3142
3143 auto isConnected = m_FilterWheel && m_FilterWheel->isConnected();
3144 FilterPosLabel->setEnabled(isConnected);
3145 alignFilter->setEnabled(isConnected);
3146
3147 checkFilter();
3148 return true;
3149}
3150
3151QString Align::filterWheel()
3152{
3153 if (m_FilterWheel)
3154 return m_FilterWheel->getDeviceName();
3155
3156 return QString();
3157}
3158
3159bool Align::setFilter(const QString &filter)
3160{
3161 if (m_FilterWheel)
3162 {
3163 alignFilter->setCurrentText(filter);
3164 return true;
3165 }
3166
3167 return false;
3168}
3169
3170
3171QString Align::filter()
3172{
3173 return alignFilter->currentText();
3174}
3175
3177{
3178 alignFilter->clear();
3179
3180 if (!m_FilterWheel)
3181 {
3182 FilterPosLabel->setEnabled(false);
3183 alignFilter->setEnabled(false);
3184 return;
3185 }
3186
3187 auto isConnected = m_FilterWheel->isConnected();
3188 FilterPosLabel->setEnabled(isConnected);
3189 alignFilter->setEnabled(alignUseCurrentFilter->isChecked() == false);
3190
3191 setupFilterManager();
3192
3193 alignFilter->addItems(m_FilterManager->getFilterLabels());
3194 currentFilterPosition = m_FilterManager->getFilterPosition();
3195
3196 if (alignUseCurrentFilter->isChecked())
3197 {
3198 // use currently selected filter
3199 alignFilter->setCurrentIndex(currentFilterPosition - 1);
3200 }
3201 else
3202 {
3203 // use the fixed filter
3204 auto filter = m_Settings["alignFilter"];
3205 if (filter.isValid())
3206 alignFilter->setCurrentText(filter.toString());
3207 }
3208}
3209
3211{
3212 if ((Manager::Instance()->existRotatorController()) && (!m_Rotator || !(Device == m_Rotator)))
3213 {
3214 rotatorB->setEnabled(false);
3215 if (m_Rotator)
3216 {
3217 m_Rotator->disconnect(this);
3218 m_RotatorControlPanel->close();
3219 }
3220 m_Rotator = Device;
3221 if (m_Rotator)
3222 {
3223 if (Manager::Instance()->getRotatorController(m_Rotator->getDeviceName(), m_RotatorControlPanel))
3224 {
3225 connect(m_Rotator, &ISD::Rotator::propertyUpdated, this, &Ekos::Align::updateProperty, Qt::UniqueConnection);
3226 connect(rotatorB, &QPushButton::clicked, this, [this]()
3227 {
3228 m_RotatorControlPanel->show();
3229 m_RotatorControlPanel->raise();
3230 });
3231 rotatorB->setEnabled(true);
3232 }
3233 }
3234 }
3235}
3236
3237void Align::setWCSEnabled(bool enable)
3238{
3239 if (!m_Camera)
3240 return;
3241
3242 auto wcsControl = m_Camera->getSwitch("WCS_CONTROL");
3243
3244 if (!wcsControl)
3245 return;
3246
3247 auto wcs_enable = wcsControl->findWidgetByName("WCS_ENABLE");
3248 auto wcs_disable = wcsControl->findWidgetByName("WCS_DISABLE");
3249
3250 if (!wcs_enable || !wcs_disable)
3251 return;
3252
3253 if ((wcs_enable->getState() == ISS_ON && enable) || (wcs_disable->getState() == ISS_ON && !enable))
3254 return;
3255
3256 wcsControl->reset();
3257 if (enable)
3258 {
3259 appendLogText(i18n("World Coordinate System (WCS) is enabled."));
3260 wcs_enable->setState(ISS_ON);
3261 }
3262 else
3263 {
3264 appendLogText(i18n("World Coordinate System (WCS) is disabled."));
3265 wcs_disable->setState(ISS_ON);
3266 m_wcsSynced = false;
3267 }
3268
3269 auto clientManager = m_Camera->getDriverInfo()->getClientManager();
3270 if (clientManager)
3271 clientManager->sendNewProperty(wcsControl);
3272}
3273
3274void Align::checkCameraExposureProgress(ISD::CameraChip * targetChip, double remaining, IPState state)
3275{
3276 INDI_UNUSED(targetChip);
3277 INDI_UNUSED(remaining);
3278
3279 if (state == IPS_ALERT)
3280 {
3281 if (++m_CaptureErrorCounter == 3 && !matchPAHStage(PolarAlignmentAssistant::PAH_REFRESH))
3282 {
3283 appendLogText(i18n("Capture error. Aborting..."));
3284 abort();
3285 return;
3286 }
3287
3288 appendLogText(i18n("Restarting capture attempt #%1", m_CaptureErrorCounter));
3289 setAlignTableResult(ALIGN_RESULT_FAILED);
3290 captureAndSolve(false);
3291 }
3292}
3293
3294void Align::setAlignTableResult(AlignResult result)
3295{
3296 // Do nothing if the progress indicator is not running.
3297 // This is necessary since it could happen that a problem occurs
3298 // before #captureAndSolve() has been started and there does not
3299 // exist a table entry for the current run.
3300 QProgressIndicator *progress_indicator = getProgressStatus();
3301 if (progress_indicator == nullptr || ! progress_indicator->isAnimated())
3302 return;
3303 stopProgressAnimation();
3304
3305 QIcon icon;
3306 switch (result)
3307 {
3308 case ALIGN_RESULT_SUCCESS:
3309 icon = QIcon(":/icons/AlignSuccess.svg");
3310 break;
3311
3312 case ALIGN_RESULT_WARNING:
3313 icon = QIcon(":/icons/AlignWarning.svg");
3314 break;
3315
3316 case ALIGN_RESULT_FAILED:
3317 default:
3318 icon = QIcon(":/icons/AlignFailure.svg");
3319 break;
3320 }
3321 int currentRow = solutionTable->rowCount() - 1;
3322 solutionTable->setCellWidget(currentRow, 3, new QWidget());
3323 QTableWidgetItem *statusReport = new QTableWidgetItem();
3324 statusReport->setIcon(icon);
3325 statusReport->setFlags(Qt::ItemIsSelectable);
3326 solutionTable->setItem(currentRow, 3, statusReport);
3327}
3328
3329void Align::setFocusStatus(Ekos::FocusState state)
3330{
3331 m_FocusState = state;
3332}
3333
3334uint8_t Align::getSolverDownsample(uint16_t binnedW)
3335{
3336 uint8_t downsample = Options::astrometryDownsample();
3337
3338 if (!Options::astrometryAutoDownsample())
3339 return downsample;
3340
3341 while (downsample < 8)
3342 {
3343 if (binnedW / downsample <= 1024)
3344 break;
3345
3346 downsample += 2;
3347 }
3348
3349 return downsample;
3350}
3351
3352void Align::setCaptureStatus(CaptureState newState)
3353{
3354 switch (newState)
3355 {
3356 case CAPTURE_ALIGNING:
3357 if (m_Mount && m_Mount->hasAlignmentModel() && Options::resetMountModelAfterMeridian())
3358 {
3359 qCDebug(KSTARS_EKOS_ALIGN) << "Post meridian flip mount model reset" << (m_Mount->clearAlignmentModel() ?
3360 "successful." : "failed.");
3361 }
3362 if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY)
3363 appendLogText(i18n("Settling..."));
3364 m_resetCaptureTimeoutCounter = true; // Enable rotator time frame estimate in 'captureandsolve()'
3365 m_CaptureTimer.start(alignSettlingTime->value());
3366 break;
3367 // Is this needed anymore with new flip policy? (sb 2023-10-20)
3368 // On meridian flip, reset Target Position Angle to fully rotated value
3369 // expected after MF so that we do not end up with reversed camera rotation
3371 if (std::isnan(m_TargetPositionAngle) == false)
3372 m_TargetPositionAngle = KSUtils::rangePA(m_TargetPositionAngle + 180.0);
3373 break;
3374 default:
3375 break;
3376 }
3377
3378 m_CaptureState = newState;
3379}
3380
3381void Align::showFITSViewer()
3382{
3383 static int lastFVTabID = -1;
3384 if (m_ImageData)
3385 {
3386 QUrl url = QUrl::fromLocalFile("align.fits");
3387 if (fv.isNull())
3388 {
3389 fv = KStars::Instance()->createFITSViewer();
3390 fv->loadData(m_ImageData, url, &lastFVTabID);
3391 connect(fv.get(), &FITSViewer::terminated, this, [this]()
3392 {
3393 fv.clear();
3394 });
3395 }
3396 else if (fv->updateData(m_ImageData, url, lastFVTabID, &lastFVTabID) == false)
3397 fv->loadData(m_ImageData, url, &lastFVTabID);
3398
3399 fv->show();
3400 }
3401}
3402
3403void Align::toggleAlignWidgetFullScreen()
3404{
3405 if (alignWidget->parent() == nullptr)
3406 {
3407 alignWidget->setParent(this);
3408 rightLayout->insertWidget(0, alignWidget);
3409 alignWidget->showNormal();
3410 }
3411 else
3412 {
3413 alignWidget->setParent(nullptr);
3414 alignWidget->setWindowTitle(i18nc("@title:window", "Align Frame"));
3415 alignWidget->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint);
3416 alignWidget->showMaximized();
3417 alignWidget->show();
3418 }
3419}
3420
3421void Align::setMountStatus(ISD::Mount::Status newState)
3422{
3423 switch (newState)
3424 {
3425 case ISD::Mount::MOUNT_PARKED:
3426 solveB->setEnabled(false);
3427 loadSlewB->setEnabled(false);
3428 break;
3429 case ISD::Mount::MOUNT_IDLE:
3430 solveB->setEnabled(true);
3431 loadSlewB->setEnabled(true);
3432 break;
3433 case ISD::Mount::MOUNT_PARKING:
3434 solveB->setEnabled(false);
3435 loadSlewB->setEnabled(false);
3436 break;
3437 case ISD::Mount::MOUNT_SLEWING:
3438 case ISD::Mount::MOUNT_MOVING:
3439 solveB->setEnabled(false);
3440 loadSlewB->setEnabled(false);
3441 break;
3442
3443 default:
3444 if (state != ALIGN_PROGRESS)
3445 {
3446 solveB->setEnabled(true);
3447 if (matchPAHStage(PAA::PAH_IDLE))
3448 {
3449 loadSlewB->setEnabled(true);
3450 }
3451 }
3452 break;
3453 }
3454
3455 RUN_PAH(setMountStatus(newState));
3456}
3457
3459{
3460 m_RemoteParserDevice = device;
3461
3462 remoteSolverR->setEnabled(true);
3463 if (remoteParser.get() != nullptr)
3464 {
3465 remoteParser->setAstrometryDevice(m_RemoteParserDevice);
3466 connect(remoteParser.get(), &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
3467 connect(remoteParser.get(), &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
3468 }
3469}
3470
3472{
3473 solverFOV->setImageDisplay(Options::astrometrySolverWCS());
3474 m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000);
3475 if (m_Rotator)
3476 m_RotatorControlPanel->updateFlipPolicy(Options::astrometryFlipRotationAllowed());
3477}
3478
3479void Align::setupOptions()
3480{
3481 KConfigDialog *dialog = new KConfigDialog(this, "alignsettings", Options::self());
3482
3483#ifdef Q_OS_OSX
3485#endif
3486
3487 opsAlign = new OpsAlign(this);
3488 connect(opsAlign, &OpsAlign::settingsUpdated, this, &Ekos::Align::refreshAlignOptions);
3489 KPageWidgetItem *page = dialog->addPage(opsAlign, i18n("StellarSolver Options"));
3490 page->setIcon(QIcon(":/icons/StellarSolverIcon.png"));
3491 // connect(rotatorB, &QPushButton::clicked, dialog, &KConfigDialog::show);
3492
3493 opsPrograms = new OpsPrograms(this);
3494 page = dialog->addPage(opsPrograms, i18n("External & Online Programs"));
3495 page->setIcon(QIcon(":/icons/astrometry.svg"));
3496
3497 opsAstrometry = new OpsAstrometry(this);
3498 page = dialog->addPage(opsAstrometry, i18n("Scale & Position"));
3499 page->setIcon(QIcon(":/icons/center_telescope_red.svg"));
3500
3501 optionsProfileEditor = new StellarSolverProfileEditor(this, Ekos::AlignProfiles, dialog);
3502 page = dialog->addPage(optionsProfileEditor, i18n("Align Options Profiles Editor"));
3503 connect(optionsProfileEditor, &StellarSolverProfileEditor::optionsProfilesUpdated, this, [this]()
3504 {
3505 if(QFile(savedOptionsProfiles).exists())
3506 m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
3507 else
3508 m_StellarSolverProfiles = getDefaultAlignOptionsProfiles();
3509 opsAlign->reloadOptionsProfiles();
3510 });
3511 page->setIcon(QIcon::fromTheme("configure"));
3512
3513 connect(opsAlign, &OpsAlign::needToLoadProfile, this, [this, dialog, page](const QString & profile)
3514 {
3515 optionsProfileEditor->loadProfile(profile);
3516 dialog->setCurrentPage(page);
3517 });
3518
3519 opsAstrometryIndexFiles = new OpsAstrometryIndexFiles(this);
3520 m_IndexFilesPage = dialog->addPage(opsAstrometryIndexFiles, i18n("Index Files"));
3521 m_IndexFilesPage->setIcon(QIcon::fromTheme("map-flat"));
3522}
3523
3524void Align::setupSolutionTable()
3525{
3526 solutionTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
3527
3528 clearAllSolutionsB->setIcon(
3529 QIcon::fromTheme("application-exit"));
3530 clearAllSolutionsB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
3531
3532 removeSolutionB->setIcon(QIcon::fromTheme("list-remove"));
3533 removeSolutionB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
3534
3535 exportSolutionsCSV->setIcon(
3536 QIcon::fromTheme("document-save-as"));
3537 exportSolutionsCSV->setAttribute(Qt::WA_LayoutUsesWidgetRect);
3538
3539 autoScaleGraphB->setIcon(QIcon::fromTheme("zoom-fit-best"));
3540 autoScaleGraphB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
3541
3542 connect(clearAllSolutionsB, &QPushButton::clicked, this, &Ekos::Align::slotClearAllSolutionPoints);
3543 connect(removeSolutionB, &QPushButton::clicked, this, &Ekos::Align::slotRemoveSolutionPoint);
3544 connect(exportSolutionsCSV, &QPushButton::clicked, this, &Ekos::Align::exportSolutionPoints);
3545 connect(autoScaleGraphB, &QPushButton::clicked, this, &Ekos::Align::slotAutoScaleGraph);
3546 connect(mountModelB, &QPushButton::clicked, this, &Ekos::Align::slotMountModel);
3547 connect(solutionTable, &QTableWidget::cellClicked, this, &Ekos::Align::selectSolutionTableRow);
3548}
3549
3550void Align::setupPlot()
3551{
3552 double accuracyRadius = alignAccuracyThreshold->value();
3553
3554 alignPlot->setBackground(QBrush(Qt::black));
3555 alignPlot->setSelectionTolerance(10);
3556
3557 alignPlot->xAxis->setBasePen(QPen(Qt::white, 1));
3558 alignPlot->yAxis->setBasePen(QPen(Qt::white, 1));
3559
3560 alignPlot->xAxis->setTickPen(QPen(Qt::white, 1));
3561 alignPlot->yAxis->setTickPen(QPen(Qt::white, 1));
3562
3563 alignPlot->xAxis->setSubTickPen(QPen(Qt::white, 1));
3564 alignPlot->yAxis->setSubTickPen(QPen(Qt::white, 1));
3565
3566 alignPlot->xAxis->setTickLabelColor(Qt::white);
3567 alignPlot->yAxis->setTickLabelColor(Qt::white);
3568
3569 alignPlot->xAxis->setLabelColor(Qt::white);
3570 alignPlot->yAxis->setLabelColor(Qt::white);
3571
3572 alignPlot->xAxis->setLabelFont(QFont(font().family(), 10));
3573 alignPlot->yAxis->setLabelFont(QFont(font().family(), 10));
3574
3575 alignPlot->xAxis->setLabelPadding(2);
3576 alignPlot->yAxis->setLabelPadding(2);
3577
3578 alignPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
3579 alignPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
3580 alignPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
3581 alignPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
3582 alignPlot->xAxis->grid()->setZeroLinePen(QPen(Qt::yellow));
3583 alignPlot->yAxis->grid()->setZeroLinePen(QPen(Qt::yellow));
3584
3585 alignPlot->xAxis->setLabel(i18n("dRA (arcsec)"));
3586 alignPlot->yAxis->setLabel(i18n("dDE (arcsec)"));
3587
3588 alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
3589 alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
3590
3591 alignPlot->setInteractions(QCP::iRangeZoom);
3592 alignPlot->setInteraction(QCP::iRangeDrag, true);
3593
3594 alignPlot->addGraph();
3595 alignPlot->graph(0)->setLineStyle(QCPGraph::lsNone);
3596 alignPlot->graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, Qt::white, 15));
3597
3598 buildTarget();
3599
3600 connect(alignPlot, &QCustomPlot::mouseMove, this, &Ekos::Align::handlePointTooltip);
3601 connect(rightLayout, &QSplitter::splitterMoved, this, &Ekos::Align::handleVerticalPlotSizeChange);
3602 connect(alignSplitter, &QSplitter::splitterMoved, this, &Ekos::Align::handleHorizontalPlotSizeChange);
3603
3604 alignPlot->resize(190, 190);
3605 alignPlot->replot();
3606}
3607
3608void Align::setupFilterManager()
3609{
3610 // Do we have an existing filter manager?
3611 if (m_FilterManager)
3612 m_FilterManager->disconnect(this);
3613
3614 // Create new or refresh device
3615 Ekos::Manager::Instance()->createFilterManager(m_FilterWheel);
3616
3617 // Return global filter manager for this filter wheel.
3618 Ekos::Manager::Instance()->getFilterManager(m_FilterWheel->getDeviceName(), m_FilterManager);
3619
3620 connect(m_FilterManager.get(), &FilterManager::ready, this, [this]()
3621 {
3622 if (filterPositionPending)
3623 {
3624 m_FocusState = FOCUS_IDLE;
3625 filterPositionPending = false;
3626 captureAndSolve(false);
3627 }
3628 });
3629
3630 connect(m_FilterManager.get(), &FilterManager::failed, this, [this]()
3631 {
3632 appendLogText(i18n("Filter operation failed."));
3633 abort();
3634 }
3635 );
3636
3637 connect(m_FilterManager.get(), &FilterManager::newStatus, this, [this](Ekos::FilterState filterState)
3638 {
3639 if (filterPositionPending)
3640 {
3641 switch (filterState)
3642 {
3643 case FILTER_OFFSET:
3644 appendLogText(i18n("Changing focus offset by %1 steps...", m_FilterManager->getTargetFilterOffset()));
3645 break;
3646
3647 case FILTER_CHANGE:
3648 {
3649 const int filterComboIndex = m_FilterManager->getTargetFilterPosition() - 1;
3650 if (filterComboIndex >= 0 && filterComboIndex < alignFilter->count())
3651 appendLogText(i18n("Changing filter to %1...", alignFilter->itemText(filterComboIndex)));
3652 }
3653 break;
3654
3655 case FILTER_AUTOFOCUS:
3656 appendLogText(i18n("Auto focus on filter change..."));
3657 break;
3658
3659 default:
3660 break;
3661 }
3662 }
3663 });
3664
3665 connect(m_FilterManager.get(), &FilterManager::labelsChanged, this, &Align::checkFilter);
3666 connect(m_FilterManager.get(), &FilterManager::positionChanged, this, &Align::checkFilter);
3667}
3668
3669QVariantMap Align::getEffectiveFOV()
3670{
3671 KStarsData::Instance()->userdb()->GetAllEffectiveFOVs(effectiveFOVs);
3672
3673 m_FOVWidth = m_FOVHeight = 0;
3674
3675 for (auto &map : effectiveFOVs)
3676 {
3677 if (map["Profile"].toString() == m_ActiveProfile->name && map["Train"].toString() == opticalTrain())
3678 {
3679 if (isEqual(map["Width"].toInt(), m_CameraWidth) &&
3680 isEqual(map["Height"].toInt(), m_CameraHeight) &&
3681 isEqual(map["PixelW"].toDouble(), m_CameraPixelWidth) &&
3682 isEqual(map["PixelH"].toDouble(), m_CameraPixelHeight) &&
3683 isEqual(map["FocalLength"].toDouble(), m_FocalLength) &&
3684 isEqual(map["FocalRedcuer"].toDouble(), m_Reducer) &&
3685 isEqual(map["FocalRatio"].toDouble(), m_FocalRatio))
3686 {
3687 m_FOVWidth = map["FovW"].toDouble();
3688 m_FOVHeight = map["FovH"].toDouble();
3689 return map;
3690 }
3691 }
3692 }
3693
3694 return QVariantMap();
3695}
3696
3697void Align::saveNewEffectiveFOV(double newFOVW, double newFOVH)
3698{
3699 if (newFOVW < 0 || newFOVH < 0 || (isEqual(newFOVW, m_FOVWidth) && isEqual(newFOVH, m_FOVHeight)))
3700 return;
3701
3702 QVariantMap effectiveMap = getEffectiveFOV();
3703
3704 // If ID exists, delete it first.
3705 if (effectiveMap.isEmpty() == false)
3706 KStarsData::Instance()->userdb()->DeleteEffectiveFOV(effectiveMap["id"].toString());
3707
3708 // If FOV is 0x0, then we just remove existing effective FOV
3709 if (newFOVW == 0.0 && newFOVH == 0.0)
3710 {
3711 calculateFOV();
3712 return;
3713 }
3714
3715 effectiveMap["Profile"] = m_ActiveProfile->name;
3716 effectiveMap["Train"] = opticalTrainCombo->currentText();
3717 effectiveMap["Width"] = m_CameraWidth;
3718 effectiveMap["Height"] = m_CameraHeight;
3719 effectiveMap["PixelW"] = m_CameraPixelWidth;
3720 effectiveMap["PixelH"] = m_CameraPixelHeight;
3721 effectiveMap["FocalLength"] = m_FocalLength;
3722 effectiveMap["FocalReducer"] = m_Reducer;
3723 effectiveMap["FocalRatio"] = m_FocalRatio;
3724 effectiveMap["FovW"] = newFOVW;
3725 effectiveMap["FovH"] = newFOVH;
3726
3727 KStarsData::Instance()->userdb()->AddEffectiveFOV(effectiveMap);
3728
3729 calculateFOV();
3730
3731}
3732
3733void Align::zoomAlignView()
3734{
3735 m_AlignView->ZoomDefault();
3736
3737 // Frame update is not immediate to reduce too many refreshes
3738 // So emit updated frame in 500ms
3739 QTimer::singleShot(500, this, [this]()
3740 {
3741 emit newFrame(m_AlignView);
3742 });
3743}
3744
3745void Align::setAlignZoom(double scale)
3746{
3747 if (scale > 1)
3748 m_AlignView->ZoomIn();
3749 else if (scale < 1)
3750 m_AlignView->ZoomOut();
3751
3752 // Frame update is not immediate to reduce too many refreshes
3753 // So emit updated frame in 500ms
3754 QTimer::singleShot(500, this, [this]()
3755 {
3756 emit newFrame(m_AlignView);
3757 });
3758}
3759
3760void Align::syncFOV()
3761{
3762 QString newFOV = FOVOut->text();
3763 QRegularExpression re("(\\d+\\.*\\d*)\\D*x\\D*(\\d+\\.*\\d*)");
3764 QRegularExpressionMatch match = re.match(newFOV);
3765 if (match.hasMatch())
3766 {
3767 double newFOVW = match.captured(1).toDouble();
3768 double newFOVH = match.captured(2).toDouble();
3769
3770 //if (newFOVW > 0 && newFOVH > 0)
3771 saveNewEffectiveFOV(newFOVW, newFOVH);
3772
3773 FOVOut->setStyleSheet(QString());
3774 }
3775 else
3776 {
3777 KSNotification::error(i18n("Invalid FOV."));
3778 FOVOut->setStyleSheet("background-color:red");
3779 }
3780}
3781
3782// m_wasSlewStarted can't be false for more than 10s after a slew starts.
3783bool Align::didSlewStart()
3784{
3785 if (m_wasSlewStarted)
3786 return true;
3787 if (slewStartTimer.isValid() && slewStartTimer.elapsed() > MAX_WAIT_FOR_SLEW_START_MSEC)
3788 {
3789 qCDebug(KSTARS_EKOS_ALIGN) << "Slew timed out...waited > 10s, it must have started already.";
3790 return true;
3791 }
3792 return false;
3793}
3794
3795void Align::setTargetCoords(double ra0, double de0)
3796{
3797 SkyPoint target;
3798 target.setRA0(ra0);
3799 target.setDec0(de0);
3800 target.updateCoordsNow(KStarsData::Instance()->updateNum());
3801 setTarget(target);
3802}
3803
3804void Align::setTarget(const SkyPoint &targetCoord)
3805{
3806 m_TargetCoord = targetCoord;
3807 qCInfo(KSTARS_EKOS_ALIGN) << "Target coordinates updated to JNow RA:" << m_TargetCoord.ra().toHMSString()
3808 << "DE:" << m_TargetCoord.dec().toDMSString();
3809}
3810
3811QList<double> Align::getTargetCoords()
3812{
3813 return QList<double>() << m_TargetCoord.ra0().Hours() << m_TargetCoord.dec0().Degrees();
3814}
3815
3816void Align::setTargetPositionAngle(double value)
3817{
3818 m_TargetPositionAngle = value;
3819 qCDebug(KSTARS_EKOS_ALIGN) << "Target PA updated to: " << m_TargetPositionAngle;
3820}
3821
3822void Align::calculateAlignTargetDiff()
3823{
3824 if (matchPAHStage(PAA::PAH_FIRST_CAPTURE) ||
3825 matchPAHStage(PAA::PAH_SECOND_CAPTURE) ||
3826 matchPAHStage(PAA::PAH_THIRD_CAPTURE) ||
3827 matchPAHStage(PAA::PAH_FIRST_SOLVE) ||
3828 matchPAHStage(PAA::PAH_SECOND_SOLVE) ||
3829 matchPAHStage(PAA::PAH_THIRD_SOLVE) ||
3830 nothingR->isChecked() ||
3831 syncR->isChecked())
3832 return;
3833
3834 if (!Options::astrometryDifferentialSlewing()) // Normal align: Target coords are destinations coords
3835 {
3836 m_TargetDiffRA = (m_AlignCoord.ra().deltaAngle(m_TargetCoord.ra())).Degrees() * 3600; // arcsec
3837 m_TargetDiffDE = (m_AlignCoord.dec().deltaAngle(m_TargetCoord.dec())).Degrees() * 3600; // arcsec
3838 }
3839 else // Differential slewing: Target coords are new position coords
3840 {
3841 m_TargetDiffRA = (m_AlignCoord.ra().deltaAngle(m_DestinationCoord.ra())).Degrees() * 3600; // arcsec
3842 m_TargetDiffDE = (m_AlignCoord.dec().deltaAngle(m_DestinationCoord.dec())).Degrees() * 3600; // arcsec
3843 qCDebug(KSTARS_EKOS_ALIGN) << "Differential slew - Solution RA:" << m_AlignCoord.ra().toHMSString()
3844 << " DE:" << m_AlignCoord.dec().toDMSString();
3845 qCDebug(KSTARS_EKOS_ALIGN) << "Differential slew - Destination RA:" << m_DestinationCoord.ra().toHMSString()
3846 << " DE:" << m_DestinationCoord.dec().toDMSString();
3847 }
3848
3849 m_TargetDiffTotal = sqrt(m_TargetDiffRA * m_TargetDiffRA + m_TargetDiffDE * m_TargetDiffDE);
3850
3851 errOut->setText(QString("%1 arcsec. RA:%2 DE:%3").arg(
3852 QString::number(m_TargetDiffTotal, 'f', 0),
3853 QString::number(m_TargetDiffRA, 'f', 0),
3854 QString::number(m_TargetDiffDE, 'f', 0)));
3855 if (m_TargetDiffTotal <= static_cast<double>(alignAccuracyThreshold->value()))
3856 errOut->setStyleSheet("color:green");
3857 else if (m_TargetDiffTotal < 1.5 * alignAccuracyThreshold->value())
3858 errOut->setStyleSheet("color:yellow");
3859 else
3860 errOut->setStyleSheet("color:red");
3861
3862 //This block of code will write the result into the solution table and plot it on the graph.
3863 int currentRow = solutionTable->rowCount() - 1;
3864 QTableWidgetItem *dRAReport = new QTableWidgetItem();
3865 if (dRAReport)
3866 {
3867 dRAReport->setText(QString::number(m_TargetDiffRA, 'f', 3) + "\"");
3869 dRAReport->setFlags(Qt::ItemIsSelectable);
3870 solutionTable->setItem(currentRow, 4, dRAReport);
3871 }
3872
3873 QTableWidgetItem *dDECReport = new QTableWidgetItem();
3874 if (dDECReport)
3875 {
3876 dDECReport->setText(QString::number(m_TargetDiffDE, 'f', 3) + "\"");
3878 dDECReport->setFlags(Qt::ItemIsSelectable);
3879 solutionTable->setItem(currentRow, 5, dDECReport);
3880 }
3881
3882 double raPlot = m_TargetDiffRA;
3883 double decPlot = m_TargetDiffDE;
3884 alignPlot->graph(0)->addData(raPlot, decPlot);
3885
3886 QCPItemText *textLabel = new QCPItemText(alignPlot);
3888
3889 textLabel->position->setType(QCPItemPosition::ptPlotCoords);
3890 textLabel->position->setCoords(raPlot, decPlot);
3891 textLabel->setColor(Qt::red);
3892 textLabel->setPadding(QMargins(0, 0, 0, 0));
3893 textLabel->setBrush(Qt::white);
3894 textLabel->setPen(Qt::NoPen);
3895 textLabel->setText(' ' + QString::number(solutionTable->rowCount()) + ' ');
3896 textLabel->setFont(QFont(font().family(), 8));
3897
3898 if (!alignPlot->xAxis->range().contains(m_TargetDiffRA))
3899 {
3900 alignPlot->graph(0)->rescaleKeyAxis(true);
3901 alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0);
3902 }
3903 if (!alignPlot->yAxis->range().contains(m_TargetDiffDE))
3904 {
3905 alignPlot->graph(0)->rescaleValueAxis(true);
3906 alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);
3907 }
3908 alignPlot->replot();
3909}
3910
3911QStringList Align::getStellarSolverProfiles()
3912{
3913 QStringList profiles;
3914 for (auto &param : m_StellarSolverProfiles)
3915 profiles << param.listName;
3916
3917 return profiles;
3918}
3919
3920void Align::exportSolutionPoints()
3921{
3922 if (solutionTable->rowCount() == 0)
3923 return;
3924
3925 QUrl exportFile = QFileDialog::getSaveFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Export Solution Points"),
3926 alignURLPath,
3927 "CSV File (*.csv)");
3928 if (exportFile.isEmpty()) // if user presses cancel
3929 return;
3930 if (exportFile.toLocalFile().endsWith(QLatin1String(".csv")) == false)
3931 exportFile.setPath(exportFile.toLocalFile() + ".csv");
3932
3933 QString path = exportFile.toLocalFile();
3934
3935 if (QFile::exists(path))
3936 {
3937 int r = KMessageBox::warningContinueCancel(nullptr,
3938 i18n("A file named \"%1\" already exists. "
3939 "Overwrite it?",
3940 exportFile.fileName()),
3941 i18n("Overwrite File?"), KStandardGuiItem::overwrite());
3942 if (r == KMessageBox::Cancel)
3943 return;
3944 }
3945
3946 if (!exportFile.isValid())
3947 {
3948 QString message = i18n("Invalid URL: %1", exportFile.url());
3949 KSNotification::sorry(message, i18n("Invalid URL"));
3950 return;
3951 }
3952
3953 QFile file;
3954 file.setFileName(path);
3955 if (!file.open(QIODevice::WriteOnly))
3956 {
3957 QString message = i18n("Unable to write to file %1", path);
3958 KSNotification::sorry(message, i18n("Could Not Open File"));
3959 return;
3960 }
3961
3962 QTextStream outstream(&file);
3963
3965
3966 outstream << "RA (J" << epoch << "),DE (J" << epoch
3967 << "),RA (degrees),DE (degrees),Name,RA Error (arcsec),DE Error (arcsec)" << Qt::endl;
3968
3969 for (int i = 0; i < solutionTable->rowCount(); i++)
3970 {
3971 QTableWidgetItem *raCell = solutionTable->item(i, 0);
3972 QTableWidgetItem *deCell = solutionTable->item(i, 1);
3973 QTableWidgetItem *objNameCell = solutionTable->item(i, 2);
3974 QTableWidgetItem *raErrorCell = solutionTable->item(i, 4);
3975 QTableWidgetItem *deErrorCell = solutionTable->item(i, 5);
3976
3977 if (!raCell || !deCell || !objNameCell || !raErrorCell || !deErrorCell)
3978 {
3979 KSNotification::sorry(i18n("Error in table structure."));
3980 return;
3981 }
3982 dms raDMS = dms::fromString(raCell->text(), false);
3983 dms deDMS = dms::fromString(deCell->text(), true);
3984 outstream << raDMS.toHMSString() << ',' << deDMS.toDMSString() << ',' << raDMS.Degrees() << ','
3985 << deDMS.Degrees() << ',' << objNameCell->text() << ',' << raErrorCell->text().remove('\"') << ','
3986 << deErrorCell->text().remove('\"') << Qt::endl;
3987 }
3988 emit newLog(i18n("Solution Points Saved as: %1", path));
3989 file.close();
3990}
3991
3992void Align::setupPolarAlignmentAssistant()
3993{
3994 // Create PAA instance
3995 m_PolarAlignmentAssistant = new PolarAlignmentAssistant(this, m_AlignView);
3996 connect(m_PolarAlignmentAssistant, &Ekos::PAA::captureAndSolve, this, [this]()
3997 {
3998 captureAndSolve(true);
3999 });
4000 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newAlignTableResult, this, &Ekos::Align::setAlignTableResult);
4001 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newFrame, this, &Ekos::Align::newFrame);
4002 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newPAHStage, this, &Ekos::Align::processPAHStage);
4003 connect(m_PolarAlignmentAssistant, &Ekos::PAA::newLog, this, &Ekos::Align::appendLogText);
4004
4005 tabWidget->addTab(m_PolarAlignmentAssistant, i18n("Polar Alignment"));
4006}
4007
4008void Align::setupManualRotator()
4009{
4010 if (m_ManualRotator)
4011 return;
4012
4013 m_ManualRotator = new ManualRotator(this);
4014 connect(m_ManualRotator, &Ekos::ManualRotator::captureAndSolve, this, [this]()
4015 {
4016 captureAndSolve(false);
4017 });
4018 // If user cancel manual rotator, reset load slew target PA, otherwise it will keep popping up
4019 // for any subsequent solves.
4020 connect(m_ManualRotator, &Ekos::ManualRotator::rejected, this, [this]()
4021 {
4022 m_TargetPositionAngle = std::numeric_limits<double>::quiet_NaN();
4023 stop(ALIGN_IDLE);
4024 });
4025}
4026
4027void Align::setuptDarkProcessor()
4028{
4029 if (m_DarkProcessor)
4030 return;
4031
4032 m_DarkProcessor = new DarkProcessor(this);
4033 connect(m_DarkProcessor, &DarkProcessor::newLog, this, &Ekos::Align::appendLogText);
4034 connect(m_DarkProcessor, &DarkProcessor::darkFrameCompleted, this, [this](bool completed)
4035 {
4036 alignDarkFrame->setChecked(completed);
4037 m_AlignView->setProperty("suspended", false);
4038 if (completed)
4039 {
4040 m_AlignView->rescale(ZOOM_KEEP_LEVEL);
4041 m_AlignView->updateFrame();
4042 }
4043 setCaptureComplete();
4044 });
4045}
4046
4047void Align::processPAHStage(int stage)
4048{
4049 switch (stage)
4050 {
4051 case PAA::PAH_IDLE:
4052 // Abort any solver that might be running.
4053 // Assumes this state change won't happen randomly (e.g. in the middle of align).
4054 // Alternatively could just let the stellarsolver finish naturally.
4055 if (m_StellarSolver && m_StellarSolver->isRunning())
4056 m_StellarSolver->abort();
4057 break;
4058 case PAA::PAH_POST_REFRESH:
4059 {
4060 Options::setAstrometrySolverWCS(rememberSolverWCS);
4061 Options::setAutoWCS(rememberAutoWCS);
4062 stop(ALIGN_IDLE);
4063 }
4064 break;
4065
4066 case PAA::PAH_FIRST_CAPTURE:
4067 nothingR->setChecked(true);
4068 m_CurrentGotoMode = GOTO_NOTHING;
4069 loadSlewB->setEnabled(false);
4070
4071 rememberSolverWCS = Options::astrometrySolverWCS();
4072 rememberAutoWCS = Options::autoWCS();
4073
4074 Options::setAutoWCS(false);
4075 Options::setAstrometrySolverWCS(true);
4076 break;
4077 case PAA::PAH_SECOND_CAPTURE:
4078 case PAA::PAH_THIRD_CAPTURE:
4079 if (alignSettlingTime->value() >= DELAY_THRESHOLD_NOTIFY)
4080 emit newLog(i18n("Settling..."));
4081 m_CaptureTimer.start(alignSettlingTime->value());
4082 break;
4083
4084 default:
4085 break;
4086 }
4087
4088 emit newPAAStage(stage);
4089}
4090
4091bool Align::matchPAHStage(uint32_t stage)
4092{
4093 return m_PolarAlignmentAssistant && m_PolarAlignmentAssistant->getPAHStage() == stage;
4094}
4095
4096void Align::toggleManualRotator(bool toggled)
4097{
4098 if (toggled)
4099 {
4100 m_ManualRotator->show();
4101 m_ManualRotator->raise();
4102 }
4103 else
4104 m_ManualRotator->close();
4105}
4106
4107void Align::setupOpticalTrainManager()
4108{
4109 connect(OpticalTrainManager::Instance(), &OpticalTrainManager::updated, this, &Align::refreshOpticalTrain);
4110 connect(trainB, &QPushButton::clicked, this, [this]()
4111 {
4112 OpticalTrainManager::Instance()->openEditor(opticalTrainCombo->currentText());
4113 });
4114 connect(opticalTrainCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index)
4115 {
4116 ProfileSettings::Instance()->setOneSetting(ProfileSettings::AlignOpticalTrain,
4117 OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(index)));
4118 refreshOpticalTrain();
4119 emit trainChanged();
4120 });
4121}
4122
4123void Align::refreshOpticalTrain()
4124{
4125 opticalTrainCombo->blockSignals(true);
4126 opticalTrainCombo->clear();
4127 opticalTrainCombo->addItems(OpticalTrainManager::Instance()->getTrainNames());
4128 trainB->setEnabled(true);
4129
4130 QVariant trainID = ProfileSettings::Instance()->getOneSetting(ProfileSettings::AlignOpticalTrain);
4131
4132 if (trainID.isValid())
4133 {
4134 auto id = trainID.toUInt();
4135
4136 // If train not found, select the first one available.
4137 if (OpticalTrainManager::Instance()->exists(id) == false)
4138 {
4139 qCWarning(KSTARS_EKOS_ALIGN) << "Optical train doesn't exist for id" << id;
4140 id = OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(0));
4141 }
4142
4143 auto name = OpticalTrainManager::Instance()->name(id);
4144
4145 opticalTrainCombo->setCurrentText(name);
4146
4147 auto scope = OpticalTrainManager::Instance()->getScope(name);
4148 m_FocalLength = scope["focal_length"].toDouble(-1);
4149 m_Aperture = scope["aperture"].toDouble(-1);
4150 m_FocalRatio = scope["focal_ratio"].toDouble(-1);
4151 m_Reducer = OpticalTrainManager::Instance()->getReducer(name);
4152
4153 // DSLR Lens Aperture
4154 if (m_Aperture < 0 && m_FocalRatio > 0)
4155 m_Aperture = m_FocalLength / m_FocalRatio;
4156
4157 auto mount = OpticalTrainManager::Instance()->getMount(name);
4158 setMount(mount);
4159
4160 auto camera = OpticalTrainManager::Instance()->getCamera(name);
4161 if (camera)
4162 {
4163 camera->setScopeInfo(m_FocalLength * m_Reducer, m_Aperture);
4164 opticalTrainCombo->setToolTip(QString("%1 @ %2").arg(camera->getDeviceName(), scope["name"].toString()));
4165 }
4166 setCamera(camera);
4167
4168 syncTelescopeInfo();
4169
4170 auto filterWheel = OpticalTrainManager::Instance()->getFilterWheel(name);
4171 setFilterWheel(filterWheel);
4172
4173 auto rotator = OpticalTrainManager::Instance()->getRotator(name);
4174 setRotator(rotator);
4175
4176 // Load train settings
4177 OpticalTrainSettings::Instance()->setOpticalTrainID(id);
4178 auto settings = OpticalTrainSettings::Instance()->getOneSetting(OpticalTrainSettings::Align);
4179 if (settings.isValid())
4180 {
4181 auto map = settings.toJsonObject().toVariantMap();
4182 if (map != m_Settings)
4183 {
4184 m_Settings.clear();
4185 setAllSettings(map);
4186 }
4187 }
4188 else
4189 m_Settings = m_GlobalSettings;
4190
4191 // Need to save information used for Mosaic planner
4192 Options::setTelescopeFocalLength(m_FocalLength);
4193 Options::setCameraPixelWidth(m_CameraPixelWidth);
4194 Options::setCameraPixelHeight(m_CameraPixelHeight);
4195 Options::setCameraWidth(m_CameraWidth);
4196 Options::setCameraHeight(m_CameraHeight);
4197 }
4198
4199 opticalTrainCombo->blockSignals(false);
4200}
4201
4202void Align::syncSettings()
4203{
4204 QDoubleSpinBox *dsb = nullptr;
4205 QSpinBox *sb = nullptr;
4206 QCheckBox *cb = nullptr;
4207 QComboBox *cbox = nullptr;
4208 QRadioButton *cradio = nullptr;
4209
4210 QString key;
4211 QVariant value;
4212
4213 if ( (dsb = qobject_cast<QDoubleSpinBox*>(sender())))
4214 {
4215 key = dsb->objectName();
4216 value = dsb->value();
4217
4218 }
4219 else if ( (sb = qobject_cast<QSpinBox*>(sender())))
4220 {
4221 key = sb->objectName();
4222 value = sb->value();
4223 }
4224 else if ( (cb = qobject_cast<QCheckBox*>(sender())))
4225 {
4226 key = cb->objectName();
4227 value = cb->isChecked();
4228 }
4229 else if ( (cbox = qobject_cast<QComboBox*>(sender())))
4230 {
4231 key = cbox->objectName();
4232 value = cbox->currentText();
4233 }
4234 else if ( (cradio = qobject_cast<QRadioButton*>(sender())))
4235 {
4236 key = cradio->objectName();
4237 // Discard false requests
4238 if (cradio->isChecked() == false)
4239 {
4240 m_Settings.remove(key);
4241 return;
4242 }
4243 value = true;
4244 }
4245
4246 // Save immediately
4247 Options::self()->setProperty(key.toLatin1(), value);
4248
4249 m_Settings[key] = value;
4250 m_GlobalSettings[key] = value;
4251 m_DebounceTimer.start();
4252}
4253
4254///////////////////////////////////////////////////////////////////////////////////////////
4255///
4256///////////////////////////////////////////////////////////////////////////////////////////
4257void Align::settleSettings()
4258{
4259 emit settingsUpdated(getAllSettings());
4260 // Save to optical train specific settings as well
4261 OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
4262 OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Align, m_Settings);
4263}
4264
4265void Align::loadGlobalSettings()
4266{
4267 QString key;
4268 QVariant value;
4269
4270 QVariantMap settings;
4271 // All Combo Boxes
4272 for (auto &oneWidget : findChildren<QComboBox*>())
4273 {
4274 if (oneWidget->objectName() == "opticalTrainCombo")
4275 continue;
4276
4277 key = oneWidget->objectName();
4278 value = Options::self()->property(key.toLatin1());
4279 if (value.isValid() && oneWidget->count() > 0)
4280 {
4281 oneWidget->setCurrentText(value.toString());
4282 settings[key] = value;
4283 }
4284 }
4285
4286 // All Double Spin Boxes
4287 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
4288 {
4289 key = oneWidget->objectName();
4290 value = Options::self()->property(key.toLatin1());
4291 if (value.isValid())
4292 {
4293 oneWidget->setValue(value.toDouble());
4294 settings[key] = value;
4295 }
4296 }
4297
4298 // All Spin Boxes
4299 for (auto &oneWidget : findChildren<QSpinBox*>())
4300 {
4301 key = oneWidget->objectName();
4302 value = Options::self()->property(key.toLatin1());
4303 if (value.isValid())
4304 {
4305 oneWidget->setValue(value.toInt());
4306 settings[key] = value;
4307 }
4308 }
4309
4310 // All Checkboxes
4311 for (auto &oneWidget : findChildren<QCheckBox*>())
4312 {
4313 key = oneWidget->objectName();
4314 value = Options::self()->property(key.toLatin1());
4315 if (value.isValid())
4316 {
4317 oneWidget->setChecked(value.toBool());
4318 settings[key] = value;
4319 }
4320 }
4321
4322 // All Radio buttons
4323 for (auto &oneWidget : findChildren<QRadioButton*>())
4324 {
4325 key = oneWidget->objectName();
4326 value = Options::self()->property(key.toLatin1());
4327 if (value.isValid())
4328 {
4329 oneWidget->setChecked(value.toBool());
4330 settings[key] = value;
4331 }
4332 }
4333
4334 m_GlobalSettings = m_Settings = settings;
4335}
4336
4337
4338void Align::connectSettings()
4339{
4340 // All Combo Boxes
4341 for (auto &oneWidget : findChildren<QComboBox*>())
4342 connect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Align::syncSettings);
4343
4344 // All Double Spin Boxes
4345 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
4346 connect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::Align::syncSettings);
4347
4348 // All Spin Boxes
4349 for (auto &oneWidget : findChildren<QSpinBox*>())
4350 connect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::Align::syncSettings);
4351
4352 // All Checkboxes
4353 for (auto &oneWidget : findChildren<QCheckBox*>())
4354 connect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings);
4355
4356 // All Radio buttons
4357 for (auto &oneWidget : findChildren<QRadioButton*>())
4358 connect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings);
4359
4360 // Train combo box should NOT be synced.
4361 disconnect(opticalTrainCombo, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Align::syncSettings);
4362}
4363
4364void Align::disconnectSettings()
4365{
4366 // All Combo Boxes
4367 for (auto &oneWidget : findChildren<QComboBox*>())
4368 disconnect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Align::syncSettings);
4369
4370 // All Double Spin Boxes
4371 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
4372 disconnect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::Align::syncSettings);
4373
4374 // All Spin Boxes
4375 for (auto &oneWidget : findChildren<QSpinBox*>())
4376 disconnect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::Align::syncSettings);
4377
4378 // All Checkboxes
4379 for (auto &oneWidget : findChildren<QCheckBox*>())
4380 disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings);
4381
4382 // All Radio buttons
4383 for (auto &oneWidget : findChildren<QRadioButton*>())
4384 disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::Align::syncSettings);
4385
4386}
4387
4388///////////////////////////////////////////////////////////////////////////////////////////
4389///
4390///////////////////////////////////////////////////////////////////////////////////////////
4391QVariantMap Align::getAllSettings() const
4392{
4393 QVariantMap settings;
4394
4395 // All Combo Boxes
4396 for (auto &oneWidget : findChildren<QComboBox*>())
4397 settings.insert(oneWidget->objectName(), oneWidget->currentText());
4398
4399 // All Double Spin Boxes
4400 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
4401 settings.insert(oneWidget->objectName(), oneWidget->value());
4402
4403 // All Spin Boxes
4404 for (auto &oneWidget : findChildren<QSpinBox*>())
4405 settings.insert(oneWidget->objectName(), oneWidget->value());
4406
4407 // All Checkboxes
4408 for (auto &oneWidget : findChildren<QCheckBox*>())
4409 settings.insert(oneWidget->objectName(), oneWidget->isChecked());
4410
4411 // All Radio Buttons
4412 for (auto &oneWidget : findChildren<QRadioButton*>())
4413 settings.insert(oneWidget->objectName(), oneWidget->isChecked());
4414
4415 return settings;
4416}
4417
4418///////////////////////////////////////////////////////////////////////////////////////////
4419///
4420///////////////////////////////////////////////////////////////////////////////////////////
4421void Align::setAllSettings(const QVariantMap &settings)
4422{
4423 // Disconnect settings that we don't end up calling syncSettings while
4424 // performing the changes.
4425 disconnectSettings();
4426
4427 for (auto &name : settings.keys())
4428 {
4429 // Combo
4430 auto comboBox = findChild<QComboBox*>(name);
4431 if (comboBox)
4432 {
4433 syncControl(settings, name, comboBox);
4434 continue;
4435 }
4436
4437 // Double spinbox
4438 auto doubleSpinBox = findChild<QDoubleSpinBox*>(name);
4439 if (doubleSpinBox)
4440 {
4441 syncControl(settings, name, doubleSpinBox);
4442 continue;
4443 }
4444
4445 // spinbox
4446 auto spinBox = findChild<QSpinBox*>(name);
4447 if (spinBox)
4448 {
4449 syncControl(settings, name, spinBox);
4450 continue;
4451 }
4452
4453 // checkbox
4454 auto checkbox = findChild<QCheckBox*>(name);
4455 if (checkbox)
4456 {
4457 syncControl(settings, name, checkbox);
4458 continue;
4459 }
4460
4461 // Radio button
4462 auto radioButton = findChild<QRadioButton*>(name);
4463 if (radioButton)
4464 {
4465 syncControl(settings, name, radioButton);
4466 continue;
4467 }
4468 }
4469
4470 // Sync to options
4471 for (auto &key : settings.keys())
4472 {
4473 auto value = settings[key];
4474 // Save immediately
4475 Options::self()->setProperty(key.toLatin1(), value);
4476 Options::self()->save();
4477
4478 m_Settings[key] = value;
4479 m_GlobalSettings[key] = value;
4480 }
4481
4482 emit settingsUpdated(getAllSettings());
4483
4484 // Save to optical train specific settings as well
4485 OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
4486 OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Align, m_Settings);
4487
4488 // Restablish connections
4489 connectSettings();
4490}
4491
4492///////////////////////////////////////////////////////////////////////////////////////////
4493///
4494///////////////////////////////////////////////////////////////////////////////////////////
4495bool Align::syncControl(const QVariantMap &settings, const QString &key, QWidget * widget)
4496{
4497 QSpinBox *pSB = nullptr;
4498 QDoubleSpinBox *pDSB = nullptr;
4499 QCheckBox *pCB = nullptr;
4500 QComboBox *pComboBox = nullptr;
4501 QRadioButton *pRadioButton = nullptr;
4502 bool ok = false;
4503
4504 if ((pSB = qobject_cast<QSpinBox *>(widget)))
4505 {
4506 const int value = settings[key].toInt(&ok);
4507 if (ok)
4508 {
4509 pSB->setValue(value);
4510 return true;
4511 }
4512 }
4513 else if ((pDSB = qobject_cast<QDoubleSpinBox *>(widget)))
4514 {
4515 const double value = settings[key].toDouble(&ok);
4516 if (ok)
4517 {
4518 pDSB->setValue(value);
4519 return true;
4520 }
4521 }
4522 else if ((pCB = qobject_cast<QCheckBox *>(widget)))
4523 {
4524 const bool value = settings[key].toBool();
4525 if (value != pCB->isChecked())
4526 pCB->click();
4527 return true;
4528 }
4529 // ONLY FOR STRINGS, not INDEX
4530 else if ((pComboBox = qobject_cast<QComboBox *>(widget)))
4531 {
4532 const QString value = settings[key].toString();
4533 pComboBox->setCurrentText(value);
4534 return true;
4535 }
4536 else if ((pRadioButton = qobject_cast<QRadioButton *>(widget)))
4537 {
4538 const bool value = settings[key].toBool();
4539 if (value)
4540 pRadioButton->click();
4541 return true;
4542 }
4543
4544 return false;
4545}
4546
4547void Align::setState(AlignState value)
4548{
4549 qCDebug(KSTARS_EKOS_ALIGN) << "Align state changed to" << getAlignStatusString(value);
4550 state = value;
4551};
4552
4553void Align::processCaptureTimeout()
4554{
4555 if (m_CaptureTimeoutCounter++ > 3)
4556 {
4557 appendLogText(i18n("Capture timed out."));
4558 m_CaptureTimer.stop();
4559 abort();
4560 }
4561 else
4562 {
4563 ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
4564 if (targetChip->isCapturing())
4565 {
4566 appendLogText(i18n("Capturing still running, Retrying in %1 seconds...", m_CaptureTimer.interval() / 500));
4567 targetChip->abortExposure();
4568 m_CaptureTimer.start( m_CaptureTimer.interval() * 2);
4569 }
4570 else
4571 {
4572 setAlignTableResult(ALIGN_RESULT_FAILED);
4573 if (m_resetCaptureTimeoutCounter)
4574 {
4575 m_resetCaptureTimeoutCounter = false;
4576 m_CaptureTimeoutCounter = 0;
4577 }
4578 captureAndSolve(false);
4579 }
4580 }
4581}
4582}
void Slew()
Slew the telescope to the solved alignment coordinate.
Definition align.cpp:2941
static QStringList generateRemoteOptions(const QVariantMap &optionsMap)
generateOptions Generate astrometry.net option given the supplied map
Definition align.cpp:1189
void checkCameraExposureProgress(ISD::CameraChip *targetChip, double remaining, IPState state)
checkCameraExposureProgress Track the progress of CCD exposure
Definition align.cpp:3274
QStringList generateRemoteArgs(const QSharedPointer< FITSData > &imageData)
Generate arguments we pass to the remote solver.
Definition align.cpp:1279
Q_SCRIPTABLE bool setFilter(const QString &filter)
DBUS interface function.
Definition align.cpp:3159
void syncCameraInfo()
CCD information is updated, sync them.
Definition align.cpp:857
bool setCamera(ISD::Camera *device)
Add Camera to the list of available Cameras.
Definition align.cpp:638
bool setDome(ISD::Dome *device)
Add new Dome.
Definition align.cpp:745
bool setMount(ISD::Mount *device)
Add new mount.
Definition align.cpp:688
void syncCameraControls()
syncCCDControls Update camera controls like gain, offset, ISO..etc.
Definition align.cpp:908
void checkCamera()
Check CCD and make sure information is updated and FOV is re-calculated.
Definition align.cpp:600
bool isParserOK()
Does our parser exist in the system?
Definition align.cpp:526
Q_SCRIPTABLE QList< double > getSolutionResult()
DBUS interface function.
Definition align.cpp:2589
Q_SCRIPTABLE Q_NOREPLY void startSolving()
DBUS interface function.
Definition align.cpp:1811
void refreshAlignOptions()
refreshAlignOptions is called when settings are updated in OpsAlign.
Definition align.cpp:3471
void getFOVScale(double &fov_w, double &fov_h, double &fov_scale)
getFOVScale Returns calculated FOV values
Definition align.cpp:967
void executeGOTO()
After a solver process is completed successfully, sync, slew to target, or do nothing as set by the u...
Definition align.cpp:2901
bool loadAndSlew(const QByteArray &image, const QString &extension)
DBUS interface function.
Definition align.cpp:3072
void setAstrometryDevice(const QSharedPointer< ISD::GenericDevice > &device)
setAstrometryDevice
Definition align.cpp:3458
void solverFinished(double orientation, double ra, double dec, double pixscale, bool eastToTheRight)
Solver finished successfully, process the data and execute the required actions depending on the mode...
Definition align.cpp:2063
bool checkIfRotationRequired()
checkIfRotationRequired Check whether we need to perform an ALIGN_ROTATING action,...
Definition align.cpp:2424
bool setFilterWheel(ISD::FilterWheel *device)
addFilterWheel Add new filter wheel filter device.
Definition align.cpp:3116
void Sync()
Sync the telescope to the solved alignment coordinate.
Definition align.cpp:2923
void checkFilter()
Check Filter and make sure information is updated accordingly.
Definition align.cpp:3176
void setRotator(ISD::Rotator *device)
Add new Rotator.
Definition align.cpp:3210
void processData(const QSharedPointer< FITSData > &data)
Process new FITS received from CCD.
Definition align.cpp:1690
void getCalculatedFOVScale(double &fov_w, double &fov_h, double &fov_scale)
getCalculatedFOVScale Get calculated FOV scales from the current CCD+Telescope combination.
Definition align.cpp:1001
Q_SCRIPTABLE Q_NOREPLY void setExposure(double value)
DBUS interface function.
Definition align.cpp:3096
void settleSettings()
settleSettings Run this function after timeout from debounce timer to update database and emit settin...
Definition align.cpp:4257
Q_SCRIPTABLE Q_NOREPLY void setSolverAction(int mode)
DBUS interface function.
Definition align.cpp:1805
bool syncTelescopeInfo()
We received new telescope info, process them and update FOV.
Definition align.cpp:814
void updateProperty(INDI::Property prop)
Process updated device properties.
Definition align.cpp:2614
void solverFailed()
Process solver failure.
Definition align.cpp:2343
void SlewToTarget()
Sync the telescope to the solved alignment coordinate, and then slew to the target coordinate.
Definition align.cpp:2970
The PolarAlignmentAssistant class.
RemoteAstrometryParser invokes the remote astrometry.net solver in the remote CCD driver to solve the...
A simple class encapsulating a Field-of-View symbol.
Definition fov.h:28
CameraChip class controls a particular chip in camera.
Camera class controls an INDI Camera device.
Definition indicamera.h:44
INDI::PropertyView< ISwitch > * getSwitch(const QString &name) const
Class handles control of INDI dome devices.
Definition indidome.h:25
device handle controlling Mounts.
Definition indimount.h:29
Rotator class handles control of INDI Rotator devices.
Definition indirotator.h:20
Q_INVOKABLE QAction * action(const QString &name) const
KPageWidgetItem * addPage(QWidget *page, const QString &itemName, const QString &pixmapName=QString(), const QString &header=QString(), bool manage=true)
static KConfigDialog * exists(const QString &name)
void setCurrentPage(KPageWidgetItem *item)
void setIcon(const QIcon &icon)
const KStarsDateTime & lt() const
Definition kstarsdata.h:151
KSUserDB * userdb()
Definition kstarsdata.h:215
void addTransientFOV(std::shared_ptr< FOV > newFOV)
addTransientFOV Adds a new FOV to the list.
Definition kstarsdata.h:323
SkyMapComposite * skyComposite()
Definition kstarsdata.h:166
static KStarsDateTime currentDateTime()
static KStars * Instance()
Definition kstars.h:123
virtual KActionCollection * actionCollection() const
The abstract base class for all items in a plot.
void setBrush(const QBrush &brush)
void setPen(const QPen &pen)
Holds the data of one single data point for QCPCurve.
A plottable representing a parametric curve in a plot.
@ lsNone
No line is drawn between data points (e.g. only scatters)
void setLineStyle(LineStyle style)
void setScatterStyle(const QCPScatterStyle &style)
QSharedPointer< QCPCurveDataContainer > data() const
void setScatterSkip(int skip)
@ lsNone
data points are not connected with any lines (e.g.
void setType(PositionType type)
void setCoords(double key, double value)
@ ptPlotCoords
Dynamic positioning at a plot coordinate defined by two axes (see setAxes).
A text label.
void setBrush(const QBrush &brush)
void setText(const QString &text)
void setPositionAlignment(Qt::Alignment alignment)
void setFont(const QFont &font)
void setPen(const QPen &pen)
void setColor(const QColor &color)
void setPadding(const QMargins &padding)
Represents the visual appearance of scatter points.
@ ssDisc
\enumimage{ssDisc.png} a circle which is filled with the pen's color (not the brush as with ssCircle)
void mouseMove(QMouseEvent *event)
The QProgressIndicator class lets an application display a progress indicator to show that a long tas...
void stopAnimation()
Stops the spin animation.
void startAnimation()
Starts the spin animation.
bool isAnimated() const
Returns a Boolean value indicating whether the component is currently animated.
SkyObject * objectNearest(SkyPoint *p, double &maxrad) override
Provides all necessary information about an object in the sky: its coordinates, name(s),...
Definition skyobject.h:42
virtual QString longname(void) const
Definition skyobject.h:164
The sky coordinates of a point in the sky.
Definition skypoint.h:45
void apparentCoord(long double jd0, long double jdf)
Computes the apparent coordinates for this SkyPoint for any epoch, accounting for the effects of prec...
Definition skypoint.cpp:700
const CachingDms & dec() const
Definition skypoint.h:269
void setDec(dms d)
Sets Dec, the current Declination.
Definition skypoint.h:169
virtual void updateCoordsNow(const KSNumbers *num)
updateCoordsNow Shortcut for updateCoords( const KSNumbers *num, false, nullptr, nullptr,...
Definition skypoint.h:382
void setRA(dms &r)
Sets RA, the current Right Ascension.
Definition skypoint.h:144
const CachingDms & ra() const
Definition skypoint.h:263
void EquatorialToHorizontal(const CachingDms *LST, const CachingDms *lat)
Determine the (Altitude, Azimuth) coordinates of the SkyPoint from its (RA, Dec) coordinates,...
Definition skypoint.cpp:77
void setRA0(dms r)
Sets RA0, the catalog Right Ascension.
Definition skypoint.h:94
void setDec0(dms d)
Sets Dec0, the catalog Declination.
Definition skypoint.h:119
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
static dms fromString(const QString &s, bool deg)
Static function to create a DMS object from a QString.
Definition dms.cpp:429
double Hours() const
Definition dms.h:168
virtual void setH(const double &x)
Sets floating-point value of angle, in hours.
Definition dms.h:210
int second() const
Definition dms.cpp:231
const QString toDMSString(const bool forceSign=false, const bool machineReadable=false, const bool highPrecision=false) const
Definition dms.cpp:287
const dms deltaAngle(dms angle) const
deltaAngle Return the shortest difference (path) between this angle and the supplied angle.
Definition dms.cpp:267
int minute() const
Definition dms.cpp:221
const QString toHMSString(const bool machineReadable=false, const bool highPrecision=false) const
Definition dms.cpp:378
int degree() const
Definition dms.h:116
int arcmin() const
Definition dms.cpp:180
int arcsec() const
Definition dms.cpp:193
int hour() const
Definition dms.h:147
virtual void setD(const double &x)
Sets floating-point value of angle, in degrees.
Definition dms.h:179
const double & Degrees() const
Definition dms.h:141
static constexpr double DegToRad
DegToRad is a const static member equal to the number of radians in one degree (dms::PI/180....
Definition dms.h:390
Q_SCRIPTABLE Q_NOREPLY void setSolverMode(int mode)
DBUS interface function.
Definition align.cpp:557
void stop(Ekos::AlignState mode)
Stop aligning.
Definition align.cpp:2503
Q_SCRIPTABLE Q_NOREPLY void setBinningIndex(int binIndex)
DBUS interface function.
Definition align.cpp:3101
Q_SCRIPTABLE bool captureAndSolve(bool initialCall=true)
DBUS interface function.
Definition align.cpp:1412
void suspend()
Suspend aligning, recovery handled by the align module itself.
Definition align.h:408
Q_SCRIPTABLE Q_NOREPLY void abort()
DBUS interface function.
Definition align.h:400
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
char * toString(const EngineQuery &query)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:79
AlignState
Definition ekos.h:145
@ ALIGN_FAILED
Alignment failed.
Definition ekos.h:148
@ ALIGN_PROGRESS
Alignment operation in progress.
Definition ekos.h:150
@ ALIGN_SUCCESSFUL
Alignment Astrometry solver successfully solved the image.
Definition ekos.h:151
@ ALIGN_SLEWING
Slewing mount to target coordinates.
Definition ekos.h:153
@ ALIGN_ABORTED
Alignment aborted by user or agent.
Definition ekos.h:149
@ ALIGN_SYNCING
Syncing mount to solution coordinates.
Definition ekos.h:152
@ ALIGN_IDLE
No ongoing operations.
Definition ekos.h:146
@ ALIGN_COMPLETE
Alignment successfully completed.
Definition ekos.h:147
@ ALIGN_SUSPENDED
Alignment operations suspended.
Definition ekos.h:155
@ ALIGN_ROTATING
Rotating (Automatic or Manual) to target position angle.
Definition ekos.h:154
@ CAPTURE_MERIDIAN_FLIP
Definition ekos.h:111
@ CAPTURE_ALIGNING
Definition ekos.h:109
ISD is a collection of INDI Standard Devices.
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
QString name(GameStandardAction id)
KIOCORE_EXPORT SimpleJob * mount(bool ro, const QByteArray &fstype, const QString &dev, const QString &point, JobFlags flags=DefaultFlags)
QString path(const QString &relativePath)
ButtonCode warningContinueCancel(QWidget *parent, const QString &text, const QString &title=QString(), const KGuiItem &buttonContinue=KStandardGuiItem::cont(), const KGuiItem &buttonCancel=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Notify)
KIOCORE_EXPORT QString dir(const QString &fileClass)
KGuiItem overwrite()
KGuiItem insert()
KGuiItem stop()
@ iRangeDrag
0x001 Axis ranges are draggable (see QCPAxisRect::setRangeDrag, QCPAxisRect::setRangeDragAxes)
@ iRangeZoom
0x002 Axis ranges are zoomable with the mouse wheel (see QCPAxisRect::setRangeZoom,...
bool isChecked() const const
void clicked(bool checked)
void toggled(bool checked)
void editingFinished()
void setEnabled(bool)
void addWidget(QWidget *widget, int stretch, Qt::Alignment alignment)
void buttonToggled(QAbstractButton *button, bool checked)
void idToggled(int id, bool checked)
const char * constData() const const
void activated(int index)
void currentIndexChanged(int index)
QString toString(QStringView format, QCalendar cal) const const
bool registerObject(const QString &path, QObject *object, RegisterOptions options)
QDBusConnection sessionBus()
void accepted()
void rejected()
QString filePath(const QString &fileName) const const
QString homePath()
QString tempPath()
qint64 elapsed() const const
bool exists() const const
bool open(FILE *fh, OpenMode mode, FileHandleFlags handleFlags)
void setFileName(const QString &name)
virtual void close() override
QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, Options options)
QUrl getSaveFileUrl(QWidget *parent, const QString &caption, const QUrl &dir, const QString &filter, QString *selectedFilter, Options options, const QStringList &supportedSchemes)
QString absolutePath() const const
bool exists(const QString &path)
QIcon fromTheme(const QString &name)
QVariantMap toVariantMap() const const
void editingFinished()
const_reference at(qsizetype i) const const
void clear()
qsizetype count() const const
iterator insert(const_iterator before, parameter_type value)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
bool disconnect(const QMetaObject::Connection &connection)
T qobject_cast(QObject *object)
QObject * sender() const const
T * get() const const
bool isNull() const const
void valueChanged(int i)
void splitterMoved(int pos, int index)
QString & append(QChar ch)
QString arg(Args &&... args) const const
void clear()
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString number(double n, char format, int precision)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
double toDouble(bool *ok) const const
int toInt(bool *ok, int base) const const
QByteArray toLatin1() const const
AlignHCenter
UniqueConnection
ItemIsSelectable
WA_LayoutUsesWidgetRect
QTextStream & bin(QTextStream &stream)
QTextStream & endl(QTextStream &stream)
void cellClicked(int row, int column)
void setFlags(Qt::ItemFlags flags)
void setIcon(const QIcon &icon)
void setText(const QString &text)
void setTextAlignment(Qt::Alignment alignment)
QString text() const const
QFuture< void > map(Iterator begin, Iterator end, MapFunctor &&function)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void setInterval(int msec)
void start()
void stop()
void timeout()
void showText(const QPoint &pos, const QString &text, QWidget *w, const QRect &rect, int msecDisplayTime)
QString fileName(ComponentFormattingOptions options) const const
QUrl fromLocalFile(const QString &localFile)
bool isEmpty() const const
bool isValid() const const
void setPath(const QString &path, ParsingMode mode)
QString toLocalFile() const const
QString url(FormattingOptions options) const const
QUuid createUuid()
bool isValid() const const
void setValue(QVariant &&value)
bool toBool() const const
double toDouble(bool *ok) const const
int toInt(bool *ok) const const
QString toString() const const
uint toUInt(bool *ok) const const
QWidget(QWidget *parent, Qt::WindowFlags f)
void setEnabled(bool)
virtual bool event(QEvent *event) override
void raise()
void repaint()
void show()
void setWindowFlags(Qt::WindowFlags type)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:59:51 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.