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 ¶ms)
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(¶m);
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(¶m);
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