1 /*
2     SPDX-FileCopyrightText: 2003-2017 Jasem Mutlaq <mutlaqja@ikarustech.com>
3     SPDX-FileCopyrightText: 2016-2017 Robert Lancaster <rlancaste@gmail.com>
4 
5     SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 
8 #include "config-kstars.h"
9 #include "fitsview.h"
10 
11 #include "fitsdata.h"
12 #include "fitslabel.h"
13 #include "kspopupmenu.h"
14 #include "kstarsdata.h"
15 #include "ksutils.h"
16 #include "Options.h"
17 #include "skymap.h"
18 #include "fits_debug.h"
19 #include "stretch.h"
20 
21 #ifdef HAVE_STELLARSOLVER
22 #include "ekos/auxiliary/stellarsolverprofileeditor.h"
23 #endif
24 
25 #ifdef HAVE_INDI
26 #include "basedevice.h"
27 #include "indi/indilistener.h"
28 #endif
29 
30 #include <KActionCollection>
31 
32 #include <QtConcurrent>
33 #include <QScrollBar>
34 #include <QToolBar>
35 #include <QGraphicsOpacityEffect>
36 #include <QApplication>
37 #include <QImageReader>
38 #include <QGestureEvent>
39 
40 #include <unistd.h>
41 
42 #define BASE_OFFSET    50
43 #define ZOOM_DEFAULT   100.0f
44 #define ZOOM_MIN       10
45 // ZOOM_MAX is adjusted in the constructor if the amount of physical memory is known.
46 #define ZOOM_MAX       300
47 #define ZOOM_LOW_INCR  10
48 #define ZOOM_HIGH_INCR 50
49 #define FONT_SIZE      14
50 
51 namespace
52 {
53 
54 // Derive the Green and Blue stretch parameters from their previous values and the
55 // changes made to the Red parameters. We apply the same offsets used for Red to the
56 // other channels' parameters, but clip them.
ComputeGBStretchParams(const StretchParams & newParams,StretchParams * params)57 void ComputeGBStretchParams(const StretchParams &newParams, StretchParams* params)
58 {
59     float shadow_diff = newParams.grey_red.shadows - params->grey_red.shadows;
60     float highlight_diff = newParams.grey_red.highlights - params->grey_red.highlights;
61     float midtones_diff = newParams.grey_red.midtones - params->grey_red.midtones;
62 
63     params->green.shadows = params->green.shadows + shadow_diff;
64     params->green.shadows = KSUtils::clamp(params->green.shadows, 0.0f, 1.0f);
65     params->green.highlights = params->green.highlights + highlight_diff;
66     params->green.highlights = KSUtils::clamp(params->green.highlights, 0.0f, 1.0f);
67     params->green.midtones = params->green.midtones + midtones_diff;
68     params->green.midtones = std::max(params->green.midtones, 0.0f);
69 
70     params->blue.shadows = params->blue.shadows + shadow_diff;
71     params->blue.shadows = KSUtils::clamp(params->blue.shadows, 0.0f, 1.0f);
72     params->blue.highlights = params->blue.highlights + highlight_diff;
73     params->blue.highlights = KSUtils::clamp(params->blue.highlights, 0.0f, 1.0f);
74     params->blue.midtones = params->blue.midtones + midtones_diff;
75     params->blue.midtones = std::max(params->blue.midtones, 0.0f);
76 }
77 
78 }  // namespace
79 
80 // Runs the stretch checking the variables to see which parameters to use.
81 // We call stretch even if we're not stretching, as the stretch code still
82 // converts the image to the uint8 output image which will be displayed.
83 // In that case, it will use an identity stretch.
doStretch(QImage * outputImage)84 void FITSView::doStretch(QImage *outputImage)
85 {
86     if (outputImage->isNull() || m_ImageData.isNull())
87         return;
88     Stretch stretch(static_cast<int>(m_ImageData->width()),
89                     static_cast<int>(m_ImageData->height()),
90                     m_ImageData->channels(), m_ImageData->dataType());
91 
92     StretchParams tempParams;
93     if (!stretchImage)
94         tempParams = StretchParams();  // Keeping it linear
95     else if (autoStretch)
96     {
97         // Compute new auto-stretch params.
98         stretchParams = stretch.computeParams(m_ImageData->getImageBuffer());
99         tempParams = stretchParams;
100     }
101     else
102         // Use the existing stretch params.
103         tempParams = stretchParams;
104 
105     stretch.setParams(tempParams);
106     stretch.run(m_ImageData->getImageBuffer(), outputImage, m_PreviewSampling);
107 }
108 
109 // Store stretch parameters, and turn on stretching if it isn't already on.
setStretchParams(const StretchParams & params)110 void FITSView::setStretchParams(const StretchParams &params)
111 {
112     if (m_ImageData->channels() == 3)
113         ComputeGBStretchParams(params, &stretchParams);
114 
115     stretchParams.grey_red = params.grey_red;
116     stretchParams.grey_red.shadows = std::max(stretchParams.grey_red.shadows, 0.0f);
117     stretchParams.grey_red.highlights = std::max(stretchParams.grey_red.highlights, 0.0f);
118     stretchParams.grey_red.midtones = std::max(stretchParams.grey_red.midtones, 0.0f);
119 
120     autoStretch = false;
121     stretchImage = true;
122 
123     if (m_ImageFrame && rescale(ZOOM_KEEP_LEVEL))
124         updateFrame(true);
125 }
126 
127 // Turn on or off stretching, and if on, use whatever parameters are currently stored.
setStretch(bool onOff)128 void FITSView::setStretch(bool onOff)
129 {
130     if (stretchImage != onOff)
131     {
132         stretchImage = onOff;
133         if (m_ImageFrame && rescale(ZOOM_KEEP_LEVEL))
134             updateFrame(true);
135     }
136 }
137 
138 // Turn on stretching, using automatically generated parameters.
setAutoStretchParams()139 void FITSView::setAutoStretchParams()
140 {
141     stretchImage = true;
142     autoStretch = true;
143     if (m_ImageFrame && rescale(ZOOM_KEEP_LEVEL))
144         updateFrame(true);
145 }
146 
FITSView(QWidget * parent,FITSMode fitsMode,FITSScale filterType)147 FITSView::FITSView(QWidget * parent, FITSMode fitsMode, FITSScale filterType) : QScrollArea(parent), m_ZoomFactor(1.2)
148 {
149     // stretchImage is whether to stretch or not--the stretch may or may not use automatically generated parameters.
150     // The user may enter his/her own.
151     stretchImage = Options::autoStretch();
152     // autoStretch means use automatically-generated parameters. This is the default, unless the user overrides
153     // by adjusting the stretchBar's sliders.
154     autoStretch = true;
155 
156     // Adjust the maximum zoom according to the amount of memory.
157     // There have been issues with users running out system memory because of zoom memory.
158     // Note: this is not currently image dependent. It's possible, but not implemented,
159     // to allow for more zooming on smaller images.
160     zoomMax = ZOOM_MAX;
161 
162 #if defined (Q_OS_LINUX) || defined (Q_OS_OSX)
163     const long numPages = sysconf(_SC_PAGESIZE);
164     const long pageSize = sysconf(_SC_PHYS_PAGES);
165 
166     // _SC_PHYS_PAGES "may not be standard" http://man7.org/linux/man-pages/man3/sysconf.3.html
167     // If an OS doesn't support it, sysconf should return -1.
168     if (numPages > 0 && pageSize > 0)
169     {
170         // (numPages * pageSize) will likely overflow a 32bit int, so use floating point calculations.
171         const int memoryMb = numPages * (static_cast<double>(pageSize) / 1e6);
172         if (memoryMb < 2000)
173             zoomMax = 100;
174         else if (memoryMb < 4000)
175             zoomMax = 200;
176         else if (memoryMb < 8000)
177             zoomMax = 300;
178         else if (memoryMb < 16000)
179             zoomMax = 400;
180         else
181             zoomMax = 600;
182     }
183 #endif
184 
185     grabGesture(Qt::PinchGesture);
186 
187     filter = filterType;
188     mode   = fitsMode;
189 
190     setBackgroundRole(QPalette::Dark);
191 
192     markerCrosshair.setX(0);
193     markerCrosshair.setY(0);
194 
195     setBaseSize(740, 530);
196 
197     m_ImageFrame = new FITSLabel(this);
198     m_ImageFrame->setMouseTracking(true);
199     connect(m_ImageFrame, SIGNAL(newStatus(QString, FITSBar)), this, SIGNAL(newStatus(QString, FITSBar)));
200     connect(m_ImageFrame, SIGNAL(pointSelected(int, int)), this, SLOT(processPointSelection(int, int)));
201     connect(m_ImageFrame, SIGNAL(markerSelected(int, int)), this, SLOT(processMarkerSelection(int, int)));
202 
203     connect(&wcsWatcher, SIGNAL(finished()), this, SLOT(syncWCSState()));
204 
205     m_UpdateFrameTimer.setInterval(50);
206     m_UpdateFrameTimer.setSingleShot(true);
207     connect(&m_UpdateFrameTimer, &QTimer::timeout, [this]()
208     {
209         if (toggleStretchAction)
210             toggleStretchAction->setChecked(stretchImage);
211 
212         // We employ two schemes for managing the image and its overlays, depending on the size of the image
213         // and whether we need to therefore conserve memory. The small-image strategy explicitly scales up
214         // the image, and writes overlays on the scaled pixmap. The large-image strategy uses a pixmap that's
215         // the size of the image itself, never scaling that up.
216         if (isLargeImage())
217             updateFrameLargeImage();
218         else
219             updateFrameSmallImage();
220     });
221 
222     connect(&fitsWatcher, &QFutureWatcher<bool>::finished, this, &FITSView::loadInFrame);
223 
224     setCursorMode(
225         selectCursor); //This is the default mode because the Focus and Align FitsViews should not be in dragMouse mode
226 
227     noImageLabel = new QLabel();
228     noImage.load(":/images/noimage.png");
229     noImageLabel->setPixmap(noImage);
230     noImageLabel->setAlignment(Qt::AlignCenter);
231     setWidget(noImageLabel);
232 
233     redScopePixmap = QPixmap(":/icons/center_telescope_red.svg").scaled(32, 32, Qt::KeepAspectRatio, Qt::FastTransformation);
234     magentaScopePixmap = QPixmap(":/icons/center_telescope_magenta.svg").scaled(32, 32, Qt::KeepAspectRatio,
235                          Qt::FastTransformation);
236 }
237 
~FITSView()238 FITSView::~FITSView()
239 {
240     fitsWatcher.waitForFinished();
241     wcsWatcher.waitForFinished();
242 }
243 
244 /**
245 This method looks at what mouse mode is currently selected and updates the cursor to match.
246  */
247 
updateMouseCursor()248 void FITSView::updateMouseCursor()
249 {
250     if (cursorMode == dragCursor)
251     {
252         if (horizontalScrollBar()->maximum() > 0 || verticalScrollBar()->maximum() > 0)
253         {
254             if (!m_ImageFrame->getMouseButtonDown())
255                 viewport()->setCursor(Qt::PointingHandCursor);
256             else
257                 viewport()->setCursor(Qt::ClosedHandCursor);
258         }
259         else
260             viewport()->setCursor(Qt::CrossCursor);
261     }
262     else if (cursorMode == selectCursor)
263     {
264         viewport()->setCursor(Qt::CrossCursor);
265     }
266     else if (cursorMode == scopeCursor)
267     {
268         viewport()->setCursor(QCursor(redScopePixmap, 10, 10));
269     }
270     else if (cursorMode == crosshairCursor)
271     {
272         viewport()->setCursor(QCursor(magentaScopePixmap, 10, 10));
273     }
274 }
275 
276 /**
277 This is how the mouse mode gets set.
278 The default for a FITSView in a FITSViewer should be the dragMouse
279 The default for a FITSView in the Focus or Align module should be the selectMouse
280 The different defaults are accomplished by putting making the actual default mouseMode
281 the selectMouse, but when a FITSViewer loads an image, it immediately makes it the dragMouse.
282  */
283 
setCursorMode(CursorMode mode)284 void FITSView::setCursorMode(CursorMode mode)
285 {
286     cursorMode = mode;
287     updateMouseCursor();
288 
289     if (mode == scopeCursor && imageHasWCS())
290     {
291         if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
292         {
293             QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS, true);
294             wcsWatcher.setFuture(future);
295         }
296     }
297 }
298 
resizeEvent(QResizeEvent * event)299 void FITSView::resizeEvent(QResizeEvent * event)
300 {
301     if (m_ImageData == nullptr && noImageLabel != nullptr)
302     {
303         noImageLabel->setPixmap(
304             noImage.scaled(width() - 20, height() - 20, Qt::KeepAspectRatio, Qt::FastTransformation));
305         noImageLabel->setFixedSize(width() - 5, height() - 5);
306     }
307 
308     QScrollArea::resizeEvent(event);
309 }
310 
311 
loadFile(const QString & inFilename,bool silent)312 void FITSView::loadFile(const QString &inFilename, bool silent)
313 {
314     if (floatingToolBar != nullptr)
315     {
316         floatingToolBar->setVisible(true);
317     }
318 
319     bool setBayerParams = false;
320 
321     BayerParams param;
322     if ((m_ImageData != nullptr) && m_ImageData->hasDebayer())
323     {
324         setBayerParams = true;
325         m_ImageData->getBayerParams(&param);
326     }
327 
328     // In case image is still loading, wait until it is done.
329     fitsWatcher.waitForFinished();
330     // In case loadWCS is still running for previous image data, let's wait until it's over
331     wcsWatcher.waitForFinished();
332 
333     //    delete m_ImageData;
334     //    m_ImageData = nullptr;
335 
336     filterStack.clear();
337     filterStack.push(FITS_NONE);
338     if (filter != FITS_NONE)
339         filterStack.push(filter);
340 
341     m_ImageData.reset(new FITSData(mode), &QObject::deleteLater);
342 
343     if (setBayerParams)
344         m_ImageData->setBayerParams(&param);
345 
346     fitsWatcher.setFuture(m_ImageData->loadFromFile(inFilename, silent));
347 }
348 
clearData()349 void FITSView::clearData()
350 {
351     if (!noImageLabel)
352     {
353         noImageLabel = new QLabel();
354         noImage.load(":/images/noimage.png");
355         noImageLabel->setPixmap(noImage);
356         noImageLabel->setAlignment(Qt::AlignCenter);
357     }
358 
359     setWidget(noImageLabel);
360 
361     m_ImageData.clear();
362 }
363 
loadData(const QSharedPointer<FITSData> & data)364 bool FITSView::loadData(const QSharedPointer<FITSData> &data)
365 {
366     if (floatingToolBar != nullptr)
367     {
368         floatingToolBar->setVisible(true);
369     }
370 
371     // In case loadWCS is still running for previous image data, let's wait until it's over
372     wcsWatcher.waitForFinished();
373 
374     filterStack.clear();
375     filterStack.push(FITS_NONE);
376     if (filter != FITS_NONE)
377         filterStack.push(filter);
378 
379     // Takes control of the objects passed in.
380     m_ImageData = data;
381 
382     return processData();
383 }
384 
processData()385 bool FITSView::processData()
386 {
387     // Set current width and height
388     if (!m_ImageData)
389         return false;
390 
391     connect(m_ImageData.data(), &FITSData::dataChanged, [this]()
392     {
393         rescale(ZOOM_KEEP_LEVEL);
394         updateFrame();
395     });
396 
397     currentWidth = m_ImageData->width();
398     currentHeight = m_ImageData->height();
399 
400     int image_width  = currentWidth;
401     int image_height = currentHeight;
402 
403     if (!m_ImageFrame)
404     {
405         m_ImageFrame = new FITSLabel(this);
406         m_ImageFrame->setMouseTracking(true);
407         connect(m_ImageFrame, SIGNAL(newStatus(QString, FITSBar)), this, SIGNAL(newStatus(QString, FITSBar)));
408         connect(m_ImageFrame, SIGNAL(pointSelected(int, int)), this, SLOT(processPointSelection(int, int)));
409         connect(m_ImageFrame, SIGNAL(markerSelected(int, int)), this, SLOT(processMarkerSelection(int, int)));
410     }
411     m_ImageFrame->setSize(image_width, image_height);
412 
413     // Init the display image
414     // JM 2020.01.08: Disabling as proposed by Hy
415     //initDisplayImage();
416 
417     m_ImageData->applyFilter(filter);
418 
419     double availableRAM = 0;
420     if (Options::adaptiveSampling() && (availableRAM = KSUtils::getAvailableRAM()) > 0)
421     {
422         // Possible color maximum image size
423         double max_size = image_width * image_height * 4;
424         // Ratio of image size to available RAM size
425         double ratio = max_size / availableRAM;
426 
427         // Increase adaptive sampling with more limited RAM
428         if (ratio < 0.1)
429             m_AdaptiveSampling = 1;
430         else if (ratio < 0.2)
431             m_AdaptiveSampling = 2;
432         else
433             m_AdaptiveSampling = 4;
434 
435         m_PreviewSampling = m_AdaptiveSampling;
436     }
437 
438     // Rescale to fits window on first load
439     if (firstLoad)
440     {
441         currentZoom = 100;
442 
443         if (rescale(ZOOM_FIT_WINDOW) == false)
444         {
445             m_LastError = i18n("Rescaling image failed.");
446             return false;
447         }
448 
449         firstLoad = false;
450     }
451     else
452     {
453         if (rescale(ZOOM_KEEP_LEVEL) == false)
454         {
455             m_LastError = i18n("Rescaling image failed.");
456             return false;
457         }
458     }
459 
460     setAlignment(Qt::AlignCenter);
461 
462     // Load WCS data now if selected and image contains valid WCS header
463     if ((mode == FITS_NORMAL || mode == FITS_ALIGN) &&
464             m_ImageData->hasWCS() && m_ImageData->getWCSState() == FITSData::Idle &&
465             Options::autoWCS() &&
466             !wcsWatcher.isRunning())
467     {
468         QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS, true);
469         wcsWatcher.setFuture(future);
470     }
471     else
472         syncWCSState();
473 
474     if (isVisible())
475         emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION);
476 
477     if (showStarProfile)
478     {
479         if(floatingToolBar != nullptr)
480             toggleProfileAction->setChecked(true);
481         //Need to wait till the Focus module finds stars, if its the Focus module.
482         QTimer::singleShot(100, this, SLOT(viewStarProfile()));
483     }
484 
485     // Fore immediate load of frame for first load.
486     updateFrame(true);
487     return true;
488 }
489 
loadInFrame()490 void FITSView::loadInFrame()
491 {
492     // Check if the loading was OK
493     if (fitsWatcher.result() == false)
494     {
495         m_LastError = m_ImageData->getLastError();
496         emit failed();
497         return;
498     }
499 
500     // Notify if there is debayer data.
501     emit debayerToggled(m_ImageData->hasDebayer());
502 
503     if (processData())
504         emit loaded();
505     else
506         emit failed();
507 }
508 
saveImage(const QString & newFilename)509 bool FITSView::saveImage(const QString &newFilename)
510 {
511     const QString ext = QFileInfo(newFilename).suffix();
512     if (QImageReader::supportedImageFormats().contains(ext.toLatin1()))
513     {
514         rawImage.save(newFilename, ext.toLatin1().constData());
515         return true;
516     }
517 
518     return m_ImageData->saveImage(newFilename);
519 }
520 
getCursorMode()521 FITSView::CursorMode FITSView::getCursorMode()
522 {
523     return cursorMode;
524 }
525 
enterEvent(QEvent * event)526 void FITSView::enterEvent(QEvent * event)
527 {
528     Q_UNUSED(event)
529 
530     if (floatingToolBar && m_ImageData)
531     {
532         QPointer<QGraphicsOpacityEffect> eff = new QGraphicsOpacityEffect(this);
533         floatingToolBar->setGraphicsEffect(eff);
534         QPointer<QPropertyAnimation> a = new QPropertyAnimation(eff, "opacity");
535         a->setDuration(500);
536         a->setStartValue(0.2);
537         a->setEndValue(1);
538         a->setEasingCurve(QEasingCurve::InBack);
539         a->start(QPropertyAnimation::DeleteWhenStopped);
540     }
541 }
542 
leaveEvent(QEvent * event)543 void FITSView::leaveEvent(QEvent * event)
544 {
545     Q_UNUSED(event)
546 
547     if (floatingToolBar && m_ImageData)
548     {
549         QPointer<QGraphicsOpacityEffect> eff = new QGraphicsOpacityEffect(this);
550         floatingToolBar->setGraphicsEffect(eff);
551         QPointer<QPropertyAnimation> a = new QPropertyAnimation(eff, "opacity");
552         a->setDuration(500);
553         a->setStartValue(1);
554         a->setEndValue(0.2);
555         a->setEasingCurve(QEasingCurve::OutBack);
556         a->start(QPropertyAnimation::DeleteWhenStopped);
557     }
558 }
559 
rescale(FITSZoom type)560 bool FITSView::rescale(FITSZoom type)
561 {
562     if (!m_ImageData)
563         return false;
564 
565     int image_width  = m_ImageData->width();
566     int image_height = m_ImageData->height();
567     currentWidth  = image_width;
568     currentHeight = image_height;
569 
570     if (isVisible())
571         emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION);
572 
573     switch (type)
574     {
575         case ZOOM_FIT_WINDOW:
576             if ((image_width > width() || image_height > height()))
577             {
578                 double w = baseSize().width() - BASE_OFFSET;
579                 double h = baseSize().height() - BASE_OFFSET;
580 
581                 if (!firstLoad)
582                 {
583                     w = viewport()->rect().width() - BASE_OFFSET;
584                     h = viewport()->rect().height() - BASE_OFFSET;
585                 }
586 
587                 // Find the zoom level which will enclose the current FITS in the current window size
588                 double zoomX                  = floor((w / static_cast<double>(currentWidth)) * 100.);
589                 double zoomY                  = floor((h / static_cast<double>(currentHeight)) * 100.);
590                 (zoomX < zoomY) ? currentZoom = zoomX : currentZoom = zoomY;
591 
592                 currentWidth  = image_width * (currentZoom / ZOOM_DEFAULT);
593                 currentHeight = image_height * (currentZoom / ZOOM_DEFAULT);
594 
595                 if (currentZoom <= ZOOM_MIN)
596                     emit actionUpdated("view_zoom_out", false);
597             }
598             else
599             {
600                 currentZoom   = 100;
601                 currentWidth  = image_width;
602                 currentHeight = image_height;
603             }
604             break;
605 
606         case ZOOM_KEEP_LEVEL:
607         {
608             currentWidth  = image_width * (currentZoom / ZOOM_DEFAULT);
609             currentHeight = image_height * (currentZoom / ZOOM_DEFAULT);
610         }
611         break;
612 
613         default:
614             currentZoom = 100;
615 
616             break;
617     }
618 
619     initDisplayImage();
620     m_ImageFrame->setScaledContents(true);
621     doStretch(&rawImage);
622     setWidget(m_ImageFrame);
623 
624     // This is needed by fitstab, even if the zoom doesn't change, to change the stretch UI.
625     emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM);
626     return true;
627 }
628 
ZoomIn()629 void FITSView::ZoomIn()
630 {
631     if (!m_ImageData)
632         return;
633 
634     if (currentZoom >= ZOOM_DEFAULT && Options::limitedResourcesMode())
635     {
636         emit newStatus(i18n("Cannot zoom in further due to active limited resources mode."), FITS_MESSAGE);
637         return;
638     }
639 
640     if (currentZoom < ZOOM_DEFAULT)
641         currentZoom += ZOOM_LOW_INCR;
642     else
643         currentZoom += ZOOM_HIGH_INCR;
644 
645     emit actionUpdated("view_zoom_out", true);
646     if (currentZoom >= zoomMax)
647     {
648         currentZoom = zoomMax;
649         emit actionUpdated("view_zoom_in", false);
650     }
651 
652     currentWidth  = m_ImageData->width() * (currentZoom / ZOOM_DEFAULT);
653     currentHeight = m_ImageData->height() * (currentZoom / ZOOM_DEFAULT);
654 
655     cleanUpZoom();
656 
657     updateFrame(true);
658 
659     emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM);
660 }
661 
ZoomOut()662 void FITSView::ZoomOut()
663 {
664     if (!m_ImageData)
665         return;
666 
667     if (currentZoom <= ZOOM_DEFAULT)
668         currentZoom -= ZOOM_LOW_INCR;
669     else
670         currentZoom -= ZOOM_HIGH_INCR;
671 
672     if (currentZoom <= ZOOM_MIN)
673     {
674         currentZoom = ZOOM_MIN;
675         emit actionUpdated("view_zoom_out", false);
676     }
677 
678     emit actionUpdated("view_zoom_in", true);
679 
680     currentWidth  = m_ImageData->width() * (currentZoom / ZOOM_DEFAULT);
681     currentHeight = m_ImageData->height() * (currentZoom / ZOOM_DEFAULT);
682 
683     cleanUpZoom();
684 
685     updateFrame(true);
686 
687     emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM);
688 }
689 
ZoomToFit()690 void FITSView::ZoomToFit()
691 {
692     if (!m_ImageData)
693         return;
694 
695     if (rawImage.isNull() == false)
696     {
697         rescale(ZOOM_FIT_WINDOW);
698         updateFrame(true);
699     }
700 }
701 
setStarFilterRange(float const innerRadius,float const outerRadius)702 void FITSView::setStarFilterRange(float const innerRadius, float const outerRadius)
703 {
704     starFilter.innerRadius = innerRadius;
705     starFilter.outerRadius = outerRadius;
706 }
707 
filterStars()708 int FITSView::filterStars()
709 {
710     return starFilter.used() ? m_ImageData->filterStars(starFilter.innerRadius,
711             starFilter.outerRadius) : m_ImageData->getStarCenters().count();
712 }
713 
714 // isImageLarge() returns whether we use the large-image rendering strategy or the small-image strategy.
715 // See the comment below in getScale() for details.
isLargeImage()716 bool FITSView::isLargeImage()
717 {
718     constexpr int largeImageNumPixels = 1000 * 1000;
719     return rawImage.width() * rawImage.height() >= largeImageNumPixels;
720 }
721 
722 // getScale() is related to the image and overlay rendering strategy used.
723 // If we're using a pixmap appropriate for a large image, where we draw and render on a pixmap that's the image size
724 // and we let the QLabel deal with scaling and zooming, then the scale is 1.0.
725 // With smaller images, where memory use is not as severe, we create a pixmap that's the size of the scaled image
726 // and get scale returns the ratio of that pixmap size to the image size.
getScale()727 double FITSView::getScale()
728 {
729     return (isLargeImage() ? 1.0 : currentZoom / ZOOM_DEFAULT) / m_PreviewSampling;
730 }
731 
732 // scaleSize() is only used with the large-image rendering strategy. It may increase the line
733 // widths or font sizes, as we draw lines and render text on the full image and when zoomed out,
734 // these sizes may be too small.
scaleSize(double size)735 double FITSView::scaleSize(double size)
736 {
737     if (!isLargeImage())
738         return size;
739     return (currentZoom > 100.0 ? size : std::round(size * 100.0 / currentZoom)) / m_PreviewSampling;
740 }
741 
updateFrame(bool now)742 void FITSView::updateFrame(bool now)
743 {
744     // JM 2021-03-13: This timer is used to throttle updateFrame calls to improve performance
745     // If after 250ms no further update frames are called, then the actual update is triggered.
746     // JM 2021-03-16: When stretching in progress, immediately execute so that the user see the changes
747     // in real time
748     if (now)
749     {
750         if (toggleStretchAction)
751             toggleStretchAction->setChecked(stretchImage);
752 
753         // We employ two schemes for managing the image and its overlays, depending on the size of the image
754         // and whether we need to therefore conserve memory. The small-image strategy explicitly scales up
755         // the image, and writes overlays on the scaled pixmap. The large-image strategy uses a pixmap that's
756         // the size of the image itself, never scaling that up.
757         if (isLargeImage())
758             updateFrameLargeImage();
759         else
760             updateFrameSmallImage();
761     }
762     else
763         m_UpdateFrameTimer.start();
764 }
765 
766 
updateFrameLargeImage()767 void FITSView::updateFrameLargeImage()
768 {
769     if (!displayPixmap.convertFromImage(rawImage))
770         return;
771 
772     QPainter painter(&displayPixmap);
773 
774     // Possibly scale the fonts as we're drawing on the full image, not just the visible part of the scroll window.
775     QFont font = painter.font();
776     font.setPixelSize(scaleSize(FONT_SIZE));
777     painter.setFont(font);
778 
779     drawOverlay(&painter, 1.0 / m_PreviewSampling);
780     drawStarFilter(&painter, 1.0 / m_PreviewSampling);
781     m_ImageFrame->setPixmap(displayPixmap);
782     m_ImageFrame->resize(((m_PreviewSampling * currentZoom) / 100.0) * displayPixmap.size());
783 }
784 
updateFrameSmallImage()785 void FITSView::updateFrameSmallImage()
786 {
787     QImage scaledImage = rawImage.scaled(currentWidth, currentHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);
788     if (!displayPixmap.convertFromImage(scaledImage))
789         return;
790 
791     QPainter painter(&displayPixmap);
792 
793     //    if (m_PreviewSampling == 1)
794     //    {
795     drawOverlay(&painter, currentZoom / ZOOM_DEFAULT);
796     drawStarFilter(&painter, currentZoom / ZOOM_DEFAULT);
797     //}
798     m_ImageFrame->setPixmap(displayPixmap);
799     m_ImageFrame->resize(currentWidth, currentHeight);
800 }
801 
drawStarFilter(QPainter * painter,double scale)802 void FITSView::drawStarFilter(QPainter *painter, double scale)
803 {
804     if (!starFilter.used())
805         return;
806     const double w = m_ImageData->width() * scale;
807     const double h = m_ImageData->height() * scale;
808     double const diagonal = std::sqrt(w * w + h * h) / 2;
809     int const innerRadius = std::lround(diagonal * starFilter.innerRadius);
810     int const outerRadius = std::lround(diagonal * starFilter.outerRadius);
811     QPoint const center(w / 2, h / 2);
812     painter->save();
813     painter->setPen(QPen(Qt::blue, scaleSize(1), Qt::DashLine));
814     painter->setOpacity(0.7);
815     painter->setBrush(QBrush(Qt::transparent));
816     painter->drawEllipse(center, outerRadius, outerRadius);
817     painter->setBrush(QBrush(Qt::blue, Qt::FDiagPattern));
818     painter->drawEllipse(center, innerRadius, innerRadius);
819     painter->restore();
820 }
821 
822 namespace
823 {
824 
825 template <typename T>
drawClippingOneChannel(T * inputBuffer,QPainter * painter,int width,int height,double clipVal,double scale)826 void drawClippingOneChannel(T *inputBuffer, QPainter *painter, int width, int height, double clipVal, double scale)
827 {
828     painter->save();
829     painter->setPen(QPen(Qt::red, scale, Qt::SolidLine));
830     const T clipping = clipVal;
831     for (int y = 0; y < height; y++)
832     {
833         const auto inputLine  = inputBuffer + y * width;
834         for (int x = 0; x < width; x++)
835         {
836             if (inputLine[x] > clipping)
837                 painter->drawPoint(x, y);
838         }
839     }
840     painter->restore();
841 }
842 
843 template <typename T>
drawClippingThreeChannels(T * inputBuffer,QPainter * painter,int width,int height,double clipVal,double scale)844 void drawClippingThreeChannels(T *inputBuffer, QPainter *painter, int width, int height, double clipVal, double scale)
845 {
846     painter->save();
847     painter->setPen(QPen(Qt::red, scale, Qt::SolidLine));
848     const int size = width * height;
849     const T clipping = clipVal;
850     for (int y = 0; y < height; y++)
851     {
852         // R, G, B input images are stored one after another.
853         const T * inputLineR  = inputBuffer + y * width;
854         const T * inputLineG  = inputLineR + size;
855         const T * inputLineB  = inputLineG + size;
856 
857         for (int x = 0; x < width; x++)
858         {
859             const T inputR = inputLineR[x];
860             const T inputG = inputLineG[x];
861             const T inputB = inputLineB[x];
862             if (inputR > clipping || inputG > clipping || inputB > clipping)
863                 painter->drawPoint(x, y);
864         }
865     }
866     painter->restore();
867 }
868 
869 template <typename T>
drawClip(T * input_buffer,int num_channels,QPainter * painter,int width,int height,double clipVal,double scale)870 void drawClip(T *input_buffer, int num_channels, QPainter *painter, int width, int height, double clipVal, double scale)
871 {
872     if (num_channels == 1)
873         drawClippingOneChannel(input_buffer, painter, width, height, clipVal, scale);
874     else if (num_channels == 3)
875         drawClippingThreeChannels(input_buffer, painter, width, height, clipVal, scale);
876 }
877 
878 }  // namespace
879 
drawClipping(QPainter * painter)880 void FITSView::drawClipping(QPainter *painter)
881 {
882     auto input = m_ImageData->getImageBuffer();
883     const int height = m_ImageData->height();
884     const int width = m_ImageData->width();
885     constexpr double FLOAT_CLIP = 60000;
886     constexpr double SHORT_CLIP = 30000;
887     constexpr double USHORT_CLIP = 60000;
888     constexpr double BYTE_CLIP = 250;
889     switch (m_ImageData->dataType())
890     {
891         case TBYTE:
892             drawClip(reinterpret_cast<uint8_t const*>(input), m_ImageData->channels(), painter, width, height, BYTE_CLIP,
893                      scaleSize(1));
894             break;
895         case TSHORT:
896             drawClip(reinterpret_cast<short const*>(input), m_ImageData->channels(), painter, width, height, SHORT_CLIP,
897                      scaleSize(1));
898             break;
899         case TUSHORT:
900             drawClip(reinterpret_cast<unsigned short const*>(input), m_ImageData->channels(), painter, width, height, USHORT_CLIP,
901                      scaleSize(1));
902             break;
903         case TLONG:
904             drawClip(reinterpret_cast<long const*>(input), m_ImageData->channels(), painter, width, height, USHORT_CLIP,
905                      scaleSize(1));
906             break;
907         case TFLOAT:
908             drawClip(reinterpret_cast<float const*>(input), m_ImageData->channels(), painter, width, height, FLOAT_CLIP,
909                      scaleSize(1));
910             break;
911         case TLONGLONG:
912             drawClip(reinterpret_cast<long long const*>(input), m_ImageData->channels(), painter, width, height, USHORT_CLIP,
913                      scaleSize(1));
914             break;
915         case TDOUBLE:
916             drawClip(reinterpret_cast<double const*>(input), m_ImageData->channels(), painter, width, height, FLOAT_CLIP,
917                      scaleSize(1));
918             break;
919         default:
920             break;
921     }
922 }
923 
ZoomDefault()924 void FITSView::ZoomDefault()
925 {
926     if (m_ImageFrame)
927     {
928         emit actionUpdated("view_zoom_out", true);
929         emit actionUpdated("view_zoom_in", true);
930 
931         currentZoom   = ZOOM_DEFAULT;
932         currentWidth  = m_ImageData->width();
933         currentHeight = m_ImageData->height();
934 
935         updateFrame();
936 
937         emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM);
938 
939         update();
940     }
941 }
942 
drawOverlay(QPainter * painter,double scale)943 void FITSView::drawOverlay(QPainter * painter, double scale)
944 {
945     painter->setRenderHint(QPainter::Antialiasing, Options::useAntialias());
946 
947     if (trackingBoxEnabled && getCursorMode() != FITSView::scopeCursor)
948         drawTrackingBox(painter, scale);
949 
950     if (!markerCrosshair.isNull())
951         drawMarker(painter, scale);
952 
953     if (showCrosshair)
954         drawCrosshair(painter, scale);
955 
956     if (showObjects)
957         drawObjectNames(painter, scale);
958 
959 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
960     if (showEQGrid)
961         drawEQGrid(painter, scale);
962 #endif
963 
964     if (showPixelGrid)
965         drawPixelGrid(painter, scale);
966 
967     if (markStars)
968         drawStarCentroid(painter, scale);
969 
970     if (showClipping)
971         drawClipping(painter);
972 
973     if (showMagnifyingGlass)
974         drawMagnifyingGlass(painter, scale);
975 }
976 
977 // Draws a 100% resolution image rectangle around the mouse position.
drawMagnifyingGlass(QPainter * painter,double scale)978 void FITSView::drawMagnifyingGlass(QPainter *painter, double scale)
979 {
980     if (magnifyingGlassX >= 0 && magnifyingGlassY >= 0 &&
981             magnifyingGlassX < m_ImageData->width() &&
982             magnifyingGlassY < m_ImageData->height())
983     {
984         // Amount of magnification.
985         constexpr double magAmount = 8;
986         // Desired size in pixels of the magnification window.
987         constexpr int magWindowSize = 130;
988         // The distance from the mouse position to the magnifying glass rectangle, in the source image coordinates.
989         const int winXOffset = magWindowSize * 10.0 / currentZoom;
990         const int winYOffset = magWindowSize * 10.0 / currentZoom;
991         // Size of a side of the square of input to make a window that size.
992         const int inputDimension = magWindowSize * 100 / currentZoom;
993         // Size of the square drawn. Not the same, necessarily as the magWindowSize,
994         // since the output may be scaled (if isLargeImage()==true) to become screen pixels.
995         const int outputDimension = inputDimension * scale + .99;
996 
997         // Where the source data (to be magnified) comes from.
998         int imgLeft = magnifyingGlassX - inputDimension / (2 * magAmount);
999         int imgTop = magnifyingGlassY - inputDimension / (2 * magAmount);
1000 
1001         // Where we'll draw the magnifying glass rectangle.
1002         int winLeft = magnifyingGlassX + winXOffset;
1003         int winTop = magnifyingGlassY + winYOffset;
1004 
1005         // Normally we place the magnifying glass rectangle to the right and below the mouse curson.
1006         // However, if it would be rendered outside the image, put it on the other side.
1007         int w = rawImage.width();
1008         int h = rawImage.height();
1009         const int rightLimit = std::min(w, static_cast<int>((horizontalScrollBar()->value() + width()) * 100 / currentZoom));
1010         const int bottomLimit = std::min(h, static_cast<int>((verticalScrollBar()->value() + height()) * 100 / currentZoom));
1011         if (winLeft + winXOffset + inputDimension > rightLimit)
1012             winLeft -= (2 * winXOffset + inputDimension);
1013         if (winTop + winYOffset + inputDimension > bottomLimit)
1014             winTop -= (2 * winYOffset + inputDimension);
1015 
1016         // Blacken the output where magnifying outside the source image.
1017         if ((imgLeft < 0 ) ||
1018                 (imgLeft + inputDimension / magAmount >= w) ||
1019                 (imgTop < 0) ||
1020                 (imgTop + inputDimension / magAmount > h))
1021         {
1022             painter->setBrush(QBrush(Qt::black));
1023             painter->drawRect(winLeft * scale, winTop * scale, outputDimension, outputDimension);
1024             painter->setBrush(QBrush(Qt::transparent));
1025         }
1026 
1027         // Finally, draw the magnified image.
1028         painter->drawImage(QRect(winLeft * scale, winTop * scale, outputDimension, outputDimension),
1029                            rawImage,
1030                            QRect(imgLeft, imgTop, inputDimension / magAmount, inputDimension / magAmount));
1031         // Draw a white border.
1032         painter->setPen(QPen(Qt::white, scaleSize(1)));
1033         painter->drawRect(winLeft * scale, winTop * scale, outputDimension, outputDimension);
1034     }
1035 }
1036 
1037 // x,y are the image coordinates where the magnifying glass is positioned.
updateMagnifyingGlass(int x,int y)1038 void FITSView::updateMagnifyingGlass(int x, int y)
1039 {
1040     if (!m_ImageData)
1041         return;
1042 
1043     magnifyingGlassX = x;
1044     magnifyingGlassY = y;
1045     if (magnifyingGlassX == -1 && magnifyingGlassY == -1)
1046     {
1047         if (showMagnifyingGlass)
1048             updateFrame(true);
1049         showMagnifyingGlass = false;
1050     }
1051     else
1052     {
1053         showMagnifyingGlass = true;
1054         updateFrame(true);
1055     }
1056 }
1057 
updateMode(FITSMode fmode)1058 void FITSView::updateMode(FITSMode fmode)
1059 {
1060     mode = fmode;
1061 }
1062 
drawMarker(QPainter * painter,double scale)1063 void FITSView::drawMarker(QPainter * painter, double scale)
1064 {
1065     painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")),
1066                          scaleSize(2)));
1067     painter->setBrush(Qt::NoBrush);
1068     const float pxperdegree = scale * (57.3 / 1.8);
1069 
1070     const float s1 = 0.5 * pxperdegree;
1071     const float s2 = pxperdegree;
1072     const float s3 = 2.0 * pxperdegree;
1073 
1074     const float x0 = scale * markerCrosshair.x();
1075     const float y0 = scale * markerCrosshair.y();
1076     const float x1 = x0 - 0.5 * s1;
1077     const float y1 = y0 - 0.5 * s1;
1078     const float x2 = x0 - 0.5 * s2;
1079     const float y2 = y0 - 0.5 * s2;
1080     const float x3 = x0 - 0.5 * s3;
1081     const float y3 = y0 - 0.5 * s3;
1082 
1083     //Draw radial lines
1084     painter->drawLine(QPointF(x1, y0), QPointF(x3, y0));
1085     painter->drawLine(QPointF(x0 + s2, y0), QPointF(x0 + 0.5 * s1, y0));
1086     painter->drawLine(QPointF(x0, y1), QPointF(x0, y3));
1087     painter->drawLine(QPointF(x0, y0 + 0.5 * s1), QPointF(x0, y0 + s2));
1088     //Draw circles at 0.5 & 1 degrees
1089     painter->drawEllipse(QRectF(x1, y1, s1, s1));
1090     painter->drawEllipse(QRectF(x2, y2, s2, s2));
1091 }
1092 
drawHFR(QPainter * painter,const QString & hfr,int x,int y)1093 bool FITSView::drawHFR(QPainter * painter, const QString &hfr, int x, int y)
1094 {
1095     QRect const boundingRect(0, 0, painter->device()->width(), painter->device()->height());
1096     QSize const hfrSize = painter->fontMetrics().size(Qt::TextSingleLine, hfr);
1097 
1098     // Store the HFR text in a rect
1099     QPoint const hfrBottomLeft(x, y);
1100     QRect const hfrRect(hfrBottomLeft.x(), hfrBottomLeft.y() - hfrSize.height(), hfrSize.width(), hfrSize.height());
1101 
1102     // Render the HFR text only if it can be displayed entirely
1103     if (boundingRect.contains(hfrRect))
1104     {
1105         painter->setPen(QPen(Qt::red, scaleSize(3)));
1106         painter->drawText(hfrBottomLeft, hfr);
1107         painter->setPen(QPen(Qt::red, scaleSize(2)));
1108         return true;
1109     }
1110     return false;
1111 }
1112 
1113 
drawStarCentroid(QPainter * painter,double scale)1114 void FITSView::drawStarCentroid(QPainter * painter, double scale)
1115 {
1116     QFont painterFont;
1117     double fontSize = painterFont.pointSizeF() * 2;
1118     painter->setRenderHint(QPainter::Antialiasing);
1119     if (showStarsHFR)
1120     {
1121         // If we need to print the HFR out, give an arbitrarily sized font to the painter
1122         if (isLargeImage())
1123             fontSize = scaleSize(painterFont.pointSizeF());
1124         painterFont.setPointSizeF(fontSize);
1125         painter->setFont(painterFont);
1126     }
1127 
1128     painter->setPen(QPen(Qt::red, scaleSize(2)));
1129 
1130     for (auto const &starCenter : m_ImageData->getStarCenters())
1131     {
1132         int const w  = std::round(starCenter->width) * scale;
1133 
1134         // Draw a circle around the detected star.
1135         // SEP coordinates are in the center of pixels, and Qt at the boundary.
1136         const double xCoord = starCenter->x - 0.5;
1137         const double yCoord = starCenter->y - 0.5;
1138         const int xc = std::round((xCoord - starCenter->width / 2.0f) * scale);
1139         const int yc = std::round((yCoord - starCenter->width / 2.0f) * scale);
1140         const int hw = w / 2;
1141 
1142         BahtinovEdge* bEdge = dynamic_cast<BahtinovEdge*>(starCenter);
1143         if (bEdge != nullptr)
1144         {
1145             // Draw lines of diffraction pattern
1146             painter->setPen(QPen(Qt::red, scaleSize(2)));
1147             painter->drawLine(bEdge->line[0].x1() * scale, bEdge->line[0].y1() * scale,
1148                               bEdge->line[0].x2() * scale, bEdge->line[0].y2() * scale);
1149             painter->setPen(QPen(Qt::green, scaleSize(2)));
1150             painter->drawLine(bEdge->line[1].x1() * scale, bEdge->line[1].y1() * scale,
1151                               bEdge->line[1].x2() * scale, bEdge->line[1].y2() * scale);
1152             painter->setPen(QPen(Qt::darkGreen, scaleSize(2)));
1153             painter->drawLine(bEdge->line[2].x1() * scale, bEdge->line[2].y1() * scale,
1154                               bEdge->line[2].x2() * scale, bEdge->line[2].y2() * scale);
1155 
1156             // Draw center circle
1157             painter->setPen(QPen(Qt::white, scaleSize(2)));
1158             painter->drawEllipse(xc, yc, w, w);
1159 
1160             // Draw offset circle
1161             double factor = 15.0;
1162             QPointF offsetVector = (bEdge->offset - QPointF(starCenter->x, starCenter->y)) * factor;
1163             int const xo = std::round((starCenter->x + offsetVector.x() - starCenter->width / 2.0f) * scale);
1164             int const yo = std::round((starCenter->y + offsetVector.y() - starCenter->width / 2.0f) * scale);
1165             painter->setPen(QPen(Qt::red, scaleSize(2)));
1166             painter->drawEllipse(xo, yo, w, w);
1167 
1168             // Draw line between center circle and offset circle
1169             painter->setPen(QPen(Qt::red, scaleSize(2)));
1170             painter->drawLine(xc + hw, yc + hw, xo + hw, yo + hw);
1171         }
1172         else
1173         {
1174             const double radius = starCenter->HFR > 0 ? 2.0f * starCenter->HFR * scale : w;
1175             painter->drawEllipse(QPointF(xCoord * scale, yCoord * scale), radius, radius);
1176         }
1177 
1178         if (showStarsHFR)
1179         {
1180             // Ask the painter how large will the HFR text be
1181             QString const hfr = QString("%1").arg(starCenter->HFR, 0, 'f', 2);
1182             if (!drawHFR(painter, hfr, xc + w + 5, yc + w / 2))
1183             {
1184                 // Try a few more time with smaller fonts;
1185                 for (int i = 0; i < 10; ++i)
1186                 {
1187                     const double tempFontSize = painterFont.pointSizeF() - 2;
1188                     if (tempFontSize <= 0) break;
1189                     painterFont.setPointSizeF(tempFontSize);
1190                     painter->setFont(painterFont);
1191                     if (drawHFR(painter, hfr, xc + w + 5, yc + w / 2))
1192                         break;
1193                 }
1194                 // Reset the font size.
1195                 painterFont.setPointSize(fontSize);
1196                 painter->setFont(painterFont);
1197             }
1198         }
1199     }
1200 }
1201 
drawTrackingBox(QPainter * painter,double scale)1202 void FITSView::drawTrackingBox(QPainter * painter, double scale)
1203 {
1204     painter->setPen(QPen(Qt::green, scaleSize(2)));
1205 
1206     if (trackingBox.isNull())
1207         return;
1208 
1209     const int x1 = trackingBox.x() * scale;
1210     const int y1 = trackingBox.y() * scale;
1211     const int w  = trackingBox.width() * scale;
1212     const int h  = trackingBox.height() * scale;
1213 
1214     painter->drawRect(x1, y1, w, h);
1215 }
1216 
1217 /**
1218 This Method draws a large Crosshair in the center of the image, it is like a set of axes.
1219  */
1220 
drawCrosshair(QPainter * painter,double scale)1221 void FITSView::drawCrosshair(QPainter * painter, double scale)
1222 {
1223     if (!m_ImageData) return;
1224     const int image_width = m_ImageData->width();
1225     const int image_height = m_ImageData->height();
1226     const QPointF c   = QPointF((qreal)image_width / 2 * scale, (qreal)image_height / 2 * scale);
1227     const float midX  = (float)image_width / 2 * scale;
1228     const float midY  = (float)image_height / 2 * scale;
1229     const float maxX  = (float)image_width * scale;
1230     const float maxY  = (float)image_height * scale;
1231     const float r = 50 * scale;
1232 
1233     painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")), scaleSize(1)));
1234 
1235     //Horizontal Line to Circle
1236     painter->drawLine(0, midY, midX - r, midY);
1237 
1238     //Horizontal Line past Circle
1239     painter->drawLine(midX + r, midY, maxX, midY);
1240 
1241     //Vertical Line to Circle
1242     painter->drawLine(midX, 0, midX, midY - r);
1243 
1244     //Vertical Line past Circle
1245     painter->drawLine(midX, midY + r, midX, maxY);
1246 
1247     //Circles
1248     painter->drawEllipse(c, r, r);
1249     painter->drawEllipse(c, r / 2, r / 2);
1250 }
1251 
1252 /**
1253 This method is intended to draw a pixel grid onto the image.  It first determines useful information
1254 from the image.  Then it draws the axes on the image if the crosshairs are not displayed.
1255 Finally it draws the gridlines so that there will be 4 Gridlines on either side of the axes.
1256 Note: This has to start drawing at the center not at the edges because the center axes must
1257 be in the center of the image.
1258  */
1259 
drawPixelGrid(QPainter * painter,double scale)1260 void FITSView::drawPixelGrid(QPainter * painter, double scale)
1261 {
1262     const float width  = m_ImageData->width() * scale;
1263     const float height = m_ImageData->height() * scale;
1264     const float cX     = width / 2;
1265     const float cY     = height / 2;
1266     const float deltaX = width / 10;
1267     const float deltaY = height / 10;
1268     QFontMetrics fm(painter->font());
1269 
1270     //draw the Axes
1271     painter->setPen(QPen(Qt::red, scaleSize(1)));
1272     painter->drawText(cX - 30, height - 5, QString::number((int)((cX) / scale)));
1273     QString str = QString::number((int)((cY) / scale));
1274 #if QT_VERSION < QT_VERSION_CHECK(5,11,0)
1275     painter->drawText(width - (fm.width(str) + 10), cY - 5, str);
1276 #else
1277     painter->drawText(width - (fm.horizontalAdvance(str) + 10), cY - 5, str);
1278 #endif
1279     if (!showCrosshair)
1280     {
1281         painter->drawLine(cX, 0, cX, height);
1282         painter->drawLine(0, cY, width, cY);
1283     }
1284     painter->setPen(QPen(Qt::gray, scaleSize(1)));
1285     //Start one iteration past the Center and draw 4 lines on either side of 0
1286     for (int x = deltaX; x < cX - deltaX; x += deltaX)
1287     {
1288         painter->drawText(cX + x - 30, height - 5, QString::number((int)(cX + x) / scale));
1289         painter->drawText(cX - x - 30, height - 5, QString::number((int)(cX - x) / scale));
1290         painter->drawLine(cX - x, 0, cX - x, height);
1291         painter->drawLine(cX + x, 0, cX + x, height);
1292     }
1293     //Start one iteration past the Center and draw 4 lines on either side of 0
1294     for (int y = deltaY; y < cY - deltaY; y += deltaY)
1295     {
1296         QString str = QString::number((int)((cY + y) / scale));
1297 #if QT_VERSION < QT_VERSION_CHECK(5,11,0)
1298         painter->drawText(width - (fm.width(str) + 10), cY + y - 5, str);
1299 #else
1300         painter->drawText(width - (fm.horizontalAdvance(str) + 10), cY + y - 5, str);
1301 #endif
1302         str = QString::number((int)((cY - y) / scale));
1303 #if QT_VERSION < QT_VERSION_CHECK(5,11,0)
1304         painter->drawText(width - (fm.width(str) + 10), cY - y - 5, str);
1305 #else
1306         painter->drawText(width - (fm.horizontalAdvance(str) + 10), cY - y - 5, str);
1307 #endif
1308         painter->drawLine(0, cY + y, width, cY + y);
1309         painter->drawLine(0, cY - y, width, cY - y);
1310     }
1311 }
1312 
imageHasWCS()1313 bool FITSView::imageHasWCS()
1314 {
1315     if (m_ImageData != nullptr)
1316         return m_ImageData->hasWCS();
1317     return false;
1318 }
1319 
drawObjectNames(QPainter * painter,double scale)1320 void FITSView::drawObjectNames(QPainter * painter, double scale)
1321 {
1322     painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("FITSObjectLabelColor"))));
1323     for (const auto &listObject : m_ImageData->getSkyObjects())
1324     {
1325         painter->drawRect(listObject->x() * scale - 5, listObject->y() * scale - 5, 10, 10);
1326         painter->drawText(listObject->x() * scale + 10, listObject->y() * scale + 10, listObject->skyObject()->name());
1327     }
1328 }
1329 
1330 /**
1331 This method will paint EQ Gridlines in an overlay if there is WCS data present.
1332 It determines the minimum and maximum RA and DEC, then it uses that information to
1333 judge which gridLines to draw.  Then it calls the drawEQGridlines methods below
1334 to draw gridlines at those specific RA and Dec values.
1335  */
1336 
1337 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
drawEQGrid(QPainter * painter,double scale)1338 void FITSView::drawEQGrid(QPainter * painter, double scale)
1339 {
1340     const int image_width = m_ImageData->width();
1341     const int image_height = m_ImageData->height();
1342 
1343     if (m_ImageData->hasWCS() && m_ImageData->fullWCS())
1344     {
1345         double maxRA  = -1000;
1346         double minRA  = 1000;
1347         double maxDec = -1000;
1348         double minDec = 1000;
1349         m_ImageData->findWCSBounds(minRA, maxRA, minDec, maxDec);
1350 
1351         auto minDecMinutes = (int)(minDec * 12); //This will force the Dec Scale to 5 arc minutes in the loop
1352         auto maxDecMinutes = (int)(maxDec * 12);
1353 
1354         auto minRAMinutes =
1355             (int)(minRA / 15.0 *
1356                   120.0); //This will force the scale to 1/2 minutes of RA in the loop from 0 to 50 degrees
1357         auto maxRAMinutes = (int)(maxRA / 15.0 * 120.0);
1358 
1359         double raConvert  = 15 / 120.0; //This will undo the calculation above to retrieve the actual RA.
1360         double decConvert = 1.0 / 12.0; //This will undo the calculation above to retrieve the actual DEC.
1361 
1362         if (maxDec > 50 || minDec < -50)
1363         {
1364             minRAMinutes =
1365                 (int)(minRA / 15.0 * 60.0); //This will force the scale to 1 min of RA from 50 to 80 degrees
1366             maxRAMinutes = (int)(maxRA / 15.0 * 60.0);
1367             raConvert    = 15 / 60.0;
1368         }
1369 
1370         if (maxDec > 80 || minDec < -80)
1371         {
1372             minRAMinutes =
1373                 (int)(minRA / 15.0 * 30); //This will force the scale to 2 min of RA from 80 to 85 degrees
1374             maxRAMinutes = (int)(maxRA / 15.0 * 30);
1375             raConvert    = 15 / 30.0;
1376         }
1377         if (maxDec > 85 || minDec < -85)
1378         {
1379             minRAMinutes =
1380                 (int)(minRA / 15.0 * 6); //This will force the scale to 10 min of RA from 85 to 89 degrees
1381             maxRAMinutes = (int)(maxRA / 15.0 * 6);
1382             raConvert    = 15 / 6.0;
1383         }
1384         if (maxDec >= 89.25 || minDec <= -89.25)
1385         {
1386             minRAMinutes =
1387                 (int)(minRA /
1388                       15); //This will force the scale to whole hours of RA in the loop really close to the poles
1389             maxRAMinutes = (int)(maxRA / 15);
1390             raConvert    = 15;
1391         }
1392 
1393         painter->setPen(QPen(Qt::yellow));
1394 
1395         QPointF pixelPoint, imagePoint, pPoint;
1396 
1397         //This section draws the RA Gridlines
1398 
1399         for (int targetRA = minRAMinutes; targetRA <= maxRAMinutes; targetRA++)
1400         {
1401             painter->setPen(QPen(Qt::yellow));
1402             double target = targetRA * raConvert;
1403 
1404             if (eqGridPoints.count() != 0)
1405                 eqGridPoints.clear();
1406 
1407             double increment = std::abs((maxDec - minDec) /
1408                                         100.0); //This will determine how many points to use to create the RA Line
1409 
1410             for (double targetDec = minDec; targetDec <= maxDec; targetDec += increment)
1411             {
1412                 SkyPoint pointToGet(target / 15.0, targetDec);
1413                 bool inImage = m_ImageData->wcsToPixel(pointToGet, pixelPoint, imagePoint);
1414                 if (inImage)
1415                 {
1416                     QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale);
1417                     eqGridPoints.append(pt);
1418                 }
1419             }
1420 
1421             if (eqGridPoints.count() > 1)
1422             {
1423                 for (int i = 1; i < eqGridPoints.count(); i++)
1424                     painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i));
1425                 QString str = QString::number(dms(target).hour()) + "h " +
1426                               QString::number(dms(target).minute()) + '\'';
1427                 if  (maxDec <= 50 && maxDec >= -50)
1428                     str = str + " " + QString::number(dms(target).second()) + "''";
1429                 QPointF pt = getPointForGridLabel(painter, str, scale);
1430                 if (pt.x() != -100)
1431                     painter->drawText(pt.x(), pt.y(), str);
1432             }
1433         }
1434 
1435         //This section draws the DEC Gridlines
1436 
1437         for (int targetDec = minDecMinutes; targetDec <= maxDecMinutes; targetDec++)
1438         {
1439             if (eqGridPoints.count() != 0)
1440                 eqGridPoints.clear();
1441 
1442             double increment = std::abs((maxRA - minRA) /
1443                                         100.0); //This will determine how many points to use to create the Dec Line
1444             double target    = targetDec * decConvert;
1445 
1446             for (double targetRA = minRA; targetRA <= maxRA; targetRA += increment)
1447             {
1448                 SkyPoint pointToGet(targetRA / 15, targetDec * decConvert);
1449                 bool inImage = m_ImageData->wcsToPixel(pointToGet, pixelPoint, imagePoint);
1450                 if (inImage)
1451                 {
1452                     QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale);
1453                     eqGridPoints.append(pt);
1454                 }
1455             }
1456             if (eqGridPoints.count() > 1)
1457             {
1458                 for (int i = 1; i < eqGridPoints.count(); i++)
1459                     painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i));
1460                 QString str = QString::number(dms(target).degree()) + "° " + QString::number(dms(target).arcmin()) + '\'';
1461                 QPointF pt = getPointForGridLabel(painter, str, scale);
1462                 if (pt.x() != -100)
1463                     painter->drawText(pt.x(), pt.y(), str);
1464             }
1465         }
1466 
1467         //This Section Draws the North Celestial Pole if present
1468         SkyPoint NCP(0, 90);
1469 
1470         bool NCPtest = m_ImageData->wcsToPixel(NCP, pPoint, imagePoint);
1471         if (NCPtest)
1472         {
1473             bool NCPinImage =
1474                 (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height);
1475             if (NCPinImage)
1476             {
1477                 painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4,
1478                                   KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"));
1479                 painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15,
1480                                   i18nc("North Celestial Pole", "NCP"));
1481             }
1482         }
1483 
1484         //This Section Draws the South Celestial Pole if present
1485         SkyPoint SCP(0, -90);
1486 
1487         bool SCPtest = m_ImageData->wcsToPixel(SCP, pPoint, imagePoint);
1488         if (SCPtest)
1489         {
1490             bool SCPinImage =
1491                 (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height);
1492             if (SCPinImage)
1493             {
1494                 painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4,
1495                                   KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"));
1496                 painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15,
1497                                   i18nc("South Celestial Pole", "SCP"));
1498             }
1499         }
1500     }
1501 }
1502 #endif
1503 
pointIsInImage(QPointF pt,double scale)1504 bool FITSView::pointIsInImage(QPointF pt, double scale)
1505 {
1506     int image_width = m_ImageData->width();
1507     int image_height = m_ImageData->height();
1508     return pt.x() < image_width * scale && pt.y() < image_height * scale && pt.x() > 0 && pt.y() > 0;
1509 }
1510 
getPointForGridLabel(QPainter * painter,const QString & str,double scale)1511 QPointF FITSView::getPointForGridLabel(QPainter *painter, const QString &str, double scale)
1512 {
1513     QFontMetrics fm(painter->font());
1514 #if QT_VERSION < QT_VERSION_CHECK(5,11,0)
1515     int strWidth = fm.width(str);
1516 #else
1517     int strWidth = fm.horizontalAdvance(str);
1518 #endif
1519     int strHeight = fm.height();
1520     int image_width = m_ImageData->width();
1521     int image_height = m_ImageData->height();
1522 
1523     //These get the maximum X and Y points in the list that are in the image
1524     QPointF maxXPt(image_width * scale / 2, image_height * scale / 2);
1525     for (auto &p : eqGridPoints)
1526     {
1527         if (p.x() > maxXPt.x() && pointIsInImage(p, scale))
1528             maxXPt = p;
1529     }
1530     QPointF maxYPt(image_width * scale / 2, image_height * scale / 2);
1531 
1532     for (auto &p : eqGridPoints)
1533     {
1534         if (p.y() > maxYPt.y() && pointIsInImage(p, scale))
1535             maxYPt = p;
1536     }
1537     QPointF minXPt(image_width * scale / 2, image_height * scale / 2);
1538 
1539     for (auto &p : eqGridPoints)
1540     {
1541         if (p.x() < minXPt.x() && pointIsInImage(p, scale))
1542             minXPt = p;
1543     }
1544     QPointF minYPt(image_width * scale / 2, image_height * scale / 2);
1545 
1546     for (auto &p : eqGridPoints)
1547     {
1548         if (p.y() < minYPt.y() && pointIsInImage(p, scale))
1549             minYPt = p;
1550     }
1551 
1552     //This gives preference to points that are on the right hand side and bottom.
1553     //But if the line doesn't intersect the right or bottom, it then tries for the top and left.
1554     //If no points are found in the image, it returns a point off the screen
1555     //If all else fails, like in the case of a circle on the image, it returns the far right point.
1556 
1557     if (image_width * scale - maxXPt.x() < strWidth)
1558     {
1559         return QPointF(
1560                    image_width * scale - (strWidth + 10),
1561                    maxXPt.y() -
1562                    strHeight); //This will draw the text on the right hand side, up and to the left of the point where the line intersects
1563     }
1564     if (image_height * scale - maxYPt.y() < strHeight)
1565         return QPointF(
1566                    maxYPt.x() - (strWidth + 10),
1567                    image_height * scale -
1568                    (strHeight + 10)); //This will draw the text on the bottom side, up and to the left of the point where the line intersects
1569     if (minYPt.y() < strHeight)
1570         return QPointF(
1571                    minYPt.x() * scale + 10,
1572                    strHeight + 20); //This will draw the text on the top side, down and to the right of the point where the line intersects
1573     if (minXPt.x() < strWidth)
1574         return QPointF(
1575                    10,
1576                    minXPt.y() * scale +
1577                    strHeight +
1578                    20); //This will draw the text on the left hand side, down and to the right of the point where the line intersects
1579     if (maxXPt.x() == image_width * scale / 2 && maxXPt.y() == image_height * scale / 2)
1580         return QPointF(-100, -100); //All of the points were off the screen
1581 
1582     return QPoint(maxXPt.x() - (strWidth + 10), maxXPt.y() - (strHeight + 10));
1583 }
1584 
setFirstLoad(bool value)1585 void FITSView::setFirstLoad(bool value)
1586 {
1587     firstLoad = value;
1588 }
1589 
getTrackingBoxPixmap(uint8_t margin)1590 QPixmap &FITSView::getTrackingBoxPixmap(uint8_t margin)
1591 {
1592     if (trackingBox.isNull())
1593         return trackingBoxPixmap;
1594 
1595     // We need to know which rendering strategy updateFrame used to determine the scaling.
1596     const float scale = getScale();
1597 
1598     int x1 = (trackingBox.x() - margin) * scale;
1599     int y1 = (trackingBox.y() - margin) * scale;
1600     int w  = (trackingBox.width() + margin * 2) * scale;
1601     int h  = (trackingBox.height() + margin * 2) * scale;
1602 
1603     trackingBoxPixmap = m_ImageFrame->grab(QRect(x1, y1, w, h));
1604     return trackingBoxPixmap;
1605 }
1606 
setTrackingBox(const QRect & rect)1607 void FITSView::setTrackingBox(const QRect &rect)
1608 {
1609     if (rect != trackingBox)
1610     {
1611         trackingBox        = rect;
1612         updateFrame();
1613         if(showStarProfile)
1614             viewStarProfile();
1615     }
1616 }
1617 
resizeTrackingBox(int newSize)1618 void FITSView::resizeTrackingBox(int newSize)
1619 {
1620     int x = trackingBox.x() + trackingBox.width() / 2;
1621     int y = trackingBox.y() + trackingBox.height() / 2;
1622     int delta = newSize / 2;
1623     setTrackingBox(QRect( x - delta, y - delta, newSize, newSize));
1624 }
1625 
isImageStretched()1626 bool FITSView::isImageStretched()
1627 {
1628     return stretchImage;
1629 }
1630 
isClippingShown()1631 bool FITSView::isClippingShown()
1632 {
1633     return showClipping;
1634 }
1635 
isCrosshairShown()1636 bool FITSView::isCrosshairShown()
1637 {
1638     return showCrosshair;
1639 }
1640 
isEQGridShown()1641 bool FITSView::isEQGridShown()
1642 {
1643     return showEQGrid;
1644 }
1645 
areObjectsShown()1646 bool FITSView::areObjectsShown()
1647 {
1648     return showObjects;
1649 }
1650 
isPixelGridShown()1651 bool FITSView::isPixelGridShown()
1652 {
1653     return showPixelGrid;
1654 }
1655 
toggleCrosshair()1656 void FITSView::toggleCrosshair()
1657 {
1658     showCrosshair = !showCrosshair;
1659     updateFrame();
1660 }
1661 
toggleClipping()1662 void FITSView::toggleClipping()
1663 {
1664     showClipping = !showClipping;
1665     updateFrame();
1666 }
1667 
toggleEQGrid()1668 void FITSView::toggleEQGrid()
1669 {
1670     showEQGrid = !showEQGrid;
1671 
1672     if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
1673     {
1674         QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS, true);
1675         wcsWatcher.setFuture(future);
1676         return;
1677     }
1678 
1679     if (m_ImageFrame)
1680         updateFrame();
1681 }
1682 
toggleObjects()1683 void FITSView::toggleObjects()
1684 {
1685     showObjects = !showObjects;
1686 
1687     if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
1688     {
1689         QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS, true);
1690         wcsWatcher.setFuture(future);
1691         return;
1692     }
1693 
1694     if (m_ImageFrame)
1695     {
1696 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1697         m_ImageData->searchObjects();
1698 #endif
1699         updateFrame();
1700     }
1701 }
1702 
toggleStars()1703 void FITSView::toggleStars()
1704 {
1705     toggleStars(!markStars);
1706     if (m_ImageFrame)
1707         updateFrame();
1708 }
1709 
toggleStretch()1710 void FITSView::toggleStretch()
1711 {
1712     stretchImage = !stretchImage;
1713     if (m_ImageFrame && rescale(ZOOM_KEEP_LEVEL))
1714         updateFrame();
1715 }
1716 
toggleStarProfile()1717 void FITSView::toggleStarProfile()
1718 {
1719 #ifdef HAVE_DATAVISUALIZATION
1720     showStarProfile = !showStarProfile;
1721     if(showStarProfile && trackingBoxEnabled)
1722         viewStarProfile();
1723     if(toggleProfileAction)
1724         toggleProfileAction->setChecked(showStarProfile);
1725 
1726     if(showStarProfile)
1727     {
1728         //The tracking box is already on for Guide and Focus Views, but off for Normal and Align views.
1729         //So for Normal and Align views, we need to set up the tracking box.
1730         if(mode == FITS_NORMAL || mode == FITS_ALIGN)
1731         {
1732             setCursorMode(selectCursor);
1733             connect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int)));
1734             trackingBox = QRect(0, 0, 128, 128);
1735             setTrackingBoxEnabled(true);
1736             if(starProfileWidget)
1737                 connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int)));
1738         }
1739         if(starProfileWidget)
1740             connect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile()));
1741     }
1742     else
1743     {
1744         //This shuts down the tracking box for Normal and Align Views
1745         //It doesn't touch Guide and Focus Views because they still need a tracking box
1746         if(mode == FITS_NORMAL || mode == FITS_ALIGN)
1747         {
1748             if(getCursorMode() == selectCursor)
1749                 setCursorMode(dragCursor);
1750             disconnect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int)));
1751             setTrackingBoxEnabled(false);
1752             if(starProfileWidget)
1753                 disconnect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int)));
1754         }
1755         if(starProfileWidget)
1756         {
1757             disconnect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile()));
1758             starProfileWidget->close();
1759             starProfileWidget = nullptr;
1760         }
1761         emit starProfileWindowClosed();
1762     }
1763     updateFrame();
1764 #endif
1765 }
1766 
move3DTrackingBox(int x,int y)1767 void FITSView::move3DTrackingBox(int x, int y)
1768 {
1769     int boxSize = trackingBox.width();
1770     QRect starRect = QRect(x - boxSize / 2, y - boxSize / 2, boxSize, boxSize);
1771     setTrackingBox(starRect);
1772 }
1773 
viewStarProfile()1774 void FITSView::viewStarProfile()
1775 {
1776 #ifdef HAVE_DATAVISUALIZATION
1777     if(!trackingBoxEnabled)
1778     {
1779         setTrackingBoxEnabled(true);
1780         setTrackingBox(QRect(0, 0, 128, 128));
1781     }
1782     if(!starProfileWidget)
1783     {
1784         starProfileWidget = new StarProfileViewer(this);
1785 
1786         //This is a band-aid to fix a QT bug with createWindowContainer
1787         //It will set the cursor of the Window containing the view that called the Star Profile method to the Arrow Cursor
1788         //Note that Ekos Manager is a QDialog and FitsViewer is a KXmlGuiWindow
1789         QWidget * superParent = this->parentWidget();
1790         while(superParent->parentWidget() != 0 && !superParent->inherits("QDialog") && !superParent->inherits("KXmlGuiWindow"))
1791             superParent = superParent->parentWidget();
1792         superParent->setCursor(Qt::ArrowCursor);
1793         //This is the end of the band-aid
1794 
1795         connect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile()));
1796         if(mode == FITS_ALIGN || mode == FITS_NORMAL)
1797         {
1798             starProfileWidget->enableTrackingBox(true);
1799             m_ImageData->setStarAlgorithm(ALGORITHM_CENTROID);
1800             connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int)));
1801         }
1802     }
1803     QList<Edge *> starCenters = m_ImageData->getStarCentersInSubFrame(trackingBox);
1804     if(starCenters.size() == 0)
1805     {
1806         // FIXME, the following does not work anymore.
1807         //m_ImageData->findStars(&trackingBox, true);
1808         // FIXME replacing it with this
1809         m_ImageData->findStars(ALGORITHM_CENTROID, trackingBox).waitForFinished();
1810         starCenters = m_ImageData->getStarCentersInSubFrame(trackingBox);
1811     }
1812 
1813     starProfileWidget->loadData(m_ImageData, trackingBox, starCenters);
1814     starProfileWidget->show();
1815     starProfileWidget->raise();
1816     if(markStars)
1817         updateFrame(); //this is to update for the marked stars
1818 
1819 #endif
1820 }
1821 
togglePixelGrid()1822 void FITSView::togglePixelGrid()
1823 {
1824     showPixelGrid = !showPixelGrid;
1825     updateFrame();
1826 }
1827 
findStars(StarAlgorithm algorithm,const QRect & searchBox)1828 QFuture<bool> FITSView::findStars(StarAlgorithm algorithm, const QRect &searchBox)
1829 {
1830     if(trackingBoxEnabled)
1831         return m_ImageData->findStars(algorithm, trackingBox);
1832     else
1833         return m_ImageData->findStars(algorithm, searchBox);
1834 }
1835 
toggleStars(bool enable)1836 void FITSView::toggleStars(bool enable)
1837 {
1838     markStars = enable;
1839 
1840     if (markStars)
1841         searchStars();
1842 }
1843 
searchStars()1844 void FITSView::searchStars()
1845 {
1846     QVariant frameType;
1847     if (m_ImageData->areStarsSearched() || !m_ImageData || (m_ImageData->getRecordValue("FRAME", frameType)
1848             && frameType.toString() != "Light"))
1849         return;
1850 
1851     QApplication::setOverrideCursor(Qt::WaitCursor);
1852     emit newStatus(i18n("Finding stars..."), FITS_MESSAGE);
1853     qApp->processEvents();
1854 
1855 #ifdef HAVE_STELLARSOLVER
1856     QVariantMap extractionSettings;
1857     extractionSettings["optionsProfileIndex"] = Options::hFROptionsProfile();
1858     extractionSettings["optionsProfileGroup"] = static_cast<int>(Ekos::HFRProfiles);
1859     imageData()->setSourceExtractorSettings(extractionSettings);
1860 #endif
1861 
1862     QFuture<bool> result = findStars(ALGORITHM_SEP);
1863     result.waitForFinished();
1864     if (result.result() && isVisible())
1865     {
1866         emit newStatus("", FITS_MESSAGE);
1867     }
1868     QApplication::restoreOverrideCursor();
1869 }
1870 
processPointSelection(int x,int y)1871 void FITSView::processPointSelection(int x, int y)
1872 {
1873     emit trackingStarSelected(x, y);
1874 }
1875 
processMarkerSelection(int x,int y)1876 void FITSView::processMarkerSelection(int x, int y)
1877 {
1878     markerCrosshair.setX(x);
1879     markerCrosshair.setY(y);
1880 
1881     updateFrame();
1882 }
1883 
setTrackingBoxEnabled(bool enable)1884 void FITSView::setTrackingBoxEnabled(bool enable)
1885 {
1886     if (enable != trackingBoxEnabled)
1887     {
1888         trackingBoxEnabled = enable;
1889         //updateFrame();
1890     }
1891 }
1892 
wheelEvent(QWheelEvent * event)1893 void FITSView::wheelEvent(QWheelEvent * event)
1894 {
1895     //This attempts to send the wheel event back to the Scroll Area if it was taken from a trackpad
1896     //It should still do the zoom if it is a mouse wheel
1897     if (event->source() == Qt::MouseEventSynthesizedBySystem)
1898     {
1899         QScrollArea::wheelEvent(event);
1900     }
1901     else
1902     {
1903         QPoint mouseCenter = getImagePoint(event->pos());
1904         if (event->angleDelta().y() > 0)
1905             ZoomIn();
1906         else
1907             ZoomOut();
1908         event->accept();
1909         cleanUpZoom(mouseCenter);
1910     }
1911 }
1912 
1913 /**
1914 This method is intended to keep key locations in an image centered on the screen while zooming.
1915 If there is a marker or tracking box, it centers on those.  If not, it uses the point called
1916 viewCenter that was passed as a parameter.
1917  */
1918 
cleanUpZoom(QPoint viewCenter)1919 void FITSView::cleanUpZoom(QPoint viewCenter)
1920 {
1921     int x0       = 0;
1922     int y0       = 0;
1923     double scale = (currentZoom / ZOOM_DEFAULT);
1924     if (!markerCrosshair.isNull())
1925     {
1926         x0 = markerCrosshair.x() * scale;
1927         y0 = markerCrosshair.y() * scale;
1928     }
1929     else if (trackingBoxEnabled)
1930     {
1931         x0 = trackingBox.center().x() * scale;
1932         y0 = trackingBox.center().y() * scale;
1933     }
1934     else if (!viewCenter.isNull())
1935     {
1936         x0 = viewCenter.x() * scale;
1937         y0 = viewCenter.y() * scale;
1938     }
1939     if ((x0 != 0) || (y0 != 0))
1940         ensureVisible(x0, y0, width() / 2, height() / 2);
1941     updateMouseCursor();
1942 }
1943 
1944 /**
1945 This method converts a point from the ViewPort Coordinate System to the
1946 Image Coordinate System.
1947  */
1948 
getImagePoint(QPoint viewPortPoint)1949 QPoint FITSView::getImagePoint(QPoint viewPortPoint)
1950 {
1951     QWidget * w = widget();
1952 
1953     if (w == nullptr)
1954         return QPoint(0, 0);
1955 
1956     double scale       = (currentZoom / ZOOM_DEFAULT);
1957     QPoint widgetPoint = w->mapFromParent(viewPortPoint);
1958     QPoint imagePoint  = QPoint(widgetPoint.x() / scale, widgetPoint.y() / scale);
1959     return imagePoint;
1960 }
1961 
initDisplayImage()1962 void FITSView::initDisplayImage()
1963 {
1964     // Account for leftover when sampling. Thus a 5-wide image sampled by 2
1965     // would result in a width of 3 (samples 0, 2 and 4).
1966     int w = (m_ImageData->width() + m_PreviewSampling - 1) / m_PreviewSampling;
1967     int h = (m_ImageData->height() + m_PreviewSampling - 1) / m_PreviewSampling;
1968 
1969     if (m_ImageData->channels() == 1)
1970     {
1971         rawImage = QImage(w, h, QImage::Format_Indexed8);
1972 
1973         rawImage.setColorCount(256);
1974         for (int i = 0; i < 256; i++)
1975             rawImage.setColor(i, qRgb(i, i, i));
1976     }
1977     else
1978     {
1979         rawImage = QImage(w, h, QImage::Format_RGB32);
1980     }
1981 }
1982 
1983 /**
1984 The Following two methods allow gestures to work with trackpads.
1985 Specifically, we are targeting the pinch events, so that if one is generated,
1986 Then the pinchTriggered method will be called.  If the event is not a pinch gesture,
1987 then the event is passed back to the other event handlers.
1988  */
1989 
event(QEvent * event)1990 bool FITSView::event(QEvent * event)
1991 {
1992     if (event->type() == QEvent::Gesture)
1993         return gestureEvent(dynamic_cast<QGestureEvent *>(event));
1994     return QScrollArea::event(event);
1995 }
1996 
gestureEvent(QGestureEvent * event)1997 bool FITSView::gestureEvent(QGestureEvent * event)
1998 {
1999     if (QGesture * pinch = event->gesture(Qt::PinchGesture))
2000         pinchTriggered(dynamic_cast<QPinchGesture *>(pinch));
2001     return true;
2002 }
2003 
2004 /**
2005 This Method works with Trackpads to use the pinch gesture to scroll in and out
2006 It stores a point to keep track of the location where the gesture started so that
2007 while you are zooming, it tries to keep that initial point centered in the view.
2008 **/
pinchTriggered(QPinchGesture * gesture)2009 void FITSView::pinchTriggered(QPinchGesture * gesture)
2010 {
2011     if (!zooming)
2012     {
2013         zoomLocation = getImagePoint(mapFromGlobal(QCursor::pos()));
2014         zooming      = true;
2015     }
2016     if (gesture->state() == Qt::GestureFinished)
2017     {
2018         zooming = false;
2019     }
2020     zoomTime++;           //zoomTime is meant to slow down the zooming with a pinch gesture.
2021     if (zoomTime > 10000) //This ensures zoomtime never gets too big.
2022         zoomTime = 0;
2023     if (zooming && (zoomTime % 10 == 0)) //zoomTime is set to slow it by a factor of 10.
2024     {
2025         if (gesture->totalScaleFactor() > 1)
2026             ZoomIn();
2027         else
2028             ZoomOut();
2029     }
2030     cleanUpZoom(zoomLocation);
2031 }
2032 
2033 /*void FITSView::handleWCSCompletion()
2034 {
2035     //bool hasWCS = wcsWatcher.result();
2036     if(m_ImageData->hasWCS())
2037         this->updateFrame();
2038     emit wcsToggled(m_ImageData->hasWCS());
2039 }*/
2040 
syncWCSState()2041 void FITSView::syncWCSState()
2042 {
2043     bool hasWCS    = m_ImageData->hasWCS();
2044     bool wcsLoaded = m_ImageData->getWCSState() == FITSData::Success;
2045 
2046 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2047     if (showObjects)
2048         m_ImageData->searchObjects();
2049 #endif
2050 
2051     if (hasWCS && wcsLoaded)
2052         this->updateFrame();
2053 
2054     emit wcsToggled(hasWCS);
2055 
2056     if (toggleEQGridAction != nullptr)
2057         toggleEQGridAction->setEnabled(hasWCS);
2058     if (toggleObjectsAction != nullptr)
2059         toggleObjectsAction->setEnabled(hasWCS);
2060     if (centerTelescopeAction != nullptr)
2061         centerTelescopeAction->setEnabled(hasWCS);
2062 }
2063 
createFloatingToolBar()2064 void FITSView::createFloatingToolBar()
2065 {
2066     if (floatingToolBar != nullptr)
2067         return;
2068 
2069     floatingToolBar             = new QToolBar(this);
2070     auto * eff = new QGraphicsOpacityEffect(this);
2071     floatingToolBar->setGraphicsEffect(eff);
2072     eff->setOpacity(0.2);
2073     floatingToolBar->setVisible(false);
2074     floatingToolBar->setStyleSheet(
2075         "QToolBar{background: rgba(150, 150, 150, 210); border:none; color: yellow}"
2076         "QToolButton{background: transparent; border:none; color: yellow}"
2077         "QToolButton:hover{background: rgba(200, 200, 200, 255);border:solid; color: yellow}"
2078         "QToolButton:checked{background: rgba(110, 110, 110, 255);border:solid; color: yellow}");
2079     floatingToolBar->setFloatable(true);
2080     floatingToolBar->setIconSize(QSize(25, 25));
2081     //floatingToolBar->setMovable(true);
2082 
2083     QAction * action = nullptr;
2084 
2085     floatingToolBar->addAction(QIcon::fromTheme("zoom-in"),
2086                                i18n("Zoom In"), this, SLOT(ZoomIn()));
2087 
2088     floatingToolBar->addAction(QIcon::fromTheme("zoom-out"),
2089                                i18n("Zoom Out"), this, SLOT(ZoomOut()));
2090 
2091     floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-best"),
2092                                i18n("Default Zoom"), this, SLOT(ZoomDefault()));
2093 
2094     floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-width"),
2095                                i18n("Zoom to Fit"), this, SLOT(ZoomToFit()));
2096 
2097     toggleStretchAction = floatingToolBar->addAction(QIcon::fromTheme("transform-move"),
2098                           i18n("Toggle Stretch"),
2099                           this, SLOT(toggleStretch()));
2100     toggleStretchAction->setCheckable(true);
2101 
2102 
2103     floatingToolBar->addSeparator();
2104 
2105     action = floatingToolBar->addAction(QIcon::fromTheme("crosshairs"),
2106                                         i18n("Show Cross Hairs"), this, SLOT(toggleCrosshair()));
2107     action->setCheckable(true);
2108 
2109     action = floatingToolBar->addAction(QIcon::fromTheme("map-flat"),
2110                                         i18n("Show Pixel Gridlines"), this, SLOT(togglePixelGrid()));
2111     action->setCheckable(true);
2112 
2113     toggleStarsAction =
2114         floatingToolBar->addAction(QIcon::fromTheme("kstars_stars"),
2115                                    i18n("Detect Stars in Image"), this, SLOT(toggleStars()));
2116     toggleStarsAction->setCheckable(true);
2117 
2118 #ifdef HAVE_DATAVISUALIZATION
2119     toggleProfileAction =
2120         floatingToolBar->addAction(QIcon::fromTheme("star-profile", QIcon(":/icons/star_profile.svg")),
2121                                    i18n("View Star Profile"), this, SLOT(toggleStarProfile()));
2122     toggleProfileAction->setCheckable(true);
2123 #endif
2124 
2125     if (mode == FITS_NORMAL || mode == FITS_ALIGN)
2126     {
2127         floatingToolBar->addSeparator();
2128 
2129         toggleEQGridAction =
2130             floatingToolBar->addAction(QIcon::fromTheme("kstars_grid"),
2131                                        i18n("Show Equatorial Gridlines"), this, SLOT(toggleEQGrid()));
2132         toggleEQGridAction->setCheckable(true);
2133         toggleEQGridAction->setEnabled(false);
2134 
2135         toggleObjectsAction =
2136             floatingToolBar->addAction(QIcon::fromTheme("help-hint"),
2137                                        i18n("Show Objects in Image"), this, SLOT(toggleObjects()));
2138         toggleObjectsAction->setCheckable(true);
2139         toggleEQGridAction->setEnabled(false);
2140 
2141         centerTelescopeAction =
2142             floatingToolBar->addAction(QIcon::fromTheme("center_telescope", QIcon(":/icons/center_telescope.svg")),
2143                                        i18n("Center Telescope"), this, SLOT(centerTelescope()));
2144         centerTelescopeAction->setCheckable(true);
2145         centerTelescopeAction->setEnabled(false);
2146     }
2147 }
2148 
2149 /**
2150  This method either enables or disables the scope mouse mode so you can slew your scope to coordinates
2151  just by clicking the mouse on a spot in the image.
2152  */
2153 
centerTelescope()2154 void FITSView::centerTelescope()
2155 {
2156     if (imageHasWCS())
2157     {
2158         if (getCursorMode() == FITSView::scopeCursor)
2159         {
2160             setCursorMode(lastMouseMode);
2161         }
2162         else
2163         {
2164             lastMouseMode = getCursorMode();
2165             setCursorMode(FITSView::scopeCursor);
2166         }
2167         updateFrame();
2168     }
2169     updateScopeButton();
2170 }
2171 
updateScopeButton()2172 void FITSView::updateScopeButton()
2173 {
2174     if (centerTelescopeAction != nullptr)
2175     {
2176         if (getCursorMode() == FITSView::scopeCursor)
2177         {
2178             centerTelescopeAction->setChecked(true);
2179         }
2180         else
2181         {
2182             centerTelescopeAction->setChecked(false);
2183         }
2184     }
2185 }
2186 
2187 /**
2188 This method just verifies if INDI is online, a telescope present, and is connected
2189  */
2190 
isTelescopeActive()2191 bool FITSView::isTelescopeActive()
2192 {
2193 #ifdef HAVE_INDI
2194     if (INDIListener::Instance()->size() == 0)
2195     {
2196         return false;
2197     }
2198 
2199     foreach (ISD::GDInterface * gd, INDIListener::Instance()->getDevices())
2200     {
2201         INDI::BaseDevice * bd = gd->getBaseDevice();
2202 
2203         if (gd->getType() != KSTARS_TELESCOPE)
2204             continue;
2205 
2206         if (bd == nullptr)
2207             continue;
2208 
2209         return bd->isConnected();
2210     }
2211     return false;
2212 #else
2213     return false;
2214 #endif
2215 }
2216 
setStarsEnabled(bool enable)2217 void FITSView::setStarsEnabled(bool enable)
2218 {
2219     markStars = enable;
2220     if (floatingToolBar != nullptr)
2221     {
2222         foreach (QAction * action, floatingToolBar->actions())
2223         {
2224             if (action->text() == i18n("Detect Stars in Image"))
2225             {
2226                 action->setChecked(markStars);
2227                 break;
2228             }
2229         }
2230     }
2231 }
2232 
setStarsHFREnabled(bool enable)2233 void FITSView::setStarsHFREnabled(bool enable)
2234 {
2235     showStarsHFR = enable;
2236 }
2237