align.cpp 133 KB
Newer Older
Alexander Lohnau's avatar
Alexander Lohnau committed
1
2
3
4
5
/*
    SPDX-FileCopyrightText: 2013 Jasem Mutlaq <mutlaqja@ikarustech.com>
    SPDX-FileCopyrightText: 2013-2021 Jasem Mutlaq <mutlaqja@ikarustech.com>
    SPDX-FileCopyrightText: 2018-2020 Robert Lancaster <rlancaste@gmail.com>
    SPDX-FileCopyrightText: 2019-2021 Hy Murveit <hy@murveit.com>
6

Alexander Lohnau's avatar
Alexander Lohnau committed
7
8
    SPDX-License-Identifier: GPL-2.0-or-later
*/
9

10
#include "align.h"
11
#include "alignadaptor.h"
12
#include "alignview.h"
13
14
15
16
#include <ekos_align_debug.h>

// Options
#include "Options.h"
17
#include "opsalign.h"
18
#include "opsprograms.h"
19
#include "opsastrometry.h"
20
#include "opsastrometryindexfiles.h"
21
22

// Components
23
#include "mountmodel.h"
24
#include "polaralignmentassistant.h"
25
#include "remoteastrometryparser.h"
26
#include "polaralign.h"
27
#include "manualrotator.h"
28
29
30
31
32
33

// FITS
#include "fitsviewer/fitsdata.h"
#include "fitsviewer/fitstab.h"

// Auxiliary
34
#include "auxiliary/QProgressIndicator.h"
35
#include "auxiliary/ksmessagebox.h"
36
#include "ekos/auxiliary/stellarsolverprofileeditor.h"
37
38
39
40
41
42
43
44
45
46
#include "dialogs/finddialog.h"
#include "ksnotification.h"
#include "kspaths.h"
#include "fov.h"
#include "kstars.h"
#include "kstarsdata.h"
#include "skymapcomposite.h"

// INDI
#include "ekos/manager.h"
47
48
#include "indi/clientmanager.h"
#include "indi/driverinfo.h"
49
#include "indi/indifilter.h"
50
#include "profileinfo.h"
51

52
// System Includes
53
#include <KActionCollection>
Csaba Kertesz's avatar
Csaba Kertesz committed
54
#include <basedevice.h>
55
#include <indicom.h>
56
57
#include <memory>

58
#define MAXIMUM_SOLVER_ITERATIONS 10
59
#define CAPTURE_RETRY_DELAY       10000
60
61
62
63
64
#define PAH_CUTOFF_FOV            10 // Minimum FOV width in arcminutes for PAH to work
#define CHECK_PAH(x) \
    m_PolarAlignmentAssistant && m_PolarAlignmentAssistant->x
#define RUN_PAH(x) \
    if (m_PolarAlignmentAssistant) m_PolarAlignmentAssistant->x
65
66
67

namespace Ekos
{
68

69
using PAA = PolarAlignmentAssistant;
70

71
Align::Align(ProfileInfo *activeProfile) : m_ActiveProfile(activeProfile)
72
73
{
    setupUi(this);
74
75
76
77

    qRegisterMetaType<Ekos::AlignState>("Ekos::AlignState");
    qDBusRegisterMetaType<Ekos::AlignState>();

78
    new AlignAdaptor(this);
79
    QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Align", this);
80
81
82

    dirPath = QDir::homePath();

83
84
    KStarsData::Instance()->clearTransientFOVs();

85
    //loadSlewMode = false;
86
    solverFOV.reset(new FOV());
87
    solverFOV->setName(i18n("Solver FOV"));
88
    solverFOV->setObjectName("solver_fov");
89
    solverFOV->setLockCelestialPole(true);
90
    solverFOV->setColor(KStars::Instance()->data()->colorScheme()->colorNamed("SolverFOVColor").name());
91
92
    solverFOV->setProperty("visible", false);
    KStarsData::Instance()->addTransientFOV(solverFOV);
93

94
    sensorFOV.reset(new FOV());
95
    sensorFOV->setObjectName("sensor_fov");
96
    sensorFOV->setLockCelestialPole(true);
97
98
    sensorFOV->setProperty("visible", Options::showSensorFOV());
    KStarsData::Instance()->addTransientFOV(sensorFOV);
99
100
101
102
103

    QAction *a = KStars::Instance()->actionCollection()->action("show_sensor_fov");
    if (a)
        a->setEnabled(true);

104
    showFITSViewerB->setIcon(
105
        QIcon::fromTheme("kstars_fitsviewer"));
106
    showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
107
    connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Align::showFITSViewer);
108

109
    toggleFullScreenB->setIcon(
110
        QIcon::fromTheme("view-fullscreen"));
111
    toggleFullScreenB->setShortcut(Qt::Key_F4);
112
    toggleFullScreenB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
113
    connect(toggleFullScreenB, &QPushButton::clicked, this, &Ekos::Align::toggleAlignWidgetFullScreen);
114

115
    alignView = new AlignView(alignWidget, FITS_ALIGN);
116
117
118
    alignView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    alignView->setBaseSize(alignWidget->size());
    alignView->createFloatingToolBar();
119
    QVBoxLayout *vlayout = new QVBoxLayout();
Csaba Kertesz's avatar
Csaba Kertesz committed
120

121
122
123
    vlayout->addWidget(alignView);
    alignWidget->setLayout(vlayout);

124
125
    connect(solveB, &QPushButton::clicked, [this]()
    {
126
        updateTargetCoords();
127
128
        captureAndSolve();
    });
129
    connect(stopB, &QPushButton::clicked, this, &Ekos::Align::abort);
130

131
    // Effective FOV Edit
132
    connect(FOVOut, &QLineEdit::editingFinished, this, &Align::syncFOV);
133

134
135
    connect(CCDCaptureCombo, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated), this,
            &Ekos::Align::setDefaultCCD);
