File indexing completed on 2025-02-16 09:47:17
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 };