diff --git a/kstars/fitsviewer/fitshistogram.cpp b/kstars/fitsviewer/fitshistogram.cpp index b1008a504..b25072023 100644 --- a/kstars/fitsviewer/fitshistogram.cpp +++ b/kstars/fitsviewer/fitshistogram.cpp @@ -1,860 +1,942 @@ /* FITS Histogram Copyright (C) 2015 Jasem Mutlaq (mutlaqja@ikarustech.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. */ #include "fitshistogram.h" #include "fits_debug.h" #include "fitsdata.h" #include "fitstab.h" #include "fitsview.h" #include "fitsviewer.h" #include "Options.h" #include #include histogramUI::histogramUI(QDialog *parent) : QDialog(parent) { setupUi(parent); setModal(false); } FITSHistogram::FITSHistogram(QWidget *parent) : QDialog(parent) { ui = new histogramUI(this); tab = dynamic_cast(parent); customPlot = ui->histogramPlot; customPlot->setBackground(QBrush(Qt::black)); customPlot->xAxis->setBasePen(QPen(Qt::white, 1)); customPlot->yAxis->setBasePen(QPen(Qt::white, 1)); customPlot->xAxis->setTickPen(QPen(Qt::white, 1)); customPlot->yAxis->setTickPen(QPen(Qt::white, 1)); customPlot->xAxis->setSubTickPen(QPen(Qt::white, 1)); customPlot->yAxis->setSubTickPen(QPen(Qt::white, 1)); customPlot->xAxis->setTickLabelColor(Qt::white); customPlot->yAxis->setTickLabelColor(Qt::white); customPlot->xAxis->setLabelColor(Qt::white); customPlot->yAxis->setLabelColor(Qt::white); customPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); customPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); customPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); customPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); customPlot->xAxis->grid()->setZeroLinePen(Qt::NoPen); customPlot->yAxis->grid()->setZeroLinePen(Qt::NoPen); - r_graph = customPlot->addGraph(); - r_graph->setBrush(QBrush(QColor(170, 40, 80))); - r_graph->setPen(QPen(Qt::red)); - //r_graph->setLineStyle(QCPGraph::lsImpulse); - - connect(ui->buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), this, SLOT(applyScale())); - - connect(customPlot, SIGNAL(mouseMove(QMouseEvent*)), this, SLOT(updateValues(QMouseEvent*))); + connect(ui->applyB, &QPushButton::clicked, this, &FITSHistogram::applyScale); connect(ui->minEdit, SIGNAL(valueChanged(double)), this, SLOT(updateLimits(double))); connect(ui->maxEdit, SIGNAL(valueChanged(double)), this, SLOT(updateLimits(double))); connect(ui->minSlider, SIGNAL(valueChanged(int)), this, SLOT(updateSliders(int))); connect(ui->maxSlider, SIGNAL(valueChanged(int)), this, SLOT(updateSliders(int))); + connect(ui->hideSaturated, &QCheckBox::stateChanged, this, &FITSHistogram::toggleHideSaturated); connect(customPlot->xAxis, SIGNAL(rangeChanged(QCPRange)), this, SLOT(checkRangeLimit(QCPRange))); + connect(customPlot, &QCustomPlot::mouseMove, this, &FITSHistogram::driftMouseOverLine); + sliderScale = 10; + numDecimals = 0; } void FITSHistogram::showEvent(QShowEvent *event) { Q_UNUSED(event) syncGUI(); } +void FITSHistogram::toggleHideSaturated(int x) +{ + constructHistogram(); + Q_UNUSED(x) +} + void FITSHistogram::constructHistogram() { FITSData *image_data = tab->getView()->getImageData(); isGUISynced = false; switch (image_data->property("dataType").toInt()) { case TBYTE: constructHistogram(); break; case TSHORT: constructHistogram(); break; case TUSHORT: constructHistogram(); break; case TLONG: constructHistogram(); break; case TULONG: constructHistogram(); break; case TFLOAT: constructHistogram(); break; case TLONGLONG: constructHistogram(); break; case TDOUBLE: constructHistogram(); break; default: break; } if (isVisible()) syncGUI(); } template void FITSHistogram::constructHistogram() { FITSData *image_data = tab->getView()->getImageData(); uint16_t fits_w = image_data->width(), fits_h = image_data->height(); auto *buffer = reinterpret_cast(image_data->getImageBuffer()); image_data->getMinMax(&fits_min, &fits_max); uint32_t samples = fits_w * fits_h; - binCount = sqrt(samples); + binCount = static_cast(sqrt(samples)); intensity.fill(0, binCount); r_frequency.fill(0, binCount); cumulativeFrequency.fill(0, binCount); double pixel_range = fits_max - fits_min; binWidth = pixel_range / (binCount - 1); qCDebug(KSTARS_FITS) << "Histogram min:" << fits_min << ", max:" << fits_max << ", range:" << pixel_range << ", binW:" << binWidth << ", bin#:" << binCount; for (int i = 0; i < binCount; i++) intensity[i] = fits_min + (binWidth * i); uint16_t r_id = 0; if (image_data->channels() == 1) { for (uint32_t i = 0; i < samples; i += 4) { - r_id = round((buffer[i] - fits_min) / binWidth); + r_id = static_cast(round((buffer[i] - fits_min) / binWidth)); r_frequency[r_id >= binCount ? binCount - 1 : r_id] += 4; } } else { g_frequency.fill(0, binCount); b_frequency.fill(0, binCount); - int g_offset = samples; - int b_offset = samples * 2; + int g_offset = static_cast(samples); + int b_offset = static_cast(samples * 2); for (uint32_t i = 0; i < samples; i += 4) { uint16_t g_id = 0, b_id = 0; - r_id = round((buffer[i] - fits_min) / binWidth); + r_id = static_cast(round((buffer[i] - fits_min) / binWidth)); r_frequency[r_id >= binCount ? binCount - 1 : r_id] += 4; - g_id = round((buffer[i + g_offset] - fits_min) / binWidth); + g_id = static_cast(round((buffer[i + g_offset] - fits_min) / binWidth)); g_frequency[g_id >= binCount ? binCount - 1 : g_id] += 4; - b_id = round((buffer[i + b_offset] - fits_min) / binWidth); + b_id = static_cast(round((buffer[i + b_offset] - fits_min) / binWidth)); b_frequency[b_id >= binCount ? binCount - 1 : b_id] += 4; } } // Cumulative Frequency int j = 0; double val = 0; for (int i = 0; i < binCount; i++) { val += r_frequency[j++]; cumulativeFrequency.replace(i, val); } if (image_data->channels() == 1) { for (int i = 0; i < binCount; i++) { if (r_frequency[i] > maxFrequency) - maxFrequency = r_frequency[i]; + maxFrequency = static_cast(r_frequency[i]); } } else { for (int i = 0; i < binCount; i++) { if (r_frequency[i] > maxFrequency) - maxFrequency = r_frequency[i]; + maxFrequency = static_cast(r_frequency[i]); if (g_frequency[i] > maxFrequency) - maxFrequency = g_frequency[i]; + maxFrequency = static_cast(g_frequency[i]); if (b_frequency[i] > maxFrequency) - maxFrequency = b_frequency[i]; + maxFrequency = static_cast(b_frequency[i]); } } double median = 0; - int halfCumulative = cumulativeFrequency[binCount - 1] / 2; + int halfCumulative = static_cast(cumulativeFrequency[binCount - 1] / 2); for (int i = 0; i < binCount; i++) { if (cumulativeFrequency[i] >= halfCumulative) { median = i * binWidth + fits_min; break; } } // Custom index to indicate the overall contrast of the image JMIndex = cumulativeFrequency[binCount / 8] / cumulativeFrequency[binCount / 4]; qCDebug(KSTARS_FITS) << "FITHistogram: JMIndex " << JMIndex; - image_data->setMedian(median); + if(ui->hideSaturated->isChecked()) + { + intensity.removeFirst(); + intensity.removeLast(); + r_frequency.removeFirst(); + r_frequency.removeLast(); + if (image_data->channels() > 1) + { + g_frequency.removeFirst(); + g_frequency.removeLast(); + b_frequency.removeFirst(); + b_frequency.removeLast(); + } + } + + image_data->setMedian(median); + if(median<1) + sliderScale=1/median*100; + else + sliderScale=10; } void FITSHistogram::syncGUI() { if (isGUISynced) return; FITSData *image_data = tab->getView()->getImageData(); disconnect(ui->minEdit, SIGNAL(valueChanged(double)), this, SLOT(updateLimits(double))); disconnect(ui->maxEdit, SIGNAL(valueChanged(double)), this, SLOT(updateLimits(double))); disconnect(ui->minSlider, SIGNAL(valueChanged(int)), this, SLOT(updateSliders(int))); disconnect(ui->maxSlider, SIGNAL(valueChanged(int)), this, SLOT(updateSliders(int))); ui->meanEdit->setText(QString::number(image_data->getMean())); ui->medianEdit->setText(QString::number(image_data->getMedian())); + double median = image_data->getMedian(); + + if(median > 100) + numDecimals=0; + else if(median > 1) + numDecimals=2; + else if(median > .01) + numDecimals=4; + else if(median > .0001) + numDecimals=6; + else + numDecimals=10; + if(!ui->minSlider->isSliderDown()) { - ui->minEdit->setMinimum(fits_min); - ui->minEdit->setMaximum(fits_max - 1); + ui->minEdit->setDecimals(numDecimals); ui->minEdit->setSingleStep(fabs(fits_max - fits_min) / 20.0); + ui->minEdit->setMinimum(fits_min); + ui->minEdit->setMaximum(fits_max - ui->minEdit->singleStep()); //minus one step ui->minEdit->setValue(fits_min); - ui->minSlider->setMinimum(fits_min*10); - ui->minSlider->setMaximum((fits_max - 1)*10); - ui->minSlider->setSingleStep((fabs(fits_max - fits_min) / 20.0)*10); - ui->minSlider->setValue(fits_min*10); + ui->minSlider->setSingleStep(static_cast((fabs(fits_max - fits_min) / 20.0)*sliderScale)); + ui->minSlider->setMinimum(static_cast(fits_min*sliderScale)); + ui->minSlider->setMaximum(static_cast((fits_max)*sliderScale - ui->minSlider->singleStep())); + ui->minSlider->setValue(static_cast(fits_min*sliderScale)); } if(!ui->maxSlider->isSliderDown()) { - ui->maxEdit->setMinimum(fits_min + 1); - ui->maxEdit->setMaximum(fits_max); + ui->maxEdit->setDecimals(numDecimals); ui->maxEdit->setSingleStep(fabs(fits_max - fits_min) / 20.0); + ui->maxEdit->setMinimum(fits_min + ui->maxEdit->singleStep()); + ui->maxEdit->setMaximum(fits_max); ui->maxEdit->setValue(fits_max); + ui->maxSlider->setSingleStep(static_cast((fabs(fits_max - fits_min) / 20.0)*sliderScale)); + ui->maxSlider->setMinimum(static_cast((fits_min)*sliderScale + ui->maxSlider->singleStep())); + ui->maxSlider->setMaximum(static_cast(fits_max*sliderScale)); + ui->maxSlider->setValue(static_cast(fits_max*sliderScale)); - ui->maxSlider->setMinimum((fits_min + 1)*10); - ui->maxSlider->setMaximum(fits_max*10); - ui->maxSlider->setSingleStep((fabs(fits_max - fits_min) / 20.0)*10); - ui->maxSlider->setValue(fits_max*10); } connect(ui->minEdit, SIGNAL(valueChanged(double)), this, SLOT(updateLimits(double))); connect(ui->maxEdit, SIGNAL(valueChanged(double)), this, SLOT(updateLimits(double))); connect(ui->minSlider, SIGNAL(valueChanged(int)), this, SLOT(updateSliders(int))); connect(ui->maxSlider, SIGNAL(valueChanged(int)), this, SLOT(updateSliders(int))); + customPlot->clearGraphs(); + r_graph = customPlot->addGraph(); + r_graph->setBrush(QBrush(QColor(170, 40, 80))); + r_graph->setPen(QPen(Qt::red)); r_graph->setData(intensity, r_frequency); + if (image_data->channels() > 1) { g_graph = customPlot->addGraph(); b_graph = customPlot->addGraph(); g_graph->setBrush(QBrush(QColor(40, 170, 80))); b_graph->setBrush(QBrush(QColor(80, 40, 170))); g_graph->setPen(QPen(Qt::green)); b_graph->setPen(QPen(Qt::blue)); g_graph->setData(intensity, g_frequency); b_graph->setData(intensity, b_frequency); } customPlot->axisRect(0)->setRangeDrag(Qt::Horizontal); customPlot->axisRect(0)->setRangeZoom(Qt::Horizontal); customPlot->xAxis->setLabel(i18n("Intensity")); customPlot->yAxis->setLabel(i18n("Frequency")); - customPlot->xAxis->setRange(fits_min, fits_max); + customPlot->xAxis->setRange(fits_min - ui->minEdit->singleStep(), fits_max + ui->maxEdit->singleStep()); if (maxFrequency > 0) customPlot->yAxis->rescale(); customPlot->setInteraction(QCP::iRangeDrag, true); customPlot->setInteraction(QCP::iRangeZoom, true); customPlot->setInteraction(QCP::iSelectPlottables, true); customPlot->replot(); resizePlot(); isGUISynced = true; } void FITSHistogram::resizePlot() { if(customPlot->width()<300) customPlot->yAxis->setTickLabels(false); else customPlot->yAxis->setTickLabels(true); customPlot->xAxis->ticker()->setTickCount(customPlot->width()/100); } #if 0 template void FITSHistogram::constructHistogram() { uint16_t fits_w = 0, fits_h = 0; FITSData *image_data = tab->getView()->getImageData(); T *buffer = reinterpret_cast(image_data->getImageBuffer()); image_data->getDimensions(&fits_w, &fits_h); image_data->getMinMax(&fits_min, &fits_max); uint32_t samples = fits_w * fits_h; binCount = sqrt(samples); intensity.fill(0, binCount); r_frequency.fill(0, binCount); cumulativeFrequency.fill(0, binCount); double pixel_range = fits_max - fits_min; binWidth = pixel_range / (binCount - 1); qCDebug(KSTARS_FITS) << "Histogram min:" << fits_min << ", max:" << fits_max << ", range:" << pixel_range << ", binW:" << binWidth << ", bin#:" << binCount; for (int i = 0; i < binCount; i++) intensity[i] = fits_min + (binWidth * i); uint16_t r_id = 0; if (image_data->getNumOfChannels() == 1) { for (uint32_t i = 0; i < samples; i += 4) { r_id = round((buffer[i] - fits_min) / binWidth); r_frequency[r_id >= binCount ? binCount - 1 : r_id] += 4; } } else { g_frequency.fill(0, binCount); b_frequency.fill(0, binCount); int g_offset = samples; int b_offset = samples * 2; for (uint32_t i = 0; i < samples; i += 4) { uint16_t g_id = 0, b_id = 0; r_id = round((buffer[i] - fits_min) / binWidth); r_frequency[r_id >= binCount ? binCount - 1 : r_id] += 4; g_id = round((buffer[i + g_offset] - fits_min) / binWidth); g_frequency[g_id >= binCount ? binCount - 1 : g_id] += 4; b_id = round((buffer[i + b_offset] - fits_min) / binWidth); b_frequency[b_id >= binCount ? binCount - 1 : b_id] += 4; } } // Cumulative Frequency for (int i = 0; i < binCount; i++) for (int j = 0; j <= i; j++) cumulativeFrequency[i] += r_frequency[j]; int maxFrequency = 0; if (image_data->getNumOfChannels() == 1) { for (int i = 0; i < binCount; i++) { if (r_frequency[i] > maxFrequency) maxFrequency = r_frequency[i]; } } else { for (int i = 0; i < binCount; i++) { if (r_frequency[i] > maxFrequency) maxFrequency = r_frequency[i]; if (g_frequency[i] > maxFrequency) maxFrequency = g_frequency[i]; if (b_frequency[i] > maxFrequency) maxFrequency = b_frequency[i]; } } double median = 0; int halfCumulative = cumulativeFrequency[binCount - 1] / 2; for (int i = 0; i < binCount; i++) { if (cumulativeFrequency[i] >= halfCumulative) { median = i * binWidth + fits_min; break; } } // Custom index to indicate the overall constrast of the image JMIndex = cumulativeFrequency[binCount / 8] / cumulativeFrequency[binCount / 4]; qCDebug(KSTARS_FITS) << "FITHistogram: JMIndex " << JMIndex; image_data->setMedian(median); ui->meanEdit->setText(QString::number(image_data->getMean())); ui->medianEdit->setText(QString::number(median)); ui->minEdit->setMinimum(fits_min); ui->minEdit->setMaximum(fits_max - 1); ui->minEdit->setSingleStep(fabs(fits_max - fits_min) / 20.0); ui->minEdit->setValue(fits_min); ui->maxEdit->setMinimum(fits_min + 1); ui->maxEdit->setMaximum(fits_max); ui->maxEdit->setSingleStep(fabs(fits_max - fits_min) / 20.0); ui->maxEdit->setValue(fits_max); r_graph->setData(intensity, r_frequency); if (image_data->getNumOfChannels() > 1) { g_graph = customPlot->addGraph(); b_graph = customPlot->addGraph(); g_graph->setBrush(QBrush(QColor(40, 170, 80))); b_graph->setBrush(QBrush(QColor(80, 40, 170))); g_graph->setPen(QPen(Qt::green)); b_graph->setPen(QPen(Qt::blue)); g_graph->setData(intensity, g_frequency); b_graph->setData(intensity, b_frequency); } customPlot->axisRect(0)->setRangeDrag(Qt::Horizontal); customPlot->axisRect(0)->setRangeZoom(Qt::Horizontal); customPlot->xAxis->setLabel(i18n("Intensity")); customPlot->yAxis->setLabel(i18n("Frequency")); customPlot->xAxis->setRange(fits_min, fits_max); if (maxFrequency > 0) customPlot->yAxis->setRange(0, maxFrequency); customPlot->setInteraction(QCP::iRangeDrag, true); customPlot->setInteraction(QCP::iRangeZoom, true); customPlot->setInteraction(QCP::iSelectPlottables, true); customPlot->replot(); } #endif void FITSHistogram::updateLimits(double value) { if (sender() == ui->minEdit) { if (value > ui->maxEdit->value()) ui->maxEdit->setValue(value + 1); } else if (sender() == ui->maxEdit) { if (value < ui->minEdit->value()) { ui->minEdit->setValue(value); ui->maxEdit->setValue(value + 1); } } } void FITSHistogram::updateSliders(int value) { if (sender() == ui->minSlider) { - ui->minEdit->setValue(value/10.0); - if (value/10.0 > ui->maxEdit->value()) - ui->maxEdit->setValue(value/10.0 + 1); + if(value/sliderScale > ui->minEdit->value()) + { + ui->minEdit->setValue(value/sliderScale); + if (value/sliderScale > ui->maxEdit->value()) + ui->maxEdit->setValue(value/sliderScale + ui->maxEdit->singleStep()); + } } else if (sender() == ui->maxSlider) { - ui->maxEdit->setValue(value/10.0); - if (value/10.0 < ui->minEdit->value()) + if(value/sliderScale < ui->maxEdit->value()) { - ui->minEdit->setValue(value/10.0); - ui->maxEdit->setValue(value/10.0 + 1); + ui->maxEdit->setValue(value/sliderScale); + if (value/sliderScale < ui->minEdit->value()) + { + ui->minEdit->setValue(value/sliderScale); + ui->maxEdit->setValue(value/sliderScale + ui->maxEdit->singleStep()); + } } } applyScale(); } void FITSHistogram::checkRangeLimit(const QCPRange &range) { - if (range.lower < fits_min) - customPlot->xAxis->setRangeLower(fits_min); - else if (range.upper > fits_max) - customPlot->xAxis->setRangeUpper(fits_max); + if (range.lower < fits_min - ui->minEdit->singleStep()) + customPlot->xAxis->setRangeLower(fits_min - ui->minEdit->singleStep()); + else if (range.upper > fits_max + ui->maxEdit->singleStep()) + customPlot->xAxis->setRangeUpper(fits_max + ui->maxEdit->singleStep()); } double FITSHistogram::getJMIndex() const { return JMIndex; } void FITSHistogram::applyScale() { double min = ui->minEdit->value(); double max = ui->maxEdit->value(); FITSHistogramCommand *histC; if (ui->logR->isChecked()) type = FITS_LOG; else type = FITS_LINEAR; histC = new FITSHistogramCommand(tab, this, type, min, max); tab->getUndoStack()->push(histC); } void FITSHistogram::applyFilter(FITSScale ftype) { double min = ui->minEdit->value(); double max = ui->maxEdit->value(); FITSHistogramCommand *histC; type = ftype; histC = new FITSHistogramCommand(tab, this, type, min, max); tab->getUndoStack()->push(histC); } QVector FITSHistogram::getCumulativeFrequency() const { return cumulativeFrequency; } -void FITSHistogram::updateValues(QMouseEvent *event) -{ - int x = event->x(); - - double intensity_key = customPlot->xAxis->pixelToCoord(x); - - if (intensity_key < 0) - return; - - double frequency_val = 0; - - for (int i = 0; i < binCount; i++) - { - if (intensity[i] > intensity_key) - { - frequency_val = r_frequency[i]; - break; - } - } - - ui->intensityEdit->setText(QString::number(intensity_key)); - ui->frequencyEdit->setText(QString::number(frequency_val)); -} - FITSHistogramCommand::FITSHistogramCommand(QWidget *parent, FITSHistogram *inHisto, FITSScale newType, double lmin, double lmax) { tab = dynamic_cast(parent); type = newType; histogram = inHisto; min = lmin; max = lmax; } FITSHistogramCommand::~FITSHistogramCommand() { delete[] delta; } bool FITSHistogramCommand::calculateDelta(const uint8_t *buffer) { FITSData *image_data = tab->getView()->getImageData(); uint8_t *image_buffer = image_data->getImageBuffer(); int totalPixels = image_data->width() * image_data->height() * image_data->channels(); unsigned long totalBytes = totalPixels * image_data->getBytesPerPixel(); auto *raw_delta = new uint8_t[totalBytes]; if (raw_delta == nullptr) { qWarning() << "Error! not enough memory to create image delta" << endl; return false; } for (unsigned int i = 0; i < totalBytes; i++) raw_delta[i] = buffer[i] ^ image_buffer[i]; compressedBytes = sizeof(uint8_t) * totalBytes + totalBytes / 64 + 16 + 3; delete[] delta; delta = new uint8_t[compressedBytes]; if (delta == nullptr) { delete[] raw_delta; qCCritical(KSTARS_FITS) << "FITSHistogram Error: Ran out of memory compressing delta"; return false; } int r = compress2(delta, &compressedBytes, raw_delta, totalBytes, 5); if (r != Z_OK) { delete[] raw_delta; /* this should NEVER happen */ qCCritical(KSTARS_FITS) << "FITSHistogram Error: Failed to compress raw_delta"; return false; } //qDebug() << "compressed bytes size " << compressedBytes << " bytes" << endl; delete[] raw_delta; return true; } bool FITSHistogramCommand::reverseDelta() { FITSView *image = tab->getView(); FITSData *image_data = image->getImageData(); uint8_t *image_buffer = (image_data->getImageBuffer()); int totalPixels = image_data->width() * image_data->height() * image_data->channels(); unsigned long totalBytes = totalPixels * image_data->getBytesPerPixel(); auto *output_image = new uint8_t[totalBytes]; if (output_image == nullptr) { qWarning() << "Error! not enough memory to create output image" << endl; return false; } auto *raw_delta = new uint8_t[totalBytes]; if (raw_delta == nullptr) { delete[] output_image; qWarning() << "Error! not enough memory to create image delta" << endl; return false; } int r = uncompress(raw_delta, &totalBytes, delta, compressedBytes); if (r != Z_OK) { qCCritical(KSTARS_FITS) << "FITSHistogram compression error in reverseDelta()"; delete[] output_image; delete[] raw_delta; return false; } for (unsigned int i = 0; i < totalBytes; i++) output_image[i] = raw_delta[i] ^ image_buffer[i]; image_data->setImageBuffer(output_image); delete[] raw_delta; return true; } void FITSHistogramCommand::redo() { FITSView *image = tab->getView(); FITSData *image_data = image->getImageData(); uint8_t *image_buffer = image_data->getImageBuffer(); uint8_t *buffer = nullptr; unsigned int size = image_data->width() * image_data->height() * image_data->channels(); int BBP = image_data->getBytesPerPixel(); QApplication::setOverrideCursor(Qt::WaitCursor); if (delta != nullptr) { FITSData::Statistic prevStats; image_data->saveStatistics(prevStats); reverseDelta(); image_data->restoreStatistics(stats); stats = prevStats; } else { image_data->saveStatistics(stats); // If it's rotation of flip, no need to calculate delta if (type >= FITS_ROTATE_CW && type <= FITS_FLIP_V) { image_data->applyFilter(type, image_buffer); } else { buffer = new uint8_t[size * BBP]; if (buffer == nullptr) { qWarning() << "Error! not enough memory to create image buffer in redo()" << endl; QApplication::restoreOverrideCursor(); return; } memcpy(buffer, image_buffer, size * BBP); double dataMin = min, dataMax = max; switch (type) { case FITS_AUTO: case FITS_LINEAR: image_data->applyFilter(FITS_LINEAR, nullptr, &dataMin, &dataMax); break; case FITS_LOG: image_data->applyFilter(FITS_LOG, nullptr, &dataMin, &dataMax); break; case FITS_SQRT: image_data->applyFilter(FITS_SQRT, nullptr, &dataMin, &dataMax); break; default: image_data->applyFilter(type); break; } calculateDelta(buffer); delete[] buffer; } } if (histogram != nullptr) { histogram->constructHistogram(); if (tab->getViewer()->isStarsMarked()) image_data->findStars(); } image->pushFilter(type); image->rescale(ZOOM_KEEP_LEVEL); image->updateFrame(); QApplication::restoreOverrideCursor(); } void FITSHistogramCommand::undo() { FITSView *image = tab->getView(); FITSData *image_data = image->getImageData(); QApplication::setOverrideCursor(Qt::WaitCursor); if (delta != nullptr) { FITSData::Statistic prevStats; image_data->saveStatistics(prevStats); reverseDelta(); image_data->restoreStatistics(stats); stats = prevStats; } else { switch (type) { case FITS_ROTATE_CW: image_data->applyFilter(FITS_ROTATE_CCW); break; case FITS_ROTATE_CCW: image_data->applyFilter(FITS_ROTATE_CW); break; case FITS_FLIP_H: case FITS_FLIP_V: image_data->applyFilter(type); break; default: break; } } if (histogram != nullptr) { histogram->constructHistogram(); if (tab->getViewer()->isStarsMarked()) image_data->findStars(); } image->popFilter(); image->rescale(ZOOM_KEEP_LEVEL); image->updateFrame(); QApplication::restoreOverrideCursor(); } QString FITSHistogramCommand::text() const { switch (type) { case FITS_AUTO: return i18n("Auto Scale"); - break; case FITS_LINEAR: return i18n("Linear Scale"); - break; case FITS_LOG: return i18n("Logarithmic Scale"); - break; case FITS_SQRT: return i18n("Square Root Scale"); - break; default: if (type - 1 <= FITSViewer::filterTypes.count()) return FITSViewer::filterTypes.at(type - 1); break; } return i18n("Unknown"); } + +void FITSHistogram::driftMouseOverLine(QMouseEvent *event) +{ + double intensity = customPlot->xAxis->pixelToCoord(event->localPos().x()); + + if (customPlot->xAxis->range().contains(intensity)) + { + int r_index= r_graph->findBegin(intensity, true); + double r_Frequency = r_graph->dataMainValue(r_index); + + + if(b_graph && g_graph) + { + int g_index= g_graph->findBegin(intensity, true); + double g_Frequency = g_graph->dataMainValue(g_index); + + int b_index= b_graph->findBegin(intensity, true); + double b_Frequency = g_graph->dataMainValue(b_index); + + if( r_Frequency>0.0 || g_Frequency>0.0 || b_Frequency>0.0 ) + { + QToolTip::showText( + event->globalPos(), + i18nc("Histogram tooltip; %1 is intensity; %2 is frequency;", + "" + "" + "" + "" + "" + "
Intensity: %1
R Frequency: %2
G Frequency: %3
B Frequency: %4
", + QString::number(intensity, 'f', numDecimals), + QString::number(r_Frequency, 'f', 0), + QString::number(g_Frequency, 'f', 0), + QString::number(b_Frequency, 'f', 0))); + } + else + QToolTip::hideText(); + + } + else + { + if(r_Frequency>0.0) + { + QToolTip::showText( + event->globalPos(), + i18nc("Histogram tooltip; %1 is intensity; %2 is frequency;", + "" + "" + "" + "
Intensity: %1
R Frequency: %2
", + QString::number(intensity, 'f', numDecimals), + QString::number(r_Frequency, 'f', 0))); + } + else + QToolTip::hideText(); + } + + + + customPlot->replot(); + } +} diff --git a/kstars/fitsviewer/fitshistogram.h b/kstars/fitsviewer/fitshistogram.h index 21ca3b768..7bc4523d9 100644 --- a/kstars/fitsviewer/fitshistogram.h +++ b/kstars/fitsviewer/fitshistogram.h @@ -1,112 +1,118 @@ /* FITS Histogram Copyright (C) 2015 Jasem Mutlaq (mutlaqja@ikarustech.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. */ #pragma once #include "fitscommon.h" #include "fitsdata.h" #include "ui_fitshistogramui.h" #include #include class QMouseEvent; class FITSTab; class histogramUI : public QDialog, public Ui::FITSHistogramUI { Q_OBJECT public: explicit histogramUI(QDialog *parent = nullptr); }; class FITSHistogram : public QDialog { Q_OBJECT friend class histDrawArea; public: explicit FITSHistogram(QWidget *parent); ~FITSHistogram() = default; void constructHistogram(); void syncGUI(); void applyFilter(FITSScale ftype); double getBinWidth() { return binWidth; } QVector getCumulativeFrequency() const; double getJMIndex() const; protected: void showEvent(QShowEvent *event); + void driftMouseOverLine(QMouseEvent *event); public slots: void applyScale(); - void updateValues(QMouseEvent *event); void updateLimits(double value); void updateSliders(int value); void checkRangeLimit(const QCPRange &range); void resizePlot(); + void toggleHideSaturated(int x); private: template void constructHistogram(); + double sliderScale; + int numDecimals; + double cutMin; + double cutMax; + histogramUI *ui { nullptr }; FITSTab *tab { nullptr }; QVector intensity; QVector r_frequency, g_frequency, b_frequency; QCPGraph *r_graph { nullptr }; QCPGraph *g_graph { nullptr }; QCPGraph *b_graph { nullptr }; QVector cumulativeFrequency; double binWidth { 0 }; double JMIndex { 0 }; double fits_min { 0 }; double fits_max { 0 }; uint16_t binCount { 0 }; int maxFrequency {0}; FITSScale type { FITS_AUTO }; bool isGUISynced { false}; QCustomPlot *customPlot { nullptr }; }; class FITSHistogramCommand : public QUndoCommand { public: FITSHistogramCommand(QWidget *parent, FITSHistogram *inHisto, FITSScale newType, double lmin, double lmax); virtual ~FITSHistogramCommand(); virtual void redo(); virtual void undo(); virtual QString text() const; private: bool calculateDelta(const uint8_t *buffer); bool reverseDelta(); FITSData::Statistic stats; FITSHistogram *histogram { nullptr }; FITSScale type; double min { 0 }; double max { 0 }; unsigned char *delta { nullptr }; unsigned long compressedBytes { 0 }; FITSTab *tab { nullptr }; }; diff --git a/kstars/fitsviewer/fitshistogramui.ui b/kstars/fitsviewer/fitshistogramui.ui index bd54ab424..c91fe0541 100644 --- a/kstars/fitsviewer/fitshistogramui.ui +++ b/kstars/fitsviewer/fitshistogramui.ui @@ -1,251 +1,244 @@ FITSHistogramUI 0 0 212 - 528 + 437 0 0 0 0 16777215 16777215 Histogram true + + 0 + 4 4 4 4 0 0 0 200 16777215 16777215 - - - - - - - Mean: - - - - - - - Intensity: - - - - - - - - - - Min.: - - - - - - - Frequency: - - - - - - - - 0 - 0 - - - - true - - - - - - - - 0 - 0 - - - - true - - - + + 0 + Max: - - + + 0 0 true - + Median: - - + + + + + + + + 0 0 true + + + + Mean: + + + Qt::Horizontal Qt::Horizontal + + + + Min.: + + + 0 0 + + + 12 + + Qt::LeftToRight false FITS Scale + + 0 + + + 2 + + + 2 + + + 2 + + + 2 + L&inear true &Log - - - QDialogButtonBox::Apply|QDialogButtonBox::Close + + + Apply + + + + + + + Hide Saturation Spike QCustomPlot QWidget
auxiliary/qcustomplot.h
1
diff --git a/kstars/fitsviewer/fitstab.cpp b/kstars/fitsviewer/fitstab.cpp index f1a6d23c3..6bfeafc48 100644 --- a/kstars/fitsviewer/fitstab.cpp +++ b/kstars/fitsviewer/fitstab.cpp @@ -1,375 +1,412 @@ /*************************************************************************** FITS Tab ------------------- copyright : (C) 2012 by Jasem Mutlaq email : mutlaqja@ikarustech.com ***************************************************************************/ /*************************************************************************** * * * This program 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. * * * ***************************************************************************/ #include "fitstab.h" #include "fitsdata.h" #include "fitshistogram.h" #include "fitsview.h" #include "fitsviewer.h" #include "kstars.h" #include "Options.h" #include "ui_fitsheaderdialog.h" #include "ui_statform.h" +#include "ekos/manager.h" #include #include FITSTab::FITSTab(FITSViewer *parent) : QWidget(parent) { viewer = parent; undoStack = new QUndoStack(this); undoStack->setUndoLimit(10); undoStack->clear(); connect(undoStack, SIGNAL(cleanChanged(bool)), this, SLOT(modifyFITSState(bool))); statWidget = new QDialog(this); fitsHeaderDialog = new QDialog(this); histogram = new FITSHistogram(this); } FITSTab::~FITSTab() { // Make sure it's done histogramFuture.waitForFinished(); //disconnect(); } void FITSTab::saveUnsaved() { if (undoStack->isClean() || view->getMode() != FITS_NORMAL) return; QString caption = i18n("Save Changes to FITS?"); QString message = i18n("The current FITS file has unsaved changes. Would you like to save before closing it?"); int ans = KMessageBox::warningYesNoCancel(nullptr, message, caption, KStandardGuiItem::save(), KStandardGuiItem::discard()); if (ans == KMessageBox::Yes) saveFile(); if (ans == KMessageBox::No) { undoStack->clear(); modifyFITSState(); } } void FITSTab::closeEvent(QCloseEvent *ev) { saveUnsaved(); if (undoStack->isClean()) ev->accept(); else ev->ignore(); } QString FITSTab::getPreviewText() const { return previewText; } void FITSTab::setPreviewText(const QString &value) { previewText = value; } +void FITSTab::selectRecentFITS(int i) +{ + loadFITS(QUrl::fromLocalFile(recentImages->item(i)->text())); +} + +void FITSTab::clearRecentFITS() +{ + disconnect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS); + recentImages->clear(); + connect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS); +} + void FITSTab::loadFITS(const QUrl &imageURL, FITSMode mode, FITSScale filter, bool silent) { if (view.get() == nullptr) { view.reset(new FITSView(this, mode, filter)); view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); QVBoxLayout *vlayout = new QVBoxLayout(); fitsSplitter = new QSplitter(Qt::Horizontal,this); fitsTools = new QToolBox(); stat.setupUi(statWidget); fitsTools->addItem(statWidget,i18n("Statistics")); fitsTools->addItem(histogram,i18n("Histogram")); header.setupUi(fitsHeaderDialog); fitsTools->addItem(fitsHeaderDialog,i18n("FITS Header")); + + + QVBoxLayout *recentPanelLayout = new QVBoxLayout(); + QWidget *recentPanel = new QWidget(fitsSplitter); + recentPanel->setLayout(recentPanelLayout); + fitsTools->addItem(recentPanel,i18n("Recent Images")); + recentImages = new QListWidget(recentPanel); + recentPanelLayout->addWidget(recentImages); + QPushButton *clearRecent = new QPushButton(i18n("Clear")); + recentPanelLayout->addWidget(clearRecent); + connect(clearRecent, &QPushButton::pressed, this, &FITSTab::clearRecentFITS); + connect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS); + QScrollArea *scrollFitsPanel = new QScrollArea(fitsSplitter); scrollFitsPanel->setWidgetResizable(true); scrollFitsPanel->setWidget(fitsTools); fitsSplitter->addWidget(scrollFitsPanel); fitsSplitter->addWidget(view.get()); //This code allows the fitsTools to start in a closed state fitsSplitter->setSizes(QList() << 0 << view->width() ); vlayout->addWidget(fitsSplitter); connect(fitsSplitter, &QSplitter::splitterMoved, histogram, &FITSHistogram::resizePlot); setLayout(vlayout); connect(view.get(), &FITSView::newStatus, this, &FITSTab::newStatus); connect(view.get(), &FITSView::debayerToggled, this, &FITSTab::debayerToggled); // On Failure to load connect(view.get(), &FITSView::failed, this, &FITSTab::failed); // On Success loading image connect(view.get(), &FITSView::loaded, [&,filter]() { // If it was already running make sure it's done histogramFuture.waitForFinished(); FITSData *image_data = view->getImageData(); if (histogram == nullptr) { histogram = new FITSHistogram(this); image_data->setHistogram(histogram); } - histogramFuture = QtConcurrent::run([&]() {histogram->constructHistogram();}); + histogramFuture = QtConcurrent::run([&]() {histogram->constructHistogram(); evaluateStats();}); if(histogram->isVisible()) histogramFuture.waitForFinished(); // if (filter != FITS_NONE) // { // image_data->applyFilter(filter); // view->rescale(ZOOM_KEEP_LEVEL); // } if (viewer->isStarsMarked()) view->toggleStars(true); - evaluateStats(); + loadFITSHeader(); + if(recentImages->findItems(currentURL.toLocalFile(),Qt::MatchExactly).count()==0) //Don't add it to the list if it is already there + { + if(!image_data->isTempFile()) //Don't add it to the list if it is a preview + { + disconnect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS); + recentImages->addItem(currentURL.toLocalFile()); + recentImages->setCurrentRow(recentImages->count()-1); + connect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS); + } + } + view->updateFrame(); emit loaded(); }); } currentURL = imageURL; view->setFilter(filter); view->loadFITS(imageURL.toLocalFile(), silent); } void FITSTab::modifyFITSState(bool clean) { if (clean) { if (undoStack->isClean() == false) undoStack->setClean(); mDirty = false; } else mDirty = true; emit changeStatus(clean); } int FITSTab::saveFITS(const QString &filename) { return view->saveFITS(filename); } void FITSTab::copyFITS() { QApplication::clipboard()->setImage(view->getDisplayImage()); } void FITSTab::histoFITS() { fitsTools->setCurrentIndex(1); if(view->width()>200) fitsSplitter->setSizes(QList() << 200 << view->width() - 200); else fitsSplitter->setSizes(QList() << 50 << 50); } void FITSTab::evaluateStats() { FITSData *image_data = view->getImageData(); stat.widthOUT->setText(QString::number(image_data->width())); stat.heightOUT->setText(QString::number(image_data->height())); stat.bitpixOUT->setText(QString::number(image_data->bpp())); stat.maxOUT->setText(QString::number(image_data->getMax(), 'f', 3)); stat.minOUT->setText(QString::number(image_data->getMin(), 'f', 3)); stat.meanOUT->setText(QString::number(image_data->getMean(), 'f', 3)); stat.stddevOUT->setText(QString::number(image_data->getStdDev(), 'f', 3)); stat.HFROUT->setText(QString::number(image_data->getHFR(), 'f', 3)); stat.medianOUT->setText(QString::number(image_data->getMedian(), 'f', 3)); stat.SNROUT->setText(QString::number(image_data->getSNR(), 'f', 3)); } void FITSTab::statFITS() { fitsTools->setCurrentIndex(0); if(view->width()>200) fitsSplitter->setSizes(QList() << 200 << view->width() - 200); else fitsSplitter->setSizes(QList() << 50 << 50); } void FITSTab::loadFITSHeader() { FITSData *image_data = view->getImageData(); int nkeys = image_data->getRecords().size(); int counter=0; header.tableWidget->setRowCount(nkeys); for (FITSData::Record *oneRecord : image_data->getRecords()) { QTableWidgetItem *tempItem = new QTableWidgetItem(oneRecord->key); tempItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); header.tableWidget->setItem(counter, 0, tempItem); tempItem = new QTableWidgetItem(oneRecord->value.toString()); tempItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); header.tableWidget->setItem(counter, 1, tempItem); tempItem = new QTableWidgetItem(oneRecord->comment); tempItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); header.tableWidget->setItem(counter, 2, tempItem); counter++; } header.tableWidget->setColumnWidth(0,100); header.tableWidget->setColumnWidth(1,100); header.tableWidget->setColumnWidth(2,250); } void FITSTab::headerFITS() { fitsTools->setCurrentIndex(2); if(view->width()>200) fitsSplitter->setSizes(QList() << 200 << view->width() - 200); else fitsSplitter->setSizes(QList() << 50 << 50); } bool FITSTab::saveFile() { QUrl backupCurrent = currentURL; QUrl currentDir(Options::fitsDir()); currentDir.setScheme("file"); if (currentURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || currentURL.toLocalFile().contains("/Temp")) currentURL.clear(); // If no changes made, return. if (mDirty == false && !currentURL.isEmpty()) return false; if (currentURL.isEmpty()) { currentURL = QFileDialog::getSaveFileUrl(KStars::Instance(), i18n("Save FITS"), currentDir, "FITS (*.fits *.fits.gz *.fit)"); // if user presses cancel if (currentURL.isEmpty()) { currentURL = backupCurrent; return false; } if (currentURL.toLocalFile().contains('.') == 0) currentURL.setPath(currentURL.toLocalFile() + ".fits"); // Already display by dialog /*if (QFile::exists(currentURL.toLocalFile())) { int r = KMessageBox::warningContinueCancel(0, i18n( "A file named \"%1\" already exists. " "Overwrite it?", currentURL.fileName() ), i18n( "Overwrite File?" ), KGuiItem(i18n( "&Overwrite" )) ); if(r==KMessageBox::Cancel) return false; }*/ } if (currentURL.isValid()) { int err_status = 0; char err_text[FLEN_STATUS]; if ((err_status = saveFITS('!' + currentURL.toLocalFile())) != 0) { // -1000 = user canceled if (err_status == -1000) return false; fits_get_errstatus(err_status, err_text); // Use KMessageBox or something here KMessageBox::error(nullptr, i18n("FITS file save error: %1", QString::fromUtf8(err_text)), i18n("FITS Save")); return false; } //statusBar()->changeItem(i18n("File saved."), 3); emit newStatus(i18n("File saved to %1", currentURL.url()), FITS_MESSAGE); modifyFITSState(); return true; } else { QString message = i18n("Invalid URL: %1", currentURL.url()); KMessageBox::sorry(nullptr, message, i18n("Invalid URL")); return false; } } bool FITSTab::saveFileAs() { currentURL.clear(); return saveFile(); } void FITSTab::ZoomIn() { QPoint oldCenter = view->getImagePoint(view->viewport()->rect().center()); view->ZoomIn(); view->cleanUpZoom(oldCenter); } void FITSTab::ZoomOut() { QPoint oldCenter = view->getImagePoint(view->viewport()->rect().center()); view->ZoomOut(); view->cleanUpZoom(oldCenter); } void FITSTab::ZoomDefault() { QPoint oldCenter = view->getImagePoint(view->viewport()->rect().center()); view->ZoomDefault(); view->cleanUpZoom(oldCenter); } void FITSTab::tabPositionUpdated() { undoStack->setActive(true); emit newStatus(QString("%1%").arg(view->getCurrentZoom()), FITS_ZOOM); emit newStatus(QString("%1x%2").arg(view->getImageData()->width()).arg(view->getImageData()->height()), FITS_RESOLUTION); } diff --git a/kstars/fitsviewer/fitstab.h b/kstars/fitsviewer/fitstab.h index 7a332ef73..d3fc6a07d 100644 --- a/kstars/fitsviewer/fitstab.h +++ b/kstars/fitsviewer/fitstab.h @@ -1,125 +1,130 @@ /*************************************************************************** FITS Tab ------------------- copyright : (C) 2012 by Jasem Mutlaq email : mutlaqja@ikarustech.com ***************************************************************************/ /*************************************************************************** * * * This program 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. * * * ***************************************************************************/ #pragma once #include "fitscommon.h" #include #include #include #include #include #include "ui_fitsheaderdialog.h" #include "ui_statform.h" #include #include +#include #include class FITSHistogram; class FITSView; class FITSViewer; /** * @brief The FITSTab class holds information on the current view (drawing area) in addition to the undo/redo stacks * and status of current document (clean or dirty). It also creates the corresponding histogram associated with the * image data that is stored in the FITSView class. * @author Jasem Mutlaq */ class FITSTab : public QWidget { Q_OBJECT public: explicit FITSTab(FITSViewer *parent); virtual ~FITSTab(); + void clearRecentFITS(); + void selectRecentFITS(int i); void loadFITS(const QUrl &imageURL, FITSMode mode = FITS_NORMAL, FITSScale filter = FITS_NONE, bool silent = true); int saveFITS(const QString &filename); inline QUndoStack *getUndoStack() { return undoStack; } inline QUrl *getCurrentURL() { return ¤tURL; } inline FITSView *getView() { return view.get(); } inline QPointer getHistogram() { return histogram; } inline QPointer getViewer() { return viewer; } bool saveFile(); bool saveFileAs(); void copyFITS(); void loadFITSHeader(); void headerFITS(); void histoFITS(); void evaluateStats(); void statFITS(); void setUID(int newID) { uid = newID; } int getUID() { return uid; } void saveUnsaved(); void tabPositionUpdated(); void selectGuideStar(); QString getPreviewText() const; void setPreviewText(const QString &value); public slots: void modifyFITSState(bool clean = true); void ZoomIn(); void ZoomOut(); void ZoomDefault(); protected: virtual void closeEvent(QCloseEvent *ev); private: /** Ask user whether he wants to save changes and save if he do. */ /// The FITSTools Toolbox QPointer fitsTools; /// The Splitter for th FITSTools Toolbox QPointer fitsSplitter; /// The FITS Header Panel QPointer fitsHeaderDialog; Ui::fitsHeaderDialog header; /// The Statistics Panel QPointer statWidget; Ui::statForm stat; /// FITS Histogram QPointer histogram; QPointer viewer; + QPointer recentImages; + /// FITS image object std::unique_ptr view; /// History for undo/redo QUndoStack *undoStack { nullptr }; /// FITS File name and path QUrl currentURL; bool mDirty { false }; QString previewText; int uid { 0 }; QFuture histogramFuture; signals: void debayerToggled(bool); void newStatus(const QString &msg, FITSBar id); void changeStatus(bool clean); void loaded(); void failed(); }; diff --git a/kstars/fitsviewer/fitsview.cpp b/kstars/fitsviewer/fitsview.cpp index f47176f48..0e7310cce 100644 --- a/kstars/fitsviewer/fitsview.cpp +++ b/kstars/fitsviewer/fitsview.cpp @@ -1,1870 +1,1863 @@ /* FITS View Copyright (C) 2003-2017 Jasem Mutlaq Copyright (C) 2016-2017 Robert Lancaster 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. */ #include "config-kstars.h" #include "fitsview.h" #include "fitsdata.h" #include "fitslabel.h" #include "kspopupmenu.h" #include "kstarsdata.h" #include "ksutils.h" #include "Options.h" #include "skymap.h" #include "fits_debug.h" #ifdef HAVE_INDI #include "basedevice.h" #include "indi/indilistener.h" #endif #include #include #include #include #include #include #include #define BASE_OFFSET 50 #define ZOOM_DEFAULT 100.0 #define ZOOM_MIN 10 #define ZOOM_MAX 400 #define ZOOM_LOW_INCR 10 #define ZOOM_HIGH_INCR 50 FITSView::FITSView(QWidget *parent, FITSMode fitsMode, FITSScale filterType) : QScrollArea(parent), zoomFactor(1.2) { grabGesture(Qt::PinchGesture); image_frame.reset(new FITSLabel(this)); filter = filterType; mode = fitsMode; setBackgroundRole(QPalette::Dark); markerCrosshair.setX(0); markerCrosshair.setY(0); setBaseSize(740, 530); 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))); connect(&wcsWatcher, SIGNAL(finished()), this, SLOT(syncWCSState())); connect(&fitsWatcher, &QFutureWatcher::finished, this, &FITSView::loadInFrame); image_frame->setMouseTracking(true); setCursorMode( selectCursor); //This is the default mode because the Focus and Align FitsViews should not be in dragMouse mode noImageLabel = new QLabel(); noImage.load(":/images/noimage.png"); noImageLabel->setPixmap(noImage); noImageLabel->setAlignment(Qt::AlignCenter); this->setWidget(noImageLabel); redScopePixmap = QPixmap(":/icons/center_telescope_red.svg").scaled(32, 32, Qt::KeepAspectRatio, Qt::FastTransformation); magentaScopePixmap = QPixmap(":/icons/center_telescope_magenta.svg").scaled(32, 32, Qt::KeepAspectRatio, Qt::FastTransformation); //if (fitsMode == FITS_GUIDE) //connect(image_frame.get(), SIGNAL(pointSelected(int,int)), this, SLOT(processPointSelection(int,int))); // Default size //resize(INITIAL_W, INITIAL_H); } FITSView::~FITSView() { fitsWatcher.waitForFinished(); wcsWatcher.waitForFinished(); - #ifdef HAVE_DATAVISUALIZATION - starProfileWidget->deleteLater(); - #endif delete (imageData); } /** This method looks at what mouse mode is currently selected and updates the cursor to match. */ void FITSView::updateMouseCursor() { if (cursorMode == dragCursor) { if (horizontalScrollBar()->maximum() > 0 || verticalScrollBar()->maximum() > 0) { if (!image_frame->getMouseButtonDown()) viewport()->setCursor(Qt::PointingHandCursor); else viewport()->setCursor(Qt::ClosedHandCursor); } else viewport()->setCursor(Qt::CrossCursor); } else if (cursorMode == selectCursor) { viewport()->setCursor(Qt::CrossCursor); } else if (cursorMode == scopeCursor) { viewport()->setCursor(QCursor(redScopePixmap, 10, 10)); } else if (cursorMode == crosshairCursor) { viewport()->setCursor(QCursor(magentaScopePixmap, 10, 10)); } } /** 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. */ void FITSView::setCursorMode(CursorMode mode) { cursorMode = mode; updateMouseCursor(); if (mode == scopeCursor && imageHasWCS()) { if (!imageData->isWCSLoaded() && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); } } } void FITSView::resizeEvent(QResizeEvent *event) { if ((imageData == nullptr) && noImageLabel != nullptr) { noImageLabel->setPixmap( noImage.scaled(width() - 20, height() - 20, Qt::KeepAspectRatio, Qt::FastTransformation)); noImageLabel->setFixedSize(width() - 5, height() - 5); } QScrollArea::resizeEvent(event); } #if 0 bool FITSView::loadFITS(const QString &inFilename, bool silent) { if (floatingToolBar != nullptr) { floatingToolBar->setVisible(true); } QProgressDialog fitsProg(this); bool setBayerParams = false; BayerParams param; if ((imageData != nullptr) && imageData->hasDebayer()) { setBayerParams = true; imageData->getBayerParams(¶m); } // In case loadWCS is still running for previous image data, let's wait until it's over wcsWatcher.waitForFinished(); delete imageData; imageData = nullptr; filterStack.clear(); filterStack.push(FITS_NONE); if (filter != FITS_NONE) filterStack.push(filter); imageData = new FITSData(mode); if (setBayerParams) imageData->setBayerParams(¶m); if (mode == FITS_NORMAL) { fitsProg.setWindowModality(Qt::WindowModal); fitsProg.setLabelText(i18n("Please hold while loading FITS file...")); fitsProg.setWindowTitle(i18n("Loading FITS")); fitsProg.setValue(10); qApp->processEvents(); } if (!imageData->loadFITS(inFilename, silent)) return false; if (mode == FITS_NORMAL) { if (fitsProg.wasCanceled()) return false; else { fitsProg.setValue(65); qApp->processEvents(); } } emit debayerToggled(imageData->hasDebayer()); currentWidth = imageData->width(); currentHeight = imageData->height(); image_width = currentWidth; image_height = currentHeight; image_frame->setSize(image_width, image_height); initDisplayImage(); // Rescale to fits window if (firstLoad) { currentZoom = 100; if (rescale(ZOOM_FIT_WINDOW) != 0) return false; firstLoad = false; } else { if (rescale(ZOOM_KEEP_LEVEL) != 0) return false; } if (mode == FITS_NORMAL) { if (fitsProg.wasCanceled()) return false; else { fitsProg.setValue(100); qApp->processEvents(); } } setAlignment(Qt::AlignCenter); // Load WCS data now if selected and image contains valid WCS header if (imageData->hasWCS() && Options::autoWCS() && (mode == FITS_NORMAL || mode == FITS_ALIGN) && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); 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); QTimer::singleShot(100 , this , SLOT(viewStarProfile())); //Need to wait till the Focus module finds stars, if its the Focus module. } updateFrame(); emit imageLoaded(); return true; } #endif void FITSView::loadFITS(const QString &inFilename, bool silent) { if (floatingToolBar != nullptr) { floatingToolBar->setVisible(true); } bool setBayerParams = false; BayerParams param; if ((imageData != nullptr) && imageData->hasDebayer()) { setBayerParams = true; imageData->getBayerParams(¶m); } // In case loadWCS is still running for previous image data, let's wait until it's over wcsWatcher.waitForFinished(); delete imageData; imageData = nullptr; filterStack.clear(); filterStack.push(FITS_NONE); if (filter != FITS_NONE) filterStack.push(filter); imageData = new FITSData(mode); if (setBayerParams) imageData->setBayerParams(¶m); fitsWatcher.setFuture(imageData->loadFITS(inFilename, silent)); } void FITSView::loadInFrame() { // Check if the loading was OK if (fitsWatcher.result() == false) { m_LastError = imageData->getLastError(); emit failed(); return; } // Notify if there is debayer data. emit debayerToggled(imageData->hasDebayer()); // Set current width and height currentWidth = imageData->width(); currentHeight = imageData->height(); image_width = currentWidth; image_height = currentHeight; image_frame->setSize(image_width, image_height); // Init the display image initDisplayImage(); uint8_t *ASImageBuffer = nullptr; if (Options::autoStretch() && (filter == FITS_NONE || (filter >= FITS_ROTATE_CW && filter <= FITS_FLIP_V))) { // If we perform autostretch, we need to create a buffer to save the raw image data before // autostretch filter operation changes the data. // After rescaling is done, we uint32_t totalBytes = image_width * image_height *imageData->channels() * imageData->getBytesPerPixel(); ASImageBuffer = new uint8_t[totalBytes]; memcpy(ASImageBuffer, imageData->getImageBuffer(), totalBytes); imageData->applyFilter(FITS_AUTO_STRETCH); } else imageData->applyFilter(filter); // Rescale to fits window on first load if (firstLoad) { currentZoom = 100; if (rescale(ZOOM_FIT_WINDOW) == false) { m_LastError = i18n("Rescaling image failed."); delete [] ASImageBuffer; emit failed(); return; } firstLoad = false; } else { if (rescale(ZOOM_KEEP_LEVEL) == false) { m_LastError = i18n("Rescaling image failed."); delete [] ASImageBuffer; emit failed(); return; } } // Restore original raw buffer after Autostretch if applicable if (ASImageBuffer) { imageData->setImageBuffer(ASImageBuffer); } setAlignment(Qt::AlignCenter); // Load WCS data now if selected and image contains valid WCS header if (imageData->hasWCS() && Options::autoWCS() && (mode == FITS_NORMAL || mode == FITS_ALIGN) && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); 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. QTimer::singleShot(100 , this , SLOT(viewStarProfile())); } scaledImage = QImage(); updateFrame(); emit loaded(); } int FITSView::saveFITS(const QString &newFilename) { return imageData->saveFITS(newFilename); } bool FITSView::rescale(FITSZoom type) { switch (imageData->property("dataType").toInt()) { case TBYTE: return rescale(type); case TSHORT: return rescale(type); case TUSHORT: return rescale(type); case TLONG: return rescale(type); case TULONG: return rescale(type); case TFLOAT: return rescale(type); case TLONGLONG: return rescale(type); case TDOUBLE: return rescale(type); default: break; } return false; } FITSView::CursorMode FITSView::getCursorMode() { return cursorMode; } void FITSView::enterEvent(QEvent *event) { Q_UNUSED(event) if ((floatingToolBar != nullptr) && (imageData != nullptr)) { QPointer eff = new QGraphicsOpacityEffect(this); floatingToolBar->setGraphicsEffect(eff); QPointer a = new QPropertyAnimation(eff, "opacity"); a->setDuration(500); a->setStartValue(0.2); a->setEndValue(1); a->setEasingCurve(QEasingCurve::InBack); a->start(QPropertyAnimation::DeleteWhenStopped); } } void FITSView::leaveEvent(QEvent *event) { Q_UNUSED(event) if ((floatingToolBar != nullptr) && (imageData != nullptr)) { QPointer eff = new QGraphicsOpacityEffect(this); floatingToolBar->setGraphicsEffect(eff); QPointer a = new QPropertyAnimation(eff, "opacity"); a->setDuration(500); a->setStartValue(1); a->setEndValue(0.2); a->setEasingCurve(QEasingCurve::OutBack); a->start(QPropertyAnimation::DeleteWhenStopped); } } template bool FITSView::rescale(FITSZoom type) { if (rawImage.isNull()) return false; uint8_t *image_buffer = imageData->getImageBuffer(); uint32_t size = imageData->width() * imageData->height(); #if 0 int BBP= imageData->getBytesPerPixel(); filter = filterStack.last(); if (Options::autoStretch() && (filter == FITS_NONE || (filter >= FITS_ROTATE_CW && filter <= FITS_FLIP_V))) { image_buffer = new uint8_t[size * imageData->channels() * BBP]; memcpy(image_buffer, imageData->getImageBuffer(), size * imageData->channels() * BBP); displayBuffer = true; double data_min = -1; double data_max = -1; imageData->applyFilter(FITS_AUTO_STRETCH, image_buffer, &data_min, &data_max); min = data_min; max = data_max; } else { imageData->applyFilter(filter); imageData->getMinMax(&min, &max); } #endif scaledImage = QImage(); auto *buffer = reinterpret_cast(image_buffer); if (imageData->getMin(0) == imageData->getMax(0)) { rawImage.fill(Qt::white); emit newStatus(i18n("Image is saturated."), FITS_MESSAGE); } else { if (image_height != imageData->height() || image_width != imageData->width()) { image_width = imageData->width(); image_height = imageData->height(); initDisplayImage(); if (isVisible()) emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION); } image_frame->setScaledContents(true); currentWidth = rawImage.width(); currentHeight = rawImage.height(); if (imageData->channels() == 1) { double range = imageData->getMax(0) - imageData->getMin(0); double bscale = 255. / range; double bzero = (-imageData->getMin(0)) * (255. / range); QVector> futures; /* Fill in pixel values using indexed map, linear scale */ for (uint32_t j = 0; j < image_height; j++) { futures.append(QtConcurrent::run([=]() { T *runningBuffer = buffer +j*image_width; uint8_t *scanLine = rawImage.scanLine(j); for (uint32_t i = 0; i < image_width; i++) { //scanLine[i] = qBound(0, static_cast(runningBuffer[i] * bscale + bzero), 255); scanLine[i] = qBound(0.0, runningBuffer[i] * bscale + bzero, 255.0); } })); } for(QFuture future : futures) future.waitForFinished(); } else { QVector> futures; double bscaleR = 255. / (imageData->getMax(0) - imageData->getMin(0)); double bzeroR = (-imageData->getMin(0)) * (255. / (imageData->getMax(0) - imageData->getMin(0))); double bscaleG = 255. / (imageData->getMax(1) - imageData->getMin(1)); double bzeroG = (-imageData->getMin(1)) * (255. / (imageData->getMax(1) - imageData->getMin(1))); double bscaleB = 255. / (imageData->getMax(2) - imageData->getMin(2)); double bzeroB = (-imageData->getMin(2)) * (255. / (imageData->getMax(2) - imageData->getMin(2))); /* Fill in pixel values using indexed map, linear scale */ for (uint32_t j = 0; j < image_height; j++) { futures.append(QtConcurrent::run([=]() { auto *scanLine = reinterpret_cast((rawImage.scanLine(j))); T *runningBufferR = buffer + j*image_width; T *runningBufferG = buffer + j*image_width + size; T *runningBufferB = buffer + j*image_width + size*2; for (uint32_t i = 0; i < image_width; i++) { scanLine[i] = qRgb(runningBufferR[i] * bscaleR + bzeroR, runningBufferG[i] * bscaleG + bzeroG, runningBufferB[i] * bscaleB + bzeroB); } })); } for(QFuture future : futures) future.waitForFinished(); } #if 0 if (imageData->getNumOfChannels() == 1) { /* Fill in pixel values using indexed map, linear scale */ for (int j = 0; j < image_height; j++) { unsigned char *scanLine = display_image->scanLine(j); for (int i = 0; i < image_width; i++) { val = buffer[j * image_width + i] * bscale + bzero; scanLine[i] = qBound(0.0, val, 255.0); } } } else { double rval = 0, gval = 0, bval = 0; QRgb value; /* Fill in pixel values using indexed map, linear scale */ for (int j = 0; j < image_height; j++) { QRgb *scanLine = reinterpret_cast((display_image->scanLine(j))); for (int i = 0; i < image_width; i++) { rval = buffer[j * image_width + i]; gval = buffer[j * image_width + i + size]; bval = buffer[j * image_width + i + size * 2]; value = qRgb(rval * bscale + bzero, gval * bscale + bzero, bval * bscale + bzero); scanLine[i] = value; } } } #endif } switch (type) { case ZOOM_FIT_WINDOW: if ((rawImage.width() > width() || rawImage.height() > height())) { double w = baseSize().width() - BASE_OFFSET; double h = baseSize().height() - BASE_OFFSET; if (!firstLoad) { w = viewport()->rect().width() - BASE_OFFSET; h = viewport()->rect().height() - BASE_OFFSET; } // Find the zoom level which will enclose the current FITS in the current window size double zoomX = floor((w / static_cast(currentWidth)) * 100.); double zoomY = floor((h / static_cast(currentHeight)) * 100.); (zoomX < zoomY) ? currentZoom = zoomX : currentZoom = zoomY; currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); if (currentZoom <= ZOOM_MIN) emit actionUpdated("view_zoom_out", false); } else { currentZoom = 100; currentWidth = image_width; currentHeight = image_height; } break; case ZOOM_KEEP_LEVEL: { currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); } break; default: currentZoom = 100; break; } setWidget(image_frame.get()); if (type != ZOOM_KEEP_LEVEL) emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); return true; } void FITSView::ZoomIn() { if (currentZoom >= ZOOM_DEFAULT && Options::limitedResourcesMode()) { emit newStatus(i18n("Cannot zoom in further due to active limited resources mode."), FITS_MESSAGE); return; } if (currentZoom < ZOOM_DEFAULT) currentZoom += ZOOM_LOW_INCR; else currentZoom += ZOOM_HIGH_INCR; emit actionUpdated("view_zoom_out", true); if (currentZoom >= ZOOM_MAX) { currentZoom = ZOOM_MAX; emit actionUpdated("view_zoom_in", false); } currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); updateFrame(); emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); } void FITSView::ZoomOut() { if (currentZoom <= ZOOM_DEFAULT) currentZoom -= ZOOM_LOW_INCR; else currentZoom -= ZOOM_HIGH_INCR; if (currentZoom <= ZOOM_MIN) { currentZoom = ZOOM_MIN; emit actionUpdated("view_zoom_out", false); } emit actionUpdated("view_zoom_in", true); currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); updateFrame(); emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); } void FITSView::ZoomToFit() { if (rawImage.isNull() == false) { rescale(ZOOM_FIT_WINDOW); updateFrame(); } } void FITSView::updateFrame() { bool ok = false; if (currentZoom != ZOOM_DEFAULT) { // Only scale when necessary if (scaledImage.isNull() || currentWidth != lastWidth || currentHeight != lastHeight) { scaledImage = rawImage.scaled(currentWidth, currentHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation); lastWidth = currentWidth; lastHeight = currentHeight; } ok = displayPixmap.convertFromImage(scaledImage); } else ok = displayPixmap.convertFromImage(rawImage); if (!ok) return; QPainter painter(&displayPixmap); drawOverlay(&painter); image_frame->setPixmap(displayPixmap); image_frame->resize(currentWidth, currentHeight); } void FITSView::ZoomDefault() { if (image_frame != nullptr) { emit actionUpdated("view_zoom_out", true); emit actionUpdated("view_zoom_in", true); currentZoom = ZOOM_DEFAULT; currentWidth = image_width; currentHeight = image_height; updateFrame(); emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); update(); } } void FITSView::drawOverlay(QPainter *painter) { painter->setRenderHint(QPainter::Antialiasing, Options::useAntialias()); if (markStars) drawStarCentroid(painter); if (trackingBoxEnabled && getCursorMode() != FITSView::scopeCursor) drawTrackingBox(painter); if (!markerCrosshair.isNull()) drawMarker(painter); if (showCrosshair) drawCrosshair(painter); if (showObjects) drawObjectNames(painter); if (showEQGrid) drawEQGrid(painter); if (showPixelGrid) drawPixelGrid(painter); } void FITSView::updateMode(FITSMode fmode) { mode = fmode; } void FITSView::drawMarker(QPainter *painter) { painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")), 2)); painter->setBrush(Qt::NoBrush); float pxperdegree = (currentZoom / ZOOM_DEFAULT) * (57.3 / 1.8); float s1 = 0.5 * pxperdegree; float s2 = pxperdegree; float s3 = 2.0 * pxperdegree; float x0 = markerCrosshair.x() * (currentZoom / ZOOM_DEFAULT); float y0 = markerCrosshair.y() * (currentZoom / ZOOM_DEFAULT); float x1 = x0 - 0.5 * s1; float y1 = y0 - 0.5 * s1; float x2 = x0 - 0.5 * s2; float y2 = y0 - 0.5 * s2; float x3 = x0 - 0.5 * s3; float y3 = y0 - 0.5 * s3; //Draw radial lines painter->drawLine(QPointF(x1, y0), QPointF(x3, y0)); painter->drawLine(QPointF(x0 + s2, y0), QPointF(x0 + 0.5 * s1, y0)); painter->drawLine(QPointF(x0, y1), QPointF(x0, y3)); painter->drawLine(QPointF(x0, y0 + 0.5 * s1), QPointF(x0, y0 + s2)); //Draw circles at 0.5 & 1 degrees painter->drawEllipse(QRectF(x1, y1, s1, s1)); painter->drawEllipse(QRectF(x2, y2, s2, s2)); } void FITSView::drawStarCentroid(QPainter *painter) { painter->setPen(QPen(Qt::red, 2)); // image_data->getStarCenter(); QList starCenters = imageData->getStarCenters(); for (int i = 0; i < starCenters.count(); i++) { int x1 = (starCenters[i]->x - starCenters[i]->width / 2) * (currentZoom / ZOOM_DEFAULT); int y1 = (starCenters[i]->y - starCenters[i]->width / 2) * (currentZoom / ZOOM_DEFAULT); int w = (starCenters[i]->width) * (currentZoom / ZOOM_DEFAULT); painter->drawEllipse(x1, y1, w, w); } } void FITSView::drawTrackingBox(QPainter *painter) { painter->setPen(QPen(Qt::green, 2)); if (trackingBox.isNull()) return; int x1 = trackingBox.x() * (currentZoom / ZOOM_DEFAULT); int y1 = trackingBox.y() * (currentZoom / ZOOM_DEFAULT); int w = trackingBox.width() * (currentZoom / ZOOM_DEFAULT); int h = trackingBox.height() * (currentZoom / ZOOM_DEFAULT); painter->drawRect(x1, y1, w, h); } /** This Method draws a large Crosshair in the center of the image, it is like a set of axes. */ void FITSView::drawCrosshair(QPainter *painter) { float scale = (currentZoom / ZOOM_DEFAULT); QPointF c = QPointF((qreal)image_width / 2 * scale, (qreal)image_height / 2 * scale); float midX = (float)image_width / 2 * scale; float midY = (float)image_height / 2 * scale; float maxX = (float)image_width * scale; float maxY = (float)image_height * scale; float r = 50 * scale; painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")))); //Horizontal Line to Circle painter->drawLine(0, midY, midX - r, midY); //Horizontal Line past Circle painter->drawLine(midX + r, midY, maxX, midY); //Vertical Line to Circle painter->drawLine(midX, 0, midX, midY - r); //Vertical Line past Circle painter->drawLine(midX, midY + r, midX, maxY); //Circles painter->drawEllipse(c, r, r); painter->drawEllipse(c, r / 2, r / 2); } /** This method is intended to draw a pixel grid onto the image. It first determines useful information from the image. Then it draws the axes on the image if the crosshairs are not displayed. Finally it draws the gridlines so that there will be 4 Gridlines on either side of the axes. Note: This has to start drawing at the center not at the edges because the center axes must be in the center of the image. */ void FITSView::drawPixelGrid(QPainter *painter) { float scale = (currentZoom / ZOOM_DEFAULT); double width = image_width * scale; double height = image_height * scale; double cX = width / 2; double cY = height / 2; double deltaX = width / 10; double deltaY = height / 10; //draw the Axes painter->setPen(QPen(Qt::red)); painter->drawText(cX - 30, height - 5, QString::number((int)((cX) / scale))); painter->drawText(width - 30, cY - 5, QString::number((int)((cY) / scale))); if (!showCrosshair) { painter->drawLine(cX, 0, cX, height); painter->drawLine(0, cY, width, cY); } painter->setPen(QPen(Qt::gray)); //Start one iteration past the Center and draw 4 lines on either side of 0 for (int x = deltaX; x < cX - deltaX; x += deltaX) { painter->drawText(cX + x - 30, height - 5, QString::number((int)((cX + x) / scale))); painter->drawText(cX - x - 30, height - 5, QString::number((int)((cX - x) / scale))); painter->drawLine(cX - x, 0, cX - x, height); painter->drawLine(cX + x, 0, cX + x, height); } //Start one iteration past the Center and draw 4 lines on either side of 0 for (int y = deltaY; y < cY - deltaY; y += deltaY) { painter->drawText(width - 30, cY + y - 5, QString::number((int)((cY + y) / scale))); painter->drawText(width - 30, cY - y - 5, QString::number((int)((cY - y) / scale))); painter->drawLine(0, cY + y, width, cY + y); painter->drawLine(0, cY - y, width, cY - y); } } bool FITSView::imageHasWCS() { if (imageData != nullptr) return imageData->hasWCS(); return false; } void FITSView::drawObjectNames(QPainter *painter) { painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("FITSObjectLabelColor")))); float scale = (currentZoom / ZOOM_DEFAULT); foreach (FITSSkyObject *listObject, imageData->getSkyObjects()) { painter->drawRect(listObject->x() * scale - 5, listObject->y() * scale - 5, 10, 10); painter->drawText(listObject->x() * scale + 10, listObject->y() * scale + 10, listObject->skyObject()->name()); } } /** This method will paint EQ Gridlines in an overlay if there is WCS data present. It determines the minimum and maximum RA and DEC, then it uses that information to judge which gridLines to draw. Then it calls the drawEQGridlines methods below to draw gridlines at those specific RA and Dec values. */ void FITSView::drawEQGrid(QPainter *painter) { float scale = (currentZoom / ZOOM_DEFAULT); if (imageData->hasWCS()) { wcs_point *wcs_coord = imageData->getWCSCoord(); if (wcs_coord != nullptr) { int size = image_width * image_height; double maxRA = -1000; double minRA = 1000; double maxDec = -1000; double minDec = 1000; for (int i = 0; i < (size); i++) { double ra = wcs_coord[i].ra; double dec = wcs_coord[i].dec; if (ra > maxRA) maxRA = ra; if (ra < minRA) minRA = ra; if (dec > maxDec) maxDec = dec; if (dec < minDec) minDec = dec; } auto minDecMinutes = (int)(minDec * 12); //This will force the Dec Scale to 5 arc minutes in the loop auto maxDecMinutes = (int)(maxDec * 12); auto minRAMinutes = (int)(minRA / 15.0 * 120.0); //This will force the scale to 1/2 minutes of RA in the loop from 0 to 50 degrees auto maxRAMinutes = (int)(maxRA / 15.0 * 120.0); double raConvert = 15 / 120.0; //This will undo the calculation above to retrieve the actual RA. double decConvert = 1.0 / 12.0; //This will undo the calculation above to retrieve the actual DEC. if (maxDec > 50 || minDec < -50) { minRAMinutes = (int)(minRA / 15.0 * 60.0); //This will force the scale to 1 min of RA from 50 to 80 degrees maxRAMinutes = (int)(maxRA / 15.0 * 60.0); raConvert = 15 / 60.0; } if (maxDec > 80 || minDec < -80) { minRAMinutes = (int)(minRA / 15.0 * 30); //This will force the scale to 2 min of RA from 80 to 85 degrees maxRAMinutes = (int)(maxRA / 15.0 * 30); raConvert = 15 / 30.0; } if (maxDec > 85 || minDec < -85) { minRAMinutes = (int)(minRA / 15.0 * 6); //This will force the scale to 10 min of RA from 85 to 89 degrees maxRAMinutes = (int)(maxRA / 15.0 * 6); raConvert = 15 / 6.0; } if (maxDec >= 89.25 || minDec <= -89.25) { minRAMinutes = (int)(minRA / 15); //This will force the scale to whole hours of RA in the loop really close to the poles maxRAMinutes = (int)(maxRA / 15); raConvert = 15; } painter->setPen(QPen(Qt::yellow)); QPointF pixelPoint, imagePoint, pPoint; //This section draws the RA Gridlines for (int targetRA = minRAMinutes; targetRA <= maxRAMinutes; targetRA++) { painter->setPen(QPen(Qt::yellow)); double target = targetRA * raConvert; if (eqGridPoints.count() != 0) eqGridPoints.clear(); double increment = std::abs((maxDec - minDec) / 100.0); //This will determine how many points to use to create the RA Line for (double targetDec = minDec; targetDec <= maxDec; targetDec += increment) { SkyPoint pointToGet(target / 15.0, targetDec); bool inImage = imageData->wcsToPixel(pointToGet, pixelPoint, imagePoint); if (inImage) { QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale); eqGridPoints.append(pt); } } if (eqGridPoints.count() > 1) { for (int i = 1; i < eqGridPoints.count(); i++) painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i)); QPointF pt = getPointForGridLabel(); if (pt.x() != -100) { if (maxDec > 50 || maxDec < -50) painter->drawText(pt.x(), pt.y(), QString::number(dms(target).hour()) + "h " + QString::number(dms(target).minute()) + '\''); else painter->drawText(pt.x() - 20, pt.y(), QString::number(dms(target).hour()) + "h " + QString::number(dms(target).minute()) + "' " + QString::number(dms(target).second()) + "''"); } } } //This section draws the DEC Gridlines for (int targetDec = minDecMinutes; targetDec <= maxDecMinutes; targetDec++) { if (eqGridPoints.count() != 0) eqGridPoints.clear(); double increment = std::abs((maxRA - minRA) / 100.0); //This will determine how many points to use to create the Dec Line double target = targetDec * decConvert; for (double targetRA = minRA; targetRA <= maxRA; targetRA += increment) { SkyPoint pointToGet(targetRA / 15, targetDec * decConvert); bool inImage = imageData->wcsToPixel(pointToGet, pixelPoint, imagePoint); if (inImage) { QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale); eqGridPoints.append(pt); } } if (eqGridPoints.count() > 1) { for (int i = 1; i < eqGridPoints.count(); i++) painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i)); QPointF pt = getPointForGridLabel(); if (pt.x() != -100) painter->drawText(pt.x(), pt.y(), QString::number(dms(target).degree()) + "° " + QString::number(dms(target).arcmin()) + '\''); } } //This Section Draws the North Celestial Pole if present SkyPoint NCP(0, 90); bool NCPtest = imageData->wcsToPixel(NCP, pPoint, imagePoint); if (NCPtest) { bool NCPinImage = (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height); if (NCPinImage) { painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4, KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")); painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15, i18nc("North Celestial Pole", "NCP")); } } //This Section Draws the South Celestial Pole if present SkyPoint SCP(0, -90); bool SCPtest = imageData->wcsToPixel(SCP, pPoint, imagePoint); if (SCPtest) { bool SCPinImage = (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height); if (SCPinImage) { painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4, KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")); painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15, i18nc("South Celestial Pole", "SCP")); } } } } } bool FITSView::pointIsInImage(QPointF pt, bool scaled) { float scale = (currentZoom / ZOOM_DEFAULT); if (scaled) return pt.x() < image_width * scale && pt.y() < image_height * scale && pt.x() > 0 && pt.y() > 0; else return pt.x() < image_width && pt.y() < image_height && pt.x() > 0 && pt.y() > 0; } QPointF FITSView::getPointForGridLabel() { float scale = (currentZoom / ZOOM_DEFAULT); //These get the maximum X and Y points in the list that are in the image QPointF maxXPt(image_width * scale / 2, image_height * scale / 2); for (auto& p : eqGridPoints) { if (p.x() > maxXPt.x() && pointIsInImage(p, true)) maxXPt = p; } QPointF maxYPt(image_width * scale / 2, image_height * scale / 2); for (auto& p : eqGridPoints) { if (p.y() > maxYPt.y() && pointIsInImage(p, true)) maxYPt = p; } QPointF minXPt(image_width * scale / 2, image_height * scale / 2); for (auto& p : eqGridPoints) { if (p.x() < minXPt.x() && pointIsInImage(p, true)) minXPt = p; } QPointF minYPt(image_width * scale / 2, image_height * scale / 2); for (auto& p : eqGridPoints) { if (p.y() < minYPt.y() && pointIsInImage(p, true)) minYPt = p; } //This gives preference to points that are on the right hand side and bottom. //But if the line doesn't intersect the right or bottom, it then tries for the top and left. //If no points are found in the image, it returns a point off the screen //If all else fails, like in the case of a circle on the image, it returns the far right point. if (image_width * scale - maxXPt.x() < 10) { return QPointF( image_width * scale - 50, maxXPt.y() - 10); //This will draw the text on the right hand side, up and to the left of the point where the line intersects } if (image_height * scale - maxYPt.y() < 10) return QPointF( maxYPt.x() - 40, image_height * scale - 10); //This will draw the text on the bottom side, up and to the left of the point where the line intersects if (minYPt.y() * scale < 30) return QPointF( minYPt.x() + 10, 20); //This will draw the text on the top side, down and to the right of the point where the line intersects if (minXPt.x() * scale < 30) return QPointF( 10, minXPt.y() + 20); //This will draw the text on the left hand side, down and to the right of the point where the line intersects if (maxXPt.x() == image_width * scale / 2 && maxXPt.y() == image_height * scale / 2) return QPointF(-100, -100); //All of the points were off the screen return QPoint(maxXPt.x() - 40, maxXPt.y() - 10); } void FITSView::setFirstLoad(bool value) { firstLoad = value; } QPixmap &FITSView::getTrackingBoxPixmap(uint8_t margin) { if (trackingBox.isNull()) return trackingBoxPixmap; int x1 = (trackingBox.x() - margin) * (currentZoom / ZOOM_DEFAULT); int y1 = (trackingBox.y() - margin) * (currentZoom / ZOOM_DEFAULT); int w = (trackingBox.width() + margin*2) * (currentZoom / ZOOM_DEFAULT); int h = (trackingBox.height() + margin*2) * (currentZoom / ZOOM_DEFAULT); trackingBoxPixmap = image_frame->grab(QRect(x1, y1, w, h)); return trackingBoxPixmap; } void FITSView::setTrackingBox(const QRect &rect) { if (rect != trackingBox) { trackingBox = rect; updateFrame(); if(showStarProfile) viewStarProfile(); } } void FITSView::resizeTrackingBox(int newSize) { int x = trackingBox.x() + trackingBox.width()/2; int y = trackingBox.y() + trackingBox.height()/2; int delta = newSize / 2; setTrackingBox(QRect( x - delta, y - delta, newSize, newSize)); } bool FITSView::isCrosshairShown() { return showCrosshair; } bool FITSView::isEQGridShown() { return showEQGrid; } bool FITSView::areObjectsShown() { return showObjects; } bool FITSView::isPixelGridShown() { return showPixelGrid; } void FITSView::toggleCrosshair() { showCrosshair = !showCrosshair; updateFrame(); } void FITSView::toggleEQGrid() { showEQGrid = !showEQGrid; if (!imageData->isWCSLoaded() && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); return; } if (image_frame != nullptr) updateFrame(); } void FITSView::toggleObjects() { showObjects = !showObjects; if (!imageData->isWCSLoaded() && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); return; } if (image_frame != nullptr) updateFrame(); } void FITSView::toggleStars() { toggleStars(!markStars); if (image_frame != nullptr) updateFrame(); } void FITSView::toggleStarProfile() { #ifdef HAVE_DATAVISUALIZATION showStarProfile = !showStarProfile; if(showStarProfile && trackingBoxEnabled) viewStarProfile(); if(toggleProfileAction) toggleProfileAction->setChecked(showStarProfile); if(mode == FITS_NORMAL || mode == FITS_ALIGN) { if(showStarProfile) { setCursorMode(selectCursor); connect(this, SIGNAL(trackingStarSelected(int,int)), this, SLOT(move3DTrackingBox(int,int))); - if(floatingToolBar && starProfileWidget) + if(starProfileWidget) connect(starProfileWidget, SIGNAL(rejected()) , this, SLOT(toggleStarProfile())); if(starProfileWidget) connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)) , this, SLOT(resizeTrackingBox(int))); trackingBox = QRect(0, 0, 128, 128); setTrackingBoxEnabled(true); } else { if(getCursorMode() == selectCursor) setCursorMode(dragCursor); disconnect(this, SIGNAL(trackingStarSelected(int,int)), this, SLOT(move3DTrackingBox(int,int))); disconnect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)) , this, SLOT(resizeTrackingBox(int))); - if(floatingToolBar) - disconnect(starProfileWidget, SIGNAL(rejected()) , this, SLOT(toggleStarProfile())); + disconnect(starProfileWidget, SIGNAL(rejected()) , this, SLOT(toggleStarProfile())); setTrackingBoxEnabled(false); - starProfileWidget->deleteLater(); + if(starProfileWidget) + starProfileWidget->close(); + starProfileWidget = nullptr; + emit starProfileWindowClosed(); } updateFrame(); } #endif } void FITSView::move3DTrackingBox(int x, int y) { int boxSize = trackingBox.width(); QRect starRect = QRect(x - boxSize / 2 , y - boxSize / 2, boxSize, boxSize); setTrackingBox(starRect); } void FITSView::viewStarProfile() { #ifdef HAVE_DATAVISUALIZATION if(!trackingBoxEnabled) { setTrackingBoxEnabled(true); setTrackingBox(QRect(0, 0, 128, 128)); } if(!starProfileWidget) { - //I had to change it to nullptr instead of "this" - //because while it worked before, there was some change in QT - //With their change, if the user hid the viewer, it would come up empty next time! - //Changing it to nullptr fixes the problem - //starProfileWidget = new StarProfileViewer(this); - starProfileWidget = new StarProfileViewer(nullptr); + starProfileWidget = new StarProfileViewer(this); //This is a band-aid to fix a QT bug with createWindowContainer //It will set the cursor of the Window containing the view that called the Star Profile method to the Arrow Cursor //Note that Ekos Manager is a QDialog and FitsViewer is a KXmlGuiWindow QWidget *superParent = this->parentWidget(); while(superParent->parentWidget()!=0 && !superParent->inherits("QDialog") && !superParent->inherits("KXmlGuiWindow")) superParent=superParent->parentWidget(); superParent->setCursor(Qt::ArrowCursor); //This is the end of the band-aid - if(floatingToolBar) - connect(starProfileWidget, SIGNAL(rejected()) , this, SLOT(toggleStarProfile())); + connect(starProfileWidget, SIGNAL(rejected()) , this, SLOT(toggleStarProfile())); if(mode == FITS_ALIGN || mode == FITS_NORMAL) { starProfileWidget->enableTrackingBox(true); imageData->setStarAlgorithm(ALGORITHM_CENTROID); connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)) , this, SLOT(resizeTrackingBox(int))); } } QList starCenters = imageData->getStarCentersInSubFrame(trackingBox); if(starCenters.size() == 0) { // FIXME, the following does not work anymore. //imageData->findStars(&trackingBox, true); // FIXME replacing it with this imageData->findStars(ALGORITHM_CENTROID, trackingBox); starCenters = imageData->getStarCentersInSubFrame(trackingBox); } starProfileWidget->loadData(imageData, trackingBox, starCenters); starProfileWidget->show(); starProfileWidget->raise(); if(markStars) updateFrame(); //this is to update for the marked stars #endif } void FITSView::togglePixelGrid() { showPixelGrid = !showPixelGrid; updateFrame(); } int FITSView::findStars(StarAlgorithm algorithm, const QRect &searchBox) { int count = 0; if(trackingBoxEnabled) count = imageData->findStars(algorithm, trackingBox); else count = imageData->findStars(algorithm, searchBox); return count; } void FITSView::toggleStars(bool enable) { markStars = enable; if (markStars && !imageData->areStarsSearched()) { QApplication::setOverrideCursor(Qt::WaitCursor); emit newStatus(i18n("Finding stars..."), FITS_MESSAGE); qApp->processEvents(); int count = findStars(); if (count >= 0 && isVisible()) emit newStatus(i18np("1 star detected.", "%1 stars detected.", count), FITS_MESSAGE); QApplication::restoreOverrideCursor(); } } void FITSView::processPointSelection(int x, int y) { //if (mode != FITS_GUIDE) //return; //image_data->getCenterSelection(&x, &y); //setGuideSquare(x,y); emit trackingStarSelected(x, y); } void FITSView::processMarkerSelection(int x, int y) { markerCrosshair.setX(x); markerCrosshair.setY(y); updateFrame(); } void FITSView::setTrackingBoxEnabled(bool enable) { if (enable != trackingBoxEnabled) { trackingBoxEnabled = enable; //updateFrame(); } } void FITSView::wheelEvent(QWheelEvent *event) { //This attempts to send the wheel event back to the Scroll Area if it was taken from a trackpad //It should still do the zoom if it is a mouse wheel if (event->source() == Qt::MouseEventSynthesizedBySystem) { QScrollArea::wheelEvent(event); } else { QPoint mouseCenter = getImagePoint(event->pos()); if (event->angleDelta().y() > 0) ZoomIn(); else ZoomOut(); event->accept(); cleanUpZoom(mouseCenter); } } /** This method is intended to keep key locations in an image centered on the screen while zooming. If there is a marker or tracking box, it centers on those. If not, it uses the point called viewCenter that was passed as a parameter. */ void FITSView::cleanUpZoom(QPoint viewCenter) { int x0 = 0; int y0 = 0; double scale = (currentZoom / ZOOM_DEFAULT); if (!markerCrosshair.isNull()) { x0 = markerCrosshair.x() * scale; y0 = markerCrosshair.y() * scale; } else if (trackingBoxEnabled) { x0 = trackingBox.center().x() * scale; y0 = trackingBox.center().y() * scale; } else { x0 = viewCenter.x() * scale; y0 = viewCenter.y() * scale; } ensureVisible(x0, y0, width() / 2, height() / 2); updateMouseCursor(); } /** This method converts a point from the ViewPort Coordinate System to the Image Coordinate System. */ QPoint FITSView::getImagePoint(QPoint viewPortPoint) { QWidget *w = widget(); if (w == nullptr) return QPoint(0, 0); double scale = (currentZoom / ZOOM_DEFAULT); QPoint widgetPoint = w->mapFromParent(viewPortPoint); QPoint imagePoint = QPoint(widgetPoint.x() / scale, widgetPoint.y() / scale); return imagePoint; } void FITSView::initDisplayImage() { if (imageData->channels() == 1) { rawImage = QImage(image_width, image_height, QImage::Format_Indexed8); rawImage.setColorCount(256); for (int i = 0; i < 256; i++) rawImage.setColor(i, qRgb(i, i, i)); } else { rawImage = QImage(image_width, image_height, QImage::Format_RGB32); } } /** The Following two methods allow gestures to work with trackpads. Specifically, we are targeting the pinch events, so that if one is generated, Then the pinchTriggered method will be called. If the event is not a pinch gesture, then the event is passed back to the other event handlers. */ bool FITSView::event(QEvent *event) { if (event->type() == QEvent::Gesture) return gestureEvent(dynamic_cast(event)); return QScrollArea::event(event); } bool FITSView::gestureEvent(QGestureEvent *event) { if (QGesture *pinch = event->gesture(Qt::PinchGesture)) pinchTriggered(dynamic_cast(pinch)); return true; } /** This Method works with Trackpads to use the pinch gesture to scroll in and out It stores a point to keep track of the location where the gesture started so that while you are zooming, it tries to keep that initial point centered in the view. **/ void FITSView::pinchTriggered(QPinchGesture *gesture) { if (!zooming) { zoomLocation = getImagePoint(mapFromGlobal(QCursor::pos())); zooming = true; } if (gesture->state() == Qt::GestureFinished) { zooming = false; } zoomTime++; //zoomTime is meant to slow down the zooming with a pinch gesture. if (zoomTime > 10000) //This ensures zoomtime never gets too big. zoomTime = 0; if (zooming && (zoomTime % 10 == 0)) //zoomTime is set to slow it by a factor of 10. { if (gesture->totalScaleFactor() > 1) ZoomIn(); else ZoomOut(); } cleanUpZoom(zoomLocation); } /*void FITSView::handleWCSCompletion() { //bool hasWCS = wcsWatcher.result(); if(imageData->hasWCS()) this->updateFrame(); emit wcsToggled(imageData->hasWCS()); }*/ void FITSView::syncWCSState() { bool hasWCS = imageData->hasWCS(); bool wcsLoaded = imageData->isWCSLoaded(); if (hasWCS && wcsLoaded) this->updateFrame(); emit wcsToggled(hasWCS); if (toggleEQGridAction != nullptr) toggleEQGridAction->setEnabled(hasWCS); if (toggleObjectsAction != nullptr) toggleObjectsAction->setEnabled(hasWCS); if (centerTelescopeAction != nullptr) centerTelescopeAction->setEnabled(hasWCS); } void FITSView::createFloatingToolBar() { if (floatingToolBar != nullptr) return; floatingToolBar = new QToolBar(this); auto *eff = new QGraphicsOpacityEffect(this); floatingToolBar->setGraphicsEffect(eff); eff->setOpacity(0.2); floatingToolBar->setVisible(false); floatingToolBar->setStyleSheet( "QToolBar{background: rgba(150, 150, 150, 210); border:none; color: yellow}" "QToolButton{background: transparent; border:none; color: yellow}" "QToolButton:hover{background: rgba(200, 200, 200, 255);border:solid; color: yellow}" "QToolButton:checked{background: rgba(110, 110, 110, 255);border:solid; color: yellow}"); floatingToolBar->setFloatable(true); floatingToolBar->setIconSize(QSize(25, 25)); //floatingToolBar->setMovable(true); QAction *action = nullptr; floatingToolBar->addAction(QIcon::fromTheme("zoom-in"), i18n("Zoom In"), this, SLOT(ZoomIn())); floatingToolBar->addAction(QIcon::fromTheme("zoom-out"), i18n("Zoom Out"), this, SLOT(ZoomOut())); floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-best"), i18n("Default Zoom"), this, SLOT(ZoomDefault())); floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-width"), i18n("Zoom to Fit"), this, SLOT(ZoomToFit())); floatingToolBar->addSeparator(); action = floatingToolBar->addAction(QIcon::fromTheme("crosshairs"), i18n("Show Cross Hairs"), this, SLOT(toggleCrosshair())); action->setCheckable(true); action = floatingToolBar->addAction(QIcon::fromTheme("map-flat"), i18n("Show Pixel Gridlines"), this, SLOT(togglePixelGrid())); action->setCheckable(true); toggleStarsAction = floatingToolBar->addAction(QIcon::fromTheme("kstars_stars"), i18n("Detect Stars in Image"), this, SLOT(toggleStars())); toggleStarsAction->setCheckable(true); #ifdef HAVE_DATAVISUALIZATION toggleProfileAction = floatingToolBar->addAction(QIcon::fromTheme("star-profile", QIcon(":/icons/star_profile.svg")), i18n("View Star Profile"), this, SLOT(toggleStarProfile())); toggleProfileAction->setCheckable(true); #endif if (mode == FITS_NORMAL || mode == FITS_ALIGN) { floatingToolBar->addSeparator(); toggleEQGridAction = floatingToolBar->addAction(QIcon::fromTheme("kstars_grid"), i18n("Show Equatorial Gridlines"), this, SLOT(toggleEQGrid())); toggleEQGridAction->setCheckable(true); toggleEQGridAction->setEnabled(false); toggleObjectsAction = floatingToolBar->addAction(QIcon::fromTheme("help-hint"), i18n("Show Objects in Image"), this, SLOT(toggleObjects())); toggleObjectsAction->setCheckable(true); toggleEQGridAction->setEnabled(false); centerTelescopeAction = floatingToolBar->addAction(QIcon::fromTheme("center_telescope", QIcon(":/icons/center_telescope.svg")), i18n("Center Telescope"), this, SLOT(centerTelescope())); centerTelescopeAction->setCheckable(true); centerTelescopeAction->setEnabled(false); } } /** This methood either enables or disables the scope mouse mode so you can slew your scope to coordinates just by clicking the mouse on a spot in the image. */ void FITSView::centerTelescope() { if (imageHasWCS()) { if (getCursorMode() == FITSView::scopeCursor) { setCursorMode(lastMouseMode); } else { lastMouseMode = getCursorMode(); setCursorMode(FITSView::scopeCursor); } updateFrame(); } updateScopeButton(); } void FITSView::updateScopeButton() { if (centerTelescopeAction != nullptr) { if (getCursorMode() == FITSView::scopeCursor) { centerTelescopeAction->setChecked(true); } else { centerTelescopeAction->setChecked(false); } } } /** This method just verifies if INDI is online, a telescope present, and is connected */ bool FITSView::isTelescopeActive() { #ifdef HAVE_INDI if (INDIListener::Instance()->size() == 0) { return false; } foreach (ISD::GDInterface *gd, INDIListener::Instance()->getDevices()) { INDI::BaseDevice *bd = gd->getBaseDevice(); if (gd->getType() != KSTARS_TELESCOPE) continue; if (bd == nullptr) continue; return bd->isConnected(); } return false; #else return false; #endif } void FITSView::setStarsEnabled(bool enable) { markStars = enable; if (floatingToolBar != nullptr) { foreach (QAction *action, floatingToolBar->actions()) { if (action->text() == i18n("Detect Stars in Image")) { action->setChecked(markStars); break; } } } } diff --git a/kstars/fitsviewer/fitsview.h b/kstars/fitsviewer/fitsview.h index 80366eb22..3dd813529 100644 --- a/kstars/fitsviewer/fitsview.h +++ b/kstars/fitsviewer/fitsview.h @@ -1,293 +1,294 @@ /* FITS Label Copyright (C) 2003-2017 Jasem Mutlaq Copyright (C) 2016-2017 Robert Lancaster 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. */ #pragma once #include "fitscommon.h" #include #ifdef HAVE_DATAVISUALIZATION #include "starprofileviewer.h" #endif #include #include #include #include #include #ifdef WIN32 // avoid compiler warning when windows.h is included after fitsio.h #include #endif #include #include #define MINIMUM_PIXEL_RANGE 5 #define MINIMUM_STDVAR 5 class QAction; class QEvent; class QGestureEvent; class QImage; class QLabel; class QPinchGesture; class QResizeEvent; class QToolBar; class FITSData; class FITSLabel; class FITSView : public QScrollArea { Q_OBJECT public: explicit FITSView(QWidget *parent = nullptr, FITSMode fitsMode = FITS_NORMAL, FITSScale filterType = FITS_NONE); ~FITSView(); typedef enum {dragCursor, selectCursor, scopeCursor, crosshairCursor } CursorMode; /** * @brief loadFITS Loads FITS data and display it in FITSView frame * @param inFilename FITS File name * @param silent if set, error popups are suppressed. * @note If image is successfully, loaded() signal is emitted, otherwise failed() signal is emitted. * Obtain error by calling lastError() */ void loadFITS(const QString &inFilename, bool silent = true); // Save FITS int saveFITS(const QString &newFilename); // Rescale image lineary from image_buffer, fit to window if desired bool rescale(FITSZoom type); // Access functions FITSData *getImageData() const { return imageData; } double getCurrentZoom() const { return currentZoom; } QImage getDisplayImage() const { return rawImage; } const QPixmap &getDisplayPixmap() const { return displayPixmap; } // Tracking square void setTrackingBoxEnabled(bool enable); bool isTrackingBoxEnabled() const { return trackingBoxEnabled; } QPixmap &getTrackingBoxPixmap(uint8_t margin=0); void setTrackingBox(const QRect &rect); const QRect &getTrackingBox() const { return trackingBox; } // last error const QString &lastError() const { return m_LastError; } // Overlay virtual void drawOverlay(QPainter *); // Overlay objects void drawStarCentroid(QPainter *); void drawTrackingBox(QPainter *); void drawMarker(QPainter *); void drawCrosshair(QPainter *); void drawEQGrid(QPainter *); void drawObjectNames(QPainter *painter); void drawPixelGrid(QPainter *painter); bool isCrosshairShown(); bool areObjectsShown(); bool isEQGridShown(); bool isPixelGridShown(); bool imageHasWCS(); void updateFrame(); bool isTelescopeActive(); void enterEvent(QEvent *event); void leaveEvent(QEvent *event); CursorMode getCursorMode(); void setCursorMode(CursorMode mode); void updateMouseCursor(); void updateScopeButton(); void setScopeButton(QAction *action) { centerTelescopeAction = action; } // Zoom related void cleanUpZoom(QPoint viewCenter); QPoint getImagePoint(QPoint viewPortPoint); uint16_t zoomedWidth() { return currentWidth; } uint16_t zoomedHeight() { return currentHeight; } // Star Detection int findStars(StarAlgorithm algorithm = ALGORITHM_CENTROID, const QRect &searchBox = QRect()); void toggleStars(bool enable); void setStarsEnabled(bool enable); // FITS Mode void updateMode(FITSMode fmode); FITSMode getMode() { return mode; } void setFilter(FITSScale newFilter) { filter = newFilter; } void setFirstLoad(bool value); void pushFilter(FITSScale value) { filterStack.push(value); } FITSScale popFilter() { return filterStack.pop(); } // Floating toolbar void createFloatingToolBar(); //void setLoadWCSEnabled(bool value); public slots: void wheelEvent(QWheelEvent *event); void resizeEvent(QResizeEvent *event); void ZoomIn(); void ZoomOut(); void ZoomDefault(); void ZoomToFit(); // Grids void toggleEQGrid(); void toggleObjects(); void togglePixelGrid(); void toggleCrosshair(); // Stars void toggleStars(); void toggleStarProfile(); void viewStarProfile(); void centerTelescope(); void processPointSelection(int x, int y); void processMarkerSelection(int x, int y); void move3DTrackingBox(int x, int y); void resizeTrackingBox(int newSize); protected slots: /** * @brief syncWCSState Update toolbar and actions depending on whether WCS is available or not */ void syncWCSState(); private: bool event(QEvent *event); bool gestureEvent(QGestureEvent *event); void pinchTriggered(QPinchGesture *gesture); template bool rescale(FITSZoom type); double average(); double stddev(); void calculateMaxPixel(double min, double max); void initDisplayImage(); QPointF getPointForGridLabel(); bool pointIsInImage(QPointF pt, bool scaled); void loadInFrame(); public: CursorMode lastMouseMode { selectCursor }; bool isStarProfileShown() { return showStarProfile; } protected: /// WCS Future Watcher QFutureWatcher wcsWatcher; /// FITS Future Watcher QFutureWatcher fitsWatcher; /// Cross hair QPointF markerCrosshair; /// Pointer to FITSData object FITSData *imageData { nullptr }; /// Current zoom level double currentZoom { 0 }; private: QLabel *noImageLabel { nullptr }; QPixmap noImage; QVector eqGridPoints; std::unique_ptr image_frame; uint32_t image_width { 0 }; uint32_t image_height { 0 }; /// Current width due to zoom uint16_t currentWidth { 0 }; uint16_t lastWidth { 0 }; /// Current height due to zoom uint16_t currentHeight { 0 }; uint16_t lastHeight { 0 }; /// Image zoom factor const double zoomFactor; // Original full-size image QImage rawImage; // Scaled images QImage scaledImage; // Actual pixmap after all the overlays QPixmap displayPixmap; bool firstLoad { true }; bool markStars { false }; bool showStarProfile { false }; bool showCrosshair { false }; bool showObjects { false }; bool showEQGrid { false }; bool showPixelGrid { false }; CursorMode cursorMode { selectCursor }; bool zooming { false }; int zoomTime { 0 }; QPoint zoomLocation; QString filename; FITSMode mode; FITSScale filter; QString m_LastError; QStack filterStack; // Tracking box bool trackingBoxEnabled { false }; QRect trackingBox; QPixmap trackingBoxPixmap; // Scope pixmap QPixmap redScopePixmap; // Magenta Scope Pixmap QPixmap magentaScopePixmap; // Floating toolbar QToolBar *floatingToolBar { nullptr }; QAction *centerTelescopeAction { nullptr }; QAction *toggleEQGridAction { nullptr }; QAction *toggleObjectsAction { nullptr }; QAction *toggleStarsAction { nullptr }; QAction *toggleProfileAction { nullptr }; //Star Profile Viewer #ifdef HAVE_DATAVISUALIZATION QPointer starProfileWidget; #endif signals: void newStatus(const QString &msg, FITSBar id); void debayerToggled(bool); void wcsToggled(bool); void actionUpdated(const QString &name, bool enable); void trackingStarSelected(int x, int y); void loaded(); void failed(); + void starProfileWindowClosed(); friend class FITSLabel; }; diff --git a/kstars/fitsviewer/fitsviewer.cpp b/kstars/fitsviewer/fitsviewer.cpp index 57d3e7f1d..ee2ce7e8a 100644 --- a/kstars/fitsviewer/fitsviewer.cpp +++ b/kstars/fitsviewer/fitsviewer.cpp @@ -1,946 +1,953 @@ /*************************************************************************** FITSViewer.cpp - A FITSViewer for KStars ------------------- begin : Thu Jan 22 2004 copyright : (C) 2004 by Jasem Mutlaq email : mutlaqja@ikarustech.com 2006-03-03 Using CFITSIO, Porting to Qt4 ***************************************************************************/ /*************************************************************************** * * * This program 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. * * * ***************************************************************************/ #include "fitsviewer.h" #include "config-kstars.h" #include "fitsdata.h" #include "fitsdebayer.h" #include "fitstab.h" #include "fitsview.h" #include "kstars.h" #include "ksutils.h" #include "Options.h" #ifdef HAVE_INDI #include "indi/indilistener.h" #endif #include #include #include #include #ifndef KSTARS_LITE #include "fitshistogram.h" #endif #include #define INITIAL_W 785 #define INITIAL_H 640 QStringList FITSViewer::filterTypes = QStringList() << I18N_NOOP("Auto Stretch") << I18N_NOOP("High Contrast") << I18N_NOOP("Equalize") << I18N_NOOP("High Pass") << I18N_NOOP("Median") << I18N_NOOP("Rotate Right") << I18N_NOOP("Rotate Left") << I18N_NOOP("Flip Horizontal") << I18N_NOOP("Flip Vertical"); FITSViewer::FITSViewer(QWidget *parent) : KXmlGuiWindow(parent) { #ifdef Q_OS_OSX if (Options::independentWindowFITS()) setWindowFlags(Qt::Window); else { setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); connect(QApplication::instance(), SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(changeAlwaysOnTop(Qt::ApplicationState))); } #endif fitsTabWidget = new QTabWidget(this); undoGroup = new QUndoGroup(this); lastURL = QUrl(QDir::homePath()); fitsTabWidget->setTabsClosable(true); setWindowIcon(QIcon::fromTheme("kstars_fitsviewer")); setCentralWidget(fitsTabWidget); connect(fitsTabWidget, SIGNAL(currentChanged(int)), this, SLOT(tabFocusUpdated(int))); connect(fitsTabWidget, SIGNAL(tabCloseRequested(int)), this, SLOT(closeTab(int))); //These two connections will enable or disable the scope button if a scope is available or not. //Of course this is also dependent on the presence of WCS data in the image. #ifdef HAVE_INDI connect(INDIListener::Instance(), SIGNAL(newTelescope(ISD::GDInterface*)), this, SLOT(updateWCSFunctions())); connect(INDIListener::Instance(), SIGNAL(deviceRemoved(ISD::GDInterface*)), this, SLOT(updateWCSFunctions())); #endif led.setColor(Qt::green); fitsPosition.setAlignment(Qt::AlignCenter); fitsValue.setAlignment(Qt::AlignCenter); //fitsPosition.setFixedWidth(100); //fitsValue.setFixedWidth(100); fitsWCS.setVisible(false); statusBar()->insertPermanentWidget(FITS_WCS, &fitsWCS); statusBar()->insertPermanentWidget(FITS_VALUE, &fitsValue); statusBar()->insertPermanentWidget(FITS_POSITION, &fitsPosition); statusBar()->insertPermanentWidget(FITS_ZOOM, &fitsZoom); statusBar()->insertPermanentWidget(FITS_RESOLUTION, &fitsResolution); statusBar()->insertPermanentWidget(FITS_LED, &led); QAction *action = actionCollection()->addAction("rotate_right", this, SLOT(rotateCW())); action->setText(i18n("Rotate Right")); action->setIcon(QIcon::fromTheme("object-rotate-right")); action = actionCollection()->addAction("rotate_left", this, SLOT(rotateCCW())); action->setText(i18n("Rotate Left")); action->setIcon(QIcon::fromTheme("object-rotate-left")); action = actionCollection()->addAction("flip_horizontal", this, SLOT(flipHorizontal())); action->setText(i18n("Flip Horizontal")); action->setIcon( QIcon::fromTheme("object-flip-horizontal")); action = actionCollection()->addAction("flip_vertical", this, SLOT(flipVertical())); action->setText(i18n("Flip Vertical")); action->setIcon(QIcon::fromTheme("object-flip-vertical")); action = actionCollection()->addAction("image_histogram"); action->setText(i18n("Histogram")); connect(action, SIGNAL(triggered(bool)), SLOT(histoFITS())); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_T)); action->setIcon(QIcon(":/icons/histogram.png")); action = KStandardAction::open(this, SLOT(openFile()), actionCollection()); action->setIcon(QIcon::fromTheme("document-open")); saveFileAction = KStandardAction::save(this, SLOT(saveFile()), actionCollection()); saveFileAction->setIcon(QIcon::fromTheme("document-save")); saveFileAsAction = KStandardAction::saveAs(this, SLOT(saveFileAs()), actionCollection()); saveFileAsAction->setIcon( QIcon::fromTheme("document-save_as")); action = actionCollection()->addAction("fits_header"); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_H)); action->setIcon(QIcon::fromTheme("document-properties")); action->setText(i18n("FITS Header")); connect(action, SIGNAL(triggered(bool)), SLOT(headerFITS())); action = actionCollection()->addAction("fits_debayer"); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_D)); action->setIcon(QIcon::fromTheme("view-preview")); action->setText(i18n("Debayer...")); connect(action, SIGNAL(triggered(bool)), SLOT(debayerFITS())); action = actionCollection()->addAction("image_stretch"); action->setText(i18n("Auto stretch")); connect(action, SIGNAL(triggered(bool)), SLOT(stretchFITS())); actionCollection()->setDefaultShortcut(action, QKeySequence::SelectAll); action->setIcon(QIcon::fromTheme("transform-move")); action = KStandardAction::close(this, SLOT(close()), actionCollection()); action->setIcon(QIcon::fromTheme("window-close")); action = KStandardAction::copy(this, SLOT(copyFITS()), actionCollection()); action->setIcon(QIcon::fromTheme("edit-copy")); action = KStandardAction::zoomIn(this, SLOT(ZoomIn()), actionCollection()); action->setIcon(QIcon::fromTheme("zoom-in")); action = KStandardAction::zoomOut(this, SLOT(ZoomOut()), actionCollection()); action->setIcon(QIcon::fromTheme("zoom-out")); action = KStandardAction::actualSize(this, SLOT(ZoomDefault()), actionCollection()); action->setIcon(QIcon::fromTheme("zoom-fit-best")); QAction *kundo = KStandardAction::undo(undoGroup, SLOT(undo()), actionCollection()); kundo->setIcon(QIcon::fromTheme("edit-undo")); QAction *kredo = KStandardAction::redo(undoGroup, SLOT(redo()), actionCollection()); kredo->setIcon(QIcon::fromTheme("edit-redo")); connect(undoGroup, SIGNAL(canUndoChanged(bool)), kundo, SLOT(setEnabled(bool))); connect(undoGroup, SIGNAL(canRedoChanged(bool)), kredo, SLOT(setEnabled(bool))); action = actionCollection()->addAction("image_stats"); action->setIcon(QIcon::fromTheme("view-statistics")); action->setText(i18n("Statistics")); connect(action, SIGNAL(triggered(bool)), SLOT(statFITS())); action = actionCollection()->addAction("view_crosshair"); action->setIcon(QIcon::fromTheme("crosshairs")); action->setText(i18n("Show Cross Hairs")); action->setCheckable(true); connect(action, SIGNAL(triggered(bool)), SLOT(toggleCrossHair())); action = actionCollection()->addAction("view_pixel_grid"); action->setIcon(QIcon::fromTheme("map-flat")); action->setText(i18n("Show Pixel Gridlines")); action->setCheckable(true); connect(action, SIGNAL(triggered(bool)), SLOT(togglePixelGrid())); action = actionCollection()->addAction("view_eq_grid"); action->setIcon(QIcon::fromTheme("kstars_grid")); action->setText(i18n("Show Equatorial Gridlines")); action->setCheckable(true); action->setDisabled(true); connect(action, SIGNAL(triggered(bool)), SLOT(toggleEQGrid())); action = actionCollection()->addAction("view_objects"); action->setIcon(QIcon::fromTheme("help-hint")); action->setText(i18n("Show Objects in Image")); action->setCheckable(true); action->setDisabled(true); connect(action, SIGNAL(triggered(bool)), SLOT(toggleObjects())); action = actionCollection()->addAction("center_telescope"); action->setIcon(QIcon(":/icons/center_telescope.svg")); action->setText(i18n("Center Telescope\n*No Telescopes Detected*")); action->setDisabled(true); action->setCheckable(true); connect(action, SIGNAL(triggered(bool)), SLOT(centerTelescope())); action = actionCollection()->addAction("view_zoom_fit"); action->setIcon(QIcon::fromTheme("zoom-fit-width")); action->setText(i18n("Zoom To Fit")); connect(action, SIGNAL(triggered(bool)), SLOT(ZoomToFit())); #ifdef HAVE_DATAVISUALIZATION action = actionCollection()->addAction("toggle_3D_graph"); action->setIcon(QIcon::fromTheme("star_profile", QIcon(":/icons/star_profile.svg"))); action->setText(i18n("View 3D Graph")); action->setCheckable(true); connect(action, SIGNAL(triggered(bool)), SLOT(toggle3DGraph())); #endif action = actionCollection()->addAction("mark_stars"); action->setText(i18n("Mark Stars")); connect(action, SIGNAL(triggered(bool)), SLOT(toggleStars())); QSignalMapper *filterMapper = new QSignalMapper(this); int filterCounter = 1; for (auto& filter : FITSViewer::filterTypes) { action = actionCollection()->addAction(QString("filter%1").arg(filterCounter)); action->setText(i18n(filter.toUtf8().constData())); filterMapper->setMapping(action, filterCounter++); connect(action, SIGNAL(triggered()), filterMapper, SLOT(map())); } connect(filterMapper, SIGNAL(mapped(int)), this, SLOT(applyFilter(int))); /* Create GUI */ createGUI("fitsviewerui.rc"); setWindowTitle(i18n("KStars FITS Viewer")); /* initially resize in accord with KDE rules */ show(); resize(INITIAL_W, INITIAL_H); } void FITSViewer::changeAlwaysOnTop(Qt::ApplicationState state) { if (isVisible()) { if (state == Qt::ApplicationActive) setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); else setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint); show(); } } FITSViewer::~FITSViewer() { // if (KStars::Instance()) // { // for (QPointer fv : KStars::Instance()->getFITSViewersList()) // { // if (fv.data() == this) // { // KStars::Instance()->getFITSViewersList().removeOne(this); // break; // } // } // } fitsTabWidget->disconnect(); qDeleteAll(fitsTabs); fitsTabs.clear(); } void FITSViewer::closeEvent(QCloseEvent * /*event*/) { KStars *ks = KStars::Instance(); if (ks) { QAction *a = KStars::Instance()->actionCollection()->action("show_fits_viewer"); QList viewers = KStars::Instance()->findChildren(); if (a && viewers.count() == 1) { a->setEnabled(false); a->setChecked(false); } } } void FITSViewer::hideEvent(QHideEvent * /*event*/) { KStars *ks = KStars::Instance(); if (ks) { QAction *a = KStars::Instance()->actionCollection()->action("show_fits_viewer"); if (a) { QList viewers = KStars::Instance()->findChildren(); if (viewers.count() <= 1) a->setChecked(false); } } } void FITSViewer::showEvent(QShowEvent * /*event*/) { QAction *a = KStars::Instance()->actionCollection()->action("show_fits_viewer"); if (a) { a->setEnabled(true); a->setChecked(true); } } void FITSViewer::addFITS(const QUrl &imageName, FITSMode mode, FITSScale filter, const QString &previewText, bool silent) { led.setColor(Qt::yellow); QApplication::setOverrideCursor(Qt::WaitCursor); FITSTab *tab = new FITSTab(this); connect(tab, &FITSTab::failed, [&]() { QApplication::restoreOverrideCursor(); led.setColor(Qt::red); if (fitsTabs.size() == 0) { // Close FITS Viewer and let KStars know it is no longer needed in memory. close(); } emit failed(); }); connect(tab, &FITSTab::loaded, [=]() { int tabIndex = fitsTabWidget->indexOf(tab); if (tabIndex != -1) return; lastURL = QUrl(imageName.url(QUrl::RemoveFilename)); QApplication::restoreOverrideCursor(); tab->setPreviewText(previewText); // Connect tab signals connect(tab, &FITSTab::newStatus, this, &FITSViewer::updateStatusBar); connect(tab, &FITSTab::changeStatus, this, &FITSViewer::updateTabStatus); connect(tab, &FITSTab::debayerToggled, this, &FITSViewer::setDebayerAction); // Connect tab view signals connect(tab->getView(), &FITSView::actionUpdated, this, &FITSViewer::updateAction); connect(tab->getView(), &FITSView::wcsToggled, this, &FITSViewer::updateWCSFunctions); + connect(tab->getView(),&FITSView::starProfileWindowClosed, this, &FITSViewer::starProfileButtonOff); switch (mode) { case FITS_NORMAL: fitsTabWidget->addTab(tab, previewText.isEmpty() ? imageName.fileName() : previewText); break; case FITS_CALIBRATE: fitsTabWidget->addTab(tab, i18n("Calibrate")); break; case FITS_FOCUS: fitsTabWidget->addTab(tab, i18n("Focus")); break; case FITS_GUIDE: fitsTabWidget->addTab(tab, i18n("Guide")); break; case FITS_ALIGN: fitsTabWidget->addTab(tab, i18n("Align")); break; } saveFileAction->setEnabled(true); saveFileAsAction->setEnabled(true); undoGroup->addStack(tab->getUndoStack()); fitsTabs.push_back(tab); fitsMap[fitsID] = tab; fitsTabWidget->setCurrentWidget(tab); actionCollection()->action("fits_debayer")->setEnabled(tab->getView()->getImageData()->hasDebayer()); tab->tabPositionUpdated(); tab->setUID(fitsID); led.setColor(Qt::green); updateStatusBar(i18n("Ready."), FITS_MESSAGE); tab->getView()->setCursorMode(FITSView::dragCursor); updateWCSFunctions(); emit loaded(fitsID++); }); tab->loadFITS(imageName, mode, filter, silent); } bool FITSViewer::removeFITS(int fitsUID) { FITSTab *tab = fitsMap.value(fitsUID); if (tab == nullptr) { qCWarning(KSTARS_FITS) << "Cannot find tab with UID " << fitsUID << " in the FITS Viewer"; return false; } int index = fitsTabs.indexOf(tab); if (index >= 0) { closeTab(index); return true; } return false; } void FITSViewer::updateFITS(const QUrl &imageName, int fitsUID, FITSScale filter, bool silent) { FITSTab *tab = fitsMap.value(fitsUID); if (tab == nullptr) { qCWarning(KSTARS_FITS) << "Cannot find tab with UID " << fitsUID << " in the FITS Viewer"; emit failed(); return; } if (tab->isVisible()) led.setColor(Qt::yellow); // On tab load success auto conn = std::make_shared(); *conn = connect(tab, &FITSTab::loaded, this, [=]() { int tabIndex = fitsTabWidget->indexOf(tab); if (tabIndex == -1) return; if (tab->getView()->getMode() == FITS_NORMAL) { if ((imageName.path().startsWith(QLatin1String("/tmp")) || imageName.path().contains("/Temp")) && Options::singlePreviewFITS()) fitsTabWidget->setTabText(tabIndex, tab->getPreviewText().isEmpty() ? i18n("Preview") : tab->getPreviewText()); else fitsTabWidget->setTabText(tabIndex, imageName.fileName()); } tab->getUndoStack()->clear(); if (tab->isVisible()) led.setColor(Qt::green); QObject::disconnect(*conn); emit loaded(tabIndex); }); tab->loadFITS(imageName, tab->getView()->getMode(), filter, silent); } void FITSViewer::tabFocusUpdated(int currentIndex) { if (currentIndex < 0 || fitsTabs.empty()) return; fitsTabs[currentIndex]->tabPositionUpdated(); FITSView *view = fitsTabs[currentIndex]->getView(); view->toggleStars(markStars); if (isVisible()) view->updateFrame(); if (markStars) updateStatusBar(i18np("%1 star detected.", "%1 stars detected.", view->getImageData()->getDetectedStars()), FITS_MESSAGE); else updateStatusBar("", FITS_MESSAGE); if (view->getImageData()->hasDebayer()) { actionCollection()->action("fits_debayer")->setEnabled(true); if (debayerDialog) { BayerParams param; view->getImageData()->getBayerParams(¶m); debayerDialog->setBayerParams(¶m); } } else actionCollection()->action("fits_debayer")->setEnabled(false); updateStatusBar("", FITS_WCS); - updateButtonStatus("toggle_3D_graph", "View 3D Graph", getCurrentView()->isStarProfileShown()); - updateButtonStatus("view_crosshair", "Cross Hairs", getCurrentView()->isCrosshairShown()); - updateButtonStatus("view_eq_grid", "Equatorial Gridines", getCurrentView()->isEQGridShown()); - updateButtonStatus("view_objects", "Objects in Image", getCurrentView()->areObjectsShown()); - updateButtonStatus("view_pixel_grid", "Pixel Gridines", getCurrentView()->isPixelGridShown()); + connect(view,&FITSView::starProfileWindowClosed, this, &FITSViewer::starProfileButtonOff); + updateButtonStatus("toggle_3D_graph", i18n("View 3D Graph"), getCurrentView()->isStarProfileShown()); + updateButtonStatus("view_crosshair", i18n("Cross Hairs"), getCurrentView()->isCrosshairShown()); + updateButtonStatus("view_eq_grid", i18n("Equatorial Gridines"), getCurrentView()->isEQGridShown()); + updateButtonStatus("view_objects", i18n("Objects in Image"), getCurrentView()->areObjectsShown()); + updateButtonStatus("view_pixel_grid", i18n("Pixel Gridines"), getCurrentView()->isPixelGridShown()); updateScopeButton(); updateWCSFunctions(); } +void FITSViewer::starProfileButtonOff() +{ + updateButtonStatus("toggle_3D_graph", i18n("View 3D Graph"), false); +} + void FITSViewer::openFile() { QUrl fileURL = QFileDialog::getOpenFileUrl(KStars::Instance(), i18n("Open FITS Image"), lastURL, "FITS (*.fits *.fit)"); if (fileURL.isEmpty()) return; lastURL = QUrl(fileURL.url(QUrl::RemoveFilename)); QString fpath = fileURL.toLocalFile(); QString cpath; // Make sure we don't have it open already, if yes, switch to it foreach (FITSTab *tab, fitsTabs) { cpath = tab->getCurrentURL()->path(); if (fpath == cpath) { fitsTabWidget->setCurrentWidget(tab); return; } } addFITS(fileURL, FITS_NORMAL, FITS_NONE, QString(), false); } void FITSViewer::saveFile() { fitsTabs[fitsTabWidget->currentIndex()]->saveFile(); } void FITSViewer::saveFileAs() { if (fitsTabs.empty()) return; if (fitsTabs[fitsTabWidget->currentIndex()]->saveFileAs() && fitsTabs[fitsTabWidget->currentIndex()]->getView()->getMode() == FITS_NORMAL) fitsTabWidget->setTabText(fitsTabWidget->currentIndex(), fitsTabs[fitsTabWidget->currentIndex()]->getCurrentURL()->fileName()); } void FITSViewer::copyFITS() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->copyFITS(); } void FITSViewer::histoFITS() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->histoFITS(); } void FITSViewer::statFITS() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->statFITS(); } void FITSViewer::stretchFITS() { applyFilter(FITS_AUTO_STRETCH); } void FITSViewer::rotateCW() { applyFilter(FITS_ROTATE_CW); } void FITSViewer::rotateCCW() { applyFilter(FITS_ROTATE_CCW); } void FITSViewer::flipHorizontal() { applyFilter(FITS_FLIP_H); } void FITSViewer::flipVertical() { applyFilter(FITS_FLIP_V); } void FITSViewer::headerFITS() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->headerFITS(); } void FITSViewer::debayerFITS() { if (debayerDialog == nullptr) { debayerDialog = new FITSDebayer(this); } FITSView *view = getCurrentView(); if (view == nullptr) return; BayerParams param; view->getImageData()->getBayerParams(¶m); debayerDialog->setBayerParams(¶m); debayerDialog->show(); } void FITSViewer::updateStatusBar(const QString &msg, FITSBar id) { switch (id) { case FITS_POSITION: fitsPosition.setText(msg); break; case FITS_RESOLUTION: fitsResolution.setText(msg); break; case FITS_ZOOM: fitsZoom.setText(msg); break; case FITS_WCS: fitsWCS.setVisible(true); fitsWCS.setText(msg); break; case FITS_VALUE: fitsValue.setText(msg); break; case FITS_MESSAGE: statusBar()->showMessage(msg); break; default: break; } } void FITSViewer::ZoomIn() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->ZoomIn(); } void FITSViewer::ZoomOut() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->ZoomOut(); } void FITSViewer::ZoomDefault() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->ZoomDefault(); } void FITSViewer::ZoomToFit() { if (fitsTabs.empty()) return; getCurrentView()->ZoomToFit(); } void FITSViewer::updateAction(const QString &name, bool enable) { QAction *toolAction = actionCollection()->action(name); if (toolAction != nullptr) toolAction->setEnabled(enable); } void FITSViewer::updateTabStatus(bool clean) { if (fitsTabs.empty() || (fitsTabWidget->currentIndex() >= fitsTabs.size())) return; if (fitsTabs[fitsTabWidget->currentIndex()]->getView()->getMode() != FITS_NORMAL) return; //QString tabText = fitsImages[fitsTab->currentIndex()]->getCurrentURL()->fileName(); QString tabText = fitsTabWidget->tabText(fitsTabWidget->currentIndex()); fitsTabWidget->setTabText(fitsTabWidget->currentIndex(), clean ? tabText.remove('*') : tabText + '*'); } void FITSViewer::closeTab(int index) { if (fitsTabs.empty()) return; FITSTab *tab = fitsTabs[index]; fitsMap.remove(tab->getUID()); fitsTabs.removeOne(tab); delete tab; if (fitsTabs.empty()) { saveFileAction->setEnabled(false); saveFileAsAction->setEnabled(false); } } /** This is helper function to make it really easy to make the update the state of toggle buttons that either show or hide information in the Current view. This method would get called both when one of them gets pushed and also when tabs are switched. */ void FITSViewer::updateButtonStatus(const QString& action, const QString& item, bool showing) { QAction *a = actionCollection()->action(action); if (a == nullptr) return; if (showing) { a->setText("Hide " + item); a->setChecked(true); } else { a->setText("Show " + item); a->setChecked(false); } } /** This is a method that either enables or disables the WCS based features in the Current View. */ void FITSViewer::updateWCSFunctions() { if (getCurrentView() == nullptr) return; if (getCurrentView()->imageHasWCS()) { actionCollection()->action("view_eq_grid")->setDisabled(false); actionCollection()->action("view_eq_grid")->setText(i18n("Show Equatorial Gridlines")); actionCollection()->action("view_objects")->setDisabled(false); actionCollection()->action("view_objects")->setText(i18n("Show Objects in Image")); if (getCurrentView()->isTelescopeActive()) { actionCollection()->action("center_telescope")->setDisabled(false); actionCollection()->action("center_telescope")->setText(i18n("Center Telescope\n*Ready*")); } else { actionCollection()->action("center_telescope")->setDisabled(true); actionCollection()->action("center_telescope")->setText(i18n("Center Telescope\n*No Telescopes Detected*")); } } else { actionCollection()->action("view_eq_grid")->setDisabled(true); actionCollection()->action("view_eq_grid")->setText(i18n("Show Equatorial Gridlines\n*No WCS Info*")); actionCollection()->action("center_telescope")->setDisabled(true); actionCollection()->action("center_telescope")->setText(i18n("Center Telescope\n*No WCS Info*")); actionCollection()->action("view_objects")->setDisabled(true); actionCollection()->action("view_objects")->setText(i18n("Show Objects in Image\n*No WCS Info*")); } } void FITSViewer::updateScopeButton() { if (getCurrentView()->getCursorMode() == FITSView::scopeCursor) { actionCollection()->action("center_telescope")->setChecked(true); } else { actionCollection()->action("center_telescope")->setChecked(false); } } /** This methood either enables or disables the scope mouse mode so you can slew your scope to coordinates just by clicking the mouse on a spot in the image. */ void FITSViewer::centerTelescope() { getCurrentView()->setScopeButton(actionCollection()->action("center_telescope")); if (getCurrentView()->getCursorMode() == FITSView::scopeCursor) { getCurrentView()->setCursorMode(getCurrentView()->lastMouseMode); } else { getCurrentView()->lastMouseMode = getCurrentView()->getCursorMode(); getCurrentView()->setCursorMode(FITSView::scopeCursor); } updateScopeButton(); } void FITSViewer::toggleCrossHair() { if (fitsTabs.empty()) return; getCurrentView()->toggleCrosshair(); updateButtonStatus("view_crosshair", "Cross Hairs", getCurrentView()->isCrosshairShown()); } void FITSViewer::toggleEQGrid() { if (fitsTabs.empty()) return; getCurrentView()->toggleEQGrid(); updateButtonStatus("view_eq_grid", "Equatorial Gridines", getCurrentView()->isEQGridShown()); } void FITSViewer::toggleObjects() { if (fitsTabs.empty()) return; getCurrentView()->toggleObjects(); updateButtonStatus("view_objects", "Objects in Image", getCurrentView()->areObjectsShown()); } void FITSViewer::togglePixelGrid() { if (fitsTabs.empty()) return; getCurrentView()->togglePixelGrid(); updateButtonStatus("view_pixel_grid", "Pixel Gridines", getCurrentView()->isPixelGridShown()); } void FITSViewer::toggle3DGraph() { if (fitsTabs.empty()) return; getCurrentView()->toggleStarProfile(); updateButtonStatus("toggle_3D_graph", "View 3D Graph", getCurrentView()->isStarProfileShown()); } void FITSViewer::toggleStars() { if (markStars) { markStars = false; actionCollection()->action("mark_stars")->setText(i18n("Mark Stars")); } else { markStars = true; actionCollection()->action("mark_stars")->setText(i18n("Unmark Stars")); } foreach (FITSTab *tab, fitsTabs) { tab->getView()->toggleStars(markStars); tab->getView()->updateFrame(); } } void FITSViewer::applyFilter(int ftype) { if (fitsTabs.empty()) return; QApplication::setOverrideCursor(Qt::WaitCursor); updateStatusBar(i18n("Processing %1...", filterTypes[ftype - 1]), FITS_MESSAGE); qApp->processEvents(); fitsTabs[fitsTabWidget->currentIndex()]->getHistogram()->applyFilter(static_cast(ftype)); qApp->processEvents(); fitsTabs[fitsTabWidget->currentIndex()]->getView()->updateFrame(); QApplication::restoreOverrideCursor(); updateStatusBar(i18n("Ready."), FITS_MESSAGE); } FITSView *FITSViewer::getView(int fitsUID) { FITSTab *tab = fitsMap.value(fitsUID); if (tab) return tab->getView(); return nullptr; } FITSView *FITSViewer::getCurrentView() { if (fitsTabs.empty() || fitsTabWidget->currentIndex() >= fitsTabs.count()) return nullptr; return fitsTabs[fitsTabWidget->currentIndex()]->getView(); } void FITSViewer::setDebayerAction(bool enable) { actionCollection()->addAction("fits_debayer")->setEnabled(enable); } diff --git a/kstars/fitsviewer/fitsviewer.h b/kstars/fitsviewer/fitsviewer.h index 8252a4db5..642217a9e 100644 --- a/kstars/fitsviewer/fitsviewer.h +++ b/kstars/fitsviewer/fitsviewer.h @@ -1,141 +1,142 @@ /*************************************************************************** FITSViewer.cpp - A FITSViewer for KStars ------------------- begin : Thu Jan 22 2004 copyright : (C) 2004 by Jasem Mutlaq email : mutlaqja@ikarustech.com ***************************************************************************/ /*************************************************************************** * * * This program 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. * * * * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* * See http://members.aol.com/pkirchg for more details. * ***************************************************************************/ #pragma once #include "fitscommon.h" #include #include #include #include #include #include #ifdef WIN32 // avoid compiler warning when windows.h is included after fitsio.h #include #endif #include class QCloseEvent; class QUndoGroup; class QTabWidget; class FITSDebayer; class FITSTab; class FITSView; /** * @class FITSViewer * @short Primary window to view monochrome and color FITS images. * The FITSviewer can open multiple images each in a separate. It supports simple filters, histogram transforms, flip and rotation operations, and star detection. * * @author Jasem Mutlaq * @version 1.0 */ class FITSViewer : public KXmlGuiWindow { Q_OBJECT public: /** Constructor. */ explicit FITSViewer(QWidget *parent); ~FITSViewer(); void addFITS(const QUrl &imageName, FITSMode mode = FITS_NORMAL, FITSScale filter = FITS_NONE, const QString &previewText = QString(), bool silent = true); void updateFITS(const QUrl &imageName, int fitsUID, FITSScale filter = FITS_NONE, bool silent = true); bool removeFITS(int fitsUID); bool isStarsMarked() { return markStars; } bool empty() const { return fitsTabs.empty(); } QList getTabs() { return fitsTabs; } FITSView *getView(int fitsUID); FITSView *getCurrentView(); static QStringList filterTypes; protected: void closeEvent(QCloseEvent *); void hideEvent(QHideEvent *); void showEvent(QShowEvent *); public slots: void changeAlwaysOnTop(Qt::ApplicationState state); void openFile(); void saveFile(); void saveFileAs(); void copyFITS(); void statFITS(); void headerFITS(); void debayerFITS(); void histoFITS(); void stretchFITS(); void tabFocusUpdated(int currentIndex); void updateStatusBar(const QString &msg, FITSBar id); void ZoomIn(); void ZoomOut(); void ZoomDefault(); void ZoomToFit(); void updateAction(const QString &name, bool enable); void updateTabStatus(bool clean); void closeTab(int index); void toggleStars(); void toggleCrossHair(); void toggleEQGrid(); void toggleObjects(); void togglePixelGrid(); void toggle3DGraph(); + void starProfileButtonOff(); void centerTelescope(); void updateWCSFunctions(); void applyFilter(int ftype); void rotateCW(); void rotateCCW(); void flipHorizontal(); void flipVertical(); void setDebayerAction(bool); void updateScopeButton(); private: void updateButtonStatus(const QString &action, const QString &item, bool showing); QTabWidget *fitsTabWidget { nullptr }; QUndoGroup *undoGroup { nullptr }; FITSDebayer *debayerDialog { nullptr }; KLed led; QLabel fitsPosition, fitsValue, fitsResolution, fitsZoom, fitsWCS; QAction *saveFileAction { nullptr }; QAction *saveFileAsAction { nullptr }; QList fitsTabs; int fitsID { 0 }; bool markStars { false }; QMap fitsMap; QUrl lastURL; signals: void trackingStarSelected(int x, int y); void loaded(int tabIndex); void failed(); };