fitsview.cpp 69.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
/*  FITS View
    Copyright (C) 2003-2017 Jasem Mutlaq <mutlaqja@ikarustech.com>
    Copyright (C) 2016-2017 Robert Lancaster <rlancaste@gmail.com>

    This application is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.
*/

11
#include "config-kstars.h"
Jasem Mutlaq's avatar
Jasem Mutlaq committed
12
#include "fitsview.h"
13

14 15 16
#include "fitsdata.h"
#include "fitslabel.h"
#include "kspopupmenu.h"
17
#include "kstarsdata.h"
Jasem Mutlaq's avatar
Jasem Mutlaq committed
18
#include "ksutils.h"
19 20
#include "Options.h"
#include "skymap.h"
21
#include "fits_debug.h"
22
#include "stretch.h"
Jasem Mutlaq's avatar
Jasem Mutlaq committed
23

24
#ifdef HAVE_STELLARSOLVER
25
#include "ekos/auxiliary/stellarsolverprofileeditor.h"
26 27
#endif

28
#ifdef HAVE_INDI
29
#include "basedevice.h"
30 31 32
#include "indi/indilistener.h"
#endif

33 34 35
#include <KActionCollection>

#include <QtConcurrent>
Jasem Mutlaq's avatar
Jasem Mutlaq committed
36 37 38 39
#include <QScrollBar>
#include <QToolBar>
#include <QGraphicsOpacityEffect>
#include <QApplication>
40
#include <QImageReader>
Jasem Mutlaq's avatar
Jasem Mutlaq committed
41
#include <QGestureEvent>
42

43 44
#include <unistd.h>

45
#define BASE_OFFSET    50
46
#define ZOOM_DEFAULT   100.0f
47
#define ZOOM_MIN       10
48 49
// ZOOM_MAX is adjusted in the constructor if the amount of physical memory is known.
#define ZOOM_MAX       300
50 51
#define ZOOM_LOW_INCR  10
#define ZOOM_HIGH_INCR 50
52
#define FONT_SIZE      14
Jasem Mutlaq's avatar
Jasem Mutlaq committed
53

54 55
namespace
{
56 57 58 59

// Derive the Green and Blue stretch parameters from their previous values and the
// changes made to the Red parameters. We apply the same offsets used for Red to the
// other channels' parameters, but clip them.
60 61
void ComputeGBStretchParams(const StretchParams &newParams, StretchParams* params)
{
62 63 64 65 66 67 68 69 70 71
    float shadow_diff = newParams.grey_red.shadows - params->grey_red.shadows;
    float highlight_diff = newParams.grey_red.highlights - params->grey_red.highlights;
    float midtones_diff = newParams.grey_red.midtones - params->grey_red.midtones;

    params->green.shadows = params->green.shadows + shadow_diff;
    params->green.shadows = KSUtils::clamp(params->green.shadows, 0.0f, 1.0f);
    params->green.highlights = params->green.highlights + highlight_diff;
    params->green.highlights = KSUtils::clamp(params->green.highlights, 0.0f, 1.0f);
    params->green.midtones = params->green.midtones + midtones_diff;
    params->green.midtones = std::max(params->green.midtones, 0.0f);
72

73 74 75 76 77 78 79 80 81 82 83 84 85 86
    params->blue.shadows = params->blue.shadows + shadow_diff;
    params->blue.shadows = KSUtils::clamp(params->blue.shadows, 0.0f, 1.0f);
    params->blue.highlights = params->blue.highlights + highlight_diff;
    params->blue.highlights = KSUtils::clamp(params->blue.highlights, 0.0f, 1.0f);
    params->blue.midtones = params->blue.midtones + midtones_diff;
    params->blue.midtones = std::max(params->blue.midtones, 0.0f);
}

}  // namespace

