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 }