1 /*
2     SPDX-FileCopyrightText: 2001 Heiko Evermann <heiko@evermann.de>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "kstarsdata.h"
8 
9 #include "ksutils.h"
10 #include "Options.h"
11 #include "auxiliary/kspaths.h"
12 #include "skycomponents/supernovaecomponent.h"
13 #include "skycomponents/skymapcomposite.h"
14 #include "ksnotification.h"
15 #include "skyobjectuserdata.h"
16 #include <kio/job_base.h>
17 #include <kio/filecopyjob.h>
18 #ifndef KSTARS_LITE
19 #include "fov.h"
20 #include "imageexporter.h"
21 #include "kstars.h"
22 #include "observinglist.h"
23 #include "skymap.h"
24 #include "dialogs/detaildialog.h"
25 #include "oal/execute.h"
26 #endif
27 
28 #ifndef KSTARS_LITE
29 #include <KMessageBox>
30 #endif
31 
32 #include <QSqlQuery>
33 #include <QSqlRecord>
34 #include <QtConcurrent>
35 
36 #include "kstars_debug.h"
37 
38 namespace
39 {
40 // Report fatal error during data loading to user
41 // Calls QApplication::exit
fatalErrorMessage(QString fname)42 void fatalErrorMessage(QString fname)
43 {
44     qCCritical(KSTARS) << i18n("Critical File not Found: %1", fname);
45     KSNotification::sorry(i18n("The file  %1 could not be found. "
46                                "KStars cannot run properly without this file. "
47                                "KStars searches for this file in following locations:\n\n\t"
48                                "%2\n\n"
49                                "It appears that your setup is broken.",
50                                fname, QStandardPaths::standardLocations(QStandardPaths::DataLocation).join("\n\t")),
51                           i18n("Critical File Not Found: %1", fname)); // FIXME: Must list locations depending on file type
52 
53     qApp->exit(1);
54 }
55 
56 // Report non-fatal error during data loading to user and ask
57 // whether he wants to continue.
58 // Calls QApplication::exit if he don't
nonFatalErrorMessage(QString fname)59 bool nonFatalErrorMessage(QString fname)
60 {
61     qCWarning(KSTARS) << i18n( "Non-Critical File Not Found: %1", fname );
62 #ifdef KSTARS_LITE
63     Q_UNUSED(fname);
64     return true;
65 #else
66     int res = KMessageBox::warningContinueCancel(nullptr,
67               i18n("The file %1 could not be found. "
68                    "KStars can still run without this file. "
69                    "KStars search for this file in following locations:\n\n\t"
70                    "%2\n\n"
71                    "It appears that you setup is broken. Press Continue to run KStars without this file ",
72                    fname, QStandardPaths::standardLocations( QStandardPaths::DataLocation ).join("\n\t") ),
73               i18n( "Non-Critical File Not Found: %1", fname ));  // FIXME: Must list locations depending on file type
74     if( res != KMessageBox::Continue )
75         qApp->exit(1);
76     return res == KMessageBox::Continue;
77 #endif
78 }
79 }
80 
81 KStarsData *KStarsData::pinstance = nullptr;
82 
Create()83 KStarsData *KStarsData::Create()
84 {
85     // This method should never be called twice within a run, since a
86     // lot of the code assumes that KStarsData, once created, is never
87     // destroyed. They maintain local copies of KStarsData::Instance()
88     // for efficiency (maybe this should change, but it is not
89     // required to delete and reinstantiate KStarsData). Thus, when we
90     // call this method, pinstance MUST be zero, i.e. this must be the
91     // first (and last) time we are calling it. -- asimha
92     Q_ASSERT(!pinstance);
93 
94     delete pinstance;
95     pinstance = new KStarsData();
96     return pinstance;
97 }
98 
KStarsData()99 KStarsData::KStarsData()
100     : m_Geo(dms(0), dms(0)), m_ksuserdb(),
101       temporaryTrail(false),
102       //locale( new KLocale( "kstars" ) ),
103       m_preUpdateID(0), m_updateID(0), m_preUpdateNumID(0), m_updateNumID(0), m_preUpdateNum(J2000), m_updateNum(J2000)
104 {
105 #ifndef KSTARS_LITE
106     m_LogObject.reset(new OAL::Log);
107 #endif
108     // at startup times run forward
109     setTimeDirection(0.0);
110 }
111 
~KStarsData()112 KStarsData::~KStarsData()
113 {
114     Q_ASSERT(pinstance);
115 
116     //delete locale;
117     qDeleteAll(geoList);
118     geoList.clear();
119     qDeleteAll(ADVtreeList);
120     ADVtreeList.clear();
121 
122     pinstance = nullptr;
123 }
124 
initialize()125 bool KStarsData::initialize()
126 {
127     //Load Time Zone Rules//
128     emit progressText(i18n("Reading time zone rules"));
129     if (!readTimeZoneRulebook())
130     {
131         fatalErrorMessage("TZrules.dat");
132         return false;
133     }
134 
135     emit progressText(
136         i18n("Upgrade existing user city db to support geographic elevation."));
137 
138     QString dbfile = QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("mycitydb.sqlite");
139 
140     /// This code to add Height column to table city in mycitydb.sqlite is a transitional measure to support a meaningful
141     /// geographic elevation.
142     if (QFile::exists(dbfile))
143     {
144         QSqlDatabase fixcitydb = QSqlDatabase::addDatabase("QSQLITE", "fixcitydb");
145 
146         fixcitydb.setDatabaseName(dbfile);
147         fixcitydb.open();
148 
149         if (fixcitydb.tables().contains("city", Qt::CaseInsensitive))
150         {
151             QSqlRecord r = fixcitydb.record("city");
152             if (!r.contains("Elevation"))
153             {
154                 emit progressText(i18n("Adding \"Elevation\" column to city table."));
155 
156                 QSqlQuery query(fixcitydb);
157                 if (query.exec(
158                         "alter table city add column Elevation real default -10;") ==
159                     false)
160                 {
161                     emit progressText(QString("failed to add Elevation column to city "
162                                               "table in mycitydb.sqlite: &1")
163                                           .arg(query.lastError().text()));
164                 }
165             }
166             else
167             {
168                 emit progressText(i18n("City table already contains \"Elevation\"."));
169             }
170         }
171         else
172         {
173             emit progressText(i18n("City table missing from database."));
174         }
175         fixcitydb.close();
176     }
177 
178     //Load Cities//
179     emit progressText(i18n("Loading city data"));
180     if (!readCityData())
181     {
182         fatalErrorMessage("citydb.sqlite");
183         return false;
184     }
185 
186     //Initialize User Database//
187     emit progressText(i18n("Loading User Information"));
188     m_ksuserdb.Initialize();
189 
190     //Initialize SkyMapComposite//
191     emit progressText(i18n("Loading sky objects"));
192     m_SkyComposite.reset(new SkyMapComposite());
193     //Load Image URLs//
194     //#ifndef Q_OS_ANDROID
195     //On Android these 2 calls produce segfault. WARNING
196     emit progressText(i18n("Loading Image URLs"));
197 
198     // if (!readURLData("image_url.dat", SkyObjectUserdata::Type::image) &&
199     //     !nonFatalErrorMessage("image_url.dat"))
200     //     return false;
201     QtConcurrent::run(this, &KStarsData::readURLData, QString("image_url.dat"),
202                       SkyObjectUserdata::Type::image);
203 
204     //Load Information URLs//
205     //emit progressText(i18n("Loading Information URLs"));
206     // if (!readURLData("info_url.dat", SkyObjectUserdata::Type::website) &&
207     //     !nonFatalErrorMessage("info_url.dat"))
208     //     return false;
209     QtConcurrent::run(this, &KStarsData::readURLData, QString("info_url.dat"),
210                       SkyObjectUserdata::Type::website);
211 
212     //#endif
213     //emit progressText( i18n("Loading Variable Stars" ) );
214 
215 #ifndef KSTARS_LITE
216     //Initialize Observing List
217     m_ObservingList = new ObservingList();
218 #endif
219 
220     readUserLog();
221 
222 #ifndef KSTARS_LITE
223     readADVTreeData();
224 #endif
225     return true;
226 }
227 
updateTime(GeoLocation * geo,const bool automaticDSTchange)228 void KStarsData::updateTime(GeoLocation *geo, const bool automaticDSTchange)
229 {
230     // sync LTime with the simulation clock
231     LTime = geo->UTtoLT(ut());
232     syncLST();
233 
234     //Only check DST if (1) TZrule is not the empty rule, and (2) if we have crossed
235     //the DST change date/time.
236     if (!geo->tzrule()->isEmptyRule())
237     {
238         if (TimeRunsForward)
239         {
240             // timedirection is forward
241             // DST change happens if current date is bigger than next calculated dst change
242             if (ut() > NextDSTChange)
243                 resetToNewDST(geo, automaticDSTchange);
244         }
245         else
246         {
247             // timedirection is backward
248             // DST change happens if current date is smaller than next calculated dst change
249             if (ut() < NextDSTChange)
250                 resetToNewDST(geo, automaticDSTchange);
251         }
252     }
253 
254     KSNumbers num(ut().djd());
255 
256     if (std::abs(ut().djd() - LastNumUpdate.djd()) > 1.0)
257     {
258         LastNumUpdate = KStarsDateTime(ut().djd());
259         m_preUpdateNumID++;
260         m_preUpdateNum = KSNumbers(num);
261         skyComposite()->update(&num);
262     }
263 
264     if (std::abs(ut().djd() - LastPlanetUpdate.djd()) > 0.01)
265     {
266         LastPlanetUpdate = KStarsDateTime(ut().djd());
267         skyComposite()->updateSolarSystemBodies(&num);
268     }
269 
270     // Moon moves ~30 arcmin/hr, so update its position every minute.
271     if (std::abs(ut().djd() - LastMoonUpdate.djd()) > 0.00069444)
272     {
273         LastMoonUpdate = ut();
274         skyComposite()->updateMoons(&num);
275     }
276 
277     //Update Alt/Az coordinates.  Timescale varies with zoom level
278     //If Clock is in Manual Mode, always update. (?)
279     if (std::abs(ut().djd() - LastSkyUpdate.djd()) > 0.1 / Options::zoomFactor() || clock()->isManualMode())
280     {
281         LastSkyUpdate = ut();
282         m_preUpdateID++;
283         //omit KSNumbers arg == just update Alt/Az coords // <-- Eh? -- asimha. Looks like this behavior / ideology has changed drastically.
284         skyComposite()->update(&num);
285 
286         emit skyUpdate(clock()->isManualMode());
287     }
288 }
289 
syncUpdateIDs()290 void KStarsData::syncUpdateIDs()
291 {
292     m_updateID = m_preUpdateID;
293     if (m_updateNumID == m_preUpdateNumID)
294         return;
295     m_updateNumID = m_preUpdateNumID;
296     m_updateNum   = KSNumbers(m_preUpdateNum);
297 }
298 
incUpdateID()299 unsigned int KStarsData::incUpdateID()
300 {
301     m_preUpdateID++;
302     m_preUpdateNumID++;
303     syncUpdateIDs();
304     return m_updateID;
305 }
306 
setFullTimeUpdate()307 void KStarsData::setFullTimeUpdate()
308 {
309     //Set the update markers to invalid dates to trigger updates in each category
310     LastSkyUpdate    = KStarsDateTime(QDateTime());
311     LastPlanetUpdate = KStarsDateTime(QDateTime());
312     LastMoonUpdate   = KStarsDateTime(QDateTime());
313     LastNumUpdate    = KStarsDateTime(QDateTime());
314 }
315 
syncLST()316 void KStarsData::syncLST()
317 {
318     LST = geo()->GSTtoLST(ut().gst());
319 }
320 
changeDateTime(const KStarsDateTime & newDate)321 void KStarsData::changeDateTime(const KStarsDateTime &newDate)
322 {
323     //Turn off animated slews for the next time step.
324     setSnapNextFocus();
325 
326     clock()->setUTC(newDate);
327 
328     LTime = geo()->UTtoLT(ut());
329     //set local sideral time
330     syncLST();
331 
332     //Make sure Numbers, Moon, planets, and sky objects are updated immediately
333     setFullTimeUpdate();
334 
335     // reset tzrules data with new local time and time direction (forward or backward)
336     geo()->tzrule()->reset_with_ltime(LTime, geo()->TZ0(), isTimeRunningForward());
337 
338     // reset next dst change time
339     setNextDSTChange(geo()->tzrule()->nextDSTChange());
340 }
341 
resetToNewDST(GeoLocation * geo,const bool automaticDSTchange)342 void KStarsData::resetToNewDST(GeoLocation *geo, const bool automaticDSTchange)
343 {
344     // reset tzrules data with local time, timezone offset and time direction (forward or backward)
345     // force a DST change with option true for 3. parameter
346     geo->tzrule()->reset_with_ltime(LTime, geo->TZ0(), TimeRunsForward, automaticDSTchange);
347     // reset next DST change time
348     setNextDSTChange(geo->tzrule()->nextDSTChange());
349     //reset LTime, because TZoffset has changed
350     LTime = geo->UTtoLT(ut());
351 }
352 
setTimeDirection(float scale)353 void KStarsData::setTimeDirection(float scale)
354 {
355     TimeRunsForward = scale >= 0;
356 }
357 
locationNamed(const QString & city,const QString & province,const QString & country)358 GeoLocation *KStarsData::locationNamed(const QString &city, const QString &province, const QString &country)
359 {
360     foreach (GeoLocation *loc, geoList)
361     {
362         if (loc->translatedName() == city && (province.isEmpty() || loc->translatedProvince() == province) &&
363                 (country.isEmpty() || loc->translatedCountry() == country))
364         {
365             return loc;
366         }
367     }
368     return nullptr;
369 }
370 
nearestLocation(double longitude,double latitude)371 GeoLocation *KStarsData::nearestLocation(double longitude, double latitude)
372 {
373     GeoLocation *nearest = nullptr;
374     double distance = 1e6;
375 
376     dms lng(longitude), lat(latitude);
377     for (auto oneCity : geoList)
378     {
379         double newDistance = oneCity->distanceTo(lng, lat);
380         if (newDistance < distance)
381         {
382             distance = newDistance;
383             nearest = oneCity;
384         }
385     }
386 
387     return nearest;
388 }
389 
setLocationFromOptions()390 void KStarsData::setLocationFromOptions()
391 {
392     setLocation(GeoLocation(dms(Options::longitude()), dms(Options::latitude()), Options::cityName(),
393                             Options::provinceName(), Options::countryName(), Options::timeZone(),
394                             &(Rulebook[Options::dST()]), Options::elevation(), false, 4));
395 }
396 
setLocation(const GeoLocation & l)397 void KStarsData::setLocation(const GeoLocation &l)
398 {
399     m_Geo = GeoLocation(l);
400     if (m_Geo.lat()->Degrees() >= 90.0)
401         m_Geo.setLat(dms(89.99));
402     if (m_Geo.lat()->Degrees() <= -90.0)
403         m_Geo.setLat(dms(-89.99));
404 
405     //store data in the Options objects
406     Options::setCityName(m_Geo.name());
407     Options::setProvinceName(m_Geo.province());
408     Options::setCountryName(m_Geo.country());
409     Options::setTimeZone(m_Geo.TZ0());
410     Options::setElevation(m_Geo.elevation());
411     Options::setLongitude(m_Geo.lng()->Degrees());
412     Options::setLatitude(m_Geo.lat()->Degrees());
413     // set the rule from rulebook
414     foreach (const QString &key, Rulebook.keys())
415     {
416         if (!key.isEmpty() && m_Geo.tzrule()->equals(&Rulebook[key]))
417             Options::setDST(key);
418     }
419 
420     emit geoChanged();
421 }
422 
objectNamed(const QString & name)423 SkyObject *KStarsData::objectNamed(const QString &name)
424 {
425     if ((name == "star") || (name == "nothing") || name.isEmpty())
426         return nullptr;
427     return skyComposite()->findByName(name);
428 }
429 
readCityData()430 bool KStarsData::readCityData()
431 {
432     QSqlDatabase citydb = QSqlDatabase::addDatabase("QSQLITE", "citydb");
433     QString dbfile      = KSPaths::locate(QStandardPaths::AppDataLocation, "citydb.sqlite");
434     citydb.setDatabaseName(dbfile);
435     if (citydb.open() == false)
436     {
437         qCCritical(KSTARS) << "Unable to open city database file " << dbfile << citydb.lastError().text();
438         return false;
439     }
440 
441     QSqlQuery get_query(citydb);
442 
443     //get_query.prepare("SELECT * FROM city");
444     if (!get_query.exec("SELECT * FROM city"))
445     {
446         qCCritical(KSTARS) << get_query.lastError();
447         return false;
448     }
449 
450     bool citiesFound = false;
451     // get_query.size() always returns -1 so we set citiesFound if at least one city is found
452     while (get_query.next())
453     {
454         citiesFound          = true;
455         QString name         = get_query.value(1).toString();
456         QString province     = get_query.value(2).toString();
457         QString country      = get_query.value(3).toString();
458         dms lat              = dms(get_query.value(4).toString());
459         dms lng              = dms(get_query.value(5).toString());
460         double TZ            = get_query.value(6).toDouble();
461         TimeZoneRule *TZrule = &(Rulebook[get_query.value(7).toString()]);
462         double elevation     = get_query.value(8).toDouble();
463 
464         // appends city names to list
465         geoList.append(new GeoLocation(lng, lat, name, province, country, TZ, TZrule, elevation, true, 4));
466     }
467     citydb.close();
468 
469     // Reading local database
470     QSqlDatabase mycitydb = QSqlDatabase::addDatabase("QSQLITE", "mycitydb");
471     dbfile = QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("mycitydb.sqlite");
472 
473     if (QFile::exists(dbfile))
474     {
475         mycitydb.setDatabaseName(dbfile);
476         if (mycitydb.open())
477         {
478             QSqlQuery get_query(mycitydb);
479 
480             if (!get_query.exec("SELECT * FROM city"))
481             {
482                 qDebug() << get_query.lastError();
483                 return false;
484             }
485             while (get_query.next())
486             {
487                 QString name         = get_query.value(1).toString();
488                 QString province     = get_query.value(2).toString();
489                 QString country      = get_query.value(3).toString();
490                 dms lat              = dms(get_query.value(4).toString());
491                 dms lng              = dms(get_query.value(5).toString());
492                 double TZ            = get_query.value(6).toDouble();
493                 TimeZoneRule *TZrule = &(Rulebook[get_query.value(7).toString()]);
494                 double elevation     = get_query.value(8).toDouble();
495 
496                 // appends city names to list
497                 geoList.append(new GeoLocation(lng, lat, name, province, country, TZ, TZrule, elevation, false, 4));
498             }
499             mycitydb.close();
500         }
501     }
502 
503     return citiesFound;
504 }
505 
readTimeZoneRulebook()506 bool KStarsData::readTimeZoneRulebook()
507 {
508     QFile file;
509 
510     if (KSUtils::openDataFile(file, "TZrules.dat"))
511     {
512         QTextStream stream(&file);
513 
514         while (!stream.atEnd())
515         {
516             QString line = stream.readLine().trimmed();
517             if (line.length() && !line.startsWith('#')) //ignore commented and blank lines
518             {
519                 QStringList fields = line.split(' ', QString::SkipEmptyParts);
520                 QString id         = fields[0];
521                 QTime stime        = QTime(fields[3].leftRef(fields[3].indexOf(':')).toInt(),
522                                            fields[3].midRef(fields[3].indexOf(':') + 1, fields[3].length()).toInt());
523                 QTime rtime        = QTime(fields[6].leftRef(fields[6].indexOf(':')).toInt(),
524                                            fields[6].midRef(fields[6].indexOf(':') + 1, fields[6].length()).toInt());
525 
526                 Rulebook[id] = TimeZoneRule(fields[1], fields[2], stime, fields[4], fields[5], rtime);
527             }
528         }
529         return true;
530     }
531     else
532     {
533         return false;
534     }
535 }
536 
openUrlFile(const QString & urlfile,QFile & file)537 bool KStarsData::openUrlFile(const QString &urlfile, QFile &file)
538 {
539     //QFile file;
540     QString localFile;
541     bool fileFound = false;
542     QFile localeFile;
543 
544     //if ( locale->language() != "en_US" )
545     if (QLocale().language() != QLocale::English)
546         //localFile = locale->language() + '/' + urlfile;
547         localFile = QLocale().languageToString(QLocale().language()) + '/' + urlfile;
548 
549     if (!localFile.isEmpty() && KSUtils::openDataFile(file, localFile))
550     {
551         fileFound = true;
552     }
553     else
554     {
555         // Try to load locale file, if not successful, load regular urlfile and then copy it to locale.
556         file.setFileName(QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath(urlfile));
557         if (file.open(QIODevice::ReadOnly))
558         {
559             //local file found.  Now, if global file has newer timestamp, then merge the two files.
560             //First load local file into QStringList
561             bool newDataFound(false);
562             QStringList urlData;
563             QTextStream lStream(&file);
564             while (!lStream.atEnd())
565                 urlData.append(lStream.readLine());
566 
567             //Find global file(s) in findAllResources() list.
568             QFileInfo fi_local(file.fileName());
569 
570             QStringList flist = KSPaths::locateAll(QStandardPaths::DataLocation, urlfile);
571 
572             for (int i = 0; i < flist.size(); i++)
573             {
574                 if (flist[i] != file.fileName())
575                 {
576                     QFileInfo fi_global(flist[i]);
577 
578                     //Is this global file newer than the local file?
579                     if (fi_global.lastModified() > fi_local.lastModified())
580                     {
581                         //Global file has newer timestamp than local.  Add lines in global file that don't already exist in local file.
582                         //be smart about this; in some cases the URL is updated but the image is probably the same if its
583                         //label string is the same.  So only check strings up to last ":"
584                         QFile globalFile(flist[i]);
585                         if (globalFile.open(QIODevice::ReadOnly))
586                         {
587                             QTextStream gStream(&globalFile);
588                             while (!gStream.atEnd())
589                             {
590                                 QString line = gStream.readLine();
591 
592                                 //If global-file line begins with "XXX:" then this line should be removed from the local file.
593                                 if (line.startsWith(QLatin1String("XXX:")) && urlData.contains(line.mid(4)))
594                                 {
595                                     urlData.removeAt(urlData.indexOf(line.mid(4)));
596                                 }
597                                 else
598                                 {
599                                     //does local file contain the current global file line, up to second ':' ?
600 
601                                     bool linefound(false);
602                                     for (int j = 0; j < urlData.size(); ++j)
603                                     {
604                                         if (urlData[j].contains(line.left(line.indexOf(':', line.indexOf(':') + 1))))
605                                         {
606                                             //replace line in urlData with its equivalent in the newer global file.
607                                             urlData.replace(j, line);
608                                             if (!newDataFound)
609                                                 newDataFound = true;
610                                             linefound = true;
611                                             break;
612                                         }
613                                     }
614                                     if (!linefound)
615                                     {
616                                         urlData.append(line);
617                                         if (!newDataFound)
618                                             newDataFound = true;
619                                     }
620                                 }
621                             }
622                         }
623                     }
624                 }
625             }
626 
627             file.close();
628 
629             //(possibly) write appended local file
630             if (newDataFound)
631             {
632                 if (file.open(QIODevice::WriteOnly))
633                 {
634                     QTextStream outStream(&file);
635                     for (int i = 0; i < urlData.size(); i++)
636                     {
637                         outStream << urlData[i] << '\n';
638                     }
639                     file.close();
640                 }
641             }
642 
643             if (file.open(QIODevice::ReadOnly))
644                 fileFound = true;
645         }
646         else
647         {
648             if (KSUtils::openDataFile(file, urlfile))
649             {
650                 if (QLocale().language() != QLocale::English)
651                     qDebug() << "No localized URL file; using default English file.";
652                 // we found urlfile, we need to copy it to locale
653                 localeFile.setFileName(QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath(urlfile));
654                 if (localeFile.open(QIODevice::WriteOnly))
655                 {
656                     QTextStream readStream(&file);
657                     QTextStream writeStream(&localeFile);
658                     while (!readStream.atEnd())
659                     {
660                         QString line = readStream.readLine();
661                         if (!line.startsWith(QLatin1String("XXX:"))) //do not write "deleted" lines
662                             writeStream << line << '\n';
663                     }
664 
665                     localeFile.close();
666                     file.reset();
667                 }
668                 else
669                 {
670                     qDebug() << "Failed to copy default URL file to locale folder, modifying default object links is "
671                              "not possible";
672                 }
673                 fileFound = true;
674             }
675         }
676     }
677     return fileFound;
678 }
679 
readURLData(const QString & urlfile,SkyObjectUserdata::Type type)680 bool KStarsData::readURLData(const QString &urlfile, SkyObjectUserdata::Type type)
681 {
682 #ifndef KSTARS_LITE
683     if (KStars::Closing)
684         return true;
685 #endif
686 
687     QFile file;
688     if (!openUrlFile(urlfile, file))
689         return false;
690 
691     QTextStream stream(&file);
692     QMutexLocker _{ &m_user_data_mutex };
693 
694     while (!stream.atEnd())
695     {
696         QString line = stream.readLine();
697 
698         //ignore comment lines
699         if (!line.startsWith('#'))
700         {
701 #ifndef KSTARS_LITE
702             if (KStars::Closing)
703             {
704                 file.close();
705                 return true;
706             }
707 #endif
708 
709             int idx      = line.indexOf(':');
710             QString name = line.left(idx);
711             if (name == "XXX")
712                 continue;
713             QString sub   = line.mid(idx + 1);
714             idx           = sub.indexOf(':');
715             QString title = sub.left(idx);
716             QString url   = sub.mid(idx + 1);
717             // Dirty hack to fix things up for planets
718 
719             //            if (name == "Mercury" || name == "Venus" || name == "Mars" || name == "Jupiter" || name == "Saturn" ||
720             //                    name == "Uranus" || name == "Neptune" /* || name == "Pluto" */)
721             //                o = skyComposite()->findByName(i18n(name.toLocal8Bit().data()));
722             //            else
723 
724             auto &data_element = m_user_data[name];
725             data_element.addLink(title, QUrl{ url }, type);
726         }
727     }
728     file.close();
729     return true;
730 }
731 
732 // FIXME: Improve the user log system
733 
734 // Note: It might be very nice to keep the log in plaintext files, for
735 // portability, human-readability, and greppability. However, it takes
736 // a lot of time to parse and look up, is very messy from the
737 // reliability and programming point of view, needs to be parsed at
738 // start, can become corrupt easily because of a missing bracket...
739 
740 // An SQLite database is a good compromise. A user can easily view it
741 // using an SQLite browser. There is no need to read at start-up, one
742 // can read the log when required. Easy to edit logs / update logs
743 // etc. Will not become corrupt. Needn't be parsed.
744 
745 // However, IMHO, it is best to put these kinds of things in separate
746 // databases, instead of unifying them as a table under the user
747 // database. This ensures portability and a certain robustness that if
748 // a user opens it, they cannot incorrectly edit a part of the DB they
749 // did not intend to edit.
750 
751 // --asimha 2016 Aug 17
752 
753 // FIXME: This is a significant contributor to KStars startup time.
readUserLog()754 bool KStarsData::readUserLog()
755 {
756     QFile file;
757     QString buffer;
758     QString sub, name, data;
759 
760     if (!KSUtils::openDataFile(file, "userlog.dat"))
761         return false;
762 
763     QTextStream stream(&file);
764 
765     if (!stream.atEnd())
766 
767         buffer = stream.readAll();
768     QMutexLocker _{ &m_user_data_mutex };
769 
770     while (!buffer.isEmpty())
771     {
772         int startIndex, endIndex;
773 
774         startIndex = buffer.indexOf(QLatin1String("[KSLABEL:"));
775         sub        = buffer.mid(
776             startIndex); // FIXME: This is inefficient because we are making a copy of a huge string!
777         endIndex = sub.indexOf(QLatin1String("[KSLogEnd]"));
778 
779         // Read name after KSLABEL identifier
780         name = sub.mid(startIndex + 9, sub.indexOf(']') - (startIndex + 9));
781         // Read data and skip new line
782         data   = sub.mid(sub.indexOf(']') + 2, endIndex - (sub.indexOf(']') + 2));
783         buffer = buffer.mid(endIndex + 11);
784 
785         auto &data_element   = m_user_data[name];
786         data_element.userLog = data;
787 
788     } // end while
789     file.close();
790     return true;
791 }
792 
readADVTreeData()793 bool KStarsData::readADVTreeData()
794 {
795     QFile file;
796     QString Interface;
797     QString Name, Link, subName;
798 
799     if (!KSUtils::openDataFile(file, "advinterface.dat"))
800         return false;
801 
802     QTextStream stream(&file);
803     QString Line;
804 
805     while (!stream.atEnd())
806     {
807         int Type, interfaceIndex;
808 
809         Line = stream.readLine();
810 
811         if (Line.startsWith(QLatin1String("[KSLABEL]")))
812         {
813             Name = Line.mid(9);
814             Type = 0;
815         }
816         else if (Line.startsWith(QLatin1String("[END]")))
817             Type = 1;
818         else if (Line.startsWith(QLatin1String("[KSINTERFACE]")))
819         {
820             Interface = Line.mid(13);
821             continue;
822         }
823 
824         else
825         {
826             int idx = Line.indexOf(':');
827             Name    = Line.left(idx);
828             Link    = Line.mid(idx + 1);
829 
830             // Link is empty, using Interface instead
831             if (Link.isEmpty())
832             {
833                 Link           = Interface;
834                 subName        = Name;
835                 interfaceIndex = Link.indexOf(QLatin1String("KSINTERFACE"));
836                 Link.remove(interfaceIndex, 11);
837                 Link = Link.insert(interfaceIndex, subName.replace(' ', '+'));
838             }
839 
840             Type = 2;
841         }
842 
843         ADVTreeData *ADVData = new ADVTreeData;
844 
845         ADVData->Name = Name;
846         ADVData->Link = Link;
847         ADVData->Type = Type;
848 
849         ADVtreeList.append(ADVData);
850     }
851 
852     return true;
853 }
854 
855 //There's a lot of code duplication here, but it's not avoidable because
856 //this function is only called from main.cpp when the user is using
857 //"dump" mode to produce an image from the command line.  In this mode,
858 //there is no KStars object, so none of the DBus functions can be called
859 //directly.
executeScript(const QString & scriptname,SkyMap * map)860 bool KStarsData::executeScript(const QString &scriptname, SkyMap *map)
861 {
862 #ifndef KSTARS_LITE
863     int cmdCount(0);
864 
865     QFile f(scriptname);
866     if (!f.open(QIODevice::ReadOnly))
867     {
868         qDebug() << "Could not open file " << f.fileName();
869         return false;
870     }
871 
872     QTextStream istream(&f);
873     while (!istream.atEnd())
874     {
875         QString line = istream.readLine();
876         line.remove("string:");
877         line.remove("int32:");
878         line.remove("double:");
879         line.remove("bool:");
880 
881         //find a dbus line and extract the function name and its arguments
882         //The function name starts after the last occurrence of "org.kde.kstars."
883         //or perhaps "org.kde.kstars.SimClock.".
884         if (line.startsWith(QString("dbus-send")))
885         {
886             QString funcprefix = "org.kde.kstars.SimClock.";
887             int i              = line.lastIndexOf(funcprefix);
888             if (i >= 0)
889             {
890                 i += funcprefix.length();
891             }
892             else
893             {
894                 funcprefix = "org.kde.kstars.";
895                 i          = line.lastIndexOf(funcprefix);
896                 if (i >= 0)
897                 {
898                     i += funcprefix.length();
899                 }
900             }
901             if (i < 0)
902             {
903                 qWarning() << "Could not parse line: " << line;
904                 return false;
905             }
906 
907             QStringList fn = line.mid(i).split(' ');
908 
909             //DEBUG
910             //qDebug() << fn;
911 
912             if (fn[0] == "lookTowards" && fn.size() >= 2)
913             {
914                 double az(-1.0);
915                 QString arg = fn[1].toLower();
916                 if (arg == "n" || arg == "north")
917                     az = 0.0;
918                 if (arg == "ne" || arg == "northeast")
919                     az = 45.0;
920                 if (arg == "e" || arg == "east")
921                     az = 90.0;
922                 if (arg == "se" || arg == "southeast")
923                     az = 135.0;
924                 if (arg == "s" || arg == "south")
925                     az = 180.0;
926                 if (arg == "sw" || arg == "southwest")
927                     az = 225.0;
928                 if (arg == "w" || arg == "west")
929                     az = 270.0;
930                 if (arg == "nw" || arg == "northwest")
931                     az = 335.0;
932                 if (az >= 0.0)
933                 {
934                     // N.B. unrefract() doesn't matter at 90 degrees
935                     map->setFocusAltAz(dms(90.0), map->focus()->az());
936                     map->focus()->HorizontalToEquatorial(&LST, geo()->lat());
937                     map->setDestination(*map->focus());
938                     cmdCount++;
939                 }
940 
941                 if (arg == "z" || arg == "zenith")
942                 {
943                     // N.B. unrefract() doesn't matter at 90 degrees
944                     map->setFocusAltAz(dms(90.0), map->focus()->az());
945                     map->focus()->HorizontalToEquatorial(&LST, geo()->lat());
946                     map->setDestination(*map->focus());
947                     cmdCount++;
948                 }
949 
950                 //try a named object.  The name is everything after fn[0],
951                 //concatenated with spaces.
952                 fn.removeAll(fn.first());
953                 QString objname   = fn.join(" ");
954                 SkyObject *target = objectNamed(objname);
955                 if (target)
956                 {
957                     map->setFocus(target);
958                     map->focus()->EquatorialToHorizontal(&LST, geo()->lat());
959                     map->setDestination(*map->focus());
960                     cmdCount++;
961                 }
962             }
963             else if (fn[0] == "setRaDec" && fn.size() == 3)
964             {
965                 bool ok(false);
966                 dms r(0.0), d(0.0);
967 
968                 ok = r.setFromString(fn[1], false); //assume angle in hours
969                 if (ok)
970                     ok = d.setFromString(fn[2], true); //assume angle in degrees
971                 if (ok)
972                 {
973                     map->setFocus(r, d);
974                     map->focus()->EquatorialToHorizontal(&LST, geo()->lat());
975                     cmdCount++;
976                 }
977             }
978             else if (fn[0] == "setAltAz" && fn.size() == 3)
979             {
980                 bool ok(false);
981                 dms az(0.0), alt(0.0);
982 
983                 ok = alt.setFromString(fn[1]);
984                 if (ok)
985                     ok = az.setFromString(fn[2]);
986                 if (ok)
987                 {
988                     map->setFocusAltAz(alt, az);
989                     map->focus()->HorizontalToEquatorial(&LST, geo()->lat());
990                     cmdCount++;
991                 }
992             }
993             else if (fn[0] == "loadColorScheme")
994             {
995                 fn.removeAll(fn.first());
996                 QString csName = fn.join(" ").remove('\"');
997                 qCDebug(KSTARS) << "Loading Color scheme: " << csName;
998 
999                 QString filename = csName.toLower().trimmed();
1000                 bool ok(false);
1001 
1002                 //Parse default names which don't follow the regular file-naming scheme
1003                 if (csName == i18nc("use default color scheme", "Default Colors"))
1004                     filename = "classic.colors";
1005                 if (csName == i18nc("use 'star chart' color scheme", "Star Chart"))
1006                     filename = "chart.colors";
1007                 if (csName == i18nc("use 'night vision' color scheme", "Night Vision"))
1008                     filename = "night.colors";
1009 
1010                 //Try the filename if it ends with ".colors"
1011                 if (filename.endsWith(QLatin1String(".colors")))
1012                     ok = colorScheme()->load(filename);
1013 
1014                 //If that didn't work, try assuming that 'name' is the color scheme name
1015                 //convert it to a filename exactly as ColorScheme::save() does
1016                 if (!ok)
1017                 {
1018                     if (!filename.isEmpty())
1019                     {
1020                         for (int i = 0; i < filename.length(); ++i)
1021                             if (filename.at(i) == ' ')
1022                                 filename.replace(i, 1, "-");
1023 
1024                         filename = filename.append(".colors");
1025                         ok       = colorScheme()->load(filename);
1026                     }
1027 
1028                     if (!ok)
1029                         qDebug() << QString("Unable to load color scheme named %1. Also tried %2.")
1030                                  .arg(csName, filename);
1031                 }
1032             }
1033             else if (fn[0] == "zoom" && fn.size() == 2)
1034             {
1035                 bool ok(false);
1036                 double z = fn[1].toDouble(&ok);
1037                 if (ok)
1038                 {
1039                     if (z > MAXZOOM)
1040                         z = MAXZOOM;
1041                     if (z < MINZOOM)
1042                         z = MINZOOM;
1043                     Options::setZoomFactor(z);
1044                     cmdCount++;
1045                 }
1046             }
1047             else if (fn[0] == "zoomIn")
1048             {
1049                 if (Options::zoomFactor() < MAXZOOM)
1050                 {
1051                     Options::setZoomFactor(Options::zoomFactor() * DZOOM);
1052                     cmdCount++;
1053                 }
1054             }
1055             else if (fn[0] == "zoomOut")
1056             {
1057                 if (Options::zoomFactor() > MINZOOM)
1058                 {
1059                     Options::setZoomFactor(Options::zoomFactor() / DZOOM);
1060                     cmdCount++;
1061                 }
1062             }
1063             else if (fn[0] == "defaultZoom")
1064             {
1065                 Options::setZoomFactor(DEFAULTZOOM);
1066                 cmdCount++;
1067             }
1068             else if (fn[0] == "setLocalTime" && fn.size() == 7)
1069             {
1070                 bool ok(false);
1071                 // min is a macro - use mnt
1072                 int yr(0), mth(0), day(0), hr(0), mnt(0), sec(0);
1073                 yr = fn[1].toInt(&ok);
1074                 if (ok)
1075                     mth = fn[2].toInt(&ok);
1076                 if (ok)
1077                     day = fn[3].toInt(&ok);
1078                 if (ok)
1079                     hr = fn[4].toInt(&ok);
1080                 if (ok)
1081                     mnt = fn[5].toInt(&ok);
1082                 if (ok)
1083                     sec = fn[6].toInt(&ok);
1084                 if (ok)
1085                 {
1086                     changeDateTime(geo()->LTtoUT(KStarsDateTime(QDate(yr, mth, day), QTime(hr, mnt, sec))));
1087                     cmdCount++;
1088                 }
1089                 else
1090                 {
1091                     qWarning() << ki18n("Could not set time: %1 / %2 / %3 ; %4:%5:%6")
1092                                .subs(day)
1093                                .subs(mth)
1094                                .subs(yr)
1095                                .subs(hr)
1096                                .subs(mnt)
1097                                .subs(sec)
1098                                .toString();
1099                 }
1100             }
1101             else if (fn[0] == "changeViewOption" && fn.size() == 3)
1102             {
1103                 bool bOk(false), dOk(false);
1104 
1105                 //parse bool value
1106                 bool bVal(false);
1107                 if (fn[2].toLower() == "true")
1108                 {
1109                     bOk  = true;
1110                     bVal = true;
1111                 }
1112                 if (fn[2].toLower() == "false")
1113                 {
1114                     bOk  = true;
1115                     bVal = false;
1116                 }
1117                 if (fn[2] == "1")
1118                 {
1119                     bOk  = true;
1120                     bVal = true;
1121                 }
1122                 if (fn[2] == "0")
1123                 {
1124                     bOk  = true;
1125                     bVal = false;
1126                 }
1127 
1128                 //parse double value
1129                 double dVal = fn[2].toDouble(&dOk);
1130 
1131                 // FIXME: REGRESSION
1132                 //                if ( fn[1] == "FOVName"                ) { Options::setFOVName(       fn[2] ); cmdCount++; }
1133                 //                if ( fn[1] == "FOVSizeX"         && dOk ) { Options::setFOVSizeX( (float)dVal ); cmdCount++; }
1134                 //                if ( fn[1] == "FOVSizeY"         && dOk ) { Options::setFOVSizeY( (float)dVal ); cmdCount++; }
1135                 //                if ( fn[1] == "FOVShape"        && nOk ) { Options::setFOVShape(       nVal ); cmdCount++; }
1136                 //                if ( fn[1] == "FOVColor"               ) { Options::setFOVColor(      fn[2] ); cmdCount++; }
1137                 if (fn[1] == "ShowStars" && bOk)
1138                 {
1139                     Options::setShowStars(bVal);
1140                     cmdCount++;
1141                 }
1142                 if (fn[1] == "ShowCLines" && bOk)
1143                 {
1144                     Options::setShowCLines(bVal);
1145                     cmdCount++;
1146                 }
1147                 if (fn[1] == "ShowCNames" && bOk)
1148                 {
1149                     Options::setShowCNames(bVal);
1150                     cmdCount++;
1151                 }
1152                 if (fn[1] == "ShowMilkyWay" && bOk)
1153                 {
1154                     Options::setShowMilkyWay(bVal);
1155                     cmdCount++;
1156                 }
1157                 if (fn[1] == "ShowEquatorialGrid" && bOk)
1158                 {
1159                     Options::setShowEquatorialGrid(bVal);
1160                     cmdCount++;
1161                 }
1162                 if (fn[1] == "ShowHorizontalGrid" && bOk)
1163                 {
1164                     Options::setShowHorizontalGrid(bVal);
1165                     cmdCount++;
1166                 }
1167                 if (fn[1] == "ShowEquator" && bOk)
1168                 {
1169                     Options::setShowEquator(bVal);
1170                     cmdCount++;
1171                 }
1172                 if (fn[1] == "ShowEcliptic" && bOk)
1173                 {
1174                     Options::setShowEcliptic(bVal);
1175                     cmdCount++;
1176                 }
1177                 if (fn[1] == "ShowHorizon" && bOk)
1178                 {
1179                     Options::setShowHorizon(bVal);
1180                     cmdCount++;
1181                 }
1182                 if (fn[1] == "ShowGround" && bOk)
1183                 {
1184                     Options::setShowGround(bVal);
1185                     cmdCount++;
1186                 }
1187                 if (fn[1] == "ShowSun" && bOk)
1188                 {
1189                     Options::setShowSun(bVal);
1190                     cmdCount++;
1191                 }
1192                 if (fn[1] == "ShowMoon" && bOk)
1193                 {
1194                     Options::setShowMoon(bVal);
1195                     cmdCount++;
1196                 }
1197                 if (fn[1] == "ShowMercury" && bOk)
1198                 {
1199                     Options::setShowMercury(bVal);
1200                     cmdCount++;
1201                 }
1202                 if (fn[1] == "ShowVenus" && bOk)
1203                 {
1204                     Options::setShowVenus(bVal);
1205                     cmdCount++;
1206                 }
1207                 if (fn[1] == "ShowMars" && bOk)
1208                 {
1209                     Options::setShowMars(bVal);
1210                     cmdCount++;
1211                 }
1212                 if (fn[1] == "ShowJupiter" && bOk)
1213                 {
1214                     Options::setShowJupiter(bVal);
1215                     cmdCount++;
1216                 }
1217                 if (fn[1] == "ShowSaturn" && bOk)
1218                 {
1219                     Options::setShowSaturn(bVal);
1220                     cmdCount++;
1221                 }
1222                 if (fn[1] == "ShowUranus" && bOk)
1223                 {
1224                     Options::setShowUranus(bVal);
1225                     cmdCount++;
1226                 }
1227                 if (fn[1] == "ShowNeptune" && bOk)
1228                 {
1229                     Options::setShowNeptune(bVal);
1230                     cmdCount++;
1231                 }
1232                 //if ( fn[1] == "ShowPluto"       && bOk ) { Options::setShowPluto(    bVal ); cmdCount++; }
1233                 if (fn[1] == "ShowAsteroids" && bOk)
1234                 {
1235                     Options::setShowAsteroids(bVal);
1236                     cmdCount++;
1237                 }
1238                 if (fn[1] == "ShowComets" && bOk)
1239                 {
1240                     Options::setShowComets(bVal);
1241                     cmdCount++;
1242                 }
1243                 if (fn[1] == "ShowSolarSystem" && bOk)
1244                 {
1245                     Options::setShowSolarSystem(bVal);
1246                     cmdCount++;
1247                 }
1248                 if (fn[1] == "ShowDeepSky" && bOk)
1249                 {
1250                     Options::setShowDeepSky(bVal);
1251                     cmdCount++;
1252                 }
1253                 if (fn[1] == "ShowSupernovae" && bOk)
1254                 {
1255                     Options::setShowSupernovae(bVal);
1256                     cmdCount++;
1257                 }
1258                 if (fn[1] == "ShowStarNames" && bOk)
1259                 {
1260                     Options::setShowStarNames(bVal);
1261                     cmdCount++;
1262                 }
1263                 if (fn[1] == "ShowStarMagnitudes" && bOk)
1264                 {
1265                     Options::setShowStarMagnitudes(bVal);
1266                     cmdCount++;
1267                 }
1268                 if (fn[1] == "ShowAsteroidNames" && bOk)
1269                 {
1270                     Options::setShowAsteroidNames(bVal);
1271                     cmdCount++;
1272                 }
1273                 if (fn[1] == "ShowCometNames" && bOk)
1274                 {
1275                     Options::setShowCometNames(bVal);
1276                     cmdCount++;
1277                 }
1278                 if (fn[1] == "ShowPlanetNames" && bOk)
1279                 {
1280                     Options::setShowPlanetNames(bVal);
1281                     cmdCount++;
1282                 }
1283                 if (fn[1] == "ShowPlanetImages" && bOk)
1284                 {
1285                     Options::setShowPlanetImages(bVal);
1286                     cmdCount++;
1287                 }
1288 
1289                 if (fn[1] == "UseAltAz" && bOk)
1290                 {
1291                     Options::setUseAltAz(bVal);
1292                     cmdCount++;
1293                 }
1294                 if (fn[1] == "UseRefraction" && bOk)
1295                 {
1296                     Options::setUseRefraction(bVal);
1297                     cmdCount++;
1298                 }
1299                 if (fn[1] == "UseAutoLabel" && bOk)
1300                 {
1301                     Options::setUseAutoLabel(bVal);
1302                     cmdCount++;
1303                 }
1304                 if (fn[1] == "UseAutoTrail" && bOk)
1305                 {
1306                     Options::setUseAutoTrail(bVal);
1307                     cmdCount++;
1308                 }
1309                 if (fn[1] == "UseAnimatedSlewing" && bOk)
1310                 {
1311                     Options::setUseAnimatedSlewing(bVal);
1312                     cmdCount++;
1313                 }
1314                 if (fn[1] == "FadePlanetTrails" && bOk)
1315                 {
1316                     Options::setFadePlanetTrails(bVal);
1317                     cmdCount++;
1318                 }
1319                 if (fn[1] == "SlewTimeScale" && dOk)
1320                 {
1321                     Options::setSlewTimeScale(dVal);
1322                     cmdCount++;
1323                 }
1324                 if (fn[1] == "ZoomFactor" && dOk)
1325                 {
1326                     Options::setZoomFactor(dVal);
1327                     cmdCount++;
1328                 }
1329                 //                if ( fn[1] == "MagLimitDrawStar"     && dOk ) { Options::setMagLimitDrawStar( dVal ); cmdCount++; }
1330                 if (fn[1] == "StarDensity" && dOk)
1331                 {
1332                     Options::setStarDensity(dVal);
1333                     cmdCount++;
1334                 }
1335                 //                if ( fn[1] == "MagLimitDrawStarZoomOut" && dOk ) { Options::setMagLimitDrawStarZoomOut( dVal ); cmdCount++; }
1336                 if (fn[1] == "MagLimitDrawDeepSky" && dOk)
1337                 {
1338                     Options::setMagLimitDrawDeepSky(dVal);
1339                     cmdCount++;
1340                 }
1341                 if (fn[1] == "MagLimitDrawDeepSkyZoomOut" && dOk)
1342                 {
1343                     Options::setMagLimitDrawDeepSkyZoomOut(dVal);
1344                     cmdCount++;
1345                 }
1346                 if (fn[1] == "StarLabelDensity" && dOk)
1347                 {
1348                     Options::setStarLabelDensity(dVal);
1349                     cmdCount++;
1350                 }
1351                 if (fn[1] == "MagLimitHideStar" && dOk)
1352                 {
1353                     Options::setMagLimitHideStar(dVal);
1354                     cmdCount++;
1355                 }
1356                 if (fn[1] == "MagLimitAsteroid" && dOk)
1357                 {
1358                     Options::setMagLimitAsteroid(dVal);
1359                     cmdCount++;
1360                 }
1361                 if (fn[1] == "AsteroidLabelDensity" && dOk)
1362                 {
1363                     Options::setAsteroidLabelDensity(dVal);
1364                     cmdCount++;
1365                 }
1366                 if (fn[1] == "MaxRadCometName" && dOk)
1367                 {
1368                     Options::setMaxRadCometName(dVal);
1369                     cmdCount++;
1370                 }
1371 
1372                 //these three are a "radio group"
1373                 if (fn[1] == "UseLatinConstellationNames" && bOk)
1374                 {
1375                     Options::setUseLatinConstellNames(true);
1376                     Options::setUseLocalConstellNames(false);
1377                     Options::setUseAbbrevConstellNames(false);
1378                     cmdCount++;
1379                 }
1380                 if (fn[1] == "UseLocalConstellationNames" && bOk)
1381                 {
1382                     Options::setUseLatinConstellNames(false);
1383                     Options::setUseLocalConstellNames(true);
1384                     Options::setUseAbbrevConstellNames(false);
1385                     cmdCount++;
1386                 }
1387                 if (fn[1] == "UseAbbrevConstellationNames" && bOk)
1388                 {
1389                     Options::setUseLatinConstellNames(false);
1390                     Options::setUseLocalConstellNames(false);
1391                     Options::setUseAbbrevConstellNames(true);
1392                     cmdCount++;
1393                 }
1394             }
1395             else if (fn[0] == "setGeoLocation" && (fn.size() == 3 || fn.size() == 4))
1396             {
1397                 QString city(fn[1]), province, country(fn[2]);
1398                 province.clear();
1399                 if (fn.size() == 4)
1400                 {
1401                     province = fn[2];
1402                     country  = fn[3];
1403                 }
1404 
1405                 bool cityFound(false);
1406                 foreach (GeoLocation *loc, geoList)
1407                 {
1408                     if (loc->translatedName() == city &&
1409                             (province.isEmpty() || loc->translatedProvince() == province) &&
1410                             loc->translatedCountry() == country)
1411                     {
1412                         cityFound = true;
1413                         setLocation(*loc);
1414                         cmdCount++;
1415                         break;
1416                     }
1417                 }
1418 
1419                 if (!cityFound)
1420                     qWarning() << i18n("Could not set location named %1, %2, %3", city, province, country);
1421             }
1422         }
1423     } //end while
1424 
1425     if (cmdCount)
1426         return true;
1427 #else
1428     Q_UNUSED(map)
1429     Q_UNUSED(scriptname)
1430 #endif
1431     return false;
1432 }
1433 
1434 #ifndef KSTARS_LITE
syncFOV()1435 void KStarsData::syncFOV()
1436 {
1437     visibleFOVs.clear();
1438     // Add visible FOVs
1439     foreach (FOV *fov, availFOVs)
1440     {
1441         if (Options::fOVNames().contains(fov->name()))
1442             visibleFOVs.append(fov);
1443     }
1444     // Remove unavailable FOVs
1445     QSet<QString> names = QSet<QString>::fromList(Options::fOVNames());
1446     QSet<QString> all;
1447     foreach (FOV *fov, visibleFOVs)
1448     {
1449         all.insert(fov->name());
1450     }
1451     Options::setFOVNames(all.intersect(names).toList());
1452 }
1453 
1454 // FIXME: Why does KStarsData store the Execute instance??? -- asimha
executeSession()1455 Execute *KStarsData::executeSession()
1456 {
1457     if (!m_Execute.get())
1458         m_Execute.reset(new Execute());
1459 
1460     return m_Execute.get();
1461 }
1462 
1463 // FIXME: Why does KStarsData store the ImageExporer instance??? KStarsData is supposed to work with no reference to KStars -- asimha
imageExporter()1464 ImageExporter *KStarsData::imageExporter()
1465 {
1466     if (!m_ImageExporter.get())
1467         m_ImageExporter.reset(new ImageExporter(KStars::Instance()));
1468 
1469     return m_ImageExporter.get();
1470 }
1471 #endif
1472 
1473 std::pair<bool, QString>
addToUserData(const QString & name,const SkyObjectUserdata::LinkData & data)1474 KStarsData::addToUserData(const QString &name, const SkyObjectUserdata::LinkData &data)
1475 {
1476     QMutexLocker _{ &m_user_data_mutex };
1477 
1478     findUserData(name).links[data.type].push_back(data);
1479 
1480     QString entry;
1481     QFile file;
1482     const auto isImage = data.type == SkyObjectUserdata::Type::image;
1483 
1484     //Also, update the user's custom image links database
1485     //check for user's image-links database.  If it doesn't exist, create it.
1486     file.setFileName(
1487         KSPaths::writableLocation(QStandardPaths::AppDataLocation) +
1488         (isImage ?
1489              "image_url.dat" :
1490              "info_url.dat")); //determine filename in local user KDE directory tree.
1491 
1492     if (!file.open(QIODevice::ReadWrite | QIODevice::Append))
1493         return { false,
1494                  isImage ?
1495                      i18n("Custom image-links file could not be opened.\nLink cannot "
1496                           "be recorded for future sessions.") :
1497                      i18n("Custom information-links file could not be opened.\nLink "
1498                           "cannot be recorded for future sessions.") };
1499     else
1500     {
1501         entry = name + ':' + data.title + ':' + data.url.toString();
1502         QTextStream stream(&file);
1503         stream << entry << '\n';
1504         file.close();
1505     }
1506 
1507     return { true, {} };
1508 }
1509 
updateLocalDatabase(SkyObjectUserdata::Type type,const QString & search_line,const QString & replace_line)1510 std::pair<bool, QString> updateLocalDatabase(SkyObjectUserdata::Type type,
1511                                              const QString &search_line,
1512                                              const QString &replace_line)
1513 {
1514     QString TempFileName, file_line;
1515     QFile URLFile;
1516     QTemporaryFile TempFile;
1517     TempFile.setAutoRemove(false);
1518     TempFile.open();
1519 
1520     bool replace = !replace_line.isEmpty();
1521 
1522     if (search_line.isEmpty())
1523         return { false, "Invalid update request." };
1524 
1525     TempFileName = TempFile.fileName();
1526 
1527     switch (type)
1528     {
1529             // Info Links
1530         case SkyObjectUserdata::Type::website:
1531             // Get name for our local info_url file
1532             URLFile.setFileName(
1533                 KSPaths::writableLocation(QStandardPaths::AppDataLocation) +
1534                 "info_url.dat");
1535             break;
1536 
1537             // Image Links
1538         case SkyObjectUserdata::Type::image:
1539             // Get name for our local info_url file
1540             URLFile.setFileName(
1541                 KSPaths::writableLocation(QStandardPaths::AppDataLocation) +
1542                 "image_url.dat");
1543             break;
1544     }
1545 
1546     // Copy URL file to temp file
1547     KIO::file_copy(QUrl::fromLocalFile(URLFile.fileName()),
1548                    QUrl::fromLocalFile(TempFileName), -1,
1549                    KIO::Overwrite | KIO::HideProgressInfo);
1550 
1551     if (!URLFile.open(QIODevice::WriteOnly))
1552     {
1553         return { false, "Failed to open " + URLFile.fileName() +
1554                             "KStars cannot save to user database" };
1555     }
1556 
1557     // Get streams;
1558     QTextStream temp_stream(&TempFile);
1559     QTextStream out_stream(&URLFile);
1560 
1561     bool found = false;
1562     while (!temp_stream.atEnd())
1563     {
1564         file_line = temp_stream.readLine();
1565         // If we find a match, either replace, or remove (by skipping).
1566         if (file_line == search_line)
1567         {
1568             found = true;
1569             if (replace)
1570                 (out_stream) << replace_line << '\n';
1571             else
1572                 continue;
1573         }
1574         else
1575             (out_stream) << file_line << '\n';
1576     }
1577 
1578     // just append it if we haven't found it.
1579     if (!found && replace)
1580     {
1581         out_stream << replace_line << '\n';
1582     }
1583 
1584     URLFile.close();
1585 
1586     return { true, {} };
1587 }
1588 
editUserData(const QString & name,const unsigned int index,const SkyObjectUserdata::LinkData & data)1589 std::pair<bool, QString> KStarsData::editUserData(const QString &name,
1590                                                   const unsigned int index,
1591                                                   const SkyObjectUserdata::LinkData &data)
1592 {
1593     QMutexLocker _{ &m_user_data_mutex };
1594 
1595     auto &entry = findUserData(name);
1596     if (index >= entry.links[data.type].size())
1597         return { false, i18n("Userdata at index %1 does not exist.", index) };
1598 
1599     entry.links[data.type][index] = data;
1600 
1601     QString search_line = name;
1602     search_line += ':';
1603     search_line += data.title;
1604     search_line += ':';
1605     search_line += data.url.toString();
1606 
1607     QString replace_line = name + ':' + data.title + ':' + data.url.toString();
1608     return updateLocalDatabase(data.type, search_line, replace_line);
1609 }
1610 
deleteUserData(const QString & name,const unsigned int index,SkyObjectUserdata::Type type)1611 std::pair<bool, QString> KStarsData::deleteUserData(const QString &name,
1612                                                     const unsigned int index,
1613                                                     SkyObjectUserdata::Type type)
1614 {
1615     QMutexLocker _{ &m_user_data_mutex };
1616 
1617     auto &linkList = findUserData(name).links[type];
1618     if (index >= linkList.size())
1619         return { false, i18n("Userdata at index %1 does not exist.", index) };
1620 
1621     const auto data = linkList[index];
1622     linkList.erase(linkList.begin() + index);
1623 
1624     QString search_line = name;
1625     search_line += ':';
1626     search_line += data.title;
1627     search_line += ':';
1628     search_line += data.url.toString();
1629 
1630     QString replace_line = name + ':' + data.title + ':' + data.url.toString();
1631     return updateLocalDatabase(data.type, search_line, "");
1632 }
1633 
updateUserLog(const QString & name,const QString & newLog)1634 std::pair<bool, QString> KStarsData::updateUserLog(const QString &name,
1635                                                    const QString &newLog)
1636 {
1637     QMutexLocker _{ &m_user_data_mutex };
1638 
1639     QFile file;
1640     QString logs; //existing logs
1641 
1642     //Do nothing if:
1643     //+ new log is the "default" message
1644     //+ new log is empty
1645     if (newLog == (i18n("Record here observation logs and/or data on %1.", name)) ||
1646         newLog.isEmpty())
1647         return { true, {} };
1648 
1649     // header label
1650     QString KSLabel = "[KSLABEL:" + name + ']';
1651 
1652     file.setFileName(
1653         KSPaths::writableLocation(QStandardPaths::AppDataLocation) +
1654         "userlog.dat"); //determine filename in local user KDE directory tree.
1655 
1656     if (file.open(QIODevice::ReadOnly))
1657     {
1658         QTextStream instream(&file);
1659         // read all data into memory
1660         logs = instream.readAll();
1661         file.close();
1662     }
1663 
1664     const auto &userLog = m_user_data[name].userLog;
1665 
1666     // Remove old log entry from the logs text
1667     if (!userLog.isEmpty())
1668     {
1669         int startIndex, endIndex;
1670         QString sub;
1671 
1672         startIndex = logs.indexOf(KSLabel);
1673         sub        = logs.mid(startIndex);
1674         endIndex   = sub.indexOf("[KSLogEnd]");
1675 
1676         logs.remove(startIndex, endIndex + 11);
1677     }
1678 
1679     //append the new log entry to the end of the logs text
1680     logs.append(KSLabel + '\n' + newLog.trimmed() + "\n[KSLogEnd]\n");
1681 
1682     //Open file for writing
1683     if (!file.open(QIODevice::WriteOnly))
1684     {
1685         return { false, "Cannot write to user log file" };
1686     }
1687 
1688     //Write new logs text
1689     QTextStream outstream(&file);
1690     outstream << logs;
1691 
1692     file.close();
1693 
1694     findUserData(name).userLog = newLog;
1695     return { true, {} };
1696 };
1697 
getUserData(const QString & name)1698 const SkyObjectUserdata::Data &KStarsData::getUserData(const QString &name)
1699 {
1700     QMutexLocker _{ &m_user_data_mutex };
1701 
1702     return findUserData(name); // we're consting it
1703 }
1704 
findUserData(const QString & name)1705 SkyObjectUserdata::Data &KStarsData::findUserData(const QString &name)
1706 {
1707     auto element = m_user_data.find(name);
1708     if (element != m_user_data.end())
1709     {
1710         return element->second;
1711     }
1712 
1713     // fallback: we did not find it directly, therefore we may try to
1714     // find a matching object
1715     const auto *object = m_SkyComposite->findByName(name);
1716     if (object != nullptr)
1717     {
1718         return m_user_data[object->name()];
1719     }
1720 
1721     return m_user_data[name];
1722 };
1723