// Runs the stretch checking the variables to see which parameters to use.
// We call stretch even if we're not stretching, as the stretch code still
// converts the image to the uint8 output image which will be displayed.
// In that case, it will use an identity stretch.
87
void FITSView::doStretch(QImage *outputImage)
88
{
89
    if (outputImage->isNull() || imageData.isNull())
90
        return;
91 92 93
    Stretch stretch(static_cast<int>(imageData->width()),
                    static_cast<int>(imageData->height()),
                    imageData->channels(), imageData->getStatistics().dataType);
94 95 96 97 98 99 100

    StretchParams tempParams;
    if (!stretchImage)
        tempParams = StretchParams();  // Keeping it linear
    else if (autoStretch)
    {
        // Compute new auto-stretch params.
101
        stretchParams = stretch.computeParams(imageData->getImageBuffer());
102 103 104 105 106 107 108
        tempParams = stretchParams;
    }
    else
        // Use the existing stretch params.
        tempParams = stretchParams;

    stretch.setParams(tempParams);
109
    stretch.run(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 116 117 118 119 120 121 122 123 124 125 126
{
    if (imageData->channels() == 3)
        ComputeGBStretchParams(params, &stretchParams);

    stretchParams.grey_red = params.grey_red;
    stretchParams.grey_red.shadows = std::max(stretchParams.grey_red.shadows, 0.0f);
    stretchParams.grey_red.highlights = std::max(stretchParams.grey_red.highlights, 0.0f);
    stretchParams.grey_red.midtones = std::max(stretchParams.grey_red.midtones, 0.0f);

    autoStretch = false;
    stretchImage = true;

    if (image_frame != nullptr && rescale(ZOOM_KEEP_LEVEL))
127
        updateFrame();
128 129 130 131 132 133 134 135 136 137 138 139
}

// Turn on or off stretching, and if on, use whatever parameters are currently stored.
void FITSView::setStretch(bool onOff)
{
    if (stretchImage != onOff)
    {
        stretchImage = onOff;
        if (image_frame != nullptr && rescale(ZOOM_KEEP_LEVEL))
            updateFrame();
    }
}
140

141 142 143 144 145 146 147 148
// Turn on stretching, using automatically generated parameters.
void FITSView::setAutoStretchParams()
{
    stretchImage = true;
    autoStretch = true;
    if (image_frame != nullptr && rescale(ZOOM_KEEP_LEVEL))
        updateFrame();
}
149

150
FITSView::FITSView(QWidget * parent, FITSMode fitsMode, FITSScale filterType) : QScrollArea(parent), zoomFactor(1.2)
Jasem Mutlaq's avatar
Jasem Mutlaq committed
151
{
152 153
    // stretchImage is whether to stretch or not--the stretch may or may not use automatically generated parameters.
    // The user may enter his/her own.
154
    stretchImage = Options::autoStretch();
155 156 157 158
    // autoStretch means use automatically-generated parameters. This is the default, unless the user overrides
    // by adjusting the stretchBar's sliders.
    autoStretch = true;

159 160 161 162
    // Adjust the maximum zoom according to the amount of memory.
    // There have been issues with users running out system memory because of zoom memory.
    // Note: this is not currently image dependent. It's possible, but not implemented,
    // to allow for more zooming on smaller images.
163 164 165
    zoomMax = ZOOM_MAX;

#if defined (Q_OS_LINUX) || defined (Q_OS_OSX)
166 167
    const long numPages = sysconf(_SC_PAGESIZE);
    const long pageSize = sysconf(_SC_PHYS_PAGES);
168

169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
    // _SC_PHYS_PAGES "may not be standard" http://man7.org/linux/man-pages/man3/sysconf.3.html
    // If an OS doesn't support it, sysconf should return -1.
    if (numPages > 0 && pageSize > 0)
    {
        // (numPages * pageSize) will likely overflow a 32bit int, so use floating point calculations.
        const int memoryMb = numPages * (static_cast<double>(pageSize) / 1e6);
        if (memoryMb < 2000)
            zoomMax = 100;
        else if (memoryMb < 4000)
            zoomMax = 200;
        else if (memoryMb < 8000)
            zoomMax = 300;
        else if (memoryMb < 16000)
            zoomMax = 400;
        else
            zoomMax = 600;
    }
186
#endif
187

188 189
    grabGesture(Qt::PinchGesture);

190
    image_frame.reset(new FITSLabel(this));
191 192
    filter = filterType;
    mode   = fitsMode;
Jasem Mutlaq's avatar
Jasem Mutlaq committed
193

194
    setBackgroundRole(QPalette::Dark);
195 196 197

    markerCrosshair.setX(0);
    markerCrosshair.setY(0);
Jasem Mutlaq's avatar
Jasem Mutlaq committed
198

199
    setBaseSize(740, 530);
200

201 202 203
    connect(image_frame.get(), SIGNAL(newStatus(QString, FITSBar)), this, SIGNAL(newStatus(QString, FITSBar)));
    connect(image_frame.get(), SIGNAL(pointSelected(int, int)), this, SLOT(processPointSelection(int, int)));
    connect(image_frame.get(), SIGNAL(markerSelected(int, int)), this, SLOT(processMarkerSelection(int, int)));
204
    connect(&wcsWatcher, SIGNAL(finished()), this, SLOT(syncWCSState()));
Jasem Mutlaq's avatar
Jasem Mutlaq committed
205

206 207
    connect(&fitsWatcher, &QFutureWatcher<bool>::finished, this, &FITSView::loadInFrame);

Jasem Mutlaq's avatar
Jasem Mutlaq committed
208
    image_frame->setMouseTracking(true);
209 210
    setCursorMode(
        selectCursor); //This is the default mode because the Focus and Align FitsViews should not be in dragMouse mode
Jasem Mutlaq's avatar
Jasem Mutlaq committed
211

212
    noImageLabel = new QLabel();
213 214 215 216 217
    noImage.load(":/images/noimage.png");
    noImageLabel->setPixmap(noImage);
    noImageLabel->setAlignment(Qt::AlignCenter);
    this->setWidget(noImageLabel);

218
    redScopePixmap = QPixmap(":/icons/center_telescope_red.svg").scaled(32, 32, Qt::KeepAspectRatio, Qt::FastTransformation);
219 220
    magentaScopePixmap = QPixmap(":/icons/center_telescope_magenta.svg").scaled(32, 32, Qt::KeepAspectRatio,
                         Qt::FastTransformation);
Jasem Mutlaq's avatar
Jasem Mutlaq committed
221 222 223 224
}

FITSView::~FITSView()
{
Jasem Mutlaq's avatar
Jasem Mutlaq committed
225
    fitsWatcher.waitForFinished();
Jasem Mutlaq's avatar
Jasem Mutlaq committed
226
    wcsWatcher.waitForFinished();
Jasem Mutlaq's avatar
Jasem Mutlaq committed
227 228
}

229 230 231 232
/**
This method looks at what mouse mode is currently selected and updates the cursor to match.
 */

233 234
void FITSView::updateMouseCursor()
{
235
    if (cursorMode == dragCursor)
236
    {
237
        if (horizontalScrollBar()->maximum() > 0 || verticalScrollBar()->maximum() > 0)
238
        {
239
            if (!image_frame->getMouseButtonDown())
240 241 242 243 244 245 246
                viewport()->setCursor(Qt::PointingHandCursor);
            else
                viewport()->setCursor(Qt::ClosedHandCursor);
        }
        else
            viewport()->setCursor(Qt::CrossCursor);
    }
247
    else if (cursorMode == selectCursor)
248
    {
249 250
        viewport()->setCursor(Qt::CrossCursor);
    }
251
    else if (cursorMode == scopeCursor)
252
    {
253 254 255 256 257
        viewport()->setCursor(QCursor(redScopePixmap, 10, 10));
    }
    else if (cursorMode == crosshairCursor)
    {
        viewport()->setCursor(QCursor(magentaScopePixmap, 10, 10));
258 259 260 261 262 263 264 265 266 267 268
    }
}

/**
This is how the mouse mode gets set.
The default for a FITSView in a FITSViewer should be the dragMouse
The default for a FITSView in the Focus or Align module should be the selectMouse
The different defaults are accomplished by putting making the actual default mouseMode
the selectMouse, but when a FITSViewer loads an image, it immediately makes it the dragMouse.
 */

269
void FITSView::setCursorMode(CursorMode mode)
270
{
271 272 273
    cursorMode = mode;
    updateMouseCursor();

274
    if (mode == scopeCursor && imageHasWCS())
275
    {
276
        if (imageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
277
        {
Hy Murveit's avatar
Hy Murveit committed
278
            QFuture<bool> future = QtConcurrent::run(imageData.data(), &FITSData::loadWCS, true);
279 280 281
            wcsWatcher.setFuture(future);
        }
    }
282 283
}

284
void FITSView::resizeEvent(QResizeEvent * event)
285
{
286
    if ((imageData == nullptr) && noImageLabel != nullptr)
287
    {
288 289 290
        noImageLabel->setPixmap(
            noImage.scaled(width() - 20, height() - 20, Qt::KeepAspectRatio, Qt::FastTransformation));
        noImageLabel->setFixedSize(width() - 5, height() - 5);
291 292 293 294 295
    }

    QScrollArea::resizeEvent(event);
}

296

297
void FITSView::loadFile(const QString &inFilename, bool silent)
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
{
    if (floatingToolBar != nullptr)
    {
        floatingToolBar->setVisible(true);
    }

    bool setBayerParams = false;

    BayerParams param;
    if ((imageData != nullptr) && imageData->hasDebayer())
    {
        setBayerParams = true;
        imageData->getBayerParams(&param);
    }

313 314
    // In case image is still loading, wait until it is done.
    fitsWatcher.waitForFinished();
315 316 317
    // In case loadWCS is still running for previous image data, let's wait until it's over
    wcsWatcher.waitForFinished();

318 319
    //    delete imageData;
    //    imageData = nullptr;
320 321 322 323 324 325

    filterStack.clear();
    filterStack.push(FITS_NONE);
    if (filter != FITS_NONE)
        filterStack.push(filter);

326
    imageData.reset(new FITSData(mode), &QObject::deleteLater);
327 328 329 330

    if (setBayerParams)
        imageData->setBayerParams(&param);

331
    fitsWatcher.setFuture(imageData->loadFromFile(inFilename, silent));
332 333
}

334
bool FITSView::loadData(const QSharedPointer<FITSData> &data)
335
{
336 337 338 339 340 341 342 343
    if (floatingToolBar != nullptr)
    {
        floatingToolBar->setVisible(true);
    }

    // In case loadWCS is still running for previous image data, let's wait until it's over
    wcsWatcher.waitForFinished();

344 345 346 347 348
    //    if (imageData != nullptr)
    //    {
    //        delete imageData;
    //        imageData = nullptr;
    //    }
349

350 351 352 353
    filterStack.clear();
    filterStack.push(FITS_NONE);
    if (filter != FITS_NONE)
        filterStack.push(filter);
354

355 356 357 358 359 360 361 362
    // Takes control of the objects passed in.
    imageData = data;

    return processData();
}

bool FITSView::processData()
{
363
    // Set current width and height
364 365
    if (!imageData)
        return false;
366 367 368
    currentWidth = imageData->width();
    currentHeight = imageData->height();

369 370
    int image_width  = currentWidth;
    int image_height = currentHeight;
371 372 373 374

    image_frame->setSize(image_width, image_height);

    // Init the display image
375 376
    // JM 2020.01.08: Disabling as proposed by Hy
    //initDisplayImage();
377

378
    imageData->applyFilter(filter);
Jasem Mutlaq's avatar
Jasem Mutlaq committed
379

380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398
    double availableRAM = 0;
    if (Options::adaptiveSampling() && (availableRAM = KSUtils::getAvailableRAM()) > 0)
    {
        // Possible color maximum image size
        double max_size = image_width * image_height * 4;
        // Ratio of image size to available RAM size
        double ratio = max_size / availableRAM;

        // Increase adaptive sampling with more limited RAM
        if (ratio < 0.1)
            m_AdaptiveSampling = 1;
        else if (ratio < 0.2)
            m_AdaptiveSampling = 2;
        else
            m_AdaptiveSampling = 4;

        m_PreviewSampling *= m_AdaptiveSampling;
    }

399 400 401 402 403
    // Rescale to fits window on first load
    if (firstLoad)
    {
        currentZoom = 100;

Jasem Mutlaq's avatar
Jasem Mutlaq committed
404
        if (rescale(ZOOM_FIT_WINDOW) == false)
405 406
        {
            m_LastError = i18n("Rescaling image failed.");
407
            return false;
408 409 410 411 412 413
        }

        firstLoad = false;
    }
    else
    {
Jasem Mutlaq's avatar
Jasem Mutlaq committed
414
        if (rescale(ZOOM_KEEP_LEVEL) == false)
415 416
        {
            m_LastError = i18n("Rescaling image failed.");
417
            return false;
418 419 420 421 422 423
        }
    }

    setAlignment(Qt::AlignCenter);

    // Load WCS data now if selected and image contains valid WCS header
424 425 426 427
    if ((mode == FITS_NORMAL || mode == FITS_ALIGN) &&
            imageData->hasWCS() && imageData->getWCSState() == FITSData::Idle &&
            Options::autoWCS() &&
            !wcsWatcher.isRunning())
428
    {
Hy Murveit's avatar
Hy Murveit committed
429
        QFuture<bool> future = QtConcurrent::run(imageData.data(), &FITSData::loadWCS, true);
430 431 432 433 434 435 436 437 438 439 440 441 442
        wcsWatcher.setFuture(future);
    }
    else
        syncWCSState();

    if (isVisible())
        emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION);

    if (showStarProfile)
    {
        if(floatingToolBar != nullptr)
            toggleProfileAction->setChecked(true);
        //Need to wait till the Focus module finds stars, if its the Focus module.
443
        QTimer::singleShot(100, this, SLOT(viewStarProfile()));
444 445 446
    }

    updateFrame();
447 448 449 450 451 452 453 454 455 456 457 458
    return true;
}

void FITSView::loadInFrame()
{
    // Check if the loading was OK
    if (fitsWatcher.result() == false)
    {
        m_LastError = imageData->getLastError();
        emit failed();
        return;
    }
459

460 461 462 463
    // Notify if there is debayer data.
    emit debayerToggled(imageData->hasDebayer());

    if (processData())
464
        emit loaded();
465
    else
466
        emit failed();
467
}
Jasem Mutlaq's avatar
Jasem Mutlaq committed
468

469
bool FITSView::saveImage(const QString &newFilename)
Jasem Mutlaq's avatar
Jasem Mutlaq committed
470
{
471
    const QString ext = QFileInfo(newFilename).suffix();
472
    if (QImageReader::supportedImageFormats().contains(ext.toLatin1()))
473 474 475 476 477 478
    {
        rawImage.save(newFilename, ext.toLatin1().constData());
        return true;
    }

    return imageData->saveImage(newFilename);
Jasem Mutlaq's avatar
Jasem Mutlaq committed
479 480
}

481
bool FITSView::rescale(FITSZoom type)
482
{
483
    switch (imageData->getStatistics().dataType)
484
    {
485
        case TBYTE:
486
            return rescale<uint8_t>(type);
487 488 489 490 491 492

        case TSHORT:
            return rescale<int16_t>(type);

        case TUSHORT:
            return rescale<uint16_t>(type);
493

494 495
        case TLONG:
            return rescale<int32_t>(type);
496

497 498 499 500
        case TULONG:
            return rescale<uint32_t>(type);

        case TFLOAT:
Jasem Mutlaq's avatar
Jasem Mutlaq committed
501
            return rescale<float>(type);
502 503 504 505 506 507 508 509

        case TLONGLONG:
            return rescale<int64_t>(type);

        case TDOUBLE:
            return rescale<double>(type);

        default:
510
            break;
511 512
    }

513
    return false;
514 515
}

516
FITSView::CursorMode FITSView::getCursorMode()
517
{
518
    return cursorMode;
519 520
}

521
void FITSView::enterEvent(QEvent * event)
522
{
523 524 525
    Q_UNUSED(event)

    if ((floatingToolBar != nullptr) && (imageData != nullptr))
526
    {
527
        QPointer<QGraphicsOpacityEffect> eff = new QGraphicsOpacityEffect(this);
528
        floatingToolBar->setGraphicsEffect(eff);
529
        QPointer<QPropertyAnimation> a = new QPropertyAnimation(eff, "opacity");
530 531 532 533 534 535 536 537
        a->setDuration(500);
        a->setStartValue(0.2);
        a->setEndValue(1);
        a->setEasingCurve(QEasingCurve::InBack);
        a->start(QPropertyAnimation::DeleteWhenStopped);
    }
}

538
void FITSView::leaveEvent(QEvent * event)
539
{
540 541 542
    Q_UNUSED(event)

    if ((floatingToolBar != nullptr) && (imageData != nullptr))
543
    {
544
        QPointer<QGraphicsOpacityEffect> eff = new QGraphicsOpacityEffect(this);
545
        floatingToolBar->setGraphicsEffect(eff);
546
        QPointer<QPropertyAnimation> a = new QPropertyAnimation(eff, "opacity");
547 548 549 550 551 552 553 554
        a->setDuration(500);
        a->setStartValue(1);
        a->setEndValue(0.2);
        a->setEasingCurve(QEasingCurve::OutBack);
        a->start(QPropertyAnimation::DeleteWhenStopped);
    }
}

555
template <typename T>
556
bool FITSView::rescale(FITSZoom type)
557
{
558 559 560
    // JM 2020.01.08: Disabling as proposed by Hy
    //    if (rawImage.isNull())
    //        return false;
561

562 563
    if (!imageData)
        return false;
564 565 566 567
    int image_width  = imageData->width();
    int image_height = imageData->height();
    currentWidth  = image_width;
    currentHeight = image_height;
568

569
    if (isVisible())
570
        emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION);
571

Jasem Mutlaq's avatar
Jasem Mutlaq committed
572 573
    switch (type)
    {
574
        case ZOOM_FIT_WINDOW:
575
            if ((image_width > width() || image_height > height()))
576
            {
577 578
                double w = baseSize().width() - BASE_OFFSET;
                double h = baseSize().height() - BASE_OFFSET;
579

580
                if (!firstLoad)
581 582 583 584
                {
                    w = viewport()->rect().width() - BASE_OFFSET;
                    h = viewport()->rect().height() - BASE_OFFSET;
                }
Jasem Mutlaq's avatar
Jasem Mutlaq committed
585

586
                // Find the zoom level which will enclose the current FITS in the current window size
587 588
                double zoomX                  = floor((w / static_cast<double>(currentWidth)) * 100.);
                double zoomY                  = floor((h / static_cast<double>(currentHeight)) * 100.);
589
                (zoomX < zoomY) ? currentZoom = zoomX : currentZoom = zoomY;
Jasem Mutlaq's avatar
Jasem Mutlaq committed
590

591 592
                currentWidth  = image_width * (currentZoom / ZOOM_DEFAULT);
                currentHeight = image_height * (currentZoom / ZOOM_DEFAULT);
Jasem Mutlaq's avatar
Jasem Mutlaq committed
593

594 595 596 597 598 599 600 601 602 603
                if (currentZoom <= ZOOM_MIN)
                    emit actionUpdated("view_zoom_out", false);
            }
            else
            {
                currentZoom   = 100;
                currentWidth  = image_width;
                currentHeight = image_height;
            }
            break;
Jasem Mutlaq's avatar
Jasem Mutlaq committed
604

605 606 607 608 609
        case ZOOM_KEEP_LEVEL:
        {
            currentWidth  = image_width * (currentZoom / ZOOM_DEFAULT);
            currentHeight = image_height * (currentZoom / ZOOM_DEFAULT);
        }
Jasem Mutlaq's avatar
Jasem Mutlaq committed
610 611
        break;

612
        default:
613
            currentZoom = 100;
Jasem Mutlaq's avatar
Jasem Mutlaq committed
614

615
            break;
Jasem Mutlaq's avatar
Jasem Mutlaq committed
616 617
    }

618 619
    initDisplayImage();
    image_frame->setScaledContents(true);
620
    doStretch(&rawImage);
621
    setWidget(image_frame.get());
Jasem Mutlaq's avatar
Jasem Mutlaq committed
622

623 624
    // This is needed by fitstab, even if the zoom doesn't change, to change the stretch UI.
    emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM);
Jasem Mutlaq's avatar
Jasem Mutlaq committed
625

626
    return true;
Jasem Mutlaq's avatar
Jasem Mutlaq committed
627 628 629 630
}