136
    connect(CCDCaptureCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &Ekos::Align::checkCCD);
137

138
139
    connect(loadSlewB, &QPushButton::clicked, [&]()
    {
140
141
        loadAndSlew();
    });
142

143
144
    FilterDevicesCombo->addItem("--");
    connect(FilterDevicesCombo, static_cast<void(QComboBox::*)(const QString &)>(&QComboBox::activated),
145
            [ = ](const QString & text)
146
    {
147
        syncSettings();
148
        Options::setDefaultAlignFilterWheel(text);
149
150
    });

151
    connect(FilterDevicesCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &Ekos::Align::checkFilter);
152
153

    connect(FilterPosCombo, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated),
154
            [ = ](int index)
155
    {
156
        syncSettings();
157
158
        Options::setLockAlignFilterIndex(index);
    }
159
           );
160

161
162
163
164
    gotoModeButtonGroup->setId(syncR, GOTO_SYNC);
    gotoModeButtonGroup->setId(slewR, GOTO_SLEW);
    gotoModeButtonGroup->setId(nothingR, GOTO_NOTHING);

165
    connect(gotoModeButtonGroup, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), this,
166
167
            [ = ](int id)
    {
168
        this->m_CurrentGotoMode = static_cast<GotoMode>(id);
169
    });
170

171
    m_CaptureTimer.setSingleShot(true);
172
    m_CaptureTimer.setInterval(CAPTURE_RETRY_DELAY);
173
174
    connect(&m_CaptureTimer, &QTimer::timeout, [&]()
    {
175
176
177
        if (m_CaptureTimeoutCounter++ > 3)
        {
            appendLogText(i18n("Capture timed out."));
178
            m_CaptureTimer.stop();
179
180
181
182
183
184
185
            abort();
        }
        else
        {
            ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD);
            if (targetChip->isCapturing())
            {
186
                appendLogText(i18n("Capturing still running, Retrying in %1 seconds...", m_CaptureTimer.interval() / 500));
187
                targetChip->abortExposure();
188
                m_CaptureTimer.start( m_CaptureTimer.interval() * 2);
189
190
            }
            else
191
192
            {
                setAlignTableResult(ALIGN_RESULT_FAILED);
193
                captureAndSolve();
194
            }
195
196
197
198
199
200
        }
    });

    m_AlignTimer.setSingleShot(true);
    m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000);
    connect(&m_AlignTimer, &QTimer::timeout, this, &Ekos::Align::checkAlignmentTimeout);
201

202
203
    m_CurrentGotoMode = static_cast<GotoMode>(Options::solverGotoOption());
    gotoModeButtonGroup->button(m_CurrentGotoMode)->setChecked(true);
204

205
    KConfigDialog *dialog = new KConfigDialog(this, "alignsettings", Options::self());
206

207
#ifdef Q_OS_OSX
208
    dialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
209
210
#endif

211
    opsAlign = new OpsAlign(this);
212
    connect(opsAlign, &OpsAlign::settingsUpdated, this, &Ekos::Align::refreshAlignOptions);
213
214
215
216
217
    KPageWidgetItem *page = dialog->addPage(opsAlign, i18n("StellarSolver Options"));
    page->setIcon(QIcon(":/icons/StellarSolverIcon.png"));

    opsPrograms = new OpsPrograms(this);
    page = dialog->addPage(opsPrograms, i18n("External & Online Programs"));
