1 /*
2     SPDX-FileCopyrightText: 2015 Jasem Mutlaq <mutlaqja@ikarustech.com>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "fitshistogram.h"
8 
9 #include "fits_debug.h"
10 
11 #include "Options.h"
12 #include "fitsdata.h"
13 #include "fitstab.h"
14 #include "fitsview.h"
15 #include "fitsviewer.h"
16 
17 #include <KMessageBox>
18 
19 #include <QtConcurrent>
20 #include <type_traits>
21 #include <zlib.h>
22 
histogramUI(QDialog * parent)23 histogramUI::histogramUI(QDialog * parent) : QDialog(parent)
24 {
25     setupUi(parent);
26     setModal(false);
27 }
28 
FITSHistogram(QWidget * parent)29 FITSHistogram::FITSHistogram(QWidget * parent) : QDialog(parent)
30 {
31     ui = new histogramUI(this);
32     tab = dynamic_cast<FITSTab *>(parent);
33 
34     customPlot = ui->histogramPlot;
35 
36     customPlot->setBackground(QBrush(Qt::black));
37 
38     customPlot->xAxis->setBasePen(QPen(Qt::white, 1));
39     customPlot->yAxis->setBasePen(QPen(Qt::white, 1));
40 
41     customPlot->xAxis->setTickPen(QPen(Qt::white, 1));
42     customPlot->yAxis->setTickPen(QPen(Qt::white, 1));
43 
44     customPlot->xAxis->setSubTickPen(QPen(Qt::white, 1));
45     customPlot->yAxis->setSubTickPen(QPen(Qt::white, 1));
46 
47     customPlot->xAxis->setTickLabelColor(Qt::white);
48     customPlot->yAxis->setTickLabelColor(Qt::white);
49 
50     customPlot->xAxis->setLabelColor(Qt::white);
51     customPlot->yAxis->setLabelColor(Qt::white);
52 
53     // Reserve 3 channels
54     cumulativeFrequency.resize(3);
55     intensity.resize(3);
56     frequency.resize(3);
57 
58     FITSMin.fill(0, 3);
59     FITSMax.fill(0, 3);
60     binWidth.fill(0, 3);
61 
62     rgbWidgets.resize(3);
63     rgbWidgets[RED_CHANNEL] << ui->RLabel << ui->minREdit << ui->redSlider
64                             << ui->maxREdit;
65     rgbWidgets[GREEN_CHANNEL] << ui->GLabel << ui->minGEdit << ui->greenSlider
66                               << ui->maxGEdit;
67     rgbWidgets[BLUE_CHANNEL] << ui->BLabel << ui->minBEdit << ui->blueSlider
68                              << ui->maxBEdit;
69 
70     minBoxes << ui->minREdit << ui->minGEdit << ui->minBEdit;
71     maxBoxes << ui->maxREdit << ui->maxGEdit << ui->maxBEdit;
72     sliders << ui->redSlider << ui->greenSlider << ui->blueSlider;
73 
74     customPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
75     customPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
76     customPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
77     customPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
78     customPlot->xAxis->grid()->setZeroLinePen(Qt::NoPen);
79     customPlot->yAxis->grid()->setZeroLinePen(Qt::NoPen);
80 
81     connect(ui->applyB, &QPushButton::clicked, this, &FITSHistogram::applyScale);
82     connect(ui->hideSaturated, &QCheckBox::stateChanged, [this]()
83     {
84         constructHistogram();
85     });
86 
87     //    connect(customPlot->xAxis, SIGNAL(rangeChanged(QCPRange)), this,
88     //            SLOT(checkRangeLimit(QCPRange)));
89     connect(customPlot, &QCustomPlot::mouseMove, this,
90             &FITSHistogram::driftMouseOverLine);
91 
92     for (int i = 0; i < 3; i++)
93     {
94         // Box --> Slider
95         QVector<QWidget *> w = rgbWidgets[i];
96         connect(qobject_cast<QDoubleSpinBox *>(w[1]), &QDoubleSpinBox::editingFinished, [this, i, w]()
97         {
98             double value = qobject_cast<QDoubleSpinBox *>(w[1])->value();
99             w[2]->blockSignals(true);
100             qobject_cast<ctkRangeSlider *>(w[2])->setMinimumPosition((value - FITSMin[i])*sliderScale[i]);
101             w[2]->blockSignals(false);
102         });
103         connect(qobject_cast<QDoubleSpinBox *>(w[3]), &QDoubleSpinBox::editingFinished, [this, i, w]()
104         {
105             double value = qobject_cast<QDoubleSpinBox *>(w[3])->value();
106             w[2]->blockSignals(true);
107             qobject_cast<ctkRangeSlider *>(w[2])->setMaximumPosition((value - FITSMin[i] - sliderTick[i])*sliderScale[i]);
108             w[2]->blockSignals(false);
109         });
110 
111         // Slider --> Box
112         connect(qobject_cast<ctkRangeSlider *>(w[2]), &ctkRangeSlider::minimumValueChanged, [this, i, w](int position)
113         {
114             qobject_cast<QDoubleSpinBox *>(w[1])->setValue(FITSMin[i] + (position / sliderScale[i]));
115         });
116         connect(qobject_cast<ctkRangeSlider *>(w[2]), &ctkRangeSlider::maximumValueChanged, [this, i, w](int position)
117         {
118             qobject_cast<QDoubleSpinBox *>(w[3])->setValue(FITSMin[i] + sliderTick[i] + (position / sliderScale[i]));
119         });
120     }
121 
122 }
123 
showEvent(QShowEvent * event)124 void FITSHistogram::showEvent(QShowEvent * event)
125 {
126     Q_UNUSED(event)
127     if (!m_Constructed)
128         constructHistogram();
129     syncGUI();
130 }
131 
constructHistogram()132 void FITSHistogram::constructHistogram()
133 {
134     const QSharedPointer<FITSData> &imageData = tab->getView()->imageData();
135 
136     isGUISynced = false;
137 
138     switch (imageData->getStatistics().dataType)
139     {
140         case TBYTE:
141             constructHistogram<uint8_t>();
142             break;
143 
144         case TSHORT:
145             constructHistogram<int16_t>();
146             break;
147 
148         case TUSHORT:
149             constructHistogram<uint16_t>();
150             break;
151 
152         case TLONG:
153             constructHistogram<int32_t>();
154             break;
155 
156         case TULONG:
157             constructHistogram<uint32_t>();
158             break;
159 
160         case TFLOAT:
161             constructHistogram<float>();
162             break;
163 
164         case TLONGLONG:
165             constructHistogram<int64_t>();
166             break;
167 
168         case TDOUBLE:
169             constructHistogram<double>();
170             break;
171 
172         default:
173             break;
174     }
175 
176     m_Constructed = true;
177     if (isVisible())
178         syncGUI();
179 }
180 
constructHistogram()181 template <typename T> void FITSHistogram::constructHistogram()
182 {
183     const QSharedPointer<FITSData> &imageData = tab->getView()->imageData();
184     uint16_t width = imageData->width(), height = imageData->height();
185     uint8_t channels = imageData->channels();
186 
187     auto * const buffer = reinterpret_cast<T const *>(imageData->getImageBuffer());
188 
189     double min, max;
190     for (int i = 0 ; i < 3; i++)
191     {
192         imageData->getMinMax(&min, &max, i);
193         FITSMin[i] = min;
194         FITSMax[i] = max;
195     }
196 
197     uint32_t samples = width * height;
198     const uint32_t sampleBy = samples > 1000000 ? samples / 1000000 : 1;
199 
200     //binCount = static_cast<uint16_t>(sqrt(samples));
201     binCount = qMin(FITSMax[0] - FITSMin[0], 400.0);
202     if (binCount <= 0)
203         binCount = 400;
204 
205     for (int n = 0; n < channels; n++)
206     {
207         intensity[n].fill(0, binCount);
208         frequency[n].fill(0, binCount);
209         cumulativeFrequency[n].fill(0, binCount);
210         binWidth[n] = (FITSMax[n] - FITSMin[n]) / (binCount - 1);
211         // Initialize the median to 0 in case the computation below fails.
212         imageData->setMedian(0, n);
213     }
214 
215     QVector<QFuture<void>> futures;
216 
217     for (int n = 0; n < channels; n++)
218     {
219         futures.append(QtConcurrent::run([ = ]()
220         {
221             for (int i = 0; i < binCount; i++)
222                 intensity[n][i] = FITSMin[n] + (binWidth[n] * i);
223         }));
224     }
225 
226     for (int n = 0; n < channels; n++)
227     {
228         futures.append(QtConcurrent::run([ = ]()
229         {
230             uint32_t offset = n * samples;
231 
232             for (uint32_t i = 0; i < samples; i += sampleBy)
233             {
234                 int32_t id = rint((buffer[i + offset] - FITSMin[n]) / binWidth[n]);
235                 if (id < 0)
236                     id = 0;
237                 frequency[n][id] += sampleBy;
238             }
239         }));
240     }
241 
242     for (QFuture<void> future : futures)
243         future.waitForFinished();
244 
245     futures.clear();
246 
247     for (int n = 0; n < channels; n++)
248     {
249         futures.append(QtConcurrent::run([ = ]()
250         {
251             uint32_t accumulator = 0;
252             for (int i = 0; i < binCount; i++)
253             {
254                 accumulator += frequency[n][i];
255                 cumulativeFrequency[n].replace(i, accumulator);
256             }
257         }));
258     }
259 
260     for (QFuture<void> future : futures)
261         future.waitForFinished();
262 
263     futures.clear();
264 
265     for (int n = 0; n < channels; n++)
266     {
267         futures.append(QtConcurrent::run([ = ]()
268         {
269             double median[3] = {0};
270             const bool cutoffSpikes = ui->hideSaturated->isChecked();
271             const uint32_t halfCumulative = cumulativeFrequency[n][binCount - 1] / 2;
272 
273             // Find which bin contains the median.
274             int median_bin = -1;
275             for (int i = 0; i < binCount; i++)
276             {
277                 if (cumulativeFrequency[n][i] > halfCumulative)
278                 {
279                     median_bin = i;
280                     break;
281                 }
282             }
283 
284             if (median_bin >= 0)
285             {
286                 // The number of items in the median bin
287                 const uint32_t median_bin_size = frequency[n][median_bin] / sampleBy;
288                 if (median_bin_size > 0)
289                 {
290                     // The median is this element inside the sorted median_bin;
291                     const uint32_t samples_before_median_bin = median_bin == 0 ? 0 : cumulativeFrequency[n][median_bin - 1];
292                     uint32_t median_position = (halfCumulative - samples_before_median_bin) / sampleBy;
293 
294                     if (median_position >= median_bin_size)
295                         median_position = median_bin_size - 1;
296                     if (median_position >= 0 && median_position < median_bin_size)
297                     {
298                         // Fill a vector with the values in the median bin (sampled by sampleBy).
299                         std::vector<T> median_bin_samples(median_bin_size);
300                         uint32_t upto = 0;
301                         const uint32_t offset = n * samples;
302                         for (uint32_t i = 0; i < samples; i += sampleBy)
303                         {
304                             if (upto >= median_bin_size) break;
305                             const int32_t id = rint((buffer[i + offset] - FITSMin[n]) / binWidth[n]);
306                             if (id == median_bin)
307                                 median_bin_samples[upto++] = buffer[i + offset];
308                         }
309                         // Get the Nth value using N = the median position.
310                         if (upto > 0)
311                         {
312                             if (median_position >= upto) median_position = upto - 1;
313                             std::nth_element(median_bin_samples.begin(), median_bin_samples.begin() + median_position,
314                                              median_bin_samples.begin() + upto);
315                             median[n] = median_bin_samples[median_position];
316                         }
317                     }
318                 }
319             }
320 
321             imageData->setMedian(median[n], n);
322 
323             if (cutoffSpikes)
324             {
325                 QVector<double> sortedFreq = frequency[n];
326                 std::sort(sortedFreq.begin(), sortedFreq.end());
327                 double cutoff = sortedFreq[binCount * 0.99];
328                 for (int i = 0; i < binCount; i++)
329                 {
330                     if (frequency[n][i] >= cutoff)
331                         frequency[n][i] = cutoff;
332                 }
333             }
334 
335         }));
336     }
337 
338     for (QFuture<void> future : futures)
339         future.waitForFinished();
340 
341     // Custom index to indicate the overall contrast of the image
342     if (cumulativeFrequency[RED_CHANNEL][binCount / 4] > 0)
343         JMIndex = cumulativeFrequency[RED_CHANNEL][binCount / 8] / static_cast<double>(cumulativeFrequency[RED_CHANNEL][binCount /
344                   4]);
345     else
346         JMIndex = 1;
347     qCDebug(KSTARS_FITS) << "FITHistogram: JMIndex " << JMIndex;
348 
349     sliderTick.clear();
350     sliderScale.clear();
351     for (int n = 0; n < channels; n++)
352     {
353         sliderTick  << fabs(FITSMax[n] - FITSMin[n]) / 99.0;
354         sliderScale << 99.0 / (FITSMax[n] - FITSMin[n] - sliderTick[n]);
355     }
356 }
357 
syncGUI()358 void FITSHistogram::syncGUI()
359 {
360     if (isGUISynced)
361         return;
362 
363     const QSharedPointer<FITSData> &imageData = tab->getView()->imageData();
364     bool isColor = imageData->channels() > 1;
365     // R/K is always enabled
366     for (auto w : rgbWidgets[RED_CHANNEL])
367         w->setEnabled(true);
368     // G Channel
369     for (auto w : rgbWidgets[GREEN_CHANNEL])
370         w->setEnabled(isColor);
371     // B Channel
372     for (auto w : rgbWidgets[BLUE_CHANNEL])
373         w->setEnabled(isColor);
374 
375     ui->meanEdit->setText(QString::number(imageData->getMean()));
376     ui->medianEdit->setText(QString::number(imageData->getMedian()));
377 
378     for (int n = 0; n < imageData->channels(); n++)
379     {
380         double median = imageData->getMedian(n);
381 
382         if (median > 100)
383             numDecimals << 0;
384         else if (median > 1)
385             numDecimals << 2;
386         else if (median > .01)
387             numDecimals << 4;
388         else if (median > .0001)
389             numDecimals << 6;
390         else
391             numDecimals << 10;
392 
393         minBoxes[n]->setDecimals(numDecimals[n]);
394         minBoxes[n]->setSingleStep(fabs(FITSMax[n] - FITSMin[n]) / 20.0);
395         minBoxes[n]->setMinimum(FITSMin[n]);
396         minBoxes[n]->setMaximum(FITSMax[n] - sliderTick[n]);
397         minBoxes[n]->setValue(FITSMin[n] + (sliders[n]->minimumValue() / sliderScale[n]));
398 
399         maxBoxes[n]->setDecimals(numDecimals[n]);
400         maxBoxes[n]->setSingleStep(fabs(FITSMax[n] - FITSMin[n]) / 20.0);
401         maxBoxes[n]->setMinimum(FITSMin[n] + sliderTick[n]);
402         maxBoxes[n]->setMaximum(FITSMax[n]);
403         maxBoxes[n]->setValue(FITSMin[n] + sliderTick[n] + (sliders[n]->maximumValue() / sliderScale[n]));
404     }
405 
406     customPlot->clearGraphs();
407     graphs.clear();
408 
409     for (int n = 0; n < imageData->channels(); n++)
410     {
411         graphs.append(customPlot->addGraph());
412         graphs[n]->setData(intensity[n], frequency[n]);
413     }
414 
415     graphs[RED_CHANNEL]->setBrush(QBrush(QColor(170, 40, 80)));
416     graphs[RED_CHANNEL]->setPen(QPen(Qt::red));
417 
418     if (isColor)
419     {
420         graphs[GREEN_CHANNEL]->setBrush(QBrush(QColor(80, 40, 170)));
421         graphs[GREEN_CHANNEL]->setPen(QPen(Qt::green));
422 
423         graphs[BLUE_CHANNEL]->setBrush(QBrush(QColor(170, 40, 80)));
424         graphs[BLUE_CHANNEL]->setPen(QPen(Qt::blue));
425     }
426 
427     customPlot->axisRect(0)->setRangeDrag(Qt::Horizontal);
428     customPlot->axisRect(0)->setRangeZoom(Qt::Horizontal);
429 
430     customPlot->xAxis->setLabel(i18n("Intensity"));
431     customPlot->yAxis->setLabel(i18n("Frequency"));
432 
433     //    customPlot->xAxis->setRange(fits_min - ui->minEdit->singleStep(),
434     //                                fits_max + ui->maxEdit->singleStep());
435 
436     customPlot->xAxis->rescale();
437     customPlot->yAxis->rescale();
438 
439     customPlot->setInteraction(QCP::iRangeDrag, true);
440     customPlot->setInteraction(QCP::iRangeZoom, true);
441     customPlot->setInteraction(QCP::iSelectPlottables, true);
442 
443     customPlot->replot();
444     resizePlot();
445 
446     isGUISynced = true;
447 }
448 
resizePlot()449 void FITSHistogram::resizePlot()
450 {
451     if (!m_Constructed)
452         constructHistogram();
453 
454     if (customPlot->width() < 300)
455         customPlot->yAxis->setTickLabels(false);
456     else
457         customPlot->yAxis->setTickLabels(true);
458     customPlot->xAxis->ticker()->setTickCount(customPlot->width() / 100);
459 }
460 
getJMIndex() const461 double FITSHistogram::getJMIndex() const
462 {
463     return JMIndex;
464 }
465 
applyScale()466 void FITSHistogram::applyScale()
467 {
468     QVector<double> min, max;
469 
470     min << minBoxes[0]->value() << minBoxes[1]->value() <<  minBoxes[2]->value();
471     max << maxBoxes[0]->value() << maxBoxes[1]->value() << maxBoxes[2]->value();
472 
473     FITSHistogramCommand * histC;
474 
475     if (ui->logR->isChecked())
476         type = FITS_LOG;
477     else
478         type = FITS_LINEAR;
479 
480     histC = new FITSHistogramCommand(tab, this, type, min, max);
481 
482     tab->getUndoStack()->push(histC);
483 }
484 
applyFilter(FITSScale ftype)485 void FITSHistogram::applyFilter(FITSScale ftype)
486 {
487     QVector<double> min, max;
488 
489     min.append(ui->minREdit->value());
490 
491     FITSHistogramCommand * histC;
492 
493     type = ftype;
494 
495     histC = new FITSHistogramCommand(tab, this, type, min, max);
496 
497     tab->getUndoStack()->push(histC);
498 }
499 
getCumulativeFrequency(int channel) const500 QVector<uint32_t> FITSHistogram::getCumulativeFrequency(int channel) const
501 {
502     return cumulativeFrequency[channel];
503 }
504 
FITSHistogramCommand(QWidget * parent,FITSHistogram * inHisto,FITSScale newType,const QVector<double> & lmin,const QVector<double> & lmax)505 FITSHistogramCommand::FITSHistogramCommand(QWidget * parent,
506         FITSHistogram * inHisto,
507         FITSScale newType,
508         const QVector<double> &lmin,
509         const QVector<double> &lmax)
510 {
511     tab = dynamic_cast<FITSTab *>(parent);
512     type = newType;
513     histogram = inHisto;
514     min = lmin;
515     max = lmax;
516 }
517 
~FITSHistogramCommand()518 FITSHistogramCommand::~FITSHistogramCommand()
519 {
520     delete[] delta;
521 }
522 
calculateDelta(const uint8_t * buffer)523 bool FITSHistogramCommand::calculateDelta(const uint8_t * buffer)
524 {
525     const QSharedPointer<FITSData> &imageData = tab->getView()->imageData();
526 
527     uint8_t const * image_buffer = imageData->getImageBuffer();
528     int totalPixels =
529         imageData->width() * imageData->height() * imageData->channels();
530     unsigned long totalBytes = totalPixels * imageData->getBytesPerPixel();
531 
532     auto * raw_delta = new uint8_t[totalBytes];
533 
534     if (raw_delta == nullptr)
535     {
536         qWarning() << "Error! not enough memory to create image delta" << endl;
537         return false;
538     }
539 
540     for (unsigned int i = 0; i < totalBytes; i++)
541         raw_delta[i] = buffer[i] ^ image_buffer[i];
542 
543     compressedBytes = sizeof(uint8_t) * totalBytes + totalBytes / 64 + 16 + 3;
544     delete[] delta;
545     delta = new uint8_t[compressedBytes];
546 
547     if (delta == nullptr)
548     {
549         delete[] raw_delta;
550         qCCritical(KSTARS_FITS)
551                 << "FITSHistogram Error: Ran out of memory compressing delta";
552         return false;
553     }
554 
555     int r = compress2(delta, &compressedBytes, raw_delta, totalBytes, 5);
556 
557     if (r != Z_OK)
558     {
559         delete[] raw_delta;
560         /* this should NEVER happen */
561         qCCritical(KSTARS_FITS)
562                 << "FITSHistogram Error: Failed to compress raw_delta";
563         return false;
564     }
565 
566     // qDebug() << "compressed bytes size " << compressedBytes << " bytes" <<
567     // endl;
568 
569     delete[] raw_delta;
570 
571     return true;
572 }
573 
reverseDelta()574 bool FITSHistogramCommand::reverseDelta()
575 {
576     FITSView * image = tab->getView();
577     const QSharedPointer<FITSData> &imageData = image->imageData();
578     uint8_t const * image_buffer = (imageData->getImageBuffer());
579 
580     int totalPixels =
581         imageData->width() * imageData->height() * imageData->channels();
582     unsigned long totalBytes = totalPixels * imageData->getBytesPerPixel();
583 
584     auto * output_image = new uint8_t[totalBytes];
585 
586     if (output_image == nullptr)
587     {
588         qWarning() << "Error! not enough memory to create output image" << endl;
589         return false;
590     }
591 
592     auto * raw_delta = new uint8_t[totalBytes];
593 
594     if (raw_delta == nullptr)
595     {
596         delete[] output_image;
597         qWarning() << "Error! not enough memory to create image delta" << endl;
598         return false;
599     }
600 
601     int r = uncompress(raw_delta, &totalBytes, delta, compressedBytes);
602     if (r != Z_OK)
603     {
604         qCCritical(KSTARS_FITS)
605                 << "FITSHistogram compression error in reverseDelta()";
606         delete[] output_image;
607         delete[] raw_delta;
608         return false;
609     }
610 
611     for (unsigned int i = 0; i < totalBytes; i++)
612         output_image[i] = raw_delta[i] ^ image_buffer[i];
613 
614     imageData->setImageBuffer(output_image);
615 
616     delete[] raw_delta;
617 
618     return true;
619 }
620 
redo()621 void FITSHistogramCommand::redo()
622 {
623     FITSView * image = tab->getView();
624     const QSharedPointer<FITSData> &imageData = image->imageData();
625 
626     uint8_t const * image_buffer = imageData->getImageBuffer();
627     uint8_t * buffer = nullptr;
628     unsigned int size =
629         imageData->width() * imageData->height() * imageData->channels();
630     int BBP = imageData->getBytesPerPixel();
631 
632     QApplication::setOverrideCursor(Qt::WaitCursor);
633 
634     if (delta != nullptr)
635     {
636         FITSImage::Statistic prevStats;
637         imageData->saveStatistics(prevStats);
638 
639         reverseDelta();
640 
641         imageData->restoreStatistics(stats);
642 
643         stats = prevStats;
644     }
645     else
646     {
647         imageData->saveStatistics(stats);
648 
649         // If it's rotation of flip, no need to calculate delta
650         if (type >= FITS_ROTATE_CW && type <= FITS_FLIP_V)
651         {
652             imageData->applyFilter(type);
653         }
654         else
655         {
656             buffer = new uint8_t[size * BBP];
657 
658             if (buffer == nullptr)
659             {
660                 qWarning()
661                         << "Error! not enough memory to create image buffer in redo()"
662                         << endl;
663                 QApplication::restoreOverrideCursor();
664                 return;
665             }
666 
667             memcpy(buffer, image_buffer, size * BBP);
668 
669             QVector<double> dataMin = min, dataMax = max;
670             switch (type)
671             {
672                 case FITS_AUTO:
673                 case FITS_LINEAR:
674                     imageData->applyFilter(FITS_LINEAR, nullptr, &dataMin, &dataMax);
675                     break;
676 
677                 case FITS_LOG:
678                     imageData->applyFilter(FITS_LOG, nullptr, &dataMin, &dataMax);
679                     break;
680 
681                 case FITS_SQRT:
682                     imageData->applyFilter(FITS_SQRT, nullptr, &dataMin, &dataMax);
683                     break;
684 
685                 default:
686                     imageData->applyFilter(type);
687                     break;
688             }
689 
690             calculateDelta(buffer);
691             delete[] buffer;
692         }
693     }
694 
695     if (histogram != nullptr)
696     {
697         histogram->constructHistogram();
698 
699         if (tab->getViewer()->isStarsMarked())
700             imageData->findStars().waitForFinished();
701     }
702 
703     image->pushFilter(type);
704     image->rescale(ZOOM_KEEP_LEVEL);
705     image->updateFrame();
706 
707     QApplication::restoreOverrideCursor();
708 }
709 
undo()710 void FITSHistogramCommand::undo()
711 {
712     FITSView * image = tab->getView();
713     const QSharedPointer<FITSData> &imageData = image->imageData();
714 
715     QApplication::setOverrideCursor(Qt::WaitCursor);
716 
717     if (delta != nullptr)
718     {
719         FITSImage::Statistic prevStats;
720         imageData->saveStatistics(prevStats);
721 
722         reverseDelta();
723 
724         imageData->restoreStatistics(stats);
725 
726         stats = prevStats;
727     }
728     else
729     {
730         switch (type)
731         {
732             case FITS_ROTATE_CW:
733                 imageData->applyFilter(FITS_ROTATE_CCW);
734                 break;
735             case FITS_ROTATE_CCW:
736                 imageData->applyFilter(FITS_ROTATE_CW);
737                 break;
738             case FITS_FLIP_H:
739             case FITS_FLIP_V:
740                 imageData->applyFilter(type);
741                 break;
742             default:
743                 break;
744         }
745     }
746 
747     if (histogram != nullptr)
748     {
749         histogram->constructHistogram();
750 
751         if (tab->getViewer()->isStarsMarked())
752             imageData->findStars().waitForFinished();
753     }
754 
755     image->popFilter();
756     image->rescale(ZOOM_KEEP_LEVEL);
757     image->updateFrame();
758 
759     QApplication::restoreOverrideCursor();
760 }
761 
text() const762 QString FITSHistogramCommand::text() const
763 {
764     switch (type)
765     {
766         case FITS_AUTO:
767             return i18n("Auto Scale");
768         case FITS_LINEAR:
769             return i18n("Linear Scale");
770         case FITS_LOG:
771             return i18n("Logarithmic Scale");
772         case FITS_SQRT:
773             return i18n("Square Root Scale");
774 
775         default:
776             if (type - 1 <= FITSViewer::filterTypes.count())
777                 return FITSViewer::filterTypes.at(type - 1);
778             break;
779     }
780 
781     return i18n("Unknown");
782 }
783 
driftMouseOverLine(QMouseEvent * event)784 void FITSHistogram::driftMouseOverLine(QMouseEvent * event)
785 {
786     double intensity = customPlot->xAxis->pixelToCoord(event->localPos().x());
787 
788     const QSharedPointer<FITSData> &imageData = tab->getView()->imageData();
789     uint8_t channels = imageData->channels();
790     QVector<double> freq(3, -1);
791 
792     QVector<bool> inRange(3, false);
793     for (int n = 0; n < channels; n++)
794     {
795         if (intensity >= imageData->getMin(n) && intensity <= imageData->getMax(n))
796             inRange[n] = true;
797     }
798 
799     if ( (channels == 1 && inRange[0] == false) || (!inRange[0] && !inRange[1] && !inRange[2]) )
800     {
801         QToolTip::hideText();
802         return;
803     }
804 
805     if (customPlot->xAxis->range().contains(intensity))
806     {
807         for (int n = 0; n < channels; n++)
808         {
809             int index = graphs[n]->findBegin(intensity, true);
810             freq[n] = graphs[n]->dataMainValue(index);
811         }
812 
813         if (channels == 1 && freq[0] > 0)
814         {
815             QToolTip::showText(
816                 event->globalPos(),
817                 i18nc("Histogram tooltip; %1 is intensity; %2 is frequency;",
818                       "<table>"
819                       "<tr><td>Intensity:   </td><td>%1</td></tr>"
820                       "<tr><td>R Frequency:   </td><td>%2</td></tr>"
821                       "</table>",
822                       QString::number(intensity, 'f', numDecimals[0]),
823                       QString::number(freq[0], 'f', 0)));
824         }
825         else if (freq[1] > 0)
826         {
827             QToolTip::showText(
828                 event->globalPos(),
829                 i18nc("Histogram tooltip; %1 is intensity; %2 is frequency;",
830                       "<table>"
831                       "<tr><td>Intensity:   </td><td>%1</td></tr>"
832                       "<tr><td>R Frequency:   </td><td>%2</td></tr>"
833                       "<tr><td>G Frequency:   </td><td>%3</td></tr>"
834                       "<tr><td>B Frequency:   </td><td>%4</td></tr>"
835                       "</table>",
836                       QString::number(intensity, 'f', numDecimals[0]),
837                       QString::number(freq[0], 'f', 0),
838                       QString::number(freq[1], 'f', 0),
839                       QString::number(freq[2], 'f', 0)));
840         }
841         else
842             QToolTip::hideText();
843 
844         customPlot->replot();
845     }
846 }
847