void FITSView::ZoomIn()
{
631
    if (currentZoom >= ZOOM_DEFAULT && Options::limitedResourcesMode())
632
    {
633
        emit newStatus(i18n("Cannot zoom in further due to active limited resources mode."), FITS_MESSAGE);
634 635
        return;
    }
Jasem Mutlaq's avatar
Jasem Mutlaq committed
636 637 638 639 640 641 642

    if (currentZoom < ZOOM_DEFAULT)
        currentZoom += ZOOM_LOW_INCR;
    else
        currentZoom += ZOOM_HIGH_INCR;

    emit actionUpdated("view_zoom_out", true);
643
    if (currentZoom >= zoomMax)
644
    {
645
        currentZoom = zoomMax;
Jasem Mutlaq's avatar
Jasem Mutlaq committed
646
        emit actionUpdated("view_zoom_in", false);
647
    }
Jasem Mutlaq's avatar
Jasem Mutlaq committed
648

649 650 651
    if (!imageData) return;
    currentWidth  = imageData->width() * (currentZoom / ZOOM_DEFAULT);
    currentHeight = imageData->height() * (currentZoom / ZOOM_DEFAULT);
Jasem Mutlaq's avatar
Jasem Mutlaq committed
652 653 654

    updateFrame();

655
    emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM);