218
    page->setIcon(QIcon(":/icons/astrometry.svg"));
219
220

    opsAstrometry = new OpsAstrometry(this);
221
222
223
    page = dialog->addPage(opsAstrometry, i18n("Scale & Position"));
    page->setIcon(QIcon(":/icons/center_telescope_red.svg"));

224
    optionsProfileEditor = new StellarSolverProfileEditor(this, Ekos::AlignProfiles, dialog);
225
    page = dialog->addPage(optionsProfileEditor, i18n("Align Options Profiles Editor"));
226
    connect(optionsProfileEditor, &StellarSolverProfileEditor::optionsProfilesUpdated, this, [this]()
227
    {
228
        if(QFile(savedOptionsProfiles).exists())
229
            m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
230
        else
231
            m_StellarSolverProfiles = getDefaultAlignOptionsProfiles();
232
233
        opsAlign->reloadOptionsProfiles();
    });
234
    page->setIcon(QIcon::fromTheme("configure"));
235

236
237
238
239
240
    connect(opsAlign, &OpsAlign::needToLoadProfile, this, [this, dialog, page](int profile)
    {
        optionsProfileEditor->loadProfile(profile);
        dialog->setCurrentPage(page);
    });
241
242

    opsAstrometryIndexFiles = new OpsAstrometryIndexFiles(this);
243
244
    m_IndexFilesPage = dialog->addPage(opsAstrometryIndexFiles, i18n("Index Files"));
    m_IndexFilesPage->setIcon(QIcon::fromTheme("map-flat"));
245

246
247
    appendLogText(i18n("Idle."));

248
    pi.reset(new QProgressIndicator(this));
249

250
    stopLayout->addWidget(pi.get());
251
252

    exposureIN->setValue(Options::alignExposure());
253
254
255
256
    connect(exposureIN, static_cast<void(QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), [&]()
    {
        syncSettings();
    });
257

258
259
260
    rememberSolverWCS = Options::astrometrySolverWCS();
    rememberAutoWCS   = Options::autoWCS();

261
262
    solverModeButtonGroup->setId(localSolverR, SOLVER_LOCAL);
    solverModeButtonGroup->setId(remoteSolverR, SOLVER_REMOTE);
263

264
265
266
267
268
    localSolverR->setChecked(Options::solverMode() == SOLVER_LOCAL);
    remoteSolverR->setChecked(Options::solverMode() == SOLVER_REMOTE);
    connect(solverModeButtonGroup, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), this,
            &Align::setSolverMode);
    setSolverMode(solverModeButtonGroup->checkedId());
269

270
    // Which telescope info to use for FOV calculations
271
    FOVScopeCombo->setCurrentIndex(Options::solverScopeType());
272
273
    connect(FOVScopeCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
            &Ekos::Align::updateTelescopeType);
274
275

    accuracySpin->setValue(Options::solverAccuracyThreshold());
276
277
278
279
280
    connect(accuracySpin, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), [this]()
    {
        Options::setSolverAccuracyThreshold(accuracySpin->value());
        buildTarget();
    });
Jasem Mutlaq's avatar
Jasem Mutlaq committed
281
282
283
284
285

    connect(alignDarkFrameCheck, &QCheckBox::toggled, [this]()
    {
        Options::setAlignDarkFrame(alignDarkFrameCheck->isChecked());
    });
286
287
288
    alignDarkFrameCheck->setChecked(Options::alignDarkFrame());

    delaySpin->setValue(Options::settlingTime());
289
    connect(delaySpin, &QSpinBox::editingFinished, this, &Ekos::Align::saveSettleTime);
290

291
292
    connect(binningCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
            &Ekos::Align::setBinningIndex);
293

294
    double accuracyRadius = accuracySpin->value();
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329

    alignPlot->setBackground(QBrush(Qt::black));
    alignPlot->setSelectionTolerance(10);

    alignPlot->xAxis->setBasePen(QPen(Qt::white, 1));
    alignPlot->yAxis->setBasePen(QPen(Qt::white, 1));

    alignPlot->xAxis->setTickPen(QPen(Qt::white, 1));
    alignPlot->yAxis->setTickPen(QPen(Qt::white, 1));

    alignPlot->xAxis->setSubTickPen(QPen(Qt::white, 1));
    alignPlot->yAxis->setSubTickPen(QPen(Qt::white, 1));

    alignPlot->xAxis->setTickLabelColor(Qt::white);
    alignPlot->yAxis->setTickLabelColor(Qt::white);

    alignPlot->xAxis->setLabelColor(Qt::white);
    alignPlot->yAxis->setLabelColor(Qt::white);

    alignPlot->xAxis->setLabelFont(QFont(font().family(), 10));
    alignPlot->yAxis->setLabelFont(QFont(font().family(), 10));

    alignPlot->xAxis->setLabelPadding(2);
    alignPlot->yAxis->setLabelPadding(2);

    alignPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
    alignPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
    alignPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
    alignPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
    alignPlot->xAxis->grid()->setZeroLinePen(QPen(Qt::yellow));
    alignPlot->yAxis->grid()->setZeroLinePen(QPen(Qt::yellow));

    alignPlot->xAxis->setLabel(i18n("dRA (arcsec)"));
    alignPlot->yAxis->setLabel(i18n("dDE (arcsec)"));

