File indexing completed on 2024-12-22 03:35:44

0001 /*
0002     File                 : FITSFilter.cpp
0003     Project              : LabPlot
0004     Description          : FITS I/O-filter
0005     --------------------------------------------------------------------
0006     SPDX-FileCopyrightText: 2016 Fabian Kristof <fkristofszabolcs@gmail.com>
0007     SPDX-FileCopyrightText: 2017 Alexander Semke <alexander.semke@web.de>
0008     SPDX-FileCopyrightText: 2022 Stefan Gerlach <stefan.gerlach@uni.kn>
0009     SPDX-License-Identifier: GPL-2.0-or-later
0010 */
0011 
0012 #include "FITSFilter.h"
0013 #include "FITSFilterPrivate.h"
0014 #include "backend/core/column/Column.h"
0015 #include "backend/core/column/ColumnStringIO.h"
0016 #include "backend/core/datatypes/Double2StringFilter.h"
0017 #include "backend/datasources/AbstractDataSource.h"
0018 #include "backend/matrix/Matrix.h"
0019 #include "backend/matrix/MatrixModel.h"
0020 #include "backend/spreadsheet/Spreadsheet.h"
0021 #include "commonfrontend/matrix/MatrixView.h"
0022 
0023 #include <QDebug>
0024 #include <QFile>
0025 #include <QMultiMap>
0026 
0027 /*! \class FITSFilter
0028  * \brief Manages the import/export of data from/to a FITS file.
0029  * \since 2.2.0
0030  * \ingroup datasources
0031  */
0032 FITSFilter::FITSFilter()
0033     : AbstractFileFilter(FileType::FITS)
0034     , d(new FITSFilterPrivate(this)) {
0035 }
0036 
0037 FITSFilter::~FITSFilter() = default;
0038 
0039 void FITSFilter::readDataFromFile(const QString& fileName, AbstractDataSource* dataSource, AbstractFileFilter::ImportMode importMode) {
0040     d->readCHDU(fileName, dataSource, importMode);
0041 }
0042 
0043 QVector<QStringList> FITSFilter::readChdu(const QString& fileName, bool* okToMatrix, int lines) {
0044     return d->readCHDU(fileName, nullptr, AbstractFileFilter::ImportMode::Replace, okToMatrix, lines);
0045 }
0046 
0047 void FITSFilter::write(const QString& fileName, AbstractDataSource* dataSource) {
0048     d->writeCHDU(fileName, dataSource);
0049 }
0050 
0051 void FITSFilter::addNewKeyword(const QString& filename, const QList<Keyword>& keywords) {
0052     d->addNewKeyword(filename, keywords);
0053 }
0054 
0055 void FITSFilter::updateKeywords(const QString& fileName, const QList<Keyword>& originals, const QVector<Keyword>& updates) {
0056     d->updateKeywords(fileName, originals, updates);
0057 }
0058 
0059 void FITSFilter::deleteKeyword(const QString& fileName, const QList<Keyword>& keywords) {
0060     d->deleteKeyword(fileName, keywords);
0061 }
0062 
0063 void FITSFilter::addKeywordUnit(const QString& fileName, const QList<Keyword>& keywords) {
0064     d->addKeywordUnit(fileName, keywords);
0065 }
0066 
0067 void FITSFilter::removeExtensions(const QStringList& extensions) {
0068     d->removeExtensions(extensions);
0069 }
0070 
0071 void FITSFilter::parseHeader(const QString& fileName, QTableWidget* headerEditTable, bool readKeys, const QList<Keyword>& keys) {
0072     d->parseHeader(fileName, headerEditTable, readKeys, keys);
0073 }
0074 
0075 void FITSFilter::parseExtensions(const QString& fileName, QTreeWidget* tw, bool checkPrimary) {
0076     d->parseExtensions(fileName, tw, checkPrimary);
0077 }
0078 
0079 QList<FITSFilter::Keyword> FITSFilter::chduKeywords(const QString& fileName) {
0080     return d->chduKeywords(fileName);
0081 }
0082 
0083 /*!
0084  * \brief contains the {StandardKeywords \ MandatoryKeywords} keywords
0085  * \return A list of keywords
0086  */
0087 QStringList FITSFilter::standardKeywords() {
0088     return QStringList() << QLatin1String("(blank)") << QLatin1String("CROTA") << QLatin1String("EQUINOX") << QLatin1String("NAXIS") << QLatin1String("TBCOL")
0089                          << QLatin1String("TUNIT") << QLatin1String("AUTHOR") << QLatin1String("CRPIX") << QLatin1String("EXTEND") << QLatin1String("OBJECT")
0090                          << QLatin1String("TDIM") << QLatin1String("TZERO") << QLatin1String("BITPIX") << QLatin1String("CRVAL") << QLatin1String("EXTLEVEL")
0091                          << QLatin1String("OBSERVER") << QLatin1String("TDISP") << QLatin1String("XTENSION") << QLatin1String("BLANK") << QLatin1String("CTYPE")
0092                          << QLatin1String("EXTNAME") << QLatin1String("ORIGIN") << QLatin1String("TELESCOP") << QLatin1String("BLOCKED")
0093                          << QLatin1String("DATAMAX") << QLatin1String("EXTVER") << QLatin1String("BSCALE") << QLatin1String("DATAMIN") << QLatin1String("PSCAL")
0094                          << QLatin1String("TFORM") << QLatin1String("BUNIT") << QLatin1String("DATE") << QLatin1String("GROUPS") << QLatin1String("PTYPE")
0095                          << QLatin1String("THEAP") << QLatin1String("BZERO") << QLatin1String("DATE-OBS") << QLatin1String("HISTORY") << QLatin1String("PZERO")
0096                          << QLatin1String("TNULL") << QLatin1String("CDELT") << QLatin1String("INSTRUME") << QLatin1String("REFERENC") << QLatin1String("TSCAL")
0097                          << QLatin1String("COMMENT") << QLatin1String("EPOCH") << QLatin1String("NAXIS") << QLatin1String("SIMPLE") << QLatin1String("TTYPE");
0098 }
0099 
0100 /*!
0101  * \brief Returns a list of keywords, that are mandatory for an image extension of a FITS file
0102  * see:
0103  * https://archive.stsci.edu/fits/fits_standard/node64.html
0104  * \return A list of keywords
0105  */
0106 
0107 QStringList FITSFilter::mandatoryImageExtensionKeywords() {
0108     return QStringList() << QLatin1String("XTENSION") << QLatin1String("BITPIX") << QLatin1String("NAXIS") << QLatin1String("PCOUNT") << QLatin1String("GCOUNT")
0109                          << QLatin1String("END");
0110 }
0111 
0112 /*!
0113  * \brief Returns a list of keywords, that are mandatory for a table extension (ascii or bintable)
0114  * of a FITS file
0115  * see:
0116  * https://archive.stsci.edu/fits/fits_standard/node58.html
0117  * https://archive.stsci.edu/fits/fits_standard/node68.html
0118  * \return A list of keywords
0119  */
0120 QStringList FITSFilter::mandatoryTableExtensionKeywords() {
0121     return QStringList() << QLatin1String("XTENSION") << QLatin1String("BITPIX") << QLatin1String("NAXIS") << QLatin1String("NAXIS1") << QLatin1String("NAXIS2")
0122                          << QLatin1String("PCOUNT") << QLatin1String("GCOUNT") << QLatin1String("TFIELDS") << QLatin1String("END");
0123 }
0124 
0125 /*!
0126  * \brief Returns a list of strings that represent units which are used for autocompletion when adding
0127  * keyword units to keywords
0128  * \return A list of strings that represent units
0129  */
0130 QStringList FITSFilter::units() {
0131     return QStringList() << QLatin1String("m (Metre)") << QLatin1String("kg (Kilogram)") << QLatin1String("s (Second)") << QString::fromUtf8("M☉ (Solar mass)")
0132                          << QLatin1String("AU (Astronomical unit") << QLatin1String("l.y (Light year)") << QLatin1String("km (Kilometres")
0133                          << QLatin1String("pc (Parsec)") << QLatin1String("K (Kelvin)") << QLatin1String("mol (Mole)") << QLatin1String("cd (Candela)");
0134 }
0135 
0136 /*!
0137  * \brief Sets the startColumn to \a column
0138  * \param column the column to be set
0139  */
0140 void FITSFilter::setStartColumn(const int column) {
0141     d->startColumn = column;
0142 }
0143 
0144 /*!
0145  * \brief Returns startColumn
0146  * \return The startColumn
0147  */
0148 int FITSFilter::startColumn() const {
0149     return d->startColumn;
0150 }
0151 
0152 /*!
0153  * \brief Sets the endColumn to \a column
0154  * \param column the column to be set
0155  */
0156 void FITSFilter::setEndColumn(const int column) {
0157     d->endColumn = column;
0158 }
0159 
0160 /*!
0161  * \brief Returns endColumn
0162  * \return The endColumn
0163  */
0164 int FITSFilter::endColumn() const {
0165     return d->endColumn;
0166 }
0167 
0168 /*!
0169  * \brief Sets the startRow to \a row
0170  * \param row the row to be set
0171  */
0172 void FITSFilter::setStartRow(const int row) {
0173     d->startRow = row;
0174 }
0175 
0176 /*!
0177  * \brief Returns startRow
0178  * \return The startRow
0179  */
0180 int FITSFilter::startRow() const {
0181     return d->startRow;
0182 }
0183 
0184 /*!
0185  * \brief Sets the endRow to \a row
0186  * \param row the row to be set
0187  */
0188 void FITSFilter::setEndRow(const int row) {
0189     d->endRow = row;
0190 }
0191 
0192 /*!
0193  * \brief Returns endRow
0194  * \return The endRow
0195  */
0196 int FITSFilter::endRow() const {
0197     return d->endRow;
0198 }
0199 
0200 /*!
0201  * \brief Sets commentsAsUnits to \a commentsAsUnits
0202  *
0203  * This is used when spreadsheets are exported to FITS table extensions and comments are used as the
0204  * units of the table's columns.
0205  * \param commentsAsUnits
0206  */
0207 void FITSFilter::setCommentsAsUnits(const bool commentsAsUnits) {
0208     d->commentsAsUnits = commentsAsUnits;
0209 }
0210 
0211 /*!
0212  * \brief Sets exportTo to \a exportTo
0213  *
0214  * This is used to decide whether the container should be exported to a FITS image or a FITS table
0215  * For an image \a exportTo should be 0, for a table 1
0216  * \param exportTo
0217  */
0218 void FITSFilter::setExportTo(const int exportTo) {
0219     d->exportTo = exportTo;
0220 }
0221 
0222 QString FITSFilter::fileInfoString(const QString& fileName) {
0223     const int imagesCount = FITSFilterPrivate::extensionNames(fileName).values(QLatin1String("IMAGES")).size();
0224     QString info(i18n("Images: %1", QString::number(imagesCount)));
0225 
0226     info += QLatin1String("<br>");
0227 
0228     const int tablesCount = FITSFilterPrivate::extensionNames(fileName).values(QLatin1String("TABLES")).size();
0229     info += i18n("Tables: %1", QString::number(tablesCount));
0230 
0231     return info;
0232 }
0233 
0234 // #####################################################################
0235 // ################### Private implementation ##########################
0236 // #####################################################################
0237 
0238 FITSFilterPrivate::FITSFilterPrivate(FITSFilter* owner)
0239     : q(owner) {
0240 }
0241 
0242 /*!
0243  * \brief Read the current header data unit from file \a filename in data source \a dataSource in \a importMode import mode
0244  * \param fileName the name of the file to be read
0245  * \param dataSource the data source to be filled
0246  * \param importMode
0247  */
0248 QVector<QStringList>
0249 FITSFilterPrivate::readCHDU(const QString& fileName, AbstractDataSource* dataSource, AbstractFileFilter::ImportMode importMode, bool* okToMatrix, int lines) {
0250     DEBUG(Q_FUNC_INFO << ", file name = " << STDSTRING(fileName));
0251     QVector<QStringList> dataStrings;
0252 
0253 #ifdef HAVE_FITS
0254     int status = 0;
0255 
0256     if (fits_open_file(&m_fitsFile, qPrintable(fileName), READONLY, &status)) {
0257         DEBUG(Q_FUNC_INFO << ", ERROR opening file " << STDSTRING(fileName));
0258         printError(status);
0259         return dataStrings;
0260     }
0261 
0262     int chduType;
0263 
0264     if (fits_get_hdu_type(m_fitsFile, &chduType, &status)) {
0265         printError(status);
0266         return dataStrings;
0267     }
0268 
0269     long actualRows;
0270     int actualCols;
0271     int columnOffset = 0;
0272 
0273     if (chduType == IMAGE_HDU) {
0274         DEBUG("IMAGE_HDU");
0275         int maxdim = 2;
0276         int bitpix;
0277         int naxis;
0278         long naxes[2];
0279 
0280         if (fits_get_img_param(m_fitsFile, maxdim, &bitpix, &naxis, naxes, &status)) {
0281             printError(status);
0282             return dataStrings;
0283         }
0284 
0285         if (naxis == 0)
0286             return dataStrings;
0287         actualRows = naxes[1];
0288         actualCols = naxes[0];
0289         DEBUG("rows/cols = " << actualRows << " " << actualCols)
0290         if (lines == -1)
0291             lines = actualRows;
0292         else {
0293             if (lines > actualRows)
0294                 lines = actualRows;
0295         }
0296 
0297         if (endRow != -1) {
0298             if (dataSource)
0299                 lines = endRow;
0300         }
0301         if (endColumn != -1)
0302             actualCols = endColumn;
0303         if (!dataSource)
0304             dataStrings.reserve(lines);
0305 
0306         int i = 0;
0307         int j = 0;
0308         if (startRow != 1)
0309             i = startRow;
0310         if (startColumn != 1)
0311             j = startColumn;
0312 
0313         const int jstart = j;
0314 
0315         // TODO: support other modes
0316         QVector<AbstractColumn::ColumnMode> columnModes;
0317         columnModes.resize(actualCols - j);
0318         QStringList vectorNames;
0319 
0320         DEBUG("lines/cols = " << lines << " " << actualCols << ", i/j = " << i << " " << j)
0321         std::vector<void*> dataContainer;
0322         if (dataSource) {
0323             dataContainer.reserve(actualCols - j);
0324             columnOffset = dataSource->prepareImport(dataContainer, importMode, lines - i, actualCols - j, vectorNames, columnModes);
0325         }
0326 
0327         long pixelCount = lines * actualCols;
0328         double* data = new double[pixelCount];
0329 
0330         if (!data) {
0331             DEBUG(Q_FUNC_INFO << ", Not enough memory for data");
0332             return dataStrings;
0333         }
0334 
0335         // TODO: other types
0336         if (fits_read_img(m_fitsFile, TDOUBLE, 1, pixelCount, nullptr, data, nullptr, &status)) {
0337             printError(status);
0338             return dataStrings << (QStringList() << QLatin1String("Error"));
0339         }
0340 
0341         int ii = 0;
0342         DEBUG(" Import " << lines << " lines");
0343         for (; i < lines; ++i) {
0344             int jj = 0;
0345             QStringList line;
0346             line.reserve(actualCols - j);
0347             for (; j < actualCols; ++j) {
0348                 if (dataSource)
0349                     static_cast<QVector<double>*>(dataContainer[jj++])->operator[](ii) = data[i * naxes[0] + j];
0350                 else
0351                     line << QString::number(data[i * naxes[0] + j]);
0352             }
0353             if (!dataSource)
0354                 dataStrings << line;
0355             j = jstart;
0356             ii++;
0357         }
0358         delete[] data;
0359 
0360         if (dataSource)
0361             dataSource->finalizeImport(columnOffset, 1, actualCols, QString(), importMode);
0362 
0363         fits_close_file(m_fitsFile, &status);
0364 
0365         return dataStrings;
0366 
0367     } else if ((chduType == ASCII_TBL) || (chduType == BINARY_TBL)) {
0368         DEBUG("ASCII_TBL or BINARY_TBL");
0369 
0370         if (endColumn != -1)
0371             actualCols = endColumn;
0372         else
0373             fits_get_num_cols(m_fitsFile, &actualCols, &status);
0374 
0375         if (endRow != -1)
0376             actualRows = endRow;
0377         else
0378             fits_get_num_rows(m_fitsFile, &actualRows, &status);
0379 
0380         QStringList columnNames;
0381         QList<int> columnsWidth;
0382         QStringList columnUnits;
0383         columnUnits.reserve(actualCols);
0384         columnsWidth.reserve(actualCols);
0385         columnNames.reserve(actualCols);
0386         int colWidth;
0387         char keyword[FLEN_KEYWORD];
0388         char value[FLEN_VALUE];
0389         int col = 1;
0390         if (startColumn != 1) {
0391             if (startColumn != 0)
0392                 col = startColumn;
0393         }
0394         for (; col <= actualCols; ++col) {
0395             status = 0;
0396             fits_make_keyn("TTYPE", col, keyword, &status);
0397             fits_read_key(m_fitsFile, TSTRING, keyword, value, nullptr, &status);
0398             columnNames.append(QLatin1String(value));
0399 
0400             fits_make_keyn("TUNIT", col, keyword, &status);
0401             fits_read_key(m_fitsFile, TSTRING, keyword, value, nullptr, &status);
0402             columnUnits.append(QLatin1String(value));
0403 
0404             fits_get_col_display_width(m_fitsFile, col, &colWidth, &status);
0405             columnsWidth.append(colWidth);
0406         }
0407 
0408         status = 0;
0409         if (lines == -1)
0410             lines = actualRows;
0411         else if (lines > actualRows)
0412             lines = actualRows;
0413 
0414         if (endRow != -1)
0415             lines = endRow;
0416         QVector<QVector<QString>*> stringDataPointers;
0417         std::vector<void*> numericDataPointers;
0418         QList<bool> columnNumericTypes;
0419 
0420         int startCol = 0;
0421         if (startColumn != 1)
0422             startCol = startColumn;
0423         int startRrow = 0;
0424         if (startRow != 1)
0425             startRrow = startRow;
0426 
0427         columnNumericTypes.reserve(actualCols);
0428         int datatype;
0429         int c = 1;
0430         if (startColumn != 1) {
0431             if (startColumn != 0)
0432                 c = startColumn;
0433         }
0434         QList<int> matrixNumericColumnIndices;
0435         for (; c <= actualCols; ++c) {
0436             fits_get_coltype(m_fitsFile, c, &datatype, nullptr, nullptr, &status);
0437 
0438             switch (datatype) {
0439             case TSTRING:
0440                 columnNumericTypes.append(false);
0441                 break;
0442             case TSHORT:
0443                 columnNumericTypes.append(true);
0444                 break;
0445             case TLONG:
0446                 columnNumericTypes.append(true);
0447                 break;
0448             case TFLOAT:
0449                 columnNumericTypes.append(true);
0450                 break;
0451             case TDOUBLE:
0452                 columnNumericTypes.append(true);
0453                 break;
0454             case TLOGICAL:
0455                 columnNumericTypes.append(false);
0456                 break;
0457             case TBIT:
0458                 columnNumericTypes.append(true);
0459                 break;
0460             case TBYTE:
0461                 columnNumericTypes.append(true);
0462                 break;
0463             case TCOMPLEX:
0464                 columnNumericTypes.append(true);
0465                 break;
0466             default:
0467                 columnNumericTypes.append(false);
0468                 break;
0469             }
0470             if ((datatype != TSTRING) && (datatype != TLOGICAL))
0471                 matrixNumericColumnIndices.append(c);
0472         }
0473 
0474         if (!dataSource)
0475             *okToMatrix = matrixNumericColumnIndices.isEmpty() ? false : true;
0476         else {
0477             DEBUG("HAS DataSource");
0478             auto* spreadsheet = dynamic_cast<Spreadsheet*>(dataSource);
0479             if (spreadsheet) {
0480                 numericDataPointers.reserve(actualCols - startCol);
0481 
0482                 stringDataPointers.reserve(actualCols - startCol);
0483                 spreadsheet->setUndoAware(false);
0484                 columnOffset = spreadsheet->resize(importMode, columnNames, actualCols - startCol);
0485 
0486                 if (importMode == AbstractFileFilter::ImportMode::Replace) {
0487                     spreadsheet->clear();
0488                     spreadsheet->setRowCount(lines - startRrow);
0489                 } else {
0490                     if (spreadsheet->rowCount() < (lines - startRrow))
0491                         spreadsheet->setRowCount(lines - startRrow);
0492                 }
0493                 DEBUG("Reading columns ...");
0494                 for (int n = 0; n < actualCols - startCol; ++n) {
0495                     if (columnNumericTypes.at(n)) {
0496                         spreadsheet->column(columnOffset + n)->setColumnMode(AbstractColumn::ColumnMode::Double);
0497                         auto* datap = static_cast<QVector<double>*>(spreadsheet->column(columnOffset + n)->data());
0498                         numericDataPointers.push_back(datap);
0499                         if (importMode == AbstractFileFilter::ImportMode::Replace)
0500                             datap->clear();
0501                     } else {
0502                         spreadsheet->column(columnOffset + n)->setColumnMode(AbstractColumn::ColumnMode::Text);
0503                         auto* list = static_cast<QVector<QString>*>(spreadsheet->column(columnOffset + n)->data());
0504                         stringDataPointers.push_back(list);
0505                         if (importMode == AbstractFileFilter::ImportMode::Replace)
0506                             list->clear();
0507                     }
0508                 }
0509                 DEBUG(" ... DONE");
0510                 stringDataPointers.squeeze();
0511             } else {
0512                 numericDataPointers.reserve(matrixNumericColumnIndices.size());
0513 
0514                 columnOffset = dataSource->prepareImport(numericDataPointers, importMode, lines - startRrow, matrixNumericColumnIndices.size());
0515             }
0516         }
0517 
0518         int row = 1;
0519         if (startRow != 1) {
0520             if (startRow != 0)
0521                 row = startRow;
0522         }
0523 
0524         int coll = 1;
0525         if (startColumn != 1) {
0526             if (startColumn != 0)
0527                 coll = startColumn;
0528         }
0529         bool isMatrix = false;
0530         if (dynamic_cast<Matrix*>(dataSource)) {
0531             isMatrix = true;
0532             coll = matrixNumericColumnIndices.first();
0533             actualCols = matrixNumericColumnIndices.last();
0534             if (importMode == AbstractFileFilter::ImportMode::Replace) {
0535                 for (auto* col : numericDataPointers)
0536                     static_cast<QVector<double>*>(col)->clear();
0537             }
0538         }
0539 
0540         char array[FLEN_VALUE];
0541         char* tmpArr[1] = {array};
0542         for (; row <= lines; ++row) {
0543             int numericixd = 0;
0544             int stringidx = 0;
0545             QStringList line;
0546             line.reserve(actualCols - coll);
0547             for (int col = coll; col <= actualCols; ++col) {
0548                 if (isMatrix) {
0549                     if (!matrixNumericColumnIndices.contains(col))
0550                         continue;
0551                 }
0552                 if (fits_read_col_str(m_fitsFile, col, row, 1, 1, nullptr, tmpArr, nullptr, &status))
0553                     printError(status);
0554                 if (dataSource) {
0555                     QString str = QString::fromLatin1(array);
0556                     if (str.isEmpty()) {
0557                         if (columnNumericTypes.at(col - 1))
0558                             static_cast<QVector<double>*>(numericDataPointers[numericixd++])->push_back(0);
0559                         else
0560                             stringDataPointers[stringidx++]->append(QLatin1String("NULL"));
0561                     } else {
0562                         if (columnNumericTypes.at(col - 1))
0563                             static_cast<QVector<double>*>(numericDataPointers[numericixd++])->push_back(str.toDouble());
0564                         else {
0565                             if (!stringDataPointers.isEmpty())
0566                                 stringDataPointers[stringidx++]->operator<<(str.simplified());
0567                         }
0568                     }
0569                 } else {
0570                     QString tmpColstr = QString::fromLatin1(array);
0571                     tmpColstr = tmpColstr.simplified();
0572                     if (tmpColstr.isEmpty())
0573                         line << QLatin1String("NULL");
0574                     else
0575                         line << tmpColstr;
0576                 }
0577             }
0578             if (!dataSource)
0579                 dataStrings << line;
0580         }
0581 
0582         if (dataSource)
0583             dataSource->finalizeImport(columnOffset, 1, actualCols, QString(), importMode);
0584 
0585         fits_close_file(m_fitsFile, &status);
0586         return dataStrings;
0587     } else
0588         DEBUG(Q_FUNC_INFO << ", Incorrect header type");
0589 
0590     fits_close_file(m_fitsFile, &status);
0591 
0592 #else
0593     Q_UNUSED(fileName)
0594     Q_UNUSED(dataSource)
0595     Q_UNUSED(importMode)
0596     Q_UNUSED(okToMatrix)
0597     Q_UNUSED(lines)
0598 #endif
0599     return dataStrings;
0600 }
0601 
0602 /*!
0603  * \brief Export from data source \a dataSource to file \a fileName
0604  * \param fileName the name of the file to be exported to
0605  * \param dataSource the datasource whose data is exported
0606  */
0607 
0608 void FITSFilterPrivate::writeCHDU(const QString& fileName, AbstractDataSource* dataSource) {
0609 #ifdef HAVE_FITS
0610     if (!fileName.endsWith(QLatin1String(".fits")))
0611         return;
0612     int status = 0;
0613     bool existed = false;
0614     if (!QFile::exists(fileName)) {
0615         if (fits_create_file(&m_fitsFile, qPrintable(fileName), &status)) {
0616             printError(status);
0617             qDebug() << fileName;
0618             return;
0619         }
0620     } else {
0621         if (fits_open_file(&m_fitsFile, qPrintable(fileName), READWRITE, &status)) {
0622             printError(status);
0623             return;
0624         } else
0625             existed = true;
0626     }
0627 
0628     Matrix* const matrix = dynamic_cast<Matrix*>(dataSource);
0629     if (matrix) {
0630         // FITS image
0631         if (exportTo == 0) {
0632             long naxes[2] = {matrix->columnCount(), matrix->rowCount()};
0633             if (fits_create_img(m_fitsFile, FLOAT_IMG, 2, naxes, &status)) {
0634                 printError(status);
0635                 status = 0;
0636                 fits_close_file(m_fitsFile, &status);
0637                 return;
0638             }
0639             const long nelem = naxes[0] * naxes[1];
0640             double* const array = new double[nelem];
0641             const QVector<QVector<double>>* const data = static_cast<QVector<QVector<double>>*>(matrix->data());
0642 
0643             for (int col = 0; col < naxes[0]; ++col)
0644                 for (int row = 0; row < naxes[1]; ++row)
0645                     array[row * naxes[0] + col] = data->at(row).at(col);
0646 
0647             if (fits_write_img(m_fitsFile, TDOUBLE, 1, nelem, array, &status)) {
0648                 printError(status);
0649                 status = 0;
0650             }
0651 
0652             fits_close_file(m_fitsFile, &status);
0653             delete[] array;
0654             // FITS table
0655         } else {
0656             const int nrows = matrix->rowCount();
0657             const int tfields = matrix->columnCount();
0658             QVector<char*> columnNames;
0659             columnNames.resize(tfields);
0660             columnNames.squeeze();
0661             QVector<char*> tform;
0662             tform.resize(tfields);
0663             tform.squeeze();
0664             // TODO: mode
0665             const QVector<QVector<double>>* const matrixData = static_cast<QVector<QVector<double>>*>(matrix->data());
0666             const MatrixModel* matrixModel = static_cast<MatrixView*>(matrix->view())->model();
0667             const int precision = matrix->precision();
0668             for (int i = 0; i < tfields; ++i) {
0669                 const QString& columnName = matrixModel->headerData(i, Qt::Horizontal).toString();
0670                 columnNames[i] = new char[columnName.size() + 1];
0671                 strcpy(columnNames[i], columnName.toLatin1().constData());
0672                 int maxSize = -1;
0673                 for (int row = 0; row < nrows; ++row) {
0674                     if (matrix->text<double>(row, i).size() > maxSize)
0675                         maxSize = matrix->text<double>(row, i).size();
0676                 }
0677                 QString tformn;
0678                 if (precision > 0) {
0679                     tformn = QLatin1String("F") + QString::number(maxSize) + QLatin1String(".") + QString::number(precision);
0680                 } else
0681                     tformn = QLatin1String("F") + QString::number(maxSize) + QLatin1String(".0");
0682                 tform[i] = new char[tformn.size() + 1];
0683                 strcpy(tform[i], tformn.toLatin1().constData());
0684             }
0685             // TODO extension name containing[] ?
0686 
0687             int r = fits_create_tbl(m_fitsFile,
0688                                     ASCII_TBL,
0689                                     nrows,
0690                                     tfields,
0691                                     columnNames.data(),
0692                                     tform.data(),
0693                                     nullptr,
0694                                     matrix->name().toLatin1().constData(),
0695                                     &status);
0696             for (int i = 0; i < tfields; ++i) {
0697                 delete[] tform[i];
0698                 delete[] columnNames[i];
0699             }
0700             if (r) {
0701                 printError(status);
0702                 status = 0;
0703                 fits_close_file(m_fitsFile, &status);
0704                 if (!existed) {
0705                     QFile file(fileName);
0706                     file.remove();
0707                 }
0708                 return;
0709             }
0710 
0711             double* columnNumeric = new double[nrows];
0712             for (int col = 1; col <= tfields; ++col) {
0713                 const QVector<double>& column = matrixData->at(col - 1);
0714                 for (int r = 0; r < column.size(); ++r)
0715                     columnNumeric[r] = column.at(r);
0716 
0717                 fits_write_col(m_fitsFile, TDOUBLE, col, 1, 1, nrows, columnNumeric, &status);
0718                 if (status) {
0719                     printError(status);
0720                     delete[] columnNumeric;
0721                     status = 0;
0722                     if (!existed) {
0723                         QFile file(fileName);
0724                         file.remove();
0725                     }
0726 
0727                     fits_close_file(m_fitsFile, &status);
0728                     return;
0729                 }
0730             }
0731             delete[] columnNumeric;
0732             fits_close_file(m_fitsFile, &status);
0733         }
0734         return;
0735     }
0736 
0737     auto* const spreadsheet = dynamic_cast<Spreadsheet*>(dataSource);
0738     if (spreadsheet) {
0739         // FITS image
0740         if (exportTo == 0) {
0741             int maxRowIdx = -1;
0742             // don't export lots of empty lines if all of those contain nans
0743             //  TODO: option?
0744             for (int c = 0; c < spreadsheet->columnCount(); ++c) {
0745                 const Column* const col = spreadsheet->column(c);
0746                 int currMaxRoxIdx = -1;
0747                 for (int r = col->rowCount(); r >= 0; --r) {
0748                     if (col->isValid(r)) {
0749                         currMaxRoxIdx = r;
0750                         break;
0751                     }
0752                 }
0753 
0754                 if (currMaxRoxIdx > maxRowIdx) {
0755                     maxRowIdx = currMaxRoxIdx;
0756                 }
0757             }
0758             long naxes[2] = {spreadsheet->columnCount(), maxRowIdx + 1};
0759             if (fits_create_img(m_fitsFile, FLOAT_IMG, 2, naxes, &status)) {
0760                 printError(status);
0761                 status = 0;
0762                 fits_close_file(m_fitsFile, &status);
0763                 if (!existed) {
0764                     QFile file(fileName);
0765                     file.remove();
0766                 }
0767                 return;
0768             }
0769             const long nelem = naxes[0] * naxes[1];
0770             double* array = new double[nelem];
0771 
0772             for (int row = 0; row < naxes[1]; ++row) {
0773                 for (int col = 0; col < naxes[0]; ++col)
0774                     array[row * naxes[0] + col] = spreadsheet->column(col)->valueAt(row);
0775             }
0776 
0777             if (fits_write_img(m_fitsFile, TDOUBLE, 1, nelem, array, &status)) {
0778                 printError(status);
0779                 status = 0;
0780                 fits_close_file(m_fitsFile, &status);
0781                 if (!existed) {
0782                     QFile file(fileName);
0783                     file.remove();
0784                 }
0785                 return;
0786             }
0787 
0788             fits_close_file(m_fitsFile, &status);
0789             delete[] array;
0790         } else {
0791             const int nrows = spreadsheet->rowCount();
0792             const int tfields = spreadsheet->columnCount();
0793 
0794             QVector<char*> columnNames;
0795             columnNames.resize(tfields);
0796             columnNames.squeeze();
0797             QVector<char*> tform;
0798             tform.resize(tfields);
0799             tform.squeeze();
0800             QVector<char*> tunit;
0801             tunit.resize(tfields);
0802             tunit.squeeze();
0803 
0804             for (int i = 0; i < tfields; ++i) {
0805                 const Column* const column = spreadsheet->column(i);
0806 
0807                 columnNames[i] = new char[column->name().size() + 1];
0808                 strcpy(columnNames[i], column->name().toLatin1().constData());
0809                 if (commentsAsUnits) {
0810                     tunit[i] = new char[column->comment().size() + 1];
0811                     strcpy(tunit[i], column->comment().toLatin1().constData());
0812                 } else {
0813                     tunit[i] = new char[1];
0814                     tunit[i][0] = '\0';
0815                 }
0816                 switch (column->columnMode()) {
0817                 case AbstractColumn::ColumnMode::Double: {
0818                     int maxSize = -1;
0819                     for (int row = 0; row < nrows; ++row) {
0820                         if (QString::number(column->valueAt(row)).size() > maxSize)
0821                             maxSize = QString::number(column->valueAt(row)).size();
0822                     }
0823 
0824                     const Double2StringFilter* const filter = static_cast<Double2StringFilter*>(column->outputFilter());
0825                     bool decimals = false;
0826                     for (int ii = 0; ii < nrows; ++ii) {
0827                         bool ok;
0828                         QString cell = column->asStringColumn()->textAt(ii);
0829                         double val = cell.toDouble(&ok);
0830                         if (cell.size() > QString::number(val).size() + 1) {
0831                             decimals = true;
0832                             break;
0833                         }
0834                     }
0835                     QString tformn;
0836                     if (decimals) {
0837                         int maxStringSize = -1;
0838                         for (int row = 0; row < nrows; ++row) {
0839                             if (column->asStringColumn()->textAt(row).size() > maxStringSize)
0840                                 maxStringSize = column->asStringColumn()->textAt(row).size();
0841                         }
0842                         const int diff = abs(maxSize - maxStringSize);
0843                         maxSize += diff;
0844                         tformn = QLatin1String("F") + QString::number(maxSize) + QLatin1String(".") + QString::number(filter->numDigits());
0845                     } else
0846                         tformn = QLatin1String("F") + QString::number(maxSize) + QLatin1String(".0");
0847                     tform[i] = new char[tformn.size()];
0848                     strcpy(tform[i], tformn.toLatin1().data());
0849                     break;
0850                 }
0851                 case AbstractColumn::ColumnMode::Text: {
0852                     int maxSize = -1;
0853                     for (int row = 0; row < nrows; ++row) {
0854                         if (column->textAt(row).size() > maxSize)
0855                             maxSize = column->textAt(row).size();
0856                     }
0857                     const QString& tformn = QLatin1String("A") + QString::number(maxSize);
0858                     tform[i] = new char[tformn.size()];
0859                     strcpy(tform[i], tformn.toLatin1().data());
0860                     break;
0861                 }
0862                 case AbstractColumn::ColumnMode::Integer: // TODO
0863                 case AbstractColumn::ColumnMode::BigInt:
0864                 case AbstractColumn::ColumnMode::DateTime:
0865                 case AbstractColumn::ColumnMode::Day:
0866                 case AbstractColumn::ColumnMode::Month:
0867                     break;
0868                 }
0869             }
0870             // TODO extension name containing[] ?
0871 
0872             int r = fits_create_tbl(m_fitsFile,
0873                                     ASCII_TBL,
0874                                     nrows,
0875                                     tfields,
0876                                     columnNames.data(),
0877                                     tform.data(),
0878                                     tunit.data(),
0879                                     spreadsheet->name().toLatin1().constData(),
0880                                     &status);
0881             for (int i = 0; i < tfields; ++i) {
0882                 delete[] tform[i];
0883                 delete[] tunit[i];
0884                 delete[] columnNames[i];
0885             }
0886             if (r) {
0887                 printError(status);
0888                 status = 0;
0889                 fits_close_file(m_fitsFile, &status);
0890                 if (!existed) {
0891                     QFile file(fileName);
0892                     file.remove();
0893                 }
0894                 return;
0895             }
0896 
0897             QVector<char*> column;
0898             column.resize(nrows);
0899             column.squeeze();
0900 
0901             double* columnNumeric = new double[nrows];
0902             for (int col = 1; col <= tfields; ++col) {
0903                 const Column* c = spreadsheet->column(col - 1);
0904                 auto columnMode = c->columnMode();
0905 
0906                 if (columnMode == AbstractColumn::ColumnMode::Double) {
0907                     for (int row = 0; row < nrows; ++row)
0908                         columnNumeric[row] = c->valueAt(row);
0909 
0910                     fits_write_col(m_fitsFile, TDOUBLE, col, 1, 1, nrows, columnNumeric, &status);
0911                     if (status) {
0912                         printError(status);
0913                         delete[] columnNumeric;
0914                         status = 0;
0915                         fits_close_file(m_fitsFile, &status);
0916                         if (!existed) {
0917                             QFile file(fileName);
0918                             file.remove();
0919                         }
0920                         return;
0921                     }
0922                 } else {
0923                     for (int row = 0; row < nrows; ++row) {
0924                         column[row] = new char[c->textAt(row).size() + 1];
0925                         strcpy(column[row], c->textAt(row).toLatin1().constData());
0926                     }
0927                     fits_write_col(m_fitsFile, TSTRING, col, 1, 1, nrows, column.data(), &status);
0928                     for (int row = 0; row < nrows; ++row)
0929                         delete[] column[row];
0930                     if (status) {
0931                         printError(status);
0932                         status = 0;
0933                         fits_close_file(m_fitsFile, &status);
0934 
0935                         delete[] columnNumeric;
0936                         return;
0937                     }
0938                 }
0939             }
0940 
0941             delete[] columnNumeric;
0942 
0943             status = 0;
0944             fits_close_file(m_fitsFile, &status);
0945         }
0946     }
0947 #else
0948     Q_UNUSED(fileName)
0949     Q_UNUSED(dataSource)
0950 #endif
0951 }
0952 
0953 /*!
0954  * \brief Return a map of the available extensions names in file \a filename
0955  *        The keys of the map are the extension types, the values are the names
0956  * \param fileName the name of the FITS file to be analyzed
0957  */
0958 QMultiMap<QString, QString> FITSFilterPrivate::extensionNames(const QString& fileName) {
0959     DEBUG(Q_FUNC_INFO << ", file name = " << STDSTRING(fileName));
0960 #ifdef HAVE_FITS
0961     QMultiMap<QString, QString> extensions;
0962     int status = 0;
0963     fitsfile* fitsFile = nullptr;
0964     if (fits_open_file(&fitsFile, qPrintable(fileName), READONLY, &status))
0965         return {};
0966     int hduCount;
0967 
0968     if (fits_get_num_hdus(fitsFile, &hduCount, &status))
0969         return {};
0970     int imageCount = 0;
0971     int asciiTableCount = 0;
0972     int binaryTableCount = 0;
0973     for (int currentHDU = 1; (currentHDU <= hduCount) && !status; ++currentHDU) {
0974         int hduType;
0975         status = 0;
0976 
0977         fits_get_hdu_type(fitsFile, &hduType, &status);
0978         switch (hduType) {
0979         case IMAGE_HDU:
0980             imageCount++;
0981             break;
0982         case ASCII_TBL:
0983             asciiTableCount++;
0984             break;
0985         case BINARY_TBL:
0986             binaryTableCount++;
0987             break;
0988         }
0989         char* keyVal = new char[FLEN_VALUE];
0990         QString extName;
0991         if (!fits_read_keyword(fitsFile, "EXTNAME", keyVal, nullptr, &status)) {
0992             extName = QLatin1String(keyVal);
0993             extName = extName.mid(1, extName.length() - 2).simplified();
0994         } else {
0995             status = 0;
0996             if (!fits_read_keyword(fitsFile, "HDUNAME", keyVal, nullptr, &status)) {
0997                 extName = QLatin1String(keyVal);
0998                 extName = extName.mid(1, extName.length() - 2).simplified();
0999             } else {
1000                 status = 0;
1001                 switch (hduType) {
1002                 case IMAGE_HDU:
1003                     if (imageCount == 1)
1004                         extName = i18n("Primary header");
1005                     else
1006                         extName = i18n("IMAGE #%1", imageCount);
1007                     break;
1008                 case ASCII_TBL:
1009                     extName = i18n("ASCII_TBL #%1", asciiTableCount);
1010                     break;
1011                 case BINARY_TBL:
1012                     extName = i18n("BINARY_TBL #%1", binaryTableCount);
1013                     break;
1014                 }
1015             }
1016         }
1017         delete[] keyVal;
1018         status = 0;
1019         extName = extName.trimmed();
1020         switch (hduType) {
1021         case IMAGE_HDU:
1022             extensions.insert(QLatin1String("IMAGES"), extName);
1023             break;
1024         case ASCII_TBL:
1025             extensions.insert(QLatin1String("TABLES"), extName);
1026             break;
1027         case BINARY_TBL:
1028             extensions.insert(QLatin1String("TABLES"), extName);
1029             break;
1030         }
1031         fits_movrel_hdu(fitsFile, 1, nullptr, &status);
1032     }
1033 
1034     if (status == END_OF_FILE)
1035         status = 0;
1036 
1037     fits_close_file(fitsFile, &status);
1038     return extensions;
1039 #else
1040     Q_UNUSED(fileName)
1041     return {};
1042 #endif
1043 }
1044 
1045 /*!
1046  * \brief Prints the error text corresponding to the status code \a status
1047  * \param status the status code of the error
1048  */
1049 void FITSFilterPrivate::printError(int status) const {
1050 #ifdef HAVE_FITS
1051     if (status) {
1052         char errorText[FLEN_ERRMSG];
1053         fits_get_errstatus(status, errorText);
1054         qDebug() << QLatin1String(errorText);
1055     }
1056 #else
1057     Q_UNUSED(status)
1058 #endif
1059 }
1060 
1061 /*!
1062  * \brief Add the keywords \a keywords to the current header unit
1063  * \param keywords the keywords to be added
1064  * \param fileName the name of the FITS file (extension) in which the keywords are added
1065  */
1066 
1067 void FITSFilterPrivate::addNewKeyword(const QString& fileName, const QList<FITSFilter::Keyword>& keywords) {
1068 #ifdef HAVE_FITS
1069     int status = 0;
1070     if (fits_open_file(&m_fitsFile, qPrintable(fileName), READWRITE, &status)) {
1071         printError(status);
1072         return;
1073     }
1074     for (const FITSFilter::Keyword& keyword : keywords) {
1075         status = 0;
1076         if (!keyword.key.compare(QLatin1String("COMMENT"))) {
1077             if (fits_write_comment(m_fitsFile, qPrintable(keyword.value), &status))
1078                 printError(status);
1079         } else if (!keyword.key.compare(QLatin1String("HISTORY"))) {
1080             if (fits_write_history(m_fitsFile, qPrintable(keyword.value), &status))
1081                 printError(status);
1082         } else if (!keyword.key.compare(QLatin1String("DATE"))) {
1083             if (fits_write_date(m_fitsFile, &status))
1084                 printError(status);
1085         } else {
1086             int ok = 0;
1087             if (keyword.key.length() <= FLEN_KEYWORD) {
1088                 ok++;
1089                 if (keyword.value.length() <= FLEN_VALUE) {
1090                     ok++;
1091                     if (keyword.comment.length() <= FLEN_COMMENT)
1092                         ok++;
1093                 }
1094             }
1095             if (ok == 3) {
1096                 bool ok;
1097                 double val = keyword.value.toDouble(&ok);
1098                 if (ok) {
1099                     if (fits_write_key(m_fitsFile, TDOUBLE, keyword.key.toLatin1().data(), &val, keyword.comment.toLatin1().data(), &status))
1100                         printError(status);
1101                 } else {
1102                     if (fits_write_key(m_fitsFile,
1103                                        TSTRING,
1104                                        keyword.key.toLatin1().data(),
1105                                        keyword.value.toLatin1().data(),
1106                                        keyword.comment.toLatin1().data(),
1107                                        &status))
1108                         printError(status);
1109                 }
1110             } else if (ok == 2) {
1111                 // comment too long
1112             } else if (ok == 1) {
1113                 // value too long
1114             } else {
1115                 // keyword too long
1116             }
1117         }
1118     }
1119     status = 0;
1120     fits_close_file(m_fitsFile, &status);
1121 #else
1122     Q_UNUSED(keywords)
1123     Q_UNUSED(fileName)
1124 
1125 #endif
1126 }
1127 /*!
1128  * \brief Update keywords in the current header unit
1129  * \param fileName The name of the FITS file (extension) in which the keywords will be updated
1130  * \param originals The original keywords of the FITS file (extension)
1131  * \param updates The keywords that contain the updated values
1132  */
1133 void FITSFilterPrivate::updateKeywords(const QString& fileName, const QList<FITSFilter::Keyword>& originals, const QVector<FITSFilter::Keyword>& updates) {
1134 #ifdef HAVE_FITS
1135     int status = 0;
1136     if (fits_open_file(&m_fitsFile, qPrintable(fileName), READWRITE, &status)) {
1137         printError(status);
1138         return;
1139     }
1140     FITSFilter::Keyword updatedKeyword;
1141     FITSFilter::Keyword originalKeyword;
1142     FITSFilter::KeywordUpdate keywordUpdate;
1143 
1144     for (int i = 0; i < updates.size(); ++i) {
1145         updatedKeyword = updates.at(i);
1146         originalKeyword = originals.at(i);
1147         keywordUpdate = originals.at(i).updates;
1148         if (keywordUpdate.keyUpdated && keywordUpdate.valueUpdated && keywordUpdate.commentUpdated) {
1149             if (updatedKeyword.isEmpty()) {
1150                 if (fits_delete_key(m_fitsFile, qPrintable(originalKeyword.key), &status)) {
1151                     printError(status);
1152                     status = 0;
1153                 }
1154                 continue;
1155             }
1156         }
1157         if (!updatedKeyword.key.isEmpty()) {
1158             if (fits_modify_name(m_fitsFile, qPrintable(originalKeyword.key), qPrintable(updatedKeyword.key), &status)) {
1159                 printError(status);
1160                 status = 0;
1161             }
1162         }
1163 
1164         if (!updatedKeyword.value.isEmpty()) {
1165             bool ok;
1166             int intValue;
1167             double doubleValue;
1168             bool updated = false;
1169 
1170             doubleValue = updatedKeyword.value.toDouble(&ok);
1171             if (ok) {
1172                 if (fits_update_key(m_fitsFile,
1173                                     TDOUBLE,
1174                                     keywordUpdate.keyUpdated ? qPrintable(updatedKeyword.key) : qPrintable(originalKeyword.key),
1175                                     &doubleValue,
1176                                     nullptr,
1177                                     &status))
1178                     printError(status);
1179                 else
1180                     updated = true;
1181             }
1182             if (!updated) {
1183                 intValue = updatedKeyword.value.toInt(&ok);
1184                 if (ok) {
1185                     if (fits_update_key(m_fitsFile,
1186                                         TINT,
1187                                         keywordUpdate.keyUpdated ? qPrintable(updatedKeyword.key) : qPrintable(originalKeyword.key),
1188                                         &intValue,
1189                                         nullptr,
1190                                         &status))
1191                         printError(status);
1192                     else
1193                         updated = true;
1194                 }
1195             }
1196             if (!updated) {
1197                 if (fits_update_key(m_fitsFile,
1198                                     TSTRING,
1199                                     keywordUpdate.keyUpdated ? qPrintable(updatedKeyword.key) : qPrintable(originalKeyword.key),
1200                                     updatedKeyword.value.toLatin1().data(),
1201                                     nullptr,
1202                                     &status))
1203                     printError(status);
1204             }
1205         } else {
1206             if (keywordUpdate.valueUpdated) {
1207                 if (fits_update_key_null(m_fitsFile,
1208                                          keywordUpdate.keyUpdated ? qPrintable(updatedKeyword.key) : qPrintable(originalKeyword.key),
1209                                          nullptr,
1210                                          &status)) {
1211                     printError(status);
1212                     status = 0;
1213                 }
1214             }
1215         }
1216 
1217         if (!updatedKeyword.comment.isEmpty()) {
1218             if (fits_modify_comment(m_fitsFile,
1219                                     keywordUpdate.keyUpdated ? qPrintable(updatedKeyword.key) : qPrintable(originalKeyword.key),
1220                                     updatedKeyword.comment.toLatin1().data(),
1221                                     &status)) {
1222                 printError(status);
1223                 status = 0;
1224             }
1225         } else {
1226             if (keywordUpdate.commentUpdated) {
1227                 if (fits_modify_comment(m_fitsFile,
1228                                         keywordUpdate.keyUpdated ? qPrintable(updatedKeyword.key) : qPrintable(originalKeyword.key),
1229                                         QByteArray().constData(),
1230                                         &status)) {
1231                     printError(status);
1232                     status = 0;
1233                 }
1234             }
1235         }
1236     }
1237     status = 0;
1238     fits_close_file(m_fitsFile, &status);
1239 #else
1240     Q_UNUSED(fileName)
1241     Q_UNUSED(originals)
1242     Q_UNUSED(updates)
1243 #endif
1244 }
1245 
1246 /*!
1247  * \brief Delete the keywords \a keywords from the current header unit
1248  * \param fileName the name of the FITS file (extension) in which the keywords will be deleted.
1249  * \param keywords the keywords to deleted
1250  */
1251 
1252 void FITSFilterPrivate::deleteKeyword(const QString& fileName, const QList<FITSFilter::Keyword>& keywords) {
1253 #ifdef HAVE_FITS
1254     int status = 0;
1255     if (fits_open_file(&m_fitsFile, qPrintable(fileName), READWRITE, &status)) {
1256         printError(status);
1257         return;
1258     }
1259     for (const auto& keyword : keywords) {
1260         if (!keyword.key.isEmpty()) {
1261             status = 0;
1262             if (fits_delete_key(m_fitsFile, qPrintable(keyword.key), &status))
1263                 printError(status);
1264         }
1265     }
1266     status = 0;
1267     fits_close_file(m_fitsFile, &status);
1268 #else
1269     Q_UNUSED(keywords)
1270     Q_UNUSED(fileName)
1271 #endif
1272 }
1273 
1274 /*!
1275  * \brief FITSFilterPrivate::addKeywordUnit
1276  * \param fileName the FITS file (extension) in which the keyword units are updated/added
1277  * \param keywords the keywords whose units were modified/added
1278  */
1279 
1280 void FITSFilterPrivate::addKeywordUnit(const QString& fileName, const QList<FITSFilter::Keyword>& keywords) {
1281 #ifdef HAVE_FITS
1282     int status = 0;
1283     if (fits_open_file(&m_fitsFile, qPrintable(fileName), READWRITE, &status)) {
1284         printError(status);
1285         return;
1286     }
1287 
1288     for (const FITSFilter::Keyword& keyword : keywords) {
1289         if (keyword.updates.unitUpdated) {
1290             if (fits_write_key_unit(m_fitsFile, qPrintable(keyword.key), keyword.unit.toLatin1().data(), &status)) {
1291                 printError(status);
1292                 status = 0;
1293             }
1294         }
1295     }
1296     status = 0;
1297     fits_close_file(m_fitsFile, &status);
1298 #else
1299     Q_UNUSED(fileName)
1300     Q_UNUSED(keywords)
1301 #endif
1302 }
1303 
1304 /*!
1305  * \brief Remove extensions from a FITS file
1306  * \param extensions The extensions to be removed from the FITS file
1307  */
1308 void FITSFilterPrivate::removeExtensions(const QStringList& extensions) {
1309 #ifdef HAVE_FITS
1310     int status = 0;
1311     for (const auto& ext : extensions) {
1312         status = 0;
1313         if (fits_open_file(&m_fitsFile, qPrintable(ext), READWRITE, &status)) {
1314             printError(status);
1315             continue;
1316         }
1317 
1318         if (fits_delete_hdu(m_fitsFile, nullptr, &status))
1319             printError(status);
1320 
1321         status = 0;
1322         fits_close_file(m_fitsFile, &status);
1323     }
1324 #else
1325     Q_UNUSED(extensions)
1326 #endif
1327 }
1328 
1329 /*!
1330  * \brief Returns a list of keywords in the current header of \a fileName
1331  * \param fileName the name of the FITS file (extension) to be opened
1332  * \return A list of keywords
1333  */
1334 QList<FITSFilter::Keyword> FITSFilterPrivate::chduKeywords(const QString& fileName) {
1335 #ifdef HAVE_FITS
1336     int status = 0;
1337 
1338     if (fits_open_file(&m_fitsFile, qPrintable(fileName), READONLY, &status)) {
1339         printError(status);
1340         return {};
1341     }
1342     int numberOfKeys;
1343     if (fits_get_hdrspace(m_fitsFile, &numberOfKeys, nullptr, &status)) {
1344         printError(status);
1345         return {};
1346     }
1347 
1348     QList<FITSFilter::Keyword> keywords;
1349     keywords.reserve(numberOfKeys);
1350     char* key = new char[FLEN_KEYWORD];
1351     char* value = new char[FLEN_VALUE];
1352     char* comment = new char[FLEN_COMMENT];
1353     char* unit = new char[FLEN_VALUE];
1354     for (int i = 1; i <= numberOfKeys; ++i) {
1355         QStringList recordValues;
1356         FITSFilter::Keyword keyword;
1357 
1358         if (fits_read_keyn(m_fitsFile, i, key, value, comment, &status)) {
1359             printError(status);
1360             status = 0;
1361             continue;
1362         }
1363 
1364         fits_read_key_unit(m_fitsFile, key, unit, &status);
1365 
1366         recordValues << QLatin1String(key) << QLatin1String(value) << QLatin1String(comment) << QLatin1String(unit);
1367 
1368         keyword.key = recordValues[0].simplified();
1369         keyword.value = recordValues[1].simplified();
1370         keyword.comment = recordValues[2].simplified();
1371         keyword.unit = recordValues[3].simplified();
1372 
1373         keywords.append(keyword);
1374     }
1375     delete[] key;
1376     delete[] value;
1377     delete[] comment;
1378     delete[] unit;
1379 
1380     fits_close_file(m_fitsFile, &status);
1381 
1382     return keywords;
1383 #else
1384     Q_UNUSED(fileName)
1385     return QList<FITSFilter::Keyword>();
1386 #endif
1387 }
1388 
1389 /*!
1390  * \brief Builds the table \a headerEditTable from the keywords \a keys
1391  * \param fileName The name of the FITS file from which the keys are read if \a readKeys is \c true
1392  * \param headerEditTable The table to be built
1393  * \param readKeys It's used to determine whether the keywords are provided or they should be read from
1394  * file \a fileName
1395  * \param keys The keywords that are provided if the keywords were read already.
1396  */
1397 void FITSFilterPrivate::parseHeader(const QString& fileName, QTableWidget* headerEditTable, bool readKeys, const QList<FITSFilter::Keyword>& keys) {
1398     DEBUG(Q_FUNC_INFO)
1399 #ifdef HAVE_FITS
1400     QList<FITSFilter::Keyword> keywords;
1401     if (readKeys)
1402         keywords = chduKeywords(fileName);
1403     else
1404         keywords = keys;
1405 
1406     headerEditTable->setRowCount(keywords.size());
1407     QTableWidgetItem* item;
1408     for (int i = 0; i < keywords.size(); ++i) {
1409         const FITSFilter::Keyword& keyword = keywords.at(i);
1410         const bool mandatory =
1411             FITSFilter::mandatoryImageExtensionKeywords().contains(keyword.key) || FITSFilter::mandatoryTableExtensionKeywords().contains(keyword.key);
1412         item = new QTableWidgetItem(keyword.key);
1413         const QString& itemText = item->text();
1414         const bool notEditableKey = mandatory || itemText.contains(QLatin1String("TFORM")) || itemText.contains(QLatin1String("TTYPE"))
1415             || itemText.contains(QLatin1String("TUNIT")) || itemText.contains(QLatin1String("TDISP")) || itemText.contains(QLatin1String("TBCOL"))
1416             || itemText.contains(QLatin1String("TZERO"));
1417         const bool notEditableValue = mandatory || itemText.contains(QLatin1String("TFORM")) || itemText.contains(QLatin1String("TDISP"))
1418             || itemText.contains(QLatin1String("TBCOL")) || itemText.contains(QLatin1String("TZERO"));
1419 
1420         if (notEditableKey)
1421             item->setFlags(item->flags() & ~Qt::ItemIsEditable);
1422         else
1423             item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1424         headerEditTable->setItem(i, 0, item);
1425 
1426         item = new QTableWidgetItem(keyword.value);
1427         if (notEditableValue)
1428             item->setFlags(item->flags() & ~Qt::ItemIsEditable);
1429         else
1430             item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1431         headerEditTable->setItem(i, 1, item);
1432         QString commentFieldText;
1433         if (!keyword.unit.isEmpty()) {
1434             if (keyword.updates.unitUpdated) {
1435                 const QString& comment = keyword.comment.right(keyword.comment.size() - keyword.comment.indexOf(QLatin1Char(']')) - 1);
1436                 commentFieldText = QLatin1String("[") + keyword.unit + QLatin1String("] ") + comment;
1437             } else {
1438                 if (keyword.comment.at(0) == QLatin1Char('['))
1439                     commentFieldText = keyword.comment;
1440                 else
1441                     commentFieldText = QStringLiteral("[") + keyword.unit + QStringLiteral("] ") + keyword.comment;
1442             }
1443         } else
1444             commentFieldText = keyword.comment;
1445         item = new QTableWidgetItem(commentFieldText);
1446 
1447         item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1448         headerEditTable->setItem(i, 2, item);
1449     }
1450 
1451     headerEditTable->resizeColumnsToContents();
1452 #else
1453     Q_UNUSED(fileName)
1454     Q_UNUSED(headerEditTable)
1455     Q_UNUSED(readKeys)
1456     Q_UNUSED(keys)
1457 #endif
1458 }
1459 
1460 /*!
1461  * \brief Helper function to return the value of the key \a key
1462  * \param fileName The name of the FITS file (extension) in which the keyword with key \a key should exist
1463  * \param key The key of the keyword whose value it's returned
1464  * \return The value of the keyword as a string
1465  */
1466 const QString FITSFilterPrivate::valueOf(const QString& fileName, const char* key) {
1467 #ifdef HAVE_FITS
1468     int status = 0;
1469     if (fits_open_file(&m_fitsFile, qPrintable(fileName), READONLY, &status)) {
1470         printError(status);
1471         return {};
1472     }
1473 
1474     char* keyVal = new char[FLEN_VALUE];
1475     QString keyValue;
1476     if (!fits_read_keyword(m_fitsFile, key, keyVal, nullptr, &status)) {
1477         keyValue = QLatin1String(keyVal);
1478         keyValue = keyValue.simplified();
1479     } else {
1480         printError(status);
1481         delete[] keyVal;
1482         fits_close_file(m_fitsFile, &status);
1483         return {};
1484     }
1485 
1486     delete[] keyVal;
1487     status = 0;
1488     fits_close_file(m_fitsFile, &status);
1489     return keyValue;
1490 #else
1491     Q_UNUSED(fileName)
1492     Q_UNUSED(key)
1493     return {};
1494 #endif
1495 }
1496 
1497 /*!
1498  * \brief Build the extensions tree from FITS file (extension) \a fileName
1499  * \param fileName The name of the FITS file to be opened
1500  * \param tw The QTreeWidget to be built
1501  * \param checkPrimary Used to determine whether the tree will be used for import or the header edit,
1502  * if it's \c true and if the primary array it's empty, then the item won't be added to the tree
1503  */
1504 void FITSFilterPrivate::parseExtensions(const QString& fileName, QTreeWidget* tw, bool checkPrimary) {
1505     DEBUG(Q_FUNC_INFO);
1506 #ifdef HAVE_FITS
1507     const QMultiMap<QString, QString>& extensions = extensionNames(fileName);
1508     const QStringList& imageExtensions = extensions.values(QLatin1String("IMAGES"));
1509     const QStringList& tableExtensions = extensions.values(QLatin1String("TABLES"));
1510 
1511     QTreeWidgetItem* root = tw->invisibleRootItem();
1512     // TODO: fileName may contain any data type: check if it's a FITS file
1513     auto* treeNameItem = new QTreeWidgetItem((QTreeWidgetItem*)nullptr, QStringList() << fileName);
1514     root->addChild(treeNameItem);
1515     treeNameItem->setExpanded(true);
1516 
1517     auto* imageExtensionItem = new QTreeWidgetItem((QTreeWidgetItem*)nullptr, QStringList() << i18n("Images"));
1518     imageExtensionItem->setFlags(imageExtensionItem->flags() & ~Qt::ItemIsSelectable);
1519     QString primaryHeaderNaxis = valueOf(fileName, "NAXIS");
1520     const int naxis = primaryHeaderNaxis.toInt();
1521     bool noImage = false;
1522     for (const QString& ext : imageExtensions) {
1523         auto* treeItem = new QTreeWidgetItem((QTreeWidgetItem*)nullptr, QStringList() << ext);
1524         if (ext == i18n("Primary header")) {
1525             if (checkPrimary && naxis == 0) {
1526                 delete treeItem;
1527                 continue;
1528             }
1529         }
1530         imageExtensionItem->addChild(treeItem);
1531     }
1532     if (imageExtensionItem->childCount() > 0) {
1533         treeNameItem->addChild(imageExtensionItem);
1534         imageExtensionItem->setIcon(0, QIcon::fromTheme(QStringLiteral("view-preview")));
1535         imageExtensionItem->setExpanded(true);
1536         imageExtensionItem->child(0)->setSelected(true);
1537 
1538         tw->setCurrentItem(imageExtensionItem->child(0));
1539     } else
1540         noImage = true;
1541 
1542     if (tableExtensions.size() > 0) {
1543         auto* tableExtensionItem = new QTreeWidgetItem((QTreeWidgetItem*)nullptr, QStringList() << i18n("Tables"));
1544         tableExtensionItem->setFlags(tableExtensionItem->flags() & ~Qt::ItemIsSelectable);
1545 
1546         for (const QString& ext : tableExtensions) {
1547             auto* treeItem = new QTreeWidgetItem((QTreeWidgetItem*)nullptr, QStringList() << ext);
1548             tableExtensionItem->addChild(treeItem);
1549         }
1550         if (tableExtensionItem->childCount() > 0) {
1551             treeNameItem->addChild(tableExtensionItem);
1552             tableExtensionItem->setIcon(0, QIcon::fromTheme(QStringLiteral("x-office-spreadsheet")));
1553             tableExtensionItem->setExpanded(true);
1554             if (noImage) {
1555                 tableExtensionItem->child(0)->setSelected(true);
1556                 tw->setCurrentItem(tableExtensionItem->child(0));
1557             }
1558         }
1559     }
1560 #else
1561     Q_UNUSED(fileName)
1562     Q_UNUSED(tw)
1563     Q_UNUSED(checkPrimary)
1564 #endif
1565     DEBUG(Q_FUNC_INFO << " DONE");
1566 }
1567 
1568 /*!
1569  * \brief FITSFilterPrivate::~FITSFilterPrivate
1570  */
1571 
1572 FITSFilterPrivate::~FITSFilterPrivate() = default;
1573 
1574 // ##############################################################################
1575 // ##################  Serialization/Deserialization  ###########################
1576 // ##############################################################################
1577 
1578 /*!
1579   Saves as XML.
1580 */
1581 
1582 void FITSFilter::save(QXmlStreamWriter*) const {
1583 }
1584 
1585 /*!
1586   Loads from XML.
1587 */
1588 
1589 bool FITSFilter::load(XmlStreamReader*) {
1590     return false;
1591 }