Jasem Mutlaq's avatar
Jasem Mutlaq committed
656 657 658 659 660 661 662 663 664 665
}

void FITSView::ZoomOut()
{
    if (currentZoom <= ZOOM_DEFAULT)
        currentZoom -= ZOOM_LOW_INCR;
    else
        currentZoom -= ZOOM_HIGH_INCR;

    if (currentZoom <= ZOOM_MIN)
666 667
    {
        currentZoom = ZOOM_MIN;
Jasem Mutlaq's avatar
Jasem Mutlaq committed
668
        emit actionUpdated("view_zoom_out", false);
669
    }
Jasem Mutlaq's avatar
Jasem Mutlaq committed
670 671 672

    emit actionUpdated("view_zoom_in", true);

673 674 675
    if (!imageData) return;
    currentWidth  = imageData->width() * (currentZoom / ZOOM_DEFAULT);
    currentHeight = imageData->height() * (currentZoom / ZOOM_DEFAULT);
Jasem Mutlaq's avatar
Jasem Mutlaq committed
676 677 678

    updateFrame();

679
    emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM);
Jasem Mutlaq's avatar
Jasem Mutlaq committed
680 681
}

682 683
void FITSView::ZoomToFit()
{
684
    if (rawImage.isNull() == false)
685
    {
686 687 688
        rescale(ZOOM_FIT_WINDOW);
        updateFrame();
    }
689 690
}

