Kstars

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

KDE's Doxygen guidelines are available online.