File indexing completed on 2024-04-21 03:45:03

0001 /*
0002     SPDX-FileCopyrightText: 2001 Heiko Evermann <heiko@evermann.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "kstarsdata.h"
0008 
0009 #include "ksutils.h"
0010 #include "Options.h"
0011 #include "auxiliary/kspaths.h"
0012 #include "skycomponents/supernovaecomponent.h"
0013 #include "skycomponents/skymapcomposite.h"
0014 #include "ksnotification.h"
0015 #include "skyobjectuserdata.h"
0016 #include <kio/job_base.h>
0017 #include <kio/filecopyjob.h>
0018 #ifndef KSTARS_LITE
0019 #include "fov.h"
0020 #include "imageexporter.h"
0021 #include "kstars.h"
0022 #include "observinglist.h"
0023 #include "skymap.h"
0024 #include "dialogs/detaildialog.h"
0025 #include "oal/execute.h"
0026 #endif
0027 
0028 #ifndef KSTARS_LITE
0029 #include <KMessageBox>
0030 #endif
0031 
0032 #include <QSqlQuery>
0033 #include <QSqlRecord>
0034 #include <QtConcurrent>
0035 
0036 #include "kstars_debug.h"
0037 
0038 // Qt version calming
0039 #include <qtskipemptyparts.h>
0040 
0041 namespace
0042 {
0043 // Report fatal error during data loading to user
0044 // Calls QApplication::exit
0045 void fatalErrorMessage(QString fname)
0046 {
0047     qCCritical(KSTARS) << i18n("Critical File not Found: %1", fname);
0048     KSNotification::sorry(i18n("The file  %1 could not be found. "
0049                                "KStars cannot run properly without this file. "
0050                                "KStars searches for this file in following locations:\n\n\t"
0051                                "%2\n\n"
0052                                "It appears that your setup is broken.",
0053                                fname, QStandardPaths::standardLocations(QStandardPaths::DataLocation).join("\n\t")),
0054                           i18n("Critical File Not Found: %1", fname)); // FIXME: Must list locations depending on file type
0055 
0056     qApp->exit(1);
0057 }
0058 
0059 // Report non-fatal error during data loading to user and ask
0060 // whether he wants to continue.
0061 //
0062 // No remaining calls so commented out to suppress unused warning
0063 //
0064 // Calls QApplication::exit if he don't
0065 //bool nonFatalErrorMessage(QString fname)
0066 //{
0067 //    qCWarning(KSTARS) << i18n( "Non-Critical File Not Found: %1", fname );
0068 //#ifdef KSTARS_LITE
0069 //    Q_UNUSED(fname);
0070 //    return true;
0071 //#else
0072 //    int res = KMessageBox::warningContinueCancel(nullptr,
0073 //              i18n("The file %1 could not be found. "
0074 //                   "KStars can still run without this file. "
0075 //                   "KStars search for this file in following locations:\n\n\t"
0076 //                   "%2\n\n"
0077 //                   "It appears that you setup is broken. Press Continue to run KStars without this file ",
0078 //                   fname, QStandardPaths::standardLocations( QStandardPaths::DataLocation ).join("\n\t") ),
0079 //              i18n( "Non-Critical File Not Found: %1", fname ));  // FIXME: Must list locations depending on file type
0080 //    if( res != KMessageBox::Continue )
0081 //        qApp->exit(1);
0082 //    return res == KMessageBox::Continue;
0083 //#endif
0084 //}
0085 }
0086 
0087 KStarsData *KStarsData::pinstance = nullptr;
0088 
0089 KStarsData *KStarsData::Create()
0090 {
0091     // This method should never be called twice within a run, since a
0092     // lot of the code assumes that KStarsData, once created, is never
0093     // destroyed. They maintain local copies of KStarsData::Instance()
0094     // for efficiency (maybe this should change, but it is not
0095     // required to delete and reinstantiate KStarsData). Thus, when we
0096     // call this method, pinstance MUST be zero, i.e. this must be the
0097     // first (and last) time we are calling it. -- asimha
0098     Q_ASSERT(!pinstance);
0099 
0100     delete pinstance;
0101     pinstance = new KStarsData();
0102     return pinstance;
0103 }
0104 
0105 KStarsData::KStarsData()
0106     : m_Geo(dms(0), dms(0)), m_ksuserdb(),
0107       temporaryTrail(false),
0108       //locale( new KLocale( "kstars" ) ),
0109       m_preUpdateID(0), m_updateID(0), m_preUpdateNumID(0), m_updateNumID(0), m_preUpdateNum(J2000), m_updateNum(J2000)
0110 {
0111 #ifndef KSTARS_LITE
0112     m_LogObject.reset(new OAL::Log);
0113 #endif
0114     // at startup times run forward
0115     setTimeDirection(0.0);
0116 }
0117 
0118 KStarsData::~KStarsData()
0119 {
0120     Q_ASSERT(pinstance);
0121 
0122     //delete locale;
0123     qDeleteAll(geoList);
0124     geoList.clear();
0125     qDeleteAll(ADVtreeList);
0126     ADVtreeList.clear();
0127 
0128     pinstance = nullptr;
0129 }
0130 
0131 bool KStarsData::initialize()
0132 {
0133     //Load Time Zone Rules//
0134     emit progressText(i18n("Reading time zone rules"));
0135     if (!readTimeZoneRulebook())
0136     {
0137         fatalErrorMessage("TZrules.dat");
0138         return false;
0139     }
0140 
0141     emit progressText(
0142         i18n("Upgrade existing user city db to support geographic elevation."));
0143 
0144     QString dbfile = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("mycitydb.sqlite");
0145 
0146     /// This code to add Height column to table city in mycitydb.sqlite is a transitional measure to support a meaningful
0147     /// geographic elevation.
0148     if (QFile::exists(dbfile))
0149     {
0150         QSqlDatabase fixcitydb = QSqlDatabase::addDatabase("QSQLITE", "fixcitydb");
0151 
0152         fixcitydb.setDatabaseName(dbfile);
0153         fixcitydb.open();
0154 
0155         if (fixcitydb.tables().contains("city", Qt::CaseInsensitive))
0156         {
0157             QSqlRecord r = fixcitydb.record("city");
0158             if (!r.contains("Elevation"))
0159             {
0160                 emit progressText(i18n("Adding \"Elevation\" column to city table."));
0161 
0162                 QSqlQuery query(fixcitydb);
0163                 if (query.exec(
0164                         "alter table city add column Elevation real default -10;") ==
0165                     false)
0166                 {
0167                     emit progressText(QString("failed to add Elevation column to city "
0168                                               "table in mycitydb.sqlite: &1")
0169                                           .arg(query.lastError().text()));
0170                 }
0171             }
0172             else
0173             {
0174                 emit progressText(i18n("City table already contains \"Elevation\"."));
0175             }
0176         }
0177         else
0178         {
0179             emit progressText(i18n("City table missing from database."));
0180         }
0181         fixcitydb.close();
0182     }
0183 
0184     //Load Cities//
0185     emit progressText(i18n("Loading city data"));
0186     if (!readCityData())
0187     {
0188         fatalErrorMessage("citydb.sqlite");
0189         return false;
0190     }
0191 
0192     //Initialize User Database//
0193     emit progressText(i18n("Loading User Information"));
0194     m_ksuserdb.Initialize();
0195 
0196     //Initialize SkyMapComposite//
0197     emit progressText(i18n("Loading sky objects"));
0198     m_SkyComposite.reset(new SkyMapComposite());
0199     //Load Image URLs//
0200     //#ifndef Q_OS_ANDROID
0201     //On Android these 2 calls produce segfault. WARNING
0202     emit progressText(i18n("Loading Image URLs"));
0203 
0204     // if (!readURLData("image_url.dat", SkyObjectUserdata::Type::image) &&
0205     //     !nonFatalErrorMessage("image_url.dat"))
0206     //     return false;
0207     QtConcurrent::run(this, &KStarsData::readURLData, QString("image_url.dat"),
0208                       SkyObjectUserdata::Type::image);
0209 
0210     //Load Information URLs//
0211     //emit progressText(i18n("Loading Information URLs"));
0212     // if (!readURLData("info_url.dat", SkyObjectUserdata::Type::website) &&
0213     //     !nonFatalErrorMessage("info_url.dat"))
0214     //     return false;
0215     QtConcurrent::run(this, &KStarsData::readURLData, QString("info_url.dat"),
0216                       SkyObjectUserdata::Type::website);
0217 
0218     //#endif
0219     //emit progressText( i18n("Loading Variable Stars" ) );
0220 
0221 #ifndef KSTARS_LITE
0222     //Initialize Observing List
0223     m_ObservingList = new ObservingList();
0224 #endif
0225 
0226     readUserLog();
0227 
0228 #ifndef KSTARS_LITE
0229     readADVTreeData();
0230 #endif
0231     return true;
0232 }
0233 
0234 void KStarsData::updateTime(GeoLocation *geo, const bool automaticDSTchange)
0235 {
0236     // sync LTime with the simulation clock
0237     LTime = geo->UTtoLT(ut());
0238     syncLST();
0239 
0240     //Only check DST if (1) TZrule is not the empty rule, and (2) if we have crossed
0241     //the DST change date/time.
0242     if (!geo->tzrule()->isEmptyRule())
0243     {
0244         if (TimeRunsForward)
0245         {
0246             // timedirection is forward
0247             // DST change happens if current date is bigger than next calculated dst change
0248             if (ut() > NextDSTChange)
0249                 resetToNewDST(geo, automaticDSTchange);
0250         }
0251         else
0252         {
0253             // timedirection is backward
0254             // DST change happens if current date is smaller than next calculated dst change
0255             if (ut() < NextDSTChange)
0256                 resetToNewDST(geo, automaticDSTchange);
0257         }
0258     }
0259 
0260     KSNumbers num(ut().djd());
0261 
0262     if (std::abs(ut().djd() - LastNumUpdate.djd()) > 1.0)
0263     {
0264         LastNumUpdate = KStarsDateTime(ut().djd());
0265         m_preUpdateNumID++;
0266         m_preUpdateNum = KSNumbers(num);
0267         skyComposite()->update(&num);
0268     }
0269 
0270     if (std::abs(ut().djd() - LastPlanetUpdate.djd()) > 0.01)
0271     {
0272         LastPlanetUpdate = KStarsDateTime(ut().djd());
0273         skyComposite()->updateSolarSystemBodies(&num);
0274     }
0275 
0276     // Moon moves ~30 arcmin/hr, so update its position every minute.
0277     if (std::abs(ut().djd() - LastMoonUpdate.djd()) > 0.00069444)
0278     {
0279         LastMoonUpdate = ut();
0280         skyComposite()->updateMoons(&num);
0281     }
0282 
0283     //Update Alt/Az coordinates.  Timescale varies with zoom level
0284     //If Clock is in Manual Mode, always update. (?)
0285     if (std::abs(ut().djd() - LastSkyUpdate.djd()) > 0.1 / Options::zoomFactor() || clock()->isManualMode())
0286     {
0287         LastSkyUpdate = ut();
0288         m_preUpdateID++;
0289         //omit KSNumbers arg == just update Alt/Az coords // <-- Eh? -- asimha. Looks like this behavior / ideology has changed drastically.
0290         skyComposite()->update(&num);
0291 
0292         emit skyUpdate(clock()->isManualMode());
0293     }
0294 }
0295 
0296 void KStarsData::syncUpdateIDs()
0297 {
0298     m_updateID = m_preUpdateID;
0299     if (m_updateNumID == m_preUpdateNumID)
0300         return;
0301     m_updateNumID = m_preUpdateNumID;
0302     m_updateNum   = KSNumbers(m_preUpdateNum);
0303 }
0304 
0305 unsigned int KStarsData::incUpdateID()
0306 {
0307     m_preUpdateID++;
0308     m_preUpdateNumID++;
0309     syncUpdateIDs();
0310     return m_updateID;
0311 }
0312 
0313 void KStarsData::setFullTimeUpdate()
0314 {
0315     //Set the update markers to invalid dates to trigger updates in each category
0316     LastSkyUpdate    = KStarsDateTime(QDateTime());
0317     LastPlanetUpdate = KStarsDateTime(QDateTime());
0318     LastMoonUpdate   = KStarsDateTime(QDateTime());
0319     LastNumUpdate    = KStarsDateTime(QDateTime());
0320 }
0321 
0322 void KStarsData::syncLST()
0323 {
0324     LST = geo()->GSTtoLST(ut().gst());
0325 }
0326 
0327 void KStarsData::changeDateTime(const KStarsDateTime &newDate)
0328 {
0329     //Turn off animated slews for the next time step.
0330     setSnapNextFocus();
0331 
0332     clock()->setUTC(newDate);
0333 
0334     LTime = geo()->UTtoLT(ut());
0335     //set local sideral time
0336     syncLST();
0337 
0338     //Make sure Numbers, Moon, planets, and sky objects are updated immediately
0339     setFullTimeUpdate();
0340 
0341     // reset tzrules data with new local time and time direction (forward or backward)
0342     geo()->tzrule()->reset_with_ltime(LTime, geo()->TZ0(), isTimeRunningForward());
0343 
0344     // reset next dst change time
0345     setNextDSTChange(geo()->tzrule()->nextDSTChange());
0346 }
0347 
0348 void KStarsData::resetToNewDST(GeoLocation *geo, const bool automaticDSTchange)
0349 {
0350     // reset tzrules data with local time, timezone offset and time direction (forward or backward)
0351     // force a DST change with option true for 3. parameter
0352     geo->tzrule()->reset_with_ltime(LTime, geo->TZ0(), TimeRunsForward, automaticDSTchange);
0353     // reset next DST change time
0354     setNextDSTChange(geo->tzrule()->nextDSTChange());
0355     //reset LTime, because TZoffset has changed
0356     LTime = geo->UTtoLT(ut());
0357 }
0358 
0359 void KStarsData::setTimeDirection(float scale)
0360 {
0361     TimeRunsForward = scale >= 0;
0362 }
0363 
0364 GeoLocation *KStarsData::locationNamed(const QString &city, const QString &province, const QString &country)
0365 {
0366     foreach (GeoLocation *loc, geoList)
0367     {
0368         if (loc->translatedName() == city && (province.isEmpty() || loc->translatedProvince() == province) &&
0369                 (country.isEmpty() || loc->translatedCountry() == country))
0370         {
0371             return loc;
0372         }
0373     }
0374     return nullptr;
0375 }
0376 
0377 GeoLocation *KStarsData::nearestLocation(double longitude, double latitude)
0378 {
0379     GeoLocation *nearest = nullptr;
0380     double distance = 1e6;
0381 
0382     dms lng(longitude), lat(latitude);
0383     for (auto oneCity : geoList)
0384     {
0385         double newDistance = oneCity->distanceTo(lng, lat);
0386         if (newDistance < distance)
0387         {
0388             distance = newDistance;
0389             nearest = oneCity;
0390         }
0391     }
0392 
0393     return nearest;
0394 }
0395 
0396 void KStarsData::setLocationFromOptions()
0397 {
0398     setLocation(GeoLocation(dms(Options::longitude()), dms(Options::latitude()), Options::cityName(),
0399                             Options::provinceName(), Options::countryName(), Options::timeZone(),
0400                             &(Rulebook[Options::dST()]), Options::elevation(), false, 4));
0401 }
0402 
0403 void KStarsData::setLocation(const GeoLocation &l)
0404 {
0405     m_Geo = GeoLocation(l);
0406     if (m_Geo.lat()->Degrees() >= 90.0)
0407         m_Geo.setLat(dms(89.99));
0408     if (m_Geo.lat()->Degrees() <= -90.0)
0409         m_Geo.setLat(dms(-89.99));
0410 
0411     //store data in the Options objects
0412     Options::setCityName(m_Geo.name());
0413     Options::setProvinceName(m_Geo.province());
0414     Options::setCountryName(m_Geo.country());
0415     Options::setTimeZone(m_Geo.TZ0());
0416     Options::setElevation(m_Geo.elevation());
0417     Options::setLongitude(m_Geo.lng()->Degrees());
0418     Options::setLatitude(m_Geo.lat()->Degrees());
0419     // set the rule from rulebook
0420     foreach (const QString &key, Rulebook.keys())
0421     {
0422         if (!key.isEmpty() && m_Geo.tzrule()->equals(&Rulebook[key]))
0423             Options::setDST(key);
0424     }
0425 
0426     emit geoChanged();
0427 }
0428 
0429 SkyObject *KStarsData::objectNamed(const QString &name)
0430 {
0431     if ((name == "star") || (name == "nothing") || name.isEmpty())
0432         return nullptr;
0433     return skyComposite()->findByName(name, true); // objectNamed has to do an exact match
0434 }
0435 
0436 bool KStarsData::readCityData()
0437 {
0438     QSqlDatabase citydb = QSqlDatabase::addDatabase("QSQLITE", "citydb");
0439     QString dbfile      = KSPaths::locate(QStandardPaths::AppLocalDataLocation, "citydb.sqlite");
0440     citydb.setDatabaseName(dbfile);
0441     if (citydb.open() == false)
0442     {
0443         qCCritical(KSTARS) << "Unable to open city database file " << dbfile << citydb.lastError().text();
0444         return false;
0445     }
0446 
0447     QSqlQuery get_query(citydb);
0448 
0449     //get_query.prepare("SELECT * FROM city");
0450     if (!get_query.exec("SELECT * FROM city"))
0451     {
0452         qCCritical(KSTARS) << get_query.lastError();
0453         return false;
0454     }
0455 
0456     bool citiesFound = false;
0457     // get_query.size() always returns -1 so we set citiesFound if at least one city is found
0458     while (get_query.next())
0459     {
0460         citiesFound          = true;
0461         QString name         = get_query.value(1).toString();
0462         QString province     = get_query.value(2).toString();
0463         QString country      = get_query.value(3).toString();
0464         dms lat              = dms(get_query.value(4).toString());
0465         dms lng              = dms(get_query.value(5).toString());
0466         double TZ            = get_query.value(6).toDouble();
0467         TimeZoneRule *TZrule = &(Rulebook[get_query.value(7).toString()]);
0468         double elevation     = get_query.value(8).toDouble();
0469 
0470         // appends city names to list
0471         geoList.append(new GeoLocation(lng, lat, name, province, country, TZ, TZrule, elevation, true, 4));
0472     }
0473     citydb.close();
0474 
0475     // Reading local database
0476     QSqlDatabase mycitydb = QSqlDatabase::addDatabase("QSQLITE", "mycitydb");
0477     dbfile = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("mycitydb.sqlite");
0478 
0479     if (QFile::exists(dbfile))
0480     {
0481         mycitydb.setDatabaseName(dbfile);
0482         if (mycitydb.open())
0483         {
0484             QSqlQuery get_query(mycitydb);
0485 
0486             if (!get_query.exec("SELECT * FROM city"))
0487             {
0488                 qDebug() << Q_FUNC_INFO << get_query.lastError();
0489                 return false;
0490             }
0491             while (get_query.next())
0492             {
0493                 QString name         = get_query.value(1).toString();
0494                 QString province     = get_query.value(2).toString();
0495                 QString country      = get_query.value(3).toString();
0496                 dms lat              = dms(get_query.value(4).toString());
0497                 dms lng              = dms(get_query.value(5).toString());
0498                 double TZ            = get_query.value(6).toDouble();
0499                 TimeZoneRule *TZrule = &(Rulebook[get_query.value(7).toString()]);
0500                 double elevation     = get_query.value(8).toDouble();
0501 
0502                 // appends city names to list
0503                 geoList.append(new GeoLocation(lng, lat, name, province, country, TZ, TZrule, elevation, false, 4));
0504             }
0505             mycitydb.close();
0506         }
0507     }
0508 
0509     return citiesFound;
0510 }
0511 
0512 bool KStarsData::readTimeZoneRulebook()
0513 {
0514     QFile file;
0515 
0516     if (KSUtils::openDataFile(file, "TZrules.dat"))
0517     {
0518         QTextStream stream(&file);
0519 
0520         while (!stream.atEnd())
0521         {
0522             QString line = stream.readLine().trimmed();
0523             if (line.length() && !line.startsWith('#')) //ignore commented and blank lines
0524             {
0525                 QStringList fields = line.split(' ', Qt::SkipEmptyParts);
0526                 QString id         = fields[0];
0527                 QTime stime        = QTime(fields[3].leftRef(fields[3].indexOf(':')).toInt(),
0528                                            fields[3].midRef(fields[3].indexOf(':') + 1, fields[3].length()).toInt());
0529                 QTime rtime        = QTime(fields[6].leftRef(fields[6].indexOf(':')).toInt(),
0530                                            fields[6].midRef(fields[6].indexOf(':') + 1, fields[6].length()).toInt());
0531 
0532                 Rulebook[id] = TimeZoneRule(fields[1], fields[2], stime, fields[4], fields[5], rtime);
0533             }
0534         }
0535         return true;
0536     }
0537     else
0538     {
0539         return false;
0540     }
0541 }
0542 
0543 bool KStarsData::openUrlFile(const QString &urlfile, QFile &file)
0544 {
0545     //QFile file;
0546     QString localFile;
0547     bool fileFound = false;
0548     QFile localeFile;
0549 
0550     //if ( locale->language() != "en_US" )
0551     if (QLocale().language() != QLocale::English)
0552         //localFile = locale->language() + '/' + urlfile;
0553         localFile = QLocale().languageToString(QLocale().language()) + '/' + urlfile;
0554 
0555     if (!localFile.isEmpty() && KSUtils::openDataFile(file, localFile))
0556     {
0557         fileFound = true;
0558     }
0559     else
0560     {
0561         // Try to load locale file, if not successful, load regular urlfile and then copy it to locale.
0562         file.setFileName(QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath(urlfile));
0563         if (file.open(QIODevice::ReadOnly))
0564         {
0565             //local file found.  Now, if global file has newer timestamp, then merge the two files.
0566             //First load local file into QStringList
0567             bool newDataFound(false);
0568             QStringList urlData;
0569             QTextStream lStream(&file);
0570             while (!lStream.atEnd())
0571                 urlData.append(lStream.readLine());
0572 
0573             //Find global file(s) in findAllResources() list.
0574             QFileInfo fi_local(file.fileName());
0575 
0576             QStringList flist = KSPaths::locateAll(QStandardPaths::DataLocation, urlfile);
0577 
0578             for (int i = 0; i < flist.size(); i++)
0579             {
0580                 if (flist[i] != file.fileName())
0581                 {
0582                     QFileInfo fi_global(flist[i]);
0583 
0584                     //Is this global file newer than the local file?
0585                     if (fi_global.lastModified() > fi_local.lastModified())
0586                     {
0587                         //Global file has newer timestamp than local.  Add lines in global file that don't already exist in local file.
0588                         //be smart about this; in some cases the URL is updated but the image is probably the same if its
0589                         //label string is the same.  So only check strings up to last ":"
0590                         QFile globalFile(flist[i]);
0591                         if (globalFile.open(QIODevice::ReadOnly))
0592                         {
0593                             QTextStream gStream(&globalFile);
0594                             while (!gStream.atEnd())
0595                             {
0596                                 QString line = gStream.readLine();
0597 
0598                                 //If global-file line begins with "XXX:" then this line should be removed from the local file.
0599                                 if (line.startsWith(QLatin1String("XXX:")) && urlData.contains(line.mid(4)))
0600                                 {
0601                                     urlData.removeAt(urlData.indexOf(line.mid(4)));
0602                                 }
0603                                 else
0604                                 {
0605                                     //does local file contain the current global file line, up to second ':' ?
0606 
0607                                     bool linefound(false);
0608                                     for (int j = 0; j < urlData.size(); ++j)
0609                                     {
0610                                         if (urlData[j].contains(line.left(line.indexOf(':', line.indexOf(':') + 1))))
0611                                         {
0612                                             //replace line in urlData with its equivalent in the newer global file.
0613                                             urlData.replace(j, line);
0614                                             if (!newDataFound)
0615                                                 newDataFound = true;
0616                                             linefound = true;
0617                                             break;
0618                                         }
0619                                     }
0620                                     if (!linefound)
0621                                     {
0622                                         urlData.append(line);
0623                                         if (!newDataFound)
0624                                             newDataFound = true;
0625                                     }
0626                                 }
0627                             }
0628                         }
0629                     }
0630                 }
0631             }
0632 
0633             file.close();
0634 
0635             //(possibly) write appended local file
0636             if (newDataFound)
0637             {
0638                 if (file.open(QIODevice::WriteOnly))
0639                 {
0640                     QTextStream outStream(&file);
0641                     for (int i = 0; i < urlData.size(); i++)
0642                     {
0643                         outStream << urlData[i] << '\n';
0644                     }
0645                     file.close();
0646                 }
0647             }
0648 
0649             if (file.open(QIODevice::ReadOnly))
0650                 fileFound = true;
0651         }
0652         else
0653         {
0654             if (KSUtils::openDataFile(file, urlfile))
0655             {
0656                 if (QLocale().language() != QLocale::English)
0657                     qDebug() << Q_FUNC_INFO << "No localized URL file; using default English file.";
0658                 // we found urlfile, we need to copy it to locale
0659                 localeFile.setFileName(QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath(urlfile));
0660                 if (localeFile.open(QIODevice::WriteOnly))
0661                 {
0662                     QTextStream readStream(&file);
0663                     QTextStream writeStream(&localeFile);
0664                     while (!readStream.atEnd())
0665                     {
0666                         QString line = readStream.readLine();
0667                         if (!line.startsWith(QLatin1String("XXX:"))) //do not write "deleted" lines
0668                             writeStream << line << '\n';
0669                     }
0670 
0671                     localeFile.close();
0672                     file.reset();
0673                 }
0674                 else
0675                 {
0676                     qDebug() << Q_FUNC_INFO << "Failed to copy default URL file to locale folder, modifying default object links is "
0677                              "not possible";
0678                 }
0679                 fileFound = true;
0680             }
0681         }
0682     }
0683     return fileFound;
0684 }
0685 
0686 bool KStarsData::readURLData(const QString &urlfile, SkyObjectUserdata::Type type)
0687 {
0688 #ifndef KSTARS_LITE
0689     if (KStars::Closing)
0690         return true;
0691 #endif
0692 
0693     QFile file;
0694     if (!openUrlFile(urlfile, file))
0695         return false;
0696 
0697     QTextStream stream(&file);
0698     QMutexLocker _{ &m_user_data_mutex };
0699 
0700     while (!stream.atEnd())
0701     {
0702         QString line = stream.readLine();
0703 
0704         //ignore comment lines
0705         if (!line.startsWith('#'))
0706         {
0707 #ifndef KSTARS_LITE
0708             if (KStars::Closing)
0709             {
0710                 file.close();
0711                 return true;
0712             }
0713 #endif
0714 
0715             int idx      = line.indexOf(':');
0716             QString name = line.left(idx);
0717             if (name == "XXX")
0718                 continue;
0719             QString sub   = line.mid(idx + 1);
0720             idx           = sub.indexOf(':');
0721             QString title = sub.left(idx);
0722             QString url   = sub.mid(idx + 1);
0723             // Dirty hack to fix things up for planets
0724 
0725             //            if (name == "Mercury" || name == "Venus" || name == "Mars" || name == "Jupiter" || name == "Saturn" ||
0726             //                    name == "Uranus" || name == "Neptune" /* || name == "Pluto" */)
0727             //                o = skyComposite()->findByName(i18n(name.toLocal8Bit().data()));
0728             //            else
0729 
0730             auto &data_element = m_user_data[name];
0731             data_element.addLink(title, QUrl{ url }, type);
0732         }
0733     }
0734     file.close();
0735     return true;
0736 }
0737 
0738 // FIXME: Improve the user log system
0739 
0740 // Note: It might be very nice to keep the log in plaintext files, for
0741 // portability, human-readability, and greppability. However, it takes
0742 // a lot of time to parse and look up, is very messy from the
0743 // reliability and programming point of view, needs to be parsed at
0744 // start, can become corrupt easily because of a missing bracket...
0745 
0746 // An SQLite database is a good compromise. A user can easily view it
0747 // using an SQLite browser. There is no need to read at start-up, one
0748 // can read the log when required. Easy to edit logs / update logs
0749 // etc. Will not become corrupt. Needn't be parsed.
0750 
0751 // However, IMHO, it is best to put these kinds of things in separate
0752 // databases, instead of unifying them as a table under the user
0753 // database. This ensures portability and a certain robustness that if
0754 // a user opens it, they cannot incorrectly edit a part of the DB they
0755 // did not intend to edit.
0756 
0757 // --asimha 2016 Aug 17
0758 
0759 // FIXME: This is a significant contributor to KStars startup time.
0760 bool KStarsData::readUserLog()
0761 {
0762     QFile file;
0763     QString fullContents;
0764 
0765     if (!KSUtils::openDataFile(file, "userlog.dat"))
0766         return false;
0767 
0768     QTextStream stream(&file);
0769 
0770     if (!stream.atEnd())
0771         fullContents = stream.readAll();
0772 
0773     QMutexLocker _{ &m_user_data_mutex };
0774 
0775     QStringRef buffer(&fullContents);
0776     const QLatin1String logStart("[KSLABEL:"), logEnd("[KSLogEnd]");
0777     std::size_t currentEntryIndex = 0;
0778     while (!buffer.isEmpty())
0779     {
0780         int startIndex, endIndex;
0781         QStringRef sub, name, data;
0782 
0783         startIndex = buffer.indexOf(logStart) + logStart.size();
0784         if (startIndex < 0)
0785             break;
0786         currentEntryIndex += startIndex;
0787         endIndex   = buffer.indexOf(logEnd, startIndex);
0788 
0789         auto malformatError = [&]()
0790         {
0791             int res = KMessageBox::warningContinueCancel(
0792                           nullptr,
0793                           i18n("The user notes log file %1 is malformatted in the opening of the entry starting at %2. "
0794                                "KStars can still run without fully reading this file. "
0795                                "Press Continue to run KStars with whatever partial reading was successful. "
0796                                "The file may get truncated if KStars writes to the file later. Press Cancel to instead abort now and manually fix the problem. ",
0797                                file.fileName(), QString::number(currentEntryIndex)),
0798                           i18n( "Malformed file %1", file.fileName() )
0799                       );
0800             if( res != KMessageBox::Continue )
0801                 qApp->exit(1); // FIXME: Why does this not work?
0802         };
0803 
0804         if (endIndex < 0)
0805         {
0806             malformatError();
0807             break;
0808         }
0809 
0810         // Read name after KSLABEL identifier
0811         // Because some object names have [] within them, we have to be careful
0812         // [...] names are used by SIMBAD and NED to specify paper authors
0813         // Unbalanced [,] are not allowed in the object name, but are allowed in the notes
0814         int nameEndIndex = startIndex, openBracketCount = 1;
0815         while (openBracketCount > 0 && nameEndIndex < endIndex)
0816         {
0817             if (buffer[nameEndIndex] == ']')
0818                 --openBracketCount;
0819             else if (buffer[nameEndIndex] == '[')
0820                 ++openBracketCount;
0821             ++nameEndIndex;
0822         }
0823         if (openBracketCount > 0)
0824         {
0825             malformatError();
0826             break;
0827         }
0828         name = buffer.mid(startIndex, (nameEndIndex - 1) - startIndex);
0829 
0830         // Read data and skip new line
0831         if (buffer[nameEndIndex] == '\n')
0832             ++nameEndIndex;
0833         data   = buffer.mid(nameEndIndex, endIndex - nameEndIndex);
0834         buffer = buffer.mid(endIndex + logEnd.size() + 1);
0835         currentEntryIndex += (endIndex + logEnd.size() + 1 - startIndex);
0836 
0837         auto &data_element   = m_user_data[name.toString()];
0838         data_element.userLog = data.toString();
0839 
0840     } // end while
0841     file.close();
0842     return true;
0843 }
0844 
0845 bool KStarsData::readADVTreeData()
0846 {
0847     QFile file;
0848     QString Interface;
0849     QString Name, Link, subName;
0850 
0851     if (!KSUtils::openDataFile(file, "advinterface.dat"))
0852         return false;
0853 
0854     QTextStream stream(&file);
0855     QString Line;
0856 
0857     while (!stream.atEnd())
0858     {
0859         int Type, interfaceIndex;
0860 
0861         Line = stream.readLine();
0862 
0863         if (Line.startsWith(QLatin1String("[KSLABEL]")))
0864         {
0865             Name = Line.mid(9);
0866             Type = 0;
0867         }
0868         else if (Line.startsWith(QLatin1String("[END]")))
0869             Type = 1;
0870         else if (Line.startsWith(QLatin1String("[KSINTERFACE]")))
0871         {
0872             Interface = Line.mid(13);
0873             continue;
0874         }
0875 
0876         else
0877         {
0878             int idx = Line.indexOf(':');
0879             Name    = Line.left(idx);
0880             Link    = Line.mid(idx + 1);
0881 
0882             // Link is empty, using Interface instead
0883             if (Link.isEmpty())
0884             {
0885                 Link           = Interface;
0886                 subName        = Name;
0887                 interfaceIndex = Link.indexOf(QLatin1String("KSINTERFACE"));
0888                 Link.remove(interfaceIndex, 11);
0889                 Link = Link.insert(interfaceIndex, subName.replace(' ', '+'));
0890             }
0891 
0892             Type = 2;
0893         }
0894 
0895         ADVTreeData *ADVData = new ADVTreeData;
0896 
0897         ADVData->Name = Name;
0898         ADVData->Link = Link;
0899         ADVData->Type = Type;
0900 
0901         ADVtreeList.append(ADVData);
0902     }
0903 
0904     return true;
0905 }
0906 
0907 //There's a lot of code duplication here, but it's not avoidable because
0908 //this function is only called from main.cpp when the user is using
0909 //"dump" mode to produce an image from the command line.  In this mode,
0910 //there is no KStars object, so none of the DBus functions can be called
0911 //directly.
0912 bool KStarsData::executeScript(const QString &scriptname, SkyMap *map)
0913 {
0914 #ifndef KSTARS_LITE
0915     int cmdCount(0);
0916 
0917     QFile f(scriptname);
0918     if (!f.open(QIODevice::ReadOnly))
0919     {
0920         qDebug() << Q_FUNC_INFO << "Could not open file " << f.fileName();
0921         return false;
0922     }
0923 
0924     QTextStream istream(&f);
0925     while (!istream.atEnd())
0926     {
0927         QString line = istream.readLine();
0928         line.remove("string:");
0929         line.remove("int32:");
0930         line.remove("double:");
0931         line.remove("bool:");
0932 
0933         //find a dbus line and extract the function name and its arguments
0934         //The function name starts after the last occurrence of "org.kde.kstars."
0935         //or perhaps "org.kde.kstars.SimClock.".
0936         if (line.startsWith(QString("dbus-send")))
0937         {
0938             QString funcprefix = "org.kde.kstars.SimClock.";
0939             int i              = line.lastIndexOf(funcprefix);
0940             if (i >= 0)
0941             {
0942                 i += funcprefix.length();
0943             }
0944             else
0945             {
0946                 funcprefix = "org.kde.kstars.";
0947                 i          = line.lastIndexOf(funcprefix);
0948                 if (i >= 0)
0949                 {
0950                     i += funcprefix.length();
0951                 }
0952             }
0953             if (i < 0)
0954             {
0955                 qWarning() << "Could not parse line: " << line;
0956                 return false;
0957             }
0958 
0959             QStringList fn = line.mid(i).split(' ');
0960 
0961             //DEBUG
0962             //qDebug() << Q_FUNC_INFO << fn;
0963 
0964             if (fn[0] == "lookTowards" && fn.size() >= 2)
0965             {
0966                 double az(-1.0);
0967                 QString arg = fn[1].toLower();
0968                 if (arg == "n" || arg == "north")
0969                     az = 0.0;
0970                 if (arg == "ne" || arg == "northeast")
0971                     az = 45.0;
0972                 if (arg == "e" || arg == "east")
0973                     az = 90.0;
0974                 if (arg == "se" || arg == "southeast")
0975                     az = 135.0;
0976                 if (arg == "s" || arg == "south")
0977                     az = 180.0;
0978                 if (arg == "sw" || arg == "southwest")
0979                     az = 225.0;
0980                 if (arg == "w" || arg == "west")
0981                     az = 270.0;
0982                 if (arg == "nw" || arg == "northwest")
0983                     az = 335.0;
0984                 if (az >= 0.0)
0985                 {
0986                     // N.B. unrefract() doesn't matter at 90 degrees
0987                     map->setFocusAltAz(dms(90.0), map->focus()->az());
0988                     map->focus()->HorizontalToEquatorial(&LST, geo()->lat());
0989                     map->setDestination(*map->focus());
0990                     cmdCount++;
0991                 }
0992 
0993                 if (arg == "z" || arg == "zenith")
0994                 {
0995                     // N.B. unrefract() doesn't matter at 90 degrees
0996                     map->setFocusAltAz(dms(90.0), map->focus()->az());
0997                     map->focus()->HorizontalToEquatorial(&LST, geo()->lat());
0998                     map->setDestination(*map->focus());
0999                     cmdCount++;
1000                 }
1001 
1002                 //try a named object.  The name is everything after fn[0],
1003                 //concatenated with spaces.
1004                 fn.removeAll(fn.first());
1005                 QString objname   = fn.join(" ");
1006                 SkyObject *target = objectNamed(objname);
1007                 if (target)
1008                 {
1009                     map->setFocus(target);
1010                     map->focus()->EquatorialToHorizontal(&LST, geo()->lat());
1011                     map->setDestination(*map->focus());
1012                     cmdCount++;
1013                 }
1014             }
1015             else if (fn[0] == "setRaDec" && fn.size() == 3)
1016             {
1017                 bool ok(false);
1018                 dms r(0.0), d(0.0);
1019 
1020                 ok = r.setFromString(fn[1], false); //assume angle in hours
1021                 if (ok)
1022                     ok = d.setFromString(fn[2], true); //assume angle in degrees
1023                 if (ok)
1024                 {
1025                     map->setFocus(r, d);
1026                     map->focus()->EquatorialToHorizontal(&LST, geo()->lat());
1027                     cmdCount++;
1028                 }
1029             }
1030             else if (fn[0] == "setAltAz" && fn.size() == 3)
1031             {
1032                 bool ok(false);
1033                 dms az(0.0), alt(0.0);
1034 
1035                 ok = alt.setFromString(fn[1]);
1036                 if (ok)
1037                     ok = az.setFromString(fn[2]);
1038                 if (ok)
1039                 {
1040                     map->setFocusAltAz(alt, az);
1041                     map->focus()->HorizontalToEquatorial(&LST, geo()->lat());
1042                     cmdCount++;
1043                 }
1044             }
1045             else if (fn[0] == "loadColorScheme")
1046             {
1047                 fn.removeAll(fn.first());
1048                 QString csName = fn.join(" ").remove('\"');
1049                 qCDebug(KSTARS) << "Loading Color scheme: " << csName;
1050 
1051                 QString filename = csName.toLower().trimmed();
1052                 bool ok(false);
1053 
1054                 //Parse default names which don't follow the regular file-naming scheme
1055                 if (csName == i18nc("use default color scheme", "Default Colors"))
1056                     filename = "classic.colors";
1057                 if (csName == i18nc("use 'star chart' color scheme", "Star Chart"))
1058                     filename = "chart.colors";
1059                 if (csName == i18nc("use 'night vision' color scheme", "Night Vision"))
1060                     filename = "night.colors";
1061 
1062                 //Try the filename if it ends with ".colors"
1063                 if (filename.endsWith(QLatin1String(".colors")))
1064                     ok = colorScheme()->load(filename);
1065 
1066                 //If that didn't work, try assuming that 'name' is the color scheme name
1067                 //convert it to a filename exactly as ColorScheme::save() does
1068                 if (!ok)
1069                 {
1070                     if (!filename.isEmpty())
1071                     {
1072                         for (int i = 0; i < filename.length(); ++i)
1073                             if (filename.at(i) == ' ')
1074                                 filename.replace(i, 1, "-");
1075 
1076                         filename = filename.append(".colors");
1077                         ok       = colorScheme()->load(filename);
1078                     }
1079 
1080                     if (!ok)
1081                         qDebug() << Q_FUNC_INFO << QString("Unable to load color scheme named %1. Also tried %2.")
1082                                  .arg(csName, filename);
1083                 }
1084             }
1085             else if (fn[0] == "zoom" && fn.size() == 2)
1086             {
1087                 bool ok(false);
1088                 double z = fn[1].toDouble(&ok);
1089                 if (ok)
1090                 {
1091                     if (z > MAXZOOM)
1092                         z = MAXZOOM;
1093                     if (z < MINZOOM)
1094                         z = MINZOOM;
1095                     Options::setZoomFactor(z);
1096                     cmdCount++;
1097                 }
1098             }
1099             else if (fn[0] == "zoomIn")
1100             {
1101                 if (Options::zoomFactor() < MAXZOOM)
1102                 {
1103                     Options::setZoomFactor(Options::zoomFactor() * DZOOM);
1104                     cmdCount++;
1105                 }
1106             }
1107             else if (fn[0] == "zoomOut")
1108             {
1109                 if (Options::zoomFactor() > MINZOOM)
1110                 {
1111                     Options::setZoomFactor(Options::zoomFactor() / DZOOM);
1112                     cmdCount++;
1113                 }
1114             }
1115             else if (fn[0] == "defaultZoom")
1116             {
1117                 Options::setZoomFactor(DEFAULTZOOM);
1118                 cmdCount++;
1119             }
1120             else if (fn[0] == "setLocalTime" && fn.size() == 7)
1121             {
1122                 bool ok(false);
1123                 // min is a macro - use mnt
1124                 int yr(0), mth(0), day(0), hr(0), mnt(0), sec(0);
1125                 yr = fn[1].toInt(&ok);
1126                 if (ok)
1127                     mth = fn[2].toInt(&ok);
1128                 if (ok)
1129                     day = fn[3].toInt(&ok);
1130                 if (ok)
1131                     hr = fn[4].toInt(&ok);
1132                 if (ok)
1133                     mnt = fn[5].toInt(&ok);
1134                 if (ok)
1135                     sec = fn[6].toInt(&ok);
1136                 if (ok)
1137                 {
1138                     changeDateTime(geo()->LTtoUT(KStarsDateTime(QDate(yr, mth, day), QTime(hr, mnt, sec))));
1139                     cmdCount++;
1140                 }
1141                 else
1142                 {
1143                     qWarning() << ki18n("Could not set time: %1 / %2 / %3 ; %4:%5:%6")
1144                                .subs(day)
1145                                .subs(mth)
1146                                .subs(yr)
1147                                .subs(hr)
1148                                .subs(mnt)
1149                                .subs(sec)
1150                                .toString();
1151                 }
1152             }
1153             else if (fn[0] == "changeViewOption" && fn.size() == 3)
1154             {
1155                 bool bOk(false), dOk(false);
1156 
1157                 //parse bool value
1158                 bool bVal(false);
1159                 if (fn[2].toLower() == "true")
1160                 {
1161                     bOk  = true;
1162                     bVal = true;
1163                 }
1164                 if (fn[2].toLower() == "false")
1165                 {
1166                     bOk  = true;
1167                     bVal = false;
1168                 }
1169                 if (fn[2] == "1")
1170                 {
1171                     bOk  = true;
1172                     bVal = true;
1173                 }
1174                 if (fn[2] == "0")
1175                 {
1176                     bOk  = true;
1177                     bVal = false;
1178                 }
1179 
1180                 //parse double value
1181                 double dVal = fn[2].toDouble(&dOk);
1182 
1183                 // FIXME: REGRESSION
1184                 //                if ( fn[1] == "FOVName"                ) { Options::setFOVName(       fn[2] ); cmdCount++; }
1185                 //                if ( fn[1] == "FOVSizeX"         && dOk ) { Options::setFOVSizeX( (float)dVal ); cmdCount++; }
1186                 //                if ( fn[1] == "FOVSizeY"         && dOk ) { Options::setFOVSizeY( (float)dVal ); cmdCount++; }
1187                 //                if ( fn[1] == "FOVShape"        && nOk ) { Options::setFOVShape(       nVal ); cmdCount++; }
1188                 //                if ( fn[1] == "FOVColor"               ) { Options::setFOVColor(      fn[2] ); cmdCount++; }
1189                 if (fn[1] == "ShowStars" && bOk)
1190                 {
1191                     Options::setShowStars(bVal);
1192                     cmdCount++;
1193                 }
1194                 if (fn[1] == "ShowCLines" && bOk)
1195                 {
1196                     Options::setShowCLines(bVal);
1197                     cmdCount++;
1198                 }
1199                 if (fn[1] == "ShowCNames" && bOk)
1200                 {
1201                     Options::setShowCNames(bVal);
1202                     cmdCount++;
1203                 }
1204                 if (fn[1] == "ShowMilkyWay" && bOk)
1205                 {
1206                     Options::setShowMilkyWay(bVal);
1207                     cmdCount++;
1208                 }
1209                 if (fn[1] == "ShowEquatorialGrid" && bOk)
1210                 {
1211                     Options::setShowEquatorialGrid(bVal);
1212                     cmdCount++;
1213                 }
1214                 if (fn[1] == "ShowHorizontalGrid" && bOk)
1215                 {
1216                     Options::setShowHorizontalGrid(bVal);
1217                     cmdCount++;
1218                 }
1219                 if (fn[1] == "ShowEquator" && bOk)
1220                 {
1221                     Options::setShowEquator(bVal);
1222                     cmdCount++;
1223                 }
1224                 if (fn[1] == "ShowEcliptic" && bOk)
1225                 {
1226                     Options::setShowEcliptic(bVal);
1227                     cmdCount++;
1228                 }
1229                 if (fn[1] == "ShowHorizon" && bOk)
1230                 {
1231                     Options::setShowHorizon(bVal);
1232                     cmdCount++;
1233                 }
1234                 if (fn[1] == "ShowGround" && bOk)
1235                 {
1236                     Options::setShowGround(bVal);
1237                     cmdCount++;
1238                 }
1239                 if (fn[1] == "ShowSun" && bOk)
1240                 {
1241                     Options::setShowSun(bVal);
1242                     cmdCount++;
1243                 }
1244                 if (fn[1] == "ShowMoon" && bOk)
1245                 {
1246                     Options::setShowMoon(bVal);
1247                     cmdCount++;
1248                 }
1249                 if (fn[1] == "ShowMercury" && bOk)
1250                 {
1251                     Options::setShowMercury(bVal);
1252                     cmdCount++;
1253                 }
1254                 if (fn[1] == "ShowVenus" && bOk)
1255                 {
1256                     Options::setShowVenus(bVal);
1257                     cmdCount++;
1258                 }
1259                 if (fn[1] == "ShowMars" && bOk)
1260                 {
1261                     Options::setShowMars(bVal);
1262                     cmdCount++;
1263                 }
1264                 if (fn[1] == "ShowJupiter" && bOk)
1265                 {
1266                     Options::setShowJupiter(bVal);
1267                     cmdCount++;
1268                 }
1269                 if (fn[1] == "ShowSaturn" && bOk)
1270                 {
1271                     Options::setShowSaturn(bVal);
1272                     cmdCount++;
1273                 }
1274                 if (fn[1] == "ShowUranus" && bOk)
1275                 {
1276                     Options::setShowUranus(bVal);
1277                     cmdCount++;
1278                 }
1279                 if (fn[1] == "ShowNeptune" && bOk)
1280                 {
1281                     Options::setShowNeptune(bVal);
1282                     cmdCount++;
1283                 }
1284                 //if ( fn[1] == "ShowPluto"       && bOk ) { Options::setShowPluto(    bVal ); cmdCount++; }
1285                 if (fn[1] == "ShowAsteroids" && bOk)
1286                 {
1287                     Options::setShowAsteroids(bVal);
1288                     cmdCount++;
1289                 }
1290                 if (fn[1] == "ShowComets" && bOk)
1291                 {
1292                     Options::setShowComets(bVal);
1293                     cmdCount++;
1294                 }
1295                 if (fn[1] == "ShowSolarSystem" && bOk)
1296                 {
1297                     Options::setShowSolarSystem(bVal);
1298                     cmdCount++;
1299                 }
1300                 if (fn[1] == "ShowDeepSky" && bOk)
1301                 {
1302                     Options::setShowDeepSky(bVal);
1303                     cmdCount++;
1304                 }
1305                 if (fn[1] == "ShowSupernovae" && bOk)
1306                 {
1307                     Options::setShowSupernovae(bVal);
1308                     cmdCount++;
1309                 }
1310                 if (fn[1] == "ShowStarNames" && bOk)
1311                 {
1312                     Options::setShowStarNames(bVal);
1313                     cmdCount++;
1314                 }
1315                 if (fn[1] == "ShowStarMagnitudes" && bOk)
1316                 {
1317                     Options::setShowStarMagnitudes(bVal);
1318                     cmdCount++;
1319                 }
1320                 if (fn[1] == "ShowAsteroidNames" && bOk)
1321                 {
1322                     Options::setShowAsteroidNames(bVal);
1323                     cmdCount++;
1324                 }
1325                 if (fn[1] == "ShowCometNames" && bOk)
1326                 {
1327                     Options::setShowCometNames(bVal);
1328                     cmdCount++;
1329                 }
1330                 if (fn[1] == "ShowPlanetNames" && bOk)
1331                 {
1332                     Options::setShowPlanetNames(bVal);
1333                     cmdCount++;
1334                 }
1335                 if (fn[1] == "ShowPlanetImages" && bOk)
1336                 {
1337                     Options::setShowPlanetImages(bVal);
1338                     cmdCount++;
1339                 }
1340 
1341                 if (fn[1] == "UseAltAz" && bOk)
1342                 {
1343                     Options::setUseAltAz(bVal);
1344                     cmdCount++;
1345                 }
1346                 if (fn[1] == "UseRefraction" && bOk)
1347                 {
1348                     Options::setUseRefraction(bVal);
1349                     cmdCount++;
1350                 }
1351                 if (fn[1] == "UseAutoLabel" && bOk)
1352                 {
1353                     Options::setUseAutoLabel(bVal);
1354                     cmdCount++;
1355                 }
1356                 if (fn[1] == "UseAutoTrail" && bOk)
1357                 {
1358                     Options::setUseAutoTrail(bVal);
1359                     cmdCount++;
1360                 }
1361                 if (fn[1] == "UseAnimatedSlewing" && bOk)
1362                 {
1363                     Options::setUseAnimatedSlewing(bVal);
1364                     cmdCount++;
1365                 }
1366                 if (fn[1] == "FadePlanetTrails" && bOk)
1367                 {
1368                     Options::setFadePlanetTrails(bVal);
1369                     cmdCount++;
1370                 }
1371                 if (fn[1] == "SlewTimeScale" && dOk)
1372                 {
1373                     Options::setSlewTimeScale(dVal);
1374                     cmdCount++;
1375                 }
1376                 if (fn[1] == "ZoomFactor" && dOk)
1377                 {
1378                     Options::setZoomFactor(dVal);
1379                     cmdCount++;
1380                 }
1381                 //                if ( fn[1] == "MagLimitDrawStar"     && dOk ) { Options::setMagLimitDrawStar( dVal ); cmdCount++; }
1382                 if (fn[1] == "StarDensity" && dOk)
1383                 {
1384                     Options::setStarDensity(dVal);
1385                     cmdCount++;
1386                 }
1387                 //                if ( fn[1] == "MagLimitDrawStarZoomOut" && dOk ) { Options::setMagLimitDrawStarZoomOut( dVal ); cmdCount++; }
1388                 if (fn[1] == "MagLimitDrawDeepSky" && dOk)
1389                 {
1390                     Options::setMagLimitDrawDeepSky(dVal);
1391                     cmdCount++;
1392                 }
1393                 if (fn[1] == "MagLimitDrawDeepSkyZoomOut" && dOk)
1394                 {
1395                     Options::setMagLimitDrawDeepSkyZoomOut(dVal);
1396                     cmdCount++;
1397                 }
1398                 if (fn[1] == "StarLabelDensity" && dOk)
1399                 {
1400                     Options::setStarLabelDensity(dVal);
1401                     cmdCount++;
1402                 }
1403                 if (fn[1] == "MagLimitHideStar" && dOk)
1404                 {
1405                     Options::setMagLimitHideStar(dVal);
1406                     cmdCount++;
1407                 }
1408                 if (fn[1] == "MagLimitAsteroid" && dOk)
1409                 {
1410                     Options::setMagLimitAsteroid(dVal);
1411                     cmdCount++;
1412                 }
1413                 if (fn[1] == "AsteroidLabelDensity" && dOk)
1414                 {
1415                     Options::setAsteroidLabelDensity(dVal);
1416                     cmdCount++;
1417                 }
1418                 if (fn[1] == "MaxRadCometName" && dOk)
1419                 {
1420                     Options::setMaxRadCometName(dVal);
1421                     cmdCount++;
1422                 }
1423 
1424                 //these three are a "radio group"
1425                 if (fn[1] == "UseLatinConstellationNames" && bOk)
1426                 {
1427                     Options::setUseLatinConstellNames(true);
1428                     Options::setUseLocalConstellNames(false);
1429                     Options::setUseAbbrevConstellNames(false);
1430                     cmdCount++;
1431                 }
1432                 if (fn[1] == "UseLocalConstellationNames" && bOk)
1433                 {
1434                     Options::setUseLatinConstellNames(false);
1435                     Options::setUseLocalConstellNames(true);
1436                     Options::setUseAbbrevConstellNames(false);
1437                     cmdCount++;
1438                 }
1439                 if (fn[1] == "UseAbbrevConstellationNames" && bOk)
1440                 {
1441                     Options::setUseLatinConstellNames(false);
1442                     Options::setUseLocalConstellNames(false);
1443                     Options::setUseAbbrevConstellNames(true);
1444                     cmdCount++;
1445                 }
1446             }
1447             else if (fn[0] == "setGeoLocation" && (fn.size() == 3 || fn.size() == 4))
1448             {
1449                 QString city(fn[1]), province, country(fn[2]);
1450                 province.clear();
1451                 if (fn.size() == 4)
1452                 {
1453                     province = fn[2];
1454                     country  = fn[3];
1455                 }
1456 
1457                 bool cityFound(false);
1458                 foreach (GeoLocation *loc, geoList)
1459                 {
1460                     if (loc->translatedName() == city &&
1461                             (province.isEmpty() || loc->translatedProvince() == province) &&
1462                             loc->translatedCountry() == country)
1463                     {
1464                         cityFound = true;
1465                         setLocation(*loc);
1466                         cmdCount++;
1467                         break;
1468                     }
1469                 }
1470 
1471                 if (!cityFound)
1472                     qWarning() << i18n("Could not set location named %1, %2, %3", city, province, country);
1473             }
1474         }
1475     } //end while
1476 
1477     if (cmdCount)
1478         return true;
1479 #else
1480     Q_UNUSED(map)
1481     Q_UNUSED(scriptname)
1482 #endif
1483     return false;
1484 }
1485 
1486 #ifndef KSTARS_LITE
1487 void KStarsData::syncFOV()
1488 {
1489     visibleFOVs.clear();
1490     // Add visible FOVs
1491     foreach (FOV *fov, availFOVs)
1492     {
1493         if (Options::fOVNames().contains(fov->name()))
1494             visibleFOVs.append(fov);
1495     }
1496     // Remove unavailable FOVs
1497     #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
1498     QSet<QString> names = QSet<QString>::fromList(Options::fOVNames());
1499     #else
1500     const QStringList m_fOVNames = Options::fOVNames();
1501     QSet<QString> names (m_fOVNames.begin(), m_fOVNames.end());
1502     #endif
1503     QSet<QString> all;
1504     foreach (FOV *fov, visibleFOVs)
1505     {
1506         all.insert(fov->name());
1507     }
1508     Options::setFOVNames(all.intersect(names).values());
1509 }
1510 
1511 // FIXME: Why does KStarsData store the Execute instance??? -- asimha
1512 Execute *KStarsData::executeSession()
1513 {
1514     if (!m_Execute.get())
1515         m_Execute.reset(new Execute());
1516 
1517     return m_Execute.get();
1518 }
1519 
1520 // FIXME: Why does KStarsData store the ImageExporer instance??? KStarsData is supposed to work with no reference to KStars -- asimha
1521 ImageExporter *KStarsData::imageExporter()
1522 {
1523     if (!m_ImageExporter.get())
1524         m_ImageExporter.reset(new ImageExporter(KStars::Instance()));
1525 
1526     return m_ImageExporter.get();
1527 }
1528 #endif
1529 
1530 std::pair<bool, QString>
1531 KStarsData::addToUserData(const QString &name, const SkyObjectUserdata::LinkData &data)
1532 {
1533     QMutexLocker _{ &m_user_data_mutex };
1534 
1535     findUserData(name).links[data.type].push_back(data);
1536 
1537     QString entry;
1538     QFile file;
1539     const auto isImage = data.type == SkyObjectUserdata::Type::image;
1540 
1541     //Also, update the user's custom image links database
1542     //check for user's image-links database.  If it doesn't exist, create it.
1543     file.setFileName(
1544         KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation) +
1545         (isImage ?
1546              "image_url.dat" :
1547              "info_url.dat")); //determine filename in local user KDE directory tree.
1548 
1549     if (!file.open(QIODevice::ReadWrite | QIODevice::Append))
1550         return { false,
1551                  isImage ?
1552                      i18n("Custom image-links file could not be opened.\nLink cannot "
1553                           "be recorded for future sessions.") :
1554                      i18n("Custom information-links file could not be opened.\nLink "
1555                           "cannot be recorded for future sessions.") };
1556     else
1557     {
1558         entry = name + ':' + data.title + ':' + data.url.toString();
1559         QTextStream stream(&file);
1560         stream << entry << '\n';
1561         file.close();
1562     }
1563 
1564     return { true, {} };
1565 }
1566 
1567 std::pair<bool, QString> updateLocalDatabase(SkyObjectUserdata::Type type,
1568                                              const QString &search_line,
1569                                              const QString &replace_line)
1570 {
1571     QString TempFileName, file_line;
1572     QFile URLFile;
1573     QTemporaryFile TempFile;
1574     TempFile.setAutoRemove(false);
1575     TempFile.open();
1576 
1577     bool replace = !replace_line.isEmpty();
1578 
1579     if (search_line.isEmpty())
1580         return { false, "Invalid update request." };
1581 
1582     TempFileName = TempFile.fileName();
1583 
1584     switch (type)
1585     {
1586             // Info Links
1587         case SkyObjectUserdata::Type::website:
1588             // Get name for our local info_url file
1589             URLFile.setFileName(
1590                 KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation) +
1591                 "info_url.dat");
1592             break;
1593 
1594             // Image Links
1595         case SkyObjectUserdata::Type::image:
1596             // Get name for our local info_url file
1597             URLFile.setFileName(
1598                 KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation) +
1599                 "image_url.dat");
1600             break;
1601     }
1602 
1603     // Copy URL file to temp file
1604     KIO::file_copy(QUrl::fromLocalFile(URLFile.fileName()),
1605                    QUrl::fromLocalFile(TempFileName), -1,
1606                    KIO::Overwrite | KIO::HideProgressInfo);
1607 
1608     if (!URLFile.open(QIODevice::WriteOnly))
1609     {
1610         return { false, "Failed to open " + URLFile.fileName() +
1611                             "KStars cannot save to user database" };
1612     }
1613 
1614     // Get streams;
1615     QTextStream temp_stream(&TempFile);
1616     QTextStream out_stream(&URLFile);
1617 
1618     bool found = false;
1619     while (!temp_stream.atEnd())
1620     {
1621         file_line = temp_stream.readLine();
1622         // If we find a match, either replace, or remove (by skipping).
1623         if (file_line == search_line)
1624         {
1625             found = true;
1626             if (replace)
1627                 (out_stream) << replace_line << '\n';
1628             else
1629                 continue;
1630         }
1631         else
1632             (out_stream) << file_line << '\n';
1633     }
1634 
1635     // just append it if we haven't found it.
1636     if (!found && replace)
1637     {
1638         out_stream << replace_line << '\n';
1639     }
1640 
1641     URLFile.close();
1642 
1643     return { true, {} };
1644 }
1645 
1646 std::pair<bool, QString> KStarsData::editUserData(const QString &name,
1647                                                   const unsigned int index,
1648                                                   const SkyObjectUserdata::LinkData &data)
1649 {
1650     QMutexLocker _{ &m_user_data_mutex };
1651 
1652     auto &entry = findUserData(name);
1653     if (index >= entry.links[data.type].size())
1654         return { false, i18n("Userdata at index %1 does not exist.", index) };
1655 
1656     entry.links[data.type][index] = data;
1657 
1658     QString search_line = name;
1659     search_line += ':';
1660     search_line += data.title;
1661     search_line += ':';
1662     search_line += data.url.toString();
1663 
1664     QString replace_line = name + ':' + data.title + ':' + data.url.toString();
1665     return updateLocalDatabase(data.type, search_line, replace_line);
1666 }
1667 
1668 std::pair<bool, QString> KStarsData::deleteUserData(const QString &name,
1669                                                     const unsigned int index,
1670                                                     SkyObjectUserdata::Type type)
1671 {
1672     QMutexLocker _{ &m_user_data_mutex };
1673 
1674     auto &linkList = findUserData(name).links[type];
1675     if (index >= linkList.size())
1676         return { false, i18n("Userdata at index %1 does not exist.", index) };
1677 
1678     const auto data = linkList[index];
1679     linkList.erase(linkList.begin() + index);
1680 
1681     QString search_line = name;
1682     search_line += ':';
1683     search_line += data.title;
1684     search_line += ':';
1685     search_line += data.url.toString();
1686 
1687     QString replace_line = name + ':' + data.title + ':' + data.url.toString();
1688     return updateLocalDatabase(data.type, search_line, "");
1689 }
1690 
1691 std::pair<bool, QString> KStarsData::updateUserLog(const QString &name,
1692                                                    const QString &newLog)
1693 {
1694     QMutexLocker _{ &m_user_data_mutex };
1695 
1696     QFile file;
1697     QString logs; //existing logs
1698 
1699     //Do nothing if:
1700     //+ new log is the "default" message
1701     //+ new log is empty
1702     if (newLog == (i18n("Record here observation logs and/or data on %1.", name)) ||
1703         newLog.isEmpty())
1704         return { true, {} };
1705 
1706     // header label
1707     QString KSLabel = "[KSLABEL:" + name + ']';
1708 
1709     file.setFileName(
1710         QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath(
1711             "userlog.dat")); //determine filename in local user KDE directory tree.
1712 
1713     if (file.open(QIODevice::ReadOnly))
1714     {
1715         QTextStream instream(&file);
1716         // read all data into memory
1717         logs = instream.readAll();
1718         file.close();
1719     }
1720 
1721     const auto &userLog = m_user_data[name].userLog;
1722 
1723     // Remove old log entry from the logs text
1724     if (!userLog.isEmpty())
1725     {
1726         int startIndex, endIndex;
1727         QString sub;
1728 
1729         startIndex = logs.indexOf(KSLabel);
1730         sub        = logs.mid(startIndex);
1731         endIndex   = sub.indexOf("[KSLogEnd]");
1732 
1733         logs.remove(startIndex, endIndex + 11);
1734     }
1735 
1736     //append the new log entry to the end of the logs text
1737     logs.append(KSLabel + '\n' + newLog.trimmed() + "\n[KSLogEnd]\n");
1738 
1739     //Open file for writing
1740     if (!file.open(QIODevice::WriteOnly))
1741     {
1742         return { false, "Cannot write to user log file" };
1743     }
1744 
1745     //Write new logs text
1746     QTextStream outstream(&file);
1747     outstream << logs;
1748 
1749     file.close();
1750 
1751     findUserData(name).userLog = newLog;
1752     return { true, {} };
1753 };
1754 
1755 const SkyObjectUserdata::Data &KStarsData::getUserData(const QString &name)
1756 {
1757     QMutexLocker _{ &m_user_data_mutex };
1758 
1759     return findUserData(name); // we're consting it
1760 }
1761 
1762 SkyObjectUserdata::Data &KStarsData::findUserData(const QString &name)
1763 {
1764     auto element = m_user_data.find(name);
1765     if (element != m_user_data.end())
1766     {
1767         return element->second;
1768     }
1769 
1770     // fallback: we did not find it directly, therefore we may try to
1771     // find a matching object
1772     const auto *object = m_SkyComposite->findByName(name);
1773     if (object != nullptr)
1774     {
1775         return m_user_data[object->name()];
1776     }
1777 
1778     return m_user_data[name];
1779 };