691
void FITSView::setStarFilterRange(float const innerRadius, float const outerRadius)
Eric Dejouhanet's avatar
Eric Dejouhanet committed
692 693 694
{
    starFilter.innerRadius = innerRadius;
    starFilter.outerRadius = outerRadius;
695 696 697 698
}

int FITSView::filterStars()
{
699 700
    return starFilter.used() ? imageData->filterStars(starFilter.innerRadius,
            starFilter.outerRadius) : imageData->getStarCenters().count();
Eric Dejouhanet's avatar
Eric Dejouhanet committed
701 702
}

Hy Murveit's avatar
Hy Murveit committed
703 704 705
// isImageLarge() returns whether we use the large-image rendering strategy or the small-image strategy.
// See the comment below in getScale() for details.
bool FITSView::isLargeImage()
Jasem Mutlaq's avatar
Jasem Mutlaq committed
706
{
Hy Murveit's avatar
Hy Murveit committed
707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729
    constexpr int largeImageNumPixels = 1000 * 1000;
    return rawImage.width() * rawImage.height() >= largeImageNumPixels;
}

// getScale() is related to the image and overlay rendering strategy used.
// If we're using a pixmap apprpriate for a large image, where we draw and render on a pixmap that's the image size
// and we let the QLabel deal with scaling and zooming, then the scale is 1.0.
// With smaller images, where memory use is not as severe, we create a pixmap that's the size of the scaled image
// and get scale returns the ratio of that pixmap size to the image size.
double FITSView::getScale()
{
    return isLargeImage() ? 1.0 : currentZoom / ZOOM_DEFAULT;
}