330
331
    alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
    alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
332
333
334
335
336
337
338
339
340
341

    alignPlot->setInteractions(QCP::iRangeZoom);
    alignPlot->setInteraction(QCP::iRangeDrag, true);

    alignPlot->addGraph();
    alignPlot->graph(0)->setLineStyle(QCPGraph::lsNone);
    alignPlot->graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, Qt::white, 15));

    buildTarget();

342
343
344
    connect(alignPlot, &QCustomPlot::mouseMove, this, &Ekos::Align::handlePointTooltip);
    connect(rightLayout, &QSplitter::splitterMoved, this, &Ekos::Align::handleVerticalPlotSizeChange);
    connect(alignSplitter, &QSplitter::splitterMoved, this, &Ekos::Align::handleHorizontalPlotSizeChange);
345
346
347
348

    alignPlot->resize(190, 190);
    alignPlot->replot();

349
    solutionTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
350

351
    clearAllSolutionsB->setIcon(
352
        QIcon::fromTheme("application-exit"));
353
354
    clearAllSolutionsB->setAttribute(Qt::WA_LayoutUsesWidgetRect);

355
    removeSolutionB->setIcon(QIcon::fromTheme("list-remove"));
356
357
    removeSolutionB->setAttribute(Qt::WA_LayoutUsesWidgetRect);

358
    exportSolutionsCSV->setIcon(
359
        QIcon::fromTheme("document-save-as"));
360
361
    exportSolutionsCSV->setAttribute(Qt::WA_LayoutUsesWidgetRect);

362
    autoScaleGraphB->setIcon(QIcon::fromTheme("zoom-fit-best"));
363
    autoScaleGraphB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
364

365
366
367
368
369
370
371
    connect(clearAllSolutionsB, &QPushButton::clicked, this, &Ekos::Align::slotClearAllSolutionPoints);
    connect(removeSolutionB, &QPushButton::clicked, this, &Ekos::Align::slotRemoveSolutionPoint);
    connect(exportSolutionsCSV, &QPushButton::clicked, this, &Ekos::Align::exportSolutionPoints);
    connect(autoScaleGraphB, &QPushButton::clicked, this, &Ekos::Align::slotAutoScaleGraph);
    connect(mountModelB, &QPushButton::clicked, this, &Ekos::Align::slotMountModel);
    connect(solutionTable, &QTableWidget::cellClicked, this, &Ekos::Align::selectSolutionTableRow);

372
373
374
375
376
    //Note:  This is to prevent a button from being called the default button
    //and then executing when the user hits the enter key such as when on a Text Box
    QList<QPushButton *> qButtons = findChildren<QPushButton *>();
    for (auto &button : qButtons)
        button->setAutoDefault(false);
377

378
    savedOptionsProfiles = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("SavedAlignProfiles.ini");
379
    if(QFile(savedOptionsProfiles).exists())
380
        m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
381
    else
382
        m_StellarSolverProfiles = getDefaultAlignOptionsProfiles();
383
384

    initPolarAlignmentAssistant();
385
    initManualRotator();
386
    initDarkProcessor();
387
388
389
390
}

Align::~Align()
{
Csaba Kertesz's avatar
Csaba Kertesz committed
391
    if (alignWidget->parent() == nullptr)
392
393
        toggleAlignWidgetFullScreen();

394
395
    // Remove temporary FITS files left before by the solver
    QDir dir(QDir::tempPath());
396
    dir.setNameFilters(QStringList() << "fits*"
397
                       << "tmp.*");
398
    dir.setFilter(QDir::Files);
399
    for (auto &dirFile : dir.entryList())
400
        dir.remove(dirFile);
401
}
Jasem Mutlaq's avatar
Jasem Mutlaq committed
402
403
void Align::selectSolutionTableRow(int row, int column)
{
404
    Q_UNUSED(column)
Jasem Mutlaq's avatar
Jasem Mutlaq committed
405

406
    solutionTable->selectRow(row);
407
    for (int i = 0; i < alignPlot->itemCount(); i++)
408
    {
409
410
        QCPAbstractItem *abstractItem = alignPlot->item(i);
        if (abstractItem)
411
        {
412
413
            QCPItemText *item = qobject_cast<QCPItemText *>(abstractItem);
            if (item)
414
            {
415
                if (i == row)
416
                {
417
418
                    item->setColor(Qt::black);
                    item->setBrush(Qt::yellow);
419
420
421
                }
                else
                {
422
423
424
425
426
427
428
429
430
                    item->setColor(Qt::red);
                    item->setBrush(Qt::white);
                }
            }
        }
    }
    alignPlot->replot();
}

