Kstars

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

KDE's Doxygen guidelines are available online.