// scaleSize() is only used with the large-image rendering strategy. It may increase the line
// widths or font sizes, as we draw lines and render text on the full image and when zoomed out,
// these sizes may be too small.
double FITSView::scaleSize(double size)
{
    if (!isLargeImage())
        return size;
    return currentZoom > 100.0 ? size : std::round(size * 100.0 / currentZoom);
}
Jasem Mutlaq's avatar
Jasem Mutlaq committed
730

Hy Murveit's avatar
Hy Murveit committed
731 732
void FITSView::updateFrame()
{
733
    if (toggleStretchAction)
734 735
        toggleStretchAction->setChecked(stretchImage);

Hy Murveit's avatar
Hy Murveit committed
736 737 738 739 740 741 742 743 744 745
    // We employ two schemes for managing the image and its overlays, depending on the size of the image
    // and whether we need to therefore conserve memory. The small-image strategy explicitly scales up
    // the image, and writes overlays on the scaled pixmap. The large-image strategy uses a pixmap that's
    // the size of the image itself, never scaling that up.
    if (isLargeImage())
        updateFrameLargeImage();
    else
        updateFrameSmallImage();
}

Jasem Mutlaq's avatar
Jasem Mutlaq committed
746

Hy Murveit's avatar
Hy Murveit committed
747 748 749
void FITSView::updateFrameLargeImage()
{
    if (!displayPixmap.convertFromImage(rawImage))
Jasem Mutlaq's avatar
Jasem Mutlaq committed
750 751 752 753
        return;

    QPainter painter(&displayPixmap);

754 755 756 757 758
    // Possibly scale the fonts as we're drawing on the full image, not just the visible part of the scroll window.
    QFont font = painter.font();
    font.setPixelSize(scaleSize(FONT_SIZE));
    painter.setFont(font);

759
    if (m_PreviewSampling == 1)
Eric Dejouhanet's avatar
Eric Dejouhanet committed
760
    {
Hy Murveit's avatar
Hy Murveit committed
761 762
        drawOverlay(&painter, 1.0);
        drawStarFilter(&painter, 1.0);
763
    }
764
    image_frame->setPixmap(displayPixmap);
765

766
    image_frame->resize(((m_PreviewSampling * currentZoom) / 100.0) * image_frame->pixmap()->size());
Jasem Mutlaq's avatar
Jasem Mutlaq committed
767 768
}

