File indexing completed on 2024-04-28 03:43:42

0001 /*
0002     SPDX-FileCopyrightText: 2022 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include <indicom.h>
0008 
0009 #include "framingassistantui.h"
0010 #include "ui_framingassistant.h"
0011 #include "mosaiccomponent.h"
0012 #include "mosaictiles.h"
0013 #include "kstars.h"
0014 #include "Options.h"
0015 #include "scheduler.h"
0016 #include "skymap.h"
0017 #include "ekos/manager.h"
0018 #include "ekos/mount/mount.h"
0019 #include "skymapcomposite.h"
0020 #include "ksparser.h"
0021 
0022 #include <QDBusReply>
0023 
0024 namespace Ekos
0025 {
0026 
0027 FramingAssistantUI::FramingAssistantUI(): QDialog(KStars::Instance()), ui(new Ui::FramingAssistant())
0028 {
0029     ui->setupUi(this);
0030 
0031     auto tiles = KStarsData::Instance()->skyComposite()->mosaicComponent()->tiles();
0032 
0033     ui->raBox->setUnits(dmsBox::HOURS);
0034 
0035     // Initial optics information is taken from Ekos options
0036     ui->focalLenSpin->setValue(Options::telescopeFocalLength());
0037     ui->focalReducerSpin->setValue(Options::telescopeFocalReducer());
0038     ui->pixelWSizeSpin->setValue(Options::cameraPixelWidth());
0039     ui->pixelHSizeSpin->setValue(Options::cameraPixelHeight());
0040     ui->cameraWSpin->setValue(Options::cameraWidth());
0041     ui->cameraHSpin->setValue(Options::cameraHeight());
0042 
0043     ui->positionAngleSpin->setValue(tiles->positionAngle());
0044     ui->sequenceEdit->setText(tiles->sequenceFile());
0045     ui->directoryEdit->setText(tiles->outputDirectory());
0046     ui->targetEdit->setText(tiles->targetName());
0047     ui->focusEvery->setValue(tiles->focusEveryN());
0048     ui->alignEvery->setValue(tiles->alignEveryN());
0049     ui->trackStepCheck->setChecked(tiles->isTrackChecked());
0050     ui->focusStepCheck->setChecked(tiles->isFocusChecked());
0051     ui->alignStepCheck->setChecked(tiles->isAlignChecked());
0052     ui->guideStepCheck->setChecked(tiles->isGuideChecked());
0053     ui->mosaicWSpin->setValue(tiles->gridSize().width());
0054     ui->mosaicHSpin->setValue(tiles->gridSize().height());
0055     ui->overlapSpin->setValue(tiles->overlap());
0056 
0057     ui->groupEdit->setText(tiles->group());
0058     QString completionVal, completionArg;
0059     completionVal = tiles->completionCondition(&completionArg);
0060     if (completionVal == "FinishSequence")
0061         ui->sequenceCompletionR->setChecked(true);
0062     else if (completionVal == "FinishRepeat")
0063     {
0064         ui->repeatCompletionR->setChecked(true);
0065         ui->repeatsSpin->setValue(completionArg.toInt());
0066     }
0067     else if (completionVal == "FinishLoop")
0068         ui->loopCompletionR->setChecked(true);
0069 
0070     if (tiles->operationMode() == MosaicTiles::MODE_OPERATION)
0071     {
0072         m_CenterPoint = *tiles.data();
0073     }
0074     else
0075     {
0076         // Focus only has JNow coords (in both ra0 and ra)
0077         // so we need to get catalog coords so it can have valid coordinates.
0078         m_CenterPoint = *SkyMap::Instance()->focus();
0079         auto J2000Coords = m_CenterPoint.catalogueCoord(KStars::Instance()->data()->ut().djd());
0080         m_CenterPoint.setRA0(J2000Coords.ra0());
0081         m_CenterPoint.setDec0(J2000Coords.dec0());
0082     }
0083 
0084     m_CenterPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
0085     ui->raBox->show(m_CenterPoint.ra0());
0086     ui->decBox->show(m_CenterPoint.dec0());
0087 
0088     // Page Navigation
0089     connect(ui->backToEquipmentB, &QPushButton::clicked, this, [this]()
0090     {
0091         ui->stackedWidget->setCurrentIndex(PAGE_EQUIPMENT);
0092     });
0093 
0094     // Go and Solve
0095     if (Ekos::Manager::Instance()->ekosStatus() == Ekos::Success)
0096     {
0097         ui->goSolveB->setEnabled(true);
0098         connect(Ekos::Manager::Instance()->mountModule(), &Ekos::Mount::newStatus, this, &Ekos::FramingAssistantUI::setMountState,
0099                 Qt::UniqueConnection);
0100         connect(Ekos::Manager::Instance()->alignModule(), &Ekos::Align::newStatus, this, &Ekos::FramingAssistantUI::setAlignState,
0101                 Qt::UniqueConnection);
0102     }
0103     connect(Ekos::Manager::Instance(), &Ekos::Manager::ekosStatusChanged, this, [this](Ekos::CommunicationStatus status)
0104     {
0105         ui->goSolveB->setEnabled(status == Ekos::Success);
0106 
0107         // GO AND SOLVE
0108         if (status == Ekos::Success)
0109         {
0110             connect(Ekos::Manager::Instance()->mountModule(), &Ekos::Mount::newStatus, this, &Ekos::FramingAssistantUI::setMountState,
0111                     Qt::UniqueConnection);
0112             connect(Ekos::Manager::Instance()->alignModule(), &Ekos::Align::newStatus, this, &Ekos::FramingAssistantUI::setAlignState,
0113                     Qt::UniqueConnection);
0114         }
0115     });
0116     connect(ui->goSolveB, &QPushButton::clicked, this, &Ekos::FramingAssistantUI::goAndSolve);
0117 
0118     // Import
0119     connect(ui->importB, &QPushButton::clicked, this, &Ekos::FramingAssistantUI::selectImport);
0120 
0121     // Page Navigation Controls
0122     connect(ui->nextToAdjustGrid, &QPushButton::clicked, this, [this]()
0123     {
0124         ui->stackedWidget->setCurrentIndex(PAGE_ADJUST_GRID);
0125     });
0126     connect(ui->backToAdjustGridB, &QPushButton::clicked, this, [this]()
0127     {
0128         ui->stackedWidget->setCurrentIndex(PAGE_ADJUST_GRID);
0129     });
0130     connect(ui->nextToSelectGridB, &QPushButton::clicked, this, [this]()
0131     {
0132         ui->stackedWidget->setCurrentIndex(PAGE_SELECT_GRID);
0133     });
0134     connect(ui->backToSelectGrid, &QPushButton::clicked, this, [this]()
0135     {
0136         ui->stackedWidget->setCurrentIndex(PAGE_SELECT_GRID);
0137     });
0138     connect(ui->nextToJobsB, &QPushButton::clicked, this, [this]()
0139     {
0140         ui->stackedWidget->setCurrentIndex(PAGE_CREATE_JOBS);
0141         ui->createJobsB->setEnabled(!ui->targetEdit->text().isEmpty() && !ui->sequenceEdit->text().isEmpty() &&
0142                                     !ui->directoryEdit->text().isEmpty());
0143     });
0144 
0145     // Respond to sky map drag event that causes a shift in the ra and de coords of the center
0146     connect(SkyMap::Instance(), &SkyMap::mosaicCenterChanged, this, [this](dms dRA, dms dDE)
0147     {
0148         m_CenterPoint.setRA0(range24(m_CenterPoint.ra0().Hours() + dRA.Hours()));
0149         m_CenterPoint.setDec0(rangeDec(m_CenterPoint.dec0().Degrees() + dDE.Degrees()));
0150         m_CenterPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
0151         ui->raBox->show(m_CenterPoint.ra0());
0152         ui->decBox->show(m_CenterPoint.dec0());
0153         //m_CenterPoint.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd());
0154         m_DebounceTimer->start();
0155     });
0156 
0157     // Update target name after edit
0158     connect(ui->targetEdit, &QLineEdit::editingFinished, this, &FramingAssistantUI::sanitizeTarget);
0159 
0160     // Recenter
0161     connect(ui->recenterB, &QPushButton::clicked, this, [this]()
0162     {
0163         // Focus only has JNow coords (in both ra0 and ra)
0164         // so we need to get catalog coords so it can have valid coordinates.
0165         m_CenterPoint = *SkyMap::Instance()->focus();
0166         auto J2000Coords = m_CenterPoint.catalogueCoord(KStars::Instance()->data()->ut().djd());
0167         m_CenterPoint.setRA0(J2000Coords.ra0());
0168         m_CenterPoint.setDec0(J2000Coords.dec0());
0169 
0170         m_CenterPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
0171         ui->raBox->show(m_CenterPoint.ra0());
0172         ui->decBox->show(m_CenterPoint.dec0());
0173         m_DebounceTimer->start();
0174     });
0175 
0176     // Set initial target on startup
0177     if (tiles->operationMode() == MosaicTiles::MODE_PLANNING && SkyMap::IsFocused())
0178     {
0179         auto sanitized = KSUtils::sanitize(SkyMap::Instance()->focusObject()->name());
0180         if (sanitized != i18n("unnamed"))
0181         {
0182             ui->targetEdit->setText(sanitized);
0183 
0184             if (m_JobsDirectory.isEmpty())
0185                 ui->directoryEdit->setText(QDir::cleanPath(QDir::homePath() + QDir::separator() + sanitized));
0186             else
0187                 ui->directoryEdit->setText(m_JobsDirectory + QDir::separator() + sanitized);
0188         }
0189     }
0190 
0191     // Update object name
0192     connect(SkyMap::Instance(), &SkyMap::objectChanged, this, [this](SkyObject * o)
0193     {
0194         QString sanitized = o->name();
0195         if (sanitized != i18n("unnamed"))
0196         {
0197             // Remove illegal characters that can be problematic
0198             sanitized = KSUtils::sanitize(sanitized);
0199             ui->targetEdit->setText(sanitized);
0200 
0201             if (m_JobsDirectory.isEmpty())
0202                 ui->directoryEdit->setText(QDir::cleanPath(QDir::homePath() + QDir::separator() + sanitized));
0203             else
0204                 ui->directoryEdit->setText(m_JobsDirectory + QDir::separator() + sanitized);
0205         }
0206     });
0207 
0208     // Watch for manual changes in ra box
0209     connect(ui->raBox, &dmsBox::editingFinished, this, [this]
0210     {
0211         m_CenterPoint.setRA0(ui->raBox->createDms());
0212         m_CenterPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
0213         m_DebounceTimer->start();
0214     });
0215 
0216     // Watch for manual hanges in de box
0217     connect(ui->decBox, &dmsBox::editingFinished, this, [this]
0218     {
0219         m_CenterPoint.setDec0(ui->decBox->createDms());
0220         m_CenterPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
0221         m_DebounceTimer->start();
0222     });
0223 
0224     connect(ui->loadSequenceB, &QPushButton::clicked, this, &FramingAssistantUI::selectSequence);
0225     connect(ui->selectJobsDirB, &QPushButton::clicked, this, &Ekos::FramingAssistantUI::selectDirectory);
0226     // Rendering options
0227     ui->transparencySlider->setValue(Options::mosaicTransparencyLevel());
0228     ui->transparencySlider->setEnabled(!Options::mosaicTransparencyAuto());
0229     tiles->setPainterAlpha(Options::mosaicTransparencyLevel());
0230     connect(ui->transparencySlider, QOverload<int>::of(&QSlider::valueChanged), this, [&](int v)
0231     {
0232         ui->transparencySlider->setToolTip(QString("%1%").arg(v));
0233         Options::setMosaicTransparencyLevel(v);
0234         auto tiles = KStarsData::Instance()->skyComposite()->mosaicComponent()->tiles();
0235         tiles->setPainterAlpha(v);
0236         m_DebounceTimer->start();
0237     });
0238     ui->transparencyAuto->setChecked(Options::mosaicTransparencyAuto());
0239     tiles->setPainterAlphaAuto(Options::mosaicTransparencyAuto());
0240     connect(ui->transparencyAuto, &QCheckBox::toggled, this, [&](bool v)
0241     {
0242         ui->transparencySlider->setEnabled(!v);
0243         Options::setMosaicTransparencyAuto(v);
0244         auto tiles = KStarsData::Instance()->skyComposite()->mosaicComponent()->tiles();
0245         tiles->setPainterAlphaAuto(v);
0246         if (v)
0247             m_DebounceTimer->start();
0248     });
0249 
0250     // The update timer avoids stacking updates which crash the sky map renderer
0251     m_DebounceTimer = new QTimer(this);
0252     m_DebounceTimer->setSingleShot(true);
0253     m_DebounceTimer->setInterval(500);
0254     connect(m_DebounceTimer, &QTimer::timeout, this, &Ekos::FramingAssistantUI::constructMosaic);
0255 
0256     // Scope optics information
0257     // - Changing the optics configuration changes the FOV, which changes the target field dimensions
0258     connect(ui->focalLenSpin, &QDoubleSpinBox::editingFinished, this, &Ekos::FramingAssistantUI::calculateFOV);
0259     connect(ui->focalReducerSpin, &QDoubleSpinBox::editingFinished, this, &Ekos::FramingAssistantUI::calculateFOV);
0260     connect(ui->cameraWSpin, &QSpinBox::editingFinished, this, &Ekos::FramingAssistantUI::calculateFOV);
0261     connect(ui->cameraHSpin, &QSpinBox::editingFinished, this, &Ekos::FramingAssistantUI::calculateFOV);
0262     connect(ui->pixelWSizeSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
0263             &Ekos::FramingAssistantUI::calculateFOV);
0264     connect(ui->pixelHSizeSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
0265             &Ekos::FramingAssistantUI::calculateFOV);
0266     connect(ui->positionAngleSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
0267             &Ekos::FramingAssistantUI::calculateFOV);
0268 
0269     // Mosaic configuration
0270     // - Changing the target field dimensions changes the grid dimensions
0271     // - Changing the overlap field changes the grid dimensions (more intuitive than changing the field)
0272     // - Changing the grid dimensions changes the target field dimensions
0273     connect(ui->targetHFOVSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
0274             &Ekos::FramingAssistantUI::updateGridFromTargetFOV);
0275     connect(ui->targetWFOVSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
0276             &Ekos::FramingAssistantUI::updateGridFromTargetFOV);
0277     connect(ui->overlapSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
0278             &Ekos::FramingAssistantUI::updateGridFromTargetFOV);
0279     connect(ui->mosaicWSpin, QOverload<int>::of(&QSpinBox::valueChanged), this,
0280             &Ekos::FramingAssistantUI::updateTargetFOVFromGrid);
0281     connect(ui->mosaicHSpin, QOverload<int>::of(&QSpinBox::valueChanged), this,
0282             &Ekos::FramingAssistantUI::updateTargetFOVFromGrid);
0283 
0284     // Lazy update for s-shape
0285     connect(ui->reverseOddRows, &QCheckBox::toggled, this, [&]()
0286     {
0287         renderedHFOV = 0;
0288         m_DebounceTimer->start();
0289     });
0290 
0291     // Buttons
0292     connect(ui->resetB, &QPushButton::clicked, this, &Ekos::FramingAssistantUI::updateTargetFOVFromGrid);
0293     connect(ui->fetchB, &QPushButton::clicked, this, &FramingAssistantUI::fetchINDIInformation);
0294     connect(ui->createJobsB, &QPushButton::clicked, this, &FramingAssistantUI::createJobs);
0295 
0296     // Job options
0297     connect(ui->alignEvery, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::FramingAssistantUI::rewordStepEvery);
0298     connect(ui->focusEvery, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::FramingAssistantUI::rewordStepEvery);
0299 
0300     // Get INDI Information, if avaialble.
0301     if (tiles->operationMode() == MosaicTiles::MODE_PLANNING)
0302         fetchINDIInformation();
0303 
0304     if (isEquipmentValid())
0305         ui->stackedWidget->setCurrentIndex(PAGE_SELECT_GRID);
0306 
0307     tiles->setOperationMode(MosaicTiles::MODE_PLANNING);
0308 }
0309 
0310 FramingAssistantUI::~FramingAssistantUI()
0311 {
0312     delete m_DebounceTimer;
0313 }
0314 
0315 bool FramingAssistantUI::isEquipmentValid() const
0316 {
0317     return (ui->focalLenSpin->value() > 0 && ui->cameraWSpin->value() > 0 && ui->cameraHSpin->value() > 0 &&
0318             ui->pixelWSizeSpin->value() > 0 && ui->pixelHSizeSpin->value() > 0);
0319 }
0320 
0321 double FramingAssistantUI::getTargetWFOV() const
0322 {
0323     double const xFOV = ui->cameraWFOVSpin->value() * (1 - ui->overlapSpin->value() / 100.0);
0324     return ui->cameraWFOVSpin->value() + xFOV * (ui->mosaicWSpin->value() - 1);
0325 }
0326 
0327 double FramingAssistantUI::getTargetHFOV() const
0328 {
0329     double const yFOV = ui->cameraHFOVSpin->value() * (1 - ui->overlapSpin->value() / 100.0);
0330     return ui->cameraHFOVSpin->value() + yFOV * (ui->mosaicHSpin->value() - 1);
0331 }
0332 
0333 double FramingAssistantUI::getTargetMosaicW() const
0334 {
0335     // If FOV is invalid, or target FOV is null, or target FOV is smaller than camera FOV, we get one tile
0336     if (!isEquipmentValid() || !ui->targetWFOVSpin->value() || ui->targetWFOVSpin->value() <= ui->cameraWFOVSpin->value())
0337         return 1;
0338 
0339     // Else we get one tile, plus as many overlapping camera FOVs in the remnant of the target FOV
0340     double const xFOV = ui->cameraWFOVSpin->value() * (1 - ui->overlapSpin->value() / 100.0);
0341     int const tiles = 1 + ceil((ui->targetWFOVSpin->value() - ui->cameraWFOVSpin->value()) / xFOV);
0342     //Ekos::Manager::Instance()->schedulerModule()->appendLogText(QString("[W] Target FOV %1, camera FOV %2 after overlap %3, %4 tiles.").arg(ui->targetWFOVSpin->value()).arg(ui->cameraWFOVSpin->value()).arg(xFOV).arg(tiles));
0343     return tiles;
0344 }
0345 
0346 double FramingAssistantUI::getTargetMosaicH() const
0347 {
0348     // If FOV is invalid, or target FOV is null, or target FOV is smaller than camera FOV, we get one tile
0349     if (!isEquipmentValid() || !ui->targetHFOVSpin->value() || ui->targetHFOVSpin->value() <= ui->cameraHFOVSpin->value())
0350         return 1;
0351 
0352     // Else we get one tile, plus as many overlapping camera FOVs in the remnant of the target FOV
0353     double const yFOV = ui->cameraHFOVSpin->value() * (1 - ui->overlapSpin->value() / 100.0);
0354     int const tiles = 1 + ceil((ui->targetHFOVSpin->value() - ui->cameraHFOVSpin->value()) / yFOV);
0355     //Ekos::Manager::Instance()->schedulerModule()->appendLogText(QString("[H] Target FOV %1, camera FOV %2 after overlap %3, %4 tiles.").arg(ui->targetHFOVSpin->value()).arg(ui->cameraHFOVSpin->value()).arg(yFOV).arg(tiles));
0356     return tiles;
0357 }
0358 
0359 void FramingAssistantUI::calculateFOV()
0360 {
0361     if (!isEquipmentValid())
0362         return;
0363 
0364     ui->nextToSelectGridB->setEnabled(true);
0365 
0366     ui->targetWFOVSpin->setMinimum(ui->cameraWFOVSpin->value());
0367     ui->targetHFOVSpin->setMinimum(ui->cameraHFOVSpin->value());
0368 
0369     Options::setTelescopeFocalLength(ui->focalLenSpin->value());
0370     Options::setTelescopeFocalReducer(ui->focalReducerSpin->value());
0371     Options::setCameraPixelWidth(ui->pixelWSizeSpin->value());
0372     Options::setCameraPixelHeight(ui->pixelHSizeSpin->value());
0373     Options::setCameraWidth(ui->cameraWSpin->value());
0374     Options::setCameraHeight(ui->cameraHSpin->value());
0375     Options::setCameraRotation(ui->positionAngleSpin->value());
0376 
0377     auto reducedFocalLength = ui->focalLenSpin->value() * ui->focalReducerSpin->value();
0378     // Calculate FOV in arcmins
0379     const auto fov_x = 206264.8062470963552 * ui->cameraWSpin->value() * ui->pixelWSizeSpin->value() / 60000.0 /
0380                        reducedFocalLength;
0381     const auto fov_y = 206264.8062470963552 * ui->cameraHSpin->value() * ui->pixelHSizeSpin->value() / 60000.0 /
0382                        reducedFocalLength;
0383 
0384     ui->cameraWFOVSpin->setValue(fov_x);
0385     ui->cameraHFOVSpin->setValue(fov_y);
0386 
0387     double const target_fov_w = getTargetWFOV();
0388     double const target_fov_h = getTargetHFOV();
0389 
0390     if (ui->targetWFOVSpin->value() < target_fov_w)
0391     {
0392         bool const sig = ui->targetWFOVSpin->blockSignals(true);
0393         ui->targetWFOVSpin->setValue(target_fov_w);
0394         ui->targetWFOVSpin->blockSignals(sig);
0395     }
0396 
0397     if (ui->targetHFOVSpin->value() < target_fov_h)
0398     {
0399         bool const sig = ui->targetHFOVSpin->blockSignals(true);
0400         ui->targetHFOVSpin->setValue(target_fov_h);
0401         ui->targetHFOVSpin->blockSignals(sig);
0402     }
0403 
0404     m_DebounceTimer->start();
0405 }
0406 
0407 void FramingAssistantUI::resetFOV()
0408 {
0409     if (!isEquipmentValid())
0410         return;
0411 
0412     ui->targetWFOVSpin->setValue(getTargetWFOV());
0413     ui->targetHFOVSpin->setValue(getTargetHFOV());
0414 }
0415 
0416 void FramingAssistantUI::updateTargetFOVFromGrid()
0417 {
0418     if (!isEquipmentValid())
0419         return;
0420 
0421     double const targetWFOV = getTargetWFOV();
0422     double const targetHFOV = getTargetHFOV();
0423 
0424     if (ui->targetWFOVSpin->value() != targetWFOV)
0425     {
0426         bool const sig = ui->targetWFOVSpin->blockSignals(true);
0427         ui->targetWFOVSpin->setValue(targetWFOV);
0428         ui->targetWFOVSpin->blockSignals(sig);
0429         m_DebounceTimer->start();
0430     }
0431 
0432     if (ui->targetHFOVSpin->value() != targetHFOV)
0433     {
0434         bool const sig = ui->targetHFOVSpin->blockSignals(true);
0435         ui->targetHFOVSpin->setValue(targetHFOV);
0436         ui->targetHFOVSpin->blockSignals(sig);
0437         m_DebounceTimer->start();
0438     }
0439 }
0440 
0441 void FramingAssistantUI::updateGridFromTargetFOV()
0442 {
0443     if (!isEquipmentValid())
0444         return;
0445 
0446     double const expectedW = getTargetMosaicW();
0447     double const expectedH = getTargetMosaicH();
0448 
0449     if (expectedW != ui->mosaicWSpin->value())
0450     {
0451         bool const sig = ui->mosaicWSpin->blockSignals(true);
0452         ui->mosaicWSpin->setValue(expectedW);
0453         ui->mosaicWSpin->blockSignals(sig);
0454     }
0455 
0456     if (expectedH != ui->mosaicHSpin->value())
0457     {
0458         bool const sig = ui->mosaicHSpin->blockSignals(true);
0459         ui->mosaicHSpin->setValue(expectedH);
0460         ui->mosaicHSpin->blockSignals(sig);
0461     }
0462 
0463     // Update unconditionally, as we may be updating the overlap or the target FOV covered by the mosaic
0464     m_DebounceTimer->start();
0465 }
0466 
0467 void FramingAssistantUI::constructMosaic()
0468 {
0469     m_DebounceTimer->stop();
0470 
0471     if (!isEquipmentValid())
0472         return;
0473 
0474     auto tiles = KStarsData::Instance()->skyComposite()->mosaicComponent()->tiles();
0475     // Set Basic Metadata
0476 
0477     // Center
0478     tiles->setRA0(m_CenterPoint.ra0());
0479     tiles->setDec0(m_CenterPoint.dec0());
0480     tiles->updateCoordsNow(KStarsData::Instance()->updateNum());
0481 
0482     // Grid Size
0483     tiles->setGridSize(QSize(ui->mosaicWSpin->value(), ui->mosaicHSpin->value()));
0484     // Position Angle
0485     tiles->setPositionAngle(ui->positionAngleSpin->value());
0486     // Camera FOV in arcmins
0487     tiles->setCameraFOV(QSizeF(ui->cameraWFOVSpin->value(), ui->cameraHFOVSpin->value()));
0488     // Mosaic overall FOV in arcsmins
0489     tiles->setMosaicFOV(QSizeF(ui->targetWFOVSpin->value(), ui->targetHFOVSpin->value()));
0490     // Overlap in %
0491     tiles->setOverlap(ui->overlapSpin->value());
0492     // Generate Tiles
0493     tiles->createTiles(ui->reverseOddRows->checkState() == Qt::CheckState::Checked);
0494 }
0495 
0496 void FramingAssistantUI::fetchINDIInformation()
0497 {
0498     // Block all signals so we can set the values directly.
0499     for (auto oneWidget : ui->equipment->children())
0500         oneWidget->blockSignals(true);
0501     for (auto oneWidget : ui->createGrid->children())
0502         oneWidget->blockSignals(true);
0503 
0504     QDBusInterface alignInterface("org.kde.kstars",
0505                                   "/KStars/Ekos/Align",
0506                                   "org.kde.kstars.Ekos.Align",
0507                                   QDBusConnection::sessionBus());
0508 
0509     QDBusReply<QList<double>> cameraReply = alignInterface.call("cameraInfo");
0510     if (cameraReply.isValid())
0511     {
0512         QList<double> const values = cameraReply.value();
0513 
0514         m_CameraSize = QSize(values[0], values[1]);
0515         ui->cameraWSpin->setValue(m_CameraSize.width());
0516         ui->cameraHSpin->setValue(m_CameraSize.height());
0517         m_PixelSize = QSizeF(values[2], values[3]);
0518         ui->pixelWSizeSpin->setValue(m_PixelSize.width());
0519         ui->pixelHSizeSpin->setValue(m_PixelSize.height());
0520     }
0521 
0522     QDBusReply<QList<double>> telescopeReply = alignInterface.call("telescopeInfo");
0523     if (telescopeReply.isValid())
0524     {
0525         QList<double> const values = telescopeReply.value();
0526         m_FocalLength = values[0];
0527         m_FocalReducer = values[2];
0528         ui->focalLenSpin->setValue(m_FocalLength);
0529         ui->focalReducerSpin->setValue(m_FocalReducer);
0530     }
0531 
0532     QDBusReply<QList<double>> solutionReply = alignInterface.call("getSolutionResult");
0533     if (solutionReply.isValid())
0534     {
0535         QList<double> const values = solutionReply.value();
0536         if (values[0] > INVALID_VALUE)
0537         {
0538             m_PA = KSUtils::rotationToPositionAngle(values[0]);
0539             ui->positionAngleSpin->setValue(m_PA);
0540         }
0541     }
0542 
0543     calculateFOV();
0544 
0545     // Restore all signals
0546     for (auto oneWidget : ui->equipment->children())
0547         oneWidget->blockSignals(false);
0548     for (auto oneWidget : ui->createGrid->children())
0549         oneWidget->blockSignals(false);
0550 }
0551 
0552 void FramingAssistantUI::rewordStepEvery(int v)
0553 {
0554     QSpinBox * sp = dynamic_cast<QSpinBox *>(sender());
0555     if (0 < v)
0556         sp->setSuffix(i18np(" Scheduler job", " Scheduler jobs", v));
0557     else
0558         sp->setSuffix(i18n(" (first only)"));
0559 }
0560 
0561 void FramingAssistantUI::goAndSolve()
0562 {
0563     // If user click again before solver did not start while GOTO is pending
0564     // let's start solver immediately if the mount is already tracking.
0565     if (m_GOTOSolvePending && m_MountState == ISD::Mount::MOUNT_TRACKING)
0566     {
0567         m_GOTOSolvePending = false;
0568         ui->goSolveB->setStyleSheet("border: 1px outset yellow");
0569         Ekos::Manager::Instance()->alignModule()->captureAndSolve();
0570     }
0571     // Otherwise, initiate GOTO
0572     else
0573     {
0574         Ekos::Manager::Instance()->alignModule()->setSolverAction(Ekos::Align::GOTO_SLEW);
0575         Ekos::Manager::Instance()->mountModule()->gotoTarget(m_CenterPoint);
0576         ui->goSolveB->setStyleSheet("border: 1px outset magenta");
0577         m_GOTOSolvePending = true;
0578     }
0579 }
0580 
0581 void FramingAssistantUI::createJobs()
0582 {
0583     auto scheduler = Ekos::Manager::Instance()->schedulerModule();
0584     auto tiles = KStarsData::Instance()->skyComposite()->mosaicComponent()->tiles();
0585     auto sequence = ui->sequenceEdit->text();
0586     auto outputDirectory = ui->directoryEdit->text();
0587     auto target = ui->targetEdit->text();
0588     auto group = ui->groupEdit->text();
0589 
0590     tiles->setTargetName(target);
0591     tiles->setGroup(group);
0592     tiles->setOutputDirectory(outputDirectory);
0593     tiles->setSequenceFile(sequence);
0594     tiles->setFocusEveryN(ui->focusEvery->value());
0595     tiles->setAlignEveryN(ui->alignEvery->value());
0596     tiles->setStepChecks(ui->trackStepCheck->isChecked(), ui->focusStepCheck->isChecked(),
0597                          ui->alignStepCheck->isChecked(), ui->guideStepCheck->isChecked());
0598 
0599     if (ui->sequenceCompletionR->isChecked())
0600         tiles->setCompletionCondition("FinishSequence", "");
0601     else if (ui->loopCompletionR->isChecked())
0602         tiles->setCompletionCondition("FinishLoop", "");
0603     else if (ui->repeatCompletionR->isChecked())
0604         tiles->setCompletionCondition("FinishRepeat", QString("%1").arg(ui->repeatsSpin->value()));
0605 
0606     tiles->setPositionAngle(ui->positionAngleSpin->value());
0607     // Start by removing any jobs.
0608     scheduler->removeAllJobs();
0609 
0610     QString completionVal, completionArg;
0611 
0612     // Completion values are for all tiles.
0613     completionVal = tiles->completionCondition(&completionArg);
0614     QJsonObject completionSettings;
0615     if (completionVal == "FinishSequence")
0616         completionSettings = {{"sequenceCheck", true}};
0617     else if (completionVal == "FinishRepeat")
0618         completionSettings = {{"repeatCheck", true}, {"repeatRuns", completionArg.toInt()}};
0619     else if (completionVal == "FinishLoop")
0620         completionSettings = {{"loopCheck", true}};
0621 
0622     int batchCount = 0;
0623     for (auto oneTile : tiles->tiles())
0624     {
0625         batchCount++;
0626         XMLEle *root = scheduler->getSequenceJobRoot(sequence);
0627         if (root == nullptr)
0628             return;
0629 
0630         const auto oneTarget = QString("%1-Part_%2").arg(target).arg(batchCount);
0631         if (scheduler->createJobSequence(root, oneTarget, outputDirectory) == false)
0632         {
0633             delXMLEle(root);
0634             return;
0635         }
0636 
0637         delXMLEle(root);
0638         auto oneSequence = QString("%1/%2.esq").arg(outputDirectory, oneTarget);
0639 
0640         // First job should Always focus if possible
0641         bool shouldFocus = ui->focusStepCheck->isChecked() && (batchCount == 1 || (batchCount % ui->focusEvery->value()) == 0);
0642         bool shouldAlign = ui->alignStepCheck->isChecked() && (batchCount == 1 || (batchCount % ui->alignEvery->value()) == 0);
0643         QVariantMap settings =
0644         {
0645             {"nameEdit", oneTarget},
0646             {"groupEdit", tiles->group()},
0647             {"raBox", oneTile->skyCenter.ra0().toHMSString()},
0648             {"decBox", oneTile->skyCenter.dec0().toDMSString()},
0649             // Take care of standard range for position angle
0650             {"positionAngleSpin", KSUtils::rangePA(tiles->positionAngle())},
0651             {"sequenceEdit", oneSequence},
0652             {"schedulerTrackStep", ui->trackStepCheck->isChecked()},
0653             {"schedulerFocusStep", shouldFocus},
0654             {"schedulerFocusStep", shouldAlign},
0655             {"schedulerGuideStep", ui->guideStepCheck->isChecked()}
0656         };
0657 
0658         scheduler->setAllSettings(settings);
0659         scheduler->saveJob();
0660     }
0661 
0662     auto schedulerListFile = QString("%1/%2.esl").arg(outputDirectory, target);
0663     scheduler->saveScheduler(QUrl::fromLocalFile(schedulerListFile));
0664     accept();
0665     Ekos::Manager::Instance()->activateModule(i18n("Scheduler"), true);
0666     scheduler->updateJobTable();
0667 }
0668 
0669 void FramingAssistantUI::setMountState(ISD::Mount::Status value)
0670 {
0671     m_MountState = value;
0672     if (m_GOTOSolvePending && m_MountState == ISD::Mount::MOUNT_TRACKING)
0673     {
0674         m_GOTOSolvePending = false;
0675         ui->goSolveB->setStyleSheet("border: 1px outset yellow");
0676         Ekos::Manager::Instance()->alignModule()->captureAndSolve();
0677     }
0678 }
0679 
0680 void FramingAssistantUI::setAlignState(AlignState value)
0681 {
0682     m_AlignState = value;
0683 
0684     if (m_AlignState == Ekos::ALIGN_COMPLETE)
0685         ui->goSolveB->setStyleSheet("border: 1px outset green");
0686     else if (m_AlignState == Ekos::ALIGN_ABORTED || m_AlignState == Ekos::ALIGN_FAILED)
0687         ui->goSolveB->setStyleSheet("border: 1px outset red");
0688 }
0689 
0690 void FramingAssistantUI::selectSequence()
0691 {
0692     QString file = QFileDialog::getOpenFileName(Ekos::Manager::Instance(), i18nc("@title:window", "Select Sequence Queue"),
0693                    QDir::homePath(),
0694                    i18n("Ekos Sequence Queue (*.esq)"));
0695 
0696     if (!file.isEmpty())
0697     {
0698         ui->sequenceEdit->setText(file);
0699         ui->createJobsB->setEnabled(!ui->targetEdit->text().isEmpty() && !ui->sequenceEdit->text().isEmpty() &&
0700                                     !ui->directoryEdit->text().isEmpty());
0701     }
0702 }
0703 
0704 void FramingAssistantUI::selectImport()
0705 {
0706     QString file = QFileDialog::getOpenFileName(Ekos::Manager::Instance(), i18nc("@title:window", "Select Mosaic Import"),
0707                    QDir::homePath(),
0708                    i18n("Telescopius CSV (*.csv)"));
0709 
0710     if (!file.isEmpty())
0711         parseMosaicCSV(file);
0712 }
0713 
0714 bool FramingAssistantUI::parseMosaicCSV(const QString &filename)
0715 {
0716     QList< QPair<QString, KSParser::DataTypes> > csv_sequence;
0717     csv_sequence.append(qMakePair(QString("Pane"), KSParser::D_QSTRING));
0718     csv_sequence.append(qMakePair(QString("RA"), KSParser::D_QSTRING));
0719     csv_sequence.append(qMakePair(QString("DEC"), KSParser::D_QSTRING));
0720     csv_sequence.append(qMakePair(QString("Position Angle (East)"), KSParser::D_DOUBLE));
0721     csv_sequence.append(qMakePair(QString("Pane width (arcmins)"), KSParser::D_DOUBLE));
0722     csv_sequence.append(qMakePair(QString("Pane height (arcmins)"), KSParser::D_DOUBLE));
0723     csv_sequence.append(qMakePair(QString("Overlap"), KSParser::D_QSTRING));
0724     csv_sequence.append(qMakePair(QString("Row"), KSParser::D_INT));
0725     csv_sequence.append(qMakePair(QString("Column"), KSParser::D_INT));
0726     KSParser csvParser(filename, ',', csv_sequence);
0727 
0728     QHash<QString, QVariant> row_content;
0729     int maxRow = 1, maxCol = 1;
0730     auto haveCenter = false;
0731     while (csvParser.HasNextRow())
0732     {
0733         row_content = csvParser.ReadNextRow();
0734         auto pane = row_content["Pane"].toString();
0735 
0736         // Skip first line
0737         if (pane == "Pane")
0738             continue;
0739 
0740         if (pane != "Center")
0741         {
0742             auto row = row_content["Row"].toInt();
0743             maxRow = qMax(row, maxRow);
0744             auto col = row_content["Column"].toInt();
0745             maxCol = qMax(col, maxCol);
0746             continue;
0747         }
0748 
0749         haveCenter = true;
0750 
0751         auto ra = row_content["RA"].toString().trimmed();
0752         auto dec = row_content["DEC"].toString().trimmed();
0753 
0754         ui->raBox->setText(ra.replace("hr", "h"));
0755         ui->decBox->setText(dec.remove("ยบ"));
0756 
0757         auto pa      = row_content["Position Angle (East)"].toDouble();
0758         ui->positionAngleSpin->setValue(pa);
0759 
0760         // eg. 10% --> 10
0761         auto overlap = row_content["Overlap"].toString().trimmed().midRef(0, 2).toDouble();
0762         ui->overlapSpin->setValue(overlap);
0763     }
0764 
0765     if (haveCenter == false)
0766     {
0767         KSNotification::sorry(i18n("Import must contain center coordinates."), i18n("Sorry"), 15);
0768         return false;
0769     }
0770 
0771     // Set WxH
0772     ui->mosaicWSpin->setValue(maxRow);
0773     ui->mosaicHSpin->setValue(maxCol);
0774     // Set J2000 Center
0775     m_CenterPoint.setRA0(ui->raBox->createDms());
0776     m_CenterPoint.setDec0(ui->decBox->createDms());
0777     m_CenterPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
0778     // Slew to center
0779     SkyMap::Instance()->setDestination(m_CenterPoint);
0780     SkyMap::Instance()->slewFocus();
0781     // Now go to position adjustments
0782     ui->nextToAdjustGrid->click();
0783 
0784     return true;
0785 }
0786 
0787 bool FramingAssistantUI::importMosaic(const QJsonObject &payload)
0788 {
0789     // CSV should contain postion angle, ra/de of each panel, and center coordinates.
0790     auto csv      = payload["csv"].toString();
0791     // Full path to sequence file to be used for imaging.
0792     auto sequence = payload["sequence"].toString();
0793     // Name of target (needs sanitization)
0794     auto target   = payload["target"].toString();
0795     // Jobs directory
0796     auto directory = payload["directory"].toString();
0797 
0798     // Scheduler steps
0799     auto track    = payload["track"].toBool();
0800     auto focus    = payload["focus"].toBool();
0801     auto align    = payload["align"].toBool();
0802     auto guide    = payload["guide"].toBool();
0803 
0804     // Create temporary file to save the CSV info
0805     QTemporaryFile csvFile;
0806     if (!csvFile.open())
0807         return false;
0808     csvFile.write(csv.toUtf8());
0809     csvFile.close();
0810 
0811     // Delete debounce timer since we update all parameters programatically at once
0812     m_DebounceTimer->disconnect();
0813 
0814     if (parseMosaicCSV(csvFile.fileName()) == false)
0815         return false;
0816 
0817     constructMosaic();
0818 
0819     m_JobsDirectory = directory;
0820 
0821     // Set scheduler options.
0822     ui->trackStepCheck->setChecked(track);
0823     ui->focusStepCheck->setChecked(focus);
0824     ui->alignStepCheck->setChecked(align);
0825     ui->guideStepCheck->setChecked(guide);
0826 
0827     ui->sequenceEdit->setText(sequence);
0828     ui->targetEdit->setText(target);
0829 
0830     sanitizeTarget();
0831 
0832     // If create job is still disabled, then some configuation is missing or wrong.
0833     if (ui->createJobsB->isEnabled() == false)
0834         return false;
0835 
0836     // Need to wait a bit since parseMosaicCSV needs to trigger UI
0837     // But button clicks need to be executed first in the event loop
0838     ui->createJobsB->click();
0839 
0840     return true;
0841 }
0842 
0843 void FramingAssistantUI::selectDirectory()
0844 {
0845     m_JobsDirectory = QFileDialog::getExistingDirectory(Ekos::Manager::Instance(), i18nc("@title:window",
0846                       "Select Jobs Directory"),
0847                       QDir::homePath());
0848 
0849     if (!m_JobsDirectory.isEmpty())
0850     {
0851         // If we already have a target specified, then append it to directory path.
0852         QString sanitized = ui->targetEdit->text();
0853         if (sanitized.isEmpty() == false && sanitized != i18n("unnamed"))
0854         {
0855             // Remove illegal characters that can be problematic
0856             sanitized = KSUtils::sanitize(sanitized);
0857             ui->directoryEdit->setText(m_JobsDirectory + QDir::separator() + sanitized);
0858 
0859         }
0860         else
0861             ui->directoryEdit->setText(m_JobsDirectory);
0862 
0863 
0864         ui->createJobsB->setEnabled(!ui->targetEdit->text().isEmpty() && !ui->sequenceEdit->text().isEmpty() &&
0865                                     !ui->directoryEdit->text().isEmpty());
0866     }
0867 }
0868 
0869 void FramingAssistantUI::sanitizeTarget()
0870 {
0871     QString sanitized = ui->targetEdit->text();
0872     if (sanitized != i18n("unnamed"))
0873     {
0874         // Remove illegal characters that can be problematic
0875         sanitized = KSUtils::sanitize(sanitized);
0876         ui->targetEdit->blockSignals(true);
0877         ui->targetEdit->setText(sanitized);
0878         ui->targetEdit->blockSignals(false);
0879 
0880         if (m_JobsDirectory.isEmpty())
0881             ui->directoryEdit->setText(QDir::cleanPath(QDir::homePath() + QDir::separator() + sanitized));
0882         else
0883             ui->directoryEdit->setText(m_JobsDirectory + QDir::separator() + sanitized);
0884 
0885         ui->createJobsB->setEnabled(!ui->targetEdit->text().isEmpty() && !ui->sequenceEdit->text().isEmpty() &&
0886                                     !ui->directoryEdit->text().isEmpty());
0887     }
0888 }
0889 }