File indexing completed on 2024-09-15 09:13:16
0001 /* 0002 SPDX-FileCopyrightText: 2023 Hy Murveit <hy@murveit.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "imageoverlaycomponent.h" 0008 0009 #include "kstars.h" 0010 #include "Options.h" 0011 #include "skypainter.h" 0012 #include "skymap.h" 0013 #ifdef HAVE_CFITSIO 0014 #include "fitsviewer/fitsdata.h" 0015 #endif 0016 #include "auxiliary/kspaths.h" 0017 #include "ekos/auxiliary/solverutils.h" 0018 #include "ekos/auxiliary/stellarsolverprofile.h" 0019 0020 #include <QTableWidget> 0021 #include <QImageReader> 0022 #include <QCheckBox> 0023 #include <QComboBox> 0024 #include <QtConcurrent> 0025 #include <QRegularExpression> 0026 0027 namespace 0028 { 0029 0030 enum ColumnIndex 0031 { 0032 FILENAME_COL = 0, 0033 //ENABLED_COL, 0034 //NICKNAME_COL, 0035 STATUS_COL, 0036 RA_COL, 0037 DEC_COL, 0038 ARCSEC_PER_PIXEL_COL, 0039 ORIENTATION_COL, 0040 WIDTH_COL, 0041 HEIGHT_COL, 0042 EAST_TO_RIGHT_COL, 0043 NUM_COLUMNS 0044 }; 0045 0046 // These needs to be syncronized with enum Status and initializeGui::StatusNames(). 0047 constexpr int UNPROCESSED_INDEX = 0; 0048 constexpr int OK_INDEX = 4; 0049 0050 // Helper to create the image overlay table. 0051 // Start the table, displaying the heading and timing information, common to all sessions. 0052 void setupTable(QTableWidget *table) 0053 { 0054 table->clear(); 0055 table->setRowCount(0); 0056 table->setColumnCount(NUM_COLUMNS); 0057 table->setShowGrid(false); 0058 table->setWordWrap(true); 0059 0060 QStringList HeaderNames = 0061 { 0062 i18n("Filename"), 0063 // "", "Nickname", 0064 i18n("Status"), i18n("RA"), i18n("DEC"), i18n("A-S/px"), i18n("Angle"), 0065 i18n("Width"), i18n("Height"), i18n("EastRight") 0066 }; 0067 table->setHorizontalHeaderLabels(HeaderNames); 0068 } 0069 0070 // This initializes an item in the table widget. 0071 void setupTableItem(QTableWidget *table, int row, int column, const QString &text, bool editable = true) 0072 { 0073 if (table->rowCount() < row + 1) 0074 table->setRowCount(row + 1); 0075 if (column >= NUM_COLUMNS) 0076 return; 0077 QTableWidgetItem *item = new QTableWidgetItem(); 0078 item->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); 0079 item->setText(text); 0080 if (!editable) 0081 item->setFlags(item->flags() ^ Qt::ItemIsEditable); 0082 table->setItem(row, column, item); 0083 } 0084 0085 // Helper for sorting the overlays alphabetically (case-insensitive). 0086 bool overlaySorter(const ImageOverlay &o1, const ImageOverlay &o2) 0087 { 0088 return o1.m_Filename.toLower() < o2.m_Filename.toLower(); 0089 } 0090 0091 // The dms method for initializing from text requires knowing if the input is degrees or hours. 0092 // This is a crude way to detect HMS input, and may not internationalize well. 0093 bool isHMS(const QString &input) 0094 { 0095 QString trimmedInput = input.trimmed(); 0096 // Just 14h 0097 QRegularExpression re1("^(\\d+)\\s*h$"); 0098 // 14h 2m 0099 QRegularExpression re2("^(\\d+)\\s*h\\s(\\d+)\\s*(?:[m\']?)$"); 0100 // 14h 2m 3.2s 0101 QRegularExpression re3("^(\\d+)\\s*h\\s(\\d+)\\s*[m\'\\s]\\s*(\\d+\\.*\\d*)\\s*([s\"]?)$"); 0102 0103 return re1.match(trimmedInput).hasMatch() || 0104 re2.match(trimmedInput).hasMatch() || 0105 re3.match(trimmedInput).hasMatch(); 0106 } 0107 0108 // Even if an image is already solved, the user may have changed the status in the UI. 0109 bool shouldSolveAnyway(QTableWidget *table, int row) 0110 { 0111 QComboBox *item = dynamic_cast<QComboBox*>(table->cellWidget(row, STATUS_COL)); 0112 if (!item) return false; 0113 return (item->currentIndex() != OK_INDEX); 0114 } 0115 0116 QString toDecString(const dms &dec) 0117 { 0118 // Sadly DMS::setFromString doesn't parse dms::toDMSString() 0119 // return dec.toDMSString(); 0120 return QString("%1 %2' %3\"").arg(dec.degree()).arg(dec.arcmin()).arg(dec.arcsec()); 0121 } 0122 0123 QString toDecString(double dec) 0124 { 0125 return toDecString(dms(dec)); 0126 } 0127 } // namespace 0128 0129 ImageOverlayComponent::ImageOverlayComponent(SkyComposite *parent) : SkyComponent(parent) 0130 { 0131 QDir dir = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/imageOverlays"); 0132 dir.mkpath("."); 0133 m_Directory = dir.absolutePath(); 0134 connect(&m_TryAgainTimer, &QTimer::timeout, this, &ImageOverlayComponent::tryAgain, Qt::UniqueConnection); 0135 connect(this, &ImageOverlayComponent::updateLog, this, &ImageOverlayComponent::updateStatusDisplay, Qt::UniqueConnection); 0136 0137 // Get the latest from the User DB 0138 loadFromUserDB(); 0139 0140 // The rest of the initialization happens when we get the setWidgets() call. 0141 } 0142 0143 // Validate the user inputs, and if invalid, replace with the previous values. 0144 void ImageOverlayComponent::cellChanged(int row, int col) 0145 { 0146 if (!m_Initialized || col < 0 || col >= NUM_COLUMNS || row < 0 || row >= m_ImageOverlayTable->rowCount()) return; 0147 // Note there are a couple columns with widgets instead of normal text items. 0148 // This method shouldn't get called for those, but... 0149 if (col == STATUS_COL || col == EAST_TO_RIGHT_COL) return; 0150 0151 QTableWidgetItem *item = m_ImageOverlayTable->item(row, col); 0152 if (!item) return; 0153 0154 disconnect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged); 0155 QString itemString = item->text(); 0156 auto overlay = m_Overlays[row]; 0157 if (col == RA_COL) 0158 { 0159 dms raDMS; 0160 const bool useHMS = isHMS(itemString); 0161 const bool raOK = raDMS.setFromString(itemString, !useHMS); 0162 if (!raOK) 0163 { 0164 item->setText(dms(overlay.m_RA).toHMSString()); 0165 QString msg = i18n("Bad RA string entered for %1. Reset to original value.", overlay.m_Filename); 0166 emit updateLog(msg); 0167 } 0168 else 0169 // Re-format the user-entered value. 0170 item->setText(raDMS.toHMSString()); 0171 } 0172 else if (col == DEC_COL) 0173 { 0174 dms decDMS; 0175 const bool decOK = decDMS.setFromString(itemString); 0176 if (!decOK) 0177 { 0178 item->setText(toDecString(overlay.m_DEC)); 0179 QString msg = i18n("Bad DEC string entered for %1. Reset to original value.", overlay.m_Filename); 0180 emit updateLog(msg); 0181 } 0182 else 0183 item->setText(toDecString(decDMS)); 0184 } 0185 else if (col == ORIENTATION_COL) 0186 { 0187 bool angleOK = false; 0188 double angle = itemString.toDouble(&angleOK); 0189 if (!angleOK || angle > 360 || angle < -360) 0190 { 0191 item->setText(QString("%1").arg(overlay.m_Orientation, 0, 'f', 2)); 0192 QString msg = i18n("Bad orientation angle string entered for %1. Reset to original value.", overlay.m_Filename); 0193 emit updateLog(msg); 0194 } 0195 } 0196 else if (col == ARCSEC_PER_PIXEL_COL) 0197 { 0198 bool scaleOK = false; 0199 double scale = itemString.toDouble(&scaleOK); 0200 if (!scaleOK || scale < 0 || scale > 1000) 0201 { 0202 item->setText(QString("%1").arg(overlay.m_ArcsecPerPixel, 0, 'f', 2)); 0203 QString msg = i18n("Bad scale angle string entered for %1. Reset to original value.", overlay.m_Filename); 0204 emit updateLog(msg); 0205 } 0206 } 0207 connect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged, Qt::UniqueConnection); 0208 } 0209 0210 // Like cellChanged() but for the status column which contains QComboxBox widgets. 0211 void ImageOverlayComponent::statusCellChanged(int row) 0212 { 0213 if (row < 0 || row >= m_ImageOverlayTable->rowCount()) return; 0214 0215 auto overlay = m_Overlays[row]; 0216 disconnect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged); 0217 0218 // If the user changed the status of a column to OK, 0219 // then we check to make sure the required columns are filled out. 0220 // If so, then we also save it to the DB. 0221 // If the required columns are not filled out, the QComboBox value is reverted to UNPROCESSED. 0222 QComboBox *statusItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(row, STATUS_COL)); 0223 bool failed = false; 0224 if (statusItem->currentIndex() == OK_INDEX) 0225 { 0226 dms raDMS; 0227 QTableWidgetItem *raItem = m_ImageOverlayTable->item(row, RA_COL); 0228 if (!raItem) return; 0229 const bool useHMS = isHMS(raItem->text()); 0230 const bool raOK = raDMS.setFromString(raItem->text(), !useHMS); 0231 if (!raOK || raDMS.Degrees() == 0) 0232 { 0233 QString msg = i18n("Cannot set status to OK. Legal non-0 RA value required."); 0234 emit updateLog(msg); 0235 failed = true; 0236 } 0237 0238 dms decDMS; 0239 QTableWidgetItem *decItem = m_ImageOverlayTable->item(row, DEC_COL); 0240 if (!decItem) return; 0241 const bool decOK = decDMS.setFromString(decItem->text()); 0242 if (!decOK) 0243 { 0244 QString msg = i18n("Cannot set status to OK. Legal non-0 DEC value required."); 0245 emit updateLog(msg); 0246 failed = true; 0247 } 0248 0249 bool angleOK = false; 0250 QTableWidgetItem *angleItem = m_ImageOverlayTable->item(row, ORIENTATION_COL); 0251 if (!angleItem) return; 0252 const double angle = angleItem->text().toDouble(&angleOK); 0253 if (!angleOK || angle > 360 || angle < -360) 0254 { 0255 QString msg = i18n("Cannot set status to OK. Legal orientation value required."); 0256 emit updateLog(msg); 0257 failed = true; 0258 } 0259 0260 bool scaleOK = false; 0261 QTableWidgetItem *scaleItem = m_ImageOverlayTable->item(row, ARCSEC_PER_PIXEL_COL); 0262 if (!scaleItem) return; 0263 const double scale = scaleItem->text().toDouble(&scaleOK); 0264 if (!scaleOK || scale < 0 || scale > 1000) 0265 { 0266 QString msg = i18n("Cannot set status to OK. Legal non-0 a-s/px value required."); 0267 emit updateLog(msg); 0268 failed = true; 0269 } 0270 0271 if (failed) 0272 { 0273 QComboBox *statusItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(row, STATUS_COL)); 0274 statusItem->setCurrentIndex(UNPROCESSED_INDEX); 0275 } 0276 else 0277 { 0278 m_Overlays[row].m_Status = ImageOverlay::AVAILABLE; 0279 m_Overlays[row].m_RA = raDMS.Degrees(); 0280 m_Overlays[row].m_DEC = decDMS.Degrees(); 0281 m_Overlays[row].m_ArcsecPerPixel = scale; 0282 m_Overlays[row].m_Orientation = angle; 0283 const QComboBox *ewItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(row, EAST_TO_RIGHT_COL)); 0284 m_Overlays[row].m_EastToTheRight = ewItem->currentIndex(); 0285 0286 if (m_Overlays[row].m_Img.get() == nullptr) 0287 { 0288 // Load the image. 0289 const QString fullFilename = QString("%1/%2").arg(m_Directory).arg(m_Overlays[row].m_Filename); 0290 QImage *img = loadImageFile(fullFilename, !m_Overlays[row].m_EastToTheRight); 0291 m_Overlays[row].m_Width = img->width(); 0292 m_Overlays[row].m_Height = img->height(); 0293 m_Overlays[row].m_Img.reset(img); 0294 } 0295 saveToUserDB(); 0296 QString msg = i18n("Stored OK status for %1.", m_Overlays[row].m_Filename); 0297 emit updateLog(msg); 0298 } 0299 } 0300 connect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged, Qt::UniqueConnection); 0301 } 0302 0303 ImageOverlayComponent::~ImageOverlayComponent() 0304 { 0305 if (m_LoadImagesFuture.isRunning()) 0306 { 0307 m_LoadImagesFuture.cancel(); 0308 m_LoadImagesFuture.waitForFinished(); 0309 } 0310 } 0311 0312 void ImageOverlayComponent::selectionChanged() 0313 { 0314 if (m_Initialized && Options::showSelectedImageOverlay()) 0315 show(); 0316 } 0317 0318 bool ImageOverlayComponent::selected() 0319 { 0320 return Options::showImageOverlays(); 0321 } 0322 0323 void ImageOverlayComponent::draw(SkyPainter *skyp) 0324 { 0325 #if !defined(KSTARS_LITE) 0326 if (m_Initialized) 0327 skyp->drawImageOverlay(&m_Overlays); 0328 #else 0329 Q_UNUSED(skyp); 0330 #endif 0331 } 0332 0333 void ImageOverlayComponent::setWidgets(QTableWidget *table, QPlainTextEdit *statusDisplay, 0334 QPushButton *solveButton, QGroupBox *tableGroupBox, 0335 QComboBox *solverProfile) 0336 { 0337 m_ImageOverlayTable = table; 0338 // Temporarily make the table uneditable. 0339 m_EditTriggers = m_ImageOverlayTable->editTriggers(); 0340 table->setEditTriggers(QAbstractItemView::NoEditTriggers); 0341 0342 m_SolveButton = solveButton; 0343 m_TableGroupBox = tableGroupBox; 0344 m_SolverProfile = solverProfile; 0345 solveButton->setText(i18n("Solve")); 0346 0347 m_StatusDisplay = statusDisplay; 0348 setupTable(table); 0349 updateTable(); 0350 connect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged, Qt::UniqueConnection); 0351 connect(m_ImageOverlayTable, &QTableWidget::itemSelectionChanged, this, &ImageOverlayComponent::selectionChanged, 0352 Qt::UniqueConnection); 0353 0354 initSolverProfiles(); 0355 loadAllImageFiles(); 0356 } 0357 0358 void ImageOverlayComponent::initSolverProfiles() 0359 { 0360 QString savedOptionsProfiles = QDir(KSPaths::writableLocation( 0361 QStandardPaths::AppLocalDataLocation)).filePath("SavedAlignProfiles.ini"); 0362 0363 QList<SSolver::Parameters> optionsList; 0364 if(QFile(savedOptionsProfiles).exists()) 0365 optionsList = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles); 0366 else 0367 optionsList = Ekos::getDefaultAlignOptionsProfiles(); 0368 0369 m_SolverProfile->clear(); 0370 for(auto ¶m : optionsList) 0371 m_SolverProfile->addItem(param.listName); 0372 m_SolverProfile->setCurrentIndex(Options::solveOptionsProfile()); 0373 } 0374 0375 void ImageOverlayComponent::updateStatusDisplay(const QString &message) 0376 { 0377 if (!m_StatusDisplay) 0378 return; 0379 m_LogText.insert(0, message); 0380 m_StatusDisplay->setPlainText(m_LogText.join("\n")); 0381 } 0382 0383 // Find all the files in the directory, see if they are in the m_Overlays. 0384 // If no, append to the end of m_Overlays, and set status as unprocessed. 0385 void ImageOverlayComponent::updateTable() 0386 { 0387 #ifdef HAVE_CFITSIO 0388 // Get the list of files from the image overlay directory. 0389 QDir directory(m_Directory); 0390 emit updateLog(i18n("Updating from directory: %1", m_Directory)); 0391 QStringList images = directory.entryList(QStringList() << "*", QDir::Files); 0392 QSet<QString> imageFiles; 0393 foreach(QString filename, images) 0394 { 0395 if (!FITSData::readableFilename(filename)) 0396 continue; 0397 imageFiles.insert(filename); 0398 } 0399 0400 // Sort the files alphabetically. 0401 QList<QString> sortedImageFiles; 0402 for (const auto &fn : imageFiles) 0403 sortedImageFiles.push_back(fn); 0404 std::sort(sortedImageFiles.begin(), sortedImageFiles.end(), overlaySorter); 0405 0406 // Remove any overlays that aren't in the directory. 0407 QList<ImageOverlay> tempOverlays; 0408 QMap<QString, int> tempMap; 0409 int numDeleted = 0; 0410 for (int i = 0; i < m_Overlays.size(); ++i) 0411 { 0412 auto &fname = m_Overlays[i].m_Filename; 0413 if (sortedImageFiles.indexOf(fname) >= 0) 0414 { 0415 tempOverlays.append(m_Overlays[i]); 0416 tempMap[fname] = tempOverlays.size() - 1; 0417 } 0418 else 0419 numDeleted++; 0420 } 0421 m_Overlays = tempOverlays; 0422 m_Filenames = tempMap; 0423 0424 // Add the new files into the overlay list. 0425 int numNew = 0; 0426 for (const auto &filename : sortedImageFiles) 0427 { 0428 auto item = m_Filenames.find(filename); 0429 if (item == m_Filenames.end()) 0430 { 0431 // If it doesn't already exist in our database: 0432 ImageOverlay overlay(filename); 0433 const int size = m_Filenames.size(); // place before push_back(). 0434 m_Overlays.push_back(overlay); 0435 m_Filenames[filename] = size; 0436 numNew++; 0437 } 0438 } 0439 emit updateLog(i18n("%1 overlays (%2 new, %3 deleted) %4 solved", m_Overlays.size(), numNew, numDeleted, 0440 numAvailable())); 0441 m_TableGroupBox->setTitle(i18n("Image Overlays. %1 images, %2 available.", m_Overlays.size(), numAvailable())); 0442 0443 initializeGui(); 0444 saveToUserDB(); 0445 #endif 0446 } 0447 0448 void ImageOverlayComponent::loadAllImageFiles() 0449 { 0450 m_LoadImagesFuture = QtConcurrent::run(this, &ImageOverlayComponent::loadImageFileLoop); 0451 } 0452 0453 void ImageOverlayComponent::loadImageFileLoop() 0454 { 0455 emit updateLog(i18n("Loading image files...")); 0456 while (loadImageFile()); 0457 int num = 0; 0458 for (const auto &o : m_Overlays) 0459 if (o.m_Img.get() != nullptr) 0460 num++; 0461 emit updateLog(i18n("%1 image files loaded.", num)); 0462 // Restore editing for the table. 0463 m_ImageOverlayTable->setEditTriggers(m_EditTriggers); 0464 m_Initialized = true; 0465 } 0466 0467 QImage *ImageOverlayComponent::loadImageFile (const QString &fullFilename, bool mirror) 0468 { 0469 QSharedPointer<QImage> tempImage(new QImage(fullFilename)); 0470 if (tempImage.get() == nullptr) return nullptr; 0471 int scaleWidth = std::min(tempImage->width(), Options::imageOverlayMaxDimension()); 0472 QImage *processedImg = new QImage; 0473 if (mirror) 0474 *processedImg = tempImage->mirrored(true, false).scaledToWidth(scaleWidth); // It's reflected horizontally. 0475 else 0476 *processedImg = tempImage->scaledToWidth(scaleWidth); 0477 0478 return processedImg; 0479 } 0480 0481 bool ImageOverlayComponent::loadImageFile() 0482 { 0483 bool updatedSomething = false; 0484 0485 for (auto &o : m_Overlays) 0486 { 0487 if (o.m_Status == o.ImageOverlay::AVAILABLE && o.m_Img.get() == nullptr) 0488 { 0489 QString fullFilename = QString("%1%2%3").arg(m_Directory).arg(QDir::separator()).arg(o.m_Filename); 0490 QImage *img = loadImageFile(fullFilename, !o.m_EastToTheRight); 0491 o.m_Img.reset(img); 0492 updatedSomething = true; 0493 0494 // Note: The original width and height in o.m_Width/m_Height is kept even 0495 // though the image was rescaled. This is to get the rendering right 0496 // with the original scale. 0497 } 0498 } 0499 return updatedSomething; 0500 } 0501 0502 0503 // Copies the info in m_Overlays into m_ImageOverlayTable UI. 0504 void ImageOverlayComponent::initializeGui() 0505 { 0506 if (!m_ImageOverlayTable) return; 0507 0508 // Don't call callback on programmatic changes. 0509 disconnect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged); 0510 0511 // This clears the table. 0512 setupTable(m_ImageOverlayTable); 0513 0514 int row = 0; 0515 for (int i = 0; i < m_Overlays.size(); ++i) 0516 { 0517 const ImageOverlay &overlay = m_Overlays[row]; 0518 // False: The user can't edit filename. 0519 setupTableItem(m_ImageOverlayTable, row, FILENAME_COL, overlay.m_Filename, false); 0520 0521 QStringList StatusNames = 0522 { 0523 i18n("Unprocessed"), i18n("Bad File"), i18n("Solve Failed"), i18n("Error"), i18n("OK") 0524 }; 0525 QComboBox *statusBox = new QComboBox(); 0526 for (int i = 0; i < ImageOverlay::NUM_STATUS; ++i) 0527 statusBox->addItem(StatusNames[i]); 0528 connect(statusBox, QOverload<int>::of(&QComboBox::activated), this, [row, this](int newIndex) 0529 { 0530 Q_UNUSED(newIndex); 0531 statusCellChanged(row); 0532 }); 0533 statusBox->setCurrentIndex(static_cast<int>(overlay.m_Status)); 0534 m_ImageOverlayTable->setCellWidget(row, STATUS_COL, statusBox); 0535 0536 setupTableItem(m_ImageOverlayTable, row, ORIENTATION_COL, QString("%1").arg(overlay.m_Orientation, 0, 'f', 2)); 0537 setupTableItem(m_ImageOverlayTable, row, RA_COL, dms(overlay.m_RA).toHMSString()); 0538 setupTableItem(m_ImageOverlayTable, row, DEC_COL, toDecString(overlay.m_DEC)); 0539 setupTableItem(m_ImageOverlayTable, row, ARCSEC_PER_PIXEL_COL, QString("%1").arg(overlay.m_ArcsecPerPixel, 0, 'f', 2)); 0540 0541 // The user can't edit width & height--taken from image. 0542 setupTableItem(m_ImageOverlayTable, row, WIDTH_COL, QString("%1").arg(overlay.m_Width), false); 0543 setupTableItem(m_ImageOverlayTable, row, HEIGHT_COL, QString("%1").arg(overlay.m_Height), false); 0544 0545 QComboBox *mirroredBox = new QComboBox(); 0546 mirroredBox->addItem(i18n("West-Right")); 0547 mirroredBox->addItem(i18n("East-Right")); 0548 connect(mirroredBox, QOverload<int>::of(&QComboBox::activated), this, [row](int newIndex) 0549 { 0550 Q_UNUSED(row); 0551 Q_UNUSED(newIndex); 0552 // Don't need to do anything. Will get picked up on change of status to OK. 0553 }); 0554 mirroredBox->setCurrentIndex(overlay.m_EastToTheRight ? 1 : 0); 0555 m_ImageOverlayTable->setCellWidget(row, EAST_TO_RIGHT_COL, mirroredBox); 0556 0557 row++; 0558 } 0559 m_ImageOverlayTable->resizeColumnsToContents(); 0560 m_TableGroupBox->setTitle(i18n("Image Overlays. %1 images, %2 available.", m_Overlays.size(), numAvailable())); 0561 connect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged, Qt::UniqueConnection); 0562 0563 } 0564 0565 void ImageOverlayComponent::loadFromUserDB() 0566 { 0567 QList<ImageOverlay *> list; 0568 KStarsData::Instance()->userdb()->GetAllImageOverlays(&m_Overlays); 0569 // Alphabetize. 0570 std::sort(m_Overlays.begin(), m_Overlays.end(), overlaySorter); 0571 m_Filenames.clear(); 0572 int index = 0; 0573 for (const auto &o : m_Overlays) 0574 { 0575 m_Filenames[o.m_Filename] = index; 0576 index++; 0577 } 0578 } 0579 0580 void ImageOverlayComponent::saveToUserDB() 0581 { 0582 KStarsData::Instance()->userdb()->DeleteAllImageOverlays(); 0583 for (const ImageOverlay &metadata : m_Overlays) 0584 KStarsData::Instance()->userdb()->AddImageOverlay(metadata); 0585 } 0586 0587 void ImageOverlayComponent::solveImage(const QString &filename) 0588 { 0589 if (!m_Initialized) return; 0590 m_SolveButton->setText(i18n("Abort")); 0591 const int solverTimeout = Options::imageOverlayTimeout(); 0592 auto profiles = Ekos::getDefaultAlignOptionsProfiles(); 0593 auto parameters = profiles.at(m_SolverProfile->currentIndex()); 0594 // Double search radius 0595 parameters.search_radius = parameters.search_radius * 2; 0596 0597 m_Solver.reset(new SolverUtils(parameters, solverTimeout), &QObject::deleteLater); 0598 connect(m_Solver.get(), &SolverUtils::done, this, &ImageOverlayComponent::solverDone, Qt::UniqueConnection); 0599 0600 if (m_RowsToSolve.size() > 1) 0601 emit updateLog(i18n("Solving: %1. %2 in queue.", filename, m_RowsToSolve.size())); 0602 else 0603 emit updateLog(i18n("Solving: %1.", filename)); 0604 0605 // If the user added some RA/DEC/Scale values to the table, they will be used in the solve 0606 // (but aren't remembered in the DB unless the solve is successful). 0607 int row = m_RowsToSolve[0]; 0608 QString raString = m_ImageOverlayTable->item(row, RA_COL)->text().toLatin1().data(); 0609 QString decString = m_ImageOverlayTable->item(row, DEC_COL)->text().toLatin1().data(); 0610 QString scaleString = m_ImageOverlayTable->item(row, ARCSEC_PER_PIXEL_COL)->text().toLatin1().data(); 0611 0612 dms raDMS, decDMS; 0613 const bool useHMS = isHMS(raString); 0614 const bool raOK = raDMS.setFromString(raString, !useHMS) && (raDMS.Degrees() != 0.00); 0615 const bool decOK = decDMS.setFromString(decString) && (decDMS.Degrees() != 0.00); 0616 bool scaleOK = false; 0617 double scale = scaleString.toDouble(&scaleOK); 0618 scaleOK = scaleOK && scale != 0.00; 0619 0620 // Use the default scale if it is > 0 and scale was not specified in the UI table. 0621 if (!scaleOK && Options::imageOverlayDefaultScale() > 0.0001) 0622 { 0623 scale = Options::imageOverlayDefaultScale(); 0624 scaleOK = true; 0625 } 0626 0627 if (scaleOK) 0628 { 0629 auto lowScale = scale * 0.75; 0630 auto highScale = scale * 1.25; 0631 m_Solver->useScale(true, lowScale, highScale); 0632 } 0633 if (raOK && decOK) 0634 m_Solver->usePosition(true, raDMS.Degrees(), decDMS.Degrees()); 0635 0636 m_Solver->runSolver(filename); 0637 } 0638 0639 void ImageOverlayComponent::tryAgain() 0640 { 0641 m_TryAgainTimer.stop(); 0642 if (!m_Initialized) return; 0643 if (m_RowsToSolve.size() > 0) 0644 startSolving(); 0645 } 0646 0647 int ImageOverlayComponent::numAvailable() 0648 { 0649 int num = 0; 0650 for (const auto &o : m_Overlays) 0651 if (o.m_Status == ImageOverlay::AVAILABLE) 0652 num++; 0653 return num; 0654 } 0655 0656 void ImageOverlayComponent::show() 0657 { 0658 if (!m_Initialized || !m_ImageOverlayTable) return; 0659 auto selections = m_ImageOverlayTable->selectionModel(); 0660 if (selections->hasSelection()) 0661 { 0662 auto selectedIndexes = selections->selectedIndexes(); 0663 const int row = selectedIndexes.at(0).row(); 0664 if (m_Overlays.size() > row && row >= 0) 0665 { 0666 if (m_Overlays[row].m_Status != ImageOverlay::AVAILABLE) 0667 { 0668 emit updateLog(i18n("Can't show %1. Not plate solved.", m_Overlays[row].m_Filename)); 0669 return; 0670 } 0671 if (m_Overlays[row].m_Img.get() == nullptr) 0672 { 0673 emit updateLog(i18n("Can't show %1. Image not loaded.", m_Overlays[row].m_Filename)); 0674 return; 0675 } 0676 const double ra = m_Overlays[row].m_RA; 0677 const double dec = m_Overlays[row].m_DEC; 0678 0679 // Convert the RA/DEC from j2000 to jNow. 0680 auto localTime = KStarsData::Instance()->geo()->UTtoLT(KStarsData::Instance()->clock()->utc()); 0681 const dms raDms(ra), decDms(dec); 0682 SkyPoint coord(raDms, decDms); 0683 coord.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd()); 0684 0685 // Is this the right way to move to the SkyMap? 0686 Options::setIsTracking(false); 0687 SkyMap::Instance()->setFocusObject(nullptr); 0688 SkyMap::Instance()->setFocusPoint(nullptr); 0689 SkyMap::Instance()->setFocus(dms(coord.ra()), dms(coord.dec())); 0690 0691 // Zoom factor is in pixels per radian. 0692 double zoomFactor = (400 * 60.0 * 10800.0) / (m_Overlays[row].m_Width * m_Overlays[row].m_ArcsecPerPixel * dms::PI); 0693 SkyMap::Instance()->setZoomFactor(zoomFactor); 0694 0695 SkyMap::Instance()->forceUpdate(true); 0696 } 0697 } 0698 } 0699 0700 void ImageOverlayComponent::abortSolving() 0701 { 0702 if (!m_Initialized) return; 0703 m_RowsToSolve.clear(); 0704 if (m_Solver) 0705 m_Solver->abort(); 0706 emit updateLog(i18n("Solving aborted.")); 0707 m_SolveButton->setText(i18n("Solve")); 0708 } 0709 0710 void ImageOverlayComponent::startSolving() 0711 { 0712 if (!m_Initialized) return; 0713 if (m_SolveButton->text() == i18n("Abort")) 0714 { 0715 abortSolving(); 0716 return; 0717 } 0718 if (m_Solver && m_Solver->isRunning()) 0719 { 0720 m_Solver->abort(); 0721 if (m_RowsToSolve.size() > 0) 0722 m_TryAgainTimer.start(2000); 0723 return; 0724 } 0725 0726 if (m_RowsToSolve.size() == 0) 0727 { 0728 QSet<int> selectedRows; 0729 auto selections = m_ImageOverlayTable->selectionModel(); 0730 if (selections->hasSelection()) 0731 { 0732 // Need to de-dup, as selecting the whole row will select all the columns. 0733 auto selectedIndexes = selections->selectedIndexes(); 0734 for (int i = 0; i < selectedIndexes.count(); ++i) 0735 { 0736 // Don't insert a row that's already solved. 0737 const int row = selectedIndexes.at(i).row(); 0738 if ((m_Overlays[row].m_Status == ImageOverlay::AVAILABLE) && 0739 !shouldSolveAnyway(m_ImageOverlayTable, row)) 0740 { 0741 emit updateLog(i18n("Skipping already solved: %1.", m_Overlays[row].m_Filename)); 0742 continue; 0743 } 0744 selectedRows.insert(row); 0745 } 0746 } 0747 m_RowsToSolve.clear(); 0748 for (int row : selectedRows) 0749 m_RowsToSolve.push_back(row); 0750 } 0751 0752 if (m_RowsToSolve.size() > 0) 0753 { 0754 const int row = m_RowsToSolve[0]; 0755 const QString filename = 0756 QString("%1/%2").arg(m_Directory).arg(m_Overlays[row].m_Filename); 0757 if ((m_Overlays[row].m_Status == ImageOverlay::AVAILABLE) && 0758 !shouldSolveAnyway(m_ImageOverlayTable, row)) 0759 { 0760 emit updateLog(i18n("%1 already solved. Skipping.", filename)); 0761 m_RowsToSolve.removeFirst(); 0762 if (m_RowsToSolve.size() > 0) 0763 startSolving(); 0764 return; 0765 } 0766 0767 auto img = new QImage(filename); 0768 m_Overlays[row].m_Width = img->width(); 0769 m_Overlays[row].m_Height = img->height(); 0770 solveImage(filename); 0771 } 0772 } 0773 0774 void ImageOverlayComponent::reload() 0775 { 0776 if (!m_Initialized) return; 0777 m_Initialized = false; 0778 emit updateLog(i18n("Reloading. Image overlays temporarily disabled.")); 0779 updateTable(); 0780 loadAllImageFiles(); 0781 } 0782 0783 void ImageOverlayComponent::solverDone(bool timedOut, bool success, const FITSImage::Solution &solution, 0784 double elapsedSeconds) 0785 { 0786 disconnect(m_Solver.get(), &SolverUtils::done, this, &ImageOverlayComponent::solverDone); 0787 m_SolveButton->setText(i18n("Solve")); 0788 if (m_RowsToSolve.size() == 0) 0789 return; 0790 0791 const int solverRow = m_RowsToSolve[0]; 0792 m_RowsToSolve.removeFirst(); 0793 0794 QComboBox *statusItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(solverRow, STATUS_COL)); 0795 if (timedOut) 0796 { 0797 emit updateLog(i18n("Solver timed out in %1s", QString::number(elapsedSeconds, 'f', 1))); 0798 m_Overlays[solverRow].m_Status = ImageOverlay::PLATE_SOLVE_FAILURE; 0799 statusItem->setCurrentIndex(static_cast<int>(m_Overlays[solverRow].m_Status)); 0800 } 0801 else if (!success) 0802 { 0803 emit updateLog(i18n("Solver failed in %1s", QString::number(elapsedSeconds, 'f', 1))); 0804 m_Overlays[solverRow].m_Status = ImageOverlay::PLATE_SOLVE_FAILURE; 0805 statusItem->setCurrentIndex(static_cast<int>(m_Overlays[solverRow].m_Status)); 0806 } 0807 else 0808 { 0809 m_Overlays[solverRow].m_Orientation = solution.orientation; 0810 m_Overlays[solverRow].m_RA = solution.ra; 0811 m_Overlays[solverRow].m_DEC = solution.dec; 0812 m_Overlays[solverRow].m_ArcsecPerPixel = solution.pixscale; 0813 m_Overlays[solverRow].m_EastToTheRight = solution.parity; 0814 m_Overlays[solverRow].m_Status = ImageOverlay::AVAILABLE; 0815 0816 QString msg = i18n("Solver success in %1s: RA %2 DEC %3 Scale %4 Angle %5", 0817 QString::number(elapsedSeconds, 'f', 1), 0818 QString::number(solution.ra, 'f', 2), 0819 QString::number(solution.dec, 'f', 2), 0820 QString::number(solution.pixscale, 'f', 2), 0821 QString::number(solution.orientation, 'f', 2)); 0822 emit updateLog(msg); 0823 0824 // Store the new values in the table. 0825 auto overlay = m_Overlays[solverRow]; 0826 m_ImageOverlayTable->item(solverRow, RA_COL)->setText(dms(overlay.m_RA).toHMSString()); 0827 m_ImageOverlayTable->item(solverRow, DEC_COL)->setText(toDecString(overlay.m_DEC)); 0828 m_ImageOverlayTable->item(solverRow, ARCSEC_PER_PIXEL_COL)->setText( 0829 QString("%1").arg(overlay.m_ArcsecPerPixel, 0, 'f', 2)); 0830 m_ImageOverlayTable->item(solverRow, ORIENTATION_COL)->setText(QString("%1").arg(overlay.m_Orientation, 0, 'f', 2)); 0831 QComboBox *ewItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(solverRow, EAST_TO_RIGHT_COL)); 0832 ewItem->setCurrentIndex(overlay.m_EastToTheRight ? 1 : 0); 0833 0834 QComboBox *statusItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(solverRow, STATUS_COL)); 0835 statusItem->setCurrentIndex(static_cast<int>(overlay.m_Status)); 0836 0837 // Load the image. 0838 QString fullFilename = QString("%1/%2").arg(m_Directory).arg(m_Overlays[solverRow].m_Filename); 0839 QImage *img = loadImageFile(fullFilename, !m_Overlays[solverRow].m_EastToTheRight); 0840 m_Overlays[solverRow].m_Img.reset(img); 0841 } 0842 saveToUserDB(); 0843 0844 if (m_RowsToSolve.size() > 0) 0845 startSolving(); 0846 else 0847 { 0848 emit updateLog(i18n("Done solving. %1 available.", numAvailable())); 0849 m_TableGroupBox->setTitle(i18n("Image Overlays. %1 images, %2 available.", m_Overlays.size(), numAvailable())); 0850 } 0851 }