Hy Murveit's avatar
Hy Murveit committed
769 770 771 772 773 774 775 776
void FITSView::updateFrameSmallImage()
{
    QImage scaledImage = rawImage.scaled(currentWidth, currentHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);
    if (!displayPixmap.convertFromImage(scaledImage))
        return;

    QPainter painter(&displayPixmap);

777
    if (m_PreviewSampling == 1)
Hy Murveit's avatar
Hy Murveit committed
778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805
    {
        drawOverlay(&painter, currentZoom / ZOOM_DEFAULT);
        drawStarFilter(&painter, currentZoom / ZOOM_DEFAULT);
    }
    image_frame->setPixmap(displayPixmap);
    image_frame->resize(currentWidth, currentHeight);
}

void FITSView::drawStarFilter(QPainter *painter, double scale)
{
    if (!starFilter.used())
        return;
    const double w = imageData->width() * scale;
    const double h = imageData->height() * scale;
    double const diagonal = std::sqrt(w * w + h * h) / 2;
    int const innerRadius = std::lround(diagonal * starFilter.innerRadius);
    int const outerRadius = std::lround(diagonal * starFilter.outerRadius);
    QPoint const center(w / 2, h / 2);
    painter->save();
    painter->setPen(QPen(Qt::blue, scaleSize(1), Qt::DashLine));
    painter->setOpacity(0.7);
    painter->setBrush(QBrush(Qt::transparent));
    painter->drawEllipse(center, outerRadius, outerRadius);
    painter->setBrush(QBrush(Qt::blue, Qt::FDiagPattern));
    painter->drawEllipse(center, innerRadius, innerRadius);
    painter->restore();
}

806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907
namespace
{

template <typename T>
void drawClippingOneChannel(T *inputBuffer, QPainter *painter, int width, int height, double clipVal, double scale)
{
    painter->save();
    painter->setPen(QPen(Qt::red, scale, Qt::SolidLine));
    const T clipping = clipVal;
    for (int y = 0; y < height; y++)
    {
        const auto inputLine  = inputBuffer + y * width;
        for (int x = 0; x < width; x++)
        {
            if (inputLine[x] > clipping)
                painter->drawPoint(x, y);
        }
    }
    painter->restore();
}

template <typename T>
void drawClippingThreeChannels(T *inputBuffer, QPainter *painter, int width, int height, double clipVal, double scale)
{
    painter->save();
    painter->setPen(QPen(Qt::red, scale, Qt::SolidLine));
    const int size = width * height;
    const T clipping = clipVal;
    for (int y = 0; y < height; y++)
    {
        // R, G, B input images are stored one after another.
        const T * inputLineR  = inputBuffer + y * width;
        const T * inputLineG  = inputLineR + size;
        const T * inputLineB  = inputLineG + size;

        for (int x = 0; x < width; x++)
        {
            const T inputR = inputLineR[x];
            const T inputG = inputLineG[x];
            const T inputB = inputLineB[x];
            if (inputR > clipping || inputG > clipping || inputB > clipping)
                painter->drawPoint(x, y);
        }
    }
    painter->restore();
}

template <typename T>
void drawClip(T *input_buffer, int num_channels, QPainter *painter, int width, int height, double clipVal, double scale)
{
    if (num_channels == 1)
        drawClippingOneChannel(input_buffer, painter, width, height, clipVal, scale);
    else if (num_channels == 3)
        drawClippingThreeChannels(input_buffer, painter, width, height, clipVal, scale);
}

}  // namespace

