File indexing completed on 2024-03-24 15:16:58

0001 /*
0002     SPDX-FileCopyrightText: 2003-2017 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003     SPDX-FileCopyrightText: 2016-2017 Robert Lancaster <rlancaste@gmail.com>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "config-kstars.h"
0009 #include "fitsview.h"
0010 
0011 #include "fitsdata.h"
0012 #include "fitslabel.h"
0013 #include "hips/hipsfinder.h"
0014 #include "kstarsdata.h"
0015 
0016 #include "ksutils.h"
0017 #include "Options.h"
0018 #include "skymap.h"
0019 
0020 #include "stretch.h"
0021 
0022 #ifdef HAVE_STELLARSOLVER
0023 #include "ekos/auxiliary/stellarsolverprofileeditor.h"
0024 #endif
0025 
0026 #ifdef HAVE_INDI
0027 #include "basedevice.h"
0028 #include "indi/indilistener.h"
0029 #endif
0030 
0031 #include <KActionCollection>
0032 
0033 #include <QtConcurrent>
0034 #include <QScrollBar>
0035 #include <QToolBar>
0036 #include <QGraphicsOpacityEffect>
0037 #include <QApplication>
0038 #include <QImageReader>
0039 #include <QGestureEvent>
0040 #include <QMutexLocker>
0041 #include <QElapsedTimer>
0042 
0043 #ifndef _WIN32
0044 #include <unistd.h>
0045 #endif
0046 
0047 #define BASE_OFFSET    50
0048 #define ZOOM_DEFAULT   100.0f
0049 #define ZOOM_MIN       10
0050 // ZOOM_MAX is adjusted in the constructor if the amount of physical memory is known.
0051 #define ZOOM_MAX       300
0052 #define ZOOM_LOW_INCR  10
0053 #define ZOOM_HIGH_INCR 50
0054 #define FONT_SIZE      14
0055 
0056 namespace
0057 {
0058 
0059 // Derive the Green and Blue stretch parameters from their previous values and the
0060 // changes made to the Red parameters. We apply the same offsets used for Red to the
0061 // other channels' parameters, but clip them.
0062 void ComputeGBStretchParams(const StretchParams &newParams, StretchParams* params)
0063 {
0064     float shadow_diff = newParams.grey_red.shadows - params->grey_red.shadows;
0065     float highlight_diff = newParams.grey_red.highlights - params->grey_red.highlights;
0066     float midtones_diff = newParams.grey_red.midtones - params->grey_red.midtones;
0067 
0068     params->green.shadows = params->green.shadows + shadow_diff;
0069     params->green.shadows = KSUtils::clamp(params->green.shadows, 0.0f, 1.0f);
0070     params->green.highlights = params->green.highlights + highlight_diff;
0071     params->green.highlights = KSUtils::clamp(params->green.highlights, 0.0f, 1.0f);
0072     params->green.midtones = params->green.midtones + midtones_diff;
0073     params->green.midtones = std::max(params->green.midtones, 0.0f);
0074 
0075     params->blue.shadows = params->blue.shadows + shadow_diff;
0076     params->blue.shadows = KSUtils::clamp(params->blue.shadows, 0.0f, 1.0f);
0077     params->blue.highlights = params->blue.highlights + highlight_diff;
0078     params->blue.highlights = KSUtils::clamp(params->blue.highlights, 0.0f, 1.0f);
0079     params->blue.midtones = params->blue.midtones + midtones_diff;
0080     params->blue.midtones = std::max(params->blue.midtones, 0.0f);
0081 }
0082 
0083 }  // namespace
0084 
0085 // Runs the stretch checking the variables to see which parameters to use.
0086 // We call stretch even if we're not stretching, as the stretch code still
0087 // converts the image to the uint8 output image which will be displayed.
0088 // In that case, it will use an identity stretch.
0089 void FITSView::doStretch(QImage *outputImage)
0090 {
0091     if (outputImage->isNull() || m_ImageData.isNull())
0092         return;
0093     Stretch stretch(static_cast<int>(m_ImageData->width()),
0094                     static_cast<int>(m_ImageData->height()),
0095                     m_ImageData->channels(), m_ImageData->dataType());
0096 
0097     StretchParams tempParams;
0098     if (!stretchImage)
0099         tempParams = StretchParams();  // Keeping it linear
0100     else if (autoStretch)
0101     {
0102         // Compute new auto-stretch params.
0103         stretchParams = stretch.computeParams(m_ImageData->getImageBuffer());
0104         emit newStretch(stretchParams);
0105         tempParams = stretchParams;
0106     }
0107     else
0108         // Use the existing stretch params.
0109         tempParams = stretchParams;
0110 
0111     stretch.setParams(tempParams);
0112     stretch.run(m_ImageData->getImageBuffer(), outputImage, m_PreviewSampling);
0113 }
0114 
0115 // Store stretch parameters, and turn on stretching if it isn't already on.
0116 void FITSView::setStretchParams(const StretchParams &params)
0117 {
0118     if (m_ImageData->channels() == 3)
0119         ComputeGBStretchParams(params, &stretchParams);
0120 
0121     stretchParams.grey_red = params.grey_red;
0122     stretchParams.grey_red.shadows = std::max(stretchParams.grey_red.shadows, 0.0f);
0123     stretchParams.grey_red.highlights = std::max(stretchParams.grey_red.highlights, 0.0f);
0124     stretchParams.grey_red.midtones = std::max(stretchParams.grey_red.midtones, 0.0f);
0125 
0126     autoStretch = false;
0127     stretchImage = true;
0128 
0129     if (m_ImageFrame && rescale(ZOOM_KEEP_LEVEL))
0130     {
0131         m_QueueUpdate = true;
0132         updateFrame(true);
0133     }
0134 }
0135 
0136 // Turn on or off stretching, and if on, use whatever parameters are currently stored.
0137 void FITSView::setStretch(bool onOff)
0138 {
0139     if (stretchImage != onOff)
0140     {
0141         stretchImage = onOff;
0142         if (m_ImageFrame && rescale(ZOOM_KEEP_LEVEL))
0143         {
0144             m_QueueUpdate = true;
0145             updateFrame(true);
0146         }
0147     }
0148 }
0149 
0150 // Turn on stretching, using automatically generated parameters.
0151 void FITSView::setAutoStretchParams()
0152 {
0153     stretchImage = true;
0154     autoStretch = true;
0155     if (m_ImageFrame && rescale(ZOOM_KEEP_LEVEL))
0156     {
0157         m_QueueUpdate = true;
0158         updateFrame(true);
0159     }
0160 }
0161 
0162 FITSView::FITSView(QWidget * parent, FITSMode fitsMode, FITSScale filterType) : QScrollArea(parent), m_ZoomFactor(1.2)
0163 {
0164     // stretchImage is whether to stretch or not--the stretch may or may not use automatically generated parameters.
0165     // The user may enter his/her own.
0166     stretchImage = Options::autoStretch();
0167     // autoStretch means use automatically-generated parameters. This is the default, unless the user overrides
0168     // by adjusting the stretchBar's sliders.
0169     autoStretch = true;
0170 
0171     // Adjust the maximum zoom according to the amount of memory.
0172     // There have been issues with users running out system memory because of zoom memory.
0173     // Note: this is not currently image dependent. It's possible, but not implemented,
0174     // to allow for more zooming on smaller images.
0175     zoomMax = ZOOM_MAX;
0176 
0177 #if defined (Q_OS_LINUX) || defined (Q_OS_OSX)
0178     const long numPages = sysconf(_SC_PAGESIZE);
0179     const long pageSize = sysconf(_SC_PHYS_PAGES);
0180 
0181     // _SC_PHYS_PAGES "may not be standard" http://man7.org/linux/man-pages/man3/sysconf.3.html
0182     // If an OS doesn't support it, sysconf should return -1.
0183     if (numPages > 0 && pageSize > 0)
0184     {
0185         // (numPages * pageSize) will likely overflow a 32bit int, so use floating point calculations.
0186         const int memoryMb = numPages * (static_cast<double>(pageSize) / 1e6);
0187         if (memoryMb < 2000)
0188             zoomMax = 100;
0189         else if (memoryMb < 4000)
0190             zoomMax = 200;
0191         else if (memoryMb < 8000)
0192             zoomMax = 300;
0193         else if (memoryMb < 16000)
0194             zoomMax = 400;
0195         else
0196             zoomMax = 600;
0197     }
0198 #endif
0199 
0200     grabGesture(Qt::PinchGesture);
0201 
0202     filter = filterType;
0203     mode   = fitsMode;
0204 
0205     setBackgroundRole(QPalette::Dark);
0206 
0207     markerCrosshair.setX(0);
0208     markerCrosshair.setY(0);
0209 
0210     setBaseSize(740, 530);
0211 
0212     m_ImageFrame = new FITSLabel(this);
0213     m_ImageFrame->setMouseTracking(true);
0214     connect(m_ImageFrame, &FITSLabel::newStatus, this, &FITSView::newStatus);
0215     connect(m_ImageFrame, &FITSLabel::mouseOverPixel, this, &FITSView::mouseOverPixel);
0216     connect(m_ImageFrame, &FITSLabel::pointSelected, this, &FITSView::processPointSelection);
0217     connect(m_ImageFrame, &FITSLabel::markerSelected, this, &FITSView::processMarkerSelection);
0218     connect(m_ImageFrame, &FITSLabel::rectangleSelected, this, &FITSView::processRectangle);
0219     connect(this, &FITSView::setRubberBand, m_ImageFrame, &FITSLabel::setRubberBand);
0220     connect(this, &FITSView::showRubberBand, m_ImageFrame, &FITSLabel::showRubberBand);
0221     connect(this, &FITSView::zoomRubberBand, m_ImageFrame, &FITSLabel::zoomRubberBand);
0222 
0223     connect(Options::self(), &Options::HIPSOpacityChanged, this, [this]()
0224     {
0225         if (showHiPSOverlay)
0226         {
0227             m_QueueUpdate = true;
0228             updateFrame();
0229         }
0230     });
0231     connect(Options::self(), &Options::HIPSOffsetXChanged, this, [this]()
0232     {
0233         if (showHiPSOverlay)
0234         {
0235             m_QueueUpdate = true;
0236             m_HiPSOverlayPixmap = QPixmap();
0237             updateFrame();
0238         }
0239     });
0240     connect(Options::self(), &Options::HIPSOffsetYChanged, this, [this]()
0241     {
0242         if (showHiPSOverlay)
0243         {
0244             m_QueueUpdate = true;
0245             m_HiPSOverlayPixmap = QPixmap();
0246             updateFrame();
0247         }
0248     });
0249 
0250     connect(&wcsWatcher, &QFutureWatcher<bool>::finished, this, &FITSView::syncWCSState);
0251 
0252     m_UpdateFrameTimer.setInterval(50);
0253     m_UpdateFrameTimer.setSingleShot(true);
0254     connect(&m_UpdateFrameTimer, &QTimer::timeout, this, [this]()
0255     {
0256         this->updateFrame(true);
0257     });
0258 
0259     connect(&fitsWatcher, &QFutureWatcher<bool>::finished, this, &FITSView::loadInFrame);
0260 
0261     setCursorMode(
0262         selectCursor); //This is the default mode because the Focus and Align FitsViews should not be in dragMouse mode
0263 
0264     noImageLabel = new QLabel();
0265     noImage.load(":/images/noimage.png");
0266     noImageLabel->setPixmap(noImage);
0267     noImageLabel->setAlignment(Qt::AlignCenter);
0268     setWidget(noImageLabel);
0269 
0270     redScopePixmap = QPixmap(":/icons/center_telescope_red.svg").scaled(32, 32, Qt::KeepAspectRatio, Qt::FastTransformation);
0271     magentaScopePixmap = QPixmap(":/icons/center_telescope_magenta.svg").scaled(32, 32, Qt::KeepAspectRatio,
0272                          Qt::FastTransformation);
0273 
0274     // Hack! This is same initialization as roiRB in FITSLabel.
0275     // Otherwise initial ROI selection wouldn't have stats.
0276     selectionRectangleRaw = QRect(QPoint(1, 1), QPoint(100, 100));
0277 }
0278 
0279 FITSView::~FITSView()
0280 {
0281     QMutexLocker locker(&updateMutex);
0282     m_UpdateFrameTimer.stop();
0283     m_Suspended = true;
0284     fitsWatcher.waitForFinished();
0285     wcsWatcher.waitForFinished();
0286 }
0287 
0288 /**
0289 This method looks at what mouse mode is currently selected and updates the cursor to match.
0290  */
0291 
0292 void FITSView::updateMouseCursor()
0293 {
0294     if (cursorMode == dragCursor)
0295     {
0296         if (horizontalScrollBar()->maximum() > 0 || verticalScrollBar()->maximum() > 0)
0297         {
0298             if (!m_ImageFrame->getMouseButtonDown())
0299                 viewport()->setCursor(Qt::PointingHandCursor);
0300             else
0301                 viewport()->setCursor(Qt::ClosedHandCursor);
0302         }
0303         else
0304             viewport()->setCursor(Qt::CrossCursor);
0305     }
0306     else if (cursorMode == selectCursor)
0307     {
0308         viewport()->setCursor(Qt::CrossCursor);
0309     }
0310     else if (cursorMode == scopeCursor)
0311     {
0312         viewport()->setCursor(QCursor(redScopePixmap, 10, 10));
0313     }
0314     else if (cursorMode == crosshairCursor)
0315     {
0316         viewport()->setCursor(QCursor(magentaScopePixmap, 10, 10));
0317     }
0318 }
0319 
0320 /**
0321 This is how the mouse mode gets set.
0322 The default for a FITSView in a FITSViewer should be the dragMouse
0323 The default for a FITSView in the Focus or Align module should be the selectMouse
0324 The different defaults are accomplished by putting making the actual default mouseMode
0325 the selectMouse, but when a FITSViewer loads an image, it immediately makes it the dragMouse.
0326  */
0327 
0328 void FITSView::setCursorMode(CursorMode mode)
0329 {
0330     cursorMode = mode;
0331     updateMouseCursor();
0332 
0333     if (mode == scopeCursor && imageHasWCS())
0334     {
0335         if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
0336         {
0337             QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS);
0338             wcsWatcher.setFuture(future);
0339         }
0340     }
0341 }
0342 
0343 void FITSView::resizeEvent(QResizeEvent * event)
0344 {
0345     if (m_ImageData == nullptr && noImageLabel != nullptr)
0346     {
0347         noImageLabel->setPixmap(
0348             noImage.scaled(width() - 20, height() - 20, Qt::KeepAspectRatio, Qt::FastTransformation));
0349         noImageLabel->setFixedSize(width() - 5, height() - 5);
0350     }
0351 
0352     QScrollArea::resizeEvent(event);
0353 }
0354 
0355 
0356 void FITSView::loadFile(const QString &inFilename)
0357 {
0358     if (floatingToolBar != nullptr)
0359     {
0360         floatingToolBar->setVisible(true);
0361     }
0362 
0363     bool setBayerParams = false;
0364 
0365     BayerParams param;
0366     if ((m_ImageData != nullptr) && m_ImageData->hasDebayer())
0367     {
0368         setBayerParams = true;
0369         m_ImageData->getBayerParams(&param);
0370     }
0371 
0372     // In case image is still loading, wait until it is done.
0373     fitsWatcher.waitForFinished();
0374     // In case loadWCS is still running for previous image data, let's wait until it's over
0375     wcsWatcher.waitForFinished();
0376 
0377     //    delete m_ImageData;
0378     //    m_ImageData = nullptr;
0379 
0380     filterStack.clear();
0381     filterStack.push(FITS_NONE);
0382     if (filter != FITS_NONE)
0383         filterStack.push(filter);
0384 
0385     m_ImageData.reset(new FITSData(mode), &QObject::deleteLater);
0386 
0387     if (setBayerParams)
0388         m_ImageData->setBayerParams(&param);
0389 
0390     fitsWatcher.setFuture(m_ImageData->loadFromFile(inFilename));
0391 }
0392 
0393 void FITSView::clearData()
0394 {
0395     if (!noImageLabel)
0396     {
0397         noImageLabel = new QLabel();
0398         noImage.load(":/images/noimage.png");
0399         noImageLabel->setPixmap(noImage);
0400         noImageLabel->setAlignment(Qt::AlignCenter);
0401     }
0402 
0403     setWidget(noImageLabel);
0404 
0405     m_ImageData.clear();
0406 }
0407 
0408 bool FITSView::loadData(const QSharedPointer<FITSData> &data)
0409 {
0410     if (floatingToolBar != nullptr)
0411     {
0412         floatingToolBar->setVisible(true);
0413     }
0414 
0415     // In case loadWCS is still running for previous image data, let's wait until it's over
0416     wcsWatcher.waitForFinished();
0417 
0418     filterStack.clear();
0419     filterStack.push(FITS_NONE);
0420     if (filter != FITS_NONE)
0421         filterStack.push(filter);
0422 
0423     m_HiPSOverlayPixmap = QPixmap();
0424 
0425     // Takes control of the objects passed in.
0426     m_ImageData = data;
0427     // set the image mask geometry
0428     if (m_ImageMask != nullptr)
0429         m_ImageMask->setImageGeometry(data->width(), data->height());
0430 
0431     if (processData())
0432     {
0433         emit loaded();
0434         return true;
0435     }
0436     else
0437     {
0438         emit failed(m_LastError);
0439         return false;
0440     }
0441 }
0442 
0443 bool FITSView::processData()
0444 {
0445     // Set current width and height
0446     if (!m_ImageData)
0447         return false;
0448 
0449     connect(m_ImageData.data(), &FITSData::dataChanged, this, [this]()
0450     {
0451         rescale(ZOOM_KEEP_LEVEL);
0452         updateFrame();
0453     });
0454 
0455     currentWidth = m_ImageData->width();
0456     currentHeight = m_ImageData->height();
0457 
0458     int image_width  = currentWidth;
0459     int image_height = currentHeight;
0460 
0461     if (!m_ImageFrame)
0462     {
0463         m_ImageFrame = new FITSLabel(this);
0464         m_ImageFrame->setMouseTracking(true);
0465         connect(m_ImageFrame, &FITSLabel::newStatus, this, &FITSView::newStatus);
0466         connect(m_ImageFrame, &FITSLabel::pointSelected, this, &FITSView::processPointSelection);
0467         connect(m_ImageFrame, &FITSLabel::markerSelected, this, &FITSView::processMarkerSelection);
0468     }
0469     m_ImageFrame->setSize(image_width, image_height);
0470 
0471     // Init the display image
0472     // JM 2020.01.08: Disabling as proposed by Hy
0473     //initDisplayImage();
0474 
0475     m_ImageData->applyFilter(filter);
0476 
0477     double availableRAM = 0;
0478     if (Options::adaptiveSampling() && (availableRAM = KSUtils::getAvailableRAM()) > 0)
0479     {
0480         // Possible color maximum image size
0481         double max_size = image_width * image_height * 4;
0482         // Ratio of image size to available RAM size
0483         double ratio = max_size / availableRAM;
0484 
0485         // Increase adaptive sampling with more limited RAM
0486         if (ratio < 0.1)
0487             m_AdaptiveSampling = 1;
0488         else if (ratio < 0.2)
0489             m_AdaptiveSampling = 2;
0490         else
0491             m_AdaptiveSampling = 4;
0492 
0493         m_PreviewSampling = m_AdaptiveSampling;
0494     }
0495 
0496     // Rescale to fits window on first load
0497     if (firstLoad)
0498     {
0499         currentZoom = 100;
0500 
0501         if (rescale(ZOOM_FIT_WINDOW) == false)
0502         {
0503             m_LastError = i18n("Rescaling image failed.");
0504             return false;
0505         }
0506 
0507         firstLoad = false;
0508     }
0509     else
0510     {
0511         if (rescale(ZOOM_KEEP_LEVEL) == false)
0512         {
0513             m_LastError = i18n("Rescaling image failed.");
0514             return false;
0515         }
0516     }
0517 
0518     setAlignment(Qt::AlignCenter);
0519 
0520     // Load WCS data now if selected and image contains valid WCS header
0521     if ((mode == FITS_NORMAL || mode == FITS_ALIGN) &&
0522             m_ImageData->hasWCS() && m_ImageData->getWCSState() == FITSData::Idle &&
0523             Options::autoWCS() &&
0524             !wcsWatcher.isRunning())
0525     {
0526         QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS);
0527         wcsWatcher.setFuture(future);
0528     }
0529     else
0530         syncWCSState();
0531 
0532     if (isVisible())
0533         emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION);
0534 
0535     if (showStarProfile)
0536     {
0537         if(floatingToolBar != nullptr)
0538             toggleProfileAction->setChecked(true);
0539         //Need to wait till the Focus module finds stars, if its the Focus module.
0540         QTimer::singleShot(100, this, SLOT(viewStarProfile()));
0541     }
0542 
0543     // Fore immediate load of frame for first load.
0544     m_QueueUpdate = true;
0545     updateFrame(true);
0546     return true;
0547 }
0548 
0549 void FITSView::loadInFrame()
0550 {
0551     m_LastError = m_ImageData->getLastError();
0552 
0553     // Check if the loading was OK
0554     if (fitsWatcher.result() == false)
0555     {
0556         emit failed(m_LastError);
0557         return;
0558     }
0559 
0560     // Notify if there is debayer data.
0561     emit debayerToggled(m_ImageData->hasDebayer());
0562 
0563     if (processData())
0564         emit loaded();
0565     else
0566         emit failed(m_LastError);
0567 }
0568 
0569 bool FITSView::saveImage(const QString &newFilename)
0570 {
0571     const QString ext = QFileInfo(newFilename).suffix();
0572     if (QImageReader::supportedImageFormats().contains(ext.toLatin1()))
0573     {
0574         rawImage.save(newFilename, ext.toLatin1().constData());
0575         return true;
0576     }
0577 
0578     return m_ImageData->saveImage(newFilename);
0579 }
0580 
0581 FITSView::CursorMode FITSView::getCursorMode()
0582 {
0583     return cursorMode;
0584 }
0585 
0586 void FITSView::enterEvent(QEvent * event)
0587 {
0588     Q_UNUSED(event)
0589 
0590     if (floatingToolBar && m_ImageData)
0591     {
0592         QPointer<QGraphicsOpacityEffect> eff = new QGraphicsOpacityEffect(this);
0593         floatingToolBar->setGraphicsEffect(eff);
0594         QPointer<QPropertyAnimation> a = new QPropertyAnimation(eff, "opacity");
0595         a->setDuration(500);
0596         a->setStartValue(0.2);
0597         a->setEndValue(1);
0598         a->setEasingCurve(QEasingCurve::InBack);
0599         a->start(QPropertyAnimation::DeleteWhenStopped);
0600     }
0601 }
0602 
0603 void FITSView::leaveEvent(QEvent * event)
0604 {
0605     Q_UNUSED(event)
0606 
0607     if (floatingToolBar && m_ImageData)
0608     {
0609         QPointer<QGraphicsOpacityEffect> eff = new QGraphicsOpacityEffect(this);
0610         floatingToolBar->setGraphicsEffect(eff);
0611         QPointer<QPropertyAnimation> a = new QPropertyAnimation(eff, "opacity");
0612         a->setDuration(500);
0613         a->setStartValue(1);
0614         a->setEndValue(0.2);
0615         a->setEasingCurve(QEasingCurve::OutBack);
0616         a->start(QPropertyAnimation::DeleteWhenStopped);
0617     }
0618 }
0619 
0620 bool FITSView::rescale(FITSZoom type)
0621 {
0622     if (!m_ImageData)
0623         return false;
0624 
0625     int image_width  = m_ImageData->width();
0626     int image_height = m_ImageData->height();
0627     currentWidth  = image_width;
0628     currentHeight = image_height;
0629 
0630     if (isVisible())
0631         emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION);
0632 
0633     switch (type)
0634     {
0635         case ZOOM_FIT_WINDOW:
0636             if ((image_width > width() || image_height > height()))
0637             {
0638                 double w = baseSize().width() - BASE_OFFSET;
0639                 double h = baseSize().height() - BASE_OFFSET;
0640 
0641                 if (!firstLoad)
0642                 {
0643                     w = viewport()->rect().width() - BASE_OFFSET;
0644                     h = viewport()->rect().height() - BASE_OFFSET;
0645                 }
0646 
0647                 // Find the zoom level which will enclose the current FITS in the current window size
0648                 double zoomX                  = floor((w / static_cast<double>(currentWidth)) * 100.);
0649                 double zoomY                  = floor((h / static_cast<double>(currentHeight)) * 100.);
0650                 (zoomX < zoomY) ? currentZoom = zoomX : currentZoom = zoomY;
0651 
0652                 currentWidth  = image_width * (currentZoom / ZOOM_DEFAULT);
0653                 currentHeight = image_height * (currentZoom / ZOOM_DEFAULT);
0654 
0655                 if (currentZoom <= ZOOM_MIN)
0656                     emit actionUpdated("view_zoom_out", false);
0657             }
0658             else
0659             {
0660                 currentZoom   = 100;
0661                 currentWidth  = image_width;
0662                 currentHeight = image_height;
0663             }
0664             break;
0665 
0666         case ZOOM_KEEP_LEVEL:
0667         {
0668             currentWidth  = image_width * (currentZoom / ZOOM_DEFAULT);
0669             currentHeight = image_height * (currentZoom / ZOOM_DEFAULT);
0670         }
0671         break;
0672 
0673         default:
0674             currentZoom = 100;
0675 
0676             break;
0677     }
0678 
0679     initDisplayImage();
0680     m_ImageFrame->setScaledContents(true);
0681     doStretch(&rawImage);
0682     setWidget(m_ImageFrame);
0683 
0684     // This is needed by fitstab, even if the zoom doesn't change, to change the stretch UI.
0685     emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM);
0686     return true;
0687 }
0688 
0689 void FITSView::ZoomIn()
0690 {
0691     if (!m_ImageData)
0692         return;
0693 
0694     if (currentZoom >= ZOOM_DEFAULT && Options::limitedResourcesMode())
0695     {
0696         emit newStatus(i18n("Cannot zoom in further due to active limited resources mode."), FITS_MESSAGE);
0697         return;
0698     }
0699 
0700     if (currentZoom < ZOOM_DEFAULT)
0701         currentZoom += ZOOM_LOW_INCR;
0702     else
0703         currentZoom += ZOOM_HIGH_INCR;
0704 
0705     emit actionUpdated("view_zoom_out", true);
0706     if (currentZoom >= zoomMax)
0707     {
0708         currentZoom = zoomMax;
0709         emit actionUpdated("view_zoom_in", false);
0710     }
0711 
0712     currentWidth  = m_ImageData->width() * (currentZoom / ZOOM_DEFAULT);
0713     currentHeight = m_ImageData->height() * (currentZoom / ZOOM_DEFAULT);
0714 
0715     cleanUpZoom();
0716 
0717     updateFrame(true);
0718 
0719     emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM);
0720     emit zoomRubberBand(getCurrentZoom() / ZOOM_DEFAULT);
0721 }
0722 
0723 void FITSView::ZoomOut()
0724 {
0725     if (!m_ImageData)
0726         return;
0727 
0728     if (currentZoom <= ZOOM_DEFAULT)
0729         currentZoom -= ZOOM_LOW_INCR;
0730     else
0731         currentZoom -= ZOOM_HIGH_INCR;
0732 
0733     if (currentZoom <= ZOOM_MIN)
0734     {
0735         currentZoom = ZOOM_MIN;
0736         emit actionUpdated("view_zoom_out", false);
0737     }
0738 
0739     emit actionUpdated("view_zoom_in", true);
0740 
0741     currentWidth  = m_ImageData->width() * (currentZoom / ZOOM_DEFAULT);
0742     currentHeight = m_ImageData->height() * (currentZoom / ZOOM_DEFAULT);
0743 
0744     cleanUpZoom();
0745 
0746     updateFrame(true);
0747 
0748     emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM);
0749     emit zoomRubberBand(getCurrentZoom() / ZOOM_DEFAULT);
0750 }
0751 
0752 void FITSView::ZoomToFit()
0753 {
0754     if (!m_ImageData)
0755         return;
0756 
0757     if (rawImage.isNull() == false)
0758     {
0759         rescale(ZOOM_FIT_WINDOW);
0760         updateFrame(true);
0761     }
0762     emit zoomRubberBand(getCurrentZoom() / ZOOM_DEFAULT);
0763 }
0764 
0765 
0766 
0767 int FITSView::filterStars()
0768 {
0769     return ((m_ImageMask.isNull() == false
0770              && m_ImageMask->active()) ? m_ImageData->filterStars(m_ImageMask) : m_ImageData->getStarCenters().count());
0771 }
0772 
0773 void FITSView::setImageMask(ImageMask *mask)
0774 {
0775     if (m_ImageMask.isNull() == false)
0776     {
0777         // copy image geometry from the old mask before deleting it
0778         if (mask != nullptr)
0779             mask->setImageGeometry(m_ImageMask->width(), m_ImageMask->height());
0780     }
0781 
0782     m_ImageMask.reset(mask);
0783 }
0784 
0785 // isImageLarge() returns whether we use the large-image rendering strategy or the small-image strategy.
0786 // See the comment below in getScale() for details.
0787 bool FITSView::isLargeImage()
0788 {
0789     constexpr int largeImageNumPixels = 1000 * 1000;
0790     return rawImage.width() * rawImage.height() >= largeImageNumPixels;
0791 }
0792 
0793 // getScale() is related to the image and overlay rendering strategy used.
0794 // If we're using a pixmap appropriate for a large image, where we draw and render on a pixmap that's the image size
0795 // and we let the QLabel deal with scaling and zooming, then the scale is 1.0.
0796 // With smaller images, where memory use is not as severe, we create a pixmap that's the size of the scaled image
0797 // and get scale returns the ratio of that pixmap size to the image size.
0798 double FITSView::getScale()
0799 {
0800     return (isLargeImage() ? 1.0 : currentZoom / ZOOM_DEFAULT) / m_PreviewSampling;
0801 }
0802 
0803 // scaleSize() is only used with the large-image rendering strategy. It may increase the line
0804 // widths or font sizes, as we draw lines and render text on the full image and when zoomed out,
0805 // these sizes may be too small.
0806 double FITSView::scaleSize(double size)
0807 {
0808     if (!isLargeImage())
0809         return size;
0810     return (currentZoom > 100.0 ? size : std::round(size * 100.0 / currentZoom)) / m_PreviewSampling;
0811 }
0812 
0813 void FITSView::updateFrame(bool now)
0814 {
0815     QMutexLocker locker(&updateMutex);
0816 
0817     // Do not process if suspended.
0818     if (m_Suspended)
0819         return;
0820 
0821     // JM 2021-03-13: This timer is used to throttle updateFrame calls to improve performance
0822     // If after 250ms no further update frames are called, then the actual update is triggered.
0823     // JM 2021-03-16: When stretching in progress, immediately execute so that the user see the changes
0824     // in real time
0825     if (now)
0826     {
0827         if (toggleStretchAction)
0828             toggleStretchAction->setChecked(stretchImage);
0829 
0830         // We employ two schemes for managing the image and its overlays, depending on the size of the image
0831         // and whether we need to therefore conserve memory. The small-image strategy explicitly scales up
0832         // the image, and writes overlays on the scaled pixmap. The large-image strategy uses a pixmap that's
0833         // the size of the image itself, never scaling that up.
0834         if (isLargeImage())
0835             updateFrameLargeImage();
0836         else
0837             updateFrameSmallImage();
0838 
0839         if (m_QueueUpdate && m_StretchingInProgress == false)
0840         {
0841             m_QueueUpdate = false;
0842             emit updated();
0843         }
0844     }
0845     else
0846         m_UpdateFrameTimer.start();
0847 }
0848 
0849 
0850 bool FITSView::initDisplayPixmap(QImage &image, float scale)
0851 {
0852     ImageMosaicMask *mask = dynamic_cast<ImageMosaicMask *>(m_ImageMask.get());
0853 
0854     // if no mosaic should be created, simply convert the original image
0855     if (mask == nullptr)
0856         return displayPixmap.convertFromImage(image);
0857 
0858     // check image geometry, sincd scaling could have changed it
0859     // create the 3x3 mosaic
0860     int width = mask->tileWidth() * mask->width() / 100;
0861     int space = mask->space();
0862     // create a new all black pixmap with mosaic size
0863     displayPixmap = QPixmap((3 * width + 2 * space) * scale, (3 * width + 2 * space) * scale);
0864     displayPixmap.fill(Qt::black);
0865 
0866     QPainter painter(&displayPixmap);
0867     int pos = 0;
0868     // paint tiles
0869     for (QRect tile : mask->tiles())
0870     {
0871         const int posx = pos % 3;
0872         const int posy = pos++ / 3;
0873         const int tilewidth = width * scale;
0874         QRectF source(tile.x() * scale, tile.y()*scale, tilewidth, tilewidth);
0875         QRectF target((posx * (width + space)) * scale, (posy * (width + space)) * scale, width * scale, width * scale);
0876         painter.drawImage(target, image, source);
0877     }
0878     return true;
0879 }
0880 
0881 void FITSView::updateFrameLargeImage()
0882 {
0883     if (!initDisplayPixmap(rawImage, 1.0 / m_PreviewSampling))
0884         return;
0885     QPainter painter(&displayPixmap);
0886     // Possibly scale the fonts as we're drawing on the full image, not just the visible part of the scroll window.
0887     QFont font = painter.font();
0888     font.setPixelSize(scaleSize(FONT_SIZE));
0889     painter.setFont(font);
0890 
0891     drawStarRingFilter(&painter, 1.0 / m_PreviewSampling, dynamic_cast<ImageRingMask *>(m_ImageMask.get()));
0892     drawOverlay(&painter, 1.0 / m_PreviewSampling);
0893     m_ImageFrame->setPixmap(displayPixmap);
0894     m_ImageFrame->resize(((m_PreviewSampling * currentZoom) / 100.0) * displayPixmap.size());
0895 }
0896 
0897 void FITSView::updateFrameSmallImage()
0898 {
0899     QImage scaledImage = rawImage.scaled(currentWidth, currentHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);
0900     if (!initDisplayPixmap(scaledImage, currentZoom / ZOOM_DEFAULT))
0901         return;
0902 
0903     QPainter painter(&displayPixmap);
0904     // Possibly scale the fonts as we're drawing on the full image, not just the visible part of the scroll window.
0905     QFont font = painter.font();
0906     drawStarRingFilter(&painter, currentZoom / ZOOM_DEFAULT, dynamic_cast<ImageRingMask *>(m_ImageMask.get()));
0907     drawOverlay(&painter, currentZoom / ZOOM_DEFAULT);
0908     m_ImageFrame->setPixmap(displayPixmap);
0909     m_ImageFrame->resize(currentWidth, currentHeight);
0910 }
0911 
0912 void FITSView::drawStarRingFilter(QPainter *painter, double scale, ImageRingMask *ringMask)
0913 {
0914     if (ringMask == nullptr || !ringMask->active())
0915         return;
0916 
0917     const double w = m_ImageData->width() * scale;
0918     const double h = m_ImageData->height() * scale;
0919     double const diagonal = std::sqrt(w * w + h * h) / 2;
0920     int const innerRadius = std::lround(diagonal * ringMask->innerRadius());
0921     int const outerRadius = std::lround(diagonal * ringMask->outerRadius());
0922     QPoint const center(w / 2, h / 2);
0923     painter->save();
0924     painter->setPen(QPen(Qt::blue, scaleSize(1), Qt::DashLine));
0925     painter->setOpacity(0.7);
0926     painter->setBrush(QBrush(Qt::transparent));
0927     painter->drawEllipse(center, outerRadius, outerRadius);
0928     painter->setBrush(QBrush(Qt::blue, Qt::FDiagPattern));
0929     painter->drawEllipse(center, innerRadius, innerRadius);
0930     painter->restore();
0931 }
0932 
0933 namespace
0934 {
0935 
0936 template <typename T>
0937 int drawClippingOneChannel(T *inputBuffer, QPainter *painter, int width, int height, double clipVal, double scale)
0938 {
0939     int numClipped = 0;
0940     painter->save();
0941     painter->setPen(QPen(Qt::red, scale, Qt::SolidLine));
0942     const T clipping = clipVal;
0943     constexpr int timeoutMilliseconds = 3 * 1000;
0944     QElapsedTimer timer;
0945     timer.start();
0946     QPoint p;
0947     for (int y = 0; y < height; y++)
0948     {
0949         auto inputLine  = inputBuffer + y * width;
0950         p.setY(y);
0951         for (int x = 0; x < width; x++)
0952         {
0953             if (*inputLine++ > clipping)
0954             {
0955                 numClipped++;
0956                 const int start = x;
0957                 // Use this inner loop to recognize strings of clipped pixels
0958                 // and draw lines instead of multiple calls to drawPoints.
0959                 while (true)
0960                 {
0961                     if (++x >= width)
0962                     {
0963                         painter->drawLine(start, y, width - 1, y);
0964                         break;
0965                     }
0966                     if (*inputLine++ > clipping)
0967                         numClipped++;
0968                     else
0969                     {
0970                         if (x == start + 1)
0971                         {
0972                             p.setX(start);
0973                             painter->drawPoints(&p, 1);
0974                         }
0975                         else
0976                             painter->drawLine(start, y, x - 1, y);
0977                         break;
0978                     }
0979                 }
0980             }
0981         }
0982         if (timer.elapsed() > timeoutMilliseconds)
0983         {
0984             painter->restore();
0985             return -1;
0986         }
0987     }
0988     painter->restore();
0989     return numClipped;
0990 }
0991 
0992 template <typename T>
0993 int drawClippingThreeChannels(T *inputBuffer, QPainter *painter, int width, int height, double clipVal, double scale)
0994 {
0995     int numClipped = 0;
0996     painter->save();
0997     painter->setPen(QPen(Qt::red, scale, Qt::SolidLine));
0998     const T clipping = clipVal;
0999     constexpr int timeoutMilliseconds = 3 * 1000;
1000     QElapsedTimer timer;
1001     timer.start();
1002     QPoint p;
1003     const int size = width * height;
1004     for (int y = 0; y < height; y++)
1005     {
1006         // R, G, B input images are stored one after another.
1007         const T * inputLineR  = inputBuffer + y * width;
1008         const T * inputLineG  = inputLineR + size;
1009         const T * inputLineB  = inputLineG + size;
1010         p.setY(y);
1011 
1012         for (int x = 0; x < width; x++)
1013         {
1014             T inputR = inputLineR[x];
1015             T inputG = inputLineG[x];
1016             T inputB = inputLineB[x];
1017 
1018             if (inputR > clipping || inputG > clipping || inputB > clipping)
1019             {
1020                 numClipped++;
1021                 const int start = x;
1022 
1023                 // Use this inner loop to recognize strings of clipped pixels
1024                 // and draw lines instead of multiple calls to drawPoints.
1025                 while (true)
1026                 {
1027                     if (++x >= width)
1028                     {
1029                         painter->drawLine(start, y, width - 1, y);
1030                         break;
1031                     }
1032                     T inputR2 = inputLineR[x];
1033                     T inputG2 = inputLineG[x];
1034                     T inputB2 = inputLineB[x];
1035                     if (inputR2 > clipping || inputG2 > clipping || inputB2 > clipping)
1036                         numClipped++;
1037                     else
1038                     {
1039                         if (x == start + 1)
1040                         {
1041                             p.setX(start);
1042                             painter->drawPoints(&p, 1);
1043                         }
1044                         else
1045                             painter->drawLine(start, y, x - 1, y);
1046                         break;
1047                     }
1048                 }
1049             }
1050         }
1051         if (timer.elapsed() > timeoutMilliseconds)
1052         {
1053             painter->restore();
1054             return -1;
1055         }
1056     }
1057     painter->restore();
1058     return numClipped;
1059 }
1060 
1061 template <typename T>
1062 int drawClip(T *input_buffer, int num_channels, QPainter *painter, int width, int height, double clipVal, double scale)
1063 {
1064     if (num_channels == 1)
1065         return drawClippingOneChannel(input_buffer, painter, width, height, clipVal, scale);
1066     else if (num_channels == 3)
1067         return drawClippingThreeChannels(input_buffer, painter, width, height, clipVal, scale);
1068     else return 0;
1069 }
1070 
1071 }  // namespace
1072 
1073 void FITSView::drawClipping(QPainter *painter)
1074 {
1075     auto input = m_ImageData->getImageBuffer();
1076     const int height = m_ImageData->height();
1077     const int width = m_ImageData->width();
1078     const double FLOAT_CLIP = Options::clipping64KValue();
1079     const double SHORT_CLIP = Options::clipping64KValue();
1080     const double USHORT_CLIP = Options::clipping64KValue();
1081     const double BYTE_CLIP = Options::clipping256Value();
1082     switch (m_ImageData->dataType())
1083     {
1084         case TBYTE:
1085             m_NumClipped = drawClip(reinterpret_cast<uint8_t const*>(input), m_ImageData->channels(), painter, width, height, BYTE_CLIP,
1086                                     scaleSize(1));
1087             break;
1088         case TSHORT:
1089             m_NumClipped = drawClip(reinterpret_cast<short const*>(input), m_ImageData->channels(), painter, width, height, SHORT_CLIP,
1090                                     scaleSize(1));
1091             break;
1092         case TUSHORT:
1093             m_NumClipped = drawClip(reinterpret_cast<unsigned short const*>(input), m_ImageData->channels(), painter, width, height,
1094                                     USHORT_CLIP,
1095                                     scaleSize(1));
1096             break;
1097         case TLONG:
1098             m_NumClipped = drawClip(reinterpret_cast<long const*>(input), m_ImageData->channels(), painter, width, height, USHORT_CLIP,
1099                                     scaleSize(1));
1100             break;
1101         case TFLOAT:
1102             m_NumClipped = drawClip(reinterpret_cast<float const*>(input), m_ImageData->channels(), painter, width, height, FLOAT_CLIP,
1103                                     scaleSize(1));
1104             break;
1105         case TLONGLONG:
1106             m_NumClipped = drawClip(reinterpret_cast<long long const*>(input), m_ImageData->channels(), painter, width, height,
1107                                     USHORT_CLIP,
1108                                     scaleSize(1));
1109             break;
1110         case TDOUBLE:
1111             m_NumClipped = drawClip(reinterpret_cast<double const*>(input), m_ImageData->channels(), painter, width, height, FLOAT_CLIP,
1112                                     scaleSize(1));
1113             break;
1114         default:
1115             m_NumClipped = 0;
1116             break;
1117     }
1118     if (m_NumClipped < 0)
1119         emit newStatus(QString("Clip:failed"), FITS_CLIP);
1120     else
1121         emit newStatus(QString("Clip:%1").arg(m_NumClipped), FITS_CLIP);
1122 }
1123 
1124 void FITSView::ZoomDefault()
1125 {
1126     if (m_ImageFrame)
1127     {
1128         emit actionUpdated("view_zoom_out", true);
1129         emit actionUpdated("view_zoom_in", true);
1130 
1131         currentZoom   = ZOOM_DEFAULT;
1132         currentWidth  = m_ImageData->width();
1133         currentHeight = m_ImageData->height();
1134 
1135         updateFrame();
1136 
1137         emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM);
1138 
1139         update();
1140     }
1141 }
1142 
1143 void FITSView::drawOverlay(QPainter * painter, double scale)
1144 {
1145     painter->setRenderHint(QPainter::Antialiasing, Options::useAntialias());
1146 
1147 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1148     if (showHiPSOverlay)
1149         drawHiPSOverlay(painter, scale);
1150 #endif
1151 
1152     if (trackingBoxEnabled && getCursorMode() != FITSView::scopeCursor)
1153         drawTrackingBox(painter, scale);
1154 
1155     if (!markerCrosshair.isNull())
1156         drawMarker(painter, scale);
1157 
1158     if (showCrosshair)
1159         drawCrosshair(painter, scale);
1160 
1161     if (showObjects)
1162         drawObjectNames(painter, scale);
1163 
1164 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1165     if (showEQGrid)
1166         drawEQGrid(painter, scale);
1167 #endif
1168 
1169     if (showPixelGrid)
1170         drawPixelGrid(painter, scale);
1171 
1172     if (markStars)
1173         drawStarCentroid(painter, scale);
1174 
1175     if (showClipping)
1176         drawClipping(painter);
1177 
1178     if (showMagnifyingGlass)
1179         drawMagnifyingGlass(painter, scale);
1180 }
1181 
1182 // Draws a 100% resolution image rectangle around the mouse position.
1183 void FITSView::drawMagnifyingGlass(QPainter *painter, double scale)
1184 {
1185     if (magnifyingGlassX >= 0 && magnifyingGlassY >= 0 &&
1186             magnifyingGlassX < m_ImageData->width() &&
1187             magnifyingGlassY < m_ImageData->height())
1188     {
1189         // Amount of magnification.
1190         constexpr double magAmount = 8;
1191         // Desired size in pixels of the magnification window.
1192         constexpr int magWindowSize = 130;
1193         // The distance from the mouse position to the magnifying glass rectangle, in the source image coordinates.
1194         const int winXOffset = magWindowSize * 10.0 / currentZoom;
1195         const int winYOffset = magWindowSize * 10.0 / currentZoom;
1196         // Size of a side of the square of input to make a window that size.
1197         const int inputDimension = magWindowSize * 100 / currentZoom;
1198         // Size of the square drawn. Not the same, necessarily as the magWindowSize,
1199         // since the output may be scaled (if isLargeImage()==true) to become screen pixels.
1200         const int outputDimension = inputDimension * scale + .99;
1201 
1202         // Where the source data (to be magnified) comes from.
1203         int imgLeft = magnifyingGlassX - inputDimension / (2 * magAmount);
1204         int imgTop = magnifyingGlassY - inputDimension / (2 * magAmount);
1205 
1206         // Where we'll draw the magnifying glass rectangle.
1207         int winLeft = magnifyingGlassX + winXOffset;
1208         int winTop = magnifyingGlassY + winYOffset;
1209 
1210         // Normally we place the magnifying glass rectangle to the right and below the mouse curson.
1211         // However, if it would be rendered outside the image, put it on the other side.
1212         int w = rawImage.width();
1213         int h = rawImage.height();
1214         const int rightLimit = std::min(w, static_cast<int>((horizontalScrollBar()->value() + width()) * 100 / currentZoom));
1215         const int bottomLimit = std::min(h, static_cast<int>((verticalScrollBar()->value() + height()) * 100 / currentZoom));
1216         if (winLeft + winXOffset + inputDimension > rightLimit)
1217             winLeft -= (2 * winXOffset + inputDimension);
1218         if (winTop + winYOffset + inputDimension > bottomLimit)
1219             winTop -= (2 * winYOffset + inputDimension);
1220 
1221         // Blacken the output where magnifying outside the source image.
1222         if ((imgLeft < 0 ) ||
1223                 (imgLeft + inputDimension / magAmount >= w) ||
1224                 (imgTop < 0) ||
1225                 (imgTop + inputDimension / magAmount > h))
1226         {
1227             painter->setBrush(QBrush(Qt::black));
1228             painter->drawRect(winLeft * scale, winTop * scale, outputDimension, outputDimension);
1229             painter->setBrush(QBrush(Qt::transparent));
1230         }
1231 
1232         // Finally, draw the magnified image.
1233         painter->drawImage(QRect(winLeft * scale, winTop * scale, outputDimension, outputDimension),
1234                            rawImage,
1235                            QRect(imgLeft, imgTop, inputDimension / magAmount, inputDimension / magAmount));
1236         // Draw a white border.
1237         painter->setPen(QPen(Qt::white, scaleSize(1)));
1238         painter->drawRect(winLeft * scale, winTop * scale, outputDimension, outputDimension);
1239     }
1240 }
1241 
1242 // x,y are the image coordinates where the magnifying glass is positioned.
1243 void FITSView::updateMagnifyingGlass(int x, int y)
1244 {
1245     if (!m_ImageData)
1246         return;
1247 
1248     magnifyingGlassX = x;
1249     magnifyingGlassY = y;
1250     if (magnifyingGlassX == -1 && magnifyingGlassY == -1)
1251     {
1252         if (showMagnifyingGlass)
1253             updateFrame(true);
1254         showMagnifyingGlass = false;
1255     }
1256     else
1257     {
1258         showMagnifyingGlass = true;
1259         updateFrame(true);
1260     }
1261 }
1262 
1263 void FITSView::updateMode(FITSMode fmode)
1264 {
1265     mode = fmode;
1266 }
1267 
1268 void FITSView::drawMarker(QPainter * painter, double scale)
1269 {
1270     painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")),
1271                          scaleSize(2)));
1272     painter->setBrush(Qt::NoBrush);
1273     const float pxperdegree = scale * (57.3 / 1.8);
1274 
1275     const float s1 = 0.5 * pxperdegree;
1276     const float s2 = pxperdegree;
1277     const float s3 = 2.0 * pxperdegree;
1278 
1279     const float x0 = scale * markerCrosshair.x();
1280     const float y0 = scale * markerCrosshair.y();
1281     const float x1 = x0 - 0.5 * s1;
1282     const float y1 = y0 - 0.5 * s1;
1283     const float x2 = x0 - 0.5 * s2;
1284     const float y2 = y0 - 0.5 * s2;
1285     const float x3 = x0 - 0.5 * s3;
1286     const float y3 = y0 - 0.5 * s3;
1287 
1288     //Draw radial lines
1289     painter->drawLine(QPointF(x1, y0), QPointF(x3, y0));
1290     painter->drawLine(QPointF(x0 + s2, y0), QPointF(x0 + 0.5 * s1, y0));
1291     painter->drawLine(QPointF(x0, y1), QPointF(x0, y3));
1292     painter->drawLine(QPointF(x0, y0 + 0.5 * s1), QPointF(x0, y0 + s2));
1293     //Draw circles at 0.5 & 1 degrees
1294     painter->drawEllipse(QRectF(x1, y1, s1, s1));
1295     painter->drawEllipse(QRectF(x2, y2, s2, s2));
1296 }
1297 
1298 bool FITSView::drawHFR(QPainter * painter, const QString &hfr, int x, int y)
1299 {
1300     QRect const boundingRect(0, 0, painter->device()->width(), painter->device()->height());
1301     QSize const hfrSize = painter->fontMetrics().size(Qt::TextSingleLine, hfr);
1302 
1303     // Store the HFR text in a rect
1304     QPoint const hfrBottomLeft(x, y);
1305     QRect const hfrRect(hfrBottomLeft.x(), hfrBottomLeft.y() - hfrSize.height(), hfrSize.width(), hfrSize.height());
1306 
1307     // Render the HFR text only if it can be displayed entirely
1308     if (boundingRect.contains(hfrRect))
1309     {
1310         painter->setPen(QPen(Qt::red, scaleSize(3)));
1311         painter->drawText(hfrBottomLeft, hfr);
1312         painter->setPen(QPen(Qt::red, scaleSize(2)));
1313         return true;
1314     }
1315     return false;
1316 }
1317 
1318 
1319 void FITSView::drawStarCentroid(QPainter * painter, double scale)
1320 {
1321     QFont painterFont;
1322     double fontSize = painterFont.pointSizeF() * 2;
1323     painter->setRenderHint(QPainter::Antialiasing);
1324     if (showStarsHFR)
1325     {
1326         // If we need to print the HFR out, give an arbitrarily sized font to the painter
1327         if (isLargeImage())
1328             fontSize = scaleSize(painterFont.pointSizeF());
1329         painterFont.setPointSizeF(fontSize);
1330         painter->setFont(painterFont);
1331     }
1332 
1333     painter->setPen(QPen(Qt::red, scaleSize(2)));
1334     ImageMosaicMask *mask = dynamic_cast<ImageMosaicMask *>(m_ImageMask.get());
1335 
1336     for (auto const &starCenter : m_ImageData->getStarCenters())
1337     {
1338         int const w  = std::round(starCenter->width) * scale;
1339 
1340         // translate if a mosaic mask is present
1341         const QPointF center = (mask == nullptr) ? QPointF(starCenter->x, starCenter->y) : mask->translate(QPointF(starCenter->x,
1342                                starCenter->y));
1343         // Draw a circle around the detected star.
1344         // SEP coordinates are in the center of pixels, and Qt at the boundary.
1345         const double xCoord = center.x() - 0.5;
1346         const double yCoord = center.y() - 0.5;
1347         const int xc = std::round((xCoord - starCenter->width / 2.0f) * scale);
1348         const int yc = std::round((yCoord - starCenter->width / 2.0f) * scale);
1349         const int hw = w / 2;
1350 
1351         BahtinovEdge* bEdge = dynamic_cast<BahtinovEdge*>(starCenter);
1352         if (bEdge != nullptr)
1353         {
1354             // Draw lines of diffraction pattern
1355             painter->setPen(QPen(Qt::red, scaleSize(2)));
1356             painter->drawLine(bEdge->line[0].x1() * scale, bEdge->line[0].y1() * scale,
1357                               bEdge->line[0].x2() * scale, bEdge->line[0].y2() * scale);
1358             painter->setPen(QPen(Qt::green, scaleSize(2)));
1359             painter->drawLine(bEdge->line[1].x1() * scale, bEdge->line[1].y1() * scale,
1360                               bEdge->line[1].x2() * scale, bEdge->line[1].y2() * scale);
1361             painter->setPen(QPen(Qt::darkGreen, scaleSize(2)));
1362             painter->drawLine(bEdge->line[2].x1() * scale, bEdge->line[2].y1() * scale,
1363                               bEdge->line[2].x2() * scale, bEdge->line[2].y2() * scale);
1364 
1365             // Draw center circle
1366             painter->setPen(QPen(Qt::white, scaleSize(2)));
1367             painter->drawEllipse(xc, yc, w, w);
1368 
1369             // Draw offset circle
1370             double factor = 15.0;
1371             QPointF offsetVector = (bEdge->offset - QPointF(center.x(), center.y())) * factor;
1372             int const xo = std::round((center.x() + offsetVector.x() - starCenter->width / 2.0f) * scale);
1373             int const yo = std::round((center.y() + offsetVector.y() - starCenter->width / 2.0f) * scale);
1374             painter->setPen(QPen(Qt::red, scaleSize(2)));
1375             painter->drawEllipse(xo, yo, w, w);
1376 
1377             // Draw line between center circle and offset circle
1378             painter->setPen(QPen(Qt::red, scaleSize(2)));
1379             painter->drawLine(xc + hw, yc + hw, xo + hw, yo + hw);
1380         }
1381         else
1382         {
1383             if (!showStarsHFR)
1384             {
1385                 const double radius = starCenter->HFR > 0 ? 2.0f * starCenter->HFR * scale : w;
1386                 painter->drawEllipse(QPointF(xCoord * scale, yCoord * scale), radius, radius);
1387             }
1388         }
1389 
1390         if (showStarsHFR)
1391         {
1392             // Ask the painter how large will the HFR text be
1393             QString const hfr = QString("%1").arg(starCenter->HFR, 0, 'f', 2);
1394             if (!drawHFR(painter, hfr, xc + w + 5, yc + w / 2))
1395             {
1396                 // Try a few more time with smaller fonts;
1397                 for (int i = 0; i < 10; ++i)
1398                 {
1399                     const double tempFontSize = painterFont.pointSizeF() - 2;
1400                     if (tempFontSize <= 0) break;
1401                     painterFont.setPointSizeF(tempFontSize);
1402                     painter->setFont(painterFont);
1403                     if (drawHFR(painter, hfr, xc + w + 5, yc + w / 2))
1404                         break;
1405                 }
1406                 // Reset the font size.
1407                 painterFont.setPointSize(fontSize);
1408                 painter->setFont(painterFont);
1409             }
1410         }
1411     }
1412 }
1413 
1414 void FITSView::drawTrackingBox(QPainter * painter, double scale)
1415 {
1416     painter->setPen(QPen(Qt::green, scaleSize(2)));
1417 
1418     if (trackingBox.isNull())
1419         return;
1420 
1421     const int x1 = trackingBox.x() * scale;
1422     const int y1 = trackingBox.y() * scale;
1423     const int w  = trackingBox.width() * scale;
1424     const int h  = trackingBox.height() * scale;
1425 
1426     painter->drawRect(x1, y1, w, h);
1427 }
1428 
1429 /**
1430 This Method draws a large Crosshair in the center of the image, it is like a set of axes.
1431  */
1432 
1433 void FITSView::drawCrosshair(QPainter * painter, double scale)
1434 {
1435     if (!m_ImageData) return;
1436     const int image_width = m_ImageData->width();
1437     const int image_height = m_ImageData->height();
1438     const QPointF c   = QPointF((qreal)image_width / 2 * scale, (qreal)image_height / 2 * scale);
1439     const float midX  = (float)image_width / 2 * scale;
1440     const float midY  = (float)image_height / 2 * scale;
1441     const float maxX  = (float)image_width * scale;
1442     const float maxY  = (float)image_height * scale;
1443     const float r = 50 * scale;
1444 
1445     painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")), scaleSize(1)));
1446 
1447     //Horizontal Line to Circle
1448     painter->drawLine(0, midY, midX - r, midY);
1449 
1450     //Horizontal Line past Circle
1451     painter->drawLine(midX + r, midY, maxX, midY);
1452 
1453     //Vertical Line to Circle
1454     painter->drawLine(midX, 0, midX, midY - r);
1455 
1456     //Vertical Line past Circle
1457     painter->drawLine(midX, midY + r, midX, maxY);
1458 
1459     //Circles
1460     painter->drawEllipse(c, r, r);
1461     painter->drawEllipse(c, r / 2, r / 2);
1462 }
1463 
1464 /**
1465 This method is intended to draw a pixel grid onto the image.  It first determines useful information
1466 from the image.  Then it draws the axes on the image if the crosshairs are not displayed.
1467 Finally it draws the gridlines so that there will be 4 Gridlines on either side of the axes.
1468 Note: This has to start drawing at the center not at the edges because the center axes must
1469 be in the center of the image.
1470  */
1471 
1472 void FITSView::drawPixelGrid(QPainter * painter, double scale)
1473 {
1474     const float width  = m_ImageData->width() * scale;
1475     const float height = m_ImageData->height() * scale;
1476     const float cX     = width / 2;
1477     const float cY     = height / 2;
1478     const float deltaX = width / 10;
1479     const float deltaY = height / 10;
1480     QFontMetrics fm(painter->font());
1481 
1482     //draw the Axes
1483     painter->setPen(QPen(Qt::red, scaleSize(1)));
1484     painter->drawText(cX - 30, height - 5, QString::number((int)((cX) / scale)));
1485     QString str = QString::number((int)((cY) / scale));
1486 #if QT_VERSION < QT_VERSION_CHECK(5,11,0)
1487     painter->drawText(width - (fm.width(str) + 10), cY - 5, str);
1488 #else
1489     painter->drawText(width - (fm.horizontalAdvance(str) + 10), cY - 5, str);
1490 #endif
1491     if (!showCrosshair)
1492     {
1493         painter->drawLine(cX, 0, cX, height);
1494         painter->drawLine(0, cY, width, cY);
1495     }
1496     painter->setPen(QPen(Qt::gray, scaleSize(1)));
1497     //Start one iteration past the Center and draw 4 lines on either side of 0
1498     for (int x = deltaX; x < cX - deltaX; x += deltaX)
1499     {
1500         painter->drawText(cX + x - 30, height - 5, QString::number((int)(cX + x) / scale));
1501         painter->drawText(cX - x - 30, height - 5, QString::number((int)(cX - x) / scale));
1502         painter->drawLine(cX - x, 0, cX - x, height);
1503         painter->drawLine(cX + x, 0, cX + x, height);
1504     }
1505     //Start one iteration past the Center and draw 4 lines on either side of 0
1506     for (int y = deltaY; y < cY - deltaY; y += deltaY)
1507     {
1508         QString str = QString::number((int)((cY + y) / scale));
1509 #if QT_VERSION < QT_VERSION_CHECK(5,11,0)
1510         painter->drawText(width - (fm.width(str) + 10), cY + y - 5, str);
1511 #else
1512         painter->drawText(width - (fm.horizontalAdvance(str) + 10), cY + y - 5, str);
1513 #endif
1514         str = QString::number((int)((cY - y) / scale));
1515 #if QT_VERSION < QT_VERSION_CHECK(5,11,0)
1516         painter->drawText(width - (fm.width(str) + 10), cY - y - 5, str);
1517 #else
1518         painter->drawText(width - (fm.horizontalAdvance(str) + 10), cY - y - 5, str);
1519 #endif
1520         painter->drawLine(0, cY + y, width, cY + y);
1521         painter->drawLine(0, cY - y, width, cY - y);
1522     }
1523 }
1524 bool FITSView::imageHasWCS()
1525 {
1526     if (m_ImageData != nullptr)
1527         return m_ImageData->hasWCS();
1528     return false;
1529 }
1530 
1531 void FITSView::drawObjectNames(QPainter * painter, double scale)
1532 {
1533     painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("FITSObjectLabelColor"))));
1534     for (const auto &listObject : m_ImageData->getSkyObjects())
1535     {
1536         painter->drawRect(listObject->x() * scale - 5, listObject->y() * scale - 5, 10, 10);
1537         painter->drawText(listObject->x() * scale + 10, listObject->y() * scale + 10, listObject->skyObject()->name());
1538     }
1539 }
1540 
1541 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1542 void FITSView::drawHiPSOverlay(QPainter * painter, double scale)
1543 {
1544     if (m_HiPSOverlayPixmap.isNull())
1545     {
1546         auto width = m_ImageData->width();
1547         auto height = m_ImageData->height();
1548         QImage image(width, height, QImage::Format_ARGB32_Premultiplied);
1549         SkyPoint startPoint;
1550         SkyPoint endPoint;
1551         SkyPoint centerPoint;
1552 
1553         m_ImageData->pixelToWCS(QPointF(0, 0), startPoint);
1554         m_ImageData->pixelToWCS(QPointF(width - 1, height - 1), endPoint);
1555         m_ImageData->pixelToWCS(QPointF( (width - Options::hIPSOffsetX()) / 2.0, (height - Options::hIPSOffsetY()) / 2.0),
1556                                 centerPoint);
1557 
1558         startPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
1559         endPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
1560         centerPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
1561 
1562         auto fov_radius = startPoint.angularDistanceTo(&endPoint).Degrees() / 2;
1563         QVariant PA (0.0);
1564         m_ImageData->getRecordValue("CROTA1", PA);
1565 
1566         auto rotation = 180 - PA.toDouble();
1567         if (rotation > 360)
1568             rotation -= 360;
1569 
1570         if (HIPSFinder::Instance()->renderFOV(&centerPoint, fov_radius, rotation, &image) == false)
1571             return;
1572         m_HiPSOverlayPixmap = QPixmap::fromImage(image);
1573     }
1574 
1575     Q_UNUSED(scale);
1576     painter->setOpacity(Options::hIPSOpacity());
1577     painter->drawPixmap(0, 0, m_HiPSOverlayPixmap);
1578     painter->setOpacity(1);
1579 }
1580 #endif
1581 
1582 /**
1583 This method will paint EQ Gridlines in an overlay if there is WCS data present.
1584 It determines the minimum and maximum RA and DEC, then it uses that information to
1585 judge which gridLines to draw.  Then it calls the drawEQGridlines methods below
1586 to draw gridlines at those specific RA and Dec values.
1587  */
1588 
1589 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1590 void FITSView::drawEQGrid(QPainter * painter, double scale)
1591 {
1592     const int image_width = m_ImageData->width();
1593     const int image_height = m_ImageData->height();
1594 
1595     if (m_ImageData->hasWCS())
1596     {
1597         double maxRA  = -1000;
1598         double minRA  = 1000;
1599         double maxDec = -1000;
1600         double minDec = 1000;
1601         m_ImageData->findWCSBounds(minRA, maxRA, minDec, maxDec);
1602 
1603         auto minDecMinutes = (int)(minDec * 12); //This will force the Dec Scale to 5 arc minutes in the loop
1604         auto maxDecMinutes = (int)(maxDec * 12);
1605 
1606         auto minRAMinutes =
1607             (int)(minRA / 15.0 *
1608                   120.0); //This will force the scale to 1/2 minutes of RA in the loop from 0 to 50 degrees
1609         auto maxRAMinutes = (int)(maxRA / 15.0 * 120.0);
1610 
1611         double raConvert  = 15 / 120.0; //This will undo the calculation above to retrieve the actual RA.
1612         double decConvert = 1.0 / 12.0; //This will undo the calculation above to retrieve the actual DEC.
1613 
1614         if (maxDec > 50 || minDec < -50)
1615         {
1616             minRAMinutes =
1617                 (int)(minRA / 15.0 * 60.0); //This will force the scale to 1 min of RA from 50 to 80 degrees
1618             maxRAMinutes = (int)(maxRA / 15.0 * 60.0);
1619             raConvert    = 15 / 60.0;
1620         }
1621 
1622         if (maxDec > 80 || minDec < -80)
1623         {
1624             minRAMinutes =
1625                 (int)(minRA / 15.0 * 30); //This will force the scale to 2 min of RA from 80 to 85 degrees
1626             maxRAMinutes = (int)(maxRA / 15.0 * 30);
1627             raConvert    = 15 / 30.0;
1628         }
1629         if (maxDec > 85 || minDec < -85)
1630         {
1631             minRAMinutes =
1632                 (int)(minRA / 15.0 * 6); //This will force the scale to 10 min of RA from 85 to 89 degrees
1633             maxRAMinutes = (int)(maxRA / 15.0 * 6);
1634             raConvert    = 15 / 6.0;
1635         }
1636         if (maxDec >= 89.25 || minDec <= -89.25)
1637         {
1638             minRAMinutes =
1639                 (int)(minRA /
1640                       15); //This will force the scale to whole hours of RA in the loop really close to the poles
1641             maxRAMinutes = (int)(maxRA / 15);
1642             raConvert    = 15;
1643         }
1644 
1645         painter->setPen(QPen(Qt::yellow));
1646 
1647         QPointF pixelPoint, imagePoint, pPoint;
1648 
1649         //This section draws the RA Gridlines
1650 
1651         for (int targetRA = minRAMinutes; targetRA <= maxRAMinutes; targetRA++)
1652         {
1653             painter->setPen(QPen(Qt::yellow));
1654             double target = targetRA * raConvert;
1655 
1656             if (eqGridPoints.count() != 0)
1657                 eqGridPoints.clear();
1658 
1659             double increment = std::abs((maxDec - minDec) /
1660                                         100.0); //This will determine how many points to use to create the RA Line
1661 
1662             for (double targetDec = minDec; targetDec <= maxDec; targetDec += increment)
1663             {
1664                 SkyPoint pointToGet(target / 15.0, targetDec);
1665                 bool inImage = m_ImageData->wcsToPixel(pointToGet, pixelPoint, imagePoint);
1666                 if (inImage)
1667                 {
1668                     QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale);
1669                     eqGridPoints.append(pt);
1670                 }
1671             }
1672 
1673             if (eqGridPoints.count() > 1)
1674             {
1675                 for (int i = 1; i < eqGridPoints.count(); i++)
1676                     painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i));
1677                 QString str = QString::number(dms(target).hour()) + "h " +
1678                               QString::number(dms(target).minute()) + '\'';
1679                 if  (maxDec <= 50 && maxDec >= -50)
1680                     str = str + " " + QString::number(dms(target).second()) + "''";
1681                 QPointF pt = getPointForGridLabel(painter, str, scale);
1682                 if (pt.x() != -100)
1683                     painter->drawText(pt.x(), pt.y(), str);
1684             }
1685         }
1686 
1687         //This section draws the DEC Gridlines
1688 
1689         for (int targetDec = minDecMinutes; targetDec <= maxDecMinutes; targetDec++)
1690         {
1691             if (eqGridPoints.count() != 0)
1692                 eqGridPoints.clear();
1693 
1694             double increment = std::abs((maxRA - minRA) /
1695                                         100.0); //This will determine how many points to use to create the Dec Line
1696             double target    = targetDec * decConvert;
1697 
1698             for (double targetRA = minRA; targetRA <= maxRA; targetRA += increment)
1699             {
1700                 SkyPoint pointToGet(targetRA / 15, targetDec * decConvert);
1701                 bool inImage = m_ImageData->wcsToPixel(pointToGet, pixelPoint, imagePoint);
1702                 if (inImage)
1703                 {
1704                     QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale);
1705                     eqGridPoints.append(pt);
1706                 }
1707             }
1708             if (eqGridPoints.count() > 1)
1709             {
1710                 for (int i = 1; i < eqGridPoints.count(); i++)
1711                     painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i));
1712                 QString str = QString::number(dms(target).degree()) + "° " + QString::number(dms(target).arcmin()) + '\'';
1713                 QPointF pt = getPointForGridLabel(painter, str, scale);
1714                 if (pt.x() != -100)
1715                     painter->drawText(pt.x(), pt.y(), str);
1716             }
1717         }
1718 
1719         //This Section Draws the North Celestial Pole if present
1720         SkyPoint NCP(0, 90);
1721 
1722         bool NCPtest = m_ImageData->wcsToPixel(NCP, pPoint, imagePoint);
1723         if (NCPtest)
1724         {
1725             bool NCPinImage =
1726                 (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height);
1727             if (NCPinImage)
1728             {
1729                 painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4,
1730                                   KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"));
1731                 painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15,
1732                                   i18nc("North Celestial Pole", "NCP"));
1733             }
1734         }
1735 
1736         //This Section Draws the South Celestial Pole if present
1737         SkyPoint SCP(0, -90);
1738 
1739         bool SCPtest = m_ImageData->wcsToPixel(SCP, pPoint, imagePoint);
1740         if (SCPtest)
1741         {
1742             bool SCPinImage =
1743                 (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height);
1744             if (SCPinImage)
1745             {
1746                 painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4,
1747                                   KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"));
1748                 painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15,
1749                                   i18nc("South Celestial Pole", "SCP"));
1750             }
1751         }
1752     }
1753 }
1754 #endif
1755 
1756 bool FITSView::pointIsInImage(QPointF pt, double scale)
1757 {
1758     int image_width = m_ImageData->width();
1759     int image_height = m_ImageData->height();
1760     return pt.x() < image_width * scale && pt.y() < image_height * scale && pt.x() > 0 && pt.y() > 0;
1761 }
1762 
1763 QPointF FITSView::getPointForGridLabel(QPainter *painter, const QString &str, double scale)
1764 {
1765     QFontMetrics fm(painter->font());
1766 #if QT_VERSION < QT_VERSION_CHECK(5,11,0)
1767     int strWidth = fm.width(str);
1768 #else
1769     int strWidth = fm.horizontalAdvance(str);
1770 #endif
1771     int strHeight = fm.height();
1772     int image_width = m_ImageData->width();
1773     int image_height = m_ImageData->height();
1774 
1775     //These get the maximum X and Y points in the list that are in the image
1776     QPointF maxXPt(image_width * scale / 2, image_height * scale / 2);
1777     for (auto &p : eqGridPoints)
1778     {
1779         if (p.x() > maxXPt.x() && pointIsInImage(p, scale))
1780             maxXPt = p;
1781     }
1782     QPointF maxYPt(image_width * scale / 2, image_height * scale / 2);
1783 
1784     for (auto &p : eqGridPoints)
1785     {
1786         if (p.y() > maxYPt.y() && pointIsInImage(p, scale))
1787             maxYPt = p;
1788     }
1789     QPointF minXPt(image_width * scale / 2, image_height * scale / 2);
1790 
1791     for (auto &p : eqGridPoints)
1792     {
1793         if (p.x() < minXPt.x() && pointIsInImage(p, scale))
1794             minXPt = p;
1795     }
1796     QPointF minYPt(image_width * scale / 2, image_height * scale / 2);
1797 
1798     for (auto &p : eqGridPoints)
1799     {
1800         if (p.y() < minYPt.y() && pointIsInImage(p, scale))
1801             minYPt = p;
1802     }
1803 
1804     //This gives preference to points that are on the right hand side and bottom.
1805     //But if the line doesn't intersect the right or bottom, it then tries for the top and left.
1806     //If no points are found in the image, it returns a point off the screen
1807     //If all else fails, like in the case of a circle on the image, it returns the far right point.
1808 
1809     if (image_width * scale - maxXPt.x() < strWidth)
1810     {
1811         return QPointF(
1812                    image_width * scale - (strWidth + 10),
1813                    maxXPt.y() -
1814                    strHeight); //This will draw the text on the right hand side, up and to the left of the point where the line intersects
1815     }
1816     if (image_height * scale - maxYPt.y() < strHeight)
1817         return QPointF(
1818                    maxYPt.x() - (strWidth + 10),
1819                    image_height * scale -
1820                    (strHeight + 10)); //This will draw the text on the bottom side, up and to the left of the point where the line intersects
1821     if (minYPt.y() < strHeight)
1822         return QPointF(
1823                    minYPt.x() * scale + 10,
1824                    strHeight + 20); //This will draw the text on the top side, down and to the right of the point where the line intersects
1825     if (minXPt.x() < strWidth)
1826         return QPointF(
1827                    10,
1828                    minXPt.y() * scale +
1829                    strHeight +
1830                    20); //This will draw the text on the left hand side, down and to the right of the point where the line intersects
1831     if (maxXPt.x() == image_width * scale / 2 && maxXPt.y() == image_height * scale / 2)
1832         return QPointF(-100, -100); //All of the points were off the screen
1833 
1834     return QPoint(maxXPt.x() - (strWidth + 10), maxXPt.y() - (strHeight + 10));
1835 }
1836 
1837 void FITSView::setFirstLoad(bool value)
1838 {
1839     firstLoad = value;
1840 }
1841 
1842 QPixmap &FITSView::getTrackingBoxPixmap(uint8_t margin)
1843 {
1844     if (trackingBox.isNull())
1845         return trackingBoxPixmap;
1846 
1847     // We need to know which rendering strategy updateFrame used to determine the scaling.
1848     const float scale = getScale();
1849 
1850     int x1 = (trackingBox.x() - margin) * scale;
1851     int y1 = (trackingBox.y() - margin) * scale;
1852     int w  = (trackingBox.width() + margin * 2) * scale;
1853     int h  = (trackingBox.height() + margin * 2) * scale;
1854 
1855     trackingBoxPixmap = m_ImageFrame->grab(QRect(x1, y1, w, h));
1856     return trackingBoxPixmap;
1857 }
1858 
1859 void FITSView::setTrackingBox(const QRect &rect)
1860 {
1861     if (rect != trackingBox)
1862     {
1863         trackingBox        = rect;
1864         updateFrame();
1865         if(showStarProfile)
1866             viewStarProfile();
1867     }
1868 }
1869 
1870 void FITSView::resizeTrackingBox(int newSize)
1871 {
1872     int x = trackingBox.x() + trackingBox.width() / 2;
1873     int y = trackingBox.y() + trackingBox.height() / 2;
1874     int delta = newSize / 2;
1875     setTrackingBox(QRect( x - delta, y - delta, newSize, newSize));
1876 }
1877 
1878 void FITSView::processRectangleFixed(int s)
1879 {
1880     int w = m_ImageData->width();
1881     int h = m_ImageData->height();
1882 
1883     QPoint c = selectionRectangleRaw.center();
1884     c.setX(qMax((int)round(s / 2.0), c.x()));
1885     c.setX(qMin(w - (int)round(s / 2.0), c.x()));
1886     c.setY(qMax((int)round(s / 2.0), c.y()));
1887     c.setY(qMin(h - (int)round(s / 2.0), c.y()));
1888 
1889     QPoint topLeft, botRight;
1890     topLeft = QPoint(c.x() - round(s / 2.0), c.y() - round(s / 2.0));
1891     botRight = QPoint(c.x() + round(s / 2.0), c.y() + round(s / 2.0));
1892 
1893     emit setRubberBand(QRect(topLeft, botRight));
1894     processRectangle(topLeft, botRight, true);
1895 }
1896 
1897 void FITSView::processRectangle(QPoint p1, QPoint p2, bool calculate)
1898 {
1899     if(!isSelectionRectShown())
1900         return;
1901     //the user can draw a rectangle by dragging the mouse to any direction
1902     //but we need to feed Rectangle(topleft, topright)
1903     //hence we calculate topleft and topright for each case
1904 
1905     //p1 is the point where the user presses the mouse
1906     //p2 is the point where the user releases the mouse
1907     selectionRectangleRaw = QRect(p1, p2).normalized();
1908     //Index out of bounds Check for raw Rectangle, this effectively works when user does shift + drag, other wise becomes redundant
1909 
1910     QPoint topLeft = selectionRectangleRaw.topLeft();
1911     QPoint botRight = selectionRectangleRaw.bottomRight();
1912 
1913     topLeft.setX(qMax(1, topLeft.x()));
1914     topLeft.setY(qMax(1, topLeft.y()));
1915     botRight.setX(qMin((int)m_ImageData->width(), botRight.x()));
1916     botRight.setY(qMin((int)m_ImageData->height(), botRight.y()));
1917 
1918     selectionRectangleRaw.setTopLeft(topLeft);
1919     selectionRectangleRaw.setBottomRight(botRight);
1920 
1921     if(calculate)
1922     {
1923         if(m_ImageData)
1924         {
1925             m_ImageData->makeRoiBuffer(selectionRectangleRaw);
1926             emit rectangleUpdated(selectionRectangleRaw);
1927         }
1928     }
1929     //updateFrameRoi();
1930 
1931     //emit raw rectangle for calculation
1932     //update the stats pane after calculation; there should be ample time for calculation before showing the values
1933 }
1934 
1935 bool FITSView::isImageStretched()
1936 {
1937     return stretchImage;
1938 }
1939 
1940 bool FITSView::isClippingShown()
1941 {
1942     return showClipping;
1943 }
1944 
1945 bool FITSView::isCrosshairShown()
1946 {
1947     return showCrosshair;
1948 }
1949 
1950 bool FITSView::isEQGridShown()
1951 {
1952     return showEQGrid;
1953 }
1954 
1955 bool FITSView::isSelectionRectShown()
1956 {
1957     return showSelectionRect;
1958 }
1959 bool FITSView::areObjectsShown()
1960 {
1961     return showObjects;
1962 }
1963 
1964 bool FITSView::isPixelGridShown()
1965 {
1966     return showPixelGrid;
1967 }
1968 
1969 bool FITSView::isHiPSOverlayShown()
1970 {
1971     return showHiPSOverlay;
1972 }
1973 
1974 void FITSView::toggleCrosshair()
1975 {
1976     showCrosshair = !showCrosshair;
1977     updateFrame();
1978 }
1979 
1980 void FITSView::toggleClipping()
1981 {
1982     showClipping = !showClipping;
1983     updateFrame();
1984 }
1985 
1986 void FITSView::toggleEQGrid()
1987 {
1988     showEQGrid = !showEQGrid;
1989 
1990     if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
1991     {
1992         QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS);
1993         wcsWatcher.setFuture(future);
1994         return;
1995     }
1996 
1997     if (m_ImageFrame)
1998         updateFrame();
1999 }
2000 
2001 void FITSView::toggleHiPSOverlay()
2002 {
2003     showHiPSOverlay = !showHiPSOverlay;
2004 
2005     if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
2006     {
2007         QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS);
2008         wcsWatcher.setFuture(future);
2009         return;
2010     }
2011 
2012     if (m_ImageFrame)
2013     {
2014         m_QueueUpdate = true;
2015         updateFrame();
2016     }
2017 }
2018 
2019 void FITSView::toggleSelectionMode()
2020 {
2021     showSelectionRect = !showSelectionRect;
2022     if (!showSelectionRect)
2023         emit rectangleUpdated(QRect());
2024     else if (m_ImageData)
2025     {
2026         m_ImageData->makeRoiBuffer(selectionRectangleRaw);
2027         emit rectangleUpdated(selectionRectangleRaw);
2028     }
2029 
2030     emit showRubberBand(showSelectionRect);
2031     if (m_ImageFrame)
2032         updateFrame();
2033 
2034 }
2035 void FITSView::toggleObjects()
2036 {
2037     showObjects = !showObjects;
2038 
2039     if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
2040     {
2041         QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS);
2042         wcsWatcher.setFuture(future);
2043         return;
2044     }
2045 
2046     if (m_ImageFrame)
2047     {
2048 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2049         m_ImageData->searchObjects();
2050 #endif
2051         updateFrame();
2052     }
2053 }
2054 
2055 void FITSView::toggleStars()
2056 {
2057     toggleStars(!markStars);
2058     if (m_ImageFrame)
2059         updateFrame();
2060 }
2061 
2062 void FITSView::toggleStretch()
2063 {
2064     stretchImage = !stretchImage;
2065     if (m_ImageFrame && rescale(ZOOM_KEEP_LEVEL))
2066         updateFrame();
2067 }
2068 
2069 void FITSView::toggleStarProfile()
2070 {
2071 #ifdef HAVE_DATAVISUALIZATION
2072     showStarProfile = !showStarProfile;
2073     if(showStarProfile && trackingBoxEnabled)
2074         viewStarProfile();
2075     if(toggleProfileAction)
2076         toggleProfileAction->setChecked(showStarProfile);
2077 
2078     if(showStarProfile)
2079     {
2080         //The tracking box is already on for Guide and Focus Views, but off for Normal and Align views.
2081         //So for Normal and Align views, we need to set up the tracking box.
2082         if(mode == FITS_NORMAL || mode == FITS_ALIGN)
2083         {
2084             setCursorMode(selectCursor);
2085             connect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int)));
2086             trackingBox = QRect(0, 0, 128, 128);
2087             setTrackingBoxEnabled(true);
2088             if(starProfileWidget)
2089                 connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int)));
2090         }
2091         if(starProfileWidget)
2092             connect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile()));
2093     }
2094     else
2095     {
2096         //This shuts down the tracking box for Normal and Align Views
2097         //It doesn't touch Guide and Focus Views because they still need a tracking box
2098         if(mode == FITS_NORMAL || mode == FITS_ALIGN)
2099         {
2100             if(getCursorMode() == selectCursor)
2101                 setCursorMode(dragCursor);
2102             disconnect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int)));
2103             setTrackingBoxEnabled(false);
2104             if(starProfileWidget)
2105                 disconnect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int)));
2106         }
2107         if(starProfileWidget)
2108         {
2109             disconnect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile()));
2110             starProfileWidget->close();
2111             starProfileWidget = nullptr;
2112         }
2113         emit starProfileWindowClosed();
2114     }
2115     updateFrame();
2116 #endif
2117 }
2118 
2119 void FITSView::move3DTrackingBox(int x, int y)
2120 {
2121     int boxSize = trackingBox.width();
2122     QRect starRect = QRect(x - boxSize / 2, y - boxSize / 2, boxSize, boxSize);
2123     setTrackingBox(starRect);
2124 }
2125 
2126 void FITSView::viewStarProfile()
2127 {
2128 #ifdef HAVE_DATAVISUALIZATION
2129     if(!trackingBoxEnabled)
2130     {
2131         setTrackingBoxEnabled(true);
2132         setTrackingBox(QRect(0, 0, 128, 128));
2133     }
2134     if(!starProfileWidget)
2135     {
2136         starProfileWidget = new StarProfileViewer(this);
2137 
2138         //This is a band-aid to fix a QT bug with createWindowContainer
2139         //It will set the cursor of the Window containing the view that called the Star Profile method to the Arrow Cursor
2140         //Note that Ekos Manager is a QDialog and FitsViewer is a KXmlGuiWindow
2141         QWidget * superParent = this->parentWidget();
2142         while(superParent->parentWidget() != 0 && !superParent->inherits("QDialog") && !superParent->inherits("KXmlGuiWindow"))
2143             superParent = superParent->parentWidget();
2144         superParent->setCursor(Qt::ArrowCursor);
2145         //This is the end of the band-aid
2146 
2147         connect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile()));
2148         if(mode == FITS_ALIGN || mode == FITS_NORMAL)
2149         {
2150             starProfileWidget->enableTrackingBox(true);
2151             m_ImageData->setStarAlgorithm(ALGORITHM_CENTROID);
2152             connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int)));
2153         }
2154     }
2155     QList<Edge *> starCenters = m_ImageData->getStarCentersInSubFrame(trackingBox);
2156     if(starCenters.size() == 0)
2157     {
2158         // FIXME, the following does not work anymore.
2159         //m_ImageData->findStars(&trackingBox, true);
2160         // FIXME replacing it with this
2161         m_ImageData->findStars(ALGORITHM_CENTROID, trackingBox).waitForFinished();
2162         starCenters = m_ImageData->getStarCentersInSubFrame(trackingBox);
2163     }
2164 
2165     starProfileWidget->loadData(m_ImageData, trackingBox, starCenters);
2166     starProfileWidget->show();
2167     starProfileWidget->raise();
2168     if(markStars)
2169         updateFrame(); //this is to update for the marked stars
2170 
2171 #endif
2172 }
2173 
2174 void FITSView::togglePixelGrid()
2175 {
2176     showPixelGrid = !showPixelGrid;
2177     updateFrame();
2178 }
2179 
2180 QFuture<bool> FITSView::findStars(StarAlgorithm algorithm, const QRect &searchBox)
2181 {
2182     if(trackingBoxEnabled)
2183         return m_ImageData->findStars(algorithm, trackingBox);
2184     else
2185         return m_ImageData->findStars(algorithm, searchBox);
2186 }
2187 
2188 void FITSView::toggleStars(bool enable)
2189 {
2190     markStars = enable;
2191 
2192     if (markStars)
2193         searchStars();
2194 }
2195 
2196 void FITSView::searchStars()
2197 {
2198     QVariant frameType;
2199     if (m_ImageData->areStarsSearched() || !m_ImageData || (m_ImageData->getRecordValue("FRAME", frameType)
2200             && frameType.toString() != "Light"))
2201         return;
2202 
2203     QApplication::setOverrideCursor(Qt::WaitCursor);
2204     emit newStatus(i18n("Finding stars..."), FITS_MESSAGE);
2205     qApp->processEvents();
2206 
2207 #ifdef HAVE_STELLARSOLVER
2208     QVariantMap extractionSettings;
2209     extractionSettings["optionsProfileIndex"] = Options::hFROptionsProfile();
2210     extractionSettings["optionsProfileGroup"] = static_cast<int>(Ekos::HFRProfiles);
2211     imageData()->setSourceExtractorSettings(extractionSettings);
2212 #endif
2213 
2214     QFuture<bool> result = findStars(ALGORITHM_SEP);
2215     result.waitForFinished();
2216     if (result.result() && isVisible())
2217     {
2218         emit newStatus("", FITS_MESSAGE);
2219     }
2220     QApplication::restoreOverrideCursor();
2221 }
2222 
2223 void FITSView::processPointSelection(int x, int y)
2224 {
2225     emit trackingStarSelected(x, y);
2226 }
2227 
2228 void FITSView::processMarkerSelection(int x, int y)
2229 {
2230     markerCrosshair.setX(x);
2231     markerCrosshair.setY(y);
2232 
2233     updateFrame();
2234 }
2235 
2236 void FITSView::setTrackingBoxEnabled(bool enable)
2237 {
2238     if (enable != trackingBoxEnabled)
2239     {
2240         trackingBoxEnabled = enable;
2241         //updateFrame();
2242     }
2243 }
2244 
2245 void FITSView::wheelEvent(QWheelEvent * event)
2246 {
2247     //This attempts to send the wheel event back to the Scroll Area if it was taken from a trackpad
2248     //It should still do the zoom if it is a mouse wheel
2249     if (event->source() == Qt::MouseEventSynthesizedBySystem)
2250     {
2251         QScrollArea::wheelEvent(event);
2252     }
2253     else
2254     {
2255 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
2256         QPoint mouseCenter = getImagePoint(event->pos());
2257 #else
2258         QPoint mouseCenter = getImagePoint(event->position().toPoint());
2259 #endif
2260         if (event->angleDelta().y() > 0)
2261             ZoomIn();
2262         else
2263             ZoomOut();
2264         event->accept();
2265         cleanUpZoom(mouseCenter);
2266     }
2267     emit zoomRubberBand(getCurrentZoom() / ZOOM_DEFAULT);
2268 }
2269 
2270 /**
2271 This method is intended to keep key locations in an image centered on the screen while zooming.
2272 If there is a marker or tracking box, it centers on those.  If not, it uses the point called
2273 viewCenter that was passed as a parameter.
2274  */
2275 
2276 void FITSView::cleanUpZoom(QPoint viewCenter)
2277 {
2278     int x0       = 0;
2279     int y0       = 0;
2280     double scale = (currentZoom / ZOOM_DEFAULT);
2281     if (!markerCrosshair.isNull())
2282     {
2283         x0 = markerCrosshair.x() * scale;
2284         y0 = markerCrosshair.y() * scale;
2285     }
2286     else if (trackingBoxEnabled)
2287     {
2288         x0 = trackingBox.center().x() * scale;
2289         y0 = trackingBox.center().y() * scale;
2290     }
2291     else if (!viewCenter.isNull())
2292     {
2293         x0 = viewCenter.x() * scale;
2294         y0 = viewCenter.y() * scale;
2295     }
2296     if ((x0 != 0) || (y0 != 0))
2297         ensureVisible(x0, y0, width() / 2, height() / 2);
2298     updateMouseCursor();
2299 }
2300 
2301 /**
2302 This method converts a point from the ViewPort Coordinate System to the
2303 Image Coordinate System.
2304  */
2305 
2306 QPoint FITSView::getImagePoint(QPoint viewPortPoint)
2307 {
2308     QWidget * w = widget();
2309 
2310     if (w == nullptr)
2311         return QPoint(0, 0);
2312 
2313     double scale       = (currentZoom / ZOOM_DEFAULT);
2314     QPoint widgetPoint = w->mapFromParent(viewPortPoint);
2315     QPoint imagePoint  = QPoint(widgetPoint.x() / scale, widgetPoint.y() / scale);
2316     return imagePoint;
2317 }
2318 
2319 void FITSView::initDisplayImage()
2320 {
2321     // Account for leftover when sampling. Thus a 5-wide image sampled by 2
2322     // would result in a width of 3 (samples 0, 2 and 4).
2323     int w = (m_ImageData->width() + m_PreviewSampling - 1) / m_PreviewSampling;
2324     int h = (m_ImageData->height() + m_PreviewSampling - 1) / m_PreviewSampling;
2325 
2326     if (m_ImageData->channels() == 1)
2327     {
2328         rawImage = QImage(w, h, QImage::Format_Indexed8);
2329 
2330         rawImage.setColorCount(256);
2331         for (int i = 0; i < 256; i++)
2332             rawImage.setColor(i, qRgb(i, i, i));
2333     }
2334     else
2335     {
2336         rawImage = QImage(w, h, QImage::Format_RGB32);
2337     }
2338 }
2339 
2340 /**
2341 The Following two methods allow gestures to work with trackpads.
2342 Specifically, we are targeting the pinch events, so that if one is generated,
2343 Then the pinchTriggered method will be called.  If the event is not a pinch gesture,
2344 then the event is passed back to the other event handlers.
2345  */
2346 
2347 bool FITSView::event(QEvent * event)
2348 {
2349     if (event->type() == QEvent::Gesture)
2350         return gestureEvent(dynamic_cast<QGestureEvent *>(event));
2351     return QScrollArea::event(event);
2352 }
2353 
2354 bool FITSView::gestureEvent(QGestureEvent * event)
2355 {
2356     if (QGesture * pinch = event->gesture(Qt::PinchGesture))
2357         pinchTriggered(dynamic_cast<QPinchGesture *>(pinch));
2358     return true;
2359 }
2360 
2361 /**
2362 This Method works with Trackpads to use the pinch gesture to scroll in and out
2363 It stores a point to keep track of the location where the gesture started so that
2364 while you are zooming, it tries to keep that initial point centered in the view.
2365 **/
2366 void FITSView::pinchTriggered(QPinchGesture * gesture)
2367 {
2368     if (!zooming)
2369     {
2370         zoomLocation = getImagePoint(mapFromGlobal(QCursor::pos()));
2371         zooming      = true;
2372     }
2373     if (gesture->state() == Qt::GestureFinished)
2374     {
2375         zooming = false;
2376     }
2377     zoomTime++;           //zoomTime is meant to slow down the zooming with a pinch gesture.
2378     if (zoomTime > 10000) //This ensures zoomtime never gets too big.
2379         zoomTime = 0;
2380     if (zooming && (zoomTime % 10 == 0)) //zoomTime is set to slow it by a factor of 10.
2381     {
2382         if (gesture->totalScaleFactor() > 1)
2383             ZoomIn();
2384         else
2385             ZoomOut();
2386     }
2387     cleanUpZoom(zoomLocation);
2388 }
2389 
2390 /*void FITSView::handleWCSCompletion()
2391 {
2392     //bool hasWCS = wcsWatcher.result();
2393     if(m_ImageData->hasWCS())
2394         this->updateFrame();
2395     emit wcsToggled(m_ImageData->hasWCS());
2396 }*/
2397 
2398 void FITSView::syncWCSState()
2399 {
2400     bool hasWCS    = m_ImageData->hasWCS();
2401     bool wcsLoaded = m_ImageData->getWCSState() == FITSData::Success;
2402 
2403 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2404     if (showObjects)
2405         m_ImageData->searchObjects();
2406 #endif
2407 
2408     if (hasWCS && wcsLoaded)
2409         this->updateFrame();
2410 
2411     emit wcsToggled(hasWCS);
2412 
2413     if (toggleEQGridAction != nullptr)
2414         toggleEQGridAction->setEnabled(hasWCS);
2415     if (toggleObjectsAction != nullptr)
2416         toggleObjectsAction->setEnabled(hasWCS);
2417     if (centerTelescopeAction != nullptr)
2418         centerTelescopeAction->setEnabled(hasWCS);
2419     if (toggleHiPSOverlayAction != nullptr)
2420         toggleHiPSOverlayAction->setEnabled(hasWCS);
2421 }
2422 
2423 void FITSView::createFloatingToolBar()
2424 {
2425     if (floatingToolBar != nullptr)
2426         return;
2427 
2428     floatingToolBar             = new QToolBar(this);
2429     auto * eff = new QGraphicsOpacityEffect(this);
2430     floatingToolBar->setGraphicsEffect(eff);
2431     eff->setOpacity(0.2);
2432     floatingToolBar->setVisible(false);
2433     floatingToolBar->setStyleSheet(
2434         "QToolBar{background: rgba(150, 150, 150, 210); border:none; color: yellow}"
2435         "QToolButton{background: transparent; border:none; color: yellow}"
2436         "QToolButton:hover{background: rgba(200, 200, 200, 255);border:solid; color: yellow}"
2437         "QToolButton:checked{background: rgba(110, 110, 110, 255);border:solid; color: yellow}");
2438     floatingToolBar->setFloatable(true);
2439     floatingToolBar->setIconSize(QSize(25, 25));
2440     //floatingToolBar->setMovable(true);
2441 
2442     QAction * action = nullptr;
2443 
2444     floatingToolBar->addAction(QIcon::fromTheme("zoom-in"),
2445                                i18n("Zoom In"), this, SLOT(ZoomIn()));
2446 
2447     floatingToolBar->addAction(QIcon::fromTheme("zoom-out"),
2448                                i18n("Zoom Out"), this, SLOT(ZoomOut()));
2449 
2450     floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-best"),
2451                                i18n("Default Zoom"), this, SLOT(ZoomDefault()));
2452 
2453     floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-width"),
2454                                i18n("Zoom to Fit"), this, SLOT(ZoomToFit()));
2455 
2456     toggleStretchAction = floatingToolBar->addAction(QIcon::fromTheme("transform-move"),
2457                           i18n("Toggle Stretch"),
2458                           this, SLOT(toggleStretch()));
2459     toggleStretchAction->setCheckable(true);
2460 
2461 
2462     floatingToolBar->addSeparator();
2463 
2464     action = floatingToolBar->addAction(QIcon::fromTheme("crosshairs"),
2465                                         i18n("Show Cross Hairs"), this, SLOT(toggleCrosshair()));
2466     action->setCheckable(true);
2467 
2468     action = floatingToolBar->addAction(QIcon::fromTheme("map-flat"),
2469                                         i18n("Show Pixel Gridlines"), this, SLOT(togglePixelGrid()));
2470     action->setCheckable(true);
2471 
2472     toggleStarsAction =
2473         floatingToolBar->addAction(QIcon::fromTheme("kstars_stars"),
2474                                    i18n("Detect Stars in Image"), this, SLOT(toggleStars()));
2475     toggleStarsAction->setCheckable(true);
2476 
2477 #ifdef HAVE_DATAVISUALIZATION
2478     toggleProfileAction =
2479         floatingToolBar->addAction(QIcon::fromTheme("star-profile", QIcon(":/icons/star_profile.svg")),
2480                                    i18n("View Star Profile..."), this, SLOT(toggleStarProfile()));
2481     toggleProfileAction->setCheckable(true);
2482 #endif
2483 
2484     if (mode == FITS_NORMAL || mode == FITS_ALIGN)
2485     {
2486         floatingToolBar->addSeparator();
2487 
2488         toggleEQGridAction =
2489             floatingToolBar->addAction(QIcon::fromTheme("kstars_grid"),
2490                                        i18n("Show Equatorial Gridlines"), this, &FITSView::toggleEQGrid);
2491         toggleEQGridAction->setCheckable(true);
2492         toggleEQGridAction->setEnabled(false);
2493 
2494         toggleObjectsAction =
2495             floatingToolBar->addAction(QIcon::fromTheme("help-hint"),
2496                                        i18n("Show Objects in Image"), this, &FITSView::toggleObjects);
2497         toggleObjectsAction->setCheckable(true);
2498         toggleObjectsAction->setEnabled(false);
2499 
2500         centerTelescopeAction =
2501             floatingToolBar->addAction(QIcon::fromTheme("center_telescope", QIcon(":/icons/center_telescope.svg")),
2502                                        i18n("Center Telescope"), this, &FITSView::centerTelescope);
2503         centerTelescopeAction->setCheckable(true);
2504         centerTelescopeAction->setEnabled(false);
2505 
2506         toggleHiPSOverlayAction =
2507             floatingToolBar->addAction(QIcon::fromTheme("pixelate"),
2508                                        i18n("Show HiPS Overlay"), this, &FITSView::toggleHiPSOverlay);
2509         toggleHiPSOverlayAction->setCheckable(true);
2510         toggleHiPSOverlayAction->setEnabled(false);
2511     }
2512 }
2513 
2514 /**
2515  This method either enables or disables the scope mouse mode so you can slew your scope to coordinates
2516  just by clicking the mouse on a spot in the image.
2517  */
2518 
2519 void FITSView::centerTelescope()
2520 {
2521     if (imageHasWCS())
2522     {
2523         if (getCursorMode() == FITSView::scopeCursor)
2524         {
2525             setCursorMode(lastMouseMode);
2526         }
2527         else
2528         {
2529             lastMouseMode = getCursorMode();
2530             setCursorMode(FITSView::scopeCursor);
2531         }
2532         updateFrame();
2533     }
2534     updateScopeButton();
2535 }
2536 
2537 void FITSView::updateScopeButton()
2538 {
2539     if (centerTelescopeAction != nullptr)
2540     {
2541         if (getCursorMode() == FITSView::scopeCursor)
2542         {
2543             centerTelescopeAction->setChecked(true);
2544         }
2545         else
2546         {
2547             centerTelescopeAction->setChecked(false);
2548         }
2549     }
2550 }
2551 
2552 /**
2553 This method just verifies if INDI is online, a telescope present, and is connected
2554  */
2555 
2556 bool FITSView::isTelescopeActive()
2557 {
2558 #ifdef HAVE_INDI
2559     if (INDIListener::Instance()->size() == 0)
2560     {
2561         return false;
2562     }
2563 
2564     for (auto &oneDevice : INDIListener::devices())
2565     {
2566         if (!(oneDevice->getDriverInterface() & INDI::BaseDevice::TELESCOPE_INTERFACE))
2567             continue;
2568         return oneDevice->isConnected();
2569     }
2570     return false;
2571 #else
2572     return false;
2573 #endif
2574 }
2575 
2576 void FITSView::setStarsEnabled(bool enable)
2577 {
2578     markStars = enable;
2579     if (floatingToolBar != nullptr)
2580     {
2581         foreach (QAction * action, floatingToolBar->actions())
2582         {
2583             if (action->text() == i18n("Detect Stars in Image"))
2584             {
2585                 action->setChecked(markStars);
2586                 break;
2587             }
2588         }
2589     }
2590 }
2591 
2592 void FITSView::setStarsHFREnabled(bool enable)
2593 {
2594     showStarsHFR = enable;
2595 }