431
432
void Align::handleHorizontalPlotSizeChange()
{
433
    alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);
434
    alignPlot->replot();
435
436
}

437
438
void Align::handleVerticalPlotSizeChange()
{
439
    alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0);
440
    alignPlot->replot();
441
442
}

443
void Align::resizeEvent(QResizeEvent *event)
444
{
445
    if (event->oldSize().width() != -1)
446
    {
447
        if (event->oldSize().width() != size().width())
448
            handleHorizontalPlotSizeChange();
449
        else if (event->oldSize().height() != size().height())
450
            handleVerticalPlotSizeChange();
451
452
453
    }
    else
    {
454
        QTimer::singleShot(10, this, &Ekos::Align::handleHorizontalPlotSizeChange);
455
456
457
    }
}

458
void Align::handlePointTooltip(QMouseEvent *event)
459
{
460
461
    QCPAbstractItem *item = alignPlot->itemAt(event->localPos());
    if (item)
462
    {
463
464
        QCPItemText *label = qobject_cast<QCPItemText *>(item);
        if (label)
465
        {
466
            QString labelText = label->text();
467
            int point         = labelText.toInt() - 1;
468

469
470
            if (point < 0)
                return;
471
            QToolTip::showText(event->globalPos(),
Yuri Chornoivan's avatar
Yuri Chornoivan committed
472
                               i18n("<table>"
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
                                    "<tr>"
                                    "<th colspan=\"2\">Object %1: %2</th>"
                                    "</tr>"
                                    "<tr>"
                                    "<td>RA:</td><td>%3</td>"
                                    "</tr>"
                                    "<tr>"
                                    "<td>DE:</td><td>%4</td>"
                                    "</tr>"
                                    "<tr>"
                                    "<td>dRA:</td><td>%5</td>"
                                    "</tr>"
                                    "<tr>"
                                    "<td>dDE:</td><td>%6</td>"
                                    "</tr>"
                                    "</table>",
                                    point + 1,
                                    solutionTable->item(point, 2)->text(),
                                    solutionTable->item(point, 0)->text(),
                                    solutionTable->item(point, 1)->text(),
                                    solutionTable->item(point, 4)->text(),
                                    solutionTable->item(point, 5)->text()),
495
496
497
498
499
                               alignPlot, alignPlot->rect());
        }
    }
}

500
501
void Align::buildTarget()
{
502
    double accuracyRadius = accuracySpin->value();
503
    if (centralTarget)
504
505
506
507
508
509
510
511
    {
        concentricRings->data()->clear();
        redTarget->data()->clear();
        yellowTarget->data()->clear();
        centralTarget->data()->clear();
    }
    else
    {
512
        concentricRings = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
513
514
515
        redTarget       = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
        yellowTarget    = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
        centralTarget   = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis);
516
517
    }
    const int pointCount = 200;
518
    QVector<QCPCurveData> circleRings(
519
        pointCount * (5)); //Have to multiply by the number of rings, Rings at : 25%, 50%, 75%, 125%, 175%
520
521
522
523
    QVector<QCPCurveData> circleCentral(pointCount);
    QVector<QCPCurveData> circleYellow(pointCount);
    QVector<QCPCurveData> circleRed(pointCount);

524
525
    int circleRingPt = 0;
    for (int i = 0; i < pointCount; i++)
526
    {
527
        double theta = i / static_cast<double>(pointCount) * 2 * M_PI;
528

529
        for (double ring = 1; ring < 8; ring++)
530
        {
531
            if (ring != 4 && ring != 6)
532
            {
533
                if (i % (9 - static_cast<int>(ring)) == 0) //This causes fewer points to draw on the inner circles.
534
535
536
537
538
539
540
                {
                    circleRings[circleRingPt] = QCPCurveData(circleRingPt, accuracyRadius * ring * 0.25 * qCos(theta),
                                                accuracyRadius * ring * 0.25 * qSin(theta));
                    circleRingPt++;
                }
            }
        }
541

542
543
544
        circleCentral[i] = QCPCurveData(i, accuracyRadius * qCos(theta), accuracyRadius * qSin(theta));
        circleYellow[i]  = QCPCurveData(i, accuracyRadius * 1.5 * qCos(theta), accuracyRadius * 1.5 * qSin(theta));
        circleRed[i]     = QCPCurveData(i, accuracyRadius * 2 * qCos(theta), accuracyRadius * 2 * qSin(theta));
545
546
    }

