File indexing completed on 2024-04-21 03:43:55

0001 /*
0002     SPDX-FileCopyrightText: 2004 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     2006-03-03  Using CFITSIO, Porting to Qt4
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "fitsviewer.h"
0010 
0011 #include "config-kstars.h"
0012 
0013 #include "fitsdata.h"
0014 #include "fitsdebayer.h"
0015 #include "fitstab.h"
0016 #include "fitsview.h"
0017 #include "kstars.h"
0018 #include "ksutils.h"
0019 #include "Options.h"
0020 #ifdef HAVE_INDI
0021 #include "indi/indilistener.h"
0022 #endif
0023 
0024 #include <KActionCollection>
0025 #include <KMessageBox>
0026 #include <KToolBar>
0027 #include <KNotifications/KStatusNotifierItem>
0028 
0029 #ifndef KSTARS_LITE
0030 #include "fitshistogrameditor.h"
0031 #endif
0032 
0033 #include <fits_debug.h>
0034 
0035 #define INITIAL_W 785
0036 #define INITIAL_H 640
0037 
0038 bool FITSViewer::m_BlinkBusy = false;
0039 
0040 QStringList FITSViewer::filterTypes =
0041     QStringList() << I18N_NOOP("Auto Stretch") << I18N_NOOP("High Contrast") << I18N_NOOP("Equalize")
0042     << I18N_NOOP("High Pass") << I18N_NOOP("Median") << I18N_NOOP("Gaussian blur")
0043     << I18N_NOOP("Rotate Right") << I18N_NOOP("Rotate Left") << I18N_NOOP("Flip Horizontal")
0044     << I18N_NOOP("Flip Vertical");
0045 
0046 FITSViewer::FITSViewer(QWidget *parent) : KXmlGuiWindow(parent)
0047 {
0048 #ifdef Q_OS_OSX
0049     if (Options::independentWindowFITS())
0050         setWindowFlags(Qt::Window);
0051     else
0052     {
0053         setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint);
0054         connect(QApplication::instance(), SIGNAL(applicationStateChanged(Qt::ApplicationState)), this,
0055                 SLOT(changeAlwaysOnTop(Qt::ApplicationState)));
0056     }
0057 #endif
0058 
0059     // Since QSharedPointer is managing it, do not delete automatically.
0060     setAttribute(Qt::WA_DeleteOnClose, false);
0061 
0062     fitsTabWidget   = new QTabWidget(this);
0063     undoGroup = new QUndoGroup(this);
0064 
0065     lastURL = QUrl(QDir::homePath());
0066 
0067     fitsTabWidget->setTabsClosable(true);
0068 
0069     setWindowIcon(QIcon::fromTheme("kstars_fitsviewer"));
0070 
0071     setCentralWidget(fitsTabWidget);
0072 
0073     connect(fitsTabWidget, &QTabWidget::currentChanged, this, &FITSViewer::tabFocusUpdated);
0074     connect(fitsTabWidget, &QTabWidget::tabCloseRequested, this, &FITSViewer::closeTab);
0075 
0076     //These two connections will enable or disable the scope button if a scope is available or not.
0077     //Of course this is also dependent on the presence of WCS data in the image.
0078 
0079 #ifdef HAVE_INDI
0080     connect(INDIListener::Instance(), &INDIListener::newDevice, this, &FITSViewer::updateWCSFunctions);
0081     connect(INDIListener::Instance(), &INDIListener::newDevice, this, &FITSViewer::updateWCSFunctions);
0082 #endif
0083 
0084     led.setColor(Qt::green);
0085 
0086     fitsPosition.setAlignment(Qt::AlignCenter);
0087     fitsPosition.setMinimumWidth(100);
0088     fitsValue.setAlignment(Qt::AlignCenter);
0089     fitsValue.setMinimumWidth(40);
0090 
0091     fitsWCS.setVisible(false);
0092 
0093     statusBar()->insertPermanentWidget(FITS_CLIP, &fitsClip);
0094     statusBar()->insertPermanentWidget(FITS_HFR, &fitsHFR);
0095     statusBar()->insertPermanentWidget(FITS_WCS, &fitsWCS);
0096     statusBar()->insertPermanentWidget(FITS_VALUE, &fitsValue);
0097     statusBar()->insertPermanentWidget(FITS_POSITION, &fitsPosition);
0098     statusBar()->insertPermanentWidget(FITS_ZOOM, &fitsZoom);
0099     statusBar()->insertPermanentWidget(FITS_RESOLUTION, &fitsResolution);
0100     statusBar()->insertPermanentWidget(FITS_LED, &led);
0101 
0102     QAction *action = actionCollection()->addAction("rotate_right", this, &FITSViewer::rotateCW);
0103 
0104     action->setText(i18n("Rotate Right"));
0105     action->setIcon(QIcon::fromTheme("object-rotate-right"));
0106 
0107     action = actionCollection()->addAction("rotate_left", this, &FITSViewer::rotateCCW);
0108     action->setText(i18n("Rotate Left"));
0109     action->setIcon(QIcon::fromTheme("object-rotate-left"));
0110 
0111     action = actionCollection()->addAction("flip_horizontal", this, &FITSViewer::flipHorizontal);
0112     action->setText(i18n("Flip Horizontal"));
0113     action->setIcon(
0114         QIcon::fromTheme("object-flip-horizontal"));
0115 
0116     action = actionCollection()->addAction("flip_vertical", this, &FITSViewer::flipVertical);
0117     action->setText(i18n("Flip Vertical"));
0118     action->setIcon(QIcon::fromTheme("object-flip-vertical"));
0119 
0120     action = actionCollection()->addAction("image_histogram");
0121     action->setText(i18n("Histogram"));
0122     connect(action, &QAction::triggered, this, &FITSViewer::histoFITS);
0123     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_T));
0124 
0125     action->setIcon(QIcon(":/icons/histogram.png"));
0126 
0127     action = KStandardAction::open(this, &FITSViewer::openFile, actionCollection());
0128     action->setIcon(QIcon::fromTheme("document-open"));
0129 
0130     action = actionCollection()->addAction("blink");
0131     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_O + Qt::AltModifier));
0132     action->setText(i18n("Open/Blink Directory"));
0133     connect(action, &QAction::triggered, this, &FITSViewer::blink);
0134 
0135     saveFileAction = KStandardAction::save(this, &FITSViewer::saveFile, actionCollection());
0136     saveFileAction->setIcon(QIcon::fromTheme("document-save"));
0137 
0138     saveFileAsAction = KStandardAction::saveAs(this, &FITSViewer::saveFileAs, actionCollection());
0139     saveFileAsAction->setIcon(
0140         QIcon::fromTheme("document-save_as"));
0141 
0142     action = actionCollection()->addAction("fits_header");
0143     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_H));
0144     action->setIcon(QIcon::fromTheme("document-properties"));
0145     action->setText(i18n("FITS Header"));
0146     connect(action, &QAction::triggered, this, &FITSViewer::headerFITS);
0147 
0148     action = actionCollection()->addAction("fits_debayer");
0149     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_D));
0150     action->setIcon(QIcon::fromTheme("view-preview"));
0151     action->setText(i18n("Debayer..."));
0152     connect(action, &QAction::triggered, this, &FITSViewer::debayerFITS);
0153 
0154     action = KStandardAction::close(this, &FITSViewer::close, actionCollection());
0155     action->setIcon(QIcon::fromTheme("window-close"));
0156 
0157     action = KStandardAction::copy(this, &FITSViewer::copyFITS, actionCollection());
0158     action->setIcon(QIcon::fromTheme("edit-copy"));
0159 
0160     action = KStandardAction::zoomIn(this, &FITSViewer::ZoomIn, actionCollection());
0161     action->setIcon(QIcon::fromTheme("zoom-in"));
0162 
0163     action = KStandardAction::zoomOut(this, &FITSViewer::ZoomOut, actionCollection());
0164     action->setIcon(QIcon::fromTheme("zoom-out"));
0165 
0166     action = KStandardAction::actualSize(this, &FITSViewer::ZoomDefault, actionCollection());
0167     action->setIcon(QIcon::fromTheme("zoom-fit-best"));
0168 
0169     QAction *kundo = KStandardAction::undo(undoGroup, &QUndoGroup::undo, actionCollection());
0170     kundo->setIcon(QIcon::fromTheme("edit-undo"));
0171 
0172     QAction *kredo = KStandardAction::redo(undoGroup, &QUndoGroup::redo, actionCollection());
0173     kredo->setIcon(QIcon::fromTheme("edit-redo"));
0174 
0175     connect(undoGroup, &QUndoGroup::canUndoChanged, kundo, &QAction::setEnabled);
0176     connect(undoGroup, &QUndoGroup::canRedoChanged, kredo, &QAction::setEnabled);
0177 
0178     action = actionCollection()->addAction("image_stats");
0179     action->setIcon(QIcon::fromTheme("view-statistics"));
0180     action->setText(i18n("Statistics"));
0181     connect(action, &QAction::triggered, this, &FITSViewer::statFITS);
0182 
0183     action = actionCollection()->addAction("image_roi_stats");
0184 
0185     roiActionMenu = new KActionMenu(QIcon(":/icons/select_stat"), "Selection Statistics", action );
0186     roiActionMenu->setText(i18n("&Selection Statistics"));
0187     roiActionMenu->setDelayed(false);
0188     roiActionMenu->addSeparator();
0189     connect(roiActionMenu, &QAction::triggered, this, &FITSViewer::toggleSelectionMode);
0190 
0191     KToggleAction *ksa = actionCollection()->add<KToggleAction>("100x100");
0192     ksa->setText("100x100");
0193     ksa->setCheckable(false);
0194     roiActionMenu->addAction(ksa);
0195     ksa = actionCollection()->add<KToggleAction>("50x50");
0196     ksa->setText("50x50");
0197     ksa->setCheckable(false);
0198     roiActionMenu->addAction(ksa);
0199     ksa = actionCollection()->add<KToggleAction>("25x25");
0200     ksa->setText("25x25");
0201     ksa->setCheckable(false);
0202     roiActionMenu->addAction(ksa);
0203     ksa = actionCollection()->add<KToggleAction>("CustomRoi");
0204     ksa->setText("Custom");
0205     ksa->setCheckable(false);
0206     roiActionMenu->addAction(ksa);
0207 
0208     action->setMenu(roiActionMenu->menu());
0209     action->setIcon(QIcon(":/icons/select_stat"));
0210     action->setCheckable(true);
0211 
0212     connect(roiActionMenu->menu()->actions().at(1), &QAction::triggered, this, [this] { ROIFixedSize(100); });
0213     connect(roiActionMenu->menu()->actions().at(2), &QAction::triggered, this, [this] { ROIFixedSize(50); });
0214     connect(roiActionMenu->menu()->actions().at(3), &QAction::triggered, this, [this] { ROIFixedSize(25); });
0215     connect(roiActionMenu->menu()->actions().at(4), &QAction::triggered, this, [this] { customROIInputWindow();});
0216     connect(action, &QAction::triggered, this, &FITSViewer::toggleSelectionMode);
0217 
0218     action = actionCollection()->addAction("view_crosshair");
0219     action->setIcon(QIcon::fromTheme("crosshairs"));
0220     action->setText(i18n("Show Cross Hairs"));
0221     action->setCheckable(true);
0222     connect(action, &QAction::triggered, this, &FITSViewer::toggleCrossHair);
0223 
0224     action = actionCollection()->addAction("view_clipping");
0225     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_L));
0226     action->setIcon(QIcon::fromTheme("media-record"));
0227     action->setText(i18n("Show Clipping"));
0228     action->setCheckable(true);
0229     connect(action, &QAction::triggered, this, &FITSViewer::toggleClipping);
0230 
0231     action = actionCollection()->addAction("view_pixel_grid");
0232     action->setIcon(QIcon::fromTheme("map-flat"));
0233     action->setText(i18n("Show Pixel Gridlines"));
0234     action->setCheckable(true);
0235     connect(action, &QAction::triggered, this, &FITSViewer::togglePixelGrid);
0236 
0237     action = actionCollection()->addAction("view_eq_grid");
0238     action->setIcon(QIcon::fromTheme("kstars_grid"));
0239     action->setText(i18n("Show Equatorial Gridlines"));
0240     action->setCheckable(true);
0241     action->setDisabled(true);
0242     connect(action, &QAction::triggered, this, &FITSViewer::toggleEQGrid);
0243 
0244     action = actionCollection()->addAction("view_objects");
0245     action->setIcon(QIcon::fromTheme("help-hint"));
0246     action->setText(i18n("Show Objects in Image"));
0247     action->setCheckable(true);
0248     action->setDisabled(true);
0249     connect(action, &QAction::triggered, this, &FITSViewer::toggleObjects);
0250 
0251     action = actionCollection()->addAction("view_hips_overlay");
0252     action->setIcon(QIcon::fromTheme("pixelate"));
0253     action->setText(i18n("Show HiPS Overlay"));
0254     action->setCheckable(true);
0255     action->setDisabled(true);
0256     connect(action, &QAction::triggered, this, &FITSViewer::toggleHiPSOverlay);
0257 
0258     action = actionCollection()->addAction("center_telescope");
0259     action->setIcon(QIcon(":/icons/center_telescope.svg"));
0260     action->setText(i18n("Center Telescope\n*No Telescopes Detected*"));
0261     action->setDisabled(true);
0262     action->setCheckable(true);
0263     connect(action, &QAction::triggered, this, &FITSViewer::centerTelescope);
0264 
0265     action = actionCollection()->addAction("view_zoom_fit");
0266     action->setIcon(QIcon::fromTheme("zoom-fit-width"));
0267     action->setText(i18n("Zoom To Fit"));
0268     connect(action, &QAction::triggered, this, &FITSViewer::ZoomToFit);
0269 
0270     action = actionCollection()->addAction("next_tab");
0271     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_Tab));
0272     action->setText(i18n("Next Tab"));
0273     connect(action, &QAction::triggered, this, &FITSViewer::nextTab);
0274 
0275     action = actionCollection()->addAction("previous_tab");
0276     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_Tab + Qt::ShiftModifier));
0277     action->setText(i18n("Previous Tab"));
0278     connect(action, &QAction::triggered, this, &FITSViewer::previousTab);
0279 
0280     action = actionCollection()->addAction("next_blink");
0281     actionCollection()->setDefaultShortcut(action, QKeySequence(QKeySequence::SelectNextWord));
0282     action->setText(i18n("Next Blink Image"));
0283     connect(action, &QAction::triggered, this, &FITSViewer::nextBlink);
0284 
0285     action = actionCollection()->addAction("previous_blink");
0286     actionCollection()->setDefaultShortcut(action, QKeySequence(QKeySequence::SelectPreviousWord));
0287     action->setText(i18n("Previous Blink Image"));
0288     connect(action, &QAction::triggered, this, &FITSViewer::previousBlink);
0289 
0290     action = actionCollection()->addAction("zoom_all_in");
0291     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_Plus + Qt::AltModifier));
0292     action->setText(i18n("Zoom all tabs in"));
0293     connect(action, &QAction::triggered, this, &FITSViewer::ZoomAllIn);
0294 
0295     action = actionCollection()->addAction("zoom_all_out");
0296     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_Minus + Qt::AltModifier));
0297     action->setText(i18n("Zoom all tabs out"));
0298     connect(action, &QAction::triggered, this, &FITSViewer::ZoomAllOut);
0299 
0300     action = actionCollection()->addAction("mark_stars");
0301     action->setIcon(QIcon::fromTheme("glstarbase", QIcon(":/icons/glstarbase.png")));
0302     action->setText(i18n("Mark Stars"));
0303     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_A));
0304     action->setCheckable(true);
0305     connect(action, &QAction::triggered, this, &FITSViewer::toggleStars);
0306 
0307 #ifdef HAVE_DATAVISUALIZATION
0308     action = actionCollection()->addAction("toggle_3D_graph");
0309     action->setIcon(QIcon::fromTheme("star_profile", QIcon(":/icons/star_profile.svg")));
0310     action->setText(i18n("View 3D Graph"));
0311     action->setCheckable(true);
0312     connect(action, &QAction::triggered, this, &FITSViewer::toggle3DGraph);
0313 #endif
0314 
0315 
0316     int filterCounter = 1;
0317 
0318     for (auto &filter : FITSViewer::filterTypes)
0319     {
0320         action = actionCollection()->addAction(QString("filter%1").arg(filterCounter));
0321         action->setText(i18n(filter.toUtf8().constData()));
0322         connect(action, &QAction::triggered, this, [this, filterCounter] { applyFilter(filterCounter);});
0323         filterCounter++;
0324     }
0325 
0326     this->setAttribute(Qt::WA_AlwaysShowToolTips);
0327     /* Create GUI */
0328     createGUI("fitsviewerui.rc");
0329 
0330     setWindowTitle(i18nc("@title:window", "KStars FITS Viewer"));
0331 
0332     /* initially resize in accord with KDE rules */
0333     show();
0334     resize(INITIAL_W, INITIAL_H);
0335 }
0336 
0337 void FITSViewer::changeAlwaysOnTop(Qt::ApplicationState state)
0338 {
0339     if (isVisible())
0340     {
0341         if (state == Qt::ApplicationActive)
0342             setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint);
0343         else
0344             setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint);
0345         show();
0346     }
0347 }
0348 
0349 FITSViewer::~FITSViewer()
0350 {
0351 }
0352 
0353 void FITSViewer::closeEvent(QCloseEvent * /*event*/)
0354 {
0355     KStars *ks = KStars::Instance();
0356 
0357     if (ks)
0358     {
0359         QAction *a                  = KStars::Instance()->actionCollection()->action("show_fits_viewer");
0360         QList<FITSViewer *> viewers = KStars::Instance()->findChildren<FITSViewer *>();
0361 
0362         if (a && viewers.count() == 1)
0363         {
0364             a->setEnabled(false);
0365             a->setChecked(false);
0366         }
0367     }
0368 
0369     emit terminated();
0370 }
0371 
0372 void FITSViewer::hideEvent(QHideEvent * /*event*/)
0373 {
0374     KStars *ks = KStars::Instance();
0375 
0376     if (ks)
0377     {
0378         QAction *a = KStars::Instance()->actionCollection()->action("show_fits_viewer");
0379         if (a)
0380         {
0381             QList<FITSViewer *> viewers = KStars::Instance()->findChildren<FITSViewer *>();
0382 
0383             if (viewers.count() <= 1)
0384                 a->setChecked(false);
0385         }
0386     }
0387 }
0388 
0389 void FITSViewer::showEvent(QShowEvent * /*event*/)
0390 {
0391     QAction *a = KStars::Instance()->actionCollection()->action("show_fits_viewer");
0392     if (a)
0393     {
0394         a->setEnabled(true);
0395         a->setChecked(true);
0396     }
0397 }
0398 
0399 
0400 namespace
0401 {
0402 QString HFRStatusString(const QSharedPointer<FITSData> &data)
0403 {
0404     const double hfrValue = data->getHFR();
0405     if (hfrValue <= 0.0) return QString("");
0406     if (data->getSkyBackground().starsDetected > 0)
0407         return
0408             i18np("HFR:%2 Ecc:%3 %1 star.", "HFR:%2 Ecc:%3 %1 stars.",
0409                   data->getSkyBackground().starsDetected,
0410                   QString::number(hfrValue, 'f', 2),
0411                   QString::number(data->getEccentricity(), 'f', 2));
0412     else
0413         return
0414             i18np("HFR:%2, %1 star.", "HFR:%2, %1 stars.",
0415                   data->getDetectedStars(),
0416                   QString::number(hfrValue, 'f', 2));
0417 }
0418 
0419 QString HFRClipString(FITSView* view)
0420 {
0421     if (view->isClippingShown())
0422     {
0423         const int numClipped = view->getNumClipped();
0424         if (numClipped < 0)
0425             return QString("Clip:failed");
0426         else
0427             return QString("Clip:%1").arg(view->getNumClipped());
0428     }
0429     return "";
0430 }
0431 }  // namespace
0432 
0433 bool FITSViewer::addFITSCommon(const QSharedPointer<FITSTab> &tab, const QUrl &imageName,
0434                                FITSMode mode, const QString &previewText)
0435 {
0436     int tabIndex = fitsTabWidget->indexOf(tab.get());
0437     if (tabIndex != -1)
0438         return false;
0439 
0440     if (!imageName.isValid())
0441         lastURL = QUrl(imageName.url(QUrl::RemoveFilename));
0442 
0443     QApplication::restoreOverrideCursor();
0444     tab->setPreviewText(previewText);
0445 
0446     // Connect tab signals
0447     tab->disconnect(this);
0448     connect(tab.get(), &FITSTab::newStatus, this, &FITSViewer::updateStatusBar);
0449     connect(tab.get(), &FITSTab::changeStatus, this, &FITSViewer::updateTabStatus);
0450     connect(tab.get(), &FITSTab::debayerToggled, this, &FITSViewer::setDebayerAction);
0451     // Connect tab view signals
0452     connect(tab->getView().get(), &FITSView::actionUpdated, this, &FITSViewer::updateAction);
0453     connect(tab->getView().get(), &FITSView::wcsToggled, this, &FITSViewer::updateWCSFunctions);
0454     connect(tab->getView().get(), &FITSView::starProfileWindowClosed, this, &FITSViewer::starProfileButtonOff);
0455 
0456     switch (mode)
0457     {
0458         case FITS_NORMAL:
0459             fitsTabWidget->addTab(tab.get(), previewText.isEmpty() ? imageName.fileName() : previewText);
0460             break;
0461 
0462         case FITS_CALIBRATE:
0463             fitsTabWidget->addTab(tab.get(), i18n("Calibrate"));
0464             break;
0465 
0466         case FITS_FOCUS:
0467             fitsTabWidget->addTab(tab.get(), i18n("Focus"));
0468             break;
0469 
0470         case FITS_GUIDE:
0471             fitsTabWidget->addTab(tab.get(), i18n("Guide"));
0472             break;
0473 
0474         case FITS_ALIGN:
0475             fitsTabWidget->addTab(tab.get(), i18n("Align"));
0476             break;
0477 
0478         case FITS_UNKNOWN:
0479             break;
0480     }
0481 
0482     saveFileAction->setEnabled(true);
0483     saveFileAsAction->setEnabled(true);
0484 
0485     undoGroup->addStack(tab->getUndoStack());
0486 
0487     fitsMap[fitsID] = tab;
0488 
0489     fitsTabWidget->setCurrentWidget(tab.get());
0490 
0491     actionCollection()->action("fits_debayer")->setEnabled(tab->getView()->imageData()->hasDebayer());
0492 
0493     tab->tabPositionUpdated();
0494 
0495     tab->setUID(fitsID);
0496 
0497     led.setColor(Qt::green);
0498 
0499     if (tab->shouldComputeHFR())
0500         updateStatusBar(HFRStatusString(tab->getView()->imageData()), FITS_HFR);
0501     else
0502         updateStatusBar("", FITS_HFR);
0503     updateStatusBar(i18n("Ready."), FITS_MESSAGE);
0504 
0505     updateStatusBar(HFRClipString(tab->getView().get()), FITS_CLIP);
0506 
0507     tab->getView()->setCursorMode(FITSView::dragCursor);
0508 
0509     actionCollection()->action("next_blink")->setEnabled(tab->blinkFilenames().size() > 1);
0510     actionCollection()->action("previous_blink")->setEnabled(tab->blinkFilenames().size() > 1);
0511 
0512     updateWCSFunctions();
0513 
0514     return true;
0515 }
0516 
0517 void FITSViewer::loadFiles()
0518 {
0519     if (m_urls.size() == 0)
0520         return;
0521 
0522     const QUrl imageName = m_urls[0];
0523     m_urls.pop_front();
0524 
0525     // Make sure we don't have it open already, if yes, switch to it
0526     QString fpath = imageName.toLocalFile();
0527     for (auto tab : m_Tabs)
0528     {
0529         const QString cpath = tab->getCurrentURL()->path();
0530         if (fpath == cpath)
0531         {
0532             fitsTabWidget->setCurrentWidget(tab.get());
0533             if (m_urls.size() > 0)
0534                 loadFiles();
0535             return;
0536         }
0537     }
0538 
0539     led.setColor(Qt::yellow);
0540     QApplication::setOverrideCursor(Qt::WaitCursor);
0541 
0542     QSharedPointer<FITSTab> tab(new FITSTab(this));
0543 
0544     m_Tabs.push_back(tab);
0545 
0546     connect(tab.get(), &FITSTab::failed, this, [ this ](const QString & errorMessage)
0547     {
0548         QApplication::restoreOverrideCursor();
0549         led.setColor(Qt::red);
0550         m_Tabs.removeLast();
0551         emit failed(errorMessage);
0552         if (m_Tabs.size() == 0)
0553         {
0554             // Close FITS Viewer and let KStars know it is no longer needed in memory.
0555             close();
0556         }
0557 
0558         if (m_urls.size() > 0)
0559             loadFiles();
0560     });
0561 
0562     connect(tab.get(), &FITSTab::loaded, this, [ = ]()
0563     {
0564         if (addFITSCommon(m_Tabs.last(), imageName, FITS_NORMAL, ""))
0565             emit loaded(fitsID++);
0566         else
0567             m_Tabs.removeLast();
0568 
0569         if (m_urls.size() > 0)
0570             loadFiles();
0571     });
0572 
0573     tab->loadFile(imageName, FITS_NORMAL, FITS_NONE);
0574 }
0575 
0576 void FITSViewer::loadFile(const QUrl &imageName, FITSMode mode, FITSScale filter, const QString &previewText)
0577 {
0578     led.setColor(Qt::yellow);
0579     QApplication::setOverrideCursor(Qt::WaitCursor);
0580 
0581     QSharedPointer<FITSTab> tab(new FITSTab(this));
0582 
0583     m_Tabs.push_back(tab);
0584 
0585     connect(tab.get(), &FITSTab::failed, this, [ this ](const QString & errorMessage)
0586     {
0587         QApplication::restoreOverrideCursor();
0588         led.setColor(Qt::red);
0589         m_Tabs.removeLast();
0590         emit failed(errorMessage);
0591         if (m_Tabs.size() == 0)
0592         {
0593             // Close FITS Viewer and let KStars know it is no longer needed in memory.
0594             close();
0595         }
0596     });
0597 
0598     connect(tab.get(), &FITSTab::loaded, this, [ = ]()
0599     {
0600         if (addFITSCommon(m_Tabs.last(), imageName, mode, previewText))
0601             emit loaded(fitsID++);
0602         else
0603             m_Tabs.removeLast();
0604     });
0605 
0606     tab->loadFile(imageName, mode, filter);
0607 }
0608 
0609 bool FITSViewer::loadData(const QSharedPointer<FITSData> &data, const QUrl &imageName, int *tab_uid, FITSMode mode,
0610                           FITSScale filter, const QString &previewText)
0611 {
0612     led.setColor(Qt::yellow);
0613     QApplication::setOverrideCursor(Qt::WaitCursor);
0614 
0615     QSharedPointer<FITSTab> tab(new FITSTab(this));
0616 
0617     m_Tabs.push_back(tab);
0618 
0619     if (!tab->loadData(data, mode, filter))
0620     {
0621         auto errorMessage = tab->getView()->imageData()->getLastError();
0622         QApplication::restoreOverrideCursor();
0623         led.setColor(Qt::red);
0624         m_Tabs.removeLast();
0625         emit failed(errorMessage);
0626         if (m_Tabs.size() == 0)
0627         {
0628             // Close FITS Viewer and let KStars know it is no longer needed in memory.
0629             close();
0630         }
0631         return false;
0632     }
0633 
0634     if (!addFITSCommon(tab, imageName, mode, previewText))
0635     {
0636         m_Tabs.removeLast();
0637         return false;
0638     }
0639 
0640     *tab_uid = fitsID++;
0641     return true;
0642 }
0643 
0644 bool FITSViewer::removeFITS(int fitsUID)
0645 {
0646     auto tab = fitsMap.value(fitsUID);
0647 
0648     if (tab.isNull())
0649     {
0650         qCWarning(KSTARS_FITS) << "Cannot find tab with UID " << fitsUID << " in the FITS Viewer";
0651         return false;
0652     }
0653 
0654     int index = m_Tabs.indexOf(tab);
0655 
0656     if (index >= 0)
0657     {
0658         closeTab(index);
0659         return true;
0660     }
0661 
0662     return false;
0663 }
0664 
0665 void FITSViewer::updateFile(const QUrl &imageName, int fitsUID, FITSScale filter)
0666 {
0667     static bool updateBusy = false;
0668     if (updateBusy)
0669         return;
0670     updateBusy = true;
0671 
0672     auto tab = fitsMap.value(fitsUID);
0673 
0674     if (tab.isNull())
0675     {
0676         QString message = i18n("Cannot find tab with UID %1 in the FITS Viewer", fitsUID);
0677         emit failed(message);
0678         updateBusy = false;
0679         return;
0680     }
0681 
0682     if (tab->isVisible())
0683         led.setColor(Qt::yellow);
0684 
0685     // On tab load success
0686     auto conn = std::make_shared<QMetaObject::Connection>();
0687     *conn = connect(tab.get(), &FITSTab::loaded, this, [ = ]()
0688     {
0689         if (updateFITSCommon(tab, imageName))
0690         {
0691             QObject::disconnect(*conn);
0692             emit loaded(tab->getUID());
0693             updateBusy = false;
0694         }
0695     });
0696 
0697     auto conn2 = std::make_shared<QMetaObject::Connection>();
0698     *conn2 = connect(tab.get(), &FITSTab::failed, this, [ = ](const QString & errorMessage)
0699     {
0700         Q_UNUSED(errorMessage);
0701         QObject::disconnect(*conn2);
0702         updateBusy = false;
0703     });
0704 
0705     tab->loadFile(imageName, tab->getView()->getMode(), filter);
0706 }
0707 
0708 bool FITSViewer::updateFITSCommon(const QSharedPointer<FITSTab> &tab, const QUrl &imageName)
0709 {
0710     // On tab load success
0711     int tabIndex = fitsTabWidget->indexOf(tab.get());
0712     if (tabIndex == -1)
0713         return false;
0714 
0715     if (tab->getView()->getMode() == FITS_NORMAL)
0716     {
0717         if ((imageName.path().startsWith(QLatin1String("/tmp")) ||
0718                 imageName.path().contains("/Temp")) &&
0719                 Options::singlePreviewFITS())
0720             fitsTabWidget->setTabText(tabIndex,
0721                                       tab->getPreviewText().isEmpty() ? i18n("Preview") : tab->getPreviewText());
0722         else
0723             fitsTabWidget->setTabText(tabIndex, imageName.fileName());
0724     }
0725 
0726     tab->getUndoStack()->clear();
0727 
0728     if (tab->isVisible())
0729         led.setColor(Qt::green);
0730 
0731     if (tab->shouldComputeHFR())
0732         updateStatusBar(HFRStatusString(tab->getView()->imageData()), FITS_HFR);
0733     else
0734         updateStatusBar("", FITS_HFR);
0735 
0736     updateStatusBar(HFRClipString(tab->getView().get()), FITS_CLIP);
0737 
0738     actionCollection()->action("next_blink")->setEnabled(tab->blinkFilenames().size() > 1);
0739     actionCollection()->action("previous_blink")->setEnabled(tab->blinkFilenames().size() > 1);
0740 
0741     return true;
0742 }
0743 
0744 bool FITSViewer::updateData(const QSharedPointer<FITSData> &data, const QUrl &imageName, int fitsUID, int *tab_uid,
0745                             FITSScale filter, FITSMode mode)
0746 {
0747     auto tab = fitsMap.value(fitsUID);
0748 
0749     if (tab.isNull())
0750         return false;
0751 
0752     if (mode != FITS_UNKNOWN)
0753         tab->getView()->updateMode(mode);
0754 
0755     if (tab->isVisible())
0756         led.setColor(Qt::yellow);
0757 
0758     if (!tab->loadData(data, tab->getView()->getMode(), filter))
0759         return false;
0760 
0761     if (!updateFITSCommon(tab, imageName))
0762         return false;
0763 
0764     *tab_uid = tab->getUID();
0765     return true;
0766 }
0767 
0768 void FITSViewer::tabFocusUpdated(int currentIndex)
0769 {
0770     if (currentIndex < 0 || m_Tabs.empty())
0771         return;
0772 
0773     m_Tabs[currentIndex]->tabPositionUpdated();
0774 
0775     auto view = m_Tabs[currentIndex]->getView();
0776 
0777     view->toggleStars(markStars);
0778 
0779     if (isVisible())
0780         view->updateFrame();
0781 
0782     if (m_Tabs[currentIndex]->shouldComputeHFR())
0783         updateStatusBar(HFRStatusString(view->imageData()), FITS_HFR);
0784     else
0785         updateStatusBar("", FITS_HFR);
0786 
0787     updateStatusBar(HFRClipString(m_Tabs[currentIndex]->getView().get()), FITS_CLIP);
0788 
0789     if (view->imageData()->hasDebayer())
0790     {
0791         actionCollection()->action("fits_debayer")->setEnabled(true);
0792 
0793         if (debayerDialog)
0794         {
0795             BayerParams param;
0796             view->imageData()->getBayerParams(&param);
0797             debayerDialog->setBayerParams(&param);
0798         }
0799     }
0800     else
0801         actionCollection()->action("fits_debayer")->setEnabled(false);
0802 
0803     updateStatusBar("", FITS_WCS);
0804     connect(view.get(), &FITSView::starProfileWindowClosed, this, &FITSViewer::starProfileButtonOff);
0805     QSharedPointer<FITSView> currentView;
0806     if (getCurrentView(currentView))
0807     {
0808         updateButtonStatus("toggle_3D_graph", i18n("currentView 3D Graph"), currentView->isStarProfileShown());
0809         updateButtonStatus("view_crosshair", i18n("Cross Hairs"), currentView->isCrosshairShown());
0810         updateButtonStatus("view_clipping", i18n("Clipping"), currentView->isClippingShown());
0811         updateButtonStatus("view_eq_grid", i18n("Equatorial Gridlines"), currentView->isEQGridShown());
0812         updateButtonStatus("view_objects", i18n("Objects in Image"), currentView->areObjectsShown());
0813         updateButtonStatus("view_pixel_grid", i18n("Pixel Gridlines"), currentView->isPixelGridShown());
0814         updateButtonStatus("view_hips_overlay", i18n("HiPS Overlay"), currentView->isHiPSOverlayShown());
0815     }
0816 
0817     actionCollection()->action("next_blink")->setEnabled(m_Tabs[currentIndex]->blinkFilenames().size() > 1);
0818     actionCollection()->action("previous_blink")->setEnabled(m_Tabs[currentIndex]->blinkFilenames().size() > 1);
0819 
0820     updateScopeButton();
0821     updateWCSFunctions();
0822 }
0823 
0824 void FITSViewer::starProfileButtonOff()
0825 {
0826     updateButtonStatus("toggle_3D_graph", i18n("View 3D Graph"), false);
0827 }
0828 
0829 
0830 QList<QString> findAllImagesBelowDir(const QDir &topDir)
0831 {
0832     QList<QString> result;
0833     QList<QString> nameFilter = { "*" };
0834     QDir::Filters filter = QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Files | QDir::NoSymLinks;
0835 
0836     QList<QDir> dirs;
0837     dirs.push_back(topDir);
0838 
0839     QRegularExpression re(".*(fits|fits.fz|fit|fts|xisf|jpg|jpeg|png|gif|bmp|cr2|cr3|crw|nef|raf|dng|arw|orf)$");
0840     while (!dirs.empty())
0841     {
0842         auto dir = dirs.back();
0843         dirs.removeLast();
0844         auto list = dir.entryInfoList( nameFilter, filter );
0845         foreach( const QFileInfo &entry,  list)
0846         {
0847             if( entry.isDir() )
0848                 dirs.push_back(entry.filePath());
0849             else
0850             {
0851                 const QString suffix = entry.completeSuffix();
0852                 QRegularExpressionMatch match = re.match(suffix);
0853                 if (match.hasMatch())
0854                     result.append(entry.absoluteFilePath());
0855             }
0856         }
0857     }
0858     return result;
0859 }
0860 
0861 void FITSViewer::blink()
0862 {
0863     if (m_BlinkBusy)
0864         return;
0865     m_BlinkBusy = true;
0866     QFileDialog dialog(KStars::Instance(), i18nc("@title:window", "Blink Top Directory"));
0867     dialog.setFileMode(QFileDialog::Directory);
0868     dialog.setDirectoryUrl(lastURL);
0869 
0870     if (!dialog.exec())
0871     {
0872         m_BlinkBusy = false;
0873         return;
0874     }
0875     QStringList selected = dialog.selectedFiles();
0876     if (selected.size() < 1)
0877     {
0878         m_BlinkBusy = false;
0879         return;
0880     }
0881     QString topDir = selected[0];
0882 
0883     auto allImages = findAllImagesBelowDir(QDir(topDir));
0884     if (allImages.size() == 0)
0885     {
0886         m_BlinkBusy = false;
0887         return;
0888     }
0889 
0890     const QUrl imageName(QUrl::fromLocalFile(allImages[0]));
0891 
0892     led.setColor(Qt::yellow);
0893     QApplication::setOverrideCursor(Qt::WaitCursor);
0894 
0895     QSharedPointer<FITSTab> tab(new FITSTab(this));
0896 
0897     int tabIndex = m_Tabs.size();
0898     if (allImages.size() > 1)
0899     {
0900         m_Tabs.push_back(tab);
0901         tab->initBlink(allImages);
0902         tab->setBlinkUpto(1);
0903     }
0904     QString tabName = QString("%1/%2 %3")
0905                       .arg(1).arg(allImages.size()).arg(QFileInfo(allImages[0]).fileName());
0906     connect(tab.get(), &FITSTab::failed, this, [ this ](const QString & errorMessage)
0907     {
0908         Q_UNUSED(errorMessage);
0909         QObject::sender()->disconnect(this);
0910         QApplication::restoreOverrideCursor();
0911         led.setColor(Qt::red);
0912         m_BlinkBusy = false;
0913     }, Qt::UniqueConnection);
0914 
0915     connect(tab.get(), &FITSTab::loaded, this, [ = ]()
0916     {
0917         QObject::sender()->disconnect(this);
0918         addFITSCommon(m_Tabs.last(), imageName, FITS_NORMAL, "");
0919         //fitsTabWidget->tabBar()->setTabTextColor(tabIndex, Qt::red);
0920         fitsTabWidget->setTabText(tabIndex, tabName);
0921         m_BlinkBusy = false;
0922     }, Qt::UniqueConnection);
0923 
0924     actionCollection()->action("next_blink")->setEnabled(allImages.size() > 1);
0925     actionCollection()->action("previous_blink")->setEnabled(allImages.size() > 1);
0926 
0927     tab->loadFile(imageName, FITS_NORMAL, FITS_NONE);
0928 }
0929 
0930 
0931 void FITSViewer::changeBlink(bool increment)
0932 {
0933     if (m_Tabs.empty() || m_BlinkBusy)
0934         return;
0935 
0936     m_BlinkBusy = true;
0937     const int tabIndex = fitsTabWidget->currentIndex();
0938     if (tabIndex >= m_Tabs.count() || tabIndex < 0)
0939     {
0940         m_BlinkBusy = false;
0941         return;
0942     }
0943     auto tab = m_Tabs[tabIndex];
0944     const QList<QString> &filenames = tab->blinkFilenames();
0945     if (filenames.size() <= 1)
0946     {
0947         m_BlinkBusy = false;
0948         return;
0949     }
0950 
0951     int blinkIndex = tab->blinkUpto() + (increment ? 1 : -1);
0952     if (blinkIndex >= filenames.size())
0953         blinkIndex = 0;
0954     else if (blinkIndex < 0)
0955         blinkIndex = filenames.size() - 1;
0956 
0957     QString nextFilename = filenames[blinkIndex];
0958     QString tabName = QString("%1/%2 %3")
0959                       .arg(blinkIndex + 1).arg(filenames.size()).arg(QFileInfo(nextFilename).fileName());
0960     tab->disconnect(this);
0961     connect(tab.get(), &FITSTab::failed, this, [ this, nextFilename ](const QString & errorMessage)
0962     {
0963         Q_UNUSED(errorMessage);
0964         QObject::sender()->disconnect(this);
0965         QApplication::restoreOverrideCursor();
0966         led.setColor(Qt::red);
0967         m_BlinkBusy = false;
0968     }, Qt::UniqueConnection);
0969 
0970     connect(tab.get(), &FITSTab::loaded, this, [ = ]()
0971     {
0972         QObject::sender()->disconnect(this);
0973         updateFITSCommon(tab, QUrl::fromLocalFile(nextFilename));
0974         fitsTabWidget->setTabText(tabIndex, tabName);
0975         m_BlinkBusy = false;
0976     }, Qt::UniqueConnection);
0977 
0978     tab->setBlinkUpto(blinkIndex);
0979     tab->loadFile(QUrl::fromLocalFile(nextFilename), FITS_NORMAL, FITS_NONE);
0980 }
0981 
0982 void FITSViewer::nextBlink()
0983 {
0984     changeBlink(true);
0985 }
0986 
0987 void FITSViewer::previousBlink()
0988 {
0989     changeBlink(false);
0990 }
0991 
0992 void FITSViewer::openFile()
0993 {
0994     QFileDialog dialog(KStars::Instance(), i18nc("@title:window", "Open Image"));
0995     dialog.setFileMode(QFileDialog::ExistingFiles);
0996     dialog.setDirectoryUrl(lastURL);
0997     dialog.setNameFilter("Images (*.fits *.fits.fz *.fit *.fts *.xisf "
0998                          "*.jpg *.jpeg *.png *.gif *.bmp "
0999                          "*.cr2 *.cr3 *.crw *.nef *.raf *.dng *.arw *.orf)");
1000     if (!dialog.exec())
1001         return;
1002     m_urls = dialog.selectedUrls();
1003     if (m_urls.size() < 1)
1004         return;
1005     // Protect against, e.g. opening 1000 tabs. Not sure what the right number is.
1006     constexpr int MAX_NUM_OPENS = 40;
1007     if (m_urls.size() > MAX_NUM_OPENS)
1008         return;
1009 
1010     lastURL = QUrl(m_urls[0].url(QUrl::RemoveFilename));
1011     loadFiles();
1012 }
1013 
1014 void FITSViewer::saveFile()
1015 {
1016     m_Tabs[fitsTabWidget->currentIndex()]->saveFile();
1017 }
1018 
1019 void FITSViewer::saveFileAs()
1020 {
1021     if (m_Tabs.empty())
1022         return;
1023 
1024     if (m_Tabs[fitsTabWidget->currentIndex()]->saveFileAs() &&
1025             m_Tabs[fitsTabWidget->currentIndex()]->getView()->getMode() == FITS_NORMAL)
1026         fitsTabWidget->setTabText(fitsTabWidget->currentIndex(),
1027                                   m_Tabs[fitsTabWidget->currentIndex()]->getCurrentURL()->fileName());
1028 }
1029 
1030 void FITSViewer::copyFITS()
1031 {
1032     if (m_Tabs.empty())
1033         return;
1034 
1035     m_Tabs[fitsTabWidget->currentIndex()]->copyFITS();
1036 }
1037 
1038 void FITSViewer::histoFITS()
1039 {
1040     if (m_Tabs.empty())
1041         return;
1042 
1043     m_Tabs[fitsTabWidget->currentIndex()]->histoFITS();
1044 }
1045 
1046 void FITSViewer::statFITS()
1047 {
1048     if (m_Tabs.empty())
1049         return;
1050 
1051     m_Tabs[fitsTabWidget->currentIndex()]->statFITS();
1052 }
1053 
1054 void FITSViewer::rotateCW()
1055 {
1056     applyFilter(FITS_ROTATE_CW);
1057 }
1058 
1059 void FITSViewer::rotateCCW()
1060 {
1061     applyFilter(FITS_ROTATE_CCW);
1062 }
1063 
1064 void FITSViewer::flipHorizontal()
1065 {
1066     applyFilter(FITS_MOUNT_FLIP_H);
1067 }
1068 
1069 void FITSViewer::flipVertical()
1070 {
1071     applyFilter(FITS_MOUNT_FLIP_V);
1072 }
1073 
1074 void FITSViewer::headerFITS()
1075 {
1076     if (m_Tabs.empty())
1077         return;
1078 
1079     m_Tabs[fitsTabWidget->currentIndex()]->headerFITS();
1080 }
1081 
1082 void FITSViewer::debayerFITS()
1083 {
1084     if (debayerDialog == nullptr)
1085     {
1086         debayerDialog = new FITSDebayer(this);
1087     }
1088 
1089     QSharedPointer<FITSView> view;
1090     if (getCurrentView(view))
1091     {
1092         BayerParams param;
1093         view->imageData()->getBayerParams(&param);
1094         debayerDialog->setBayerParams(&param);
1095         debayerDialog->show();
1096     }
1097 }
1098 
1099 void FITSViewer::updateStatusBar(const QString &msg, FITSBar id)
1100 {
1101     switch (id)
1102     {
1103         case FITS_POSITION:
1104             fitsPosition.setText(msg);
1105             break;
1106         case FITS_RESOLUTION:
1107             fitsResolution.setText(msg);
1108             break;
1109         case FITS_ZOOM:
1110             fitsZoom.setText(msg);
1111             break;
1112         case FITS_WCS:
1113             fitsWCS.setVisible(true);
1114             fitsWCS.setText(msg);
1115             break;
1116         case FITS_VALUE:
1117             fitsValue.setText(msg);
1118             break;
1119         case FITS_HFR:
1120             fitsHFR.setText(msg);
1121             break;
1122         case FITS_CLIP:
1123             fitsClip.setText(msg);
1124             break;
1125         case FITS_MESSAGE:
1126             statusBar()->showMessage(msg);
1127             break;
1128 
1129         default:
1130             break;
1131     }
1132 }
1133 
1134 void FITSViewer::ZoomAllIn()
1135 {
1136     if (m_Tabs.empty())
1137         return;
1138 
1139     // Could add code to not call View::updateFrame for these
1140     for (int i = 0; i < fitsTabWidget->count(); ++i)
1141         if (i != fitsTabWidget->currentIndex())
1142             m_Tabs[i]->ZoomIn();
1143 
1144     m_Tabs[fitsTabWidget->currentIndex()]->ZoomIn();
1145 }
1146 
1147 void FITSViewer::ZoomAllOut()
1148 {
1149     if (m_Tabs.empty())
1150         return;
1151 
1152     // Could add code to not call View::updateFrame for these
1153     for (int i = 0; i < fitsTabWidget->count(); ++i)
1154         if (i != fitsTabWidget->currentIndex())
1155             m_Tabs[i]->ZoomOut();
1156 
1157     m_Tabs[fitsTabWidget->currentIndex()]->ZoomOut();
1158 }
1159 
1160 void FITSViewer::ZoomIn()
1161 {
1162     if (m_Tabs.empty())
1163         return;
1164 
1165     m_Tabs[fitsTabWidget->currentIndex()]->ZoomIn();
1166 }
1167 
1168 void FITSViewer::ZoomOut()
1169 {
1170     if (m_Tabs.empty())
1171         return;
1172 
1173     m_Tabs[fitsTabWidget->currentIndex()]->ZoomOut();
1174 }
1175 
1176 void FITSViewer::ZoomDefault()
1177 {
1178     if (m_Tabs.empty())
1179         return;
1180 
1181     m_Tabs[fitsTabWidget->currentIndex()]->ZoomDefault();
1182 }
1183 
1184 void FITSViewer::ZoomToFit()
1185 {
1186     if (m_Tabs.empty())
1187         return;
1188 
1189     QSharedPointer<FITSView> currentView;
1190     if (getCurrentView(currentView))
1191         currentView->ZoomToFit();
1192 }
1193 
1194 void FITSViewer::updateAction(const QString &name, bool enable)
1195 {
1196     QAction *toolAction = actionCollection()->action(name);
1197 
1198     if (toolAction != nullptr)
1199         toolAction->setEnabled(enable);
1200 }
1201 
1202 void FITSViewer::updateTabStatus(bool clean, const QUrl &imageURL)
1203 {
1204     if (m_Tabs.empty() || (fitsTabWidget->currentIndex() >= m_Tabs.size()))
1205         return;
1206 
1207     if (m_Tabs[fitsTabWidget->currentIndex()]->getView()->getMode() != FITS_NORMAL)
1208         return;
1209 
1210     //QString tabText = fitsImages[fitsTab->currentIndex()]->getCurrentURL()->fileName();
1211 
1212     QString tabText = imageURL.isEmpty() ? fitsTabWidget->tabText(fitsTabWidget->currentIndex()) : imageURL.fileName();
1213 
1214     fitsTabWidget->setTabText(fitsTabWidget->currentIndex(), clean ? tabText.remove('*') : tabText + '*');
1215 }
1216 
1217 void FITSViewer::closeTab(int index)
1218 {
1219     if (m_Tabs.empty())
1220         return;
1221 
1222     auto tab = m_Tabs[index];
1223 
1224     int UID = tab->getUID();
1225 
1226     fitsMap.remove(UID);
1227     m_Tabs.removeOne(tab);
1228 
1229     if (m_Tabs.empty())
1230     {
1231         saveFileAction->setEnabled(false);
1232         saveFileAsAction->setEnabled(false);
1233     }
1234 
1235     emit closed(UID);
1236 }
1237 
1238 /**
1239  This is helper function to make it really easy to make the update the state of toggle buttons
1240  that either show or hide information in the Current view.  This method would get called both
1241  when one of them gets pushed and also when tabs are switched.
1242  */
1243 
1244 void FITSViewer::updateButtonStatus(const QString &action, const QString &item, bool showing)
1245 {
1246     QAction *a = actionCollection()->action(action);
1247     if (a == nullptr)
1248         return;
1249 
1250     if (showing)
1251     {
1252         a->setText(i18n("Hide %1", item));
1253         a->setChecked(true);
1254     }
1255     else
1256     {
1257         a->setText(i18n("Show %1", item));
1258         a->setChecked(false);
1259     }
1260 }
1261 
1262 /**
1263 This is a method that either enables or disables the WCS based features in the Current View.
1264  */
1265 
1266 void FITSViewer::updateWCSFunctions()
1267 {
1268     QSharedPointer<FITSView> currentView;
1269     if (!getCurrentView(currentView))
1270         return;
1271 
1272     if (currentView->imageHasWCS())
1273     {
1274         actionCollection()->action("view_eq_grid")->setDisabled(false);
1275         actionCollection()->action("view_eq_grid")->setText(i18n("Show Equatorial Gridlines"));
1276         actionCollection()->action("view_objects")->setDisabled(false);
1277         actionCollection()->action("view_objects")->setText(i18n("Show Objects in Image"));
1278         if (currentView->isTelescopeActive())
1279         {
1280             actionCollection()->action("center_telescope")->setDisabled(false);
1281             actionCollection()->action("center_telescope")->setText(i18n("Center Telescope\n*Ready*"));
1282         }
1283         else
1284         {
1285             actionCollection()->action("center_telescope")->setDisabled(true);
1286             actionCollection()->action("center_telescope")->setText(i18n("Center Telescope\n*No Telescopes Detected*"));
1287         }
1288         actionCollection()->action("view_hips_overlay")->setDisabled(false);
1289     }
1290     else
1291     {
1292         actionCollection()->action("view_eq_grid")->setDisabled(true);
1293         actionCollection()->action("view_eq_grid")->setText(i18n("Show Equatorial Gridlines\n*No WCS Info*"));
1294         actionCollection()->action("center_telescope")->setDisabled(true);
1295         actionCollection()->action("center_telescope")->setText(i18n("Center Telescope\n*No WCS Info*"));
1296         actionCollection()->action("view_objects")->setDisabled(true);
1297         actionCollection()->action("view_objects")->setText(i18n("Show Objects in Image\n*No WCS Info*"));
1298         actionCollection()->action("view_hips_overlay")->setDisabled(true);
1299     }
1300 }
1301 
1302 void FITSViewer::updateScopeButton()
1303 {
1304     QSharedPointer<FITSView> currentView;
1305     if (!getCurrentView(currentView))
1306         return;
1307 
1308     if (currentView->getCursorMode() == FITSView::scopeCursor)
1309     {
1310         actionCollection()->action("center_telescope")->setChecked(true);
1311     }
1312     else
1313     {
1314         actionCollection()->action("center_telescope")->setChecked(false);
1315     }
1316 }
1317 
1318 void FITSViewer::ROIFixedSize(int s)
1319 {
1320     if (m_Tabs.empty())
1321         return;
1322 
1323     QSharedPointer<FITSView> currentView;
1324     if (getCurrentView(currentView))
1325     {
1326         if(!currentView->isSelectionRectShown())
1327         {
1328             toggleSelectionMode();
1329             updateButtonStatus("image_roi_stats", i18n("Selection Rectangle"), currentView->isSelectionRectShown());
1330         }
1331         currentView->processRectangleFixed(s);
1332     }
1333 }
1334 
1335 void FITSViewer::customROIInputWindow()
1336 {
1337     if(m_Tabs.empty())
1338         return;
1339 
1340     QSharedPointer<FITSView> currentView;
1341     if (getCurrentView(currentView))
1342     {
1343         if(!currentView->isSelectionRectShown())
1344             return;
1345 
1346         int mh = currentView->imageData()->height();
1347         int mw = currentView->imageData()->width();
1348 
1349         if(mh % 2)
1350             mh++;
1351         if(mw % 2)
1352             mw++;
1353 
1354         QDialog customRoiDialog;
1355         QFormLayout form(&customRoiDialog);
1356         QDialogButtonBox buttonBox(QDialogButtonBox:: Ok | QDialogButtonBox:: Cancel, Qt::Horizontal, &customRoiDialog);
1357 
1358         form.addRow(new QLabel(i18n("Size")));
1359 
1360         QLineEdit wle(&customRoiDialog);
1361         QLineEdit hle(&customRoiDialog);
1362 
1363         wle.setValidator(new QIntValidator(1, mw, &wle));
1364         hle.setValidator(new QIntValidator(1, mh, &hle));
1365 
1366         form.addRow(i18n("Width"), &wle);
1367         form.addRow(i18n("Height"), &hle);
1368         form.addRow(&buttonBox);
1369 
1370         connect(&buttonBox, &QDialogButtonBox::accepted, &customRoiDialog, &QDialog::accept);
1371         connect(&buttonBox, &QDialogButtonBox::rejected, &customRoiDialog, &QDialog::reject);
1372 
1373         if(customRoiDialog.exec() == QDialog::Accepted)
1374         {
1375             QPoint resetCenter = currentView->getSelectionRegion().center();
1376             int newheight = hle.text().toInt();
1377             int newwidth = wle.text().toInt();
1378 
1379             newheight = qMin(newheight, mh) ;
1380             newheight = qMax(newheight, 1) ;
1381             newwidth = qMin(newwidth, mw);
1382             newwidth = qMax(newwidth, 1);
1383 
1384             QPoint topLeft = resetCenter;
1385             QPoint botRight = resetCenter;
1386 
1387             topLeft.setX((topLeft.x() - newwidth / 2));
1388             topLeft.setY((topLeft.y() - newheight / 2));
1389             botRight.setX((botRight.x() + newwidth / 2));
1390             botRight.setY((botRight.y() + newheight / 2));
1391 
1392             emit currentView->setRubberBand(QRect(topLeft, botRight));
1393             currentView->processRectangle(topLeft, botRight, true);
1394         }
1395     }
1396 }
1397 /**
1398  This method either enables or disables the scope mouse mode so you can slew your scope to coordinates
1399  just by clicking the mouse on a spot in the image.
1400  */
1401 
1402 void FITSViewer::centerTelescope()
1403 {
1404     QSharedPointer<FITSView> currentView;
1405     if (!getCurrentView(currentView))
1406         return;
1407 
1408     currentView->setScopeButton(actionCollection()->action("center_telescope"));
1409     if (currentView->getCursorMode() == FITSView::scopeCursor)
1410     {
1411         currentView->setCursorMode(currentView->lastMouseMode);
1412     }
1413     else
1414     {
1415         currentView->lastMouseMode = currentView->getCursorMode();
1416         currentView->setCursorMode(FITSView::scopeCursor);
1417     }
1418     updateScopeButton();
1419 }
1420 
1421 void FITSViewer::toggleCrossHair()
1422 {
1423     if (m_Tabs.empty())
1424         return;
1425 
1426     QSharedPointer<FITSView> currentView;
1427     if (!getCurrentView(currentView))
1428         return;
1429 
1430     currentView->toggleCrosshair();
1431     updateButtonStatus("view_crosshair", i18n("Cross Hairs"), currentView->isCrosshairShown());
1432 }
1433 
1434 void FITSViewer::toggleClipping()
1435 {
1436     if (m_Tabs.empty())
1437         return;
1438 
1439     QSharedPointer<FITSView> currentView;
1440     if (!getCurrentView(currentView))
1441         return;
1442     currentView->toggleClipping();
1443     if (!currentView->isClippingShown())
1444         fitsClip.clear();
1445     updateButtonStatus("view_clipping", i18n("Clipping"), currentView->isClippingShown());
1446 }
1447 
1448 void FITSViewer::toggleEQGrid()
1449 {
1450     if (m_Tabs.empty())
1451         return;
1452 
1453     QSharedPointer<FITSView> currentView;
1454     if (!getCurrentView(currentView))
1455         return;
1456 
1457     currentView->toggleEQGrid();
1458     updateButtonStatus("view_eq_grid", i18n("Equatorial Gridlines"), currentView->isEQGridShown());
1459 }
1460 
1461 void FITSViewer::toggleHiPSOverlay()
1462 {
1463     if (m_Tabs.empty())
1464         return;
1465 
1466     QSharedPointer<FITSView> currentView;
1467     if (!getCurrentView(currentView))
1468         return;
1469 
1470     currentView->toggleHiPSOverlay();
1471     updateButtonStatus("view_hips_overlay", i18n("HiPS Overlay"), currentView->isHiPSOverlayShown());
1472 }
1473 
1474 void FITSViewer::toggleSelectionMode()
1475 {
1476     if (m_Tabs.empty())
1477         return;
1478 
1479     QSharedPointer<FITSView> currentView;
1480     if (!getCurrentView(currentView))
1481         return;
1482 
1483     currentView->toggleSelectionMode();
1484     updateButtonStatus("image_roi_stats", i18n("Selection Rectangle"), currentView->isSelectionRectShown());
1485 }
1486 
1487 void FITSViewer::toggleObjects()
1488 {
1489     if (m_Tabs.empty())
1490         return;
1491 
1492     QSharedPointer<FITSView> currentView;
1493     if (!getCurrentView(currentView))
1494         return;
1495 
1496     currentView->toggleObjects();
1497     updateButtonStatus("view_objects", i18n("Objects in Image"), currentView->areObjectsShown());
1498 }
1499 
1500 void FITSViewer::togglePixelGrid()
1501 {
1502     if (m_Tabs.empty())
1503         return;
1504 
1505     QSharedPointer<FITSView> currentView;
1506     if (!getCurrentView(currentView))
1507         return;
1508 
1509     currentView->togglePixelGrid();
1510     updateButtonStatus("view_pixel_grid", i18n("Pixel Gridlines"), currentView->isPixelGridShown());
1511 }
1512 
1513 void FITSViewer::toggle3DGraph()
1514 {
1515     if (m_Tabs.empty())
1516         return;
1517 
1518     QSharedPointer<FITSView> currentView;
1519     if (!getCurrentView(currentView))
1520         return;
1521 
1522     currentView->toggleStarProfile();
1523     updateButtonStatus("toggle_3D_graph", i18n("View 3D Graph"), currentView->isStarProfileShown());
1524 }
1525 
1526 void FITSViewer::nextTab()
1527 {
1528     if (m_Tabs.empty())
1529         return;
1530 
1531     int index = fitsTabWidget->currentIndex() + 1;
1532     if (index >= m_Tabs.count() || index < 0)
1533         index = 0;
1534     fitsTabWidget->setCurrentIndex(index);
1535 
1536     actionCollection()->action("next_blink")->setEnabled(m_Tabs[index]->blinkFilenames().size() > 1);
1537     actionCollection()->action("previous_blink")->setEnabled(m_Tabs[index]->blinkFilenames().size() > 1);
1538 }
1539 
1540 void FITSViewer::previousTab()
1541 {
1542     if (m_Tabs.empty())
1543         return;
1544 
1545     int index = fitsTabWidget->currentIndex() - 1;
1546     if (index >= m_Tabs.count() || index < 0)
1547         index = m_Tabs.count() - 1;
1548     fitsTabWidget->setCurrentIndex(index);
1549 
1550     actionCollection()->action("next_blink")->setEnabled(m_Tabs[index]->blinkFilenames().size() > 1);
1551     actionCollection()->action("previous_blink")->setEnabled(m_Tabs[index]->blinkFilenames().size() > 1);
1552 
1553 }
1554 
1555 void FITSViewer::toggleStars()
1556 {
1557     if (markStars)
1558     {
1559         markStars = false;
1560         actionCollection()->action("mark_stars")->setText(i18n("Mark Stars"));
1561     }
1562     else
1563     {
1564         markStars = true;
1565         actionCollection()->action("mark_stars")->setText(i18n("Unmark Stars"));
1566     }
1567 
1568     for (auto tab : m_Tabs)
1569     {
1570         tab->getView()->toggleStars(markStars);
1571         tab->getView()->updateFrame();
1572     }
1573 }
1574 
1575 void FITSViewer::applyFilter(int ftype)
1576 {
1577     if (m_Tabs.empty())
1578         return;
1579 
1580     QApplication::setOverrideCursor(Qt::WaitCursor);
1581     updateStatusBar(i18n("Processing %1...", filterTypes[ftype - 1]), FITS_MESSAGE);
1582     qApp->processEvents();
1583     m_Tabs[fitsTabWidget->currentIndex()]->getHistogram()->applyFilter(static_cast<FITSScale>(ftype));
1584     qApp->processEvents();
1585     m_Tabs[fitsTabWidget->currentIndex()]->getView()->updateFrame();
1586     QApplication::restoreOverrideCursor();
1587     updateStatusBar(i18n("Ready."), FITS_MESSAGE);
1588 }
1589 
1590 bool FITSViewer::getView(int fitsUID, QSharedPointer<FITSView> &view)
1591 {
1592     auto tab = fitsMap.value(fitsUID);
1593     if (tab)
1594     {
1595         view = tab->getView();
1596         return true;
1597     }
1598     return false;
1599 
1600 }
1601 
1602 bool FITSViewer::getCurrentView(QSharedPointer<FITSView> &view)
1603 {
1604     if (m_Tabs.empty() || fitsTabWidget->currentIndex() >= m_Tabs.count())
1605         return false;
1606 
1607     view = m_Tabs[fitsTabWidget->currentIndex()]->getView();
1608     return true;
1609 }
1610 
1611 void FITSViewer::setDebayerAction(bool enable)
1612 {
1613     actionCollection()->addAction("fits_debayer")->setEnabled(enable);
1614 }