1 /*
2 * Solar System editor plug-in for Stellarium
3 *
4 * Copyright (C) 2010 Bogdan Marinov
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
19 */
20
21 #include "SolarSystemEditor.hpp"
22
23 #include "MpcImportWindow.hpp"
24 #include "ui_mpcImportWindow.h"
25
26 #include "StelApp.hpp"
27 #include "StelFileMgr.hpp"
28 #include "StelJsonParser.hpp"
29 #include "StelModuleMgr.hpp"
30 #include "StelTranslator.hpp"
31 #include "SolarSystem.hpp"
32 #include "StelProgressController.hpp"
33 #include "SearchDialog.hpp"
34 #include "StelUtils.hpp"
35
36 #include <QGuiApplication>
37 #include <QClipboard>
38 #include <QDesktopServices>
39 #include <QFileDialog>
40 #include <QSortFilterProxyModel>
41 #include <QHash>
42 #include <QList>
43 #include <QNetworkAccessManager>
44 #include <QNetworkRequest>
45 #include <QNetworkReply>
46 #include <QStandardItemModel>
47 #include <QString>
48 #include <QTemporaryFile>
49 #include <QTimer>
50 #include <QUrl>
51 #include <QUrlQuery>
52 #include <QDir>
53 #include <QRegularExpression>
54 #include <stdexcept>
55
MpcImportWindow()56 MpcImportWindow::MpcImportWindow()
57 : StelDialog("SolarSystemEditorMPCimport")
58 , importType(ImportType())
59 , downloadReply(Q_NULLPTR)
60 , queryReply(Q_NULLPTR)
61 , downloadProgressBar(Q_NULLPTR)
62 , queryProgressBar(Q_NULLPTR)
63 , countdown(0)
64 {
65 ui = new Ui_mpcImportWindow();
66 ssoManager = GETSTELMODULE(SolarSystemEditor);
67
68 networkManager = StelApp::getInstance().getNetworkAccessManager();
69
70 countdownTimer = new QTimer(this);
71
72 QHash<QString,QString> asteroidBookmarks;
73 QHash<QString,QString> cometBookmarks;
74 bookmarks.insert(MpcComets, cometBookmarks);
75 bookmarks.insert(MpcMinorPlanets, asteroidBookmarks);
76
77 candidateObjectsModel = new QStandardItemModel(this);
78 }
79
~MpcImportWindow()80 MpcImportWindow::~MpcImportWindow()
81 {
82 delete ui;
83 delete countdownTimer;
84 candidateObjectsModel->clear();
85 delete candidateObjectsModel;
86 if (downloadReply)
87 downloadReply->deleteLater();
88 if (queryReply)
89 queryReply->deleteLater();
90 if (downloadProgressBar)
91 StelApp::getInstance().removeProgressBar(downloadProgressBar);
92 if (queryProgressBar)
93 StelApp::getInstance().removeProgressBar(queryProgressBar);
94 }
95
createDialogContent()96 void MpcImportWindow::createDialogContent()
97 {
98 ui->setupUi(dialog);
99
100 //Signals
101 connect(&StelApp::getInstance(), SIGNAL(languageChanged()), this, SLOT(retranslate()));
102 connect(ui->closeStelWindow, SIGNAL(clicked()), this, SLOT(close()));
103 connect(ui->TitleBar, SIGNAL(movedTo(QPoint)), this, SLOT(handleMovedTo(QPoint)));
104
105 connect(ui->pushButtonAcquire, SIGNAL(clicked()),
106 this, SLOT(acquireObjectData()));
107 connect(ui->pushButtonAbortDownload, SIGNAL(clicked()),
108 this, SLOT(abortDownload()));
109 connect(ui->pushButtonAdd, SIGNAL(clicked()), this, SLOT(addObjects()));
110 connect(ui->pushButtonDiscard, SIGNAL(clicked()),
111 this, SLOT(discardObjects()));
112
113 connect(ui->pushButtonBrowse, SIGNAL(clicked()), this, SLOT(selectFile()));
114 connect(ui->comboBoxBookmarks, SIGNAL(currentIndexChanged(QString)),
115 this, SLOT(bookmarkSelected(QString)));
116
117 connect(ui->radioButtonFile, SIGNAL(toggled(bool)),
118 ui->frameFile, SLOT(setVisible(bool)));
119 connect(ui->radioButtonURL, SIGNAL(toggled(bool)),
120 ui->frameURL, SLOT(setVisible(bool)));
121
122 connect(ui->radioButtonAsteroids, SIGNAL(toggled(bool)),
123 this, SLOT(switchImportType(bool)));
124 connect(ui->radioButtonComets, SIGNAL(toggled(bool)),
125 this, SLOT(switchImportType(bool)));
126
127 connect(ui->pushButtonMarkAll, SIGNAL(clicked()),
128 this, SLOT(markAll()));
129 connect(ui->pushButtonMarkNone, SIGNAL(clicked()),
130 this, SLOT(unmarkAll()));
131
132 connect(ui->pushButtonSendQuery, SIGNAL(clicked()),
133 this, SLOT(sendQuery()));
134 connect(ui->lineEditQuery, SIGNAL(returnPressed()),
135 this, SLOT(sendQuery()));
136 connect(ui->pushButtonAbortQuery, SIGNAL(clicked()),
137 this, SLOT(abortQuery()));
138 connect(ui->lineEditQuery, SIGNAL(textEdited(QString)),
139 this, SLOT(resetNotFound()));
140 //connect(ui->lineEditQuery, SIGNAL(editingFinished()), this, SLOT(sendQuery()));
141 connect(countdownTimer, SIGNAL(timeout()), this, SLOT(updateCountdown()));
142
143 QSortFilterProxyModel * filterProxyModel = new QSortFilterProxyModel(this);
144 filterProxyModel->setSourceModel(candidateObjectsModel);
145 filterProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
146 ui->listViewObjects->setModel(filterProxyModel);
147 connect(ui->lineEditSearch, SIGNAL(textChanged(const QString&)),
148 filterProxyModel, SLOT(setFilterFixedString(const QString&)));
149
150 loadBookmarks();
151 updateTexts();
152
153 resetCountdown();
154 resetDialog();
155 }
156
updateTexts()157 void MpcImportWindow::updateTexts()
158 {
159 QString linkText("<a href=\"https://www.minorplanetcenter.net/iau/MPEph/MPEph.html\">Minor Planet & Comet Ephemeris Service</a>");
160 // TRANSLATORS: A link showing the text "Minor Planet & Comet Ephemeris Service" is inserted.
161 QString queryString(q_("Query the MPC's %1:"));
162 ui->labelQueryLink->setText(QString(queryString).arg(linkText));
163
164 QString firstLine(q_("Only one result will be returned if the query is successful."));
165 QString secondLine(q_("Both comets and asteroids can be identified with their number, name (in English) or provisional designation."));
166 QString cPrefix("<b>C/</b>");
167 QString pPrefix("<b>P/</b>");
168 QString cometQuery("<tt>C/Halley</tt>");
169 QString cometName("1P/Halley");
170 QString asteroidQuery("<tt>Halley</tt>");
171 QString asteroidName("(2688) Halley");
172 QString nameWarning(q_("Comet <i>names</i> need to be prefixed with %1 or %2. If more than one comet matches a name, only the first result will be returned. For example, searching for \"%3\" will return %4, Halley's Comet, but a search for \"%5\" will return the asteroid %6."));
173 QString thirdLine = QString(nameWarning).arg(cPrefix, pPrefix, cometQuery,
174 cometName, asteroidQuery,
175 asteroidName);
176 ui->labelQueryInstructions->setText(QString("%1<br/>%2<br/>%3").arg(firstLine, secondLine, thirdLine));
177 }
178
resetDialog()179 void MpcImportWindow::resetDialog()
180 {
181 ui->stackedWidget->setCurrentIndex(0);
182
183 //ui->tabWidget->setCurrentIndex(0);
184 ui->groupBoxType->setVisible(true);
185 ui->radioButtonAsteroids->setChecked(true);
186
187 ui->radioButtonURL->setChecked(true);
188 ui->frameFile->setVisible(false);
189
190 ui->lineEditFilePath->clear();
191 ui->lineEditQuery->clear();
192 ui->lineEditURL->setText("https://");
193 ui->checkBoxAddBookmark->setChecked(false);
194 ui->frameBookmarkTitle->setVisible(false);
195 ui->comboBoxBookmarks->setCurrentIndex(0);
196
197 ui->radioButtonUpdate->setChecked(true);
198 ui->checkBoxOnlyOrbitalElements->setChecked(true);
199
200 //TODO: Is this the right place?
201 ui->pushButtonAbortQuery->setVisible(false);
202 ui->pushButtonAbortDownload->setVisible(false);
203
204 //Resetting the dialog should not reset the timer
205 //resetCountdown();
206 resetNotFound();
207 enableInterface(true);
208 }
209
populateBookmarksList()210 void MpcImportWindow::populateBookmarksList()
211 {
212 ui->comboBoxBookmarks->clear();
213 ui->comboBoxBookmarks->addItem(q_("Select bookmark..."));
214 QStringList bookmarkTitles(bookmarks.value(importType).keys());
215 bookmarkTitles.sort();
216 ui->comboBoxBookmarks->addItems(bookmarkTitles);
217 }
218
retranslate()219 void MpcImportWindow::retranslate()
220 {
221 if (dialog)
222 {
223 ui->retranslateUi(dialog);
224 updateTexts();
225 }
226 }
227
acquireObjectData()228 void MpcImportWindow::acquireObjectData()
229 {
230 if (ui->radioButtonFile->isChecked())
231 {
232 QString filePath = ui->lineEditFilePath->text();
233 if (filePath.isEmpty())
234 return;
235
236 QList<SsoElements> objects = readElementsFromFile(importType, filePath);
237 if (objects.isEmpty())
238 return;
239
240 //Temporary, until the slot/socket mechanism is ready
241 populateCandidateObjects(objects);
242 ui->stackedWidget->setCurrentIndex(1);
243 }
244 else if (ui->radioButtonURL->isChecked())
245 {
246 QString url = ui->lineEditURL->text();
247 if (url.isEmpty())
248 return;
249 startDownload(url);
250 }
251 //close();
252 }
253
addObjects()254 void MpcImportWindow::addObjects()
255 {
256 disconnect(ssoManager, SIGNAL(solarSystemChanged()), this, SLOT(resetDialog()));
257
258 QList<QString> checkedObjectsNames;
259
260 // Collect names of marked objects
261 //TODO: Something smarter?
262 for (int row = 0; row < candidateObjectsModel->rowCount(); row++)
263 {
264 QStandardItem * item = candidateObjectsModel->item(row);
265 if (item->checkState() == Qt::Checked)
266 {
267 checkedObjectsNames.append(item->text());
268 if (row==0)
269 SearchDialog::extSearchText = item->text();
270 }
271 }
272 //qDebug() << "Checked:" << checkedObjectsNames;
273
274 // collect from candidatesForAddition all candidates that were selected by the user into `approvedForAddition` ...
275 QList<SsoElements> approvedForAddition;
276 for (int i = 0; i < candidatesForAddition.count(); i++)
277 {
278 auto candidate = candidatesForAddition.at(i);
279 QString name = candidate.value("name").toString();
280 if (checkedObjectsNames.contains(name))
281 approvedForAddition.append(candidate);
282 }
283
284 //qDebug() << "Approved for addition:" << approvedForAddition;
285
286 // collect all new (!!!) candidates that were selected by the user into `approvedForUpdate`
287 // if the user opted to overwrite, those candidates are added to `approvedForAddition` instead
288 bool overwrite = ui->radioButtonOverwrite->isChecked();
289 QList<SsoElements> approvedForUpdate;
290 for (int j = 0; j < candidatesForUpdate.count(); j++)
291 {
292 auto candidate = candidatesForUpdate.at(j);
293 QString name = candidate.value("name").toString();
294 if (checkedObjectsNames.contains(name))
295 {
296 // XXX: odd... if "overwrite" is false, data is overwritten anyway.
297 if (overwrite)
298 {
299 approvedForAddition.append(candidate);
300 }
301 else
302 {
303 approvedForUpdate.append(candidate);
304 }
305 }
306 }
307
308 //qDebug() << "Approved for updates:" << approvedForUpdate;
309
310 // append *** + update *** the approvedForAddition candidates to custom solar system config
311 ssoManager->appendToSolarSystemConfigurationFile(approvedForAddition);
312
313 // if instead "update existing objects" was selected, update existing candidates from `approvedForUpdate` in custom solar system config
314 // update name, MPC number, orbital elements
315 // if the user asked more to update, include type (asteroid, comet, plutino, cubewano, ...) and magnitude parameters
316 bool update = ui->radioButtonUpdate->isChecked();
317 // ASSERT(update != overwrite); // because of radiobutton behaviour. TODO this UI is not very clear anyway.
318 if (update)
319 {
320 SolarSystemEditor::UpdateFlags flags(SolarSystemEditor::UpdateNameAndNumber | SolarSystemEditor::UpdateOrbitalElements);
321 bool onlyorbital = ui->checkBoxOnlyOrbitalElements->isChecked();
322 if (!onlyorbital)
323 {
324 flags |= SolarSystemEditor::UpdateType;
325 flags |= SolarSystemEditor::UpdateMagnitudeParameters;
326 }
327
328 ssoManager->updateSolarSystemConfigurationFile(approvedForUpdate, flags);
329 }
330
331 //Refresh the Solar System
332 GETSTELMODULE(SolarSystem)->reloadPlanets();
333
334 resetDialog();
335 emit objectsImported();
336 }
337
discardObjects()338 void MpcImportWindow::discardObjects()
339 {
340 resetDialog();
341 }
342
pasteClipboardURL()343 void MpcImportWindow::pasteClipboardURL()
344 {
345 ui->lineEditURL->setText(QGuiApplication::clipboard()->text());
346 }
347
selectFile()348 void MpcImportWindow::selectFile()
349 {
350 QString filter = q_("Plain Text File");
351 filter.append(" (*.txt);;");
352 filter.append(q_("All Files"));
353 filter.append(" (*.*)");
354 QString filePath = QFileDialog::getOpenFileName(Q_NULLPTR, q_("Select a file"), QDir::homePath(), filter);
355 ui->lineEditFilePath->setText(filePath);
356 }
357
bookmarkSelected(QString bookmarkTitle)358 void MpcImportWindow::bookmarkSelected(QString bookmarkTitle)
359 {
360 if (bookmarkTitle.isEmpty() || bookmarks.value(importType).value(bookmarkTitle).isEmpty())
361 {
362 ui->lineEditURL->clear();
363 return;
364 }
365 QString bookmarkUrl = bookmarks.value(importType).value(bookmarkTitle);
366 ui->lineEditURL->setText(bookmarkUrl);
367 }
368
populateCandidateObjects(QList<SsoElements> objects)369 void MpcImportWindow::populateCandidateObjects(QList<SsoElements> objects)
370 {
371 candidatesForAddition.clear(); // new objects
372 candidatesForUpdate.clear(); // existing objects
373
374 //Get a list of the current objects
375 //QHash<QString,QString> defaultSsoIdentifiers = ssoManager->getDefaultSsoIdentifiers();
376 QHash<QString,QString> loadedSsoIdentifiers = ssoManager->listAllLoadedSsoIdentifiers();
377
378 //Separate the objects into visual (internally unsorted, anyone?) groups in the list
379
380 //int newDefaultSsoIndex = 0;
381 int newLoadedSsoIndex = 0; // existing objects
382 int newNovelSsoIndex = 0; // new objects
383
384 int insertionIndex = 0; // index of object to be inserted next
385
386 QStandardItemModel * model = candidateObjectsModel;
387 model->clear();
388 model->setColumnCount(1);
389
390 for (auto object : objects)
391 {
392 QString name = object.value("name").toString();
393 if (name.isEmpty())
394 continue;
395
396 QString group = object.value("section_name").toString();
397 if (group.isEmpty())
398 continue;
399
400 //Prevent name conflicts between asteroids and moons
401 if (loadedSsoIdentifiers.contains(name))
402 {
403 if (loadedSsoIdentifiers.value(name) != group)
404 {
405 name.append('*');
406 object.insert("name", name);
407 }
408 }
409
410 QStandardItem * item = new QStandardItem();
411 item->setText(name);
412 item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
413 item->setCheckState(Qt::Unchecked);
414
415 // if (defaultSsoIdentifiers.contains(name))
416 // {
417 // //Duplicate of a default solar system object
418 // QFont itemFont(item->font());
419 // itemFont.setBold(true);
420 // item->setFont(itemFont);
421
422 // candidatesForUpdate.append(object);
423
424 // insertionIndex = newDefaultSsoIndex;
425 // newDefaultSsoIndex++;
426 // newLoadedSsoIndex++;
427 // newNovelSsoIndex++;
428 // }
429 // else
430
431 // identify existing (in italic) and new objects
432 if (loadedSsoIdentifiers.contains(name))
433 {
434 //Duplicate of another existing object
435 QFont itemFont(item->font());
436 itemFont.setItalic(true);
437 item->setFont(itemFont);
438
439 candidatesForUpdate.append(object);
440
441 insertionIndex = newLoadedSsoIndex;
442 newLoadedSsoIndex++;
443 newNovelSsoIndex++;
444 }
445 else
446 {
447 candidatesForAddition.append(object);
448
449 insertionIndex = newNovelSsoIndex;
450 newNovelSsoIndex++;
451 }
452
453 model->insertRow(insertionIndex, item);
454 }
455
456 //Scroll to the first items
457 ui->listViewObjects->scrollToTop();
458 }
459
enableInterface(bool enable)460 void MpcImportWindow::enableInterface(bool enable)
461 {
462 ui->groupBoxType->setVisible(enable);
463
464 ui->frameFile->setEnabled(enable);
465 ui->frameURL->setEnabled(enable);
466
467 ui->radioButtonFile->setEnabled(enable);
468 ui->radioButtonURL->setEnabled(enable);
469
470 ui->pushButtonAcquire->setEnabled(enable);
471 }
472
readElementsFromString(QString elements)473 SsoElements MpcImportWindow::readElementsFromString (QString elements)
474 {
475 Q_ASSERT(ssoManager);
476
477 switch (importType)
478 {
479 case MpcComets:
480 return ssoManager->readMpcOneLineCometElements(elements);
481 case MpcMinorPlanets:
482 default:
483 return ssoManager->readMpcOneLineMinorPlanetElements(elements);
484 }
485 }
486
readElementsFromFile(ImportType type,QString filePath)487 QList<SsoElements> MpcImportWindow::readElementsFromFile(ImportType type, QString filePath)
488 {
489 Q_ASSERT(ssoManager);
490
491 switch (type)
492 {
493 case MpcComets:
494 return ssoManager->readMpcOneLineCometElementsFromFile(filePath);
495 case MpcMinorPlanets:
496 default:
497 return ssoManager->readMpcOneLineMinorPlanetElementsFromFile(filePath);
498 }
499 }
500
switchImportType(bool)501 void MpcImportWindow::switchImportType(bool)
502 {
503 if (ui->radioButtonAsteroids->isChecked())
504 {
505 importType = MpcMinorPlanets;
506 }
507 else
508 {
509 importType = MpcComets;
510 }
511
512 populateBookmarksList();
513
514 //Clear the fields
515 //ui->lineEditSingle->clear();
516 ui->lineEditFilePath->clear();
517 ui->lineEditURL->clear();
518
519 //If one of the options is selected, show the rest of the dialog
520 ui->groupBoxSource->setVisible(true);
521 }
522
markAll()523 void MpcImportWindow::markAll()
524 {
525 int rowCount = candidateObjectsModel->rowCount();
526 if (rowCount < 1)
527 return;
528
529 for (int row = 0; row < rowCount; row++)
530 {
531 QStandardItem * item = candidateObjectsModel->item(row);
532 if (item)
533 {
534 item->setCheckState(Qt::Checked);
535 }
536 }
537 }
538
unmarkAll()539 void MpcImportWindow::unmarkAll()
540 {
541 int rowCount = candidateObjectsModel->rowCount();
542 if (rowCount < 1)
543 return;
544
545 for (int row = 0; row < rowCount; row++)
546 {
547 QStandardItem * item = candidateObjectsModel->item(row);
548 if (item)
549 {
550 item->setCheckState(Qt::Unchecked);
551 }
552 }
553 }
554
updateDownloadProgress(qint64 bytesReceived,qint64 bytesTotal)555 void MpcImportWindow::updateDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
556 {
557 if (downloadProgressBar == Q_NULLPTR)
558 return;
559
560 int currentValue = 0;
561 int endValue = 0;
562
563 if (bytesTotal > -1 && bytesReceived <= bytesTotal)
564 {
565 //Round to the greatest possible derived unit
566 while (bytesTotal > 1024)
567 {
568 bytesReceived >>= 10;
569 bytesTotal >>= 10;
570 }
571 currentValue = static_cast<int>(bytesReceived);
572 endValue = static_cast<int>(bytesTotal);
573 }
574
575 downloadProgressBar->setValue(currentValue);
576 downloadProgressBar->setRange(0, endValue);
577 }
578
updateQueryProgress(qint64,qint64)579 void MpcImportWindow::updateQueryProgress(qint64, qint64)
580 {
581 if (queryProgressBar == Q_NULLPTR)
582 return;
583
584 //Just show activity
585 queryProgressBar->setValue(0);
586 queryProgressBar->setRange(0, 0);
587 }
588
startDownload(QString urlString)589 void MpcImportWindow::startDownload(QString urlString)
590 {
591 if (downloadReply)
592 {
593 //There's already an operation in progress?
594 //TODO
595 return;
596 }
597
598 QUrl url(urlString);
599 if (!url.isValid() || url.isRelative() || !url.scheme().startsWith("http", Qt::CaseInsensitive))
600 {
601 qWarning() << "Invalid URL:" << urlString;
602 return;
603 }
604 //qDebug() << url.toString();
605
606 //TODO: Interface changes!
607
608 downloadProgressBar = StelApp::getInstance().addProgressBar();
609 downloadProgressBar->setValue(0);
610 downloadProgressBar->setRange(0, 0);
611
612 //TODO: Better handling of the interface
613 //dialog->setVisible(false);
614 enableInterface(false);
615 ui->pushButtonAbortDownload->setVisible(true);
616
617 connect(networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(downloadComplete(QNetworkReply*)));
618 QNetworkRequest request;
619 request.setUrl(QUrl(url));
620 request.setRawHeader("User-Agent", StelUtils::getUserAgentString().toUtf8());
621 request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
622 downloadReply = networkManager->get(request);
623 connect(downloadReply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(updateDownloadProgress(qint64,qint64)));
624 }
625
abortDownload()626 void MpcImportWindow::abortDownload()
627 {
628 if (downloadReply == Q_NULLPTR || downloadReply->isFinished())
629 return;
630
631 qDebug() << "Aborting download...";
632
633 disconnect(networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(downloadComplete(QNetworkReply*)));
634 deleteDownloadProgressBar();
635
636 downloadReply->abort();
637 downloadReply->deleteLater();
638 downloadReply = Q_NULLPTR;
639
640 enableInterface(true);
641 ui->pushButtonAbortDownload->setVisible(false);
642 }
643
downloadComplete(QNetworkReply * reply)644 void MpcImportWindow::downloadComplete(QNetworkReply *reply)
645 {
646 disconnect(networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(downloadComplete(QNetworkReply*)));
647 deleteDownloadProgressBar();
648 ui->pushButtonAbortDownload->setVisible(false);
649
650 /*
651 qDebug() << "reply->isOpen():" << reply->isOpen()
652 << "reply->isReadable():" << reply->isReadable()
653 << "reply->isFinished():" << reply->isFinished();
654 */
655
656 if(reply->error() || reply->bytesAvailable()==0)
657 {
658 qWarning() << "Download error: While downloading"
659 << reply->url().toString()
660 << "the following error occured:"
661 << reply->errorString();
662 enableInterface(true);
663 reply->deleteLater();
664 downloadReply = Q_NULLPTR;
665 return;
666 }
667
668 QList<SsoElements> objects;
669 QTemporaryFile file;
670 if (file.open())
671 {
672 file.write(reply->readAll());
673 file.close();
674 objects = readElementsFromFile(importType, file.fileName());
675 }
676 else
677 {
678 qWarning() << "Unable to open a temporary file. Aborting operation.";
679 }
680
681 if (objects.isEmpty())
682 {
683 qWarning() << "No objects found in the file downloaded from"
684 << reply->url().toString();
685 }
686 else
687 {
688 //The request has been successful: add the URL to bookmarks?
689 if (ui->checkBoxAddBookmark->isChecked())
690 {
691 QString url = reply->url().toString();
692 QString title = ui->lineEditBookmarkTitle->text().trimmed();
693 //If no title has been entered, use the URL as a title
694 if (title.isEmpty())
695 title = url;
696 if (!bookmarks.value(importType).values().contains(url))
697 {
698 bookmarks[importType].insert(title, url);
699 populateBookmarksList();
700 saveBookmarks();
701 }
702 }
703 }
704
705 reply->deleteLater();
706 downloadReply = Q_NULLPTR;
707
708 //Temporary, until the slot/socket mechanism is ready
709 populateCandidateObjects(objects);
710 ui->stackedWidget->setCurrentIndex(1);
711 //As this window is persistent, if the Solar System is changed
712 //while there is a list, it should be reset.
713 connect(ssoManager, SIGNAL(solarSystemChanged()), this, SLOT(resetDialog()));
714 }
715
deleteDownloadProgressBar()716 void MpcImportWindow::deleteDownloadProgressBar()
717 {
718 disconnect(this, SLOT(updateDownloadProgress(qint64,qint64)));
719
720 if (downloadProgressBar)
721 {
722 StelApp::getInstance().removeProgressBar(downloadProgressBar);
723 downloadProgressBar = Q_NULLPTR;
724 }
725 }
726
sendQuery()727 void MpcImportWindow::sendQuery()
728 {
729 if (queryReply != Q_NULLPTR)
730 return;
731
732 query = ui->lineEditQuery->text().trimmed();
733 if (query.isEmpty())
734 return;
735
736 //Progress bar
737 queryProgressBar = StelApp::getInstance().addProgressBar();
738 queryProgressBar->setValue(0);
739 queryProgressBar->setRange(0, 0);
740 queryProgressBar->setFormat("Searching...");
741
742 //TODO: Better handling of the interface
743 enableInterface(false);
744 ui->labelQueryMessage->setVisible(false);
745
746 startCountdown();
747 ui->pushButtonAbortQuery->setVisible(true);
748
749 //sendQueryToUrl(QUrl("http://stellarium.org/mpc-mpeph"));
750 //sendQueryToUrl(QUrl("http://scully.cfa.harvard.edu/cgi-bin/mpeph2.cgi"));
751 // MPC requirements now :(
752 sendQueryToUrl(QUrl("https://www.minorplanetcenter.net/cgi-bin/mpeph2.cgi"));
753 }
754
sendQueryToUrl(QUrl url)755 void MpcImportWindow::sendQueryToUrl(QUrl url)
756 {
757 QUrlQuery q(url);
758 q.addQueryItem("ty","e");//Type: ephemerides
759 q.addQueryItem("TextArea", query);//Object name query
760 q.addQueryItem("e", "-1");//Elements format: MPC 1-line
761 //Switch to MPC 1-line format --AW
762 //XEphem's format is used instead because it doesn't truncate object names.
763 //q.addQueryItem("e", "3");//Elements format: XEphem
764 //Yes, all of the rest are necessary
765 q.addQueryItem("d","");
766 q.addQueryItem("l","");
767 q.addQueryItem("i","");
768 q.addQueryItem("u","d");
769 q.addQueryItem("uto", "0");
770 q.addQueryItem("c", "");
771 q.addQueryItem("long", "");
772 q.addQueryItem("lat", "");
773 q.addQueryItem("alt", "");
774 q.addQueryItem("raty", "a");
775 q.addQueryItem("s", "t");
776 q.addQueryItem("m", "m");
777 q.addQueryItem("adir", "S");
778 q.addQueryItem("oed", "");
779 q.addQueryItem("resoc", "");
780 q.addQueryItem("tit", "");
781 q.addQueryItem("bu", "");
782 q.addQueryItem("ch", "c");
783 q.addQueryItem("ce", "f");
784 q.addQueryItem("js", "f");
785 url.setQuery(q);
786
787 QNetworkRequest request;
788 request.setUrl(QUrl(url));
789 request.setRawHeader("User-Agent", StelUtils::getUserAgentString().toUtf8());
790 request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); //Is this really necessary?
791 request.setHeader(QNetworkRequest::ContentLengthHeader, url.query(QUrl::FullyEncoded).length());
792 request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
793
794 connect(networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(receiveQueryReply(QNetworkReply*)));
795 queryReply = networkManager->post(request, url.query(QUrl::FullyEncoded).toUtf8());
796 connect(queryReply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(updateQueryProgress(qint64,qint64)));
797 }
798
abortQuery()799 void MpcImportWindow::abortQuery()
800 {
801 if (queryReply == Q_NULLPTR)
802 return;
803
804 disconnect(networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(receiveQueryReply(QNetworkReply*)));
805 deleteQueryProgressBar();
806
807 queryReply->abort();
808 queryReply->deleteLater();
809 queryReply = Q_NULLPTR;
810
811 //resetCountdown();
812 enableInterface(true);
813 ui->pushButtonAbortQuery->setVisible(false);
814 }
815
receiveQueryReply(QNetworkReply * reply)816 void MpcImportWindow::receiveQueryReply(QNetworkReply *reply)
817 {
818 if (reply == Q_NULLPTR)
819 return;
820
821 disconnect(networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(receiveQueryReply(QNetworkReply*)));
822
823 int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
824 if (statusCode == 301 || statusCode == 302 || statusCode == 307)
825 {
826 QUrl rawUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
827 QUrl redirectUrl(rawUrl.toString(QUrl::RemoveQuery));
828 qDebug() << "The search query has been redirected to" << redirectUrl.toString();
829
830 //TODO: Add counter and cycle check.
831
832 reply->deleteLater();
833 queryReply = Q_NULLPTR;
834 sendQueryToUrl(redirectUrl);
835 return;
836 }
837
838 deleteQueryProgressBar();
839
840 //Hide the abort button - a reply has been received
841 ui->pushButtonAbortQuery->setVisible(false);
842
843 if (reply->error() || reply->bytesAvailable()==0)
844 {
845 qWarning() << "Download error: While trying to access"
846 << reply->url().toString()
847 << "the following error occured:"
848 << reply->errorString();
849 ui->labelQueryMessage->setText(reply->errorString());//TODO: Decide if this is a good idea
850 ui->labelQueryMessage->setVisible(true);
851 enableInterface(true);
852
853 reply->deleteLater();
854 queryReply = Q_NULLPTR;
855 return;
856 }
857
858 QString contentType = reply->header(QNetworkRequest::ContentTypeHeader).toString();
859 QString contentDisposition = reply->rawHeader(QByteArray("Content-disposition"));
860 if (contentType == "text/ascii" &&
861 contentDisposition == "attachment; filename=elements.txt")
862 {
863 readQueryReply(reply);
864 }
865 else
866 {
867 ui->labelQueryMessage->setText("Object not found.");
868 ui->labelQueryMessage->setVisible(true);
869 enableInterface(true);
870 }
871
872 reply->deleteLater();
873 queryReply = Q_NULLPTR;
874 }
875
readQueryReply(QNetworkReply * reply)876 void MpcImportWindow::readQueryReply(QNetworkReply * reply)
877 {
878 Q_ASSERT(reply);
879
880 QList<SsoElements> objects;
881 QTemporaryFile file;
882 if (file.open())
883 {
884 file.write(reply->readAll());
885 file.close();
886
887 QRegularExpression cometProvisionalDesignation("[PCDXI]/");
888 QRegularExpression cometDesignation("(\\d)+[PCDXI]/");
889 QString queryData = ui->lineEditQuery->text().trimmed();
890
891 if (queryData.indexOf(cometDesignation) == 0 || queryData.indexOf(cometProvisionalDesignation) == 0)
892 objects = readElementsFromFile(MpcComets, file.fileName());
893 else
894 objects = readElementsFromFile(MpcMinorPlanets, file.fileName());
895
896 /*
897 //Try to read it as a comet first?
898 objects = readElementsFromFile(MpcComets, file.fileName());
899 if (objects.isEmpty())
900 objects = readElementsFromFile(MpcMinorPlanets, file.fileName());
901 */
902 //XEphem given wrong data for comets --AW
903 //objects = ssoManager->readXEphemOneLineElementsFromFile(file.fileName());
904 }
905 else
906 {
907 qWarning() << "Unable to open a temporary file. Aborting operation.";
908 }
909
910 if (objects.isEmpty())
911 {
912 qWarning() << "No objects found in the file downloaded from"
913 << reply->url().toString();
914 }
915 else
916 {
917 //The request has been successful: add the URL to bookmarks?
918 if (ui->checkBoxAddBookmark->isChecked())
919 {
920 QString url = reply->url().toString();
921 if (!bookmarks.value(importType).values().contains(url))
922 {
923 //Use the URL as a title for now
924 bookmarks[importType].insert(url, url);
925 }
926 }
927
928 //Temporary, until the slot/socket mechanism is ready
929 populateCandidateObjects(objects);
930 ui->stackedWidget->setCurrentIndex(1);
931 }
932 }
933
deleteQueryProgressBar()934 void MpcImportWindow::deleteQueryProgressBar()
935 {
936 disconnect(this, SLOT(updateQueryProgress(qint64,qint64)));
937 if (queryProgressBar)
938 {
939 StelApp::getInstance().removeProgressBar(queryProgressBar);
940 queryProgressBar = Q_NULLPTR;
941 }
942 }
943
startCountdown()944 void MpcImportWindow::startCountdown()
945 {
946 if (!countdownTimer->isActive())
947 countdownTimer->start(1000);//1 second
948
949 //Disable the interface
950 ui->lineEditQuery->setEnabled(false);
951 ui->pushButtonSendQuery->setEnabled(false);
952 }
953
resetCountdown()954 void MpcImportWindow::resetCountdown()
955 {
956 //Stop the timer
957 if (countdownTimer->isActive())
958 {
959 countdownTimer->stop();
960
961 //If the query is still active, kill it
962 if (queryReply != Q_NULLPTR && queryReply->isRunning())
963 {
964 abortQuery();
965 ui->labelQueryMessage->setText("The query timed out. You can try again, now or later.");
966 ui->labelQueryMessage->setVisible(true);
967 }
968 }
969
970 //Reset the counter
971 countdown = 60;
972
973 //Enable the interface
974 ui->lineEditQuery->setEnabled(true);
975 ui->pushButtonSendQuery->setEnabled(true);
976 }
977
updateCountdown()978 void MpcImportWindow::updateCountdown()
979 {
980 --countdown;
981 if (countdown < 0)
982 {
983 resetCountdown();
984 }
985 //If there has been an answer
986 else if (countdown > 50 && queryReply == Q_NULLPTR)
987 {
988 resetCountdown();
989 }
990 }
991
resetNotFound()992 void MpcImportWindow::resetNotFound()
993 {
994 ui->labelQueryMessage->setVisible(false);
995 }
996
loadBookmarks()997 void MpcImportWindow::loadBookmarks()
998 {
999 bookmarks[MpcComets].clear();
1000 bookmarks[MpcMinorPlanets].clear();
1001
1002 QString bookmarksFilePath(StelFileMgr::getUserDir() + "/modules/SolarSystemEditor/bookmarks.json");
1003 bool outdated = false;
1004 if (StelFileMgr::isReadable(bookmarksFilePath))
1005 {
1006 QFile bookmarksFile(bookmarksFilePath);
1007 if (bookmarksFile.open(QFile::ReadOnly | QFile::Text))
1008 {
1009 QVariantMap jsonRoot;
1010 QString fileVersion = "0.0.0";
1011 try
1012 {
1013 jsonRoot = StelJsonParser::parse(bookmarksFile.readAll()).toMap();
1014 bookmarksFile.close();
1015
1016 fileVersion = jsonRoot.value("version").toString();
1017 if (fileVersion.isEmpty())
1018 fileVersion = "0.0.0";
1019
1020 loadBookmarksGroup(jsonRoot.value("mpcMinorPlanets").toMap(), bookmarks[MpcMinorPlanets]);
1021 loadBookmarksGroup(jsonRoot.value("mpcComets").toMap(), bookmarks[MpcComets]);
1022 }
1023 catch (std::runtime_error &e)
1024 {
1025 qDebug() << "File format is wrong! Error: " << e.what();
1026 outdated = true;
1027 }
1028
1029 if (StelUtils::compareVersions(fileVersion, SOLARSYSTEMEDITOR_PLUGIN_VERSION)<0)
1030 outdated = true; // Oops... the list is outdated!
1031
1032 //If nothing was read, continue
1033 if (!bookmarks.value(MpcComets).isEmpty() && !bookmarks[MpcMinorPlanets].isEmpty() && !outdated)
1034 return;
1035 }
1036 }
1037
1038 if (outdated)
1039 qDebug() << "Bookmarks file is outdated! The list will be upgraded by hard-coded bookmarks.";
1040 else
1041 qDebug() << "Bookmarks file can't be read. Hard-coded bookmarks will be used.";
1042
1043 //Initialize with hard-coded values
1044 // NOTE: this list is reordered anyway when loaded
1045
1046 bookmarks[MpcMinorPlanets].insert("MPC's list of bright minor planets at opposition in 2018", "https://www.minorplanetcenter.net/iau/Ephemerides/Bright/2018/Soft00Bright.txt");
1047 bookmarks[MpcMinorPlanets].insert("MPC's list of observable critical-list numbered minor planets", "https://www.minorplanetcenter.net/iau/Ephemerides/CritList/Soft00CritList.txt");
1048 bookmarks[MpcMinorPlanets].insert("MPC's list of observable distant minor planets", "https://www.minorplanetcenter.net/iau/Ephemerides/Distant/Soft00Distant.txt");
1049 bookmarks[MpcMinorPlanets].insert("MPC's list of observable unusual minor planets", "https://www.minorplanetcenter.net/iau/Ephemerides/Unusual/Soft00Unusual.txt");
1050
1051 bookmarks[MpcMinorPlanets].insert("MPCORB: near-Earth asteroids (NEAs)", "https://www.minorplanetcenter.net/iau/MPCORB/NEA.txt");
1052 bookmarks[MpcMinorPlanets].insert("MPCORB: potentially hazardous asteroids (PHAs)", "https://www.minorplanetcenter.net/iau/MPCORB/PHA.txt");
1053 bookmarks[MpcMinorPlanets].insert("MPCORB: TNOs, Centaurs and SDOs", "https://www.minorplanetcenter.net/iau/MPCORB/Distant.txt");
1054 bookmarks[MpcMinorPlanets].insert("MPCORB: other unusual objects", "https://www.minorplanetcenter.net/iau/MPCORB/Unusual.txt");
1055 bookmarks[MpcMinorPlanets].insert("MPCORB: elements of NEAs for current epochs (today)", "https://www.minorplanetcenter.net/iau/MPCORB/NEAm00.txt");
1056 bookmarks[MpcMinorPlanets].insert("MPCORB: orbits from the latest DOU MPEC", "https://www.minorplanetcenter.net/iau/MPCORB/DAILY.DAT");
1057
1058 bookmarks[MpcMinorPlanets].insert("MPCAT: Unusual minor planets (including NEOs)", "https://www.minorplanetcenter.net/iau/ECS/MPCAT/unusual.txt");
1059 bookmarks[MpcMinorPlanets].insert("MPCAT: Distant minor planets (Centaurs and transneptunians)", "https://www.minorplanetcenter.net/iau/ECS/MPCAT/distant.txt");
1060
1061 const int start = 0;
1062 const int finish = 52;
1063 const QChar dash = QChar(0x2014);
1064
1065 QString limits, idx;
1066
1067 for (int i=start; i<=finish; i++)
1068 {
1069 if (i==start)
1070 limits = QString("%1%2%3").arg(QString::number(1).rightJustified(6), dash, QString::number(9999).rightJustified(6));
1071 else if (i==finish)
1072 limits = QString("%1...").arg(QString::number(i*10000).rightJustified(6));
1073 else
1074 limits = QString("%1%2%3").arg(QString::number(i*10000).rightJustified(6), dash, QString::number(i*10000 + 9999).rightJustified(6));
1075
1076 idx = QString::number(i+1).rightJustified(2, '0');
1077 bookmarks[MpcMinorPlanets].insert(
1078 QString("MPCAT: Numbered objects (%1)").arg(limits),
1079 QString("http://dss.stellarium.org/MPC/mpn-%1.txt").arg(idx)
1080 );
1081 }
1082
1083 bookmarks[MpcComets].insert("MPC's list of observable comets", "https://www.minorplanetcenter.net/iau/Ephemerides/Comets/Soft00Cmt.txt");
1084 bookmarks[MpcComets].insert("MPCORB: comets", "https://www.minorplanetcenter.net/iau/MPCORB/CometEls.txt");
1085
1086 bookmarks[MpcComets].insert("Gideon van Buitenen: comets", "http://astro.vanbuitenen.nl/cometelements?format=mpc&mag=obs");
1087
1088 //Try to save them to a file
1089 saveBookmarks();
1090 }
1091
loadBookmarksGroup(QVariantMap source,Bookmarks & bookmarkGroup)1092 void MpcImportWindow::loadBookmarksGroup(QVariantMap source, Bookmarks & bookmarkGroup)
1093 {
1094 if (source.isEmpty())
1095 return;
1096
1097 for (auto title : source.keys())
1098 {
1099 QString url = source.value(title).toString();
1100 if (!url.isEmpty())
1101 bookmarkGroup.insert(title, url);
1102 }
1103 }
1104
saveBookmarks()1105 void MpcImportWindow::saveBookmarks()
1106 {
1107 try
1108 {
1109 StelFileMgr::makeSureDirExistsAndIsWritable(StelFileMgr::getUserDir() + "/modules/SolarSystemEditor");
1110
1111 QVariantMap jsonRoot;
1112
1113 QString bookmarksFilePath(StelFileMgr::getUserDir() + "/modules/SolarSystemEditor/bookmarks.json");
1114
1115 //If the file exists, load it first
1116 if (StelFileMgr::isReadable(bookmarksFilePath))
1117 {
1118 QFile bookmarksFile(bookmarksFilePath);
1119 if (bookmarksFile.open(QFile::ReadOnly | QFile::Text))
1120 {
1121 jsonRoot = StelJsonParser::parse(bookmarksFile.readAll()).toMap();
1122 bookmarksFile.close();
1123 }
1124 }
1125
1126 QFile bookmarksFile(bookmarksFilePath);
1127 if (bookmarksFile.open(QFile::WriteOnly | QFile::Truncate | QFile::Text))
1128 {
1129 jsonRoot.insert("version", SOLARSYSTEMEDITOR_PLUGIN_VERSION);
1130
1131 QVariantMap minorPlanetsObject;
1132 saveBookmarksGroup(bookmarks[MpcMinorPlanets], minorPlanetsObject);
1133 //qDebug() << minorPlanetsObject.keys();
1134 jsonRoot.insert("mpcMinorPlanets", minorPlanetsObject);
1135
1136 QVariantMap cometsObject;
1137 saveBookmarksGroup(bookmarks[MpcComets], cometsObject);
1138 jsonRoot.insert("mpcComets", cometsObject);
1139
1140 StelJsonParser::write(jsonRoot, &bookmarksFile);
1141 bookmarksFile.close();
1142
1143 qDebug() << "Bookmarks file saved to" << QDir::toNativeSeparators(bookmarksFilePath);
1144 return;
1145 }
1146 else
1147 {
1148 qDebug() << "Unable to write bookmarks file to" << QDir::toNativeSeparators(bookmarksFilePath);
1149 }
1150 }
1151 catch (std::exception & e)
1152 {
1153 qDebug() << "Unable to save bookmarks file:" << e.what();
1154 }
1155 }
1156
saveBookmarksGroup(Bookmarks & bookmarkGroup,QVariantMap & output)1157 void MpcImportWindow::saveBookmarksGroup(Bookmarks & bookmarkGroup, QVariantMap & output)
1158 {
1159 for (auto title : bookmarkGroup.keys())
1160 {
1161 output.insert(title, bookmarkGroup.value(title));
1162 }
1163 }
1164