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