1 /*
2     SPDX-FileCopyrightText: 2021 Valentin Boettcher <hiro at protagon.space; @hiro98:tchncs.de>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "catalogcsvimport.h"
8 #include "rapidcsv.h"
9 #include "ui_catalogcsvimport.h"
10 #include <QFileDialog>
11 #include <QMessageBox>
12 #include <QDebug>
13 #include <QLabel>
14 #include <QComboBox>
15 #include <QFormLayout>
16 
17 /**
18  * Maps the name of the field to a tuple [Tooltip, Unit, Can be ignored?]
19  *
20  * @todo Maybe centralize that in the catalogobject class.
21  */
22 const std::vector<std::pair<QString, std::tuple<QString, QString, bool>>> fields{
23     { "Type", { "", "", true } },
24     { "Right Ascension", { "", "", false } },
25     { "Declination", { "", "", false } },
26     { "Magnitude", { "", "", false } },
27     { "Name", { "", "", false } },
28     { "Long Name", { "", "", true } },
29     { "Identifier", { "", "", true } },
30     { "Major Axis", { "", "arcmin", true } },
31     { "Minor Axis", { "", "arcmin", true } },
32     { "Position Angle", { "", "°", true } },
33     { "Flux", { "", "", true } },
34 };
35 
type_selector_widget()36 QWidget *type_selector_widget()
37 {
38     auto *pWidget = new QWidget{}; // no parent as receiver takes posession
39     auto *pLayout = new QHBoxLayout(pWidget);
40     auto *pCombo  = new QComboBox{};
41 
42     for (int i = 0; i < SkyObject::TYPE::NUMBER_OF_KNOWN_TYPES; i++)
43     {
44         pCombo->addItem(SkyObject::typeName(i), i);
45     }
46     pLayout->setAlignment(Qt::AlignCenter);
47     pLayout->setContentsMargins(0, 0, 0, 0);
48     pLayout->addWidget(pCombo);
49     pWidget->setLayout(pLayout);
50 
51     return pWidget;
52 }
53 
CatalogCSVImport(QWidget * parent)54 CatalogCSVImport::CatalogCSVImport(QWidget *parent)
55     : QDialog(parent), ui(new Ui::CatalogCSVImport)
56 {
57     ui->setupUi(this);
58     reset_mapping();
59 
60     ui->separator->setText(QString(default_separator));
61     ui->comment_prefix->setText(QString(default_comment));
62 
63     ui->preview->setModel(&m_preview_model);
64     ui->preview->horizontalHeader()->setSectionResizeMode(
65         QHeaderView::ResizeMode::Stretch);
66 
67     connect(ui->file_select_button, &QPushButton::clicked, this,
68             &CatalogCSVImport::select_file);
69 
70     connect(ui->add_map, &QPushButton::clicked, this,
71             &CatalogCSVImport::type_table_add_map);
72 
73     connect(ui->buttonBox, &QDialogButtonBox::accepted, this,
74             &CatalogCSVImport::read_objects);
75 
76     connect(ui->remove_map, &QPushButton::clicked, this,
77             &CatalogCSVImport::type_table_remove_map);
78 
79     connect(ui->preview_button, &QPushButton::clicked, this, [&]() {
80         read_n_objects(default_preview_size);
81         m_preview_model.setObjects(m_objects);
82     });
83 
84     init_column_mapping();
85     init_type_table();
86 
87     for (auto *box : { ui->ra_units, ui->dec_units })
88     {
89         box->addItem(i18n("Degrees"));
90         box->addItem(i18n("Hours"));
91     }
92 }
93 
~CatalogCSVImport()94 CatalogCSVImport::~CatalogCSVImport()
95 {
96     delete ui;
97 }
98 
select_file()99 void CatalogCSVImport::select_file()
100 {
101     QFileDialog dialog(this, i18nc("@title:window", "Import Catalog"), QDir::homePath(),
102                        QString("CSV") + i18n("File") + QString(" (*.csv);;") +
103                            i18n("Any File") + QString(" (*);;"));
104     dialog.setAcceptMode(QFileDialog::AcceptOpen);
105     dialog.setDefaultSuffix("csv");
106 
107     if (dialog.exec() != QDialog::Accepted)
108         return;
109 
110     const auto &fileName = dialog.selectedUrls().value(0).toLocalFile();
111 
112     if (!QFile::exists(fileName))
113     {
114         reset_mapping();
115         QMessageBox::warning(this, i18n("Warning"),
116                              i18n("Could not open the csv file.<br>It does not exist."));
117 
118         return;
119     }
120 
121     if (ui->separator->text().length() < 1)
122     {
123         ui->separator->setText(QString(default_separator));
124     }
125 
126     if (ui->comment_prefix->text().length() < 1)
127     {
128         ui->separator->setText(QString(default_separator));
129     }
130 
131     const auto &separator      = ui->separator->text();
132     const auto &comment_prefix = ui->comment_prefix->text();
133 
134     ui->file_path_label->setText(fileName);
135 
136     m_doc.Load(fileName.toStdString(), rapidcsv::LabelParams(),
137                rapidcsv::SeparatorParams(separator[0].toLatin1(), true),
138                rapidcsv::ConverterParams(false),
139                rapidcsv::LineReaderParams(true, comment_prefix[0].toLatin1()));
140 
141     init_mapping_selectors();
142 };
143 
reset_mapping()144 void CatalogCSVImport::reset_mapping()
145 {
146     ui->column_mapping->setEnabled(false);
147     ui->preview_button->setEnabled(false);
148     ui->obj_count->setEnabled(false);
149     m_doc.Clear();
150     ui->buttonBox->buttons()[0]->setEnabled(false);
151 };
152 
init_mapping_selectors()153 void CatalogCSVImport::init_mapping_selectors()
154 {
155     const auto &columns = m_doc.GetColumnNames();
156 
157     for (const auto &field : fields)
158     {
159         const auto can_be_ignored = std::get<2>(field.second);
160         auto *selector            = m_selectors[field.first];
161 
162         selector->clear();
163 
164         selector->setMaxCount(columns.size() + (can_be_ignored ? 0 : 1));
165 
166         if (can_be_ignored)
167             selector->addItem(i18n("Ignore"), -2);
168 
169         int i = 0;
170         for (const auto &col : columns)
171             selector->addItem(col.c_str(), i++);
172     }
173 
174     ui->column_mapping->setEnabled(true);
175     ui->buttonBox->buttons()[0]->setEnabled(true);
176     ui->obj_count->setEnabled(true);
177     ui->preview_button->setEnabled(true);
178     ui->obj_count->setText(i18np("%1 Object", "%1 Objects", m_doc.GetRowCount()));
179 };
180 
init_type_table()181 void CatalogCSVImport::init_type_table()
182 {
183     auto *const table = ui->type_table;
184     table->setHorizontalHeaderLabels(QStringList() << i18n("Text") << i18n("Type"));
185 
186     table->setColumnCount(2);
187     table->setRowCount(1);
188     auto *item = new QTableWidgetItem{ i18n("default") };
189     item->setFlags(Qt::NoItemFlags);
190     table->setItem(0, 0, item);
191 
192     table->setCellWidget(0, 1, type_selector_widget());
193     table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch);
194 };
195 
type_table_add_map()196 void CatalogCSVImport::type_table_add_map()
197 {
198     auto *const table  = ui->type_table;
199     const auto cur_row = table->rowCount();
200 
201     table->setRowCount(cur_row + 1);
202     table->setItem(cur_row, 0, new QTableWidgetItem{ "" });
203     table->setCellWidget(cur_row, 1, type_selector_widget());
204 };
205 
type_table_remove_map()206 void CatalogCSVImport::type_table_remove_map()
207 {
208     auto *const table = ui->type_table;
209 
210     for (const auto *item : table->selectedItems())
211     {
212         const auto row = item->row();
213         if (row > 0)
214             table->removeRow(row);
215     }
216 };
217 
get_type_mapping()218 CatalogCSVImport::type_map CatalogCSVImport::get_type_mapping()
219 {
220     auto *const table = ui->type_table;
221     type_map map{};
222 
223     for (int i = 0; i <= table->rowCount(); i++)
224     {
225         const auto *key  = table->item(i, 0);
226         const auto *type = table->cellWidget(i, 1);
227 
228         if (key == nullptr || type == nullptr)
229             continue;
230 
231         map[key->text().toStdString()] = static_cast<SkyObject::TYPE>(
232             dynamic_cast<QComboBox *>(type->layout()->itemAt(0)->widget())
233                 ->currentData()
234                 .toInt());
235     }
236 
237     return map;
238 };
239 
init_column_mapping()240 void CatalogCSVImport::init_column_mapping()
241 {
242     auto *cmapping = new QFormLayout();
243     for (const auto &field : fields)
244     {
245         auto name           = field.first;
246         const auto &tooltip = std::get<0>(field.second);
247         const auto &unit    = std::get<1>(field.second);
248 
249         if (unit.length() > 0)
250             name += QString(" [%1]").arg(unit);
251 
252         auto *label = new QLabel(name);
253         label->setToolTip(tooltip);
254 
255         auto *selector = new QComboBox();
256         selector->setEditable(true);
257         m_selectors[field.first] = selector;
258 
259         cmapping->addRow(label, selector);
260     }
261 
262     ui->column_mapping->setLayout(cmapping);
263 };
264 
get_column_mapping()265 CatalogCSVImport::column_map CatalogCSVImport::get_column_mapping()
266 {
267     CatalogCSVImport::column_map map{};
268     const auto &names = m_doc.GetColumnNames();
269 
270     for (const auto &item : m_selectors)
271     {
272         const auto &name     = item.first;
273         const auto *selector = item.second;
274         auto selected_value  = selector->currentData().toInt();
275 
276         QString val_string;
277         if (selected_value >= 0 &&
278             (selector->currentText() != names[selected_value].c_str()))
279         {
280             selected_value = -1;
281             val_string     = selector->currentText();
282         }
283         else
284         {
285             val_string = "";
286         }
287 
288         map[name] = { selected_value, val_string };
289     }
290 
291     return map;
292 };
293 
read_n_objects(size_t n)294 void CatalogCSVImport::read_n_objects(size_t n)
295 {
296     const auto &type_map   = get_type_mapping();
297     const auto &column_map = get_column_mapping();
298     const CatalogObject defaults{};
299 
300     m_objects.clear();
301     m_objects.reserve(std::min(m_doc.GetRowCount(), n));
302 
303     //  pure magic, it's like LISP macros
304     const auto make_getter = [this, &column_map](const QString &field, auto def) {
305         const auto &conf        = column_map.at(field);
306         const auto &default_val = get_default(conf, def);
307         const auto index        = conf.first;
308 
309         std::function<decltype(def)(const size_t)> getter;
310         if (conf.first >= 0)
311             getter = [=](const size_t row) {
312                 try
313                 {
314                     return m_doc.GetCell<decltype(def)>(index, row);
315                 }
316                 catch (...)
317                 {
318                     return default_val;
319                 };
320             };
321         else
322             getter = [=](const size_t) { return default_val; };
323 
324         return getter;
325     };
326 
327     const auto make_coord_getter = [this, &column_map](const QString &field, auto def,
328                                                        coord_unit unit) {
329         const auto &conf = column_map.at(field);
330         const auto default_val =
331             (unit == coord_unit::deg) ?
332                 get_default<typed_dms<coord_unit::deg>>(column_map.at(field), { def })
333                     .data :
334                 get_default<typed_dms<coord_unit::hours>>(column_map.at(field), { def })
335                     .data;
336         const auto index = conf.first;
337 
338         std::function<decltype(def)(const size_t)> getter;
339         if (conf.first >= 0)
340         {
341             if (unit == coord_unit::deg)
342                 getter = [=](const size_t row) {
343                     try
344                     {
345                         return m_doc.GetCell<typed_dms<coord_unit::deg>>(index, row).data;
346                     }
347                     catch (...)
348                     {
349                         return default_val;
350                     };
351                 };
352             else
353                 getter = [=](const size_t row) {
354                     try
355                     {
356                         return m_doc.GetCell<typed_dms<coord_unit::hours>>(index, row)
357                             .data;
358                     }
359                     catch (...)
360                     {
361                         return default_val;
362                     };
363                 };
364         }
365         else
366             getter = [=](const size_t) { return default_val; };
367 
368         return getter;
369     };
370 
371     const auto ra_type  = static_cast<coord_unit>(ui->ra_units->currentIndex());
372     const auto dec_type = static_cast<coord_unit>(ui->dec_units->currentIndex());
373 
374     const auto get_ra   = make_coord_getter("Right Ascension", defaults.ra(), ra_type);
375     const auto get_dec  = make_coord_getter("Declination", defaults.dec(), dec_type);
376     const auto get_mag  = make_getter("Magnitude", defaults.mag());
377     const auto get_name = make_getter("Name", defaults.name());
378     const auto get_type = make_getter("Type", std::string{ "default" });
379     const auto get_long_name  = make_getter("Long Name", defaults.name());
380     const auto get_identifier = make_getter("Identifier", defaults.catalogIdentifier());
381     const auto get_a          = make_getter("Major Axis", defaults.a());
382     const auto get_b          = make_getter("Minor Axis", defaults.b());
383     const auto get_pa         = make_getter("Position Angle", defaults.pa());
384     const auto get_flux       = make_getter("Flux", defaults.flux());
385 
386     for (size_t i = 0; i < std::min(m_doc.GetRowCount(), n); i++)
387     {
388         const auto &raw_type = get_type(i);
389 
390         const auto type = parse_type(raw_type, type_map);
391 
392         const auto ra         = get_ra(i);
393         const auto dec        = get_dec(i);
394         const auto mag        = get_mag(i);
395         const auto name       = get_name(i);
396         const auto long_name  = get_long_name(i);
397         const auto identifier = get_identifier(i);
398         const auto a          = get_a(i);
399         const auto b          = get_b(i);
400         const auto pa         = get_pa(i);
401         const auto flux       = get_flux(i);
402 
403         m_objects.emplace_back(CatalogObject::oid{}, type, ra, dec, mag, name, long_name,
404                                identifier, -1, a, b, pa, flux);
405     }
406 };
407 
parse_type(const std::string & type,const type_map & type_map)408 SkyObject::TYPE CatalogCSVImport::parse_type(const std::string &type,
409                                              const type_map &type_map)
410 {
411     if (type_map.count(type) == 0)
412         return type_map.at("default");
413 
414     return type_map.at(type);
415 };
416