File indexing completed on 2024-03-24 15:16:58
0001 /* 0002 SPDX-FileCopyrightText: 2003-2017 Jasem Mutlaq <mutlaqja@ikarustech.com> 0003 SPDX-FileCopyrightText: 2016-2017 Robert Lancaster <rlancaste@gmail.com> 0004 0005 SPDX-License-Identifier: GPL-2.0-or-later 0006 */ 0007 0008 #include "config-kstars.h" 0009 #include "fitsview.h" 0010 0011 #include "fitsdata.h" 0012 #include "fitslabel.h" 0013 #include "hips/hipsfinder.h" 0014 #include "kstarsdata.h" 0015 0016 #include "ksutils.h" 0017 #include "Options.h" 0018 #include "skymap.h" 0019 0020 #include "stretch.h" 0021 0022 #ifdef HAVE_STELLARSOLVER 0023 #include "ekos/auxiliary/stellarsolverprofileeditor.h" 0024 #endif 0025 0026 #ifdef HAVE_INDI 0027 #include "basedevice.h" 0028 #include "indi/indilistener.h" 0029 #endif 0030 0031 #include <KActionCollection> 0032 0033 #include <QtConcurrent> 0034 #include <QScrollBar> 0035 #include <QToolBar> 0036 #include <QGraphicsOpacityEffect> 0037 #include <QApplication> 0038 #include <QImageReader> 0039 #include <QGestureEvent> 0040 #include <QMutexLocker> 0041 #include <QElapsedTimer> 0042 0043 #ifndef _WIN32 0044 #include <unistd.h> 0045 #endif 0046 0047 #define BASE_OFFSET 50 0048 #define ZOOM_DEFAULT 100.0f 0049 #define ZOOM_MIN 10 0050 // ZOOM_MAX is adjusted in the constructor if the amount of physical memory is known. 0051 #define ZOOM_MAX 300 0052 #define ZOOM_LOW_INCR 10 0053 #define ZOOM_HIGH_INCR 50 0054 #define FONT_SIZE 14 0055 0056 namespace 0057 { 0058 0059 // Derive the Green and Blue stretch parameters from their previous values and the 0060 // changes made to the Red parameters. We apply the same offsets used for Red to the 0061 // other channels' parameters, but clip them. 0062 void ComputeGBStretchParams(const StretchParams &newParams, StretchParams* params) 0063 { 0064 float shadow_diff = newParams.grey_red.shadows - params->grey_red.shadows; 0065 float highlight_diff = newParams.grey_red.highlights - params->grey_red.highlights; 0066 float midtones_diff = newParams.grey_red.midtones - params->grey_red.midtones; 0067 0068 params->green.shadows = params->green.shadows + shadow_diff; 0069 params->green.shadows = KSUtils::clamp(params->green.shadows, 0.0f, 1.0f); 0070 params->green.highlights = params->green.highlights + highlight_diff; 0071 params->green.highlights = KSUtils::clamp(params->green.highlights, 0.0f, 1.0f); 0072 params->green.midtones = params->green.midtones + midtones_diff; 0073 params->green.midtones = std::max(params->green.midtones, 0.0f); 0074 0075 params->blue.shadows = params->blue.shadows + shadow_diff; 0076 params->blue.shadows = KSUtils::clamp(params->blue.shadows, 0.0f, 1.0f); 0077 params->blue.highlights = params->blue.highlights + highlight_diff; 0078 params->blue.highlights = KSUtils::clamp(params->blue.highlights, 0.0f, 1.0f); 0079 params->blue.midtones = params->blue.midtones + midtones_diff; 0080 params->blue.midtones = std::max(params->blue.midtones, 0.0f); 0081 } 0082 0083 } // namespace 0084 0085 // Runs the stretch checking the variables to see which parameters to use. 0086 // We call stretch even if we're not stretching, as the stretch code still 0087 // converts the image to the uint8 output image which will be displayed. 0088 // In that case, it will use an identity stretch. 0089 void FITSView::doStretch(QImage *outputImage) 0090 { 0091 if (outputImage->isNull() || m_ImageData.isNull()) 0092 return; 0093 Stretch stretch(static_cast<int>(m_ImageData->width()), 0094 static_cast<int>(m_ImageData->height()), 0095 m_ImageData->channels(), m_ImageData->dataType()); 0096 0097 StretchParams tempParams; 0098 if (!stretchImage) 0099 tempParams = StretchParams(); // Keeping it linear 0100 else if (autoStretch) 0101 { 0102 // Compute new auto-stretch params. 0103 stretchParams = stretch.computeParams(m_ImageData->getImageBuffer()); 0104 emit newStretch(stretchParams); 0105 tempParams = stretchParams; 0106 } 0107 else 0108 // Use the existing stretch params. 0109 tempParams = stretchParams; 0110 0111 stretch.setParams(tempParams); 0112 stretch.run(m_ImageData->getImageBuffer(), outputImage, m_PreviewSampling); 0113 } 0114 0115 // Store stretch parameters, and turn on stretching if it isn't already on. 0116 void FITSView::setStretchParams(const StretchParams ¶ms) 0117 { 0118 if (m_ImageData->channels() == 3) 0119 ComputeGBStretchParams(params, &stretchParams); 0120 0121 stretchParams.grey_red = params.grey_red; 0122 stretchParams.grey_red.shadows = std::max(stretchParams.grey_red.shadows, 0.0f); 0123 stretchParams.grey_red.highlights = std::max(stretchParams.grey_red.highlights, 0.0f); 0124 stretchParams.grey_red.midtones = std::max(stretchParams.grey_red.midtones, 0.0f); 0125 0126 autoStretch = false; 0127 stretchImage = true; 0128 0129 if (m_ImageFrame && rescale(ZOOM_KEEP_LEVEL)) 0130 { 0131 m_QueueUpdate = true; 0132 updateFrame(true); 0133 } 0134 } 0135 0136 // Turn on or off stretching, and if on, use whatever parameters are currently stored. 0137 void FITSView::setStretch(bool onOff) 0138 { 0139 if (stretchImage != onOff) 0140 { 0141 stretchImage = onOff; 0142 if (m_ImageFrame && rescale(ZOOM_KEEP_LEVEL)) 0143 { 0144 m_QueueUpdate = true; 0145 updateFrame(true); 0146 } 0147 } 0148 } 0149 0150 // Turn on stretching, using automatically generated parameters. 0151 void FITSView::setAutoStretchParams() 0152 { 0153 stretchImage = true; 0154 autoStretch = true; 0155 if (m_ImageFrame && rescale(ZOOM_KEEP_LEVEL)) 0156 { 0157 m_QueueUpdate = true; 0158 updateFrame(true); 0159 } 0160 } 0161 0162 FITSView::FITSView(QWidget * parent, FITSMode fitsMode, FITSScale filterType) : QScrollArea(parent), m_ZoomFactor(1.2) 0163 { 0164 // stretchImage is whether to stretch or not--the stretch may or may not use automatically generated parameters. 0165 // The user may enter his/her own. 0166 stretchImage = Options::autoStretch(); 0167 // autoStretch means use automatically-generated parameters. This is the default, unless the user overrides 0168 // by adjusting the stretchBar's sliders. 0169 autoStretch = true; 0170 0171 // Adjust the maximum zoom according to the amount of memory. 0172 // There have been issues with users running out system memory because of zoom memory. 0173 // Note: this is not currently image dependent. It's possible, but not implemented, 0174 // to allow for more zooming on smaller images. 0175 zoomMax = ZOOM_MAX; 0176 0177 #if defined (Q_OS_LINUX) || defined (Q_OS_OSX) 0178 const long numPages = sysconf(_SC_PAGESIZE); 0179 const long pageSize = sysconf(_SC_PHYS_PAGES); 0180 0181 // _SC_PHYS_PAGES "may not be standard" http://man7.org/linux/man-pages/man3/sysconf.3.html 0182 // If an OS doesn't support it, sysconf should return -1. 0183 if (numPages > 0 && pageSize > 0) 0184 { 0185 // (numPages * pageSize) will likely overflow a 32bit int, so use floating point calculations. 0186 const int memoryMb = numPages * (static_cast<double>(pageSize) / 1e6); 0187 if (memoryMb < 2000) 0188 zoomMax = 100; 0189 else if (memoryMb < 4000) 0190 zoomMax = 200; 0191 else if (memoryMb < 8000) 0192 zoomMax = 300; 0193 else if (memoryMb < 16000) 0194 zoomMax = 400; 0195 else 0196 zoomMax = 600; 0197 } 0198 #endif 0199 0200 grabGesture(Qt::PinchGesture); 0201 0202 filter = filterType; 0203 mode = fitsMode; 0204 0205 setBackgroundRole(QPalette::Dark); 0206 0207 markerCrosshair.setX(0); 0208 markerCrosshair.setY(0); 0209 0210 setBaseSize(740, 530); 0211 0212 m_ImageFrame = new FITSLabel(this); 0213 m_ImageFrame->setMouseTracking(true); 0214 connect(m_ImageFrame, &FITSLabel::newStatus, this, &FITSView::newStatus); 0215 connect(m_ImageFrame, &FITSLabel::mouseOverPixel, this, &FITSView::mouseOverPixel); 0216 connect(m_ImageFrame, &FITSLabel::pointSelected, this, &FITSView::processPointSelection); 0217 connect(m_ImageFrame, &FITSLabel::markerSelected, this, &FITSView::processMarkerSelection); 0218 connect(m_ImageFrame, &FITSLabel::rectangleSelected, this, &FITSView::processRectangle); 0219 connect(this, &FITSView::setRubberBand, m_ImageFrame, &FITSLabel::setRubberBand); 0220 connect(this, &FITSView::showRubberBand, m_ImageFrame, &FITSLabel::showRubberBand); 0221 connect(this, &FITSView::zoomRubberBand, m_ImageFrame, &FITSLabel::zoomRubberBand); 0222 0223 connect(Options::self(), &Options::HIPSOpacityChanged, this, [this]() 0224 { 0225 if (showHiPSOverlay) 0226 { 0227 m_QueueUpdate = true; 0228 updateFrame(); 0229 } 0230 }); 0231 connect(Options::self(), &Options::HIPSOffsetXChanged, this, [this]() 0232 { 0233 if (showHiPSOverlay) 0234 { 0235 m_QueueUpdate = true; 0236 m_HiPSOverlayPixmap = QPixmap(); 0237 updateFrame(); 0238 } 0239 }); 0240 connect(Options::self(), &Options::HIPSOffsetYChanged, this, [this]() 0241 { 0242 if (showHiPSOverlay) 0243 { 0244 m_QueueUpdate = true; 0245 m_HiPSOverlayPixmap = QPixmap(); 0246 updateFrame(); 0247 } 0248 }); 0249 0250 connect(&wcsWatcher, &QFutureWatcher<bool>::finished, this, &FITSView::syncWCSState); 0251 0252 m_UpdateFrameTimer.setInterval(50); 0253 m_UpdateFrameTimer.setSingleShot(true); 0254 connect(&m_UpdateFrameTimer, &QTimer::timeout, this, [this]() 0255 { 0256 this->updateFrame(true); 0257 }); 0258 0259 connect(&fitsWatcher, &QFutureWatcher<bool>::finished, this, &FITSView::loadInFrame); 0260 0261 setCursorMode( 0262 selectCursor); //This is the default mode because the Focus and Align FitsViews should not be in dragMouse mode 0263 0264 noImageLabel = new QLabel(); 0265 noImage.load(":/images/noimage.png"); 0266 noImageLabel->setPixmap(noImage); 0267 noImageLabel->setAlignment(Qt::AlignCenter); 0268 setWidget(noImageLabel); 0269 0270 redScopePixmap = QPixmap(":/icons/center_telescope_red.svg").scaled(32, 32, Qt::KeepAspectRatio, Qt::FastTransformation); 0271 magentaScopePixmap = QPixmap(":/icons/center_telescope_magenta.svg").scaled(32, 32, Qt::KeepAspectRatio, 0272 Qt::FastTransformation); 0273 0274 // Hack! This is same initialization as roiRB in FITSLabel. 0275 // Otherwise initial ROI selection wouldn't have stats. 0276 selectionRectangleRaw = QRect(QPoint(1, 1), QPoint(100, 100)); 0277 } 0278 0279 FITSView::~FITSView() 0280 { 0281 QMutexLocker locker(&updateMutex); 0282 m_UpdateFrameTimer.stop(); 0283 m_Suspended = true; 0284 fitsWatcher.waitForFinished(); 0285 wcsWatcher.waitForFinished(); 0286 } 0287 0288 /** 0289 This method looks at what mouse mode is currently selected and updates the cursor to match. 0290 */ 0291 0292 void FITSView::updateMouseCursor() 0293 { 0294 if (cursorMode == dragCursor) 0295 { 0296 if (horizontalScrollBar()->maximum() > 0 || verticalScrollBar()->maximum() > 0) 0297 { 0298 if (!m_ImageFrame->getMouseButtonDown()) 0299 viewport()->setCursor(Qt::PointingHandCursor); 0300 else 0301 viewport()->setCursor(Qt::ClosedHandCursor); 0302 } 0303 else 0304 viewport()->setCursor(Qt::CrossCursor); 0305 } 0306 else if (cursorMode == selectCursor) 0307 { 0308 viewport()->setCursor(Qt::CrossCursor); 0309 } 0310 else if (cursorMode == scopeCursor) 0311 { 0312 viewport()->setCursor(QCursor(redScopePixmap, 10, 10)); 0313 } 0314 else if (cursorMode == crosshairCursor) 0315 { 0316 viewport()->setCursor(QCursor(magentaScopePixmap, 10, 10)); 0317 } 0318 } 0319 0320 /** 0321 This is how the mouse mode gets set. 0322 The default for a FITSView in a FITSViewer should be the dragMouse 0323 The default for a FITSView in the Focus or Align module should be the selectMouse 0324 The different defaults are accomplished by putting making the actual default mouseMode 0325 the selectMouse, but when a FITSViewer loads an image, it immediately makes it the dragMouse. 0326 */ 0327 0328 void FITSView::setCursorMode(CursorMode mode) 0329 { 0330 cursorMode = mode; 0331 updateMouseCursor(); 0332 0333 if (mode == scopeCursor && imageHasWCS()) 0334 { 0335 if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning()) 0336 { 0337 QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS); 0338 wcsWatcher.setFuture(future); 0339 } 0340 } 0341 } 0342 0343 void FITSView::resizeEvent(QResizeEvent * event) 0344 { 0345 if (m_ImageData == nullptr && noImageLabel != nullptr) 0346 { 0347 noImageLabel->setPixmap( 0348 noImage.scaled(width() - 20, height() - 20, Qt::KeepAspectRatio, Qt::FastTransformation)); 0349 noImageLabel->setFixedSize(width() - 5, height() - 5); 0350 } 0351 0352 QScrollArea::resizeEvent(event); 0353 } 0354 0355 0356 void FITSView::loadFile(const QString &inFilename) 0357 { 0358 if (floatingToolBar != nullptr) 0359 { 0360 floatingToolBar->setVisible(true); 0361 } 0362 0363 bool setBayerParams = false; 0364 0365 BayerParams param; 0366 if ((m_ImageData != nullptr) && m_ImageData->hasDebayer()) 0367 { 0368 setBayerParams = true; 0369 m_ImageData->getBayerParams(¶m); 0370 } 0371 0372 // In case image is still loading, wait until it is done. 0373 fitsWatcher.waitForFinished(); 0374 // In case loadWCS is still running for previous image data, let's wait until it's over 0375 wcsWatcher.waitForFinished(); 0376 0377 // delete m_ImageData; 0378 // m_ImageData = nullptr; 0379 0380 filterStack.clear(); 0381 filterStack.push(FITS_NONE); 0382 if (filter != FITS_NONE) 0383 filterStack.push(filter); 0384 0385 m_ImageData.reset(new FITSData(mode), &QObject::deleteLater); 0386 0387 if (setBayerParams) 0388 m_ImageData->setBayerParams(¶m); 0389 0390 fitsWatcher.setFuture(m_ImageData->loadFromFile(inFilename)); 0391 } 0392 0393 void FITSView::clearData() 0394 { 0395 if (!noImageLabel) 0396 { 0397 noImageLabel = new QLabel(); 0398 noImage.load(":/images/noimage.png"); 0399 noImageLabel->setPixmap(noImage); 0400 noImageLabel->setAlignment(Qt::AlignCenter); 0401 } 0402 0403 setWidget(noImageLabel); 0404 0405 m_ImageData.clear(); 0406 } 0407 0408 bool FITSView::loadData(const QSharedPointer<FITSData> &data) 0409 { 0410 if (floatingToolBar != nullptr) 0411 { 0412 floatingToolBar->setVisible(true); 0413 } 0414 0415 // In case loadWCS is still running for previous image data, let's wait until it's over 0416 wcsWatcher.waitForFinished(); 0417 0418 filterStack.clear(); 0419 filterStack.push(FITS_NONE); 0420 if (filter != FITS_NONE) 0421 filterStack.push(filter); 0422 0423 m_HiPSOverlayPixmap = QPixmap(); 0424 0425 // Takes control of the objects passed in. 0426 m_ImageData = data; 0427 // set the image mask geometry 0428 if (m_ImageMask != nullptr) 0429 m_ImageMask->setImageGeometry(data->width(), data->height()); 0430 0431 if (processData()) 0432 { 0433 emit loaded(); 0434 return true; 0435 } 0436 else 0437 { 0438 emit failed(m_LastError); 0439 return false; 0440 } 0441 } 0442 0443 bool FITSView::processData() 0444 { 0445 // Set current width and height 0446 if (!m_ImageData) 0447 return false; 0448 0449 connect(m_ImageData.data(), &FITSData::dataChanged, this, [this]() 0450 { 0451 rescale(ZOOM_KEEP_LEVEL); 0452 updateFrame(); 0453 }); 0454 0455 currentWidth = m_ImageData->width(); 0456 currentHeight = m_ImageData->height(); 0457 0458 int image_width = currentWidth; 0459 int image_height = currentHeight; 0460 0461 if (!m_ImageFrame) 0462 { 0463 m_ImageFrame = new FITSLabel(this); 0464 m_ImageFrame->setMouseTracking(true); 0465 connect(m_ImageFrame, &FITSLabel::newStatus, this, &FITSView::newStatus); 0466 connect(m_ImageFrame, &FITSLabel::pointSelected, this, &FITSView::processPointSelection); 0467 connect(m_ImageFrame, &FITSLabel::markerSelected, this, &FITSView::processMarkerSelection); 0468 } 0469 m_ImageFrame->setSize(image_width, image_height); 0470 0471 // Init the display image 0472 // JM 2020.01.08: Disabling as proposed by Hy 0473 //initDisplayImage(); 0474 0475 m_ImageData->applyFilter(filter); 0476 0477 double availableRAM = 0; 0478 if (Options::adaptiveSampling() && (availableRAM = KSUtils::getAvailableRAM()) > 0) 0479 { 0480 // Possible color maximum image size 0481 double max_size = image_width * image_height * 4; 0482 // Ratio of image size to available RAM size 0483 double ratio = max_size / availableRAM; 0484 0485 // Increase adaptive sampling with more limited RAM 0486 if (ratio < 0.1) 0487 m_AdaptiveSampling = 1; 0488 else if (ratio < 0.2) 0489 m_AdaptiveSampling = 2; 0490 else 0491 m_AdaptiveSampling = 4; 0492 0493 m_PreviewSampling = m_AdaptiveSampling; 0494 } 0495 0496 // Rescale to fits window on first load 0497 if (firstLoad) 0498 { 0499 currentZoom = 100; 0500 0501 if (rescale(ZOOM_FIT_WINDOW) == false) 0502 { 0503 m_LastError = i18n("Rescaling image failed."); 0504 return false; 0505 } 0506 0507 firstLoad = false; 0508 } 0509 else 0510 { 0511 if (rescale(ZOOM_KEEP_LEVEL) == false) 0512 { 0513 m_LastError = i18n("Rescaling image failed."); 0514 return false; 0515 } 0516 } 0517 0518 setAlignment(Qt::AlignCenter); 0519 0520 // Load WCS data now if selected and image contains valid WCS header 0521 if ((mode == FITS_NORMAL || mode == FITS_ALIGN) && 0522 m_ImageData->hasWCS() && m_ImageData->getWCSState() == FITSData::Idle && 0523 Options::autoWCS() && 0524 !wcsWatcher.isRunning()) 0525 { 0526 QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS); 0527 wcsWatcher.setFuture(future); 0528 } 0529 else 0530 syncWCSState(); 0531 0532 if (isVisible()) 0533 emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION); 0534 0535 if (showStarProfile) 0536 { 0537 if(floatingToolBar != nullptr) 0538 toggleProfileAction->setChecked(true); 0539 //Need to wait till the Focus module finds stars, if its the Focus module. 0540 QTimer::singleShot(100, this, SLOT(viewStarProfile())); 0541 } 0542 0543 // Fore immediate load of frame for first load. 0544 m_QueueUpdate = true; 0545 updateFrame(true); 0546 return true; 0547 } 0548 0549 void FITSView::loadInFrame() 0550 { 0551 m_LastError = m_ImageData->getLastError(); 0552 0553 // Check if the loading was OK 0554 if (fitsWatcher.result() == false) 0555 { 0556 emit failed(m_LastError); 0557 return; 0558 } 0559 0560 // Notify if there is debayer data. 0561 emit debayerToggled(m_ImageData->hasDebayer()); 0562 0563 if (processData()) 0564 emit loaded(); 0565 else 0566 emit failed(m_LastError); 0567 } 0568 0569 bool FITSView::saveImage(const QString &newFilename) 0570 { 0571 const QString ext = QFileInfo(newFilename).suffix(); 0572 if (QImageReader::supportedImageFormats().contains(ext.toLatin1())) 0573 { 0574 rawImage.save(newFilename, ext.toLatin1().constData()); 0575 return true; 0576 } 0577 0578 return m_ImageData->saveImage(newFilename); 0579 } 0580 0581 FITSView::CursorMode FITSView::getCursorMode() 0582 { 0583 return cursorMode; 0584 } 0585 0586 void FITSView::enterEvent(QEvent * event) 0587 { 0588 Q_UNUSED(event) 0589 0590 if (floatingToolBar && m_ImageData) 0591 { 0592 QPointer<QGraphicsOpacityEffect> eff = new QGraphicsOpacityEffect(this); 0593 floatingToolBar->setGraphicsEffect(eff); 0594 QPointer<QPropertyAnimation> a = new QPropertyAnimation(eff, "opacity"); 0595 a->setDuration(500); 0596 a->setStartValue(0.2); 0597 a->setEndValue(1); 0598 a->setEasingCurve(QEasingCurve::InBack); 0599 a->start(QPropertyAnimation::DeleteWhenStopped); 0600 } 0601 } 0602 0603 void FITSView::leaveEvent(QEvent * event) 0604 { 0605 Q_UNUSED(event) 0606 0607 if (floatingToolBar && m_ImageData) 0608 { 0609 QPointer<QGraphicsOpacityEffect> eff = new QGraphicsOpacityEffect(this); 0610 floatingToolBar->setGraphicsEffect(eff); 0611 QPointer<QPropertyAnimation> a = new QPropertyAnimation(eff, "opacity"); 0612 a->setDuration(500); 0613 a->setStartValue(1); 0614 a->setEndValue(0.2); 0615 a->setEasingCurve(QEasingCurve::OutBack); 0616 a->start(QPropertyAnimation::DeleteWhenStopped); 0617 } 0618 } 0619 0620 bool FITSView::rescale(FITSZoom type) 0621 { 0622 if (!m_ImageData) 0623 return false; 0624 0625 int image_width = m_ImageData->width(); 0626 int image_height = m_ImageData->height(); 0627 currentWidth = image_width; 0628 currentHeight = image_height; 0629 0630 if (isVisible()) 0631 emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION); 0632 0633 switch (type) 0634 { 0635 case ZOOM_FIT_WINDOW: 0636 if ((image_width > width() || image_height > height())) 0637 { 0638 double w = baseSize().width() - BASE_OFFSET; 0639 double h = baseSize().height() - BASE_OFFSET; 0640 0641 if (!firstLoad) 0642 { 0643 w = viewport()->rect().width() - BASE_OFFSET; 0644 h = viewport()->rect().height() - BASE_OFFSET; 0645 } 0646 0647 // Find the zoom level which will enclose the current FITS in the current window size 0648 double zoomX = floor((w / static_cast<double>(currentWidth)) * 100.); 0649 double zoomY = floor((h / static_cast<double>(currentHeight)) * 100.); 0650 (zoomX < zoomY) ? currentZoom = zoomX : currentZoom = zoomY; 0651 0652 currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); 0653 currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); 0654 0655 if (currentZoom <= ZOOM_MIN) 0656 emit actionUpdated("view_zoom_out", false); 0657 } 0658 else 0659 { 0660 currentZoom = 100; 0661 currentWidth = image_width; 0662 currentHeight = image_height; 0663 } 0664 break; 0665 0666 case ZOOM_KEEP_LEVEL: 0667 { 0668 currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); 0669 currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); 0670 } 0671 break; 0672 0673 default: 0674 currentZoom = 100; 0675 0676 break; 0677 } 0678 0679 initDisplayImage(); 0680 m_ImageFrame->setScaledContents(true); 0681 doStretch(&rawImage); 0682 setWidget(m_ImageFrame); 0683 0684 // This is needed by fitstab, even if the zoom doesn't change, to change the stretch UI. 0685 emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); 0686 return true; 0687 } 0688 0689 void FITSView::ZoomIn() 0690 { 0691 if (!m_ImageData) 0692 return; 0693 0694 if (currentZoom >= ZOOM_DEFAULT && Options::limitedResourcesMode()) 0695 { 0696 emit newStatus(i18n("Cannot zoom in further due to active limited resources mode."), FITS_MESSAGE); 0697 return; 0698 } 0699 0700 if (currentZoom < ZOOM_DEFAULT) 0701 currentZoom += ZOOM_LOW_INCR; 0702 else 0703 currentZoom += ZOOM_HIGH_INCR; 0704 0705 emit actionUpdated("view_zoom_out", true); 0706 if (currentZoom >= zoomMax) 0707 { 0708 currentZoom = zoomMax; 0709 emit actionUpdated("view_zoom_in", false); 0710 } 0711 0712 currentWidth = m_ImageData->width() * (currentZoom / ZOOM_DEFAULT); 0713 currentHeight = m_ImageData->height() * (currentZoom / ZOOM_DEFAULT); 0714 0715 cleanUpZoom(); 0716 0717 updateFrame(true); 0718 0719 emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); 0720 emit zoomRubberBand(getCurrentZoom() / ZOOM_DEFAULT); 0721 } 0722 0723 void FITSView::ZoomOut() 0724 { 0725 if (!m_ImageData) 0726 return; 0727 0728 if (currentZoom <= ZOOM_DEFAULT) 0729 currentZoom -= ZOOM_LOW_INCR; 0730 else 0731 currentZoom -= ZOOM_HIGH_INCR; 0732 0733 if (currentZoom <= ZOOM_MIN) 0734 { 0735 currentZoom = ZOOM_MIN; 0736 emit actionUpdated("view_zoom_out", false); 0737 } 0738 0739 emit actionUpdated("view_zoom_in", true); 0740 0741 currentWidth = m_ImageData->width() * (currentZoom / ZOOM_DEFAULT); 0742 currentHeight = m_ImageData->height() * (currentZoom / ZOOM_DEFAULT); 0743 0744 cleanUpZoom(); 0745 0746 updateFrame(true); 0747 0748 emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); 0749 emit zoomRubberBand(getCurrentZoom() / ZOOM_DEFAULT); 0750 } 0751 0752 void FITSView::ZoomToFit() 0753 { 0754 if (!m_ImageData) 0755 return; 0756 0757 if (rawImage.isNull() == false) 0758 { 0759 rescale(ZOOM_FIT_WINDOW); 0760 updateFrame(true); 0761 } 0762 emit zoomRubberBand(getCurrentZoom() / ZOOM_DEFAULT); 0763 } 0764 0765 0766 0767 int FITSView::filterStars() 0768 { 0769 return ((m_ImageMask.isNull() == false 0770 && m_ImageMask->active()) ? m_ImageData->filterStars(m_ImageMask) : m_ImageData->getStarCenters().count()); 0771 } 0772 0773 void FITSView::setImageMask(ImageMask *mask) 0774 { 0775 if (m_ImageMask.isNull() == false) 0776 { 0777 // copy image geometry from the old mask before deleting it 0778 if (mask != nullptr) 0779 mask->setImageGeometry(m_ImageMask->width(), m_ImageMask->height()); 0780 } 0781 0782 m_ImageMask.reset(mask); 0783 } 0784 0785 // isImageLarge() returns whether we use the large-image rendering strategy or the small-image strategy. 0786 // See the comment below in getScale() for details. 0787 bool FITSView::isLargeImage() 0788 { 0789 constexpr int largeImageNumPixels = 1000 * 1000; 0790 return rawImage.width() * rawImage.height() >= largeImageNumPixels; 0791 } 0792 0793 // getScale() is related to the image and overlay rendering strategy used. 0794 // If we're using a pixmap appropriate for a large image, where we draw and render on a pixmap that's the image size 0795 // and we let the QLabel deal with scaling and zooming, then the scale is 1.0. 0796 // With smaller images, where memory use is not as severe, we create a pixmap that's the size of the scaled image 0797 // and get scale returns the ratio of that pixmap size to the image size. 0798 double FITSView::getScale() 0799 { 0800 return (isLargeImage() ? 1.0 : currentZoom / ZOOM_DEFAULT) / m_PreviewSampling; 0801 } 0802 0803 // scaleSize() is only used with the large-image rendering strategy. It may increase the line 0804 // widths or font sizes, as we draw lines and render text on the full image and when zoomed out, 0805 // these sizes may be too small. 0806 double FITSView::scaleSize(double size) 0807 { 0808 if (!isLargeImage()) 0809 return size; 0810 return (currentZoom > 100.0 ? size : std::round(size * 100.0 / currentZoom)) / m_PreviewSampling; 0811 } 0812 0813 void FITSView::updateFrame(bool now) 0814 { 0815 QMutexLocker locker(&updateMutex); 0816 0817 // Do not process if suspended. 0818 if (m_Suspended) 0819 return; 0820 0821 // JM 2021-03-13: This timer is used to throttle updateFrame calls to improve performance 0822 // If after 250ms no further update frames are called, then the actual update is triggered. 0823 // JM 2021-03-16: When stretching in progress, immediately execute so that the user see the changes 0824 // in real time 0825 if (now) 0826 { 0827 if (toggleStretchAction) 0828 toggleStretchAction->setChecked(stretchImage); 0829 0830 // We employ two schemes for managing the image and its overlays, depending on the size of the image 0831 // and whether we need to therefore conserve memory. The small-image strategy explicitly scales up 0832 // the image, and writes overlays on the scaled pixmap. The large-image strategy uses a pixmap that's 0833 // the size of the image itself, never scaling that up. 0834 if (isLargeImage()) 0835 updateFrameLargeImage(); 0836 else 0837 updateFrameSmallImage(); 0838 0839 if (m_QueueUpdate && m_StretchingInProgress == false) 0840 { 0841 m_QueueUpdate = false; 0842 emit updated(); 0843 } 0844 } 0845 else 0846 m_UpdateFrameTimer.start(); 0847 } 0848 0849 0850 bool FITSView::initDisplayPixmap(QImage &image, float scale) 0851 { 0852 ImageMosaicMask *mask = dynamic_cast<ImageMosaicMask *>(m_ImageMask.get()); 0853 0854 // if no mosaic should be created, simply convert the original image 0855 if (mask == nullptr) 0856 return displayPixmap.convertFromImage(image); 0857 0858 // check image geometry, sincd scaling could have changed it 0859 // create the 3x3 mosaic 0860 int width = mask->tileWidth() * mask->width() / 100; 0861 int space = mask->space(); 0862 // create a new all black pixmap with mosaic size 0863 displayPixmap = QPixmap((3 * width + 2 * space) * scale, (3 * width + 2 * space) * scale); 0864 displayPixmap.fill(Qt::black); 0865 0866 QPainter painter(&displayPixmap); 0867 int pos = 0; 0868 // paint tiles 0869 for (QRect tile : mask->tiles()) 0870 { 0871 const int posx = pos % 3; 0872 const int posy = pos++ / 3; 0873 const int tilewidth = width * scale; 0874 QRectF source(tile.x() * scale, tile.y()*scale, tilewidth, tilewidth); 0875 QRectF target((posx * (width + space)) * scale, (posy * (width + space)) * scale, width * scale, width * scale); 0876 painter.drawImage(target, image, source); 0877 } 0878 return true; 0879 } 0880 0881 void FITSView::updateFrameLargeImage() 0882 { 0883 if (!initDisplayPixmap(rawImage, 1.0 / m_PreviewSampling)) 0884 return; 0885 QPainter painter(&displayPixmap); 0886 // Possibly scale the fonts as we're drawing on the full image, not just the visible part of the scroll window. 0887 QFont font = painter.font(); 0888 font.setPixelSize(scaleSize(FONT_SIZE)); 0889 painter.setFont(font); 0890 0891 drawStarRingFilter(&painter, 1.0 / m_PreviewSampling, dynamic_cast<ImageRingMask *>(m_ImageMask.get())); 0892 drawOverlay(&painter, 1.0 / m_PreviewSampling); 0893 m_ImageFrame->setPixmap(displayPixmap); 0894 m_ImageFrame->resize(((m_PreviewSampling * currentZoom) / 100.0) * displayPixmap.size()); 0895 } 0896 0897 void FITSView::updateFrameSmallImage() 0898 { 0899 QImage scaledImage = rawImage.scaled(currentWidth, currentHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation); 0900 if (!initDisplayPixmap(scaledImage, currentZoom / ZOOM_DEFAULT)) 0901 return; 0902 0903 QPainter painter(&displayPixmap); 0904 // Possibly scale the fonts as we're drawing on the full image, not just the visible part of the scroll window. 0905 QFont font = painter.font(); 0906 drawStarRingFilter(&painter, currentZoom / ZOOM_DEFAULT, dynamic_cast<ImageRingMask *>(m_ImageMask.get())); 0907 drawOverlay(&painter, currentZoom / ZOOM_DEFAULT); 0908 m_ImageFrame->setPixmap(displayPixmap); 0909 m_ImageFrame->resize(currentWidth, currentHeight); 0910 } 0911 0912 void FITSView::drawStarRingFilter(QPainter *painter, double scale, ImageRingMask *ringMask) 0913 { 0914 if (ringMask == nullptr || !ringMask->active()) 0915 return; 0916 0917 const double w = m_ImageData->width() * scale; 0918 const double h = m_ImageData->height() * scale; 0919 double const diagonal = std::sqrt(w * w + h * h) / 2; 0920 int const innerRadius = std::lround(diagonal * ringMask->innerRadius()); 0921 int const outerRadius = std::lround(diagonal * ringMask->outerRadius()); 0922 QPoint const center(w / 2, h / 2); 0923 painter->save(); 0924 painter->setPen(QPen(Qt::blue, scaleSize(1), Qt::DashLine)); 0925 painter->setOpacity(0.7); 0926 painter->setBrush(QBrush(Qt::transparent)); 0927 painter->drawEllipse(center, outerRadius, outerRadius); 0928 painter->setBrush(QBrush(Qt::blue, Qt::FDiagPattern)); 0929 painter->drawEllipse(center, innerRadius, innerRadius); 0930 painter->restore(); 0931 } 0932 0933 namespace 0934 { 0935 0936 template <typename T> 0937 int drawClippingOneChannel(T *inputBuffer, QPainter *painter, int width, int height, double clipVal, double scale) 0938 { 0939 int numClipped = 0; 0940 painter->save(); 0941 painter->setPen(QPen(Qt::red, scale, Qt::SolidLine)); 0942 const T clipping = clipVal; 0943 constexpr int timeoutMilliseconds = 3 * 1000; 0944 QElapsedTimer timer; 0945 timer.start(); 0946 QPoint p; 0947 for (int y = 0; y < height; y++) 0948 { 0949 auto inputLine = inputBuffer + y * width; 0950 p.setY(y); 0951 for (int x = 0; x < width; x++) 0952 { 0953 if (*inputLine++ > clipping) 0954 { 0955 numClipped++; 0956 const int start = x; 0957 // Use this inner loop to recognize strings of clipped pixels 0958 // and draw lines instead of multiple calls to drawPoints. 0959 while (true) 0960 { 0961 if (++x >= width) 0962 { 0963 painter->drawLine(start, y, width - 1, y); 0964 break; 0965 } 0966 if (*inputLine++ > clipping) 0967 numClipped++; 0968 else 0969 { 0970 if (x == start + 1) 0971 { 0972 p.setX(start); 0973 painter->drawPoints(&p, 1); 0974 } 0975 else 0976 painter->drawLine(start, y, x - 1, y); 0977 break; 0978 } 0979 } 0980 } 0981 } 0982 if (timer.elapsed() > timeoutMilliseconds) 0983 { 0984 painter->restore(); 0985 return -1; 0986 } 0987 } 0988 painter->restore(); 0989 return numClipped; 0990 } 0991 0992 template <typename T> 0993 int drawClippingThreeChannels(T *inputBuffer, QPainter *painter, int width, int height, double clipVal, double scale) 0994 { 0995 int numClipped = 0; 0996 painter->save(); 0997 painter->setPen(QPen(Qt::red, scale, Qt::SolidLine)); 0998 const T clipping = clipVal; 0999 constexpr int timeoutMilliseconds = 3 * 1000; 1000 QElapsedTimer timer; 1001 timer.start(); 1002 QPoint p; 1003 const int size = width * height; 1004 for (int y = 0; y < height; y++) 1005 { 1006 // R, G, B input images are stored one after another. 1007 const T * inputLineR = inputBuffer + y * width; 1008 const T * inputLineG = inputLineR + size; 1009 const T * inputLineB = inputLineG + size; 1010 p.setY(y); 1011 1012 for (int x = 0; x < width; x++) 1013 { 1014 T inputR = inputLineR[x]; 1015 T inputG = inputLineG[x]; 1016 T inputB = inputLineB[x]; 1017 1018 if (inputR > clipping || inputG > clipping || inputB > clipping) 1019 { 1020 numClipped++; 1021 const int start = x; 1022 1023 // Use this inner loop to recognize strings of clipped pixels 1024 // and draw lines instead of multiple calls to drawPoints. 1025 while (true) 1026 { 1027 if (++x >= width) 1028 { 1029 painter->drawLine(start, y, width - 1, y); 1030 break; 1031 } 1032 T inputR2 = inputLineR[x]; 1033 T inputG2 = inputLineG[x]; 1034 T inputB2 = inputLineB[x]; 1035 if (inputR2 > clipping || inputG2 > clipping || inputB2 > clipping) 1036 numClipped++; 1037 else 1038 { 1039 if (x == start + 1) 1040 { 1041 p.setX(start); 1042 painter->drawPoints(&p, 1); 1043 } 1044 else 1045 painter->drawLine(start, y, x - 1, y); 1046 break; 1047 } 1048 } 1049 } 1050 } 1051 if (timer.elapsed() > timeoutMilliseconds) 1052 { 1053 painter->restore(); 1054 return -1; 1055 } 1056 } 1057 painter->restore(); 1058 return numClipped; 1059 } 1060 1061 template <typename T> 1062 int drawClip(T *input_buffer, int num_channels, QPainter *painter, int width, int height, double clipVal, double scale) 1063 { 1064 if (num_channels == 1) 1065 return drawClippingOneChannel(input_buffer, painter, width, height, clipVal, scale); 1066 else if (num_channels == 3) 1067 return drawClippingThreeChannels(input_buffer, painter, width, height, clipVal, scale); 1068 else return 0; 1069 } 1070 1071 } // namespace 1072 1073 void FITSView::drawClipping(QPainter *painter) 1074 { 1075 auto input = m_ImageData->getImageBuffer(); 1076 const int height = m_ImageData->height(); 1077 const int width = m_ImageData->width(); 1078 const double FLOAT_CLIP = Options::clipping64KValue(); 1079 const double SHORT_CLIP = Options::clipping64KValue(); 1080 const double USHORT_CLIP = Options::clipping64KValue(); 1081 const double BYTE_CLIP = Options::clipping256Value(); 1082 switch (m_ImageData->dataType()) 1083 { 1084 case TBYTE: 1085 m_NumClipped = drawClip(reinterpret_cast<uint8_t const*>(input), m_ImageData->channels(), painter, width, height, BYTE_CLIP, 1086 scaleSize(1)); 1087 break; 1088 case TSHORT: 1089 m_NumClipped = drawClip(reinterpret_cast<short const*>(input), m_ImageData->channels(), painter, width, height, SHORT_CLIP, 1090 scaleSize(1)); 1091 break; 1092 case TUSHORT: 1093 m_NumClipped = drawClip(reinterpret_cast<unsigned short const*>(input), m_ImageData->channels(), painter, width, height, 1094 USHORT_CLIP, 1095 scaleSize(1)); 1096 break; 1097 case TLONG: 1098 m_NumClipped = drawClip(reinterpret_cast<long const*>(input), m_ImageData->channels(), painter, width, height, USHORT_CLIP, 1099 scaleSize(1)); 1100 break; 1101 case TFLOAT: 1102 m_NumClipped = drawClip(reinterpret_cast<float const*>(input), m_ImageData->channels(), painter, width, height, FLOAT_CLIP, 1103 scaleSize(1)); 1104 break; 1105 case TLONGLONG: 1106 m_NumClipped = drawClip(reinterpret_cast<long long const*>(input), m_ImageData->channels(), painter, width, height, 1107 USHORT_CLIP, 1108 scaleSize(1)); 1109 break; 1110 case TDOUBLE: 1111 m_NumClipped = drawClip(reinterpret_cast<double const*>(input), m_ImageData->channels(), painter, width, height, FLOAT_CLIP, 1112 scaleSize(1)); 1113 break; 1114 default: 1115 m_NumClipped = 0; 1116 break; 1117 } 1118 if (m_NumClipped < 0) 1119 emit newStatus(QString("Clip:failed"), FITS_CLIP); 1120 else 1121 emit newStatus(QString("Clip:%1").arg(m_NumClipped), FITS_CLIP); 1122 } 1123 1124 void FITSView::ZoomDefault() 1125 { 1126 if (m_ImageFrame) 1127 { 1128 emit actionUpdated("view_zoom_out", true); 1129 emit actionUpdated("view_zoom_in", true); 1130 1131 currentZoom = ZOOM_DEFAULT; 1132 currentWidth = m_ImageData->width(); 1133 currentHeight = m_ImageData->height(); 1134 1135 updateFrame(); 1136 1137 emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); 1138 1139 update(); 1140 } 1141 } 1142 1143 void FITSView::drawOverlay(QPainter * painter, double scale) 1144 { 1145 painter->setRenderHint(QPainter::Antialiasing, Options::useAntialias()); 1146 1147 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) 1148 if (showHiPSOverlay) 1149 drawHiPSOverlay(painter, scale); 1150 #endif 1151 1152 if (trackingBoxEnabled && getCursorMode() != FITSView::scopeCursor) 1153 drawTrackingBox(painter, scale); 1154 1155 if (!markerCrosshair.isNull()) 1156 drawMarker(painter, scale); 1157 1158 if (showCrosshair) 1159 drawCrosshair(painter, scale); 1160 1161 if (showObjects) 1162 drawObjectNames(painter, scale); 1163 1164 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) 1165 if (showEQGrid) 1166 drawEQGrid(painter, scale); 1167 #endif 1168 1169 if (showPixelGrid) 1170 drawPixelGrid(painter, scale); 1171 1172 if (markStars) 1173 drawStarCentroid(painter, scale); 1174 1175 if (showClipping) 1176 drawClipping(painter); 1177 1178 if (showMagnifyingGlass) 1179 drawMagnifyingGlass(painter, scale); 1180 } 1181 1182 // Draws a 100% resolution image rectangle around the mouse position. 1183 void FITSView::drawMagnifyingGlass(QPainter *painter, double scale) 1184 { 1185 if (magnifyingGlassX >= 0 && magnifyingGlassY >= 0 && 1186 magnifyingGlassX < m_ImageData->width() && 1187 magnifyingGlassY < m_ImageData->height()) 1188 { 1189 // Amount of magnification. 1190 constexpr double magAmount = 8; 1191 // Desired size in pixels of the magnification window. 1192 constexpr int magWindowSize = 130; 1193 // The distance from the mouse position to the magnifying glass rectangle, in the source image coordinates. 1194 const int winXOffset = magWindowSize * 10.0 / currentZoom; 1195 const int winYOffset = magWindowSize * 10.0 / currentZoom; 1196 // Size of a side of the square of input to make a window that size. 1197 const int inputDimension = magWindowSize * 100 / currentZoom; 1198 // Size of the square drawn. Not the same, necessarily as the magWindowSize, 1199 // since the output may be scaled (if isLargeImage()==true) to become screen pixels. 1200 const int outputDimension = inputDimension * scale + .99; 1201 1202 // Where the source data (to be magnified) comes from. 1203 int imgLeft = magnifyingGlassX - inputDimension / (2 * magAmount); 1204 int imgTop = magnifyingGlassY - inputDimension / (2 * magAmount); 1205 1206 // Where we'll draw the magnifying glass rectangle. 1207 int winLeft = magnifyingGlassX + winXOffset; 1208 int winTop = magnifyingGlassY + winYOffset; 1209 1210 // Normally we place the magnifying glass rectangle to the right and below the mouse curson. 1211 // However, if it would be rendered outside the image, put it on the other side. 1212 int w = rawImage.width(); 1213 int h = rawImage.height(); 1214 const int rightLimit = std::min(w, static_cast<int>((horizontalScrollBar()->value() + width()) * 100 / currentZoom)); 1215 const int bottomLimit = std::min(h, static_cast<int>((verticalScrollBar()->value() + height()) * 100 / currentZoom)); 1216 if (winLeft + winXOffset + inputDimension > rightLimit) 1217 winLeft -= (2 * winXOffset + inputDimension); 1218 if (winTop + winYOffset + inputDimension > bottomLimit) 1219 winTop -= (2 * winYOffset + inputDimension); 1220 1221 // Blacken the output where magnifying outside the source image. 1222 if ((imgLeft < 0 ) || 1223 (imgLeft + inputDimension / magAmount >= w) || 1224 (imgTop < 0) || 1225 (imgTop + inputDimension / magAmount > h)) 1226 { 1227 painter->setBrush(QBrush(Qt::black)); 1228 painter->drawRect(winLeft * scale, winTop * scale, outputDimension, outputDimension); 1229 painter->setBrush(QBrush(Qt::transparent)); 1230 } 1231 1232 // Finally, draw the magnified image. 1233 painter->drawImage(QRect(winLeft * scale, winTop * scale, outputDimension, outputDimension), 1234 rawImage, 1235 QRect(imgLeft, imgTop, inputDimension / magAmount, inputDimension / magAmount)); 1236 // Draw a white border. 1237 painter->setPen(QPen(Qt::white, scaleSize(1))); 1238 painter->drawRect(winLeft * scale, winTop * scale, outputDimension, outputDimension); 1239 } 1240 } 1241 1242 // x,y are the image coordinates where the magnifying glass is positioned. 1243 void FITSView::updateMagnifyingGlass(int x, int y) 1244 { 1245 if (!m_ImageData) 1246 return; 1247 1248 magnifyingGlassX = x; 1249 magnifyingGlassY = y; 1250 if (magnifyingGlassX == -1 && magnifyingGlassY == -1) 1251 { 1252 if (showMagnifyingGlass) 1253 updateFrame(true); 1254 showMagnifyingGlass = false; 1255 } 1256 else 1257 { 1258 showMagnifyingGlass = true; 1259 updateFrame(true); 1260 } 1261 } 1262 1263 void FITSView::updateMode(FITSMode fmode) 1264 { 1265 mode = fmode; 1266 } 1267 1268 void FITSView::drawMarker(QPainter * painter, double scale) 1269 { 1270 painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")), 1271 scaleSize(2))); 1272 painter->setBrush(Qt::NoBrush); 1273 const float pxperdegree = scale * (57.3 / 1.8); 1274 1275 const float s1 = 0.5 * pxperdegree; 1276 const float s2 = pxperdegree; 1277 const float s3 = 2.0 * pxperdegree; 1278 1279 const float x0 = scale * markerCrosshair.x(); 1280 const float y0 = scale * markerCrosshair.y(); 1281 const float x1 = x0 - 0.5 * s1; 1282 const float y1 = y0 - 0.5 * s1; 1283 const float x2 = x0 - 0.5 * s2; 1284 const float y2 = y0 - 0.5 * s2; 1285 const float x3 = x0 - 0.5 * s3; 1286 const float y3 = y0 - 0.5 * s3; 1287 1288 //Draw radial lines 1289 painter->drawLine(QPointF(x1, y0), QPointF(x3, y0)); 1290 painter->drawLine(QPointF(x0 + s2, y0), QPointF(x0 + 0.5 * s1, y0)); 1291 painter->drawLine(QPointF(x0, y1), QPointF(x0, y3)); 1292 painter->drawLine(QPointF(x0, y0 + 0.5 * s1), QPointF(x0, y0 + s2)); 1293 //Draw circles at 0.5 & 1 degrees 1294 painter->drawEllipse(QRectF(x1, y1, s1, s1)); 1295 painter->drawEllipse(QRectF(x2, y2, s2, s2)); 1296 } 1297 1298 bool FITSView::drawHFR(QPainter * painter, const QString &hfr, int x, int y) 1299 { 1300 QRect const boundingRect(0, 0, painter->device()->width(), painter->device()->height()); 1301 QSize const hfrSize = painter->fontMetrics().size(Qt::TextSingleLine, hfr); 1302 1303 // Store the HFR text in a rect 1304 QPoint const hfrBottomLeft(x, y); 1305 QRect const hfrRect(hfrBottomLeft.x(), hfrBottomLeft.y() - hfrSize.height(), hfrSize.width(), hfrSize.height()); 1306 1307 // Render the HFR text only if it can be displayed entirely 1308 if (boundingRect.contains(hfrRect)) 1309 { 1310 painter->setPen(QPen(Qt::red, scaleSize(3))); 1311 painter->drawText(hfrBottomLeft, hfr); 1312 painter->setPen(QPen(Qt::red, scaleSize(2))); 1313 return true; 1314 } 1315 return false; 1316 } 1317 1318 1319 void FITSView::drawStarCentroid(QPainter * painter, double scale) 1320 { 1321 QFont painterFont; 1322 double fontSize = painterFont.pointSizeF() * 2; 1323 painter->setRenderHint(QPainter::Antialiasing); 1324 if (showStarsHFR) 1325 { 1326 // If we need to print the HFR out, give an arbitrarily sized font to the painter 1327 if (isLargeImage()) 1328 fontSize = scaleSize(painterFont.pointSizeF()); 1329 painterFont.setPointSizeF(fontSize); 1330 painter->setFont(painterFont); 1331 } 1332 1333 painter->setPen(QPen(Qt::red, scaleSize(2))); 1334 ImageMosaicMask *mask = dynamic_cast<ImageMosaicMask *>(m_ImageMask.get()); 1335 1336 for (auto const &starCenter : m_ImageData->getStarCenters()) 1337 { 1338 int const w = std::round(starCenter->width) * scale; 1339 1340 // translate if a mosaic mask is present 1341 const QPointF center = (mask == nullptr) ? QPointF(starCenter->x, starCenter->y) : mask->translate(QPointF(starCenter->x, 1342 starCenter->y)); 1343 // Draw a circle around the detected star. 1344 // SEP coordinates are in the center of pixels, and Qt at the boundary. 1345 const double xCoord = center.x() - 0.5; 1346 const double yCoord = center.y() - 0.5; 1347 const int xc = std::round((xCoord - starCenter->width / 2.0f) * scale); 1348 const int yc = std::round((yCoord - starCenter->width / 2.0f) * scale); 1349 const int hw = w / 2; 1350 1351 BahtinovEdge* bEdge = dynamic_cast<BahtinovEdge*>(starCenter); 1352 if (bEdge != nullptr) 1353 { 1354 // Draw lines of diffraction pattern 1355 painter->setPen(QPen(Qt::red, scaleSize(2))); 1356 painter->drawLine(bEdge->line[0].x1() * scale, bEdge->line[0].y1() * scale, 1357 bEdge->line[0].x2() * scale, bEdge->line[0].y2() * scale); 1358 painter->setPen(QPen(Qt::green, scaleSize(2))); 1359 painter->drawLine(bEdge->line[1].x1() * scale, bEdge->line[1].y1() * scale, 1360 bEdge->line[1].x2() * scale, bEdge->line[1].y2() * scale); 1361 painter->setPen(QPen(Qt::darkGreen, scaleSize(2))); 1362 painter->drawLine(bEdge->line[2].x1() * scale, bEdge->line[2].y1() * scale, 1363 bEdge->line[2].x2() * scale, bEdge->line[2].y2() * scale); 1364 1365 // Draw center circle 1366 painter->setPen(QPen(Qt::white, scaleSize(2))); 1367 painter->drawEllipse(xc, yc, w, w); 1368 1369 // Draw offset circle 1370 double factor = 15.0; 1371 QPointF offsetVector = (bEdge->offset - QPointF(center.x(), center.y())) * factor; 1372 int const xo = std::round((center.x() + offsetVector.x() - starCenter->width / 2.0f) * scale); 1373 int const yo = std::round((center.y() + offsetVector.y() - starCenter->width / 2.0f) * scale); 1374 painter->setPen(QPen(Qt::red, scaleSize(2))); 1375 painter->drawEllipse(xo, yo, w, w); 1376 1377 // Draw line between center circle and offset circle 1378 painter->setPen(QPen(Qt::red, scaleSize(2))); 1379 painter->drawLine(xc + hw, yc + hw, xo + hw, yo + hw); 1380 } 1381 else 1382 { 1383 if (!showStarsHFR) 1384 { 1385 const double radius = starCenter->HFR > 0 ? 2.0f * starCenter->HFR * scale : w; 1386 painter->drawEllipse(QPointF(xCoord * scale, yCoord * scale), radius, radius); 1387 } 1388 } 1389 1390 if (showStarsHFR) 1391 { 1392 // Ask the painter how large will the HFR text be 1393 QString const hfr = QString("%1").arg(starCenter->HFR, 0, 'f', 2); 1394 if (!drawHFR(painter, hfr, xc + w + 5, yc + w / 2)) 1395 { 1396 // Try a few more time with smaller fonts; 1397 for (int i = 0; i < 10; ++i) 1398 { 1399 const double tempFontSize = painterFont.pointSizeF() - 2; 1400 if (tempFontSize <= 0) break; 1401 painterFont.setPointSizeF(tempFontSize); 1402 painter->setFont(painterFont); 1403 if (drawHFR(painter, hfr, xc + w + 5, yc + w / 2)) 1404 break; 1405 } 1406 // Reset the font size. 1407 painterFont.setPointSize(fontSize); 1408 painter->setFont(painterFont); 1409 } 1410 } 1411 } 1412 } 1413 1414 void FITSView::drawTrackingBox(QPainter * painter, double scale) 1415 { 1416 painter->setPen(QPen(Qt::green, scaleSize(2))); 1417 1418 if (trackingBox.isNull()) 1419 return; 1420 1421 const int x1 = trackingBox.x() * scale; 1422 const int y1 = trackingBox.y() * scale; 1423 const int w = trackingBox.width() * scale; 1424 const int h = trackingBox.height() * scale; 1425 1426 painter->drawRect(x1, y1, w, h); 1427 } 1428 1429 /** 1430 This Method draws a large Crosshair in the center of the image, it is like a set of axes. 1431 */ 1432 1433 void FITSView::drawCrosshair(QPainter * painter, double scale) 1434 { 1435 if (!m_ImageData) return; 1436 const int image_width = m_ImageData->width(); 1437 const int image_height = m_ImageData->height(); 1438 const QPointF c = QPointF((qreal)image_width / 2 * scale, (qreal)image_height / 2 * scale); 1439 const float midX = (float)image_width / 2 * scale; 1440 const float midY = (float)image_height / 2 * scale; 1441 const float maxX = (float)image_width * scale; 1442 const float maxY = (float)image_height * scale; 1443 const float r = 50 * scale; 1444 1445 painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")), scaleSize(1))); 1446 1447 //Horizontal Line to Circle 1448 painter->drawLine(0, midY, midX - r, midY); 1449 1450 //Horizontal Line past Circle 1451 painter->drawLine(midX + r, midY, maxX, midY); 1452 1453 //Vertical Line to Circle 1454 painter->drawLine(midX, 0, midX, midY - r); 1455 1456 //Vertical Line past Circle 1457 painter->drawLine(midX, midY + r, midX, maxY); 1458 1459 //Circles 1460 painter->drawEllipse(c, r, r); 1461 painter->drawEllipse(c, r / 2, r / 2); 1462 } 1463 1464 /** 1465 This method is intended to draw a pixel grid onto the image. It first determines useful information 1466 from the image. Then it draws the axes on the image if the crosshairs are not displayed. 1467 Finally it draws the gridlines so that there will be 4 Gridlines on either side of the axes. 1468 Note: This has to start drawing at the center not at the edges because the center axes must 1469 be in the center of the image. 1470 */ 1471 1472 void FITSView::drawPixelGrid(QPainter * painter, double scale) 1473 { 1474 const float width = m_ImageData->width() * scale; 1475 const float height = m_ImageData->height() * scale; 1476 const float cX = width / 2; 1477 const float cY = height / 2; 1478 const float deltaX = width / 10; 1479 const float deltaY = height / 10; 1480 QFontMetrics fm(painter->font()); 1481 1482 //draw the Axes 1483 painter->setPen(QPen(Qt::red, scaleSize(1))); 1484 painter->drawText(cX - 30, height - 5, QString::number((int)((cX) / scale))); 1485 QString str = QString::number((int)((cY) / scale)); 1486 #if QT_VERSION < QT_VERSION_CHECK(5,11,0) 1487 painter->drawText(width - (fm.width(str) + 10), cY - 5, str); 1488 #else 1489 painter->drawText(width - (fm.horizontalAdvance(str) + 10), cY - 5, str); 1490 #endif 1491 if (!showCrosshair) 1492 { 1493 painter->drawLine(cX, 0, cX, height); 1494 painter->drawLine(0, cY, width, cY); 1495 } 1496 painter->setPen(QPen(Qt::gray, scaleSize(1))); 1497 //Start one iteration past the Center and draw 4 lines on either side of 0 1498 for (int x = deltaX; x < cX - deltaX; x += deltaX) 1499 { 1500 painter->drawText(cX + x - 30, height - 5, QString::number((int)(cX + x) / scale)); 1501 painter->drawText(cX - x - 30, height - 5, QString::number((int)(cX - x) / scale)); 1502 painter->drawLine(cX - x, 0, cX - x, height); 1503 painter->drawLine(cX + x, 0, cX + x, height); 1504 } 1505 //Start one iteration past the Center and draw 4 lines on either side of 0 1506 for (int y = deltaY; y < cY - deltaY; y += deltaY) 1507 { 1508 QString str = QString::number((int)((cY + y) / scale)); 1509 #if QT_VERSION < QT_VERSION_CHECK(5,11,0) 1510 painter->drawText(width - (fm.width(str) + 10), cY + y - 5, str); 1511 #else 1512 painter->drawText(width - (fm.horizontalAdvance(str) + 10), cY + y - 5, str); 1513 #endif 1514 str = QString::number((int)((cY - y) / scale)); 1515 #if QT_VERSION < QT_VERSION_CHECK(5,11,0) 1516 painter->drawText(width - (fm.width(str) + 10), cY - y - 5, str); 1517 #else 1518 painter->drawText(width - (fm.horizontalAdvance(str) + 10), cY - y - 5, str); 1519 #endif 1520 painter->drawLine(0, cY + y, width, cY + y); 1521 painter->drawLine(0, cY - y, width, cY - y); 1522 } 1523 } 1524 bool FITSView::imageHasWCS() 1525 { 1526 if (m_ImageData != nullptr) 1527 return m_ImageData->hasWCS(); 1528 return false; 1529 } 1530 1531 void FITSView::drawObjectNames(QPainter * painter, double scale) 1532 { 1533 painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("FITSObjectLabelColor")))); 1534 for (const auto &listObject : m_ImageData->getSkyObjects()) 1535 { 1536 painter->drawRect(listObject->x() * scale - 5, listObject->y() * scale - 5, 10, 10); 1537 painter->drawText(listObject->x() * scale + 10, listObject->y() * scale + 10, listObject->skyObject()->name()); 1538 } 1539 } 1540 1541 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) 1542 void FITSView::drawHiPSOverlay(QPainter * painter, double scale) 1543 { 1544 if (m_HiPSOverlayPixmap.isNull()) 1545 { 1546 auto width = m_ImageData->width(); 1547 auto height = m_ImageData->height(); 1548 QImage image(width, height, QImage::Format_ARGB32_Premultiplied); 1549 SkyPoint startPoint; 1550 SkyPoint endPoint; 1551 SkyPoint centerPoint; 1552 1553 m_ImageData->pixelToWCS(QPointF(0, 0), startPoint); 1554 m_ImageData->pixelToWCS(QPointF(width - 1, height - 1), endPoint); 1555 m_ImageData->pixelToWCS(QPointF( (width - Options::hIPSOffsetX()) / 2.0, (height - Options::hIPSOffsetY()) / 2.0), 1556 centerPoint); 1557 1558 startPoint.updateCoordsNow(KStarsData::Instance()->updateNum()); 1559 endPoint.updateCoordsNow(KStarsData::Instance()->updateNum()); 1560 centerPoint.updateCoordsNow(KStarsData::Instance()->updateNum()); 1561 1562 auto fov_radius = startPoint.angularDistanceTo(&endPoint).Degrees() / 2; 1563 QVariant PA (0.0); 1564 m_ImageData->getRecordValue("CROTA1", PA); 1565 1566 auto rotation = 180 - PA.toDouble(); 1567 if (rotation > 360) 1568 rotation -= 360; 1569 1570 if (HIPSFinder::Instance()->renderFOV(¢erPoint, fov_radius, rotation, &image) == false) 1571 return; 1572 m_HiPSOverlayPixmap = QPixmap::fromImage(image); 1573 } 1574 1575 Q_UNUSED(scale); 1576 painter->setOpacity(Options::hIPSOpacity()); 1577 painter->drawPixmap(0, 0, m_HiPSOverlayPixmap); 1578 painter->setOpacity(1); 1579 } 1580 #endif 1581 1582 /** 1583 This method will paint EQ Gridlines in an overlay if there is WCS data present. 1584 It determines the minimum and maximum RA and DEC, then it uses that information to 1585 judge which gridLines to draw. Then it calls the drawEQGridlines methods below 1586 to draw gridlines at those specific RA and Dec values. 1587 */ 1588 1589 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) 1590 void FITSView::drawEQGrid(QPainter * painter, double scale) 1591 { 1592 const int image_width = m_ImageData->width(); 1593 const int image_height = m_ImageData->height(); 1594 1595 if (m_ImageData->hasWCS()) 1596 { 1597 double maxRA = -1000; 1598 double minRA = 1000; 1599 double maxDec = -1000; 1600 double minDec = 1000; 1601 m_ImageData->findWCSBounds(minRA, maxRA, minDec, maxDec); 1602 1603 auto minDecMinutes = (int)(minDec * 12); //This will force the Dec Scale to 5 arc minutes in the loop 1604 auto maxDecMinutes = (int)(maxDec * 12); 1605 1606 auto minRAMinutes = 1607 (int)(minRA / 15.0 * 1608 120.0); //This will force the scale to 1/2 minutes of RA in the loop from 0 to 50 degrees 1609 auto maxRAMinutes = (int)(maxRA / 15.0 * 120.0); 1610 1611 double raConvert = 15 / 120.0; //This will undo the calculation above to retrieve the actual RA. 1612 double decConvert = 1.0 / 12.0; //This will undo the calculation above to retrieve the actual DEC. 1613 1614 if (maxDec > 50 || minDec < -50) 1615 { 1616 minRAMinutes = 1617 (int)(minRA / 15.0 * 60.0); //This will force the scale to 1 min of RA from 50 to 80 degrees 1618 maxRAMinutes = (int)(maxRA / 15.0 * 60.0); 1619 raConvert = 15 / 60.0; 1620 } 1621 1622 if (maxDec > 80 || minDec < -80) 1623 { 1624 minRAMinutes = 1625 (int)(minRA / 15.0 * 30); //This will force the scale to 2 min of RA from 80 to 85 degrees 1626 maxRAMinutes = (int)(maxRA / 15.0 * 30); 1627 raConvert = 15 / 30.0; 1628 } 1629 if (maxDec > 85 || minDec < -85) 1630 { 1631 minRAMinutes = 1632 (int)(minRA / 15.0 * 6); //This will force the scale to 10 min of RA from 85 to 89 degrees 1633 maxRAMinutes = (int)(maxRA / 15.0 * 6); 1634 raConvert = 15 / 6.0; 1635 } 1636 if (maxDec >= 89.25 || minDec <= -89.25) 1637 { 1638 minRAMinutes = 1639 (int)(minRA / 1640 15); //This will force the scale to whole hours of RA in the loop really close to the poles 1641 maxRAMinutes = (int)(maxRA / 15); 1642 raConvert = 15; 1643 } 1644 1645 painter->setPen(QPen(Qt::yellow)); 1646 1647 QPointF pixelPoint, imagePoint, pPoint; 1648 1649 //This section draws the RA Gridlines 1650 1651 for (int targetRA = minRAMinutes; targetRA <= maxRAMinutes; targetRA++) 1652 { 1653 painter->setPen(QPen(Qt::yellow)); 1654 double target = targetRA * raConvert; 1655 1656 if (eqGridPoints.count() != 0) 1657 eqGridPoints.clear(); 1658 1659 double increment = std::abs((maxDec - minDec) / 1660 100.0); //This will determine how many points to use to create the RA Line 1661 1662 for (double targetDec = minDec; targetDec <= maxDec; targetDec += increment) 1663 { 1664 SkyPoint pointToGet(target / 15.0, targetDec); 1665 bool inImage = m_ImageData->wcsToPixel(pointToGet, pixelPoint, imagePoint); 1666 if (inImage) 1667 { 1668 QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale); 1669 eqGridPoints.append(pt); 1670 } 1671 } 1672 1673 if (eqGridPoints.count() > 1) 1674 { 1675 for (int i = 1; i < eqGridPoints.count(); i++) 1676 painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i)); 1677 QString str = QString::number(dms(target).hour()) + "h " + 1678 QString::number(dms(target).minute()) + '\''; 1679 if (maxDec <= 50 && maxDec >= -50) 1680 str = str + " " + QString::number(dms(target).second()) + "''"; 1681 QPointF pt = getPointForGridLabel(painter, str, scale); 1682 if (pt.x() != -100) 1683 painter->drawText(pt.x(), pt.y(), str); 1684 } 1685 } 1686 1687 //This section draws the DEC Gridlines 1688 1689 for (int targetDec = minDecMinutes; targetDec <= maxDecMinutes; targetDec++) 1690 { 1691 if (eqGridPoints.count() != 0) 1692 eqGridPoints.clear(); 1693 1694 double increment = std::abs((maxRA - minRA) / 1695 100.0); //This will determine how many points to use to create the Dec Line 1696 double target = targetDec * decConvert; 1697 1698 for (double targetRA = minRA; targetRA <= maxRA; targetRA += increment) 1699 { 1700 SkyPoint pointToGet(targetRA / 15, targetDec * decConvert); 1701 bool inImage = m_ImageData->wcsToPixel(pointToGet, pixelPoint, imagePoint); 1702 if (inImage) 1703 { 1704 QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale); 1705 eqGridPoints.append(pt); 1706 } 1707 } 1708 if (eqGridPoints.count() > 1) 1709 { 1710 for (int i = 1; i < eqGridPoints.count(); i++) 1711 painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i)); 1712 QString str = QString::number(dms(target).degree()) + "° " + QString::number(dms(target).arcmin()) + '\''; 1713 QPointF pt = getPointForGridLabel(painter, str, scale); 1714 if (pt.x() != -100) 1715 painter->drawText(pt.x(), pt.y(), str); 1716 } 1717 } 1718 1719 //This Section Draws the North Celestial Pole if present 1720 SkyPoint NCP(0, 90); 1721 1722 bool NCPtest = m_ImageData->wcsToPixel(NCP, pPoint, imagePoint); 1723 if (NCPtest) 1724 { 1725 bool NCPinImage = 1726 (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height); 1727 if (NCPinImage) 1728 { 1729 painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4, 1730 KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")); 1731 painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15, 1732 i18nc("North Celestial Pole", "NCP")); 1733 } 1734 } 1735 1736 //This Section Draws the South Celestial Pole if present 1737 SkyPoint SCP(0, -90); 1738 1739 bool SCPtest = m_ImageData->wcsToPixel(SCP, pPoint, imagePoint); 1740 if (SCPtest) 1741 { 1742 bool SCPinImage = 1743 (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height); 1744 if (SCPinImage) 1745 { 1746 painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4, 1747 KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")); 1748 painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15, 1749 i18nc("South Celestial Pole", "SCP")); 1750 } 1751 } 1752 } 1753 } 1754 #endif 1755 1756 bool FITSView::pointIsInImage(QPointF pt, double scale) 1757 { 1758 int image_width = m_ImageData->width(); 1759 int image_height = m_ImageData->height(); 1760 return pt.x() < image_width * scale && pt.y() < image_height * scale && pt.x() > 0 && pt.y() > 0; 1761 } 1762 1763 QPointF FITSView::getPointForGridLabel(QPainter *painter, const QString &str, double scale) 1764 { 1765 QFontMetrics fm(painter->font()); 1766 #if QT_VERSION < QT_VERSION_CHECK(5,11,0) 1767 int strWidth = fm.width(str); 1768 #else 1769 int strWidth = fm.horizontalAdvance(str); 1770 #endif 1771 int strHeight = fm.height(); 1772 int image_width = m_ImageData->width(); 1773 int image_height = m_ImageData->height(); 1774 1775 //These get the maximum X and Y points in the list that are in the image 1776 QPointF maxXPt(image_width * scale / 2, image_height * scale / 2); 1777 for (auto &p : eqGridPoints) 1778 { 1779 if (p.x() > maxXPt.x() && pointIsInImage(p, scale)) 1780 maxXPt = p; 1781 } 1782 QPointF maxYPt(image_width * scale / 2, image_height * scale / 2); 1783 1784 for (auto &p : eqGridPoints) 1785 { 1786 if (p.y() > maxYPt.y() && pointIsInImage(p, scale)) 1787 maxYPt = p; 1788 } 1789 QPointF minXPt(image_width * scale / 2, image_height * scale / 2); 1790 1791 for (auto &p : eqGridPoints) 1792 { 1793 if (p.x() < minXPt.x() && pointIsInImage(p, scale)) 1794 minXPt = p; 1795 } 1796 QPointF minYPt(image_width * scale / 2, image_height * scale / 2); 1797 1798 for (auto &p : eqGridPoints) 1799 { 1800 if (p.y() < minYPt.y() && pointIsInImage(p, scale)) 1801 minYPt = p; 1802 } 1803 1804 //This gives preference to points that are on the right hand side and bottom. 1805 //But if the line doesn't intersect the right or bottom, it then tries for the top and left. 1806 //If no points are found in the image, it returns a point off the screen 1807 //If all else fails, like in the case of a circle on the image, it returns the far right point. 1808 1809 if (image_width * scale - maxXPt.x() < strWidth) 1810 { 1811 return QPointF( 1812 image_width * scale - (strWidth + 10), 1813 maxXPt.y() - 1814 strHeight); //This will draw the text on the right hand side, up and to the left of the point where the line intersects 1815 } 1816 if (image_height * scale - maxYPt.y() < strHeight) 1817 return QPointF( 1818 maxYPt.x() - (strWidth + 10), 1819 image_height * scale - 1820 (strHeight + 10)); //This will draw the text on the bottom side, up and to the left of the point where the line intersects 1821 if (minYPt.y() < strHeight) 1822 return QPointF( 1823 minYPt.x() * scale + 10, 1824 strHeight + 20); //This will draw the text on the top side, down and to the right of the point where the line intersects 1825 if (minXPt.x() < strWidth) 1826 return QPointF( 1827 10, 1828 minXPt.y() * scale + 1829 strHeight + 1830 20); //This will draw the text on the left hand side, down and to the right of the point where the line intersects 1831 if (maxXPt.x() == image_width * scale / 2 && maxXPt.y() == image_height * scale / 2) 1832 return QPointF(-100, -100); //All of the points were off the screen 1833 1834 return QPoint(maxXPt.x() - (strWidth + 10), maxXPt.y() - (strHeight + 10)); 1835 } 1836 1837 void FITSView::setFirstLoad(bool value) 1838 { 1839 firstLoad = value; 1840 } 1841 1842 QPixmap &FITSView::getTrackingBoxPixmap(uint8_t margin) 1843 { 1844 if (trackingBox.isNull()) 1845 return trackingBoxPixmap; 1846 1847 // We need to know which rendering strategy updateFrame used to determine the scaling. 1848 const float scale = getScale(); 1849 1850 int x1 = (trackingBox.x() - margin) * scale; 1851 int y1 = (trackingBox.y() - margin) * scale; 1852 int w = (trackingBox.width() + margin * 2) * scale; 1853 int h = (trackingBox.height() + margin * 2) * scale; 1854 1855 trackingBoxPixmap = m_ImageFrame->grab(QRect(x1, y1, w, h)); 1856 return trackingBoxPixmap; 1857 } 1858 1859 void FITSView::setTrackingBox(const QRect &rect) 1860 { 1861 if (rect != trackingBox) 1862 { 1863 trackingBox = rect; 1864 updateFrame(); 1865 if(showStarProfile) 1866 viewStarProfile(); 1867 } 1868 } 1869 1870 void FITSView::resizeTrackingBox(int newSize) 1871 { 1872 int x = trackingBox.x() + trackingBox.width() / 2; 1873 int y = trackingBox.y() + trackingBox.height() / 2; 1874 int delta = newSize / 2; 1875 setTrackingBox(QRect( x - delta, y - delta, newSize, newSize)); 1876 } 1877 1878 void FITSView::processRectangleFixed(int s) 1879 { 1880 int w = m_ImageData->width(); 1881 int h = m_ImageData->height(); 1882 1883 QPoint c = selectionRectangleRaw.center(); 1884 c.setX(qMax((int)round(s / 2.0), c.x())); 1885 c.setX(qMin(w - (int)round(s / 2.0), c.x())); 1886 c.setY(qMax((int)round(s / 2.0), c.y())); 1887 c.setY(qMin(h - (int)round(s / 2.0), c.y())); 1888 1889 QPoint topLeft, botRight; 1890 topLeft = QPoint(c.x() - round(s / 2.0), c.y() - round(s / 2.0)); 1891 botRight = QPoint(c.x() + round(s / 2.0), c.y() + round(s / 2.0)); 1892 1893 emit setRubberBand(QRect(topLeft, botRight)); 1894 processRectangle(topLeft, botRight, true); 1895 } 1896 1897 void FITSView::processRectangle(QPoint p1, QPoint p2, bool calculate) 1898 { 1899 if(!isSelectionRectShown()) 1900 return; 1901 //the user can draw a rectangle by dragging the mouse to any direction 1902 //but we need to feed Rectangle(topleft, topright) 1903 //hence we calculate topleft and topright for each case 1904 1905 //p1 is the point where the user presses the mouse 1906 //p2 is the point where the user releases the mouse 1907 selectionRectangleRaw = QRect(p1, p2).normalized(); 1908 //Index out of bounds Check for raw Rectangle, this effectively works when user does shift + drag, other wise becomes redundant 1909 1910 QPoint topLeft = selectionRectangleRaw.topLeft(); 1911 QPoint botRight = selectionRectangleRaw.bottomRight(); 1912 1913 topLeft.setX(qMax(1, topLeft.x())); 1914 topLeft.setY(qMax(1, topLeft.y())); 1915 botRight.setX(qMin((int)m_ImageData->width(), botRight.x())); 1916 botRight.setY(qMin((int)m_ImageData->height(), botRight.y())); 1917 1918 selectionRectangleRaw.setTopLeft(topLeft); 1919 selectionRectangleRaw.setBottomRight(botRight); 1920 1921 if(calculate) 1922 { 1923 if(m_ImageData) 1924 { 1925 m_ImageData->makeRoiBuffer(selectionRectangleRaw); 1926 emit rectangleUpdated(selectionRectangleRaw); 1927 } 1928 } 1929 //updateFrameRoi(); 1930 1931 //emit raw rectangle for calculation 1932 //update the stats pane after calculation; there should be ample time for calculation before showing the values 1933 } 1934 1935 bool FITSView::isImageStretched() 1936 { 1937 return stretchImage; 1938 } 1939 1940 bool FITSView::isClippingShown() 1941 { 1942 return showClipping; 1943 } 1944 1945 bool FITSView::isCrosshairShown() 1946 { 1947 return showCrosshair; 1948 } 1949 1950 bool FITSView::isEQGridShown() 1951 { 1952 return showEQGrid; 1953 } 1954 1955 bool FITSView::isSelectionRectShown() 1956 { 1957 return showSelectionRect; 1958 } 1959 bool FITSView::areObjectsShown() 1960 { 1961 return showObjects; 1962 } 1963 1964 bool FITSView::isPixelGridShown() 1965 { 1966 return showPixelGrid; 1967 } 1968 1969 bool FITSView::isHiPSOverlayShown() 1970 { 1971 return showHiPSOverlay; 1972 } 1973 1974 void FITSView::toggleCrosshair() 1975 { 1976 showCrosshair = !showCrosshair; 1977 updateFrame(); 1978 } 1979 1980 void FITSView::toggleClipping() 1981 { 1982 showClipping = !showClipping; 1983 updateFrame(); 1984 } 1985 1986 void FITSView::toggleEQGrid() 1987 { 1988 showEQGrid = !showEQGrid; 1989 1990 if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning()) 1991 { 1992 QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS); 1993 wcsWatcher.setFuture(future); 1994 return; 1995 } 1996 1997 if (m_ImageFrame) 1998 updateFrame(); 1999 } 2000 2001 void FITSView::toggleHiPSOverlay() 2002 { 2003 showHiPSOverlay = !showHiPSOverlay; 2004 2005 if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning()) 2006 { 2007 QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS); 2008 wcsWatcher.setFuture(future); 2009 return; 2010 } 2011 2012 if (m_ImageFrame) 2013 { 2014 m_QueueUpdate = true; 2015 updateFrame(); 2016 } 2017 } 2018 2019 void FITSView::toggleSelectionMode() 2020 { 2021 showSelectionRect = !showSelectionRect; 2022 if (!showSelectionRect) 2023 emit rectangleUpdated(QRect()); 2024 else if (m_ImageData) 2025 { 2026 m_ImageData->makeRoiBuffer(selectionRectangleRaw); 2027 emit rectangleUpdated(selectionRectangleRaw); 2028 } 2029 2030 emit showRubberBand(showSelectionRect); 2031 if (m_ImageFrame) 2032 updateFrame(); 2033 2034 } 2035 void FITSView::toggleObjects() 2036 { 2037 showObjects = !showObjects; 2038 2039 if (m_ImageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning()) 2040 { 2041 QFuture<bool> future = QtConcurrent::run(m_ImageData.data(), &FITSData::loadWCS); 2042 wcsWatcher.setFuture(future); 2043 return; 2044 } 2045 2046 if (m_ImageFrame) 2047 { 2048 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) 2049 m_ImageData->searchObjects(); 2050 #endif 2051 updateFrame(); 2052 } 2053 } 2054 2055 void FITSView::toggleStars() 2056 { 2057 toggleStars(!markStars); 2058 if (m_ImageFrame) 2059 updateFrame(); 2060 } 2061 2062 void FITSView::toggleStretch() 2063 { 2064 stretchImage = !stretchImage; 2065 if (m_ImageFrame && rescale(ZOOM_KEEP_LEVEL)) 2066 updateFrame(); 2067 } 2068 2069 void FITSView::toggleStarProfile() 2070 { 2071 #ifdef HAVE_DATAVISUALIZATION 2072 showStarProfile = !showStarProfile; 2073 if(showStarProfile && trackingBoxEnabled) 2074 viewStarProfile(); 2075 if(toggleProfileAction) 2076 toggleProfileAction->setChecked(showStarProfile); 2077 2078 if(showStarProfile) 2079 { 2080 //The tracking box is already on for Guide and Focus Views, but off for Normal and Align views. 2081 //So for Normal and Align views, we need to set up the tracking box. 2082 if(mode == FITS_NORMAL || mode == FITS_ALIGN) 2083 { 2084 setCursorMode(selectCursor); 2085 connect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int))); 2086 trackingBox = QRect(0, 0, 128, 128); 2087 setTrackingBoxEnabled(true); 2088 if(starProfileWidget) 2089 connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int))); 2090 } 2091 if(starProfileWidget) 2092 connect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile())); 2093 } 2094 else 2095 { 2096 //This shuts down the tracking box for Normal and Align Views 2097 //It doesn't touch Guide and Focus Views because they still need a tracking box 2098 if(mode == FITS_NORMAL || mode == FITS_ALIGN) 2099 { 2100 if(getCursorMode() == selectCursor) 2101 setCursorMode(dragCursor); 2102 disconnect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int))); 2103 setTrackingBoxEnabled(false); 2104 if(starProfileWidget) 2105 disconnect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int))); 2106 } 2107 if(starProfileWidget) 2108 { 2109 disconnect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile())); 2110 starProfileWidget->close(); 2111 starProfileWidget = nullptr; 2112 } 2113 emit starProfileWindowClosed(); 2114 } 2115 updateFrame(); 2116 #endif 2117 } 2118 2119 void FITSView::move3DTrackingBox(int x, int y) 2120 { 2121 int boxSize = trackingBox.width(); 2122 QRect starRect = QRect(x - boxSize / 2, y - boxSize / 2, boxSize, boxSize); 2123 setTrackingBox(starRect); 2124 } 2125 2126 void FITSView::viewStarProfile() 2127 { 2128 #ifdef HAVE_DATAVISUALIZATION 2129 if(!trackingBoxEnabled) 2130 { 2131 setTrackingBoxEnabled(true); 2132 setTrackingBox(QRect(0, 0, 128, 128)); 2133 } 2134 if(!starProfileWidget) 2135 { 2136 starProfileWidget = new StarProfileViewer(this); 2137 2138 //This is a band-aid to fix a QT bug with createWindowContainer 2139 //It will set the cursor of the Window containing the view that called the Star Profile method to the Arrow Cursor 2140 //Note that Ekos Manager is a QDialog and FitsViewer is a KXmlGuiWindow 2141 QWidget * superParent = this->parentWidget(); 2142 while(superParent->parentWidget() != 0 && !superParent->inherits("QDialog") && !superParent->inherits("KXmlGuiWindow")) 2143 superParent = superParent->parentWidget(); 2144 superParent->setCursor(Qt::ArrowCursor); 2145 //This is the end of the band-aid 2146 2147 connect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile())); 2148 if(mode == FITS_ALIGN || mode == FITS_NORMAL) 2149 { 2150 starProfileWidget->enableTrackingBox(true); 2151 m_ImageData->setStarAlgorithm(ALGORITHM_CENTROID); 2152 connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int))); 2153 } 2154 } 2155 QList<Edge *> starCenters = m_ImageData->getStarCentersInSubFrame(trackingBox); 2156 if(starCenters.size() == 0) 2157 { 2158 // FIXME, the following does not work anymore. 2159 //m_ImageData->findStars(&trackingBox, true); 2160 // FIXME replacing it with this 2161 m_ImageData->findStars(ALGORITHM_CENTROID, trackingBox).waitForFinished(); 2162 starCenters = m_ImageData->getStarCentersInSubFrame(trackingBox); 2163 } 2164 2165 starProfileWidget->loadData(m_ImageData, trackingBox, starCenters); 2166 starProfileWidget->show(); 2167 starProfileWidget->raise(); 2168 if(markStars) 2169 updateFrame(); //this is to update for the marked stars 2170 2171 #endif 2172 } 2173 2174 void FITSView::togglePixelGrid() 2175 { 2176 showPixelGrid = !showPixelGrid; 2177 updateFrame(); 2178 } 2179 2180 QFuture<bool> FITSView::findStars(StarAlgorithm algorithm, const QRect &searchBox) 2181 { 2182 if(trackingBoxEnabled) 2183 return m_ImageData->findStars(algorithm, trackingBox); 2184 else 2185 return m_ImageData->findStars(algorithm, searchBox); 2186 } 2187 2188 void FITSView::toggleStars(bool enable) 2189 { 2190 markStars = enable; 2191 2192 if (markStars) 2193 searchStars(); 2194 } 2195 2196 void FITSView::searchStars() 2197 { 2198 QVariant frameType; 2199 if (m_ImageData->areStarsSearched() || !m_ImageData || (m_ImageData->getRecordValue("FRAME", frameType) 2200 && frameType.toString() != "Light")) 2201 return; 2202 2203 QApplication::setOverrideCursor(Qt::WaitCursor); 2204 emit newStatus(i18n("Finding stars..."), FITS_MESSAGE); 2205 qApp->processEvents(); 2206 2207 #ifdef HAVE_STELLARSOLVER 2208 QVariantMap extractionSettings; 2209 extractionSettings["optionsProfileIndex"] = Options::hFROptionsProfile(); 2210 extractionSettings["optionsProfileGroup"] = static_cast<int>(Ekos::HFRProfiles); 2211 imageData()->setSourceExtractorSettings(extractionSettings); 2212 #endif 2213 2214 QFuture<bool> result = findStars(ALGORITHM_SEP); 2215 result.waitForFinished(); 2216 if (result.result() && isVisible()) 2217 { 2218 emit newStatus("", FITS_MESSAGE); 2219 } 2220 QApplication::restoreOverrideCursor(); 2221 } 2222 2223 void FITSView::processPointSelection(int x, int y) 2224 { 2225 emit trackingStarSelected(x, y); 2226 } 2227 2228 void FITSView::processMarkerSelection(int x, int y) 2229 { 2230 markerCrosshair.setX(x); 2231 markerCrosshair.setY(y); 2232 2233 updateFrame(); 2234 } 2235 2236 void FITSView::setTrackingBoxEnabled(bool enable) 2237 { 2238 if (enable != trackingBoxEnabled) 2239 { 2240 trackingBoxEnabled = enable; 2241 //updateFrame(); 2242 } 2243 } 2244 2245 void FITSView::wheelEvent(QWheelEvent * event) 2246 { 2247 //This attempts to send the wheel event back to the Scroll Area if it was taken from a trackpad 2248 //It should still do the zoom if it is a mouse wheel 2249 if (event->source() == Qt::MouseEventSynthesizedBySystem) 2250 { 2251 QScrollArea::wheelEvent(event); 2252 } 2253 else 2254 { 2255 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) 2256 QPoint mouseCenter = getImagePoint(event->pos()); 2257 #else 2258 QPoint mouseCenter = getImagePoint(event->position().toPoint()); 2259 #endif 2260 if (event->angleDelta().y() > 0) 2261 ZoomIn(); 2262 else 2263 ZoomOut(); 2264 event->accept(); 2265 cleanUpZoom(mouseCenter); 2266 } 2267 emit zoomRubberBand(getCurrentZoom() / ZOOM_DEFAULT); 2268 } 2269 2270 /** 2271 This method is intended to keep key locations in an image centered on the screen while zooming. 2272 If there is a marker or tracking box, it centers on those. If not, it uses the point called 2273 viewCenter that was passed as a parameter. 2274 */ 2275 2276 void FITSView::cleanUpZoom(QPoint viewCenter) 2277 { 2278 int x0 = 0; 2279 int y0 = 0; 2280 double scale = (currentZoom / ZOOM_DEFAULT); 2281 if (!markerCrosshair.isNull()) 2282 { 2283 x0 = markerCrosshair.x() * scale; 2284 y0 = markerCrosshair.y() * scale; 2285 } 2286 else if (trackingBoxEnabled) 2287 { 2288 x0 = trackingBox.center().x() * scale; 2289 y0 = trackingBox.center().y() * scale; 2290 } 2291 else if (!viewCenter.isNull()) 2292 { 2293 x0 = viewCenter.x() * scale; 2294 y0 = viewCenter.y() * scale; 2295 } 2296 if ((x0 != 0) || (y0 != 0)) 2297 ensureVisible(x0, y0, width() / 2, height() / 2); 2298 updateMouseCursor(); 2299 } 2300 2301 /** 2302 This method converts a point from the ViewPort Coordinate System to the 2303 Image Coordinate System. 2304 */ 2305 2306 QPoint FITSView::getImagePoint(QPoint viewPortPoint) 2307 { 2308 QWidget * w = widget(); 2309 2310 if (w == nullptr) 2311 return QPoint(0, 0); 2312 2313 double scale = (currentZoom / ZOOM_DEFAULT); 2314 QPoint widgetPoint = w->mapFromParent(viewPortPoint); 2315 QPoint imagePoint = QPoint(widgetPoint.x() / scale, widgetPoint.y() / scale); 2316 return imagePoint; 2317 } 2318 2319 void FITSView::initDisplayImage() 2320 { 2321 // Account for leftover when sampling. Thus a 5-wide image sampled by 2 2322 // would result in a width of 3 (samples 0, 2 and 4). 2323 int w = (m_ImageData->width() + m_PreviewSampling - 1) / m_PreviewSampling; 2324 int h = (m_ImageData->height() + m_PreviewSampling - 1) / m_PreviewSampling; 2325 2326 if (m_ImageData->channels() == 1) 2327 { 2328 rawImage = QImage(w, h, QImage::Format_Indexed8); 2329 2330 rawImage.setColorCount(256); 2331 for (int i = 0; i < 256; i++) 2332 rawImage.setColor(i, qRgb(i, i, i)); 2333 } 2334 else 2335 { 2336 rawImage = QImage(w, h, QImage::Format_RGB32); 2337 } 2338 } 2339 2340 /** 2341 The Following two methods allow gestures to work with trackpads. 2342 Specifically, we are targeting the pinch events, so that if one is generated, 2343 Then the pinchTriggered method will be called. If the event is not a pinch gesture, 2344 then the event is passed back to the other event handlers. 2345 */ 2346 2347 bool FITSView::event(QEvent * event) 2348 { 2349 if (event->type() == QEvent::Gesture) 2350 return gestureEvent(dynamic_cast<QGestureEvent *>(event)); 2351 return QScrollArea::event(event); 2352 } 2353 2354 bool FITSView::gestureEvent(QGestureEvent * event) 2355 { 2356 if (QGesture * pinch = event->gesture(Qt::PinchGesture)) 2357 pinchTriggered(dynamic_cast<QPinchGesture *>(pinch)); 2358 return true; 2359 } 2360 2361 /** 2362 This Method works with Trackpads to use the pinch gesture to scroll in and out 2363 It stores a point to keep track of the location where the gesture started so that 2364 while you are zooming, it tries to keep that initial point centered in the view. 2365 **/ 2366 void FITSView::pinchTriggered(QPinchGesture * gesture) 2367 { 2368 if (!zooming) 2369 { 2370 zoomLocation = getImagePoint(mapFromGlobal(QCursor::pos())); 2371 zooming = true; 2372 } 2373 if (gesture->state() == Qt::GestureFinished) 2374 { 2375 zooming = false; 2376 } 2377 zoomTime++; //zoomTime is meant to slow down the zooming with a pinch gesture. 2378 if (zoomTime > 10000) //This ensures zoomtime never gets too big. 2379 zoomTime = 0; 2380 if (zooming && (zoomTime % 10 == 0)) //zoomTime is set to slow it by a factor of 10. 2381 { 2382 if (gesture->totalScaleFactor() > 1) 2383 ZoomIn(); 2384 else 2385 ZoomOut(); 2386 } 2387 cleanUpZoom(zoomLocation); 2388 } 2389 2390 /*void FITSView::handleWCSCompletion() 2391 { 2392 //bool hasWCS = wcsWatcher.result(); 2393 if(m_ImageData->hasWCS()) 2394 this->updateFrame(); 2395 emit wcsToggled(m_ImageData->hasWCS()); 2396 }*/ 2397 2398 void FITSView::syncWCSState() 2399 { 2400 bool hasWCS = m_ImageData->hasWCS(); 2401 bool wcsLoaded = m_ImageData->getWCSState() == FITSData::Success; 2402 2403 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) 2404 if (showObjects) 2405 m_ImageData->searchObjects(); 2406 #endif 2407 2408 if (hasWCS && wcsLoaded) 2409 this->updateFrame(); 2410 2411 emit wcsToggled(hasWCS); 2412 2413 if (toggleEQGridAction != nullptr) 2414 toggleEQGridAction->setEnabled(hasWCS); 2415 if (toggleObjectsAction != nullptr) 2416 toggleObjectsAction->setEnabled(hasWCS); 2417 if (centerTelescopeAction != nullptr) 2418 centerTelescopeAction->setEnabled(hasWCS); 2419 if (toggleHiPSOverlayAction != nullptr) 2420 toggleHiPSOverlayAction->setEnabled(hasWCS); 2421 } 2422 2423 void FITSView::createFloatingToolBar() 2424 { 2425 if (floatingToolBar != nullptr) 2426 return; 2427 2428 floatingToolBar = new QToolBar(this); 2429 auto * eff = new QGraphicsOpacityEffect(this); 2430 floatingToolBar->setGraphicsEffect(eff); 2431 eff->setOpacity(0.2); 2432 floatingToolBar->setVisible(false); 2433 floatingToolBar->setStyleSheet( 2434 "QToolBar{background: rgba(150, 150, 150, 210); border:none; color: yellow}" 2435 "QToolButton{background: transparent; border:none; color: yellow}" 2436 "QToolButton:hover{background: rgba(200, 200, 200, 255);border:solid; color: yellow}" 2437 "QToolButton:checked{background: rgba(110, 110, 110, 255);border:solid; color: yellow}"); 2438 floatingToolBar->setFloatable(true); 2439 floatingToolBar->setIconSize(QSize(25, 25)); 2440 //floatingToolBar->setMovable(true); 2441 2442 QAction * action = nullptr; 2443 2444 floatingToolBar->addAction(QIcon::fromTheme("zoom-in"), 2445 i18n("Zoom In"), this, SLOT(ZoomIn())); 2446 2447 floatingToolBar->addAction(QIcon::fromTheme("zoom-out"), 2448 i18n("Zoom Out"), this, SLOT(ZoomOut())); 2449 2450 floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-best"), 2451 i18n("Default Zoom"), this, SLOT(ZoomDefault())); 2452 2453 floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-width"), 2454 i18n("Zoom to Fit"), this, SLOT(ZoomToFit())); 2455 2456 toggleStretchAction = floatingToolBar->addAction(QIcon::fromTheme("transform-move"), 2457 i18n("Toggle Stretch"), 2458 this, SLOT(toggleStretch())); 2459 toggleStretchAction->setCheckable(true); 2460 2461 2462 floatingToolBar->addSeparator(); 2463 2464 action = floatingToolBar->addAction(QIcon::fromTheme("crosshairs"), 2465 i18n("Show Cross Hairs"), this, SLOT(toggleCrosshair())); 2466 action->setCheckable(true); 2467 2468 action = floatingToolBar->addAction(QIcon::fromTheme("map-flat"), 2469 i18n("Show Pixel Gridlines"), this, SLOT(togglePixelGrid())); 2470 action->setCheckable(true); 2471 2472 toggleStarsAction = 2473 floatingToolBar->addAction(QIcon::fromTheme("kstars_stars"), 2474 i18n("Detect Stars in Image"), this, SLOT(toggleStars())); 2475 toggleStarsAction->setCheckable(true); 2476 2477 #ifdef HAVE_DATAVISUALIZATION 2478 toggleProfileAction = 2479 floatingToolBar->addAction(QIcon::fromTheme("star-profile", QIcon(":/icons/star_profile.svg")), 2480 i18n("View Star Profile..."), this, SLOT(toggleStarProfile())); 2481 toggleProfileAction->setCheckable(true); 2482 #endif 2483 2484 if (mode == FITS_NORMAL || mode == FITS_ALIGN) 2485 { 2486 floatingToolBar->addSeparator(); 2487 2488 toggleEQGridAction = 2489 floatingToolBar->addAction(QIcon::fromTheme("kstars_grid"), 2490 i18n("Show Equatorial Gridlines"), this, &FITSView::toggleEQGrid); 2491 toggleEQGridAction->setCheckable(true); 2492 toggleEQGridAction->setEnabled(false); 2493 2494 toggleObjectsAction = 2495 floatingToolBar->addAction(QIcon::fromTheme("help-hint"), 2496 i18n("Show Objects in Image"), this, &FITSView::toggleObjects); 2497 toggleObjectsAction->setCheckable(true); 2498 toggleObjectsAction->setEnabled(false); 2499 2500 centerTelescopeAction = 2501 floatingToolBar->addAction(QIcon::fromTheme("center_telescope", QIcon(":/icons/center_telescope.svg")), 2502 i18n("Center Telescope"), this, &FITSView::centerTelescope); 2503 centerTelescopeAction->setCheckable(true); 2504 centerTelescopeAction->setEnabled(false); 2505 2506 toggleHiPSOverlayAction = 2507 floatingToolBar->addAction(QIcon::fromTheme("pixelate"), 2508 i18n("Show HiPS Overlay"), this, &FITSView::toggleHiPSOverlay); 2509 toggleHiPSOverlayAction->setCheckable(true); 2510 toggleHiPSOverlayAction->setEnabled(false); 2511 } 2512 } 2513 2514 /** 2515 This method either enables or disables the scope mouse mode so you can slew your scope to coordinates 2516 just by clicking the mouse on a spot in the image. 2517 */ 2518 2519 void FITSView::centerTelescope() 2520 { 2521 if (imageHasWCS()) 2522 { 2523 if (getCursorMode() == FITSView::scopeCursor) 2524 { 2525 setCursorMode(lastMouseMode); 2526 } 2527 else 2528 { 2529 lastMouseMode = getCursorMode(); 2530 setCursorMode(FITSView::scopeCursor); 2531 } 2532 updateFrame(); 2533 } 2534 updateScopeButton(); 2535 } 2536 2537 void FITSView::updateScopeButton() 2538 { 2539 if (centerTelescopeAction != nullptr) 2540 { 2541 if (getCursorMode() == FITSView::scopeCursor) 2542 { 2543 centerTelescopeAction->setChecked(true); 2544 } 2545 else 2546 { 2547 centerTelescopeAction->setChecked(false); 2548 } 2549 } 2550 } 2551 2552 /** 2553 This method just verifies if INDI is online, a telescope present, and is connected 2554 */ 2555 2556 bool FITSView::isTelescopeActive() 2557 { 2558 #ifdef HAVE_INDI 2559 if (INDIListener::Instance()->size() == 0) 2560 { 2561 return false; 2562 } 2563 2564 for (auto &oneDevice : INDIListener::devices()) 2565 { 2566 if (!(oneDevice->getDriverInterface() & INDI::BaseDevice::TELESCOPE_INTERFACE)) 2567 continue; 2568 return oneDevice->isConnected(); 2569 } 2570 return false; 2571 #else 2572 return false; 2573 #endif 2574 } 2575 2576 void FITSView::setStarsEnabled(bool enable) 2577 { 2578 markStars = enable; 2579 if (floatingToolBar != nullptr) 2580 { 2581 foreach (QAction * action, floatingToolBar->actions()) 2582 { 2583 if (action->text() == i18n("Detect Stars in Image")) 2584 { 2585 action->setChecked(markStars); 2586 break; 2587 } 2588 } 2589 } 2590 } 2591 2592 void FITSView::setStarsHFREnabled(bool enable) 2593 { 2594 showStarsHFR = enable; 2595 }