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 &amp; 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