Kstars

fitsview.cpp
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 "hips/hipsfinder.h"
14#include "kstarsdata.h"
15
16#include "ksutils.h"
17#include "Options.h"
18#include "skymap.h"
19
20#include "stretch.h"
21
22#ifdef HAVE_STELLARSOLVER
23#include "ekos/auxiliary/stellarsolverprofileeditor.h"
24#endif
25
26#ifdef HAVE_INDI
27#include "basedevice.h"
28#include "indi/indilistener.h"
29#endif
30
31#include <KActionCollection>
32#include <KLocalizedString>
33
34#include <QtConcurrent>
35#include <QScrollBar>
36#include <QToolBar>
37#include <QGraphicsOpacityEffect>
38#include <QApplication>
39#include <QImageReader>
40#include <QGestureEvent>
41#include <QMutexLocker>
42#include <QElapsedTimer>
43
44#ifndef _WIN32
45#include <unistd.h>
46#endif
47
48#define BASE_OFFSET 0
49#define ZOOM_DEFAULT 100.0f
50#define ZOOM_MIN 10
51// ZOOM_MAX is adjusted in the constructor if the amount of physical memory is known.
52#define ZOOM_MAX 300
53#define ZOOM_LOW_INCR 10
54#define ZOOM_HIGH_INCR 50
55#define FONT_SIZE 14
56
57namespace
58{
59
60// Derive the Green and Blue stretch parameters from their previous values and the
61// changes made to the Red parameters. We apply the same offsets used for Red to the
62// other channels' parameters, but clip them.
63void ComputeGBStretchParams(const StretchParams &newParams, StretchParams* params)
64{
65 float shadow_diff = newParams.grey_red.shadows - params->grey_red.shadows;
66 float highlight_diff = newParams.grey_red.highlights - params->grey_red.highlights;
67 float midtones_diff = newParams.grey_red.midtones - params->grey_red.midtones;
68
69 params->green.shadows = params->green.shadows + shadow_diff;
70 params->green.shadows = KSUtils::clamp(params->green.shadows, 0.0f, 1.0f);
71 params->green.highlights = params->green.highlights + highlight_diff;
72 params->green.highlights = KSUtils::clamp(params->green.highlights, 0.0f, 1.0f);
73 params->green.midtones = params->green.midtones + midtones_diff;
74 params->green.midtones = std::max(params->green.midtones, 0.0f);
75
76 params->blue.shadows = params->blue.shadows + shadow_diff;
77 params->blue.shadows = KSUtils::clamp(params->blue.shadows, 0.0f, 1.0f);
78 params->blue.highlights = params->blue.highlights + highlight_diff;
79 params->blue.highlights = KSUtils::clamp(params->blue.highlights, 0.0f, 1.0f);
80 params->blue.midtones = params->blue.midtones + midtones_diff;
81 params->blue.midtones = std::max(params->blue.midtones, 0.0f);
82}
83
84} // namespace
85
86// Runs the stretch checking the variables to see which parameters to use.
87// We call stretch even if we're not stretching, as the stretch code still
88// converts the image to the uint8 output image which will be displayed.
89// In that case, it will use an identity stretch.
90void FITSView::doStretch(QImage *outputImage)
91{
92 if (outputImage->isNull() || m_ImageData.isNull())
93 return;
94 Stretch stretch(static_cast<int>(m_ImageData->width()),
95 static_cast<int>(m_ImageData->height()),
96 m_ImageData->channels(), m_ImageData->dataType());
97
98 StretchParams tempParams;
99 if (!stretchImage)
100 tempParams = StretchParams(); // Keeping it linear
101 else if (autoStretch)
102 {
103 // Compute new auto-stretch params.
104 stretchParams = stretch.computeParams(m_ImageData->getImageBuffer());
105 emit newStretch(stretchParams);
106 tempParams = stretchParams;
107 }
108 else
109 // Use the existing stretch params.
110 tempParams = stretchParams;
111
112 stretch.setParams(tempParams);
113 stretch.run(m_ImageData->getImageBuffer(), outputImage, m_PreviewSampling);
114}
115
116// Store stretch parameters, and turn on stretching if it isn't already on.
117void FITSView::setStretchParams(const StretchParams &params)
118{
119 if (m_ImageData->channels() == 3)
120 ComputeGBStretchParams(params, &stretchParams);
121
122 stretchParams.grey_red = params.grey_red;
123 stretchParams.grey_red.shadows = std::max(stretchParams.grey_red.shadows, 0.0f);
124 stretchParams.grey_red.highlights = std::max(stretchParams.grey_red.highlights, 0.0f);
125 stretchParams.grey_red.midtones = std::max(stretchParams.grey_red.midtones, 0.0f);
126
127 autoStretch = false;
128 stretchImage = true;
129
130 if (m_ImageFrame && rescale(ZOOM_KEEP_LEVEL))
131 {
132 m_QueueUpdate = true;
133 updateFrame(true);
134 }
135}
136
137// Turn on or off stretching, and if on, use whatever parameters are currently stored.
138void FITSView::setStretch(bool onOff)
139{
140 if (stretchImage != onOff)
141 {
142 stretchImage = onOff;
143 if (m_ImageFrame && rescale(ZOOM_KEEP_LEVEL))
144 {
145 m_QueueUpdate = true;
146 updateFrame(true);
147 }
148 }
149}
150
151// Turn on stretching, using automatically generated parameters.
152void FITSView::setAutoStretchParams()
153{
154 stretchImage = true;
155 autoStretch = true;
156 if (m_ImageFrame && rescale(ZOOM_KEEP_LEVEL))
157 {
158 m_QueueUpdate = true;
159 updateFrame(true);
160 }
161}
162
163FITSView::FITSView(QWidget * parent, FITSMode fitsMode, FITSScale filterType) : QScrollArea(parent), m_ZoomFactor(1.2)
164{
165 static const QRegularExpression re("[-{}]");
166
167 // Set UUID for each view
169 uuid = uuid.remove(re);
170 setObjectName(uuid);
171
172 // stretchImage is whether to stretch or not--the stretch may or may not use automatically generated parameters.
173 // The user may enter his/her own.
174 stretchImage = Options::autoStretch();
175 // autoStretch means use automatically-generated parameters. This is the default, unless the user overrides
176 // by adjusting the stretchBar's sliders.
177 autoStretch = true;
178
179 // Adjust the maximum zoom according to the amount of memory.
180 // There have been issues with users running out system memory because of zoom memory.
181 // Note: this is not currently image dependent. It's possible, but not implemented,
182 // to allow for more zooming on smaller images.
183 zoomMax = ZOOM_MAX;
184
185#if defined (Q_OS_LINUX) || defined (Q_OS_MACOS)
186 const long numPages = sysconf(_SC_PAGESIZE);
187 const long pageSize = sysconf(_SC_PHYS_PAGES);
188
189 // _SC_PHYS_PAGES "may not be standard" http://man7.org/linux/man-pages/man3/sysconf.3.html
190 // If an OS doesn't support it, sysconf should return -1.
191 if (numPages > 0 && pageSize > 0)
192 {
193 // (numPages * pageSize) will likely overflow a 32bit int, so use floating point calculations.
194 const int memoryMb = numPages * (static_cast<double>(pageSize) / 1e6);
195 if (memoryMb < 2000)
196 zoomMax = 100;
197 else if (memoryMb < 4000)
198 zoomMax = 200;
199 else if (memoryMb < 8000)
200 zoomMax = 300;
201 else if (memoryMb < 16000)
202 zoomMax = 400;
203 else
204 zoomMax = 600;
205 }
206#endif
207
208 grabGesture(Qt::PinchGesture);
209
210 filter = filterType;
211 mode = fitsMode;
212
213 setBackgroundRole(QPalette::Dark);
214
215 markerCrosshair.setX(0);
216 markerCrosshair.setY(0);
217
218 setBaseSize(740, 530);
219
220 m_ImageFrame = new FITSLabel(this);
221 m_ImageFrame->setMouseTracking(true);
222 connect(m_ImageFrame, &FITSLabel::newStatus, this, &FITSView::newStatus);
223 connect(m_ImageFrame, &FITSLabel::mouseOverPixel, this, &FITSView::mouseOverPixel);
224 connect(m_ImageFrame, &FITSLabel::pointSelected, this, &FITSView::processPointSelection);
225 connect(m_ImageFrame, &FITSLabel::markerSelected, this, &FITSView::processMarkerSelection);
226 connect(m_ImageFrame, &FITSLabel::rectangleSelected, this, &FITSView::processRectangle);
227 connect(m_ImageFrame, &FITSLabel::highlightSelected, this, &FITSView::processHighlight);
228 connect(m_ImageFrame, &FITSLabel::circleSelected, this, &FITSView::processCircle);
229 connect(this, &FITSView::setRubberBand, m_ImageFrame, &FITSLabel::setRubberBand);
230 connect(this, &FITSView::showRubberBand, m_ImageFrame, &FITSLabel::showRubberBand);
231 connect(this, &FITSView::zoomRubberBand, m_ImageFrame, &FITSLabel::zoomRubberBand);
232
233 connect(Options::self(), &Options::HIPSOpacityChanged, this, [this]()
234 {
235 if (showHiPSOverlay)
236 {
237 m_QueueUpdate = true;
238 updateFrame();
239 }
240 });
241 connect(Options::self(), &Options::HIPSOffsetXChanged, this, [this]()
242 {
243 if (showHiPSOverlay)
244 {
245 m_QueueUpdate = true;
246 m_HiPSOverlayPixmap = QPixmap();
247 updateFrame();
248 }
249 });
250 connect(Options::self(), &Options::HIPSOffsetYChanged, this, [this]()
251 {
252 if (showHiPSOverlay)
253 {
254 m_QueueUpdate = true;
255 m_HiPSOverlayPixmap = QPixmap();
256 updateFrame();
257 }
258 });
259
260 connect(&wcsWatcher, &QFutureWatcher<bool>::finished, this, &FITSView::syncWCSState);
261
262 m_UpdateFrameTimer.setInterval(50);
263 m_UpdateFrameTimer.setSingleShot(true);
264 connect(&m_UpdateFrameTimer, &QTimer::timeout, this, [this]()
265 {
266 this->updateFrame(true);
267 });
268
269 connect(&fitsWatcher, &QFutureWatcher<bool>::finished, this, &FITSView::loadInFrame);
270
271 setCursorMode(
272 selectCursor); //This is the default mode because the Focus and Align FitsViews should not be in dragMouse mode
273
274 noImageLabel = new QLabel();
275 noImage.load(":/images/noimage.png");
276 noImageLabel->setPixmap(noImage);
277 noImageLabel->setAlignment(Qt::AlignCenter);
278 setWidget(noImageLabel);
279
280 redScopePixmap = QPixmap(":/icons/center_telescope_red.svg").scaled(32, 32, Qt::KeepAspectRatio, Qt::FastTransformation);
281 magentaScopePixmap = QPixmap(":/icons/center_telescope_magenta.svg").scaled(32, 32, Qt::KeepAspectRatio,
283
284 // Hack! This is same initialization as roiRB in FITSLabel.
285 // Otherwise initial ROI selection wouldn't have stats.
286 selectionRectangleRaw = QRect(QPoint(1, 1), QPoint(100, 100));
287}
288
289FITSView::~FITSView()
290{
291 QMutexLocker locker(&updateMutex);
292 m_UpdateFrameTimer.stop();
293 m_Suspended = true;
294 fitsWatcher.waitForFinished();
295 wcsWatcher.waitForFinished();
296}
297
298/**
299This method looks at what mouse mode is currently selected and updates the cursor to match.
300 */
301
302void FITSView::updateMouseCursor()
303{
304 if (cursorMode == dragCursor)
305 {
306 if (horizontalScrollBar()->maximum() > 0 || verticalScrollBar()->maximum() > 0)
307 {
308 if (!m_ImageFrame->getMouseButtonDown())
310 else
312 }
313 else
315 }
316 else if (cursorMode == selectCursor)
317 {
319 }
320 else if (cursorMode == scopeCursor)
321 {
322 viewport()->setCursor(QCursor(redScopePixmap, 10, 10));
323 }
324 else if (cursorMode == crosshairCursor)
325 {
326 viewport()->setCursor(QCursor(magentaScopePixmap, 10, 10));
327 }
328}
329
330/**
331This is how the mouse mode gets set.
332The default for a FITSView in a FITSViewer should be the dragMouse
333The default for a FITSView in the Focus or Align module should be the selectMouse
334The different defaults are accomplished by putting making the actual default mouseMode
335the selectMouse, but when a FITSViewer loads an image, it immediately makes it the dragMouse.
336 */
337
338void FITSView::setCursorMode(CursorMode mode)
339{
340 cursorMode = mode;
341 updateMouseCursor();
342
343 if (mode == scopeCursor && imageHasWCS())
344 {
345 if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
346 {
347#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
348 QFuture<bool> future = QtConcurrent::run(&FITSData::loadWCS, m_ImageData.data());
349#else
350 QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS);
351#endif
352 wcsWatcher.setFuture(future);
353 }
354 }
355}
356
357void FITSView::resizeEvent(QResizeEvent * event)
358{
359 if (m_ImageData == nullptr && noImageLabel != nullptr)
360 {
361 noImageLabel->setPixmap(
362 noImage.scaled(width() - 20, height() - 20, Qt::KeepAspectRatio, Qt::FastTransformation));
363 noImageLabel->setFixedSize(width() - 5, height() - 5);
364 }
365
367}
368
369
370void FITSView::loadFile(const QString &inFilename)
371{
372 if (floatingToolBar != nullptr)
373 {
374 floatingToolBar->setVisible(true);
375 }
376
377 bool setBayerParams = false;
378
379 BayerParams param;
380 if ((m_ImageData != nullptr) && m_ImageData->hasDebayer())
381 {
382 setBayerParams = true;
383 m_ImageData->getBayerParams(&param);
384 }
385
386 // In case image is still loading, wait until it is done.
387 fitsWatcher.waitForFinished();
388 // In case loadWCS is still running for previous image data, let's wait until it's over
389 wcsWatcher.waitForFinished();
390
391 // delete m_ImageData;
392 // m_ImageData = nullptr;
393
394 filterStack.clear();
395 filterStack.push(FITS_NONE);
396 if (filter != FITS_NONE)
397 filterStack.push(filter);
398
399 m_ImageData.reset(new FITSData(mode), &QObject::deleteLater);
400
401 if (setBayerParams)
402 m_ImageData->setBayerParams(&param);
403
404 fitsWatcher.setFuture(m_ImageData->loadFromFile(inFilename));
405}
406
407void FITSView::clearData()
408{
409 if (!noImageLabel)
410 {
411 noImageLabel = new QLabel();
412 noImage.load(":/images/noimage.png");
413 noImageLabel->setPixmap(noImage);
414 noImageLabel->setAlignment(Qt::AlignCenter);
415 }
416
417 setWidget(noImageLabel);
418
419 m_ImageData.clear();
420}
421
422bool FITSView::loadData(const QSharedPointer<FITSData> &data)
423{
424 if (floatingToolBar != nullptr)
425 {
426 floatingToolBar->setVisible(true);
427 }
428
429 // In case loadWCS is still running for previous image data, let's wait until it's over
430 wcsWatcher.waitForFinished();
431
432 filterStack.clear();
433 filterStack.push(FITS_NONE);
434 if (filter != FITS_NONE)
435 filterStack.push(filter);
436
437 m_HiPSOverlayPixmap = QPixmap();
438
439 // Takes control of the objects passed in.
440 m_ImageData = data;
441 // set the image mask geometry
442 if (m_ImageMask != nullptr)
443 m_ImageMask->setImageGeometry(data->width(), data->height());
444
445 if (processData())
446 {
447 emit loaded();
448 return true;
449 }
450 else
451 {
452 emit failed(m_LastError);
453 return false;
454 }
455}
456
457bool FITSView::processData()
458{
459 // Set current width and height
460 if (!m_ImageData)
461 return false;
462
463 connect(m_ImageData.data(), &FITSData::dataChanged, this, [this]()
464 {
465 rescale(ZOOM_KEEP_LEVEL);
466 updateFrame();
467 });
468
469 connect(m_ImageData.data(), &FITSData::headerChanged, this, [this]()
470 {
471 emit headerChanged();
472 });
473
474 connect(m_ImageData.data(), &FITSData::loadingCatalogData, this, [this]()
475 {
476 emit catQueried();
477 });
478
479 connect(m_ImageData.data(), &FITSData::catalogQueryFailed, this, [this](QString text)
480 {
481 emit catQueryFailed(text);
482 });
483
484 connect(m_ImageData.data(), &FITSData::loadedCatalogData, this, [this]()
485 {
486 // Signal FITSTab to load the new data in the table
487 emit catLoaded();
488 // Reprocess FITSView
489 rescale(ZOOM_KEEP_LEVEL);
490 updateFrame();
491 });
492
493 currentWidth = m_ImageData->width();
494 currentHeight = m_ImageData->height();
495
496 int image_width = currentWidth;
497 int image_height = currentHeight;
498
499 if (!m_ImageFrame)
500 {
501 m_ImageFrame = new FITSLabel(this);
502 m_ImageFrame->setMouseTracking(true);
503 connect(m_ImageFrame, &FITSLabel::newStatus, this, &FITSView::newStatus);
504 connect(m_ImageFrame, &FITSLabel::pointSelected, this, &FITSView::processPointSelection);
505 connect(m_ImageFrame, &FITSLabel::markerSelected, this, &FITSView::processMarkerSelection);
506 connect(m_ImageFrame, &FITSLabel::highlightSelected, this, &FITSView::processHighlight);
507 connect(m_ImageFrame, &FITSLabel::circleSelected, this, &FITSView::processCircle);
508 }
509 m_ImageFrame->setSize(image_width, image_height);
510
511 // Init the display image
512 // JM 2020.01.08: Disabling as proposed by Hy
513 //initDisplayImage();
514
515 m_ImageData->applyFilter(filter);
516
517 double availableRAM = 0;
518 if (Options::adaptiveSampling() && (availableRAM = KSUtils::getAvailableRAM()) > 0)
519 {
520 // Possible color maximum image size
521 double max_size = image_width * image_height * 4;
522 // Ratio of image size to available RAM size
523 double ratio = max_size / availableRAM;
524
525 // Increase adaptive sampling with more limited RAM
526 if (ratio < 0.1)
527 m_AdaptiveSampling = 1;
528 else if (ratio < 0.2)
529 m_AdaptiveSampling = 2;
530 else
531 m_AdaptiveSampling = 4;
532
533 m_PreviewSampling = m_AdaptiveSampling;
534 }
535
536 // Rescale to fits window on first load
537 if (firstLoad)
538 {
539 currentZoom = 100;
540
541 if (rescale(ZOOM_FIT_WINDOW) == false)
542 {
543 m_LastError = i18n("Rescaling image failed.");
544 return false;
545 }
546
547 firstLoad = false;
548 }
549 else
550 {
551 if (rescale(ZOOM_KEEP_LEVEL) == false)
552 {
553 m_LastError = i18n("Rescaling image failed.");
554 return false;
555 }
556 }
557
559
560 // Load WCS data now if selected and image contains valid WCS header
561 if ((mode == FITS_NORMAL || mode == FITS_ALIGN) &&
562 m_ImageData->hasWCS() && m_ImageData->getWCSState() == FITSData::Idle &&
563 Options::autoWCS() &&
564 !wcsWatcher.isRunning())
565 {
566#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
567 QFuture<bool> future = QtConcurrent::run(&FITSData::loadWCS, m_ImageData.data());
568#else
569 QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS);
570#endif
571 wcsWatcher.setFuture(future);
572 }
573 else
574 syncWCSState();
575
576 if (isVisible())
577 emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION);
578
579 if (showStarProfile)
580 {
581 if(floatingToolBar != nullptr)
582 toggleProfileAction->setChecked(true);
583 //Need to wait till the Focus module finds stars, if its the Focus module.
584 QTimer::singleShot(100, this, SLOT(viewStarProfile()));
585 }
586
587 // Fore immediate load of frame for first load.
588 m_QueueUpdate = true;
589 updateFrame(true);
590 return true;
591}
592
593void FITSView::loadInFrame()
594{
595 // It can wind up being null if the file is manually deleted.
596 if (m_ImageData.isNull())
597 {
598 emit failed("No image file.");
599 return;
600 }
601
602 m_LastError = m_ImageData->getLastError();
603
604 // Check if the loading was OK
605 if (fitsWatcher.result() == false)
606 {
607 emit failed(m_LastError);
608 return;
609 }
610
611 // Notify if there is debayer data.
612 emit debayerToggled(m_ImageData->hasDebayer());
613
614 if (processData())
615 emit loaded();
616 else
617 emit failed(m_LastError);
618}
619
620bool FITSView::saveImage(const QString &newFilename)
621{
622 const QString ext = QFileInfo(newFilename).suffix();
623 if (QImageReader::supportedImageFormats().contains(ext.toLatin1()))
624 {
625 rawImage.save(newFilename, ext.toLatin1().constData());
626 return true;
627 }
628
629 return m_ImageData->saveImage(newFilename);
630}
631
632FITSView::CursorMode FITSView::getCursorMode()
633{
634 return cursorMode;
635}
636
637#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
638void FITSView::enterEvent(QEnterEvent *event)
639#else
640void FITSView::enterEvent(QEvent * event)
641#endif
642{
643 Q_UNUSED(event)
644
645 if (floatingToolBar && m_ImageData)
646 {
648 floatingToolBar->setGraphicsEffect(eff);
649 QPointer<QPropertyAnimation> a = new QPropertyAnimation(eff, "opacity");
650 a->setDuration(500);
651 a->setStartValue(0.2);
652 a->setEndValue(1);
653 a->setEasingCurve(QEasingCurve::InBack);
655 }
656}
657
658void FITSView::leaveEvent(QEvent * event)
659{
660 Q_UNUSED(event)
661
662 if (floatingToolBar && m_ImageData)
663 {
664 QPointer<QGraphicsOpacityEffect> eff = new QGraphicsOpacityEffect(this);
665 floatingToolBar->setGraphicsEffect(eff);
666 QPointer<QPropertyAnimation> a = new QPropertyAnimation(eff, "opacity");
667 a->setDuration(500);
668 a->setStartValue(1);
669 a->setEndValue(0.2);
670 a->setEasingCurve(QEasingCurve::OutBack);
672 }
673}
674
675bool FITSView::rescale(FITSZoom type)
676{
677 if (!m_ImageData)
678 return false;
679
680 int image_width = m_ImageData->width();
681 int image_height = m_ImageData->height();
682 currentWidth = image_width;
683 currentHeight = image_height;
684
685 if (isVisible())
686 emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION);
687
688 switch (type)
689 {
690 case ZOOM_FIT_WINDOW:
691 if ((image_width > width() || image_height > height()))
692 {
693 double w = baseSize().width() - BASE_OFFSET;
694 double h = baseSize().height() - BASE_OFFSET;
695
696 if (!firstLoad)
697 {
698 w = viewport()->rect().width() - BASE_OFFSET;
699 h = viewport()->rect().height() - BASE_OFFSET;
700 }
701
702 // Find the zoom level which will enclose the current FITS in the current window size
703 double zoomX = (w / static_cast<double>(currentWidth)) * 100.0;
704 double zoomY = (h / static_cast<double>(currentHeight)) * 100.0;
705
706 (zoomX < zoomY) ? currentZoom = zoomX : currentZoom = zoomY;
707
708 currentWidth = image_width * (currentZoom / ZOOM_DEFAULT);
709 currentHeight = image_height * (currentZoom / ZOOM_DEFAULT);
710
711 if (currentZoom <= ZOOM_MIN)
712 emit actionUpdated("view_zoom_out", false);
713 }
714 else
715 {
716 currentZoom = 100;
717 currentWidth = image_width;
718 currentHeight = image_height;
719 }
720 break;
721
722 case ZOOM_KEEP_LEVEL:
723 {
724 currentWidth = image_width * (currentZoom / ZOOM_DEFAULT);
725 currentHeight = image_height * (currentZoom / ZOOM_DEFAULT);
726 }
727 break;
728
729 default:
730 currentZoom = 100;
731
732 break;
733 }
734
735 initDisplayImage();
736 m_ImageFrame->setScaledContents(true);
737 doStretch(&rawImage);
738 setWidget(m_ImageFrame);
739
740 // This is needed by fitstab, even if the zoom doesn't change, to change the stretch UI.
741 emitZoom();
742 return true;
743}
744
745void FITSView::emitZoom()
746{
747 // Don't need to display full float precision. Limit to 2 decimal places at most.
748 double zoom = std::round(currentZoom * 100.0) / 100.0;
749 emit newStatus(i18nc("%1 is the value, % is the percent sign", "%1%", zoom), FITS_ZOOM);
750}
751
752void FITSView::ZoomIn()
753{
754 if (!m_ImageData)
755 return;
756
757 if (currentZoom >= ZOOM_DEFAULT && Options::limitedResourcesMode())
758 {
759 emit newStatus(i18n("Cannot zoom in further due to active limited resources mode."), FITS_MESSAGE);
760 return;
761 }
762
763 if (currentZoom < ZOOM_DEFAULT)
764 currentZoom += ZOOM_LOW_INCR;
765 else
766 currentZoom += ZOOM_HIGH_INCR;
767
768 emit actionUpdated("view_zoom_out", true);
769 if (currentZoom >= zoomMax)
770 {
771 currentZoom = zoomMax;
772 emit actionUpdated("view_zoom_in", false);
773 }
774
775 currentWidth = m_ImageData->width() * (currentZoom / ZOOM_DEFAULT);
776 currentHeight = m_ImageData->height() * (currentZoom / ZOOM_DEFAULT);
777
778 cleanUpZoom();
779
780 updateFrame(true);
781
782 emitZoom();
783 emit zoomRubberBand(getCurrentZoom() / ZOOM_DEFAULT);
784}
785
786void FITSView::ZoomOut()
787{
788 if (!m_ImageData)
789 return;
790
791 if (currentZoom <= ZOOM_DEFAULT)
792 currentZoom -= ZOOM_LOW_INCR;
793 else
794 currentZoom -= ZOOM_HIGH_INCR;
795
796 if (currentZoom <= ZOOM_MIN)
797 {
798 currentZoom = ZOOM_MIN;
799 emit actionUpdated("view_zoom_out", false);
800 }
801
802 emit actionUpdated("view_zoom_in", true);
803
804 currentWidth = m_ImageData->width() * (currentZoom / ZOOM_DEFAULT);
805 currentHeight = m_ImageData->height() * (currentZoom / ZOOM_DEFAULT);
806
807 cleanUpZoom();
808
809 updateFrame(true);
810
811 emitZoom();
812 emit zoomRubberBand(getCurrentZoom() / ZOOM_DEFAULT);
813}
814
815void FITSView::ZoomToFit()
816{
817 if (!m_ImageData)
818 return;
819
820 if (rawImage.isNull() == false)
821 {
822 rescale(ZOOM_FIT_WINDOW);
823 updateFrame(true);
824 }
825 emit zoomRubberBand(getCurrentZoom() / ZOOM_DEFAULT);
826}
827
828
829
830int FITSView::filterStars()
831{
832 return ((m_ImageMask.isNull() == false
833 && m_ImageMask->active()) ? m_ImageData->filterStars(m_ImageMask) : m_ImageData->getStarCenters().count());
834}
835
836void FITSView::setImageMask(ImageMask *mask)
837{
838 if (m_ImageMask.isNull() == false)
839 {
840 // copy image geometry from the old mask before deleting it
841 if (mask != nullptr)
842 mask->setImageGeometry(m_ImageMask->width(), m_ImageMask->height());
843 }
844
845 m_ImageMask.reset(mask);
846}
847
848// isImageLarge() returns whether we use the large-image rendering strategy or the small-image strategy.
849// See the comment below in getScale() for details.
850bool FITSView::isLargeImage()
851{
852 constexpr int largeImageNumPixels = 1000 * 1000;
853 return rawImage.width() * rawImage.height() >= largeImageNumPixels;
854}
855
856// getScale() is related to the image and overlay rendering strategy used.
857// If we're using a pixmap appropriate for a large image, where we draw and render on a pixmap that's the image size
858// and we let the QLabel deal with scaling and zooming, then the scale is 1.0.
859// With smaller images, where memory use is not as severe, we create a pixmap that's the size of the scaled image
860// and get scale returns the ratio of that pixmap size to the image size.
861double FITSView::getScale()
862{
863 return (isLargeImage() ? 1.0 : currentZoom / ZOOM_DEFAULT) / m_PreviewSampling;
864}
865
866// scaleSize() is only used with the large-image rendering strategy. It may increase the line
867// widths or font sizes, as we draw lines and render text on the full image and when zoomed out,
868// these sizes may be too small.
869double FITSView::scaleSize(double size)
870{
871 if (!isLargeImage())
872 return size;
873 return (currentZoom > 100.0 ? size : std::round(size * 100.0 / currentZoom)) / m_PreviewSampling;
874}
875
876void FITSView::updateFrame(bool now)
877{
878 QMutexLocker locker(&updateMutex);
879
880 // Do not process if suspended.
881 if (m_Suspended)
882 return;
883
884 // JM 2021-03-13: This timer is used to throttle updateFrame calls to improve performance
885 // If after 250ms no further update frames are called, then the actual update is triggered.
886 // JM 2021-03-16: When stretching in progress, immediately execute so that the user see the changes
887 // in real time
888 if (now)
889 {
890 if (toggleStretchAction)
891 toggleStretchAction->setChecked(stretchImage);
892
893 // We employ two schemes for managing the image and its overlays, depending on the size of the image
894 // and whether we need to therefore conserve memory. The small-image strategy explicitly scales up
895 // the image, and writes overlays on the scaled pixmap. The large-image strategy uses a pixmap that's
896 // the size of the image itself, never scaling that up.
897 if (isLargeImage())
898 updateFrameLargeImage();
899 else
900 updateFrameSmallImage();
901
902 if (m_QueueUpdate && m_StretchingInProgress == false)
903 {
904 m_QueueUpdate = false;
905 emit updated();
906 }
907 }
908 else
909 m_UpdateFrameTimer.start();
910}
911
912
913bool FITSView::initDisplayPixmap(QImage &image, float scale)
914{
915 ImageMosaicMask *mask = dynamic_cast<ImageMosaicMask *>(m_ImageMask.get());
916
917 // if no mosaic should be created, simply convert the original image
918 if (mask == nullptr)
919 return displayPixmap.convertFromImage(image);
920
921 // check image geometry, sincd scaling could have changed it
922 // create the 3x3 mosaic
923 int width = mask->tileWidth() * mask->width() / 100;
924 int space = mask->space();
925 // create a new all black pixmap with mosaic size
926 displayPixmap = QPixmap((3 * width + 2 * space) * scale, (3 * width + 2 * space) * scale);
927 displayPixmap.fill(Qt::black);
928
929 QPainter painter(&displayPixmap);
930 int pos = 0;
931 // paint tiles
932 for (QRect tile : mask->tiles())
933 {
934 const int posx = pos % 3;
935 const int posy = pos++ / 3;
936 const int tilewidth = width * scale;
937 QRectF source(tile.x() * scale, tile.y()*scale, tilewidth, tilewidth);
938 QRectF target((posx * (width + space)) * scale, (posy * (width + space)) * scale, width * scale, width * scale);
939 painter.drawImage(target, image, source);
940 }
941 return true;
942}
943
944void FITSView::updateFrameLargeImage()
945{
946 if (m_ImageFrame.isNull() || !initDisplayPixmap(rawImage, 1.0 / m_PreviewSampling))
947 return;
948 QPainter painter(&displayPixmap);
949 // Possibly scale the fonts as we're drawing on the full image, not just the visible part of the scroll window.
950 QFont font = painter.font();
951 font.setPixelSize(scaleSize(FONT_SIZE));
952 painter.setFont(font);
953
954 drawStarRingFilter(&painter, 1.0 / m_PreviewSampling, dynamic_cast<ImageRingMask *>(m_ImageMask.get()));
955 drawOverlay(&painter, 1.0 / m_PreviewSampling);
956 m_ImageFrame->setPixmap(displayPixmap);
957 m_ImageFrame->resize(((m_PreviewSampling * currentZoom) / 100.0) * displayPixmap.size());
958}
959
960void FITSView::updateFrameSmallImage()
961{
962 if (m_ImageFrame.isNull())
963 return;
964 QImage scaledImage = rawImage.scaled(currentWidth, currentHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);
965 if (!initDisplayPixmap(scaledImage, currentZoom / ZOOM_DEFAULT))
966 return;
967
968 QPainter painter(&displayPixmap);
969 // Possibly scale the fonts as we're drawing on the full image, not just the visible part of the scroll window.
970 QFont font = painter.font();
971 drawStarRingFilter(&painter, currentZoom / ZOOM_DEFAULT, dynamic_cast<ImageRingMask *>(m_ImageMask.get()));
972 drawOverlay(&painter, currentZoom / ZOOM_DEFAULT);
973 m_ImageFrame->setPixmap(displayPixmap);
974 m_ImageFrame->resize(currentWidth, currentHeight);
975}
976
977void FITSView::drawStarRingFilter(QPainter *painter, double scale, ImageRingMask *ringMask)
978{
979 if (ringMask == nullptr || !ringMask->active())
980 return;
981
982 const double w = m_ImageData->width() * scale;
983 const double h = m_ImageData->height() * scale;
984 double const diagonal = std::sqrt(w * w + h * h) / 2;
985 int const innerRadius = std::lround(diagonal * ringMask->innerRadius());
986 int const outerRadius = std::lround(diagonal * ringMask->outerRadius());
987 QPoint const center(w / 2, h / 2);
988 painter->save();
989 painter->setPen(QPen(Qt::blue, scaleSize(1), Qt::DashLine));
990 painter->setOpacity(0.7);
991 painter->setBrush(QBrush(Qt::transparent));
992 painter->drawEllipse(center, outerRadius, outerRadius);
993 painter->setBrush(QBrush(Qt::blue, Qt::FDiagPattern));
994 painter->drawEllipse(center, innerRadius, innerRadius);
995 painter->restore();
996}
997
998namespace
999{
1000
1001template <typename T>
1002int drawClippingOneChannel(T *inputBuffer, QPainter *painter, int width, int height, double clipVal, double scale)
1003{
1004 int numClipped = 0;
1005 painter->save();
1006 painter->setPen(QPen(Qt::red, scale, Qt::SolidLine));
1007 const T clipping = clipVal;
1008 constexpr int timeoutMilliseconds = 3 * 1000;
1009 QElapsedTimer timer;
1010 timer.start();
1011 QPoint p;
1012 for (int y = 0; y < height; y++)
1013 {
1014 auto inputLine = inputBuffer + y * width;
1015 p.setY(y);
1016 for (int x = 0; x < width; x++)
1017 {
1018 if (*inputLine++ > clipping)
1019 {
1020 numClipped++;
1021 const int start = x;
1022 // Use this inner loop to recognize strings of clipped pixels
1023 // and draw lines instead of multiple calls to drawPoints.
1024 while (true)
1025 {
1026 if (++x >= width)
1027 {
1028 painter->drawLine(start, y, width - 1, y);
1029 break;
1030 }
1031 if (*inputLine++ > clipping)
1032 numClipped++;
1033 else
1034 {
1035 if (x == start + 1)
1036 {
1037 p.setX(start);
1038 painter->drawPoints(&p, 1);
1039 }
1040 else
1041 painter->drawLine(start, y, x - 1, y);
1042 break;
1043 }
1044 }
1045 }
1046 }
1047 if (timer.elapsed() > timeoutMilliseconds)
1048 {
1049 painter->restore();
1050 return -1;
1051 }
1052 }
1053 painter->restore();
1054 return numClipped;
1055}
1056
1057template <typename T>
1058int drawClippingThreeChannels(T *inputBuffer, QPainter *painter, int width, int height, double clipVal, double scale)
1059{
1060 int numClipped = 0;
1061 painter->save();
1062 painter->setPen(QPen(Qt::red, scale, Qt::SolidLine));
1063 const T clipping = clipVal;
1064 constexpr int timeoutMilliseconds = 3 * 1000;
1065 QElapsedTimer timer;
1066 timer.start();
1067 QPoint p;
1068 const int size = width * height;
1069 for (int y = 0; y < height; y++)
1070 {
1071 // R, G, B input images are stored one after another.
1072 const T * inputLineR = inputBuffer + y * width;
1073 const T * inputLineG = inputLineR + size;
1074 const T * inputLineB = inputLineG + size;
1075 p.setY(y);
1076
1077 for (int x = 0; x < width; x++)
1078 {
1079 T inputR = inputLineR[x];
1080 T inputG = inputLineG[x];
1081 T inputB = inputLineB[x];
1082
1083 if (inputR > clipping || inputG > clipping || inputB > clipping)
1084 {
1085 numClipped++;
1086 const int start = x;
1087
1088 // Use this inner loop to recognize strings of clipped pixels
1089 // and draw lines instead of multiple calls to drawPoints.
1090 while (true)
1091 {
1092 if (++x >= width)
1093 {
1094 painter->drawLine(start, y, width - 1, y);
1095 break;
1096 }
1097 T inputR2 = inputLineR[x];
1098 T inputG2 = inputLineG[x];
1099 T inputB2 = inputLineB[x];
1100 if (inputR2 > clipping || inputG2 > clipping || inputB2 > clipping)
1101 numClipped++;
1102 else
1103 {
1104 if (x == start + 1)
1105 {
1106 p.setX(start);
1107 painter->drawPoints(&p, 1);
1108 }
1109 else
1110 painter->drawLine(start, y, x - 1, y);
1111 break;
1112 }
1113 }
1114 }
1115 }
1116 if (timer.elapsed() > timeoutMilliseconds)
1117 {
1118 painter->restore();
1119 return -1;
1120 }
1121 }
1122 painter->restore();
1123 return numClipped;
1124}
1125
1126template <typename T>
1127int drawClip(T *input_buffer, int num_channels, QPainter *painter, int width, int height, double clipVal, double scale)
1128{
1129 if (num_channels == 1)
1130 return drawClippingOneChannel(input_buffer, painter, width, height, clipVal, scale);
1131 else if (num_channels == 3)
1132 return drawClippingThreeChannels(input_buffer, painter, width, height, clipVal, scale);
1133 else return 0;
1134}
1135
1136} // namespace
1137
1138void FITSView::drawClipping(QPainter *painter)
1139{
1140 auto input = m_ImageData->getImageBuffer();
1141 const int height = m_ImageData->height();
1142 const int width = m_ImageData->width();
1143 const double FLOAT_CLIP = Options::clipping64KValue();
1144 const double SHORT_CLIP = Options::clipping64KValue();
1145 const double USHORT_CLIP = Options::clipping64KValue();
1146 const double BYTE_CLIP = Options::clipping256Value();
1147 switch (m_ImageData->dataType())
1148 {
1149 case TBYTE:
1150 m_NumClipped = drawClip(reinterpret_cast<uint8_t const*>(input), m_ImageData->channels(), painter, width, height, BYTE_CLIP,
1151 scaleSize(1));
1152 break;
1153 case TSHORT:
1154 m_NumClipped = drawClip(reinterpret_cast<short const*>(input), m_ImageData->channels(), painter, width, height, SHORT_CLIP,
1155 scaleSize(1));
1156 break;
1157 case TUSHORT:
1158 m_NumClipped = drawClip(reinterpret_cast<unsigned short const*>(input), m_ImageData->channels(), painter, width, height,
1159 USHORT_CLIP,
1160 scaleSize(1));
1161 break;
1162 case TLONG:
1163 m_NumClipped = drawClip(reinterpret_cast<long const*>(input), m_ImageData->channels(), painter, width, height, USHORT_CLIP,
1164 scaleSize(1));
1165 break;
1166 case TFLOAT:
1167 m_NumClipped = drawClip(reinterpret_cast<float const*>(input), m_ImageData->channels(), painter, width, height, FLOAT_CLIP,
1168 scaleSize(1));
1169 break;
1170 case TLONGLONG:
1171 m_NumClipped = drawClip(reinterpret_cast<long long const*>(input), m_ImageData->channels(), painter, width, height,
1172 USHORT_CLIP,
1173 scaleSize(1));
1174 break;
1175 case TDOUBLE:
1176 m_NumClipped = drawClip(reinterpret_cast<double const*>(input), m_ImageData->channels(), painter, width, height, FLOAT_CLIP,
1177 scaleSize(1));
1178 break;
1179 default:
1180 m_NumClipped = 0;
1181 break;
1182 }
1183 if (m_NumClipped < 0)
1184 emit newStatus(QString("Clip:failed"), FITS_CLIP);
1185 else
1186 emit newStatus(QString("Clip:%1").arg(m_NumClipped), FITS_CLIP);
1187}
1188
1189void FITSView::ZoomDefault()
1190{
1191 if (m_ImageFrame)
1192 {
1193 emit actionUpdated("view_zoom_out", true);
1194 emit actionUpdated("view_zoom_in", true);
1195
1196 currentZoom = ZOOM_DEFAULT;
1197 currentWidth = m_ImageData->width();
1198 currentHeight = m_ImageData->height();
1199
1200 updateFrame();
1201
1202 emitZoom();
1203
1204 update();
1205 }
1206}
1207
1208void FITSView::drawOverlay(QPainter * painter, double scale)
1209{
1210 painter->setRenderHint(QPainter::Antialiasing, Options::useAntialias());
1211
1212#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1213 if (showHiPSOverlay)
1214 drawHiPSOverlay(painter, scale);
1215#endif
1216
1217 if (trackingBoxEnabled && getCursorMode() != FITSView::scopeCursor)
1218 drawTrackingBox(painter, scale);
1219
1220 if (!markerCrosshair.isNull())
1221 drawMarker(painter, scale);
1222
1223 if (showCrosshair)
1224 drawCrosshair(painter, scale);
1225
1226 if (showObjects)
1227 drawObjectNames(painter, scale);
1228
1229#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1230 if (showEQGrid)
1231 drawEQGrid(painter, scale);
1232#endif
1233
1234 if (showPixelGrid)
1235 drawPixelGrid(painter, scale);
1236
1237 if (markStars)
1238 drawStarCentroid(painter, scale);
1239
1240 if (showClipping)
1241 drawClipping(painter);
1242
1243 if (showMagnifyingGlass)
1244 drawMagnifyingGlass(painter, scale);
1245}
1246
1247// Draws a 100% resolution image rectangle around the mouse position.
1248void FITSView::drawMagnifyingGlass(QPainter *painter, double scale)
1249{
1250 if (magnifyingGlassX >= 0 && magnifyingGlassY >= 0 &&
1251 magnifyingGlassX < m_ImageData->width() &&
1252 magnifyingGlassY < m_ImageData->height())
1253 {
1254 // Amount of magnification.
1255 constexpr double magAmount = 8;
1256 // Desired size in pixels of the magnification window.
1257 constexpr int magWindowSize = 130;
1258 // The distance from the mouse position to the magnifying glass rectangle, in the source image coordinates.
1259 const int winXOffset = magWindowSize * 10.0 / currentZoom;
1260 const int winYOffset = magWindowSize * 10.0 / currentZoom;
1261 // Size of a side of the square of input to make a window that size.
1262 const int inputDimension = magWindowSize * 100 / currentZoom;
1263 // Size of the square drawn. Not the same, necessarily as the magWindowSize,
1264 // since the output may be scaled (if isLargeImage()==true) to become screen pixels.
1265 const int outputDimension = inputDimension * scale + .99;
1266
1267 // Where the source data (to be magnified) comes from.
1268 int imgLeft = magnifyingGlassX - inputDimension / (2 * magAmount);
1269 int imgTop = magnifyingGlassY - inputDimension / (2 * magAmount);
1270
1271 // Where we'll draw the magnifying glass rectangle.
1272 int winLeft = magnifyingGlassX + winXOffset;
1273 int winTop = magnifyingGlassY + winYOffset;
1274
1275 // Normally we place the magnifying glass rectangle to the right and below the mouse curson.
1276 // However, if it would be rendered outside the image, put it on the other side.
1277 int w = rawImage.width();
1278 int h = rawImage.height();
1279 const int rightLimit = std::min(w, static_cast<int>((horizontalScrollBar()->value() + width()) * 100 / currentZoom));
1280 const int bottomLimit = std::min(h, static_cast<int>((verticalScrollBar()->value() + height()) * 100 / currentZoom));
1281 if (winLeft + winXOffset + inputDimension > rightLimit)
1282 winLeft -= (2 * winXOffset + inputDimension);
1283 if (winTop + winYOffset + inputDimension > bottomLimit)
1284 winTop -= (2 * winYOffset + inputDimension);
1285
1286 // Blacken the output where magnifying outside the source image.
1287 if ((imgLeft < 0 ) ||
1288 (imgLeft + inputDimension / magAmount >= w) ||
1289 (imgTop < 0) ||
1290 (imgTop + inputDimension / magAmount > h))
1291 {
1292 painter->setBrush(QBrush(Qt::black));
1293 painter->drawRect(winLeft * scale, winTop * scale, outputDimension, outputDimension);
1294 painter->setBrush(QBrush(Qt::transparent));
1295 }
1296
1297 // Finally, draw the magnified image.
1298 painter->drawPixmap(QRect(winLeft * scale, winTop * scale, outputDimension, outputDimension),
1299 displayPixmap,
1300 QRect(imgLeft, imgTop, inputDimension / magAmount, inputDimension / magAmount));
1301 // Draw a white border.
1302 painter->setPen(QPen(Qt::white, scaleSize(1)));
1303 painter->drawRect(winLeft * scale, winTop * scale, outputDimension, outputDimension);
1304 }
1305}
1306
1307// x,y are the image coordinates where the magnifying glass is positioned.
1308void FITSView::updateMagnifyingGlass(int x, int y)
1309{
1310 if (!m_ImageData)
1311 return;
1312
1313 magnifyingGlassX = x;
1314 magnifyingGlassY = y;
1315 if (magnifyingGlassX == -1 && magnifyingGlassY == -1)
1316 {
1317 if (showMagnifyingGlass)
1318 updateFrame(true);
1319 showMagnifyingGlass = false;
1320 }
1321 else
1322 {
1323 showMagnifyingGlass = true;
1324 updateFrame(true);
1325 }
1326}
1327
1328void FITSView::updateMode(FITSMode fmode)
1329{
1330 mode = fmode;
1331}
1332
1333void FITSView::drawMarker(QPainter * painter, double scale)
1334{
1335 painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")),
1336 scaleSize(2)));
1337 painter->setBrush(Qt::NoBrush);
1338 const float pxperdegree = scale * (57.3 / 1.8);
1339
1340 const float s1 = 0.5 * pxperdegree;
1341 const float s2 = pxperdegree;
1342 const float s3 = 2.0 * pxperdegree;
1343
1344 const float x0 = scale * markerCrosshair.x();
1345 const float y0 = scale * markerCrosshair.y();
1346 const float x1 = x0 - 0.5 * s1;
1347 const float y1 = y0 - 0.5 * s1;
1348 const float x2 = x0 - 0.5 * s2;
1349 const float y2 = y0 - 0.5 * s2;
1350 const float x3 = x0 - 0.5 * s3;
1351 const float y3 = y0 - 0.5 * s3;
1352
1353 //Draw radial lines
1354 painter->drawLine(QPointF(x1, y0), QPointF(x3, y0));
1355 painter->drawLine(QPointF(x0 + s2, y0), QPointF(x0 + 0.5 * s1, y0));
1356 painter->drawLine(QPointF(x0, y1), QPointF(x0, y3));
1357 painter->drawLine(QPointF(x0, y0 + 0.5 * s1), QPointF(x0, y0 + s2));
1358 //Draw circles at 0.5 & 1 degrees
1359 painter->drawEllipse(QRectF(x1, y1, s1, s1));
1360 painter->drawEllipse(QRectF(x2, y2, s2, s2));
1361}
1362
1363bool FITSView::drawHFR(QPainter * painter, const QString &hfr, int x, int y)
1364{
1365 QRect const boundingRect(0, 0, painter->device()->width(), painter->device()->height());
1366 QSize const hfrSize = painter->fontMetrics().size(Qt::TextSingleLine, hfr);
1367
1368 // Store the HFR text in a rect
1369 QPoint const hfrBottomLeft(x, y);
1370 QRect const hfrRect(hfrBottomLeft.x(), hfrBottomLeft.y() - hfrSize.height(), hfrSize.width(), hfrSize.height());
1371
1372 // Render the HFR text only if it can be displayed entirely
1373 if (boundingRect.contains(hfrRect))
1374 {
1375 painter->setPen(QPen(Qt::red, scaleSize(3)));
1376 painter->drawText(hfrBottomLeft, hfr);
1377 painter->setPen(QPen(Qt::red, scaleSize(2)));
1378 return true;
1379 }
1380 return false;
1381}
1382
1383
1384void FITSView::drawStarCentroid(QPainter * painter, double scale)
1385{
1386 QFont painterFont;
1387 double fontSize = painterFont.pointSizeF() * 2;
1389 if (showStarsHFR)
1390 {
1391 // If we need to print the HFR out, give an arbitrarily sized font to the painter
1392 if (isLargeImage())
1393 fontSize = scaleSize(painterFont.pointSizeF());
1394 painterFont.setPointSizeF(fontSize);
1395 painter->setFont(painterFont);
1396 }
1397
1398 painter->setPen(QPen(Qt::red, scaleSize(2)));
1399 ImageMosaicMask *mask = dynamic_cast<ImageMosaicMask *>(m_ImageMask.get());
1400
1401 for (auto const &starCenter : m_ImageData->getStarCenters())
1402 {
1403 int const w = std::round(starCenter->width) * scale;
1404
1405 // translate if a mosaic mask is present
1406 const QPointF center = (mask == nullptr) ? QPointF(starCenter->x, starCenter->y) : mask->translate(QPointF(starCenter->x,
1407 starCenter->y));
1408 // Draw a circle around the detected star.
1409 // SEP coordinates are in the center of pixels, and Qt at the boundary.
1410 const double xCoord = center.x() - 0.5;
1411 const double yCoord = center.y() - 0.5;
1412 const int xc = std::round((xCoord - starCenter->width / 2.0f) * scale);
1413 const int yc = std::round((yCoord - starCenter->width / 2.0f) * scale);
1414 const int hw = w / 2;
1415
1416 BahtinovEdge* bEdge = dynamic_cast<BahtinovEdge*>(starCenter);
1417 if (bEdge != nullptr)
1418 {
1419 // Draw lines of diffraction pattern
1420 painter->setPen(QPen(Qt::red, scaleSize(2)));
1421 painter->drawLine(bEdge->line[0].x1() * scale, bEdge->line[0].y1() * scale,
1422 bEdge->line[0].x2() * scale, bEdge->line[0].y2() * scale);
1423 painter->setPen(QPen(Qt::green, scaleSize(2)));
1424 painter->drawLine(bEdge->line[1].x1() * scale, bEdge->line[1].y1() * scale,
1425 bEdge->line[1].x2() * scale, bEdge->line[1].y2() * scale);
1426 painter->setPen(QPen(Qt::darkGreen, scaleSize(2)));
1427 painter->drawLine(bEdge->line[2].x1() * scale, bEdge->line[2].y1() * scale,
1428 bEdge->line[2].x2() * scale, bEdge->line[2].y2() * scale);
1429
1430 // Draw center circle
1431 painter->setPen(QPen(Qt::white, scaleSize(2)));
1432 painter->drawEllipse(xc, yc, w, w);
1433
1434 // Draw offset circle
1435 double factor = 15.0;
1436 QPointF offsetVector = (bEdge->offset - QPointF(center.x(), center.y())) * factor;
1437 int const xo = std::round((center.x() + offsetVector.x() - starCenter->width / 2.0f) * scale);
1438 int const yo = std::round((center.y() + offsetVector.y() - starCenter->width / 2.0f) * scale);
1439 painter->setPen(QPen(Qt::red, scaleSize(2)));
1440 painter->drawEllipse(xo, yo, w, w);
1441
1442 // Draw line between center circle and offset circle
1443 painter->setPen(QPen(Qt::red, scaleSize(2)));
1444 painter->drawLine(xc + hw, yc + hw, xo + hw, yo + hw);
1445 }
1446 else
1447 {
1448 if (!showStarsHFR)
1449 {
1450 const double radius = starCenter->HFR > 0 ? 2.0f * starCenter->HFR * scale : w;
1451 painter->drawEllipse(QPointF(xCoord * scale, yCoord * scale), radius, radius);
1452 }
1453 }
1454
1455 if (showStarsHFR)
1456 {
1457 // Ask the painter how large will the HFR text be
1458 QString const hfr = QString("%1").arg(starCenter->HFR, 0, 'f', 2);
1459 if (!drawHFR(painter, hfr, xc + w + 5, yc + w / 2))
1460 {
1461 // Try a few more time with smaller fonts;
1462 for (int i = 0; i < 10; ++i)
1463 {
1464 const double tempFontSize = painterFont.pointSizeF() - 2;
1465 if (tempFontSize <= 0) break;
1466 painterFont.setPointSizeF(tempFontSize);
1467 painter->setFont(painterFont);
1468 if (drawHFR(painter, hfr, xc + w + 5, yc + w / 2))
1469 break;
1470 }
1471 // Reset the font size.
1472 painterFont.setPointSize(fontSize);
1473 painter->setFont(painterFont);
1474 }
1475 }
1476 }
1477}
1478
1479void FITSView::drawTrackingBox(QPainter * painter, double scale)
1480{
1481 painter->setPen(QPen(Qt::green, scaleSize(2)));
1482
1483 if (trackingBox.isNull())
1484 return;
1485
1486 const int x1 = trackingBox.x() * scale;
1487 const int y1 = trackingBox.y() * scale;
1488 const int w = trackingBox.width() * scale;
1489 const int h = trackingBox.height() * scale;
1490
1491 painter->drawRect(x1, y1, w, h);
1492}
1493
1494/**
1495This Method draws a large Crosshair in the center of the image, it is like a set of axes.
1496 */
1497
1498void FITSView::drawCrosshair(QPainter * painter, double scale)
1499{
1500 if (!m_ImageData) return;
1501 const int image_width = m_ImageData->width();
1502 const int image_height = m_ImageData->height();
1503 const QPointF c = QPointF((qreal)image_width / 2 * scale, (qreal)image_height / 2 * scale);
1504 const float midX = (float)image_width / 2 * scale;
1505 const float midY = (float)image_height / 2 * scale;
1506 const float maxX = (float)image_width * scale;
1507 const float maxY = (float)image_height * scale;
1508 const float r = 50 * scale;
1509
1510 painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")), scaleSize(1)));
1511
1512 //Horizontal Line to Circle
1513 painter->drawLine(0, midY, midX - r, midY);
1514
1515 //Horizontal Line past Circle
1516 painter->drawLine(midX + r, midY, maxX, midY);
1517
1518 //Vertical Line to Circle
1519 painter->drawLine(midX, 0, midX, midY - r);
1520
1521 //Vertical Line past Circle
1522 painter->drawLine(midX, midY + r, midX, maxY);
1523
1524 //Circles
1525 painter->drawEllipse(c, r, r);
1526 painter->drawEllipse(c, r / 2, r / 2);
1527}
1528
1529/**
1530This method is intended to draw a pixel grid onto the image. It first determines useful information
1531from the image. Then it draws the axes on the image if the crosshairs are not displayed.
1532Finally it draws the gridlines so that there will be 4 Gridlines on either side of the axes.
1533Note: This has to start drawing at the center not at the edges because the center axes must
1534be in the center of the image.
1535 */
1536
1537void FITSView::drawPixelGrid(QPainter * painter, double scale)
1538{
1539 const float width = m_ImageData->width() * scale;
1540 const float height = m_ImageData->height() * scale;
1541 const float cX = width / 2;
1542 const float cY = height / 2;
1543 const float deltaX = width / 10;
1544 const float deltaY = height / 10;
1545 QFontMetrics fm(painter->font());
1546
1547 //draw the Axes
1548 painter->setPen(QPen(Qt::red, scaleSize(1)));
1549 painter->drawText(cX - 30, height - 5, QString::number((int)((cX) / scale)));
1550 QString str = QString::number((int)((cY) / scale));
1551#if QT_VERSION < QT_VERSION_CHECK(5,11,0)
1552 painter->drawText(width - (fm.width(str) + 10), cY - 5, str);
1553#else
1554 painter->drawText(width - (fm.horizontalAdvance(str) + 10), cY - 5, str);
1555#endif
1556 if (!showCrosshair)
1557 {
1558 painter->drawLine(cX, 0, cX, height);
1559 painter->drawLine(0, cY, width, cY);
1560 }
1561 painter->setPen(QPen(Qt::gray, scaleSize(1)));
1562 //Start one iteration past the Center and draw 4 lines on either side of 0
1563 for (int x = deltaX; x < cX - deltaX; x += deltaX)
1564 {
1565 painter->drawText(cX + x - 30, height - 5, QString::number((int)(cX + x) / scale));
1566 painter->drawText(cX - x - 30, height - 5, QString::number((int)(cX - x) / scale));
1567 painter->drawLine(cX - x, 0, cX - x, height);
1568 painter->drawLine(cX + x, 0, cX + x, height);
1569 }
1570 //Start one iteration past the Center and draw 4 lines on either side of 0
1571 for (int y = deltaY; y < cY - deltaY; y += deltaY)
1572 {
1573 QString str = QString::number((int)((cY + y) / scale));
1574#if QT_VERSION < QT_VERSION_CHECK(5,11,0)
1575 painter->drawText(width - (fm.width(str) + 10), cY + y - 5, str);
1576#else
1577 painter->drawText(width - (fm.horizontalAdvance(str) + 10), cY + y - 5, str);
1578#endif
1579 str = QString::number((int)((cY - y) / scale));
1580#if QT_VERSION < QT_VERSION_CHECK(5,11,0)
1581 painter->drawText(width - (fm.width(str) + 10), cY - y - 5, str);
1582#else
1583 painter->drawText(width - (fm.horizontalAdvance(str) + 10), cY - y - 5, str);
1584#endif
1585 painter->drawLine(0, cY + y, width, cY + y);
1586 painter->drawLine(0, cY - y, width, cY - y);
1587 }
1588}
1589bool FITSView::imageHasWCS()
1590{
1591 if (m_ImageData != nullptr)
1592 return m_ImageData->hasWCS();
1593 return false;
1594}
1595
1596void FITSView::drawObjectNames(QPainter * painter, double scale)
1597{
1598 if (Options::fitsCatalog() != CAT_SKYMAP)
1599 {
1600#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1601 drawCatObjectNames(painter, scale);
1602#endif
1603 return;
1604 }
1605
1606 painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("FITSObjectLabelColor"))));
1607 for (const auto &listObject : m_ImageData->getSkyObjects())
1608 {
1609 painter->drawRect(listObject->x() * scale - 5, listObject->y() * scale - 5, 10, 10);
1610 painter->drawText(listObject->x() * scale + 10, listObject->y() * scale + 10, listObject->skyObject()->name());
1611 }
1612}
1613
1614#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1615void FITSView::drawCatObjectNames(QPainter * painter, double scale)
1616{
1617 if (!m_ImageData)
1618 return;
1619
1620 drawCatROI(painter, scale);
1621 bool showNames = Options::fitsCatObjShowNames();
1622 for (const auto &catObject : m_ImageData->getCatObjects())
1623 {
1624 if (!catObject.show)
1625 continue;
1626 if (catObject.highlight)
1627 painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("FITSObjectHighlightLabelColor"))));
1628 else
1629 painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("FITSObjectLabelColor"))));
1630
1631 painter->drawRect(catObject.x * scale - 5.0, catObject.y * scale - 5.0, 10, 10);
1632 if (showNames)
1633 painter->drawText(catObject.x * scale + 10.0, catObject.y * scale + 10.0, catObject.name);
1634 }
1635}
1636#endif
1637
1638#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1639void FITSView::drawCatROI(QPainter * painter, double scale)
1640{
1641 Q_UNUSED(scale);
1642 const QPoint pt = m_ImageData->catROIPt();
1643 const int radius = m_ImageData->catROIRadius();
1644 if (radius > 0)
1645 {
1646 painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("FITSObjectLabelColor"))));
1647 painter->drawEllipse(pt, radius, radius);
1648 }
1649}
1650#endif
1651
1652#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1653void FITSView::drawHiPSOverlay(QPainter * painter, double scale)
1654{
1655 if (m_HiPSOverlayPixmap.isNull())
1656 {
1657 auto width = m_ImageData->width();
1658 auto height = m_ImageData->height();
1660 SkyPoint startPoint;
1661 SkyPoint endPoint;
1662 SkyPoint centerPoint;
1663
1664 m_ImageData->pixelToWCS(QPointF(0, 0), startPoint);
1665 m_ImageData->pixelToWCS(QPointF(width - 1, height - 1), endPoint);
1666 m_ImageData->pixelToWCS(QPointF( (width - Options::hIPSOffsetX()) / 2.0, (height - Options::hIPSOffsetY()) / 2.0),
1667 centerPoint);
1668
1669 startPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
1670 endPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
1671 centerPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
1672
1673 auto fov_radius = startPoint.angularDistanceTo(&endPoint).Degrees() / 2;
1674 QVariant PA (0.0);
1675 m_ImageData->getRecordValue("CROTA1", PA);
1676
1677 auto rotation = 180 - PA.toDouble();
1678 if (rotation > 360)
1679 rotation -= 360;
1680
1681 if (HIPSFinder::Instance()->renderFOV(&centerPoint, fov_radius, rotation, &image) == false)
1682 return;
1683 m_HiPSOverlayPixmap = QPixmap::fromImage(image);
1684 }
1685
1686 Q_UNUSED(scale);
1687 painter->setOpacity(Options::hIPSOpacity());
1688 painter->drawPixmap(0, 0, m_HiPSOverlayPixmap);
1689 painter->setOpacity(1);
1690}
1691#endif
1692
1693/**
1694This method will paint EQ Gridlines in an overlay if there is WCS data present.
1695It determines the minimum and maximum RA and DEC, then it uses that information to
1696judge which gridLines to draw. Then it calls the drawEQGridlines methods below
1697to draw gridlines at those specific RA and Dec values.
1698 */
1699
1700#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1701void FITSView::drawEQGrid(QPainter * painter, double scale)
1702{
1703 const int image_width = m_ImageData->width();
1704 const int image_height = m_ImageData->height();
1705
1706 if (m_ImageData->hasWCS())
1707 {
1708 double maxRA = -1000;
1709 double minRA = 1000;
1710 double maxDec = -1000;
1711 double minDec = 1000;
1712 m_ImageData->findWCSBounds(minRA, maxRA, minDec, maxDec);
1713
1714 auto minDecMinutes = (int)(minDec * 12); //This will force the Dec Scale to 5 arc minutes in the loop
1715 auto maxDecMinutes = (int)(maxDec * 12);
1716
1717 auto minRAMinutes =
1718 (int)(minRA / 15.0 *
1719 120.0); //This will force the scale to 1/2 minutes of RA in the loop from 0 to 50 degrees
1720 auto maxRAMinutes = (int)(maxRA / 15.0 * 120.0);
1721
1722 double raConvert = 15 / 120.0; //This will undo the calculation above to retrieve the actual RA.
1723 double decConvert = 1.0 / 12.0; //This will undo the calculation above to retrieve the actual DEC.
1724
1725 if (maxDec > 50 || minDec < -50)
1726 {
1727 minRAMinutes =
1728 (int)(minRA / 15.0 * 60.0); //This will force the scale to 1 min of RA from 50 to 80 degrees
1729 maxRAMinutes = (int)(maxRA / 15.0 * 60.0);
1730 raConvert = 15 / 60.0;
1731 }
1732
1733 if (maxDec > 80 || minDec < -80)
1734 {
1735 minRAMinutes =
1736 (int)(minRA / 15.0 * 30); //This will force the scale to 2 min of RA from 80 to 85 degrees
1737 maxRAMinutes = (int)(maxRA / 15.0 * 30);
1738 raConvert = 15 / 30.0;
1739 }
1740 if (maxDec > 85 || minDec < -85)
1741 {
1742 minRAMinutes =
1743 (int)(minRA / 15.0 * 6); //This will force the scale to 10 min of RA from 85 to 89 degrees
1744 maxRAMinutes = (int)(maxRA / 15.0 * 6);
1745 raConvert = 15 / 6.0;
1746 }
1747 if (maxDec >= 89.25 || minDec <= -89.25)
1748 {
1749 minRAMinutes =
1750 (int)(minRA /
1751 15); //This will force the scale to whole hours of RA in the loop really close to the poles
1752 maxRAMinutes = (int)(maxRA / 15);
1753 raConvert = 15;
1754 }
1755
1756 painter->setPen(QPen(Qt::yellow));
1757
1758 QPointF pixelPoint, imagePoint, pPoint;
1759
1760 //This section draws the RA Gridlines
1761
1762 for (int targetRA = minRAMinutes; targetRA <= maxRAMinutes; targetRA++)
1763 {
1764 painter->setPen(QPen(Qt::yellow));
1765 double target = targetRA * raConvert;
1766
1767 if (eqGridPoints.count() != 0)
1768 eqGridPoints.clear();
1769
1770 double increment = std::abs((maxDec - minDec) /
1771 100.0); //This will determine how many points to use to create the RA Line
1772
1773 for (double targetDec = minDec; targetDec <= maxDec; targetDec += increment)
1774 {
1775 SkyPoint pointToGet(target / 15.0, targetDec);
1776 bool inImage = m_ImageData->wcsToPixel(pointToGet, pixelPoint, imagePoint);
1777 if (inImage)
1778 {
1779 QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale);
1780 eqGridPoints.append(pt);
1781 }
1782 }
1783
1784 if (eqGridPoints.count() > 1)
1785 {
1786 for (int i = 1; i < eqGridPoints.count(); i++)
1787 painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i));
1788 QString str = QString::number(dms(target).hour()) + "h " +
1789 QString::number(dms(target).minute()) + '\'';
1790 if (maxDec <= 50 && maxDec >= -50)
1791 str = str + " " + QString::number(dms(target).second()) + "''";
1792 QPointF pt = getPointForGridLabel(painter, str, scale);
1793 if (pt.x() != -100)
1794 painter->drawText(pt.x(), pt.y(), str);
1795 }
1796 }
1797
1798 //This section draws the DEC Gridlines
1799
1800 for (int targetDec = minDecMinutes; targetDec <= maxDecMinutes; targetDec++)
1801 {
1802 if (eqGridPoints.count() != 0)
1803 eqGridPoints.clear();
1804
1805 double increment = std::abs((maxRA - minRA) /
1806 100.0); //This will determine how many points to use to create the Dec Line
1807 double target = targetDec * decConvert;
1808
1809 for (double targetRA = minRA; targetRA <= maxRA; targetRA += increment)
1810 {
1811 SkyPoint pointToGet(targetRA / 15, targetDec * decConvert);
1812 bool inImage = m_ImageData->wcsToPixel(pointToGet, pixelPoint, imagePoint);
1813 if (inImage)
1814 {
1815 QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale);
1816 eqGridPoints.append(pt);
1817 }
1818 }
1819 if (eqGridPoints.count() > 1)
1820 {
1821 for (int i = 1; i < eqGridPoints.count(); i++)
1822 painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i));
1823 QString str = QString::number(dms(target).degree()) + "° " + QString::number(dms(target).arcmin()) + '\'';
1824 QPointF pt = getPointForGridLabel(painter, str, scale);
1825 if (pt.x() != -100)
1826 painter->drawText(pt.x(), pt.y(), str);
1827 }
1828 }
1829
1830 //This Section Draws the North Celestial Pole if present
1831 SkyPoint NCP(0, 90);
1832
1833 bool NCPtest = m_ImageData->wcsToPixel(NCP, pPoint, imagePoint);
1834 if (NCPtest)
1835 {
1836 bool NCPinImage =
1837 (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height);
1838 if (NCPinImage)
1839 {
1840 painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4,
1841 KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"));
1842 painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15,
1843 i18nc("North Celestial Pole", "NCP"));
1844 }
1845 }
1846
1847 //This Section Draws the South Celestial Pole if present
1848 SkyPoint SCP(0, -90);
1849
1850 bool SCPtest = m_ImageData->wcsToPixel(SCP, pPoint, imagePoint);
1851 if (SCPtest)
1852 {
1853 bool SCPinImage =
1854 (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height);
1855 if (SCPinImage)
1856 {
1857 painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4,
1858 KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"));
1859 painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15,
1860 i18nc("South Celestial Pole", "SCP"));
1861 }
1862 }
1863 }
1864}
1865#endif
1866
1867bool FITSView::pointIsInImage(QPointF pt, double scale)
1868{
1869 int image_width = m_ImageData->width();
1870 int image_height = m_ImageData->height();
1871 return pt.x() < image_width * scale && pt.y() < image_height * scale && pt.x() > 0 && pt.y() > 0;
1872}
1873
1874QPointF FITSView::getPointForGridLabel(QPainter *painter, const QString &str, double scale)
1875{
1876 QFontMetrics fm(painter->font());
1877#if QT_VERSION < QT_VERSION_CHECK(5,11,0)
1878 int strWidth = fm.width(str);
1879#else
1880 int strWidth = fm.horizontalAdvance(str);
1881#endif
1882 int strHeight = fm.height();
1883 int image_width = m_ImageData->width();
1884 int image_height = m_ImageData->height();
1885
1886 //These get the maximum X and Y points in the list that are in the image
1887 QPointF maxXPt(image_width * scale / 2, image_height * scale / 2);
1888 for (auto &p : eqGridPoints)
1889 {
1890 if (p.x() > maxXPt.x() && pointIsInImage(p, scale))
1891 maxXPt = p;
1892 }
1893 QPointF maxYPt(image_width * scale / 2, image_height * scale / 2);
1894
1895 for (auto &p : eqGridPoints)
1896 {
1897 if (p.y() > maxYPt.y() && pointIsInImage(p, scale))
1898 maxYPt = p;
1899 }
1900 QPointF minXPt(image_width * scale / 2, image_height * scale / 2);
1901
1902 for (auto &p : eqGridPoints)
1903 {
1904 if (p.x() < minXPt.x() && pointIsInImage(p, scale))
1905 minXPt = p;
1906 }
1907 QPointF minYPt(image_width * scale / 2, image_height * scale / 2);
1908
1909 for (auto &p : eqGridPoints)
1910 {
1911 if (p.y() < minYPt.y() && pointIsInImage(p, scale))
1912 minYPt = p;
1913 }
1914
1915 //This gives preference to points that are on the right hand side and bottom.
1916 //But if the line doesn't intersect the right or bottom, it then tries for the top and left.
1917 //If no points are found in the image, it returns a point off the screen
1918 //If all else fails, like in the case of a circle on the image, it returns the far right point.
1919
1920 if (image_width * scale - maxXPt.x() < strWidth)
1921 {
1922 return QPointF(
1923 image_width * scale - (strWidth + 10),
1924 maxXPt.y() -
1925 strHeight); //This will draw the text on the right hand side, up and to the left of the point where the line intersects
1926 }
1927 if (image_height * scale - maxYPt.y() < strHeight)
1928 return QPointF(
1929 maxYPt.x() - (strWidth + 10),
1930 image_height * scale -
1931 (strHeight + 10)); //This will draw the text on the bottom side, up and to the left of the point where the line intersects
1932 if (minYPt.y() < strHeight)
1933 return QPointF(
1934 minYPt.x() * scale + 10,
1935 strHeight + 20); //This will draw the text on the top side, down and to the right of the point where the line intersects
1936 if (minXPt.x() < strWidth)
1937 return QPointF(
1938 10,
1939 minXPt.y() * scale +
1940 strHeight +
1941 20); //This will draw the text on the left hand side, down and to the right of the point where the line intersects
1942 if (maxXPt.x() == image_width * scale / 2 && maxXPt.y() == image_height * scale / 2)
1943 return QPointF(-100, -100); //All of the points were off the screen
1944
1945 return QPoint(maxXPt.x() - (strWidth + 10), maxXPt.y() - (strHeight + 10));
1946}
1947
1948void FITSView::setFirstLoad(bool value)
1949{
1950 firstLoad = value;
1951}
1952
1953QPixmap &FITSView::getTrackingBoxPixmap(uint8_t margin)
1954{
1955 if (trackingBox.isNull() || m_ImageFrame.isNull() || m_ImageFrame->pixmap(Qt::ReturnByValueConstant()).isNull())
1956 return trackingBoxPixmap;
1957
1958 // We need to know which rendering strategy updateFrame used to determine the scaling.
1959 const float scale = getScale();
1960
1961 int x1 = (trackingBox.x() - margin) * scale;
1962 int y1 = (trackingBox.y() - margin) * scale;
1963 int w = (trackingBox.width() + margin * 2) * scale;
1964 int h = (trackingBox.height() + margin * 2) * scale;
1965
1966 trackingBoxPixmap = m_ImageFrame->grab(QRect(x1, y1, w, h));
1967 return trackingBoxPixmap;
1968}
1969
1970void FITSView::setTrackingBox(const QRect &rect)
1971{
1972 if (rect != trackingBox)
1973 {
1974 trackingBox = rect;
1975 updateFrame();
1976 if(showStarProfile)
1977 viewStarProfile();
1978 }
1979}
1980
1981void FITSView::resizeTrackingBox(int newSize)
1982{
1983 int x = trackingBox.x() + trackingBox.width() / 2;
1984 int y = trackingBox.y() + trackingBox.height() / 2;
1985 int delta = newSize / 2;
1986 setTrackingBox(QRect( x - delta, y - delta, newSize, newSize));
1987}
1988
1989void FITSView::processRectangleFixed(int s)
1990{
1991 int w = m_ImageData->width();
1992 int h = m_ImageData->height();
1993
1994 QPoint c = selectionRectangleRaw.center();
1995 c.setX(qMax((int)round(s / 2.0), c.x()));
1996 c.setX(qMin(w - (int)round(s / 2.0), c.x()));
1997 c.setY(qMax((int)round(s / 2.0), c.y()));
1998 c.setY(qMin(h - (int)round(s / 2.0), c.y()));
1999
2000 QPoint topLeft, botRight;
2001 topLeft = QPoint(c.x() - round(s / 2.0), c.y() - round(s / 2.0));
2002 botRight = QPoint(c.x() + round(s / 2.0), c.y() + round(s / 2.0));
2003
2004 emit setRubberBand(QRect(topLeft, botRight));
2005 processRectangle(topLeft, botRight, true);
2006}
2007
2008void FITSView::processRectangle(QPoint p1, QPoint p2, bool calculate)
2009{
2010 if(!isSelectionRectShown())
2011 return;
2012 //the user can draw a rectangle by dragging the mouse to any direction
2013 //but we need to feed Rectangle(topleft, topright)
2014 //hence we calculate topleft and topright for each case
2015
2016 //p1 is the point where the user presses the mouse
2017 //p2 is the point where the user releases the mouse
2018 selectionRectangleRaw = QRect(p1, p2).normalized();
2019 //Index out of bounds Check for raw Rectangle, this effectively works when user does shift + drag, other wise becomes redundant
2020
2021 QPoint topLeft = selectionRectangleRaw.topLeft();
2022 QPoint botRight = selectionRectangleRaw.bottomRight();
2023
2024 topLeft.setX(qMax(1, topLeft.x()));
2025 topLeft.setY(qMax(1, topLeft.y()));
2026 botRight.setX(qMin((int)m_ImageData->width(), botRight.x()));
2027 botRight.setY(qMin((int)m_ImageData->height(), botRight.y()));
2028
2029 selectionRectangleRaw.setTopLeft(topLeft);
2030 selectionRectangleRaw.setBottomRight(botRight);
2031
2032 if(calculate)
2033 {
2034 if(m_ImageData)
2035 {
2036 m_ImageData->makeRoiBuffer(selectionRectangleRaw);
2037 emit rectangleUpdated(selectionRectangleRaw);
2038 }
2039 }
2040 //updateFrameRoi();
2041
2042 //emit raw rectangle for calculation
2043 //update the stats pane after calculation; there should be ample time for calculation before showing the values
2044}
2045
2046bool FITSView::isImageStretched()
2047{
2048 return stretchImage;
2049}
2050
2051bool FITSView::isClippingShown()
2052{
2053 return showClipping;
2054}
2055
2056bool FITSView::isCrosshairShown()
2057{
2058 return showCrosshair;
2059}
2060
2061bool FITSView::isEQGridShown()
2062{
2063 return showEQGrid;
2064}
2065
2066bool FITSView::isSelectionRectShown()
2067{
2068 return showSelectionRect;
2069}
2070bool FITSView::areObjectsShown()
2071{
2072 return showObjects;
2073}
2074
2075bool FITSView::isPixelGridShown()
2076{
2077 return showPixelGrid;
2078}
2079
2080bool FITSView::isHiPSOverlayShown()
2081{
2082 return showHiPSOverlay;
2083}
2084
2085void FITSView::toggleCrosshair()
2086{
2087 showCrosshair = !showCrosshair;
2088 updateFrame();
2089}
2090
2091void FITSView::toggleClipping()
2092{
2093 showClipping = !showClipping;
2094 updateFrame();
2095}
2096
2097void FITSView::toggleEQGrid()
2098{
2099 showEQGrid = !showEQGrid;
2100
2101 if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
2102 {
2103#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
2104 QFuture<bool> future = QtConcurrent::run(&FITSData::loadWCS, m_ImageData.data());
2105#else
2106 QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS);
2107#endif
2108 wcsWatcher.setFuture(future);
2109 return;
2110 }
2111
2112 if (m_ImageFrame)
2113 updateFrame();
2114}
2115
2116void FITSView::toggleHiPSOverlay()
2117{
2118 showHiPSOverlay = !showHiPSOverlay;
2119
2120 if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
2121 {
2122#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
2123 QFuture<bool> future = QtConcurrent::run(&FITSData::loadWCS, m_ImageData.data());
2124#else
2125 QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS);
2126#endif
2127 wcsWatcher.setFuture(future);
2128 return;
2129 }
2130
2131 if (m_ImageFrame)
2132 {
2133 m_QueueUpdate = true;
2134 updateFrame();
2135 }
2136}
2137
2138void FITSView::toggleSelectionMode()
2139{
2140 showSelectionRect = !showSelectionRect;
2141 if (!showSelectionRect)
2142 emit rectangleUpdated(QRect());
2143 else if (m_ImageData)
2144 {
2145 m_ImageData->makeRoiBuffer(selectionRectangleRaw);
2146 emit rectangleUpdated(selectionRectangleRaw);
2147 }
2148
2149 emit showRubberBand(showSelectionRect);
2150 if (m_ImageFrame)
2151 updateFrame();
2152
2153}
2154void FITSView::toggleObjects()
2155{
2156 showObjects = !showObjects;
2157
2158 if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
2159 {
2160#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
2161 QFuture<bool> future = QtConcurrent::run(&FITSData::loadWCS, m_ImageData.data());
2162#else
2163 QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS);
2164#endif
2165 wcsWatcher.setFuture(future);
2166 return;
2167 }
2168
2169 if (m_ImageFrame)
2170 {
2171#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2172 if (showObjects)
2173 m_ImageData->searchObjects();
2174 else
2175 emit catReset();
2176#endif
2177 updateFrame();
2178 }
2179}
2180
2181void FITSView::toggleStars()
2182{
2183 toggleStars(!markStars);
2184 if (m_ImageFrame)
2185 updateFrame();
2186}
2187
2188void FITSView::toggleStretch()
2189{
2190 stretchImage = !stretchImage;
2191 if (m_ImageFrame && rescale(ZOOM_KEEP_LEVEL))
2192 updateFrame();
2193}
2194
2195void FITSView::toggleStarProfile()
2196{
2197#ifdef HAVE_DATAVISUALIZATION
2198 showStarProfile = !showStarProfile;
2199 if(showStarProfile && trackingBoxEnabled)
2200 viewStarProfile();
2201 if(toggleProfileAction)
2202 toggleProfileAction->setChecked(showStarProfile);
2203
2204 if(showStarProfile)
2205 {
2206 //The tracking box is already on for Guide and Focus Views, but off for Normal and Align views.
2207 //So for Normal and Align views, we need to set up the tracking box.
2208 if(mode == FITS_NORMAL || mode == FITS_ALIGN)
2209 {
2210 setCursorMode(selectCursor);
2211 connect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int)));
2212 trackingBox = QRect(0, 0, 128, 128);
2213 setTrackingBoxEnabled(true);
2214 if(starProfileWidget)
2215 connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int)));
2216 }
2217 if(starProfileWidget)
2218 connect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile()));
2219 }
2220 else
2221 {
2222 //This shuts down the tracking box for Normal and Align Views
2223 //It doesn't touch Guide and Focus Views because they still need a tracking box
2224 if(mode == FITS_NORMAL || mode == FITS_ALIGN)
2225 {
2226 if(getCursorMode() == selectCursor)
2227 setCursorMode(dragCursor);
2228 disconnect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int)));
2229 setTrackingBoxEnabled(false);
2230 if(starProfileWidget)
2231 disconnect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int)));
2232 }
2233 if(starProfileWidget)
2234 {
2235 disconnect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile()));
2236 starProfileWidget->close();
2237 starProfileWidget = nullptr;
2238 }
2239 emit starProfileWindowClosed();
2240 }
2241 updateFrame();
2242#endif
2243}
2244
2245void FITSView::move3DTrackingBox(int x, int y)
2246{
2247 int boxSize = trackingBox.width();
2248 QRect starRect = QRect(x - boxSize / 2, y - boxSize / 2, boxSize, boxSize);
2249 setTrackingBox(starRect);
2250}
2251
2252void FITSView::viewStarProfile()
2253{
2254#ifdef HAVE_DATAVISUALIZATION
2255 if(!trackingBoxEnabled)
2256 {
2257 setTrackingBoxEnabled(true);
2258 setTrackingBox(QRect(0, 0, 128, 128));
2259 }
2260 if(!starProfileWidget)
2261 {
2262 starProfileWidget = new StarProfileViewer(this);
2263
2264 //This is a band-aid to fix a QT bug with createWindowContainer
2265 //It will set the cursor of the Window containing the view that called the Star Profile method to the Arrow Cursor
2266 //Note that Ekos Manager is a QDialog and FitsViewer is a KXmlGuiWindow
2267 QWidget * superParent = this->parentWidget();
2268 while(superParent->parentWidget() != 0 && !superParent->inherits("QDialog") && !superParent->inherits("KXmlGuiWindow"))
2269 superParent = superParent->parentWidget();
2270 superParent->setCursor(Qt::ArrowCursor);
2271 //This is the end of the band-aid
2272
2273 connect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile()));
2274 if(mode == FITS_ALIGN || mode == FITS_NORMAL)
2275 {
2276 starProfileWidget->enableTrackingBox(true);
2277 m_ImageData->setStarAlgorithm(ALGORITHM_CENTROID);
2278 connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int)));
2279 }
2280 }
2281 QList<Edge *> starCenters = m_ImageData->getStarCentersInSubFrame(trackingBox);
2282 if(starCenters.size() == 0)
2283 {
2284 // FIXME, the following does not work anymore.
2285 //m_ImageData->findStars(&trackingBox, true);
2286 // FIXME replacing it with this
2287 m_ImageData->findStars(ALGORITHM_CENTROID, trackingBox).waitForFinished();
2288 starCenters = m_ImageData->getStarCentersInSubFrame(trackingBox);
2289 }
2290
2291 starProfileWidget->loadData(m_ImageData, trackingBox, starCenters);
2292 starProfileWidget->show();
2293 starProfileWidget->raise();
2294 if(markStars)
2295 updateFrame(); //this is to update for the marked stars
2296
2297#endif
2298}
2299
2300void FITSView::togglePixelGrid()
2301{
2302 showPixelGrid = !showPixelGrid;
2303 updateFrame();
2304}
2305
2306QFuture<bool> FITSView::findStars(StarAlgorithm algorithm, const QRect &searchBox)
2307{
2308 if(trackingBoxEnabled)
2309 return m_ImageData->findStars(algorithm, trackingBox);
2310 else
2311 return m_ImageData->findStars(algorithm, searchBox);
2312}
2313
2314void FITSView::toggleStars(bool enable)
2315{
2316 markStars = enable;
2317
2318 if (markStars)
2319 searchStars();
2320}
2321
2322void FITSView::searchStars()
2323{
2324 QVariant frameType;
2325 if (m_ImageData->areStarsSearched() || !m_ImageData || (m_ImageData->getRecordValue("FRAME", frameType)
2326 && frameType.toString() != "Light"))
2327 return;
2328
2330 emit newStatus(i18n("Finding stars..."), FITS_MESSAGE);
2331 qApp->processEvents();
2332
2333#ifdef HAVE_STELLARSOLVER
2334 QVariantMap extractionSettings;
2335 extractionSettings["optionsProfileIndex"] = Options::hFROptionsProfile();
2336 extractionSettings["optionsProfileGroup"] = static_cast<int>(Ekos::HFRProfiles);
2337 imageData()->setSourceExtractorSettings(extractionSettings);
2338#endif
2339
2340 QFuture<bool> result = findStars(ALGORITHM_SEP);
2341 result.waitForFinished();
2342 if (result.result() && isVisible())
2343 {
2344 emit newStatus("", FITS_MESSAGE);
2345 }
2347}
2348
2349void FITSView::processPointSelection(int x, int y)
2350{
2351 emit trackingStarSelected(x, y);
2352}
2353
2354void FITSView::processMarkerSelection(int x, int y)
2355{
2356 markerCrosshair.setX(x);
2357 markerCrosshair.setY(y);
2358
2359 updateFrame();
2360}
2361
2362void FITSView::processHighlight(int x, int y)
2363{
2364 if (!m_ImageData)
2365 return;
2366
2367 const QPoint pt = QPoint(x, y);
2368 int candidate = -1;
2369 double candidateDist = -1.0;
2370 QList<CatObject> objs = m_ImageData->getCatObjects();
2371 for (int i = 0; i < objs.size(); i++)
2372 {
2373 if (!objs[i].show)
2374 continue;
2375
2376 // See if we have a direct hit - if multiple we'll just take the first one for speed
2377 const QRect rect = QRect(objs[i].x - (CAT_OBJ_BOX_SIZE / 2), objs[i].y - (CAT_OBJ_BOX_SIZE / 2),
2378 CAT_OBJ_BOX_SIZE, CAT_OBJ_BOX_SIZE);
2379 if (rect.contains(pt))
2380 {
2381 // Direct hit so no point in processing further
2382 candidate = i;
2383 break;
2384 }
2385
2386 // Also do a wider search and take the nearest
2387 const QRect bigRect = QRect(objs[i].x - 20, objs[i].y - 20, 40, 40);
2388 if (bigRect.contains(pt))
2389 {
2390 double distance = std::hypot(x - objs[i].x, y - objs[i].y);
2391 if (candidate < 0 || distance < candidateDist)
2392 {
2393 candidate = i;
2394 candidateDist = distance;
2395 }
2396 }
2397 }
2398 if (candidate >= 0)
2399 {
2400 m_ImageData->highlightCatObject(candidate, -1);
2401 emit catHighlightChanged(candidate);
2402 updateFrame();
2403 }
2404}
2405
2406void FITSView::processCircle(QPoint p1, QPoint p2)
2407{
2408#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2409 const double x = p1.x() - p2.x();
2410 const double y = p1.y() - p2.y();
2411 const int radius = std::hypot(x, y);
2412 m_ImageData->setCatSearchROI(p1, radius);
2413 m_ImageData->searchCatObjects();
2414 updateFrame();
2415#else
2416 Q_UNUSED(p1);
2417 Q_UNUSED(p2);
2418#endif
2419}
2420
2421void FITSView::setTrackingBoxEnabled(bool enable)
2422{
2423 if (enable != trackingBoxEnabled)
2424 {
2425 trackingBoxEnabled = enable;
2426 //updateFrame();
2427 }
2428}
2429
2430void FITSView::wheelEvent(QWheelEvent * event)
2431{
2432 //This attempts to send the wheel event back to the Scroll Area if it was taken from a trackpad
2433 //It should still do the zoom if it is a mouse wheel
2434 if (event->source() == Qt::MouseEventSynthesizedBySystem)
2435 {
2437 }
2438 else
2439 {
2440#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
2441 QPoint mouseCenter = getImagePoint(event->pos());
2442#else
2443 QPoint mouseCenter = getImagePoint(event->position().toPoint());
2444#endif
2445 if (event->angleDelta().y() > 0)
2446 ZoomIn();
2447 else
2448 ZoomOut();
2449 event->accept();
2450 cleanUpZoom(mouseCenter);
2451 }
2452 emit zoomRubberBand(getCurrentZoom() / ZOOM_DEFAULT);
2453}
2454
2455/**
2456This method is intended to keep key locations in an image centered on the screen while zooming.
2457If there is a marker or tracking box, it centers on those. If not, it uses the point called
2458viewCenter that was passed as a parameter.
2459 */
2460
2461void FITSView::cleanUpZoom(QPoint viewCenter)
2462{
2463 int x0 = 0;
2464 int y0 = 0;
2465 double scale = (currentZoom / ZOOM_DEFAULT);
2466 if (!markerCrosshair.isNull())
2467 {
2468 x0 = markerCrosshair.x() * scale;
2469 y0 = markerCrosshair.y() * scale;
2470 }
2471 else if (trackingBoxEnabled)
2472 {
2473 x0 = trackingBox.center().x() * scale;
2474 y0 = trackingBox.center().y() * scale;
2475 }
2476 else if (!viewCenter.isNull())
2477 {
2478 x0 = viewCenter.x() * scale;
2479 y0 = viewCenter.y() * scale;
2480 }
2481 if ((x0 != 0) || (y0 != 0))
2482 ensureVisible(x0, y0, width() / 2, height() / 2);
2483 updateMouseCursor();
2484}
2485
2486/**
2487This method converts a point from the ViewPort Coordinate System to the
2488Image Coordinate System.
2489 */
2490
2491QPoint FITSView::getImagePoint(QPoint viewPortPoint)
2492{
2493 QWidget * w = widget();
2494
2495 if (w == nullptr)
2496 return QPoint(0, 0);
2497
2498 double scale = (currentZoom / ZOOM_DEFAULT);
2499 QPoint widgetPoint = w->mapFromParent(viewPortPoint);
2500 QPoint imagePoint = QPoint(widgetPoint.x() / scale, widgetPoint.y() / scale);
2501 return imagePoint;
2502}
2503
2504void FITSView::initDisplayImage()
2505{
2506 // Account for leftover when sampling. Thus a 5-wide image sampled by 2
2507 // would result in a width of 3 (samples 0, 2 and 4).
2508 int w = (m_ImageData->width() + m_PreviewSampling - 1) / m_PreviewSampling;
2509 int h = (m_ImageData->height() + m_PreviewSampling - 1) / m_PreviewSampling;
2510
2511 if (m_ImageData->channels() == 1)
2512 {
2513 rawImage = QImage(w, h, QImage::Format_Indexed8);
2514
2515 rawImage.setColorCount(256);
2516 for (int i = 0; i < 256; i++)
2517 rawImage.setColor(i, qRgb(i, i, i));
2518 }
2519 else
2520 {
2521 rawImage = QImage(w, h, QImage::Format_RGB32);
2522 }
2523}
2524
2525/**
2526The Following two methods allow gestures to work with trackpads.
2527Specifically, we are targeting the pinch events, so that if one is generated,
2528Then the pinchTriggered method will be called. If the event is not a pinch gesture,
2529then the event is passed back to the other event handlers.
2530 */
2531
2532bool FITSView::event(QEvent * event)
2533{
2534 if (event->type() == QEvent::Gesture)
2535 return gestureEvent(dynamic_cast<QGestureEvent *>(event));
2536 return QScrollArea::event(event);
2537}
2538
2539bool FITSView::gestureEvent(QGestureEvent * event)
2540{
2541 if (QGesture * pinch = event->gesture(Qt::PinchGesture))
2542 pinchTriggered(dynamic_cast<QPinchGesture *>(pinch));
2543 return true;
2544}
2545
2546/**
2547This Method works with Trackpads to use the pinch gesture to scroll in and out
2548It stores a point to keep track of the location where the gesture started so that
2549while you are zooming, it tries to keep that initial point centered in the view.
2550**/
2551void FITSView::pinchTriggered(QPinchGesture * gesture)
2552{
2553 if (!zooming)
2554 {
2555 zoomLocation = getImagePoint(mapFromGlobal(QCursor::pos()));
2556 zooming = true;
2557 }
2558 if (gesture->state() == Qt::GestureFinished)
2559 {
2560 zooming = false;
2561 }
2562 zoomTime++; //zoomTime is meant to slow down the zooming with a pinch gesture.
2563 if (zoomTime > 10000) //This ensures zoomtime never gets too big.
2564 zoomTime = 0;
2565 if (zooming && (zoomTime % 10 == 0)) //zoomTime is set to slow it by a factor of 10.
2566 {
2567 if (gesture->totalScaleFactor() > 1)
2568 ZoomIn();
2569 else
2570 ZoomOut();
2571 }
2572 cleanUpZoom(zoomLocation);
2573}
2574
2575/*void FITSView::handleWCSCompletion()
2576{
2577 //bool hasWCS = wcsWatcher.result();
2578 if(m_ImageData->hasWCS())
2579 this->updateFrame();
2580 emit wcsToggled(m_ImageData->hasWCS());
2581}*/
2582
2583void FITSView::syncWCSState()
2584{
2585 bool hasWCS = m_ImageData->hasWCS();
2586 bool wcsLoaded = m_ImageData->getWCSState() == FITSData::Success;
2587
2588#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2589 if (showObjects)
2590 m_ImageData->searchObjects();
2591#endif
2592
2593 if (hasWCS && wcsLoaded)
2594 this->updateFrame();
2595
2596 emit wcsToggled(hasWCS);
2597
2598 if (toggleEQGridAction != nullptr)
2599 toggleEQGridAction->setEnabled(hasWCS);
2600 if (toggleObjectsAction != nullptr)
2601 toggleObjectsAction->setEnabled(hasWCS);
2602 if (centerTelescopeAction != nullptr)
2603 centerTelescopeAction->setEnabled(hasWCS);
2604 if (toggleHiPSOverlayAction != nullptr)
2605 toggleHiPSOverlayAction->setEnabled(hasWCS);
2606}
2607
2608void FITSView::createFloatingToolBar()
2609{
2610 if (floatingToolBar != nullptr)
2611 return;
2612
2613 floatingToolBar = new QToolBar(this);
2614 auto * eff = new QGraphicsOpacityEffect(this);
2615 floatingToolBar->setGraphicsEffect(eff);
2616 eff->setOpacity(0.2);
2617 floatingToolBar->setVisible(false);
2618 floatingToolBar->setStyleSheet(
2619 "QToolBar{background: rgba(150, 150, 150, 210); border:none; color: yellow}"
2620 "QToolButton{background: transparent; border:none; color: yellow}"
2621 "QToolButton:hover{background: rgba(200, 200, 200, 255);border:solid; color: yellow}"
2622 "QToolButton:checked{background: rgba(110, 110, 110, 255);border:solid; color: yellow}");
2623 floatingToolBar->setFloatable(true);
2624 floatingToolBar->setIconSize(QSize(25, 25));
2625 //floatingToolBar->setMovable(true);
2626
2627 QAction * action = nullptr;
2628
2629 floatingToolBar->addAction(QIcon::fromTheme("zoom-in"),
2630 i18n("Zoom In"), this, SLOT(ZoomIn()));
2631
2632 floatingToolBar->addAction(QIcon::fromTheme("zoom-out"),
2633 i18n("Zoom Out"), this, SLOT(ZoomOut()));
2634
2635 floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-best"),
2636 i18n("Default Zoom"), this, SLOT(ZoomDefault()));
2637
2638 floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-width"),
2639 i18n("Zoom to Fit"), this, SLOT(ZoomToFit()));
2640
2641 toggleStretchAction = floatingToolBar->addAction(QIcon::fromTheme("transform-move"),
2642 i18n("Toggle Stretch"),
2643 this, SLOT(toggleStretch()));
2644 toggleStretchAction->setCheckable(true);
2645
2646
2647 floatingToolBar->addSeparator();
2648
2649 action = floatingToolBar->addAction(QIcon::fromTheme("crosshairs"),
2650 i18n("Show Cross Hairs"), this, SLOT(toggleCrosshair()));
2651 action->setCheckable(true);
2652
2653 action = floatingToolBar->addAction(QIcon::fromTheme("map-flat"),
2654 i18n("Show Pixel Gridlines"), this, SLOT(togglePixelGrid()));
2655 action->setCheckable(true);
2656
2657 toggleStarsAction =
2658 floatingToolBar->addAction(QIcon::fromTheme("kstars_stars"),
2659 i18n("Detect Stars in Image"), this, SLOT(toggleStars()));
2660 toggleStarsAction->setCheckable(true);
2661
2662#ifdef HAVE_DATAVISUALIZATION
2663 toggleProfileAction =
2664 floatingToolBar->addAction(QIcon::fromTheme("star-profile", QIcon(":/icons/star_profile.svg")),
2665 i18n("View Star Profile..."), this, SLOT(toggleStarProfile()));
2666 toggleProfileAction->setCheckable(true);
2667#endif
2668
2669 if (mode == FITS_NORMAL || mode == FITS_ALIGN)
2670 {
2671 floatingToolBar->addSeparator();
2672
2673 toggleEQGridAction =
2674 floatingToolBar->addAction(QIcon::fromTheme("kstars_grid"),
2675 i18n("Show Equatorial Gridlines"), this, &FITSView::toggleEQGrid);
2676 toggleEQGridAction->setCheckable(true);
2677 toggleEQGridAction->setEnabled(false);
2678
2679 toggleObjectsAction =
2680 floatingToolBar->addAction(QIcon::fromTheme("help-hint"),
2681 i18n("Show Objects in Image"), this, &FITSView::toggleObjects);
2682 toggleObjectsAction->setCheckable(true);
2683 toggleObjectsAction->setEnabled(false);
2684
2685 centerTelescopeAction =
2686 floatingToolBar->addAction(QIcon::fromTheme("center_telescope", QIcon(":/icons/center_telescope.svg")),
2687 i18n("Center Telescope"), this, &FITSView::centerTelescope);
2688 centerTelescopeAction->setCheckable(true);
2689 centerTelescopeAction->setEnabled(false);
2690
2691 toggleHiPSOverlayAction =
2692 floatingToolBar->addAction(QIcon::fromTheme("pixelate"),
2693 i18n("Show HiPS Overlay"), this, &FITSView::toggleHiPSOverlay);
2694 toggleHiPSOverlayAction->setCheckable(true);
2695 toggleHiPSOverlayAction->setEnabled(false);
2696 }
2697}
2698
2699/**
2700 This method either enables or disables the scope mouse mode so you can slew your scope to coordinates
2701 just by clicking the mouse on a spot in the image.
2702 */
2703
2704void FITSView::centerTelescope()
2705{
2706 if (imageHasWCS())
2707 {
2708 if (getCursorMode() == FITSView::scopeCursor)
2709 {
2710 setCursorMode(lastMouseMode);
2711 }
2712 else
2713 {
2714 lastMouseMode = getCursorMode();
2715 setCursorMode(FITSView::scopeCursor);
2716 }
2717 updateFrame();
2718 }
2719 updateScopeButton();
2720}
2721
2722void FITSView::updateScopeButton()
2723{
2724 if (centerTelescopeAction != nullptr)
2725 {
2726 if (getCursorMode() == FITSView::scopeCursor)
2727 {
2728 centerTelescopeAction->setChecked(true);
2729 }
2730 else
2731 {
2732 centerTelescopeAction->setChecked(false);
2733 }
2734 }
2735}
2736
2737/**
2738This method just verifies if INDI is online, a telescope present, and is connected
2739 */
2740
2741bool FITSView::isTelescopeActive()
2742{
2743#ifdef HAVE_INDI
2744 if (INDIListener::Instance()->size() == 0)
2745 {
2746 return false;
2747 }
2748
2749 for (auto &oneDevice : INDIListener::devices())
2750 {
2751 if (!(oneDevice->getDriverInterface() & INDI::BaseDevice::TELESCOPE_INTERFACE))
2752 continue;
2753 return oneDevice->isConnected();
2754 }
2755 return false;
2756#else
2757 return false;
2758#endif
2759}
2760
2761void FITSView::setStarsEnabled(bool enable)
2762{
2763 markStars = enable;
2764 if (floatingToolBar != nullptr)
2765 {
2766 foreach (QAction * action, floatingToolBar->actions())
2767 {
2768 if (action->text() == i18n("Detect Stars in Image"))
2769 {
2770 action->setChecked(markStars);
2771 break;
2772 }
2773 }
2774 }
2775}
2776
2777void FITSView::setStarsHFREnabled(bool enable)
2778{
2779 showStarsHFR = enable;
2780}
2781
2782void FITSView::setStretchValues(double shadows, double midtones, double highlights)
2783{
2784 StretchParams params = getStretchParams();
2785 params.grey_red.shadows = shadows;
2786 params.grey_red.midtones = midtones;
2787 params.grey_red.highlights = highlights;
2788 //setPreviewSampling(0);
2789 setStretchParams(params);
2790}
2791
2792void FITSView::setAutoStretch()
2793{
2794 if (!getAutoStretch())
2795 setAutoStretchParams();
2796}
2797
virtual void updateCoordsNow(const KSNumbers *num)
updateCoordsNow Shortcut for updateCoords( const KSNumbers *num, false, nullptr, nullptr,...
Definition skypoint.h:410
dms angularDistanceTo(const SkyPoint *sp, double *const positionAngle=nullptr) const
Computes the angular distance between two SkyObjects.
Definition skypoint.cpp:919
const double & Degrees() const
Definition dms.h:141
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
KREPORT_EXPORT QPageSize::PageSizeId pageSize(const QString &key)
QAction * zoom(const QObject *recvr, const char *slot, QObject *parent)
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
QScrollBar * horizontalScrollBar() const const
QScrollBar * verticalScrollBar() const const
QWidget * viewport() const const
virtual void wheelEvent(QWheelEvent *e) override
void setCheckable(bool)
void setChecked(bool)
const char * constData() const const
QPoint pos()
qreal pointSizeF() const const
void setPointSize(int pointSize)
void setPointSizeF(qreal pointSize)
QSize size(int flags, const QString &text, int tabStops, int *tabArray) const const
T result() const const
void waitForFinished()
void restoreOverrideCursor()
void setOverrideCursor(const QCursor &cursor)
QIcon fromTheme(const QString &name)
Format_ARGB32_Premultiplied
bool isNull() const const
QList< QByteArray > supportedImageFormats()
qsizetype size() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
bool disconnect(const QMetaObject::Connection &connection)
bool inherits(const char *className) const const
int height() const const
int width() const const
QPaintDevice * device() const const
void drawEllipse(const QPoint &center, int rx, int ry)
void drawLine(const QLine &line)
void drawPixmap(const QPoint &point, const QPixmap &pixmap)
void drawPoints(const QPoint *points, int pointCount)
void drawRect(const QRect &rectangle)
void drawText(const QPoint &position, const QString &text)
void fillRect(const QRect &rectangle, QGradient::Preset preset)
const QFont & font() const const
QFontMetrics fontMetrics() const const
void restore()
void save()
void setBrush(Qt::BrushStyle style)
void setFont(const QFont &font)
void setOpacity(qreal opacity)
void setPen(Qt::PenStyle style)
void setRenderHint(RenderHint hint, bool on)
QPixmap fromImage(QImage &&image, Qt::ImageConversionFlags flags)
QPixmap scaled(const QSize &size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
bool isNull() const const
void setX(int x)
void setY(int y)
int x() const const
int y() const const
qreal x() const const
qreal y() const const
bool contains(const QPoint &point, bool proper) const const
void setAlignment(Qt::Alignment)
void ensureVisible(int x, int y, int xmargin, int ymargin)
virtual bool event(QEvent *e) override
virtual void resizeEvent(QResizeEvent *) override
void setWidget(QWidget *widget)
QWidget * widget() const const
int height() const const
int width() const const
QString arg(Args &&... args) const const
QString number(double n, char format, int precision)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QByteArray toLatin1() const const
AlignCenter
KeepAspectRatio
FDiagPattern
PointingHandCursor
GestureFinished
PinchGesture
MouseEventSynthesizedBySystem
DashLine
ReturnByValueConstant
TextSingleLine
FastTransformation
QTextStream & center(QTextStream &stream)
QFuture< void > filter(QThreadPool *pool, Sequence &sequence, KeepFunctor &&filterFunction)
QFuture< T > run(Function function,...)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
QUuid createUuid()
QString toString(StringFormat mode) const const
QString toString() const const
QWidget(QWidget *parent, Qt::WindowFlags f)
void setCursor(const QCursor &)
QPoint mapFromGlobal(const QPoint &pos) const const
QPoint mapFromParent(const QPoint &pos) const const
QRegion mask() const const
QWidget * parentWidget() const const
void show()
void update()
bool isVisible() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 25 2025 11:58:36 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.