Warning, file /education/kstars/kstars/dialogs/catalogcsvimport.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002     SPDX-FileCopyrightText: 2021 Valentin Boettcher <hiro at protagon.space; @hiro98:tchncs.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "catalogcsvimport.h"
0008 #include "rapidcsv.h"
0009 #include "ui_catalogcsvimport.h"
0010 #include <QFileDialog>
0011 #include <QMessageBox>
0012 #include <QDebug>
0013 #include <QLabel>
0014 #include <QComboBox>
0015 #include <QFormLayout>
0016 
0017 /**
0018  * Maps the name of the field to a tuple [Tooltip, Unit, Can be ignored?]
0019  *
0020  * @todo Maybe centralize that in the catalogobject class.
0021  */
0022 const std::vector<std::pair<QString, std::tuple<QString, QString, bool>>> fields{
0023     { "Type", { "", "", true } },
0024     { "Right Ascension", { "", "", false } },
0025     { "Declination", { "", "", false } },
0026     { "Magnitude", { "", "", false } },
0027     { "Name", { "", "", false } },
0028     { "Long Name", { "", "", true } },
0029     { "Identifier", { "", "", true } },
0030     { "Major Axis", { "", "arcmin", true } },
0031     { "Minor Axis", { "", "arcmin", true } },
0032     { "Position Angle", { "", "°", true } },
0033     { "Flux", { "", "", true } },
0034 };
0035 
0036 QWidget *type_selector_widget()
0037 {
0038     auto *pWidget = new QWidget{}; // no parent as receiver takes posession
0039     auto *pLayout = new QHBoxLayout(pWidget);
0040     auto *pCombo  = new QComboBox{};
0041 
0042     for (int i = 0; i < SkyObject::TYPE::NUMBER_OF_KNOWN_TYPES; i++)
0043     {
0044         pCombo->addItem(SkyObject::typeName(i), i);
0045     }
0046     pLayout->setAlignment(Qt::AlignCenter);
0047     pLayout->setContentsMargins(0, 0, 0, 0);
0048     pLayout->addWidget(pCombo);
0049     pWidget->setLayout(pLayout);
0050 
0051     return pWidget;
0052 }
0053 
0054 CatalogCSVImport::CatalogCSVImport(QWidget *parent)
0055     : QDialog(parent), ui(new Ui::CatalogCSVImport)
0056 {
0057     ui->setupUi(this);
0058     reset_mapping();
0059 
0060     ui->separator->setText(QString(default_separator));
0061     ui->comment_prefix->setText(QString(default_comment));
0062 
0063     ui->preview->setModel(&m_preview_model);
0064     ui->preview->horizontalHeader()->setSectionResizeMode(
0065         QHeaderView::ResizeMode::Stretch);
0066 
0067     connect(ui->file_select_button, &QPushButton::clicked, this,
0068             &CatalogCSVImport::select_file);
0069 
0070     connect(ui->add_map, &QPushButton::clicked, this,
0071             &CatalogCSVImport::type_table_add_map);
0072 
0073     connect(ui->buttonBox, &QDialogButtonBox::accepted, this,
0074             &CatalogCSVImport::read_objects);
0075 
0076     connect(ui->remove_map, &QPushButton::clicked, this,
0077             &CatalogCSVImport::type_table_remove_map);
0078 
0079     connect(ui->preview_button, &QPushButton::clicked, this, [&]() {
0080         read_n_objects(default_preview_size);
0081         m_preview_model.setObjects(m_objects);
0082     });
0083 
0084     init_column_mapping();
0085     init_type_table();
0086 
0087     for (auto *box : { ui->ra_units, ui->dec_units })
0088     {
0089         box->addItem(i18n("Degrees"));
0090         box->addItem(i18n("Hours"));
0091     }
0092 }
0093 
0094 CatalogCSVImport::~CatalogCSVImport()
0095 {
0096     delete ui;
0097 }
0098 
0099 void CatalogCSVImport::select_file()
0100 {
0101     QFileDialog dialog(this, i18nc("@title:window", "Import Catalog"), QDir::homePath(),
0102                        QString("CSV") + i18n("File") + QString(" (*.csv);;") +
0103                            i18n("Any File") + QString(" (*);;"));
0104     dialog.setAcceptMode(QFileDialog::AcceptOpen);
0105     dialog.setDefaultSuffix("csv");
0106 
0107     if (dialog.exec() != QDialog::Accepted)
0108         return;
0109 
0110     const auto &fileName = dialog.selectedUrls().value(0).toLocalFile();
0111 
0112     if (!QFile::exists(fileName))
0113     {
0114         reset_mapping();
0115         QMessageBox::warning(this, i18n("Warning"),
0116                              i18n("Could not open the csv file.<br>It does not exist."));
0117 
0118         return;
0119     }
0120 
0121     if (ui->separator->text().length() < 1)
0122     {
0123         ui->separator->setText(QString(default_separator));
0124     }
0125 
0126     if (ui->comment_prefix->text().length() < 1)
0127     {
0128         ui->separator->setText(QString(default_separator));
0129     }
0130 
0131     const auto &separator      = ui->separator->text();
0132     const auto &comment_prefix = ui->comment_prefix->text();
0133 
0134     ui->file_path_label->setText(fileName);
0135 
0136     m_doc.Load(fileName.toStdString(), rapidcsv::LabelParams(),
0137                rapidcsv::SeparatorParams(separator[0].toLatin1(), true),
0138                rapidcsv::ConverterParams(false),
0139                rapidcsv::LineReaderParams(true, comment_prefix[0].toLatin1()));
0140 
0141     init_mapping_selectors();
0142 };
0143 
0144 void CatalogCSVImport::reset_mapping()
0145 {
0146     ui->column_mapping->setEnabled(false);
0147     ui->preview_button->setEnabled(false);
0148     ui->obj_count->setEnabled(false);
0149     m_doc.Clear();
0150     ui->buttonBox->buttons()[0]->setEnabled(false);
0151 };
0152 
0153 void CatalogCSVImport::init_mapping_selectors()
0154 {
0155     const auto &columns = m_doc.GetColumnNames();
0156 
0157     for (const auto &field : fields)
0158     {
0159         const auto can_be_ignored = std::get<2>(field.second);
0160         auto *selector            = m_selectors[field.first];
0161 
0162         selector->clear();
0163 
0164         selector->setMaxCount(columns.size() + (can_be_ignored ? 0 : 1));
0165 
0166         if (can_be_ignored)
0167             selector->addItem(i18n("Ignore"), -2);
0168 
0169         int i = 0;
0170         for (const auto &col : columns)
0171             selector->addItem(col.c_str(), i++);
0172     }
0173 
0174     ui->column_mapping->setEnabled(true);
0175     ui->buttonBox->buttons()[0]->setEnabled(true);
0176     ui->obj_count->setEnabled(true);
0177     ui->preview_button->setEnabled(true);
0178     ui->obj_count->setText(i18np("%1 Object", "%1 Objects", m_doc.GetRowCount()));
0179 };
0180 
0181 void CatalogCSVImport::init_type_table()
0182 {
0183     auto *const table = ui->type_table;
0184     table->setHorizontalHeaderLabels(QStringList() << i18n("Text") << i18n("Type"));
0185 
0186     table->setColumnCount(2);
0187     table->setRowCount(1);
0188     auto *item = new QTableWidgetItem{ i18n("default") };
0189     item->setFlags(Qt::NoItemFlags);
0190     table->setItem(0, 0, item);
0191 
0192     table->setCellWidget(0, 1, type_selector_widget());
0193     table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch);
0194 };
0195 
0196 void CatalogCSVImport::type_table_add_map()
0197 {
0198     auto *const table  = ui->type_table;
0199     const auto cur_row = table->rowCount();
0200 
0201     table->setRowCount(cur_row + 1);
0202     table->setItem(cur_row, 0, new QTableWidgetItem{ "" });
0203     table->setCellWidget(cur_row, 1, type_selector_widget());
0204 };
0205 
0206 void CatalogCSVImport::type_table_remove_map()
0207 {
0208     auto *const table = ui->type_table;
0209 
0210     for (const auto *item : table->selectedItems())
0211     {
0212         const auto row = item->row();
0213         if (row > 0)
0214             table->removeRow(row);
0215     }
0216 };
0217 
0218 CatalogCSVImport::type_map CatalogCSVImport::get_type_mapping()
0219 {
0220     auto *const table = ui->type_table;
0221     type_map map{};
0222 
0223     for (int i = 0; i <= table->rowCount(); i++)
0224     {
0225         const auto *key  = table->item(i, 0);
0226         const auto *type = table->cellWidget(i, 1);
0227 
0228         if (key == nullptr || type == nullptr)
0229             continue;
0230 
0231         map[key->text().toStdString()] = static_cast<SkyObject::TYPE>(
0232             dynamic_cast<QComboBox *>(type->layout()->itemAt(0)->widget())
0233                 ->currentData()
0234                 .toInt());
0235     }
0236 
0237     return map;
0238 };
0239 
0240 void CatalogCSVImport::init_column_mapping()
0241 {
0242     auto *cmapping = new QFormLayout();
0243     for (const auto &field : fields)
0244     {
0245         auto name           = field.first;
0246         const auto &tooltip = std::get<0>(field.second);
0247         const auto &unit    = std::get<1>(field.second);
0248 
0249         if (unit.length() > 0)
0250             name += QString(" [%1]").arg(unit);
0251 
0252         auto *label = new QLabel(name);
0253         label->setToolTip(tooltip);
0254 
0255         auto *selector = new QComboBox();
0256         selector->setEditable(true);
0257         m_selectors[field.first] = selector;
0258 
0259         cmapping->addRow(label, selector);
0260     }
0261 
0262     ui->column_mapping->setLayout(cmapping);
0263 };
0264 
0265 CatalogCSVImport::column_map CatalogCSVImport::get_column_mapping()
0266 {
0267     CatalogCSVImport::column_map map{};
0268     const auto &names = m_doc.GetColumnNames();
0269 
0270     for (const auto &item : m_selectors)
0271     {
0272         const auto &name     = item.first;
0273         const auto *selector = item.second;
0274         auto selected_value  = selector->currentData().toInt();
0275 
0276         QString val_string;
0277         if (selected_value >= 0 &&
0278             (selector->currentText() != names[selected_value].c_str()))
0279         {
0280             selected_value = -1;
0281             val_string     = selector->currentText();
0282         }
0283         else
0284         {
0285             val_string = "";
0286         }
0287 
0288         map[name] = { selected_value, val_string };
0289     }
0290 
0291     return map;
0292 };
0293 
0294 void CatalogCSVImport::read_n_objects(size_t n)
0295 {
0296     const auto &type_map   = get_type_mapping();
0297     const auto &column_map = get_column_mapping();
0298     const CatalogObject defaults{};
0299 
0300     m_objects.clear();
0301     m_objects.reserve(std::min(m_doc.GetRowCount(), n));
0302 
0303     //  pure magic, it's like LISP macros
0304     const auto make_getter = [this, &column_map](const QString &field, auto def) {
0305         const auto &conf        = column_map.at(field);
0306         const auto &default_val = get_default(conf, def);
0307         const auto index        = conf.first;
0308 
0309         std::function<decltype(def)(const size_t)> getter;
0310         if (conf.first >= 0)
0311             getter = [=](const size_t row) {
0312                 try
0313                 {
0314                     return m_doc.GetCell<decltype(def)>(index, row);
0315                 }
0316                 catch (...)
0317                 {
0318                     return default_val;
0319                 };
0320             };
0321         else
0322             getter = [=](const size_t) { return default_val; };
0323 
0324         return getter;
0325     };
0326 
0327     const auto make_coord_getter = [this, &column_map](const QString &field, auto def,
0328                                                        coord_unit unit) {
0329         const auto &conf = column_map.at(field);
0330         const auto default_val =
0331             (unit == coord_unit::deg) ?
0332                 get_default<typed_dms<coord_unit::deg>>(column_map.at(field), { def })
0333                     .data :
0334                 get_default<typed_dms<coord_unit::hours>>(column_map.at(field), { def })
0335                     .data;
0336         const auto index = conf.first;
0337 
0338         std::function<decltype(def)(const size_t)> getter;
0339         if (conf.first >= 0)
0340         {
0341             if (unit == coord_unit::deg)
0342                 getter = [=](const size_t row) {
0343                     try
0344                     {
0345                         return m_doc.GetCell<typed_dms<coord_unit::deg>>(index, row).data;
0346                     }
0347                     catch (...)
0348                     {
0349                         return default_val;
0350                     };
0351                 };
0352             else
0353                 getter = [=](const size_t row) {
0354                     try
0355                     {
0356                         return m_doc.GetCell<typed_dms<coord_unit::hours>>(index, row)
0357                             .data;
0358                     }
0359                     catch (...)
0360                     {
0361                         return default_val;
0362                     };
0363                 };
0364         }
0365         else
0366             getter = [=](const size_t) { return default_val; };
0367 
0368         return getter;
0369     };
0370 
0371     const auto ra_type  = static_cast<coord_unit>(ui->ra_units->currentIndex());
0372     const auto dec_type = static_cast<coord_unit>(ui->dec_units->currentIndex());
0373 
0374     const auto get_ra   = make_coord_getter("Right Ascension", defaults.ra(), ra_type);
0375     const auto get_dec  = make_coord_getter("Declination", defaults.dec(), dec_type);
0376     const auto get_mag  = make_getter("Magnitude", defaults.mag());
0377     const auto get_name = make_getter("Name", defaults.name());
0378     const auto get_type = make_getter("Type", std::string{ "default" });
0379     const auto get_long_name  = make_getter("Long Name", defaults.name());
0380     const auto get_identifier = make_getter("Identifier", defaults.catalogIdentifier());
0381     const auto get_a          = make_getter("Major Axis", defaults.a());
0382     const auto get_b          = make_getter("Minor Axis", defaults.b());
0383     const auto get_pa         = make_getter("Position Angle", defaults.pa());
0384     const auto get_flux       = make_getter("Flux", defaults.flux());
0385 
0386     for (size_t i = 0; i < std::min(m_doc.GetRowCount(), n); i++)
0387     {
0388         const auto &raw_type = get_type(i);
0389 
0390         const auto type = parse_type(raw_type, type_map);
0391 
0392         const auto ra         = get_ra(i);
0393         const auto dec        = get_dec(i);
0394         const auto mag        = get_mag(i);
0395         const auto name       = get_name(i);
0396         const auto long_name  = get_long_name(i);
0397         const auto identifier = get_identifier(i);
0398         const auto a          = get_a(i);
0399         const auto b          = get_b(i);
0400         const auto pa         = get_pa(i);
0401         const auto flux       = get_flux(i);
0402 
0403         m_objects.emplace_back(CatalogObject::oid{}, type, ra, dec, mag, name, long_name,
0404                                identifier, -1, a, b, pa, flux);
0405     }
0406 };
0407 
0408 SkyObject::TYPE CatalogCSVImport::parse_type(const std::string &type,
0409                                              const type_map &type_map)
0410 {
0411     if (type_map.count(type) == 0)
0412         return type_map.at("default");
0413 
0414     return type_map.at(type);
0415 };