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(
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 {
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 (!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 QImage scaledImage = rawImage.scaled(currentWidth, currentHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);
963 if (!initDisplayPixmap(scaledImage, currentZoom / ZOOM_DEFAULT))
964 return;
965
966 QPainter painter(&displayPixmap);
967 // Possibly scale the fonts as we're drawing on the full image, not just the visible part of the scroll window.
968 QFont font = painter.font();
969 drawStarRingFilter(&painter, currentZoom / ZOOM_DEFAULT, dynamic_cast<ImageRingMask *>(m_ImageMask.get()));
970 drawOverlay(&painter, currentZoom / ZOOM_DEFAULT);
971 m_ImageFrame->setPixmap(displayPixmap);
972 m_ImageFrame->resize(currentWidth, currentHeight);
973}
974
975void FITSView::drawStarRingFilter(QPainter *painter, double scale, ImageRingMask *ringMask)
976{
977 if (ringMask == nullptr || !ringMask->active())
978 return;
979
980 const double w = m_ImageData->width() * scale;
981 const double h = m_ImageData->height() * scale;
982 double const diagonal = std::sqrt(w * w + h * h) / 2;
983 int const innerRadius = std::lround(diagonal * ringMask->innerRadius());
984 int const outerRadius = std::lround(diagonal * ringMask->outerRadius());
985 QPoint const center(w / 2, h / 2);
986 painter->save();
987 painter->setPen(QPen(Qt::blue, scaleSize(1), Qt::DashLine));
988 painter->setOpacity(0.7);
990 painter->drawEllipse(center, outerRadius, outerRadius);
992 painter->drawEllipse(center, innerRadius, innerRadius);
993 painter->restore();
994}
995
996namespace
997{
998
999template <typename T>
1000int drawClippingOneChannel(T *inputBuffer, QPainter *painter, int width, int height, double clipVal, double scale)
1001{
1002 int numClipped = 0;
1003 painter->save();
1004 painter->setPen(QPen(Qt::red, scale, Qt::SolidLine));
1005 const T clipping = clipVal;
1006 constexpr int timeoutMilliseconds = 3 * 1000;
1007 QElapsedTimer timer;
1008 timer.start();
1009 QPoint p;
1010 for (int y = 0; y < height; y++)
1011 {
1012 auto inputLine = inputBuffer + y * width;
1013 p.setY(y);
1014 for (int x = 0; x < width; x++)
1015 {
1016 if (*inputLine++ > clipping)
1017 {
1018 numClipped++;
1019 const int start = x;
1020 // Use this inner loop to recognize strings of clipped pixels
1021 // and draw lines instead of multiple calls to drawPoints.
1022 while (true)
1023 {
1024 if (++x >= width)
1025 {
1026 painter->drawLine(start, y, width - 1, y);
1027 break;
1028 }
1029 if (*inputLine++ > clipping)
1030 numClipped++;
1031 else
1032 {
1033 if (x == start + 1)
1034 {
1035 p.setX(start);
1036 painter->drawPoints(&p, 1);
1037 }
1038 else
1039 painter->drawLine(start, y, x - 1, y);
1040 break;
1041 }
1042 }
1043 }
1044 }
1045 if (timer.elapsed() > timeoutMilliseconds)
1046 {
1047 painter->restore();
1048 return -1;
1049 }
1050 }
1051 painter->restore();
1052 return numClipped;
1053}
1054
1055template <typename T>
1056int drawClippingThreeChannels(T *inputBuffer, QPainter *painter, int width, int height, double clipVal, double scale)
1057{
1058 int numClipped = 0;
1059 painter->save();
1060 painter->setPen(QPen(Qt::red, scale, Qt::SolidLine));
1061 const T clipping = clipVal;
1062 constexpr int timeoutMilliseconds = 3 * 1000;
1063 QElapsedTimer timer;
1064 timer.start();
1065 QPoint p;
1066 const int size = width * height;
1067 for (int y = 0; y < height; y++)
1068 {
1069 // R, G, B input images are stored one after another.
1070 const T * inputLineR = inputBuffer + y * width;
1071 const T * inputLineG = inputLineR + size;
1072 const T * inputLineB = inputLineG + size;
1073 p.setY(y);
1074
1075 for (int x = 0; x < width; x++)
1076 {
1077 T inputR = inputLineR[x];
1078 T inputG = inputLineG[x];
1079 T inputB = inputLineB[x];
1080
1081 if (inputR > clipping || inputG > clipping || inputB > clipping)
1082 {
1083 numClipped++;
1084 const int start = x;
1085
1086 // Use this inner loop to recognize strings of clipped pixels
1087 // and draw lines instead of multiple calls to drawPoints.
1088 while (true)
1089 {
1090 if (++x >= width)
1091 {
1092 painter->drawLine(start, y, width - 1, y);
1093 break;
1094 }
1095 T inputR2 = inputLineR[x];
1096 T inputG2 = inputLineG[x];
1097 T inputB2 = inputLineB[x];
1098 if (inputR2 > clipping || inputG2 > clipping || inputB2 > clipping)
1099 numClipped++;
1100 else
1101 {
1102 if (x == start + 1)
1103 {
1104 p.setX(start);
1105 painter->drawPoints(&p, 1);
1106 }
1107 else
1108 painter->drawLine(start, y, x - 1, y);
1109 break;
1110 }
1111 }
1112 }
1113 }
1114 if (timer.elapsed() > timeoutMilliseconds)
1115 {
1116 painter->restore();
1117 return -1;
1118 }
1119 }
1120 painter->restore();
1121 return numClipped;
1122}
1123
1124template <typename T>
1125int drawClip(T *input_buffer, int num_channels, QPainter *painter, int width, int height, double clipVal, double scale)
1126{
1127 if (num_channels == 1)
1128 return drawClippingOneChannel(input_buffer, painter, width, height, clipVal, scale);
1129 else if (num_channels == 3)
1130 return drawClippingThreeChannels(input_buffer, painter, width, height, clipVal, scale);
1131 else return 0;
1132}
1133
1134} // namespace
1135
1136void FITSView::drawClipping(QPainter *painter)
1137{
1138 auto input = m_ImageData->getImageBuffer();
1139 const int height = m_ImageData->height();
1140 const int width = m_ImageData->width();
1141 const double FLOAT_CLIP = Options::clipping64KValue();
1142 const double SHORT_CLIP = Options::clipping64KValue();
1143 const double USHORT_CLIP = Options::clipping64KValue();
1144 const double BYTE_CLIP = Options::clipping256Value();
1145 switch (m_ImageData->dataType())
1146 {
1147 case TBYTE:
1148 m_NumClipped = drawClip(reinterpret_cast<uint8_t const*>(input), m_ImageData->channels(), painter, width, height, BYTE_CLIP,
1149 scaleSize(1));
1150 break;
1151 case TSHORT:
1152 m_NumClipped = drawClip(reinterpret_cast<short const*>(input), m_ImageData->channels(), painter, width, height, SHORT_CLIP,
1153 scaleSize(1));
1154 break;
1155 case TUSHORT:
1156 m_NumClipped = drawClip(reinterpret_cast<unsigned short const*>(input), m_ImageData->channels(), painter, width, height,
1157 USHORT_CLIP,
1158 scaleSize(1));
1159 break;
1160 case TLONG:
1161 m_NumClipped = drawClip(reinterpret_cast<long const*>(input), m_ImageData->channels(), painter, width, height, USHORT_CLIP,
1162 scaleSize(1));
1163 break;
1164 case TFLOAT:
1165 m_NumClipped = drawClip(reinterpret_cast<float const*>(input), m_ImageData->channels(), painter, width, height, FLOAT_CLIP,
1166 scaleSize(1));
1167 break;
1168 case TLONGLONG:
1169 m_NumClipped = drawClip(reinterpret_cast<long long const*>(input), m_ImageData->channels(), painter, width, height,
1170 USHORT_CLIP,
1171 scaleSize(1));
1172 break;
1173 case TDOUBLE:
1174 m_NumClipped = drawClip(reinterpret_cast<double const*>(input), m_ImageData->channels(), painter, width, height, FLOAT_CLIP,
1175 scaleSize(1));
1176 break;
1177 default:
1178 m_NumClipped = 0;
1179 break;
1180 }
1181 if (m_NumClipped < 0)
1182 emit newStatus(QString("Clip:failed"), FITS_CLIP);
1183 else
1184 emit newStatus(QString("Clip:%1").arg(m_NumClipped), FITS_CLIP);
1185}
1186
1187void FITSView::ZoomDefault()
1188{
1189 if (m_ImageFrame)
1190 {
1191 emit actionUpdated("view_zoom_out", true);
1192 emit actionUpdated("view_zoom_in", true);
1193
1194 currentZoom = ZOOM_DEFAULT;
1195 currentWidth = m_ImageData->width();
1196 currentHeight = m_ImageData->height();
1197
1198 updateFrame();
1199
1200 emitZoom();
1201
1202 update();
1203 }
1204}
1205
1206void FITSView::drawOverlay(QPainter * painter, double scale)
1207{
1208 painter->setRenderHint(QPainter::Antialiasing, Options::useAntialias());
1209
1210#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1211 if (showHiPSOverlay)
1212 drawHiPSOverlay(painter, scale);
1213#endif
1214
1215 if (trackingBoxEnabled && getCursorMode() != FITSView::scopeCursor)
1216 drawTrackingBox(painter, scale);
1217
1218 if (!markerCrosshair.isNull())
1219 drawMarker(painter, scale);
1220
1221 if (showCrosshair)
1222 drawCrosshair(painter, scale);
1223
1224 if (showObjects)
1225 drawObjectNames(painter, scale);
1226
1227#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1228 if (showEQGrid)
1229 drawEQGrid(painter, scale);
1230#endif
1231
1232 if (showPixelGrid)
1233 drawPixelGrid(painter, scale);
1234
1235 if (markStars)
1236 drawStarCentroid(painter, scale);
1237
1238 if (showClipping)
1239 drawClipping(painter);
1240
1241 if (showMagnifyingGlass)
1242 drawMagnifyingGlass(painter, scale);
1243}
1244
1245// Draws a 100% resolution image rectangle around the mouse position.
1246void FITSView::drawMagnifyingGlass(QPainter *painter, double scale)
1247{
1248 if (magnifyingGlassX >= 0 && magnifyingGlassY >= 0 &&
1249 magnifyingGlassX < m_ImageData->width() &&
1250 magnifyingGlassY < m_ImageData->height())
1251 {
1252 // Amount of magnification.
1253 constexpr double magAmount = 8;
1254 // Desired size in pixels of the magnification window.
1255 constexpr int magWindowSize = 130;
1256 // The distance from the mouse position to the magnifying glass rectangle, in the source image coordinates.
1257 const int winXOffset = magWindowSize * 10.0 / currentZoom;
1258 const int winYOffset = magWindowSize * 10.0 / currentZoom;
1259 // Size of a side of the square of input to make a window that size.
1260 const int inputDimension = magWindowSize * 100 / currentZoom;
1261 // Size of the square drawn. Not the same, necessarily as the magWindowSize,
1262 // since the output may be scaled (if isLargeImage()==true) to become screen pixels.
1263 const int outputDimension = inputDimension * scale + .99;
1264
1265 // Where the source data (to be magnified) comes from.
1266 int imgLeft = magnifyingGlassX - inputDimension / (2 * magAmount);
1267 int imgTop = magnifyingGlassY - inputDimension / (2 * magAmount);
1268
1269 // Where we'll draw the magnifying glass rectangle.
1270 int winLeft = magnifyingGlassX + winXOffset;
1271 int winTop = magnifyingGlassY + winYOffset;
1272
1273 // Normally we place the magnifying glass rectangle to the right and below the mouse curson.
1274 // However, if it would be rendered outside the image, put it on the other side.
1275 int w = rawImage.width();
1276 int h = rawImage.height();
1277 const int rightLimit = std::min(w, static_cast<int>((horizontalScrollBar()->value() + width()) * 100 / currentZoom));
1278 const int bottomLimit = std::min(h, static_cast<int>((verticalScrollBar()->value() + height()) * 100 / currentZoom));
1279 if (winLeft + winXOffset + inputDimension > rightLimit)
1280 winLeft -= (2 * winXOffset + inputDimension);
1281 if (winTop + winYOffset + inputDimension > bottomLimit)
1282 winTop -= (2 * winYOffset + inputDimension);
1283
1284 // Blacken the output where magnifying outside the source image.
1285 if ((imgLeft < 0 ) ||
1286 (imgLeft + inputDimension / magAmount >= w) ||
1287 (imgTop < 0) ||
1288 (imgTop + inputDimension / magAmount > h))
1289 {
1290 painter->setBrush(QBrush(Qt::black));
1291 painter->drawRect(winLeft * scale, winTop * scale, outputDimension, outputDimension);
1292 painter->setBrush(QBrush(Qt::transparent));
1293 }
1294
1295 // Finally, draw the magnified image.
1296 painter->drawPixmap(QRect(winLeft * scale, winTop * scale, outputDimension, outputDimension),
1297 displayPixmap,
1298 QRect(imgLeft, imgTop, inputDimension / magAmount, inputDimension / magAmount));
1299 // Draw a white border.
1300 painter->setPen(QPen(Qt::white, scaleSize(1)));
1301 painter->drawRect(winLeft * scale, winTop * scale, outputDimension, outputDimension);
1302 }
1303}
1304
1305// x,y are the image coordinates where the magnifying glass is positioned.
1306void FITSView::updateMagnifyingGlass(int x, int y)
1307{
1308 if (!m_ImageData)
1309 return;
1310
1311 magnifyingGlassX = x;
1312 magnifyingGlassY = y;
1313 if (magnifyingGlassX == -1 && magnifyingGlassY == -1)
1314 {
1315 if (showMagnifyingGlass)
1316 updateFrame(true);
1317 showMagnifyingGlass = false;
1318 }
1319 else
1320 {
1321 showMagnifyingGlass = true;
1322 updateFrame(true);
1323 }
1324}
1325
1326void FITSView::updateMode(FITSMode fmode)
1327{
1328 mode = fmode;
1329}
1330
1331void FITSView::drawMarker(QPainter * painter, double scale)
1332{
1333 painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")),
1334 scaleSize(2)));
1335 painter->setBrush(Qt::NoBrush);
1336 const float pxperdegree = scale * (57.3 / 1.8);
1337
1338 const float s1 = 0.5 * pxperdegree;
1339 const float s2 = pxperdegree;
1340 const float s3 = 2.0 * pxperdegree;
1341
1342 const float x0 = scale * markerCrosshair.x();
1343 const float y0 = scale * markerCrosshair.y();
1344 const float x1 = x0 - 0.5 * s1;
1345 const float y1 = y0 - 0.5 * s1;
1346 const float x2 = x0 - 0.5 * s2;
1347 const float y2 = y0 - 0.5 * s2;
1348 const float x3 = x0 - 0.5 * s3;
1349 const float y3 = y0 - 0.5 * s3;
1350
1351 //Draw radial lines
1352 painter->drawLine(QPointF(x1, y0), QPointF(x3, y0));
1353 painter->drawLine(QPointF(x0 + s2, y0), QPointF(x0 + 0.5 * s1, y0));
1354 painter->drawLine(QPointF(x0, y1), QPointF(x0, y3));
1355 painter->drawLine(QPointF(x0, y0 + 0.5 * s1), QPointF(x0, y0 + s2));
1356 //Draw circles at 0.5 & 1 degrees
1357 painter->drawEllipse(QRectF(x1, y1, s1, s1));
1358 painter->drawEllipse(QRectF(x2, y2, s2, s2));
1359}
1360
1361bool FITSView::drawHFR(QPainter * painter, const QString &hfr, int x, int y)
1362{
1363 QRect const boundingRect(0, 0, painter->device()->width(), painter->device()->height());
1364 QSize const hfrSize = painter->fontMetrics().size(Qt::TextSingleLine, hfr);
1365
1366 // Store the HFR text in a rect
1367 QPoint const hfrBottomLeft(x, y);
1368 QRect const hfrRect(hfrBottomLeft.x(), hfrBottomLeft.y() - hfrSize.height(), hfrSize.width(), hfrSize.height());
1369
1370 // Render the HFR text only if it can be displayed entirely
1371 if (boundingRect.contains(hfrRect))
1372 {
1373 painter->setPen(QPen(Qt::red, scaleSize(3)));
1374 painter->drawText(hfrBottomLeft, hfr);
1375 painter->setPen(QPen(Qt::red, scaleSize(2)));
1376 return true;
1377 }
1378 return false;
1379}
1380
1381
1382void FITSView::drawStarCentroid(QPainter * painter, double scale)
1383{
1384 QFont painterFont;
1385 double fontSize = painterFont.pointSizeF() * 2;
1387 if (showStarsHFR)
1388 {
1389 // If we need to print the HFR out, give an arbitrarily sized font to the painter
1390 if (isLargeImage())
1391 fontSize = scaleSize(painterFont.pointSizeF());
1392 painterFont.setPointSizeF(fontSize);
1393 painter->setFont(painterFont);
1394 }
1395
1396 painter->setPen(QPen(Qt::red, scaleSize(2)));
1397 ImageMosaicMask *mask = dynamic_cast<ImageMosaicMask *>(m_ImageMask.get());
1398
1399 for (auto const &starCenter : m_ImageData->getStarCenters())
1400 {
1401 int const w = std::round(starCenter->width) * scale;
1402
1403 // translate if a mosaic mask is present
1404 const QPointF center = (mask == nullptr) ? QPointF(starCenter->x, starCenter->y) : mask->translate(QPointF(starCenter->x,
1405 starCenter->y));
1406 // Draw a circle around the detected star.
1407 // SEP coordinates are in the center of pixels, and Qt at the boundary.
1408 const double xCoord = center.x() - 0.5;
1409 const double yCoord = center.y() - 0.5;
1410 const int xc = std::round((xCoord - starCenter->width / 2.0f) * scale);
1411 const int yc = std::round((yCoord - starCenter->width / 2.0f) * scale);
1412 const int hw = w / 2;
1413
1414 BahtinovEdge* bEdge = dynamic_cast<BahtinovEdge*>(starCenter);
1415 if (bEdge != nullptr)
1416 {
1417 // Draw lines of diffraction pattern
1418 painter->setPen(QPen(Qt::red, scaleSize(2)));
1419 painter->drawLine(bEdge->line[0].x1() * scale, bEdge->line[0].y1() * scale,
1420 bEdge->line[0].x2() * scale, bEdge->line[0].y2() * scale);
1421 painter->setPen(QPen(Qt::green, scaleSize(2)));
1422 painter->drawLine(bEdge->line[1].x1() * scale, bEdge->line[1].y1() * scale,
1423 bEdge->line[1].x2() * scale, bEdge->line[1].y2() * scale);
1424 painter->setPen(QPen(Qt::darkGreen, scaleSize(2)));
1425 painter->drawLine(bEdge->line[2].x1() * scale, bEdge->line[2].y1() * scale,
1426 bEdge->line[2].x2() * scale, bEdge->line[2].y2() * scale);
1427
1428 // Draw center circle
1429 painter->setPen(QPen(Qt::white, scaleSize(2)));
1430 painter->drawEllipse(xc, yc, w, w);
1431
1432 // Draw offset circle
1433 double factor = 15.0;
1434 QPointF offsetVector = (bEdge->offset - QPointF(center.x(), center.y())) * factor;
1435 int const xo = std::round((center.x() + offsetVector.x() - starCenter->width / 2.0f) * scale);
1436 int const yo = std::round((center.y() + offsetVector.y() - starCenter->width / 2.0f) * scale);
1437 painter->setPen(QPen(Qt::red, scaleSize(2)));
1438 painter->drawEllipse(xo, yo, w, w);
1439
1440 // Draw line between center circle and offset circle
1441 painter->setPen(QPen(Qt::red, scaleSize(2)));
1442 painter->drawLine(xc + hw, yc + hw, xo + hw, yo + hw);
1443 }
1444 else
1445 {
1446 if (!showStarsHFR)
1447 {
1448 const double radius = starCenter->HFR > 0 ? 2.0f * starCenter->HFR * scale : w;
1449 painter->drawEllipse(QPointF(xCoord * scale, yCoord * scale), radius, radius);
1450 }
1451 }
1452
1453 if (showStarsHFR)
1454 {
1455 // Ask the painter how large will the HFR text be
1456 QString const hfr = QString("%1").arg(starCenter->HFR, 0, 'f', 2);
1457 if (!drawHFR(painter, hfr, xc + w + 5, yc + w / 2))
1458 {
1459 // Try a few more time with smaller fonts;
1460 for (int i = 0; i < 10; ++i)
1461 {
1462 const double tempFontSize = painterFont.pointSizeF() - 2;
1463 if (tempFontSize <= 0) break;
1464 painterFont.setPointSizeF(tempFontSize);
1465 painter->setFont(painterFont);
1466 if (drawHFR(painter, hfr, xc + w + 5, yc + w / 2))
1467 break;
1468 }
1469 // Reset the font size.
1470 painterFont.setPointSize(fontSize);
1471 painter->setFont(painterFont);
1472 }
1473 }
1474 }
1475}
1476
1477void FITSView::drawTrackingBox(QPainter * painter, double scale)
1478{
1479 painter->setPen(QPen(Qt::green, scaleSize(2)));
1480
1481 if (trackingBox.isNull())
1482 return;
1483
1484 const int x1 = trackingBox.x() * scale;
1485 const int y1 = trackingBox.y() * scale;
1486 const int w = trackingBox.width() * scale;
1487 const int h = trackingBox.height() * scale;
1488
1489 painter->drawRect(x1, y1, w, h);
1490}
1491
1492/**
1493This Method draws a large Crosshair in the center of the image, it is like a set of axes.
1494 */
1495
1496void FITSView::drawCrosshair(QPainter * painter, double scale)
1497{
1498 if (!m_ImageData) return;
1499 const int image_width = m_ImageData->width();
1500 const int image_height = m_ImageData->height();
1501 const QPointF c = QPointF((qreal)image_width / 2 * scale, (qreal)image_height / 2 * scale);
1502 const float midX = (float)image_width / 2 * scale;
1503 const float midY = (float)image_height / 2 * scale;
1504 const float maxX = (float)image_width * scale;
1505 const float maxY = (float)image_height * scale;
1506 const float r = 50 * scale;
1507
1508 painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")), scaleSize(1)));
1509
1510 //Horizontal Line to Circle
1511 painter->drawLine(0, midY, midX - r, midY);
1512
1513 //Horizontal Line past Circle
1514 painter->drawLine(midX + r, midY, maxX, midY);
1515
1516 //Vertical Line to Circle
1517 painter->drawLine(midX, 0, midX, midY - r);
1518
1519 //Vertical Line past Circle
1520 painter->drawLine(midX, midY + r, midX, maxY);
1521
1522 //Circles
1523 painter->drawEllipse(c, r, r);
1524 painter->drawEllipse(c, r / 2, r / 2);
1525}
1526
1527/**
1528This method is intended to draw a pixel grid onto the image. It first determines useful information
1529from the image. Then it draws the axes on the image if the crosshairs are not displayed.
1530Finally it draws the gridlines so that there will be 4 Gridlines on either side of the axes.
1531Note: This has to start drawing at the center not at the edges because the center axes must
1532be in the center of the image.
1533 */
1534
1535void FITSView::drawPixelGrid(QPainter * painter, double scale)
1536{
1537 const float width = m_ImageData->width() * scale;
1538 const float height = m_ImageData->height() * scale;
1539 const float cX = width / 2;
1540 const float cY = height / 2;
1541 const float deltaX = width / 10;
1542 const float deltaY = height / 10;
1543 QFontMetrics fm(painter->font());
1544
1545 //draw the Axes
1546 painter->setPen(QPen(Qt::red, scaleSize(1)));
1547 painter->drawText(cX - 30, height - 5, QString::number((int)((cX) / scale)));
1548 QString str = QString::number((int)((cY) / scale));
1549#if QT_VERSION < QT_VERSION_CHECK(5,11,0)
1550 painter->drawText(width - (fm.width(str) + 10), cY - 5, str);
1551#else
1552 painter->drawText(width - (fm.horizontalAdvance(str) + 10), cY - 5, str);
1553#endif
1554 if (!showCrosshair)
1555 {
1556 painter->drawLine(cX, 0, cX, height);
1557 painter->drawLine(0, cY, width, cY);
1558 }
1559 painter->setPen(QPen(Qt::gray, scaleSize(1)));
1560 //Start one iteration past the Center and draw 4 lines on either side of 0
1561 for (int x = deltaX; x < cX - deltaX; x += deltaX)
1562 {
1563 painter->drawText(cX + x - 30, height - 5, QString::number((int)(cX + x) / scale));
1564 painter->drawText(cX - x - 30, height - 5, QString::number((int)(cX - x) / scale));
1565 painter->drawLine(cX - x, 0, cX - x, height);
1566 painter->drawLine(cX + x, 0, cX + x, height);
1567 }
1568 //Start one iteration past the Center and draw 4 lines on either side of 0
1569 for (int y = deltaY; y < cY - deltaY; y += deltaY)
1570 {
1571 QString str = QString::number((int)((cY + y) / scale));
1572#if QT_VERSION < QT_VERSION_CHECK(5,11,0)
1573 painter->drawText(width - (fm.width(str) + 10), cY + y - 5, str);
1574#else
1575 painter->drawText(width - (fm.horizontalAdvance(str) + 10), cY + y - 5, str);
1576#endif
1577 str = QString::number((int)((cY - y) / scale));
1578#if QT_VERSION < QT_VERSION_CHECK(5,11,0)
1579 painter->drawText(width - (fm.width(str) + 10), cY - y - 5, str);
1580#else
1581 painter->drawText(width - (fm.horizontalAdvance(str) + 10), cY - y - 5, str);
1582#endif
1583 painter->drawLine(0, cY + y, width, cY + y);
1584 painter->drawLine(0, cY - y, width, cY - y);
1585 }
1586}
1587bool FITSView::imageHasWCS()
1588{
1589 if (m_ImageData != nullptr)
1590 return m_ImageData->hasWCS();
1591 return false;
1592}
1593
1594void FITSView::drawObjectNames(QPainter * painter, double scale)
1595{
1596 if (Options::fitsCatalog() != CAT_SKYMAP)
1597 {
1598#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1599 drawCatObjectNames(painter, scale);
1600#endif
1601 return;
1602 }
1603
1604 painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("FITSObjectLabelColor"))));
1605 for (const auto &listObject : m_ImageData->getSkyObjects())
1606 {
1607 painter->drawRect(listObject->x() * scale - 5, listObject->y() * scale - 5, 10, 10);
1608 painter->drawText(listObject->x() * scale + 10, listObject->y() * scale + 10, listObject->skyObject()->name());
1609 }
1610}
1611
1612#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1613void FITSView::drawCatObjectNames(QPainter * painter, double scale)
1614{
1615 if (!m_ImageData)
1616 return;
1617
1618 drawCatROI(painter, scale);
1619 bool showNames = Options::fitsCatObjShowNames();
1620 for (const auto &catObject : m_ImageData->getCatObjects())
1621 {
1622 if (!catObject.show)
1623 continue;
1624 if (catObject.highlight)
1625 painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("FITSObjectHighlightLabelColor"))));
1626 else
1627 painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("FITSObjectLabelColor"))));
1628
1629 painter->drawRect(catObject.x * scale - 5.0, catObject.y * scale - 5.0, 10, 10);
1630 if (showNames)
1631 painter->drawText(catObject.x * scale + 10.0, catObject.y * scale + 10.0, catObject.name);
1632 }
1633}
1634#endif
1635
1636#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1637void FITSView::drawCatROI(QPainter * painter, double scale)
1638{
1639 Q_UNUSED(scale);
1640 const QPoint pt = m_ImageData->catROIPt();
1641 const int radius = m_ImageData->catROIRadius();
1642 if (radius > 0)
1643 {
1644 painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("FITSObjectLabelColor"))));
1645 painter->drawEllipse(pt, radius, radius);
1646 }
1647}
1648#endif
1649
1650#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1651void FITSView::drawHiPSOverlay(QPainter * painter, double scale)
1652{
1653 if (m_HiPSOverlayPixmap.isNull())
1654 {
1655 auto width = m_ImageData->width();
1656 auto height = m_ImageData->height();
1658 SkyPoint startPoint;
1659 SkyPoint endPoint;
1660 SkyPoint centerPoint;
1661
1662 m_ImageData->pixelToWCS(QPointF(0, 0), startPoint);
1663 m_ImageData->pixelToWCS(QPointF(width - 1, height - 1), endPoint);
1664 m_ImageData->pixelToWCS(QPointF( (width - Options::hIPSOffsetX()) / 2.0, (height - Options::hIPSOffsetY()) / 2.0),
1665 centerPoint);
1666
1667 startPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
1668 endPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
1669 centerPoint.updateCoordsNow(KStarsData::Instance()->updateNum());
1670
1671 auto fov_radius = startPoint.angularDistanceTo(&endPoint).Degrees() / 2;
1672 QVariant PA (0.0);
1673 m_ImageData->getRecordValue("CROTA1", PA);
1674
1675 auto rotation = 180 - PA.toDouble();
1676 if (rotation > 360)
1677 rotation -= 360;
1678
1679 if (HIPSFinder::Instance()->renderFOV(&centerPoint, fov_radius, rotation, &image) == false)
1680 return;
1681 m_HiPSOverlayPixmap = QPixmap::fromImage(image);
1682 }
1683
1684 Q_UNUSED(scale);
1685 painter->setOpacity(Options::hIPSOpacity());
1686 painter->drawPixmap(0, 0, m_HiPSOverlayPixmap);
1687 painter->setOpacity(1);
1688}
1689#endif
1690
1691/**
1692This method will paint EQ Gridlines in an overlay if there is WCS data present.
1693It determines the minimum and maximum RA and DEC, then it uses that information to
1694judge which gridLines to draw. Then it calls the drawEQGridlines methods below
1695to draw gridlines at those specific RA and Dec values.
1696 */
1697
1698#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
1699void FITSView::drawEQGrid(QPainter * painter, double scale)
1700{
1701 const int image_width = m_ImageData->width();
1702 const int image_height = m_ImageData->height();
1703
1704 if (m_ImageData->hasWCS())
1705 {
1706 double maxRA = -1000;
1707 double minRA = 1000;
1708 double maxDec = -1000;
1709 double minDec = 1000;
1710 m_ImageData->findWCSBounds(minRA, maxRA, minDec, maxDec);
1711
1712 auto minDecMinutes = (int)(minDec * 12); //This will force the Dec Scale to 5 arc minutes in the loop
1713 auto maxDecMinutes = (int)(maxDec * 12);
1714
1715 auto minRAMinutes =
1716 (int)(minRA / 15.0 *
1717 120.0); //This will force the scale to 1/2 minutes of RA in the loop from 0 to 50 degrees
1718 auto maxRAMinutes = (int)(maxRA / 15.0 * 120.0);
1719
1720 double raConvert = 15 / 120.0; //This will undo the calculation above to retrieve the actual RA.
1721 double decConvert = 1.0 / 12.0; //This will undo the calculation above to retrieve the actual DEC.
1722
1723 if (maxDec > 50 || minDec < -50)
1724 {
1725 minRAMinutes =
1726 (int)(minRA / 15.0 * 60.0); //This will force the scale to 1 min of RA from 50 to 80 degrees
1727 maxRAMinutes = (int)(maxRA / 15.0 * 60.0);
1728 raConvert = 15 / 60.0;
1729 }
1730
1731 if (maxDec > 80 || minDec < -80)
1732 {
1733 minRAMinutes =
1734 (int)(minRA / 15.0 * 30); //This will force the scale to 2 min of RA from 80 to 85 degrees
1735 maxRAMinutes = (int)(maxRA / 15.0 * 30);
1736 raConvert = 15 / 30.0;
1737 }
1738 if (maxDec > 85 || minDec < -85)
1739 {
1740 minRAMinutes =
1741 (int)(minRA / 15.0 * 6); //This will force the scale to 10 min of RA from 85 to 89 degrees
1742 maxRAMinutes = (int)(maxRA / 15.0 * 6);
1743 raConvert = 15 / 6.0;
1744 }
1745 if (maxDec >= 89.25 || minDec <= -89.25)
1746 {
1747 minRAMinutes =
1748 (int)(minRA /
1749 15); //This will force the scale to whole hours of RA in the loop really close to the poles
1750 maxRAMinutes = (int)(maxRA / 15);
1751 raConvert = 15;
1752 }
1753
1754 painter->setPen(QPen(Qt::yellow));
1755
1756 QPointF pixelPoint, imagePoint, pPoint;
1757
1758 //This section draws the RA Gridlines
1759
1760 for (int targetRA = minRAMinutes; targetRA <= maxRAMinutes; targetRA++)
1761 {
1762 painter->setPen(QPen(Qt::yellow));
1763 double target = targetRA * raConvert;
1764
1765 if (eqGridPoints.count() != 0)
1766 eqGridPoints.clear();
1767
1768 double increment = std::abs((maxDec - minDec) /
1769 100.0); //This will determine how many points to use to create the RA Line
1770
1771 for (double targetDec = minDec; targetDec <= maxDec; targetDec += increment)
1772 {
1773 SkyPoint pointToGet(target / 15.0, targetDec);
1774 bool inImage = m_ImageData->wcsToPixel(pointToGet, pixelPoint, imagePoint);
1775 if (inImage)
1776 {
1777 QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale);
1778 eqGridPoints.append(pt);
1779 }
1780 }
1781
1782 if (eqGridPoints.count() > 1)
1783 {
1784 for (int i = 1; i < eqGridPoints.count(); i++)
1785 painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i));
1786 QString str = QString::number(dms(target).hour()) + "h " +
1787 QString::number(dms(target).minute()) + '\'';
1788 if (maxDec <= 50 && maxDec >= -50)
1789 str = str + " " + QString::number(dms(target).second()) + "''";
1790 QPointF pt = getPointForGridLabel(painter, str, scale);
1791 if (pt.x() != -100)
1792 painter->drawText(pt.x(), pt.y(), str);
1793 }
1794 }
1795
1796 //This section draws the DEC Gridlines
1797
1798 for (int targetDec = minDecMinutes; targetDec <= maxDecMinutes; targetDec++)
1799 {
1800 if (eqGridPoints.count() != 0)
1801 eqGridPoints.clear();
1802
1803 double increment = std::abs((maxRA - minRA) /
1804 100.0); //This will determine how many points to use to create the Dec Line
1805 double target = targetDec * decConvert;
1806
1807 for (double targetRA = minRA; targetRA <= maxRA; targetRA += increment)
1808 {
1809 SkyPoint pointToGet(targetRA / 15, targetDec * decConvert);
1810 bool inImage = m_ImageData->wcsToPixel(pointToGet, pixelPoint, imagePoint);
1811 if (inImage)
1812 {
1813 QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale);
1814 eqGridPoints.append(pt);
1815 }
1816 }
1817 if (eqGridPoints.count() > 1)
1818 {
1819 for (int i = 1; i < eqGridPoints.count(); i++)
1820 painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i));
1821 QString str = QString::number(dms(target).degree()) + "° " + QString::number(dms(target).arcmin()) + '\'';
1822 QPointF pt = getPointForGridLabel(painter, str, scale);
1823 if (pt.x() != -100)
1824 painter->drawText(pt.x(), pt.y(), str);
1825 }
1826 }
1827
1828 //This Section Draws the North Celestial Pole if present
1829 SkyPoint NCP(0, 90);
1830
1831 bool NCPtest = m_ImageData->wcsToPixel(NCP, pPoint, imagePoint);
1832 if (NCPtest)
1833 {
1834 bool NCPinImage =
1835 (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height);
1836 if (NCPinImage)
1837 {
1838 painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4,
1839 KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"));
1840 painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15,
1841 i18nc("North Celestial Pole", "NCP"));
1842 }
1843 }
1844
1845 //This Section Draws the South Celestial Pole if present
1846 SkyPoint SCP(0, -90);
1847
1848 bool SCPtest = m_ImageData->wcsToPixel(SCP, pPoint, imagePoint);
1849 if (SCPtest)
1850 {
1851 bool SCPinImage =
1852 (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height);
1853 if (SCPinImage)
1854 {
1855 painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4,
1856 KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"));
1857 painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15,
1858 i18nc("South Celestial Pole", "SCP"));
1859 }
1860 }
1861 }
1862}
1863#endif
1864
1865bool FITSView::pointIsInImage(QPointF pt, double scale)
1866{
1867 int image_width = m_ImageData->width();
1868 int image_height = m_ImageData->height();
1869 return pt.x() < image_width * scale && pt.y() < image_height * scale && pt.x() > 0 && pt.y() > 0;
1870}
1871
1872QPointF FITSView::getPointForGridLabel(QPainter *painter, const QString &str, double scale)
1873{
1874 QFontMetrics fm(painter->font());
1875#if QT_VERSION < QT_VERSION_CHECK(5,11,0)
1876 int strWidth = fm.width(str);
1877#else
1878 int strWidth = fm.horizontalAdvance(str);
1879#endif
1880 int strHeight = fm.height();
1881 int image_width = m_ImageData->width();
1882 int image_height = m_ImageData->height();
1883
1884 //These get the maximum X and Y points in the list that are in the image
1885 QPointF maxXPt(image_width * scale / 2, image_height * scale / 2);
1886 for (auto &p : eqGridPoints)
1887 {
1888 if (p.x() > maxXPt.x() && pointIsInImage(p, scale))
1889 maxXPt = p;
1890 }
1891 QPointF maxYPt(image_width * scale / 2, image_height * scale / 2);
1892
1893 for (auto &p : eqGridPoints)
1894 {
1895 if (p.y() > maxYPt.y() && pointIsInImage(p, scale))
1896 maxYPt = p;
1897 }
1898 QPointF minXPt(image_width * scale / 2, image_height * scale / 2);
1899
1900 for (auto &p : eqGridPoints)
1901 {
1902 if (p.x() < minXPt.x() && pointIsInImage(p, scale))
1903 minXPt = p;
1904 }
1905 QPointF minYPt(image_width * scale / 2, image_height * scale / 2);
1906
1907 for (auto &p : eqGridPoints)
1908 {
1909 if (p.y() < minYPt.y() && pointIsInImage(p, scale))
1910 minYPt = p;
1911 }
1912
1913 //This gives preference to points that are on the right hand side and bottom.
1914 //But if the line doesn't intersect the right or bottom, it then tries for the top and left.
1915 //If no points are found in the image, it returns a point off the screen
1916 //If all else fails, like in the case of a circle on the image, it returns the far right point.
1917
1918 if (image_width * scale - maxXPt.x() < strWidth)
1919 {
1920 return QPointF(
1921 image_width * scale - (strWidth + 10),
1922 maxXPt.y() -
1923 strHeight); //This will draw the text on the right hand side, up and to the left of the point where the line intersects
1924 }
1925 if (image_height * scale - maxYPt.y() < strHeight)
1926 return QPointF(
1927 maxYPt.x() - (strWidth + 10),
1928 image_height * scale -
1929 (strHeight + 10)); //This will draw the text on the bottom side, up and to the left of the point where the line intersects
1930 if (minYPt.y() < strHeight)
1931 return QPointF(
1932 minYPt.x() * scale + 10,
1933 strHeight + 20); //This will draw the text on the top side, down and to the right of the point where the line intersects
1934 if (minXPt.x() < strWidth)
1935 return QPointF(
1936 10,
1937 minXPt.y() * scale +
1938 strHeight +
1939 20); //This will draw the text on the left hand side, down and to the right of the point where the line intersects
1940 if (maxXPt.x() == image_width * scale / 2 && maxXPt.y() == image_height * scale / 2)
1941 return QPointF(-100, -100); //All of the points were off the screen
1942
1943 return QPoint(maxXPt.x() - (strWidth + 10), maxXPt.y() - (strHeight + 10));
1944}
1945
1946void FITSView::setFirstLoad(bool value)
1947{
1948 firstLoad = value;
1949}
1950
1951QPixmap &FITSView::getTrackingBoxPixmap(uint8_t margin)
1952{
1953 if (trackingBox.isNull())
1954 return trackingBoxPixmap;
1955
1956 // We need to know which rendering strategy updateFrame used to determine the scaling.
1957 const float scale = getScale();
1958
1959 int x1 = (trackingBox.x() - margin) * scale;
1960 int y1 = (trackingBox.y() - margin) * scale;
1961 int w = (trackingBox.width() + margin * 2) * scale;
1962 int h = (trackingBox.height() + margin * 2) * scale;
1963
1964 trackingBoxPixmap = m_ImageFrame->grab(QRect(x1, y1, w, h));
1965 return trackingBoxPixmap;
1966}
1967
1968void FITSView::setTrackingBox(const QRect &rect)
1969{
1970 if (rect != trackingBox)
1971 {
1972 trackingBox = rect;
1973 updateFrame();
1974 if(showStarProfile)
1975 viewStarProfile();
1976 }
1977}
1978
1979void FITSView::resizeTrackingBox(int newSize)
1980{
1981 int x = trackingBox.x() + trackingBox.width() / 2;
1982 int y = trackingBox.y() + trackingBox.height() / 2;
1983 int delta = newSize / 2;
1984 setTrackingBox(QRect( x - delta, y - delta, newSize, newSize));
1985}
1986
1987void FITSView::processRectangleFixed(int s)
1988{
1989 int w = m_ImageData->width();
1990 int h = m_ImageData->height();
1991
1992 QPoint c = selectionRectangleRaw.center();
1993 c.setX(qMax((int)round(s / 2.0), c.x()));
1994 c.setX(qMin(w - (int)round(s / 2.0), c.x()));
1995 c.setY(qMax((int)round(s / 2.0), c.y()));
1996 c.setY(qMin(h - (int)round(s / 2.0), c.y()));
1997
1998 QPoint topLeft, botRight;
1999 topLeft = QPoint(c.x() - round(s / 2.0), c.y() - round(s / 2.0));
2000 botRight = QPoint(c.x() + round(s / 2.0), c.y() + round(s / 2.0));
2001
2002 emit setRubberBand(QRect(topLeft, botRight));
2003 processRectangle(topLeft, botRight, true);
2004}
2005
2006void FITSView::processRectangle(QPoint p1, QPoint p2, bool calculate)
2007{
2008 if(!isSelectionRectShown())
2009 return;
2010 //the user can draw a rectangle by dragging the mouse to any direction
2011 //but we need to feed Rectangle(topleft, topright)
2012 //hence we calculate topleft and topright for each case
2013
2014 //p1 is the point where the user presses the mouse
2015 //p2 is the point where the user releases the mouse
2016 selectionRectangleRaw = QRect(p1, p2).normalized();
2017 //Index out of bounds Check for raw Rectangle, this effectively works when user does shift + drag, other wise becomes redundant
2018
2019 QPoint topLeft = selectionRectangleRaw.topLeft();
2020 QPoint botRight = selectionRectangleRaw.bottomRight();
2021
2022 topLeft.setX(qMax(1, topLeft.x()));
2023 topLeft.setY(qMax(1, topLeft.y()));
2024 botRight.setX(qMin((int)m_ImageData->width(), botRight.x()));
2025 botRight.setY(qMin((int)m_ImageData->height(), botRight.y()));
2026
2027 selectionRectangleRaw.setTopLeft(topLeft);
2028 selectionRectangleRaw.setBottomRight(botRight);
2029
2030 if(calculate)
2031 {
2032 if(m_ImageData)
2033 {
2034 m_ImageData->makeRoiBuffer(selectionRectangleRaw);
2035 emit rectangleUpdated(selectionRectangleRaw);
2036 }
2037 }
2038 //updateFrameRoi();
2039
2040 //emit raw rectangle for calculation
2041 //update the stats pane after calculation; there should be ample time for calculation before showing the values
2042}
2043
2044bool FITSView::isImageStretched()
2045{
2046 return stretchImage;
2047}
2048
2049bool FITSView::isClippingShown()
2050{
2051 return showClipping;
2052}
2053
2054bool FITSView::isCrosshairShown()
2055{
2056 return showCrosshair;
2057}
2058
2059bool FITSView::isEQGridShown()
2060{
2061 return showEQGrid;
2062}
2063
2064bool FITSView::isSelectionRectShown()
2065{
2066 return showSelectionRect;
2067}
2068bool FITSView::areObjectsShown()
2069{
2070 return showObjects;
2071}
2072
2073bool FITSView::isPixelGridShown()
2074{
2075 return showPixelGrid;
2076}
2077
2078bool FITSView::isHiPSOverlayShown()
2079{
2080 return showHiPSOverlay;
2081}
2082
2083void FITSView::toggleCrosshair()
2084{
2085 showCrosshair = !showCrosshair;
2086 updateFrame();
2087}
2088
2089void FITSView::toggleClipping()
2090{
2091 showClipping = !showClipping;
2092 updateFrame();
2093}
2094
2095void FITSView::toggleEQGrid()
2096{
2097 showEQGrid = !showEQGrid;
2098
2099 if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
2100 {
2101#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
2102 QFuture<bool> future = QtConcurrent::run(&FITSData::loadWCS, m_ImageData.data());
2103#else
2104 QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS);
2105#endif
2106 wcsWatcher.setFuture(future);
2107 return;
2108 }
2109
2110 if (m_ImageFrame)
2111 updateFrame();
2112}
2113
2114void FITSView::toggleHiPSOverlay()
2115{
2116 showHiPSOverlay = !showHiPSOverlay;
2117
2118 if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
2119 {
2120#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
2121 QFuture<bool> future = QtConcurrent::run(&FITSData::loadWCS, m_ImageData.data());
2122#else
2123 QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS);
2124#endif
2125 wcsWatcher.setFuture(future);
2126 return;
2127 }
2128
2129 if (m_ImageFrame)
2130 {
2131 m_QueueUpdate = true;
2132 updateFrame();
2133 }
2134}
2135
2136void FITSView::toggleSelectionMode()
2137{
2138 showSelectionRect = !showSelectionRect;
2139 if (!showSelectionRect)
2140 emit rectangleUpdated(QRect());
2141 else if (m_ImageData)
2142 {
2143 m_ImageData->makeRoiBuffer(selectionRectangleRaw);
2144 emit rectangleUpdated(selectionRectangleRaw);
2145 }
2146
2147 emit showRubberBand(showSelectionRect);
2148 if (m_ImageFrame)
2149 updateFrame();
2150
2151}
2152void FITSView::toggleObjects()
2153{
2154 showObjects = !showObjects;
2155
2156 if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
2157 {
2158#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
2159 QFuture<bool> future = QtConcurrent::run(&FITSData::loadWCS, m_ImageData.data());
2160#else
2161 QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS);
2162#endif
2163 wcsWatcher.setFuture(future);
2164 return;
2165 }
2166
2167 if (m_ImageFrame)
2168 {
2169#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2170 if (showObjects)
2171 m_ImageData->searchObjects();
2172 else
2173 emit catReset();
2174#endif
2175 updateFrame();
2176 }
2177}
2178
2179void FITSView::toggleStars()
2180{
2181 toggleStars(!markStars);
2182 if (m_ImageFrame)
2183 updateFrame();
2184}
2185
2186void FITSView::toggleStretch()
2187{
2188 stretchImage = !stretchImage;
2189 if (m_ImageFrame && rescale(ZOOM_KEEP_LEVEL))
2190 updateFrame();
2191}
2192
2193void FITSView::toggleStarProfile()
2194{
2195#ifdef HAVE_DATAVISUALIZATION
2196 showStarProfile = !showStarProfile;
2197 if(showStarProfile && trackingBoxEnabled)
2198 viewStarProfile();
2199 if(toggleProfileAction)
2200 toggleProfileAction->setChecked(showStarProfile);
2201
2202 if(showStarProfile)
2203 {
2204 //The tracking box is already on for Guide and Focus Views, but off for Normal and Align views.
2205 //So for Normal and Align views, we need to set up the tracking box.
2206 if(mode == FITS_NORMAL || mode == FITS_ALIGN)
2207 {
2208 setCursorMode(selectCursor);
2209 connect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int)));
2210 trackingBox = QRect(0, 0, 128, 128);
2211 setTrackingBoxEnabled(true);
2212 if(starProfileWidget)
2213 connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int)));
2214 }
2215 if(starProfileWidget)
2216 connect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile()));
2217 }
2218 else
2219 {
2220 //This shuts down the tracking box for Normal and Align Views
2221 //It doesn't touch Guide and Focus Views because they still need a tracking box
2222 if(mode == FITS_NORMAL || mode == FITS_ALIGN)
2223 {
2224 if(getCursorMode() == selectCursor)
2225 setCursorMode(dragCursor);
2226 disconnect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int)));
2227 setTrackingBoxEnabled(false);
2228 if(starProfileWidget)
2229 disconnect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int)));
2230 }
2231 if(starProfileWidget)
2232 {
2233 disconnect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile()));
2234 starProfileWidget->close();
2235 starProfileWidget = nullptr;
2236 }
2237 emit starProfileWindowClosed();
2238 }
2239 updateFrame();
2240#endif
2241}
2242
2243void FITSView::move3DTrackingBox(int x, int y)
2244{
2245 int boxSize = trackingBox.width();
2246 QRect starRect = QRect(x - boxSize / 2, y - boxSize / 2, boxSize, boxSize);
2247 setTrackingBox(starRect);
2248}
2249
2250void FITSView::viewStarProfile()
2251{
2252#ifdef HAVE_DATAVISUALIZATION
2253 if(!trackingBoxEnabled)
2254 {
2255 setTrackingBoxEnabled(true);
2256 setTrackingBox(QRect(0, 0, 128, 128));
2257 }
2258 if(!starProfileWidget)
2259 {
2260 starProfileWidget = new StarProfileViewer(this);
2261
2262 //This is a band-aid to fix a QT bug with createWindowContainer
2263 //It will set the cursor of the Window containing the view that called the Star Profile method to the Arrow Cursor
2264 //Note that Ekos Manager is a QDialog and FitsViewer is a KXmlGuiWindow
2265 QWidget * superParent = this->parentWidget();
2266 while(superParent->parentWidget() != 0 && !superParent->inherits("QDialog") && !superParent->inherits("KXmlGuiWindow"))
2267 superParent = superParent->parentWidget();
2268 superParent->setCursor(Qt::ArrowCursor);
2269 //This is the end of the band-aid
2270
2271 connect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile()));
2272 if(mode == FITS_ALIGN || mode == FITS_NORMAL)
2273 {
2274 starProfileWidget->enableTrackingBox(true);
2275 m_ImageData->setStarAlgorithm(ALGORITHM_CENTROID);
2276 connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int)));
2277 }
2278 }
2279 QList<Edge *> starCenters = m_ImageData->getStarCentersInSubFrame(trackingBox);
2280 if(starCenters.size() == 0)
2281 {
2282 // FIXME, the following does not work anymore.
2283 //m_ImageData->findStars(&trackingBox, true);
2284 // FIXME replacing it with this
2285 m_ImageData->findStars(ALGORITHM_CENTROID, trackingBox).waitForFinished();
2286 starCenters = m_ImageData->getStarCentersInSubFrame(trackingBox);
2287 }
2288
2289 starProfileWidget->loadData(m_ImageData, trackingBox, starCenters);
2290 starProfileWidget->show();
2291 starProfileWidget->raise();
2292 if(markStars)
2293 updateFrame(); //this is to update for the marked stars
2294
2295#endif
2296}
2297
2298void FITSView::togglePixelGrid()
2299{
2300 showPixelGrid = !showPixelGrid;
2301 updateFrame();
2302}
2303
2304QFuture<bool> FITSView::findStars(StarAlgorithm algorithm, const QRect &searchBox)
2305{
2306 if(trackingBoxEnabled)
2307 return m_ImageData->findStars(algorithm, trackingBox);
2308 else
2309 return m_ImageData->findStars(algorithm, searchBox);
2310}
2311
2312void FITSView::toggleStars(bool enable)
2313{
2314 markStars = enable;
2315
2316 if (markStars)
2317 searchStars();
2318}
2319
2320void FITSView::searchStars()
2321{
2322 QVariant frameType;
2323 if (m_ImageData->areStarsSearched() || !m_ImageData || (m_ImageData->getRecordValue("FRAME", frameType)
2324 && frameType.toString() != "Light"))
2325 return;
2326
2328 emit newStatus(i18n("Finding stars..."), FITS_MESSAGE);
2329 qApp->processEvents();
2330
2331#ifdef HAVE_STELLARSOLVER
2332 QVariantMap extractionSettings;
2333 extractionSettings["optionsProfileIndex"] = Options::hFROptionsProfile();
2334 extractionSettings["optionsProfileGroup"] = static_cast<int>(Ekos::HFRProfiles);
2335 imageData()->setSourceExtractorSettings(extractionSettings);
2336#endif
2337
2338 QFuture<bool> result = findStars(ALGORITHM_SEP);
2339 result.waitForFinished();
2340 if (result.result() && isVisible())
2341 {
2342 emit newStatus("", FITS_MESSAGE);
2343 }
2345}
2346
2347void FITSView::processPointSelection(int x, int y)
2348{
2349 emit trackingStarSelected(x, y);
2350}
2351
2352void FITSView::processMarkerSelection(int x, int y)
2353{
2354 markerCrosshair.setX(x);
2355 markerCrosshair.setY(y);
2356
2357 updateFrame();
2358}
2359
2360void FITSView::processHighlight(int x, int y)
2361{
2362 if (!m_ImageData)
2363 return;
2364
2365 const QPoint pt = QPoint(x, y);
2366 int candidate = -1;
2367 double candidateDist = -1.0;
2368 QList<CatObject> objs = m_ImageData->getCatObjects();
2369 for (int i = 0; i < objs.size(); i++)
2370 {
2371 if (!objs[i].show)
2372 continue;
2373
2374 // See if we have a direct hit - if multiple we'll just take the first one for speed
2375 const QRect rect = QRect(objs[i].x - (CAT_OBJ_BOX_SIZE / 2), objs[i].y - (CAT_OBJ_BOX_SIZE / 2),
2376 CAT_OBJ_BOX_SIZE, CAT_OBJ_BOX_SIZE);
2377 if (rect.contains(pt))
2378 {
2379 // Direct hit so no point in processing further
2380 candidate = i;
2381 break;
2382 }
2383
2384 // Also do a wider search and take the nearest
2385 const QRect bigRect = QRect(objs[i].x - 20, objs[i].y - 20, 40, 40);
2386 if (bigRect.contains(pt))
2387 {
2388 double distance = std::hypot(x - objs[i].x, y - objs[i].y);
2389 if (candidate < 0 || distance < candidateDist)
2390 {
2391 candidate = i;
2392 candidateDist = distance;
2393 }
2394 }
2395 }
2396 if (candidate >= 0)
2397 {
2398 m_ImageData->highlightCatObject(candidate, -1);
2399 emit catHighlightChanged(candidate);
2400 updateFrame();
2401 }
2402}
2403
2404void FITSView::processCircle(QPoint p1, QPoint p2)
2405{
2406#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2407 const double x = p1.x() - p2.x();
2408 const double y = p1.y() - p2.y();
2409 const int radius = std::hypot(x, y);
2410 m_ImageData->setCatSearchROI(p1, radius);
2411 m_ImageData->searchCatObjects();
2412 updateFrame();
2413#else
2414 Q_UNUSED(p1);
2415 Q_UNUSED(p2);
2416#endif
2417}
2418
2419void FITSView::setTrackingBoxEnabled(bool enable)
2420{
2421 if (enable != trackingBoxEnabled)
2422 {
2423 trackingBoxEnabled = enable;
2424 //updateFrame();
2425 }
2426}
2427
2428void FITSView::wheelEvent(QWheelEvent * event)
2429{
2430 //This attempts to send the wheel event back to the Scroll Area if it was taken from a trackpad
2431 //It should still do the zoom if it is a mouse wheel
2433 {
2435 }
2436 else
2437 {
2438#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
2439 QPoint mouseCenter = getImagePoint(event->pos());
2440#else
2441 QPoint mouseCenter = getImagePoint(event->position().toPoint());
2442#endif
2443 if (event->angleDelta().y() > 0)
2444 ZoomIn();
2445 else
2446 ZoomOut();
2447 event->accept();
2448 cleanUpZoom(mouseCenter);
2449 }
2450 emit zoomRubberBand(getCurrentZoom() / ZOOM_DEFAULT);
2451}
2452
2453/**
2454This method is intended to keep key locations in an image centered on the screen while zooming.
2455If there is a marker or tracking box, it centers on those. If not, it uses the point called
2456viewCenter that was passed as a parameter.
2457 */
2458
2459void FITSView::cleanUpZoom(QPoint viewCenter)
2460{
2461 int x0 = 0;
2462 int y0 = 0;
2463 double scale = (currentZoom / ZOOM_DEFAULT);
2464 if (!markerCrosshair.isNull())
2465 {
2466 x0 = markerCrosshair.x() * scale;
2467 y0 = markerCrosshair.y() * scale;
2468 }
2469 else if (trackingBoxEnabled)
2470 {
2471 x0 = trackingBox.center().x() * scale;
2472 y0 = trackingBox.center().y() * scale;
2473 }
2474 else if (!viewCenter.isNull())
2475 {
2476 x0 = viewCenter.x() * scale;
2477 y0 = viewCenter.y() * scale;
2478 }
2479 if ((x0 != 0) || (y0 != 0))
2480 ensureVisible(x0, y0, width() / 2, height() / 2);
2481 updateMouseCursor();
2482}
2483
2484/**
2485This method converts a point from the ViewPort Coordinate System to the
2486Image Coordinate System.
2487 */
2488
2489QPoint FITSView::getImagePoint(QPoint viewPortPoint)
2490{
2491 QWidget * w = widget();
2492
2493 if (w == nullptr)
2494 return QPoint(0, 0);
2495
2496 double scale = (currentZoom / ZOOM_DEFAULT);
2497 QPoint widgetPoint = w->mapFromParent(viewPortPoint);
2498 QPoint imagePoint = QPoint(widgetPoint.x() / scale, widgetPoint.y() / scale);
2499 return imagePoint;
2500}
2501
2502void FITSView::initDisplayImage()
2503{
2504 // Account for leftover when sampling. Thus a 5-wide image sampled by 2
2505 // would result in a width of 3 (samples 0, 2 and 4).
2506 int w = (m_ImageData->width() + m_PreviewSampling - 1) / m_PreviewSampling;
2507 int h = (m_ImageData->height() + m_PreviewSampling - 1) / m_PreviewSampling;
2508
2509 if (m_ImageData->channels() == 1)
2510 {
2511 rawImage = QImage(w, h, QImage::Format_Indexed8);
2512
2513 rawImage.setColorCount(256);
2514 for (int i = 0; i < 256; i++)
2515 rawImage.setColor(i, qRgb(i, i, i));
2516 }
2517 else
2518 {
2519 rawImage = QImage(w, h, QImage::Format_RGB32);
2520 }
2521}
2522
2523/**
2524The Following two methods allow gestures to work with trackpads.
2525Specifically, we are targeting the pinch events, so that if one is generated,
2526Then the pinchTriggered method will be called. If the event is not a pinch gesture,
2527then the event is passed back to the other event handlers.
2528 */
2529
2530bool FITSView::event(QEvent * event)
2531{
2532 if (event->type() == QEvent::Gesture)
2533 return gestureEvent(dynamic_cast<QGestureEvent *>(event));
2534 return QScrollArea::event(event);
2535}
2536
2537bool FITSView::gestureEvent(QGestureEvent * event)
2538{
2539 if (QGesture * pinch = event->gesture(Qt::PinchGesture))
2540 pinchTriggered(dynamic_cast<QPinchGesture *>(pinch));
2541 return true;
2542}
2543
2544/**
2545This Method works with Trackpads to use the pinch gesture to scroll in and out
2546It stores a point to keep track of the location where the gesture started so that
2547while you are zooming, it tries to keep that initial point centered in the view.
2548**/
2549void FITSView::pinchTriggered(QPinchGesture * gesture)
2550{
2551 if (!zooming)
2552 {
2553 zoomLocation = getImagePoint(mapFromGlobal(QCursor::pos()));
2554 zooming = true;
2555 }
2556 if (gesture->state() == Qt::GestureFinished)
2557 {
2558 zooming = false;
2559 }
2560 zoomTime++; //zoomTime is meant to slow down the zooming with a pinch gesture.
2561 if (zoomTime > 10000) //This ensures zoomtime never gets too big.
2562 zoomTime = 0;
2563 if (zooming && (zoomTime % 10 == 0)) //zoomTime is set to slow it by a factor of 10.
2564 {
2565 if (gesture->totalScaleFactor() > 1)
2566 ZoomIn();
2567 else
2568 ZoomOut();
2569 }
2570 cleanUpZoom(zoomLocation);
2571}
2572
2573/*void FITSView::handleWCSCompletion()
2574{
2575 //bool hasWCS = wcsWatcher.result();
2576 if(m_ImageData->hasWCS())
2577 this->updateFrame();
2578 emit wcsToggled(m_ImageData->hasWCS());
2579}*/
2580
2581void FITSView::syncWCSState()
2582{
2583 bool hasWCS = m_ImageData->hasWCS();
2584 bool wcsLoaded = m_ImageData->getWCSState() == FITSData::Success;
2585
2586#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2587 if (showObjects)
2588 m_ImageData->searchObjects();
2589#endif
2590
2591 if (hasWCS && wcsLoaded)
2592 this->updateFrame();
2593
2594 emit wcsToggled(hasWCS);
2595
2596 if (toggleEQGridAction != nullptr)
2597 toggleEQGridAction->setEnabled(hasWCS);
2598 if (toggleObjectsAction != nullptr)
2599 toggleObjectsAction->setEnabled(hasWCS);
2600 if (centerTelescopeAction != nullptr)
2601 centerTelescopeAction->setEnabled(hasWCS);
2602 if (toggleHiPSOverlayAction != nullptr)
2603 toggleHiPSOverlayAction->setEnabled(hasWCS);
2604}
2605
2606void FITSView::createFloatingToolBar()
2607{
2608 if (floatingToolBar != nullptr)
2609 return;
2610
2611 floatingToolBar = new QToolBar(this);
2612 auto * eff = new QGraphicsOpacityEffect(this);
2613 floatingToolBar->setGraphicsEffect(eff);
2614 eff->setOpacity(0.2);
2615 floatingToolBar->setVisible(false);
2616 floatingToolBar->setStyleSheet(
2617 "QToolBar{background: rgba(150, 150, 150, 210); border:none; color: yellow}"
2618 "QToolButton{background: transparent; border:none; color: yellow}"
2619 "QToolButton:hover{background: rgba(200, 200, 200, 255);border:solid; color: yellow}"
2620 "QToolButton:checked{background: rgba(110, 110, 110, 255);border:solid; color: yellow}");
2621 floatingToolBar->setFloatable(true);
2622 floatingToolBar->setIconSize(QSize(25, 25));
2623 //floatingToolBar->setMovable(true);
2624
2625 QAction * action = nullptr;
2626
2627 floatingToolBar->addAction(QIcon::fromTheme("zoom-in"),
2628 i18n("Zoom In"), this, SLOT(ZoomIn()));
2629
2630 floatingToolBar->addAction(QIcon::fromTheme("zoom-out"),
2631 i18n("Zoom Out"), this, SLOT(ZoomOut()));
2632
2633 floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-best"),
2634 i18n("Default Zoom"), this, SLOT(ZoomDefault()));
2635
2636 floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-width"),
2637 i18n("Zoom to Fit"), this, SLOT(ZoomToFit()));
2638
2639 toggleStretchAction = floatingToolBar->addAction(QIcon::fromTheme("transform-move"),
2640 i18n("Toggle Stretch"),
2641 this, SLOT(toggleStretch()));
2642 toggleStretchAction->setCheckable(true);
2643
2644
2645 floatingToolBar->addSeparator();
2646
2647 action = floatingToolBar->addAction(QIcon::fromTheme("crosshairs"),
2648 i18n("Show Cross Hairs"), this, SLOT(toggleCrosshair()));
2649 action->setCheckable(true);
2650
2651 action = floatingToolBar->addAction(QIcon::fromTheme("map-flat"),
2652 i18n("Show Pixel Gridlines"), this, SLOT(togglePixelGrid()));
2653 action->setCheckable(true);
2654
2655 toggleStarsAction =
2656 floatingToolBar->addAction(QIcon::fromTheme("kstars_stars"),
2657 i18n("Detect Stars in Image"), this, SLOT(toggleStars()));
2658 toggleStarsAction->setCheckable(true);
2659
2660#ifdef HAVE_DATAVISUALIZATION
2661 toggleProfileAction =
2662 floatingToolBar->addAction(QIcon::fromTheme("star-profile", QIcon(":/icons/star_profile.svg")),
2663 i18n("View Star Profile..."), this, SLOT(toggleStarProfile()));
2664 toggleProfileAction->setCheckable(true);
2665#endif
2666
2667 if (mode == FITS_NORMAL || mode == FITS_ALIGN)
2668 {
2669 floatingToolBar->addSeparator();
2670
2671 toggleEQGridAction =
2672 floatingToolBar->addAction(QIcon::fromTheme("kstars_grid"),
2673 i18n("Show Equatorial Gridlines"), this, &FITSView::toggleEQGrid);
2674 toggleEQGridAction->setCheckable(true);
2675 toggleEQGridAction->setEnabled(false);
2676
2677 toggleObjectsAction =
2678 floatingToolBar->addAction(QIcon::fromTheme("help-hint"),
2679 i18n("Show Objects in Image"), this, &FITSView::toggleObjects);
2680 toggleObjectsAction->setCheckable(true);
2681 toggleObjectsAction->setEnabled(false);
2682
2683 centerTelescopeAction =
2684 floatingToolBar->addAction(QIcon::fromTheme("center_telescope", QIcon(":/icons/center_telescope.svg")),
2685 i18n("Center Telescope"), this, &FITSView::centerTelescope);
2686 centerTelescopeAction->setCheckable(true);
2687 centerTelescopeAction->setEnabled(false);
2688
2689 toggleHiPSOverlayAction =
2690 floatingToolBar->addAction(QIcon::fromTheme("pixelate"),
2691 i18n("Show HiPS Overlay"), this, &FITSView::toggleHiPSOverlay);
2692 toggleHiPSOverlayAction->setCheckable(true);
2693 toggleHiPSOverlayAction->setEnabled(false);
2694 }
2695}
2696
2697/**
2698 This method either enables or disables the scope mouse mode so you can slew your scope to coordinates
2699 just by clicking the mouse on a spot in the image.
2700 */
2701
2702void FITSView::centerTelescope()
2703{
2704 if (imageHasWCS())
2705 {
2706 if (getCursorMode() == FITSView::scopeCursor)
2707 {
2708 setCursorMode(lastMouseMode);
2709 }
2710 else
2711 {
2712 lastMouseMode = getCursorMode();
2713 setCursorMode(FITSView::scopeCursor);
2714 }
2715 updateFrame();
2716 }
2717 updateScopeButton();
2718}
2719
2720void FITSView::updateScopeButton()
2721{
2722 if (centerTelescopeAction != nullptr)
2723 {
2724 if (getCursorMode() == FITSView::scopeCursor)
2725 {
2726 centerTelescopeAction->setChecked(true);
2727 }
2728 else
2729 {
2730 centerTelescopeAction->setChecked(false);
2731 }
2732 }
2733}
2734
2735/**
2736This method just verifies if INDI is online, a telescope present, and is connected
2737 */
2738
2739bool FITSView::isTelescopeActive()
2740{
2741#ifdef HAVE_INDI
2742 if (INDIListener::Instance()->size() == 0)
2743 {
2744 return false;
2745 }
2746
2747 for (auto &oneDevice : INDIListener::devices())
2748 {
2749 if (!(oneDevice->getDriverInterface() & INDI::BaseDevice::TELESCOPE_INTERFACE))
2750 continue;
2751 return oneDevice->isConnected();
2752 }
2753 return false;
2754#else
2755 return false;
2756#endif
2757}
2758
2759void FITSView::setStarsEnabled(bool enable)
2760{
2761 markStars = enable;
2762 if (floatingToolBar != nullptr)
2763 {
2764 foreach (QAction * action, floatingToolBar->actions())
2765 {
2766 if (action->text() == i18n("Detect Stars in Image"))
2767 {
2768 action->setChecked(markStars);
2769 break;
2770 }
2771 }
2772 }
2773}
2774
2775void FITSView::setStarsHFREnabled(bool enable)
2776{
2777 showStarsHFR = enable;
2778}
2779
2780void FITSView::setStretchValues(double shadows, double midtones, double highlights)
2781{
2782 StretchParams params = getStretchParams();
2783 params.grey_red.shadows = shadows;
2784 params.grey_red.midtones = midtones;
2785 params.grey_red.highlights = highlights;
2786 //setPreviewSampling(0);
2787 setStretchParams(params);
2788}
2789
2790void FITSView::setAutoStretch()
2791{
2792 if (!getAutoStretch())
2793 setAutoStretchParams();
2794}
2795
The sky coordinates of a point in the sky.
Definition skypoint.h:45
virtual void updateCoordsNow(const KSNumbers *num)
updateCoordsNow Shortcut for updateCoords( const KSNumbers *num, false, nullptr, nullptr,...
Definition skypoint.h:391
dms angularDistanceTo(const SkyPoint *sp, double *const positionAngle=nullptr) const
Computes the angular distance between two SkyObjects.
Definition skypoint.cpp:899
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
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...)
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
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)
void setEnabled(bool)
const char * constData() const const
QPoint pos()
QString suffix() const const
qreal pointSizeF() const const
void setPixelSize(int pixelSize)
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()
bool isRunning() const const
T result() const const
void setFuture(const QFuture< T > &future)
void waitForFinished()
void restoreOverrideCursor()
void setOverrideCursor(const QCursor &cursor)
QIcon fromTheme(const QString &name)
Format_ARGB32_Premultiplied
int height() const const
bool isNull() const const
bool save(QIODevice *device, const char *format, int quality) const const
QImage scaled(const QSize &size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
void setColor(int index, QRgb colorValue)
void setColorCount(int colorCount)
int width() 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)
bool convertFromImage(const QImage &image, Qt::ImageConversionFlags flags)
void fill(const QColor &color)
QPixmap fromImage(QImage &&image, Qt::ImageConversionFlags flags)
bool isNull() const const
bool load(const QString &fileName, const char *format, Qt::ImageConversionFlags flags)
QPixmap scaled(const QSize &size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
QSize size() const const
bool isNull() const const
void setX(int x)
void setY(int y)
int x() const const
int y() const const
bool isNull() const const
void setX(qreal x)
void setY(qreal y)
qreal x() const const
qreal y() const const
QPoint bottomRight() const const
QPoint center() const const
bool contains(const QPoint &point, bool proper) const const
int height() const const
bool isNull() const const
QRect normalized() const const
void setBottomRight(const QPoint &position)
void setTopLeft(const QPoint &position)
QPoint topLeft() const const
int width() const const
int x() const const
int y() const const
void translate(const QPoint &point)
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
T * data() const const
T * get() const const
bool isNull() const const
int height() const const
int width() const const
void push(const T &t)
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
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 start()
void stop()
void timeout()
QAction * addSeparator()
void setFloatable(bool floatable)
void setIconSize(const QSize &iconSize)
QUuid createUuid()
QString toString(StringFormat mode) const const
QString toString() const const
QList< QAction * > actions() const const
QAction * addAction(const QIcon &icon, const QString &text)
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 setGraphicsEffect(QGraphicsEffect *effect)
void show()
void setStyleSheet(const QString &styleSheet)
void update()
virtual void setVisible(bool visible)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Dec 27 2024 11:51:45 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.