547
548
549
    concentricRings->setLineStyle(QCPCurve::lsNone);
    concentricRings->setScatterSkip(0);
    concentricRings->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, QColor(255, 255, 255, 150), 1));
550

551
552
553
554
    concentricRings->data()->set(circleRings, true);
    redTarget->data()->set(circleRed, true);
    yellowTarget->data()->set(circleYellow, true);
    centralTarget->data()->set(circleCentral, true);
555

556
557
558
559
    concentricRings->setPen(QPen(Qt::white));
    redTarget->setPen(QPen(Qt::red));
    yellowTarget->setPen(QPen(Qt::yellow));
    centralTarget->setPen(QPen(Qt::green));
560

561
562
563
564
565
    concentricRings->setBrush(Qt::NoBrush);
    redTarget->setBrush(QBrush(QColor(255, 0, 0, 50)));
    yellowTarget->setBrush(
        QBrush(QColor(0, 255, 0, 50))); //Note this is actually yellow.  It is green on top of red with equal opacity.
    centralTarget->setBrush(QBrush(QColor(0, 255, 0, 50)));
566

567
568
569
    if (alignPlot->size().width() > 0)
        alignPlot->replot();
}
570

571
572
573
574
575
576
577
578
579
void Align::slotAutoScaleGraph()
{
    double accuracyRadius = accuracySpin->value();
    alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);
    alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3);

    alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0);

    alignPlot->replot();
580
581
}

582

Robert Lancaster's avatar
Robert Lancaster committed
583
584
585
586
587
void Align::slotClearAllSolutionPoints()
{
    if (solutionTable->rowCount() == 0)
        return;

588
    connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
Robert Lancaster's avatar
Robert Lancaster committed
589
    {
590
591
        //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
        KSMessageBox::Instance()->disconnect(this);
592

Robert Lancaster's avatar
Robert Lancaster committed
593
594
595
596
597
        solutionTable->setRowCount(0);
        alignPlot->graph(0)->data()->clear();
        alignPlot->clearItems();
        buildTarget();

598
        slotAutoScaleGraph();
599
600
601
602
603

    });

    KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to clear all of the solution points?"),
                                            i18n("Clear Solution Points"), 60);
Robert Lancaster's avatar
Robert Lancaster committed
604
605
}

606
607
void Align::slotRemoveSolutionPoint()
{
608
609
    QCPAbstractItem *abstractItem = alignPlot->item(solutionTable->currentRow());
    if (abstractItem)
610
    {
611
612
        QCPItemText *item = qobject_cast<QCPItemText *>(abstractItem);
        if (item)
613
        {
614
            double point = item->position->key();
615
616
617
618
            alignPlot->graph(0)->data()->remove(point);
        }
    }
    alignPlot->removeItem(solutionTable->currentRow());
619
    for (int i = 0; i < alignPlot->itemCount(); i++)
620
    {
621
622
        QCPAbstractItem *abstractItem = alignPlot->item(i);
        if (abstractItem)
623
        {
624
625
            QCPItemText *item = qobject_cast<QCPItemText *>(abstractItem);
            if (item)
626
                item->setText(QString::number(i + 1));
627
628
629
630
631
632
        }
    }
    solutionTable->removeRow(solutionTable->currentRow());
    alignPlot->replot();
}

633
634
void Align::slotMountModel()
{
635
    if (!m_MountModel)
636
    {
637
638
639
        m_MountModel = new MountModel(this);
        connect(m_MountModel, &Ekos::MountModel::newLog, this, &Ekos::Align::appendLogText, Qt::UniqueConnection);
        connect(this, &Ekos::Align::newStatus, m_MountModel, &Ekos::MountModel::setAlignStatus, Qt::UniqueConnection);
640
641
    }

642
    m_MountModel->show();
643

644
645
646
647
648
    //    SkyPoint spWest;
    //    spWest.setAlt(30);
    //    spWest.setAz(270);
    //    spWest.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat());
    //    mountModel.alignDec->setValue(static_cast<int>(spWest.dec().Degrees()));
649

650
    //    mountModelDialog.show();
651
652
}

653
654
655

