diff --git a/kstars/fitsviewer/fitsdata.h b/kstars/fitsviewer/fitsdata.h --- a/kstars/fitsviewer/fitsdata.h +++ b/kstars/fitsviewer/fitsdata.h @@ -149,6 +149,17 @@ * @return A QFuture that can be watched until the async operation is complete. */ QFuture loadFITS(const QString &inFilename, bool silent = true); + + /** + * @brief loadFITSFromMemory Loading FITS from memory buffer. + * @param inFilename Potential future path to FITS file (or compressed fits.gz), stored in a fitsdata class variable + * @param fits_buffer The memory buffer containing the fits data. + * @param fits_buffer_size The size in bytes of the buffer. + * @param silent If set, error messages are ignored. If set to false, the error message will get displayed in a popup. + * @return bool indicating success or failure. + */ + bool loadFITSFromMemory(const QString &inFilename, void *fits_buffer, + size_t fits_buffer_size, bool silent); /* Save FITS */ int saveFITS(const QString &newFilename); /* Rescale image lineary from image_buffer, fit to window if desired */ @@ -431,7 +442,8 @@ void converted(QImage); private: - bool privateLoad(bool silent); + void loadCommon(const QString &inFilename); + bool privateLoad(void *fits_buffer, size_t fits_buffer_size, bool silent); void rotWCSFITS(int angle, int mirror); bool checkCollision(Edge *s1, Edge *s2); int calculateMinMax(bool refresh = false); diff --git a/kstars/fitsviewer/fitsdata.cpp b/kstars/fitsviewer/fitsdata.cpp --- a/kstars/fitsviewer/fitsdata.cpp +++ b/kstars/fitsviewer/fitsdata.cpp @@ -117,7 +117,7 @@ qDeleteAll(records); } -QFuture FITSData::loadFITS(const QString &inFilename, bool silent) +void FITSData::loadCommon(const QString &inFilename) { int status = 0; qDeleteAll(starCenters); @@ -138,25 +138,51 @@ } m_Filename = inFilename; +} - qCInfo(KSTARS_FITS) << "Loading FITS file " << m_Filename; +bool FITSData::loadFITSFromMemory(const QString &inFilename, void *fits_buffer, + size_t fits_buffer_size, bool silent) +{ + loadCommon(inFilename); + qCInfo(KSTARS_FITS) << "Reading FITS file buffer "; + return privateLoad(fits_buffer, fits_buffer_size, silent); +} - QFuture result = QtConcurrent::run(this, &FITSData::privateLoad, silent); +QFuture FITSData::loadFITS(const QString &inFilename, bool silent) +{ + loadCommon(inFilename); + qCInfo(KSTARS_FITS) << "Loading FITS file " << m_Filename; + QFuture result = QtConcurrent::run( + this, &FITSData::privateLoad, nullptr, 0, silent); return result; } -bool FITSData::privateLoad(bool silent) +namespace { +// Common code for reporting fits read errors. Always returns false. +bool fitsOpenError(int status, const QString& message, bool silent) +{ + char error_status[512]; + fits_report_error(stderr, status); + fits_get_errstatus(status, error_status); + QString errMessage = message; + errMessage.append(i18n(" Error: %1", QString::fromUtf8(error_status))); + if (!silent) + KSNotification::error(errMessage, i18n("FITS Open")); + qCCritical(KSTARS_FITS) << errMessage; + return false; +} +} + +bool FITSData::privateLoad(void *fits_buffer, size_t fits_buffer_size, bool silent) { int status = 0, anynull = 0; long naxes[3]; - char error_status[512]; QString errMessage; - if (m_Filename.startsWith(m_TemporaryPath)) - m_isTemporary = true; + m_isTemporary = m_Filename.startsWith(m_TemporaryPath); - if (m_Filename.endsWith(".fz")) + if (fits_buffer == nullptr && m_Filename.endsWith(".fz")) { // Store so we don't lose. m_compressedFilename = m_Filename; @@ -182,42 +208,32 @@ m_isCompressed = true; } - // Use open diskfile as it does not use extended file names which has problems opening - // files with [ ] or ( ) in their names. - if (fits_open_diskfile(&fptr, m_Filename.toLatin1(), READONLY, &status)) + if (fits_buffer == nullptr) { - fits_report_error(stderr, status); - fits_get_errstatus(status, error_status); - errMessage = i18n("Could not open file %1. Error %2", m_Filename, QString::fromUtf8(error_status)); - if (!silent) - KSNotification::error(errMessage, i18n("FITS Open")); - qCCritical(KSTARS_FITS) << errMessage; - return false; + // Use open diskfile as it does not use extended file names which has problems opening + // files with [ ] or ( ) in their names. + if (fits_open_diskfile(&fptr, m_Filename.toLatin1(), READONLY, &status)) + return fitsOpenError(status, i18n("Error opening fits file %1", m_Filename), silent); + else + stats.size = QFile(m_Filename).size(); } - - stats.size = QFile(m_Filename).size(); - - if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) + else { - fits_report_error(stderr, status); - fits_get_errstatus(status, error_status); - errMessage = i18n("Could not locate image HDU. Error %1", QString::fromUtf8(error_status)); - if (!silent) - KSNotification::error(errMessage, i18n("FITS Open")); - qCCritical(KSTARS_FITS) << errMessage; - return false; + // Read the FITS file from a memory buffer. + void *temp_buffer = fits_buffer; + size_t temp_size = fits_buffer_size; + if (fits_open_memfile(&fptr, m_Filename.toLatin1().data(), READONLY, + &temp_buffer, &temp_size, 0, nullptr, &status)) + return fitsOpenError(status, i18n("Error reading fits buffer."), silent); + else + stats.size = fits_buffer_size; } + + if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) + return fitsOpenError(status, i18n("Could not locate image HDU."), silent); if (fits_get_img_param(fptr, 3, &(stats.bitpix), &(stats.ndim), naxes, &status)) - { - fits_report_error(stderr, status); - fits_get_errstatus(status, error_status); - errMessage = i18n("FITS file open error (fits_get_img_param): %1", QString::fromUtf8(error_status)); - if (!silent) - KSNotification::error(errMessage, i18n("FITS Open")); - qCCritical(KSTARS_FITS) << errMessage; - return false; - } + return fitsOpenError(status, i18n("FITS file open error (fits_get_img_param)."), silent); if (stats.ndim < 2) { @@ -297,9 +313,7 @@ if (m_Mode != FITS_NORMAL || !Options::auto3DCube()) m_Channels = 1; - //image_buffer = new float[stats.samples_per_channel * channels]; m_ImageBuffer = new uint8_t[stats.samples_per_channel * m_Channels * stats.bytesPerPixel]; - //if (image_buffer == nullptr) if (m_ImageBuffer == nullptr) { qCWarning(KSTARS_FITS) << "FITSData: Not enough memory for image_buffer channel. Requested: " @@ -314,16 +328,7 @@ long nelements = stats.samples_per_channel * m_Channels; if (fits_read_img(fptr, m_DataType, 1, nelements, nullptr, m_ImageBuffer, &anynull, &status)) - { - char errmsg[512]; - fits_get_errstatus(status, errmsg); - errMessage = i18n("Error reading image: %1", QString(errmsg)); - if (!silent) - KSNotification::error(errMessage, i18n("FITS Open")); - fits_report_error(stderr, status); - qCCritical(KSTARS_FITS) << errMessage; - return false; - } + return fitsOpenError(status, i18n("Error reading image."), silent); parseHeader(); diff --git a/kstars/fitsviewer/fitstab.h b/kstars/fitsviewer/fitstab.h --- a/kstars/fitsviewer/fitstab.h +++ b/kstars/fitsviewer/fitstab.h @@ -34,6 +34,7 @@ class FITSHistogram; class FITSView; class FITSViewer; +class FITSData; /** * @brief The FITSTab class holds information on the current view (drawing area) in addition to the undo/redo stacks @@ -63,7 +64,12 @@ void clearRecentFITS(); void selectRecentFITS(int i); - void loadFITS(const QUrl &imageURL, FITSMode mode = FITS_NORMAL, FITSScale filter = FITS_NONE, bool silent = true); + void loadFITS(const QUrl &imageURL, FITSMode mode = FITS_NORMAL, + FITSScale filter = FITS_NONE, bool silent = true); + + bool loadFITSFromData(FITSData *data, const QUrl &imageURL, FITSMode mode = FITS_NORMAL, + FITSScale filter = FITS_NONE); + int saveFITS(const QString &filename); inline QUndoStack *getUndoStack() @@ -122,6 +128,9 @@ virtual void closeEvent(QCloseEvent *ev) override; private: + bool setupView(FITSMode mode, FITSScale filter); + void processData(); + /** Ask user whether he wants to save changes and save if he do. */ /// The FITSTools Toolbox diff --git a/kstars/fitsviewer/fitstab.cpp b/kstars/fitsviewer/fitstab.cpp --- a/kstars/fitsviewer/fitstab.cpp +++ b/kstars/fitsviewer/fitstab.cpp @@ -98,7 +98,7 @@ connect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS); } -void FITSTab::loadFITS(const QUrl &imageURL, FITSMode mode, FITSScale filter, bool silent) +bool FITSTab::setupView(FITSMode mode, FITSScale filter) { if (view.get() == nullptr) { @@ -165,69 +165,87 @@ // On Failure to load connect(view.get(), &FITSView::failed, this, &FITSTab::failed); + return true; + } + + // returns false if no setup needed. + return false; +} + +void FITSTab::loadFITS(const QUrl &imageURL, FITSMode mode, FITSScale filter, bool silent) +{ + if (setupView(mode, filter)) { + // On Success loading image connect(view.get(), &FITSView::loaded, [&]() { - // If it was already running make sure it's done - //histogramFuture.waitForFinished(); - FITSData *image_data = view->getImageData(); - histogram->reset(); - image_data->setHistogram(histogram); - - // Only construct histogram if it is actually visible - // Otherwise wait until histogram is needed before creating it. - if (fitsSplitter->sizes().at(0) != 0) - { - histogram->constructHistogram(); - } - - evaluateStats(); - - // if (histogram == nullptr) - // { - // histogram = new FITSHistogram(this); - // image_data->setHistogram(histogram); - // } + processData(); + emit loaded(); + }); + } - //histogramFuture = QtConcurrent::run([&]() {histogram->constructHistogram(); evaluateStats();}); + currentURL = imageURL; - //if(histogram->isVisible()) - // histogramFuture.waitForFinished(); + view->setFilter(filter); - // if (filter != FITS_NONE) - // { - // image_data->applyFilter(filter); - // view->rescale(ZOOM_KEEP_LEVEL); - // } + view->loadFITS(imageURL.toLocalFile(), silent); +} - if (viewer->isStarsMarked()) - view->toggleStars(true); +void FITSTab::processData() +{ + FITSData *image_data = view->getImageData(); + histogram->reset(); + image_data->setHistogram(histogram); + // Only construct histogram if it is actually visible + // Otherwise wait until histogram is needed before creating it. + if (fitsSplitter->sizes().at(0) != 0) + { + histogram->constructHistogram(); + } - loadFITSHeader(); + evaluateStats(); - 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); - } - } + if (viewer->isStarsMarked()) + view->toggleStars(true); - view->updateFrame(); + loadFITSHeader(); - emit loaded(); - }); + // Don't add it to the list if it is already there + if (recentImages->findItems(currentURL.toLocalFile(), Qt::MatchExactly).count() == 0) + { + 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(); +} + +bool FITSTab::loadFITSFromData(FITSData* data, const QUrl &imageURL, + FITSMode mode, FITSScale filter) +{ + setupView(mode, filter); currentURL = imageURL; view->setFilter(filter); - view->loadFITS(imageURL.toLocalFile(), silent); + if (!view->loadFITSFromData(data, imageURL.toLocalFile())) + { + // On Failure to load + // connect(view.get(), &FITSView::failed, this, &FITSTab::failed); + return false; + } + + processData(); + return true; } void FITSTab::modifyFITSState(bool clean) diff --git a/kstars/fitsviewer/fitsview.h b/kstars/fitsviewer/fitsview.h --- a/kstars/fitsviewer/fitsview.h +++ b/kstars/fitsviewer/fitsview.h @@ -58,13 +58,20 @@ typedef enum {dragCursor, selectCursor, scopeCursor, crosshairCursor } CursorMode; /** - * @brief loadFITS Loads FITS data and display it in FITSView frame + * @brief loadFITS Loads FITS data and displays it in a 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); + + /** + * @brief loadFITSFromData Takes ownership of the FITSData instance passed in and displays it in a FITSView frame + * @param inFilename FITS File name to use + */ + bool loadFITSFromData(FITSData *data, const QString &inFilename); + // Save FITS int saveFITS(const QString &newFilename); // Rescale image lineary from image_buffer, fit to window if desired @@ -256,6 +263,8 @@ double currentZoom { 0 }; private: + bool processData(); + QLabel *noImageLabel { nullptr }; QPixmap noImage; diff --git a/kstars/fitsviewer/fitsview.cpp b/kstars/fitsviewer/fitsview.cpp --- a/kstars/fitsviewer/fitsview.cpp +++ b/kstars/fitsviewer/fitsview.cpp @@ -189,19 +189,35 @@ fitsWatcher.setFuture(imageData->loadFITS(inFilename, silent)); } -void FITSView::loadInFrame() +bool FITSView::loadFITSFromData(FITSData *data, const QString &inFilename) { - // Check if the loading was OK - if (fitsWatcher.result() == false) + if (imageData != nullptr) { - m_LastError = imageData->getLastError(); - emit failed(); - return; + delete imageData; + imageData = nullptr; } - // Notify if there is debayer data. - emit debayerToggled(imageData->hasDebayer()); + if (floatingToolBar != nullptr) + { + floatingToolBar->setVisible(true); + } + + // In case loadWCS is still running for previous image data, let's wait until it's over + wcsWatcher.waitForFinished(); + + filterStack.clear(); + filterStack.push(FITS_NONE); + if (filter != FITS_NONE) + filterStack.push(filter); + // Takes control of the objects passed in. + imageData = data; + + return processData(); +} + +bool FITSView::processData() +{ // Set current width and height currentWidth = imageData->width(); currentHeight = imageData->height(); @@ -214,8 +230,6 @@ // Init the display image initDisplayImage(); - uint8_t * ASImageBuffer = nullptr; - imageData->applyFilter(filter); // Rescale to fits window on first load @@ -226,9 +240,7 @@ if (rescale(ZOOM_FIT_WINDOW) == false) { m_LastError = i18n("Rescaling image failed."); - delete [] ASImageBuffer; - emit failed(); - return; + return false; } firstLoad = false; @@ -238,9 +250,7 @@ if (rescale(ZOOM_KEEP_LEVEL) == false) { m_LastError = i18n("Rescaling image failed."); - delete [] ASImageBuffer; - emit failed(); - return; + return false; } } @@ -268,8 +278,26 @@ scaledImage = QImage(); updateFrame(); + return true; +} - emit loaded(); +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()); + + if (processData()) + emit loaded(); + else + emit failed(); } int FITSView::saveFITS(const QString &newFilename) diff --git a/kstars/fitsviewer/fitsviewer.h b/kstars/fitsviewer/fitsviewer.h --- a/kstars/fitsviewer/fitsviewer.h +++ b/kstars/fitsviewer/fitsviewer.h @@ -44,6 +44,7 @@ class FITSDebayer; class FITSTab; class FITSView; +class FITSData; /** * @class FITSViewer @@ -65,7 +66,13 @@ void addFITS(const QUrl &imageName, FITSMode mode = FITS_NORMAL, FITSScale filter = FITS_NONE, const QString &previewText = QString(), bool silent = true); + bool addFITSFromData(FITSData *data, const QUrl &imageName, int *tab_uid, + FITSMode mode = FITS_NORMAL, FITSScale filter = FITS_NONE, + const QString &previewText = QString()); + void updateFITS(const QUrl &imageName, int fitsUID, FITSScale filter = FITS_NONE, bool silent = true); + bool updateFITSFromData(FITSData *data, const QUrl &imageName, int fitsUID, + int *tab_uid, FITSScale filter = FITS_NONE); bool removeFITS(int fitsUID); bool isStarsMarked() @@ -130,7 +137,11 @@ private: void updateButtonStatus(const QString &action, const QString &item, bool showing); - + // Shared utilites between the standard and "FromData" addFITS and updateFITS. + bool addFITSCommon(FITSTab *tab, const QUrl &imageName, + FITSMode mode, const QString &previewText); + bool updateFITSCommon(FITSTab *tab, const QUrl &imageName); + QTabWidget *fitsTabWidget { nullptr }; QUndoGroup *undoGroup { nullptr }; FITSDebayer *debayerDialog { nullptr }; diff --git a/kstars/fitsviewer/fitsviewer.cpp b/kstars/fitsviewer/fitsviewer.cpp --- a/kstars/fitsviewer/fitsviewer.cpp +++ b/kstars/fitsviewer/fitsviewer.cpp @@ -330,99 +330,132 @@ } } -void FITSViewer::addFITS(const QUrl &imageName, FITSMode mode, FITSScale filter, const QString &previewText, bool silent) +bool FITSViewer::addFITSCommon(FITSTab *tab, const QUrl &imageName, + FITSMode mode, const QString &previewText) { - led.setColor(Qt::yellow); - QApplication::setOverrideCursor(Qt::WaitCursor); + int tabIndex = fitsTabWidget->indexOf(tab); + if (tabIndex != -1) + return false; - FITSTab *tab = new FITSTab(this); + lastURL = QUrl(imageName.url(QUrl::RemoveFilename)); - 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(); - } + 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; - emit failed(); - }); + case FITS_CALIBRATE: + fitsTabWidget->addTab(tab, i18n("Calibrate")); + break; - connect(tab, &FITSTab::loaded, [=]() { + case FITS_FOCUS: + fitsTabWidget->addTab(tab, i18n("Focus")); + break; - int tabIndex = fitsTabWidget->indexOf(tab); - if (tabIndex != -1) - return; + case FITS_GUIDE: + fitsTabWidget->addTab(tab, i18n("Guide")); + break; - lastURL = QUrl(imageName.url(QUrl::RemoveFilename)); + case FITS_ALIGN: + fitsTabWidget->addTab(tab, i18n("Align")); + break; + } - 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; + saveFileAction->setEnabled(true); + saveFileAsAction->setEnabled(true); - case FITS_CALIBRATE: - fitsTabWidget->addTab(tab, i18n("Calibrate")); - break; + undoGroup->addStack(tab->getUndoStack()); - case FITS_FOCUS: - fitsTabWidget->addTab(tab, i18n("Focus")); - break; + fitsTabs.push_back(tab); - case FITS_GUIDE: - fitsTabWidget->addTab(tab, i18n("Guide")); - break; + fitsMap[fitsID] = tab; - case FITS_ALIGN: - fitsTabWidget->addTab(tab, i18n("Align")); - break; - } + fitsTabWidget->setCurrentWidget(tab); - saveFileAction->setEnabled(true); - saveFileAsAction->setEnabled(true); + actionCollection()->action("fits_debayer")->setEnabled(tab->getView()->getImageData()->hasDebayer()); - undoGroup->addStack(tab->getUndoStack()); + tab->tabPositionUpdated(); - fitsTabs.push_back(tab); + tab->setUID(fitsID); - fitsMap[fitsID] = tab; + led.setColor(Qt::green); - fitsTabWidget->setCurrentWidget(tab); + updateStatusBar(i18n("Ready."), FITS_MESSAGE); - actionCollection()->action("fits_debayer")->setEnabled(tab->getView()->getImageData()->hasDebayer()); + tab->getView()->setCursorMode(FITSView::dragCursor); - tab->tabPositionUpdated(); + updateWCSFunctions(); - tab->setUID(fitsID); + return true; +} - led.setColor(Qt::green); +void FITSViewer::addFITS(const QUrl &imageName, FITSMode mode, FITSScale filter, const QString &previewText, bool silent) +{ + led.setColor(Qt::yellow); + QApplication::setOverrideCursor(Qt::WaitCursor); - updateStatusBar(i18n("Ready."), FITS_MESSAGE); + FITSTab *tab = new FITSTab(this); - tab->getView()->setCursorMode(FITSView::dragCursor); + 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(); + } - updateWCSFunctions(); + emit failed(); + }); - emit loaded(fitsID++); + connect(tab, &FITSTab::loaded, [=]() { + if (addFITSCommon(tab, imageName, mode, previewText)) + emit loaded(fitsID++); }); tab->loadFITS(imageName, mode, filter, silent); } +bool FITSViewer::addFITSFromData(FITSData *data, const QUrl &imageName, int *tab_uid, + FITSMode mode, FITSScale filter, const QString &previewText) +{ + led.setColor(Qt::yellow); + QApplication::setOverrideCursor(Qt::WaitCursor); + + FITSTab *tab = new FITSTab(this); + + if (!tab->loadFITSFromData(data, imageName, mode, filter)) { + 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(); + return false; + } + + if (!addFITSCommon(tab, imageName, mode, previewText)) + return false; + + *tab_uid = fitsID++; + return true; +} + bool FITSViewer::removeFITS(int fitsUID) { FITSTab *tab = fitsMap.value(fitsUID); @@ -461,32 +494,61 @@ // 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 (updateFITSCommon(tab, imageName)) { - 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()); + QObject::disconnect(*conn); + emit loaded(tab->getUID()); } + }); - tab->getUndoStack()->clear(); + tab->loadFITS(imageName, tab->getView()->getMode(), filter, silent); +} - if (tab->isVisible()) - led.setColor(Qt::green); +bool FITSViewer::updateFITSCommon(FITSTab *tab, const QUrl &imageName) +{ + // On tab load success + int tabIndex = fitsTabWidget->indexOf(tab); + if (tabIndex == -1) + return false; - QObject::disconnect(*conn); + 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()); + } - emit loaded(tab->getUID()); + tab->getUndoStack()->clear(); - }); + if (tab->isVisible()) + led.setColor(Qt::green); - tab->loadFITS(imageName, tab->getView()->getMode(), filter, silent); + return true; +} + +bool FITSViewer::updateFITSFromData(FITSData *data, const QUrl &imageName, int fitsUID, + int *tab_uid, FITSScale filter) +{ + FITSTab *tab = fitsMap.value(fitsUID); + + if (tab == nullptr) + return false; + + if (tab->isVisible()) + led.setColor(Qt::yellow); + + if (!tab->loadFITSFromData(data, imageName, tab->getView()->getMode(), filter)) + return false; + + if (!updateFITSCommon(tab, imageName)) + return false; + + *tab_uid = tab->getUID(); + return true; } void FITSViewer::tabFocusUpdated(int currentIndex) diff --git a/kstars/indi/indiccd.h b/kstars/indi/indiccd.h --- a/kstars/indi/indiccd.h +++ b/kstars/indi/indiccd.h @@ -18,6 +18,7 @@ #include #include +#include #include @@ -349,8 +350,14 @@ void captureFailed(); private: - void addFITSKeywords(const QString &filename); - void loadImageInView(IBLOB *bp, ISD::CCDChip *targetChip); + void processStream(IBLOB *bp); + void loadImageInView(IBLOB *bp, ISD::CCDChip *targetChip, FITSData *data); + bool generateFilename(const QString &format, bool batch_mode, QString *filename); + // Saves an image to disk on a separate thread. + bool WriteImageFile(IBLOB *bp, const QString &format, bool is_fits, + bool batch_mode, QString *filename); + // Creates or finds the FITSViewer. + void setupFITSViewerWindows(); QString filter; bool ISOMode { true }; @@ -395,5 +402,10 @@ // Typically for DSLRs QMap m_ExposurePresets; QPair m_ExposurePresetsMinMax; + + // Used when writing the image fits file to disk in a separate thread. + char *fileWriteBuffer { nullptr }; + int fileWriteBufferSize { 0 }; + QFuture fileWriteThread; }; } diff --git a/kstars/indi/indiccd.cpp b/kstars/indi/indiccd.cpp --- a/kstars/indi/indiccd.cpp +++ b/kstars/indi/indiccd.cpp @@ -29,11 +29,96 @@ #include #include #include +#include #include const QStringList RAWFormats = { "cr2", "cr3", "crw", "nef", "raf", "dng", "arw" }; +namespace { +void addFITSKeywords(const QString &filename, const QString &filter_used) +{ +#ifdef HAVE_CFITSIO + int status = 0; + + if (filter_used.isEmpty() == false) + { + QString filt(filter_used); + QString key_comment("Filter name"); + filt.replace(' ', '_'); + + fitsfile *fptr = nullptr; + + // Use open diskfile as it does not use extended file names which has problems opening + // files with [ ] or ( ) in their names. + if (fits_open_diskfile(&fptr, filename.toLatin1(), READONLY, &status)) + { + fits_report_error(stderr, status); + return; + } + + if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) + { + fits_report_error(stderr, status); + return; + } + + if (fits_update_key_str(fptr, "FILTER", filt.toLatin1().data(), key_comment.toLatin1().data(), &status)) + { + fits_report_error(stderr, status); + return; + } + + fits_close_file(fptr, &status); + } +#endif +} + +// Internal function to write an image blob to disk. +bool WriteImageFileInternal(const QString& filename, char *buffer, const size_t size, + bool add_fits_keywords, const QString &filter) +{ + QFile file(filename); + if (!file.open(QIODevice::WriteOnly)) + { + qCCritical(KSTARS_INDI) << "ISD:CCD Error: Unable to open write file: " << + filename; + return false; + } + size_t n = 0; + QDataStream out(&file); + for (size_t nr = 0; nr < size; nr += n) + n = out.writeRawData(buffer + nr, size - nr); + file.close(); + if (add_fits_keywords) + addFITSKeywords(filename, filter); + return true; +} + +// Internal function to write a temporary file image blob to disk. +bool WriteTempImageFile(const QString& format, char * buffer, size_t size, QString *filename) +{ + QTemporaryFile tmpFile(QDir::tempPath() + "/fitsXXXXXX" + format); + tmpFile.setAutoRemove(false); + + if (!tmpFile.open()) + { + qCCritical(KSTARS_INDI) << "ISD:CCD Error: Unable to open tempfile: " << + tmpFile.fileName(); + return false; + } + + QDataStream out(&tmpFile); + size_t n = 0; + for (size_t nr = 0; nr < size; nr += n) + n = out.writeRawData(buffer + nr, size - nr); + tmpFile.close(); + + *filename = tmpFile.fileName(); + return true; +} +} + namespace ISD { CCDChip::CCDChip(ISD::CCD *ccd, ChipType cType) @@ -354,33 +439,9 @@ return false; } -//bool CCDChip::capture(double exposure) -//{ -// INumberVectorProperty *expProp = nullptr; - -// switch (type) -// { -// case PRIMARY_CCD: -// expProp = baseDevice->getNumber("CCD_EXPOSURE"); -// break; - -// case GUIDE_CCD: -// expProp = baseDevice->getNumber("GUIDER_EXPOSURE"); -// break; -// } - -// if (expProp == nullptr) -// return false; - -// expProp->np[0].value = exposure; - -// clientManager->sendNewNumber(expProp); - -// return true; -//} - bool CCDChip::capture(double exposure) { + qCDebug(KSTARS_INDI) << "IndiCCD: capture()" << (type==PRIMARY_CCD?"CCD":"Guide"); INumberVectorProperty *expProp = nullptr; switch (type) @@ -471,8 +532,6 @@ abort->s = ISS_ON; - //captureMode = FITS_NORMAL; - clientManager->sendNewSwitch(abortProp); return true; @@ -861,6 +920,10 @@ { if (m_ImageViewerWindow) m_ImageViewerWindow->close(); + if (fileWriteThread.isRunning()) + fileWriteThread.waitForFinished(); + if (fileWriteBuffer != nullptr) + delete fileWriteBuffer; } void CCD::setBLOBManager(const char *device, INDI::Property *prop) @@ -1299,61 +1362,166 @@ primaryCCDBLOB->blob = nullptr; } -void CCD::processBLOB(IBLOB *bp) +void CCD::processStream(IBLOB *bp) { - // Ignore write-only BLOBs since we only receive it for state-change - if (bp->bvp->p == IP_WO || bp->size == 0) + if (streamWindow->isStreamEnabled() == false) return; - BType = BLOB_OTHER; + qCDebug(KSTARS_INDI) << "processStream()"; + INumberVectorProperty *streamFrame = baseDevice->getNumber("CCD_STREAM_FRAME"); + INumber *w = nullptr, *h = nullptr; - QString format = QString(bp->format).toLower(); + if (streamFrame) + { + w = IUFindNumber(streamFrame, "WIDTH"); + h = IUFindNumber(streamFrame, "HEIGHT"); + } - // If stream, process it first - if (format.contains("stream") && streamWindow.get() != nullptr) + if (w && h) { - if (streamWindow->isStreamEnabled() == false) - return; + streamW = w->value; + streamH = h->value; + } + else + { + int x, y, w, h; + int binx, biny; - INumberVectorProperty *streamFrame = baseDevice->getNumber("CCD_STREAM_FRAME"); - INumber *w = nullptr, *h = nullptr; + primaryChip->getFrame(&x, &y, &w, &h); + primaryChip->getBinning(&binx, &biny); + streamW = w / binx; + streamH = h / biny; + } - if (streamFrame) - { - w = IUFindNumber(streamFrame, "WIDTH"); - h = IUFindNumber(streamFrame, "HEIGHT"); - } + streamWindow->setSize(streamW, streamH); - if (w && h) - { - streamW = w->value; - streamH = h->value; - } - else - { - int x, y, w, h; - int binx, biny; + streamWindow->show(); + streamWindow->newFrame(bp); +} - primaryChip->getFrame(&x, &y, &w, &h); - primaryChip->getBinning(&binx, &biny); - streamW = w / binx; - streamH = h / biny; +bool CCD::generateFilename(const QString &format, bool batch_mode, QString *filename) +{ + QString currentDir; + if (batch_mode) + currentDir = fitsDir.isEmpty() ? Options::fitsDir() : fitsDir; + else + currentDir = KSPaths::writableLocation(QStandardPaths::TempLocation); - /*IBLOBVectorProperty *rawBP = baseDevice->getBLOB("CCD1"); - if (rawBP) - { - rawBP->bp[0].aux0 = &(streamW); - rawBP->bp[0].aux1 = &(streamH); - }*/ - } + if (QDir(currentDir).exists() == false) + QDir().mkpath(currentDir); + + if (currentDir.endsWith('/') == false) + currentDir.append('/'); + + // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-' + // The timestamp is no longer ISO8601 but it should solve interoperality issues + // between different OS hosts + QString ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss"); + + if (seqPrefix.contains("_ISO8601")) + { + QString finalPrefix = seqPrefix; + finalPrefix.replace("ISO8601", ts); + *filename = currentDir + finalPrefix + + QString("_%1%2").arg(QString().sprintf("%03d", nextSequenceID), format); + } + else + *filename = currentDir + seqPrefix + (seqPrefix.isEmpty() ? "" : "_") + + QString("%1%2").arg(QString().sprintf("%03d", nextSequenceID), format); + + QFile test_file(*filename); + if (!test_file.open(QIODevice::WriteOnly)) + { + qCCritical(KSTARS_INDI) << "ISD:CCD Error: Unable to open " << test_file.fileName(); + return false; + } + test_file.close(); + return true; +} + +bool CCD::WriteImageFile(IBLOB *bp, const QString &format, bool is_fits, + bool batch_mode, QString *filename) +{ + if (!generateFilename(format, batch_mode, filename)) + return false; + + // TODO: Not yet threading the writes for non-fits files. + // Would need to deal with the raw conversion, etc. + if (is_fits) { - //if (streamWindow->getStreamWidth() != streamW || streamWindow->getStreamHeight() != streamH) + // Check if the last write is still ongoing, and if so wait. + // It is using the fileWriteBuffer. + if (fileWriteThread.isRunning()) { + fileWriteThread.waitForFinished(); + } - streamWindow->setSize(streamW, streamH); + // Will write blob data in a separate thread, and can't depend on the blob + // memory, so copy it first. - streamWindow->show(); - streamWindow->newFrame(bp); + // Check buffer size. + if (fileWriteBufferSize < bp->size) { + if (fileWriteBuffer != nullptr) delete fileWriteBuffer; + fileWriteBufferSize = bp->size; + fileWriteBuffer = new char[fileWriteBufferSize]; + } + + // Copy memory, and write file on a separate thread. + // Probably too late to return an error if the file couldn't write. + memcpy(fileWriteBuffer, bp->blob, bp->size); + fileWriteThread = QtConcurrent::run(WriteImageFileInternal, *filename, + fileWriteBuffer, bp->size, is_fits, filter); + filter = ""; + } else { + if (!WriteImageFileInternal(*filename, static_cast(bp->blob), bp->size, + false, filter)) + return false; + } + return true; +} + +void CCD::setupFITSViewerWindows() +{ + normalTabID = calibrationTabID = focusTabID = guideTabID = alignTabID = -1; + + if (Options::singleWindowCapturedFITS()) + m_FITSViewerWindows = KStars::Instance()->genericFITSViewer(); + else + { + m_FITSViewerWindows = new FITSViewer(Options::independentWindowFITS() ? + nullptr : KStars::Instance()); + KStars::Instance()->addFITSViewer(m_FITSViewerWindows); + } + + connect(m_FITSViewerWindows, &FITSViewer::closed, [&](int tabIndex) + { + if (tabIndex == normalTabID) + normalTabID = -1; + else if (tabIndex == calibrationTabID) + calibrationTabID = -1; + else if (tabIndex == focusTabID) + focusTabID = -1; + else if (tabIndex == guideTabID) + guideTabID = -1; + else if (tabIndex == alignTabID) + alignTabID = -1; + }); +} + +void CCD::processBLOB(IBLOB *bp) +{ + // Ignore write-only BLOBs since we only receive it for state-change + if (bp->bvp->p == IP_WO || bp->size == 0) return; + + BType = BLOB_OTHER; + + QString format = QString(bp->format).toLower(); + + // If stream, process it first + if (format.contains("stream") && streamWindow.get() != nullptr) + { + processStream(bp); + return; } // Format without leading . (.jpg --> jpg) @@ -1380,87 +1548,33 @@ else targetChip = primaryChip.get(); - QString currentDir; - - if (targetChip->isBatchMode() == false) - currentDir = KSPaths::writableLocation(QStandardPaths::TempLocation); - else - currentDir = fitsDir.isEmpty() ? Options::fitsDir() : fitsDir; - - int nr, n = 0; - QTemporaryFile tmpFile(QDir::tempPath() + "/fitsXXXXXX" + format); - - //if (currentDir.endsWith('/')) - //currentDir.truncate(currentDir.size()-1); - - if (QDir(currentDir).exists() == false) - QDir().mkpath(currentDir); - - QString filename(currentDir); - if (filename.endsWith('/') == false) - filename.append('/'); - + qCDebug(KSTARS_INDI) << "processBLOB() mode " << targetChip->getCaptureMode(); + // Create temporary name if ANY of the following conditions are met: // 1. file is preview or batch mode is not enabled // 2. file type is not FITS_NORMAL (focus, guide..etc) + QString filename; if (targetChip->isBatchMode() == false || targetChip->getCaptureMode() != FITS_NORMAL) { - //tmpFile.setPrefix("fits"); - tmpFile.setAutoRemove(false); - - if (!tmpFile.open()) + if (!WriteTempImageFile(format, static_cast(bp->blob), bp->size, &filename)) { - qCCritical(KSTARS_INDI) << "ISD:CCD Error: Unable to open " << filename; - emit BLOBUpdated(nullptr); - return; + emit BLOBUpdated(nullptr); + return; } + if (BType == BLOB_FITS) + addFITSKeywords(filename, filter); - QDataStream out(&tmpFile); - - for (nr = 0; nr < static_cast(bp->size); nr += n) - n = out.writeRawData(static_cast(bp->blob) + nr, bp->size - nr); - - tmpFile.close(); - - filename = tmpFile.fileName(); } // Create file name for others else { - // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-' - // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts - QString ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss"); - - if (seqPrefix.contains("_ISO8601")) - { - QString finalPrefix = seqPrefix; - finalPrefix.replace("ISO8601", ts); - filename += finalPrefix + QString("_%1%2").arg(QString().sprintf("%03d", nextSequenceID), format); - } - else - filename += seqPrefix + (seqPrefix.isEmpty() ? "" : "_") + - QString("%1%2").arg(QString().sprintf("%03d", nextSequenceID), format); - - QFile fits_temp_file(filename); - - if (!fits_temp_file.open(QIODevice::WriteOnly)) - { - qCCritical(KSTARS_INDI) << "ISD:CCD Error: Unable to open " << fits_temp_file.fileName(); - emit BLOBUpdated(nullptr); - return; - } - - QDataStream out(&fits_temp_file); - - for (nr = 0; nr < static_cast(bp->size); nr += n) - n = out.writeRawData(static_cast(bp->blob) + nr, bp->size - nr); - - fits_temp_file.close(); + if (!WriteImageFile(bp, format, BType == BLOB_FITS, targetChip->isBatchMode(), &filename)) + { + emit BLOBUpdated(nullptr); + return; + } } - if (BType == BLOB_FITS) - addFITSKeywords(filename); - // store file name strncpy(BLOBFilename, filename.toLatin1(), MAXINDIFILENAME); bp->aux0 = targetChip; @@ -1480,13 +1594,13 @@ m_LastNotificationTS = QDateTime::currentDateTime(); } - // Check if we need to process RAW or regular image + // Check if we need to process RAW or regular image. Anything but FITS. if (BType == BLOB_IMAGE || BType == BLOB_RAW) { - bool useFITSViewer = Options::autoImageToFITS() && (Options::useFITSViewer() || (Options::useDSLRImageViewer() == false && targetChip->isBatchMode() == false)); + bool useFITSViewer = Options::autoImageToFITS() && + (Options::useFITSViewer() || (Options::useDSLRImageViewer() == false && targetChip->isBatchMode() == false)); bool useDSLRViewer = (Options::useDSLRImageViewer() || targetChip->isBatchMode() == false); - // For raw image, we only process them to JPG if we need to open them in the image - // viewer + // For raw image, we only process them to JPG if we need to open them in the image viewer if (BType == BLOB_RAW && (useFITSViewer || useDSLRViewer)) { QString rawFileName = filename; @@ -1517,12 +1631,6 @@ shortFormat = "jpg"; } - // store file name in - // strncpy(BLOBFilename, filename.toLatin1(), MAXINDIFILENAME); - // bp->aux0 = targetChip; - // bp->aux1 = &BType; - // bp->aux2 = BLOBFilename; - // Convert to FITS if checked. QString output; if (useFITSViewer && (FITSData::ImageToFITS(filename, shortFormat, output))) @@ -1548,98 +1656,81 @@ #ifdef HAVE_CFITSIO if (BType == BLOB_FITS) { - QUrl fileURL = QUrl::fromLocalFile(filename); + FITSMode captureMode = targetChip->getCaptureMode(); // Get or Create FITSViewer if we are using FITSViewer // or if capture mode is calibrate since for now we are forced to open the file in the viewer // this should be fixed in the future and should only use FITSData if (Options::useFITSViewer() || targetChip->isBatchMode() == false) { - if (m_FITSViewerWindows.isNull() && targetChip->getCaptureMode() != FITS_GUIDE && - targetChip->getCaptureMode() != FITS_FOCUS && targetChip->getCaptureMode() != FITS_ALIGN) - { - normalTabID = calibrationTabID = focusTabID = guideTabID = alignTabID = -1; - - if (Options::singleWindowCapturedFITS()) - m_FITSViewerWindows = KStars::Instance()->genericFITSViewer(); - else - { - m_FITSViewerWindows = new FITSViewer(Options::independentWindowFITS() ? nullptr : KStars::Instance()); - KStars::Instance()->addFITSViewer(m_FITSViewerWindows); - } - - connect(m_FITSViewerWindows, &FITSViewer::closed, [&](int tabIndex) - { - if (tabIndex == normalTabID) - normalTabID = -1; - else if (tabIndex == calibrationTabID) - calibrationTabID = -1; - else if (tabIndex == focusTabID) - focusTabID = -1; - else if (tabIndex == guideTabID) - guideTabID = -1; - else if (tabIndex == alignTabID) - alignTabID = -1; - }); - - //connect(fv, SIGNAL(destroyed()), this, SLOT(FITSViewerDestroyed())); - //connect(fv, SIGNAL(destroyed()), this, SIGNAL(FITSViewerClosed())); - } + if (m_FITSViewerWindows.isNull() && + (captureMode == FITS_NORMAL || captureMode == FITS_CALIBRATE)) + setupFITSViewerWindows(); } - FITSScale captureFilter = targetChip->getCaptureFilter(); - FITSMode captureMode = targetChip->getCaptureMode(); - - QString previewTitle; + FITSData *blob_fits_data = new FITSData(targetChip->getCaptureMode()); - // If image is preview and we should display all captured images in a single tab called "Preview" - // Then set the title to "Preview" - // Otherwise, the title will be the captured image name - if (targetChip->isBatchMode() == false && Options::singlePreviewFITS()) + if (!blob_fits_data->loadFITSFromMemory(filename, bp->blob, bp->size, false)) { - // If we are displayed all images from all cameras in a single FITS Viewer window - // Then we prefix the camera name to the "Preview" string - if (Options::singleWindowCapturedFITS()) - previewTitle = i18n("%1 Preview", getDeviceName()); - else - // Otherwise, just use "Preview" - previewTitle = i18n("Preview"); + // If reading the blob fails, we treat it the same as exposure failure + // and recapture again if possible + qCDebug(KSTARS_INDI) << "failed reading FITS memory buffer"; + emit newExposureValue(targetChip, 0, IPS_ALERT); + return; } - + switch (captureMode) { case FITS_NORMAL: case FITS_CALIBRATE: { - int *tabID = (captureMode == FITS_NORMAL) ? &normalTabID : &calibrationTabID; // Check if we need to display the image if (Options::useFITSViewer() || targetChip->isBatchMode() == false) { - m_FITSViewerWindows->disconnect(this); - auto m_Loaded = std::make_shared(); - *m_Loaded = connect(m_FITSViewerWindows, &FITSViewer::loaded, [ = ](int tabIndex) + bool success; + int tabIndex; + int *tabID = (captureMode == FITS_NORMAL) ? &normalTabID : &calibrationTabID; + QUrl fileURL = QUrl::fromLocalFile(filename); + FITSScale captureFilter = targetChip->getCaptureFilter(); + if (*tabID == -1 || Options::singlePreviewFITS() == false) { - *tabID = tabIndex; - targetChip->setImageView(m_FITSViewerWindows->getView(tabIndex), captureMode); - if (Options::focusFITSOnNewImage()) - m_FITSViewerWindows->raise(); - QObject::disconnect(*m_Loaded); - emit BLOBUpdated(bp); - }); - - auto m_Failed = std::make_shared(); - *m_Failed = connect(m_FITSViewerWindows, &FITSViewer::failed, [ = ]() + // If image is preview and we should display all captured images in a + // single tab called "Preview", then set the title to "Preview", + // Otherwise, the title will be the captured image name + QString previewTitle; + if (targetChip->isBatchMode() == false && Options::singlePreviewFITS()) + { + // If we are displaying all images from all cameras in a single FITS + // Viewer window, then we prefix the camera name to the "Preview" string + if (Options::singleWindowCapturedFITS()) + previewTitle = i18n("%1 Preview", getDeviceName()); + else + // Otherwise, just use "Preview" + previewTitle = i18n("Preview"); + } + + success = m_FITSViewerWindows->addFITSFromData( + blob_fits_data, fileURL, &tabIndex, captureMode, captureFilter, + previewTitle); + } + else + success = m_FITSViewerWindows->updateFITSFromData( + blob_fits_data, fileURL, *tabID, &tabIndex, captureFilter); + + if (!success) { - // If opening file fails, we treat it the same as exposure failure and recapture again if possible + // If opening file fails, we treat it the same as exposure failure + // and recapture again if possible + qCDebug(KSTARS_INDI) << "error adding/updating FITS"; emit newExposureValue(targetChip, 0, IPS_ALERT); - QObject::disconnect(*m_Failed); return; - }); + } + *tabID = tabIndex; + targetChip->setImageView(m_FITSViewerWindows->getView(tabIndex), captureMode); + if (Options::focusFITSOnNewImage()) + m_FITSViewerWindows->raise(); - if (*tabID == -1 || Options::singlePreviewFITS() == false) - m_FITSViewerWindows->addFITS(fileURL, captureMode, captureFilter, previewTitle); - else - m_FITSViewerWindows->updateFITS(fileURL, *tabID, captureFilter); + emit BLOBUpdated(bp); } else // If not displayed in FITS Viewer then we just inform that a blob was received. @@ -1650,96 +1741,40 @@ case FITS_FOCUS: case FITS_GUIDE: case FITS_ALIGN: - loadImageInView(bp, targetChip); + loadImageInView(bp, targetChip, blob_fits_data); break; } } else emit BLOBUpdated(bp); #endif } -void CCD::loadImageInView(IBLOB *bp, ISD::CCDChip *targetChip) +void CCD::loadImageInView(IBLOB *bp, ISD::CCDChip *targetChip, FITSData *data) { FITSMode mode = targetChip->getCaptureMode(); FITSView *view = targetChip->getImageView(mode); QString filename = QString(static_cast(bp->aux2)); if (view) { - auto m_Loaded = std::make_shared(); - *m_Loaded = connect(view, &FITSView::loaded, [ = ]() - { - //view->updateFrame(); - // FITSViewer is shown if: - // Image in preview mode, or useFITSViewre is true; AND - // Image type is either NORMAL or CALIBRATION since the rest have their dedicated windows. - // NORMAL is used for raw INDI drivers without Ekos. - if ( (Options::useFITSViewer() || targetChip->isBatchMode() == false) && (mode == FITS_NORMAL || mode == FITS_CALIBRATE)) - m_FITSViewerWindows->show(); - - QObject::disconnect(*m_Loaded); - emit BLOBUpdated(bp); - }); - auto m_Failed = std::make_shared(); - *m_Failed = connect(view, &FITSView::failed, [ = ]() - { - QObject::disconnect(*m_Failed); - emit newExposureValue(targetChip, 0, IPS_ALERT); - return; - }); - view->setFilter(targetChip->getCaptureFilter()); - view->loadFITS(filename, true); - } - -} - -void CCD::addFITSKeywords(const QString &filename) -{ -#ifdef HAVE_CFITSIO - int status = 0; - - if (filter.isEmpty() == false) - { - QString key_comment("Filter name"); - filter.replace(' ', '_'); - - fitsfile *fptr = nullptr; - -#if 0 - if (fits_open_image(&fptr, filename.toLatin1(), READWRITE, &status)) - { - fits_report_error(stderr, status); - return; - } -#endif - - // Use open diskfile as it does not use extended file names which has problems opening - // files with [ ] or ( ) in their names. - if (fits_open_diskfile(&fptr, filename.toLatin1(), READONLY, &status)) - { - fits_report_error(stderr, status); - return; - } - - if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) + if (!view->loadFITSFromData(data, filename)) { - fits_report_error(stderr, status); + emit newExposureValue(targetChip, 0, IPS_ALERT); return; - } - if (fits_update_key_str(fptr, "FILTER", filter.toLatin1().data(), key_comment.toLatin1().data(), &status)) - { - fits_report_error(stderr, status); - return; } + // FITSViewer is shown if: + // Image in preview mode, or useFITSViewer is true; AND + // Image type is either NORMAL or CALIBRATION since the rest have their dedicated windows. + // NORMAL is used for raw INDI drivers without Ekos. + if ( (Options::useFITSViewer() || targetChip->isBatchMode() == false) && + (mode == FITS_NORMAL || mode == FITS_CALIBRATE)) + m_FITSViewerWindows->show(); - fits_close_file(fptr, &status); - - filter = ""; + emit BLOBUpdated(bp); } -#endif } CCD::TransferFormat CCD::getTargetTransferFormat() const