void FITSView::drawClipping(QPainter *painter)
{
    auto input = imageData->getImageBuffer();
    const int height = imageData->height();
    const int width = imageData->width();
    constexpr double FLOAT_CLIP = 60000;
    constexpr double SHORT_CLIP = 30000;
    constexpr double USHORT_CLIP = 60000;
    constexpr double BYTE_CLIP = 250;
    switch (imageData->getStatistics().dataType)
    {
        case TBYTE:
            drawClip(reinterpret_cast<uint8_t const*>(input), imageData->channels(), painter, width, height, BYTE_CLIP,
                     scaleSize(1));
            break;
        case TSHORT:
            drawClip(reinterpret_cast<short const*>(input), imageData->channels(), painter, width, height, SHORT_CLIP,
                     scaleSize(1));
            break;
        case TUSHORT:
            drawClip(reinterpret_cast<unsigned short const*>(input), imageData->channels(), painter, width, height, USHORT_CLIP,
                     scaleSize(1));
            break;
        case TLONG:
            drawClip(reinterpret_cast<long const*>(input), imageData->channels(), painter, width, height, USHORT_CLIP,
                     scaleSize(1));
            break;
        case TFLOAT:
            drawClip(reinterpret_cast<float const*>(input), imageData->channels(), painter, width, height, FLOAT_CLIP,
                     scaleSize(1));
            break;
        case TLONGLONG:
            drawClip(reinterpret_cast<long long const*>(input), imageData->channels(), painter, width, height, USHORT_CLIP,
                     scaleSize(1));
            break;
        case TDOUBLE:
            drawClip(reinterpret_cast<double const*>(input), imageData->channels(), painter, width, height, FLOAT_CLIP,
                     scaleSize(1));
            break;
        default:
            break;
    }
}

Jasem Mutlaq's avatar
Jasem Mutlaq committed
908 909
void FITSView::ZoomDefault()
{
910
    if (image_frame != nullptr)
911
    {
912 913
        emit actionUpdated("view_zoom_out", true);
        emit actionUpdated("view_zoom_in", true);
Jasem Mutlaq's avatar
Jasem Mutlaq committed
914

915
        currentZoom   = ZOOM_DEFAULT;
916 917
        currentWidth  = imageData->width();
        currentHeight = imageData->height();
Jasem Mutlaq's avatar
Jasem Mutlaq committed
918

919
        updateFrame();
Jasem Mutlaq's avatar
Jasem Mutlaq committed
920

921
        emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM);
Jasem Mutlaq's avatar
Jasem Mutlaq committed
922

923 924
        update();
    }
Jasem Mutlaq's avatar
Jasem Mutlaq committed
925 926
}

Hy Murveit's avatar
Hy Murveit committed
927
void FITSView::drawOverlay(QPainter * painter, double scale)
Jasem Mutlaq's avatar
Jasem Mutlaq committed
928
{
929
    painter->setRenderHint(QPainter::Antialiasing, Options::useAntialias());
930

931
    if (trackingBoxEnabled && getCursorMode() != FITSView::scopeCursor)
Hy Murveit's avatar
Hy Murveit committed
932
        drawTrackingBox(painter, scale);
Jasem Mutlaq's avatar
Jasem Mutlaq committed
933

934
    if (!markerCrosshair.isNull())
Hy Murveit's avatar
Hy Murveit committed
935
        drawMarker(painter, scale);
936 937

    if (showCrosshair)
Hy Murveit's avatar
Hy Murveit committed
938
        drawCrosshair(painter, scale);
939

940
    if (showObjects)
Hy Murveit's avatar
Hy Murveit committed
941
        drawObjectNames(painter, scale);
942 943

    if (showEQGrid)
Hy Murveit's avatar
Hy Murveit committed
944
        drawEQGrid(painter, scale);
945

946
    if (showPixelGrid)
Hy Murveit's avatar
Hy Murveit committed
947
        drawPixelGrid(painter, scale);
948 949

    if (markStars)
Hy Murveit's avatar
Hy Murveit committed
950
        drawStarCentroid(painter, scale);
951 952 953

    if (showClipping)
        drawClipping(painter);
Jasem Mutlaq's avatar
Jasem Mutlaq committed
954 955
}

956 957 958 959 960
void FITSView::updateMode(FITSMode fmode)
{
    mode = fmode;
}

Hy Murveit's avatar
Hy Murveit committed
961
void FITSView::drawMarker(QPainter * painter, double scale)
962
{