bool Align::isParserOK()
{
656
    return true; //For now
657
658
    Q_ASSERT_X(parser, __FUNCTION__, "Astrometry parser is not valid.");

659
660
661
662
    bool rc = parser->init();

    if (rc)
    {
663
664
        connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
        connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
665
666
667
668
669
    }

    return rc;
}

670
671
void Align::checkAlignmentTimeout()
{
672
    if (m_SolveFromFile || ++solverIterations == MAXIMUM_SOLVER_ITERATIONS)
673
        abort();
674
    else if (!m_SolveFromFile)
675
    {
676
        appendLogText(i18n("Solver timed out."));
Jasem Mutlaq's avatar
Jasem Mutlaq committed
677
        parser->stopSolver();
678

679
        setAlignTableResult(ALIGN_RESULT_FAILED);
680
681
682
        captureAndSolve();
    }
    // TODO must also account for loadAndSlew. Retain file name
683
684
}

685
void Align::setSolverMode(int mode)
686
{
687
    if (sender() == nullptr && mode >= 0 && mode <= 1)
688
    {
689
        solverModeButtonGroup->button(mode)->setChecked(true);
690
691
    }

692
    Options::setSolverMode(mode);
693

694
    if (mode == SOLVER_REMOTE)
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
    {
        if (remoteParser.get() != nullptr && remoteParserDevice != nullptr)
        {
            parser = remoteParser.get();
            (dynamic_cast<RemoteAstrometryParser *>(parser))->setAstrometryDevice(remoteParserDevice);
            return;
        }

        remoteParser.reset(new Ekos::RemoteAstrometryParser());
        parser = remoteParser.get();
        (dynamic_cast<RemoteAstrometryParser *>(parser))->setAstrometryDevice(remoteParserDevice);
        if (currentCCD)
            (dynamic_cast<RemoteAstrometryParser *>(parser))->setCCD(currentCCD->getDeviceName());

        parser->setAlign(this);
        if (parser->init())
        {
            connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection);
            connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection);
        }
        else
            parser->disconnect();
    }
}

720

721
bool Align::setCamera(const QString &device)
722
{
723
    for (int i = 0; i < CCDCaptureCombo->count(); i++)
724
725
726
        if (device == CCDCaptureCombo->itemText(i))
        {
            CCDCaptureCombo->setCurrentIndex(i);
727
            checkCCD(i);
728
729
730
731
732
733
            return true;
        }

    return false;
}

734
735
QString Align::camera()
{
736
737
    if (currentCCD)
        return currentCCD->getDeviceName();
738

739
    return QString();
740
741
}

742
743
void Align::setDefaultCCD(QString ccd)
{
744
    syncSettings();
745
746
747
748
749
    Options::setDefaultAlignCCD(ccd);
}

void Align::checkCCD(int ccdNum)
{
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
    // Do NOT perform checks if align is in progress as this may result
    // in signals/slots getting disconnected.
    switch (state)
    {
        // Idle, camera change is OK.
        case ALIGN_IDLE:
        case ALIGN_COMPLETE:
        case ALIGN_FAILED:
        case ALIGN_ABORTED:
            break;

        // Busy, camera change is not OK.
        case ALIGN_PROGRESS:
        case ALIGN_SYNCING:
        case ALIGN_SLEWING:
        case ALIGN_SUSPENDED:
            return;
    }


770
    if (ccdNum == -1 || ccdNum >= CCDs.count())
771
772
773
774
775
776
777
    {
        ccdNum = CCDCaptureCombo->currentIndex();

        if (ccdNum == -1)
            return;
    }

778
    currentCCD = CCDs.at(ccdNum);
779

780
781
782
783
    ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
    if (targetChip && targetChip->isCapturing())
        return;

784
    if (solverModeButtonGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr)
785
786
        (dynamic_cast<RemoteAstrometryParser *>(remoteParser.get()))->setCCD(currentCCD->getDeviceName());

787
788
    syncCCDInfo();

789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
    QStringList isoList = targetChip->getISOList();
    ISOCombo->clear();

    if (isoList.isEmpty())
    {
        ISOCombo->setEnabled(false);
    }
    else
    {
        ISOCombo->setEnabled(true);
        ISOCombo->addItems(isoList);
        ISOCombo->setCurrentIndex(targetChip->getISOIndex());
    }

    // Gain Check
    if (currentCCD->hasGain())
    {
        double min, max, step, value;
        currentCCD->getGainMinMaxStep(&min, &max, &step);

        // Allow the possibility of no gain value at all.
        GainSpinSpecialValue = min - step;
        GainSpin->setRange(GainSpinSpecialValue, max);
        GainSpin->setSpecialValueText(i18n("--"));
        GainSpin->setEnabled(true);
        GainSpin->setSingleStep(step);
        currentCCD->getGain(&value);

        // Set the custom gain if we have one
        // otherwise it will not have an effect.
Jasem Mutlaq's avatar
Jasem Mutlaq committed
819
        TargetCustomGainValue = Options::solverCameraGain();
820
821
822
823
824
825
826
827
828
        if (TargetCustomGainValue > 0)
            GainSpin->setValue(TargetCustomGainValue);
        else
            GainSpin->setValue(GainSpinSpecialValue);

        GainSpin->setReadOnly(currentCCD->getGainPermission() == IP_RO);

        connect(GainSpin, &QDoubleSpinBox::editingFinished, [this]()
        {
829
            if (GainSpin->value() > GainSpinSpecialValue)
830
            {
831
                TargetCustomGainValue = GainSpin->value();
Jasem Mutlaq's avatar
Jasem Mutlaq committed
832
833
                // Save custom gain
                Options::setSolverCameraGain(TargetCustomGainValue);
834
            }
835
836
837
838
839
        });
    }
    else
        GainSpin->setEnabled(false);

840
    syncTelescopeInfo();
841
842
}

843
void Align::addCCD(ISD::GDInterface *newCCD)
844
845
846
{
    if (CCDs.contains(static_cast<ISD::CCD *>(newCCD)))
    {
847
848
        syncCCDInfo();
        return;
849
850
851
852
853
    }

    CCDs.append(static_cast<ISD::CCD *>(newCCD));

    CCDCaptureCombo->addItem(newCCD->getDeviceName());
854
855

    checkCCD();
Jasem Mutlaq's avatar
Jasem Mutlaq committed
856
857

    syncSettings();
858
859
}

860
void Align::setTelescope(ISD::GDInterface *newTelescope)
861
{
862
    currentTelescope = static_cast<ISD::Telescope *>(newTelescope);
863
864
    currentTelescope->disconnect(this);

865
866
    RUN_PAH(setCurrentTelescope(currentTelescope));

867
    connect(currentTelescope, &ISD::GDInterface::numberUpdated, this, &Ekos::Align::processNumber, Qt::UniqueConnection);
868
    connect(currentTelescope, &ISD::GDInterface::switchUpdated, this, &Ekos::Align::processSwitch, Qt::UniqueConnection);
869
870
    connect(currentTelescope, &ISD::GDInterface::Disconnected, this, [this]()
    {
871
872
873
        m_isRateSynced = false;
    });

874

875
    if (m_isRateSynced == false)
876
    {
877
        RUN_PAH(syncMountSpeed());
878
        m_isRateSynced = !currentTelescope->slewRates().empty();
879
880
    }

881
    syncTelescopeInfo();
882
883
}

884
885
886
void Align::setDome(ISD::GDInterface *newDome)
{
    currentDome = static_cast<ISD::Dome *>(newDome);
887
    connect(currentDome, &ISD::GDInterface::switchUpdated, this, &Ekos::Align::processSwitch, Qt::UniqueConnection);
888
889
}

890
891
892
void Align::removeDevice(ISD::GDInterface *device)
{
    device->disconnect(this);
893
    if (currentTelescope && currentTelescope->getDeviceName() == device->getDeviceName())
894
895
896
897
    {
        currentTelescope = nullptr;
        m_isRateSynced = false;
    }
898
    else if (currentDome && currentDome->getDeviceName() == device->getDeviceName())
899
900
901
    {
        currentDome = nullptr;
    }
902
    else if (currentRotator && currentRotator->getDeviceName() == device->getDeviceName())
903
    {
904
905
906
907
908
909
        currentRotator = nullptr;
    }

    if (CCDs.contains(static_cast<ISD::CCD *>(device)))
    {
        CCDs.removeAll(static_cast<ISD::CCD *>(device));
910
        CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(device->getDeviceName()));
911
        CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(device->getDeviceName() + QString(" Guider")));
912
        if (CCDs.empty())
913
        {
914
            currentCCD = nullptr;
915
916
917
            CCDCaptureCombo->setCurrentIndex(-1);
        }
        else
918
919
        {
            currentCCD = CCDs[0];
920
            CCDCaptureCombo->setCurrentIndex(0);
921
        }
922
923
924
925
926
927

        QTimer::singleShot(1000, this, [this]()
        {
            checkCCD();
        });
        //checkCCD();
928
    }
929
930

    if (Filters.contains(static_cast<ISD::Filter *>(device)))
931
    {
932
933
        Filters.removeAll(static_cast<ISD::Filter *>(device));
        filterManager->removeDevice(device);