1 /*
2     SPDX-FileCopyrightText: 2016 Artem Fedoskin <afedoskin3@gmail.com>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "locationdialoglite.h"
8 
9 #include "kspaths.h"
10 #include "kstarsdata.h"
11 #include "kstarslite.h"
12 #include "Options.h"
13 
14 #include <QGeoPositionInfo>
15 #include <QGeoPositionInfoSource>
16 #include <QJsonArray>
17 #include <QJsonDocument>
18 #include <QJsonObject>
19 #include <QJsonValue>
20 #include <QNetworkAccessManager>
21 #include <QNetworkConfigurationManager>
22 #include <QNetworkReply>
23 #include <QNetworkSession>
24 #include <QQmlContext>
25 #include <QSqlQuery>
26 #include <QUrlQuery>
27 
LocationDialogLite()28 LocationDialogLite::LocationDialogLite()
29 {
30     KStarsLite *kstars = KStarsLite::Instance();
31     KStarsData *data = KStarsData::Instance();
32 
33     kstars->qmlEngine()->rootContext()->setContextProperty("CitiesModel", &m_cityList);
34 
35     //initialize cities once KStarsData finishes loading everything
36     connect(kstars, SIGNAL(dataLoadFinished()), this, SLOT(initCityList()));
37     connect(data, SIGNAL(geoChanged()), this, SLOT(updateCurrentLocation()));
38 
39     nam = new QNetworkAccessManager(this);
40     connect(nam, SIGNAL(finished(QNetworkReply*)), this, SLOT(processLocationNameData(QNetworkReply*)));
41 }
42 
getNameFromCoordinates(double latitude,double longitude)43 void LocationDialogLite::getNameFromCoordinates(double latitude, double longitude)
44 {
45     QString lat = QString::number(latitude);
46     QString lon = QString::number(longitude);
47     QString latlng(lat + ", " + lon);
48 
49     QUrl url("http://maps.googleapis.com/maps/api/geocode/json");
50     QUrlQuery query;
51     query.addQueryItem("latlng", latlng);
52     url.setQuery(query);
53     qDebug() << "submitting request";
54 
55     nam->get(QNetworkRequest(url));
56     connect(nam, SIGNAL(finished(QNetworkReply*)), this, SLOT(processLocationNameData(QNetworkReply*)));
57 }
58 
processLocationNameData(QNetworkReply * networkReply)59 void LocationDialogLite::processLocationNameData(QNetworkReply *networkReply)
60 {
61     if (!networkReply)
62         return;
63 
64     if (!networkReply->error())
65     {
66         QJsonDocument document = QJsonDocument::fromJson(networkReply->readAll());
67 
68         if (document.isObject())
69         {
70             QJsonObject obj = document.object();
71             QJsonValue val;
72 
73             if (obj.contains(QStringLiteral("results")))
74             {
75                 val = obj["results"];
76 
77                 QString city =
78                     val.toArray()[0].toObject()["address_components"].toArray()[2].toObject()["long_name"].toString();
79                 QString region =
80                     val.toArray()[0].toObject()["address_components"].toArray()[3].toObject()["long_name"].toString();
81                 QString country =
82                     val.toArray()[0].toObject()["address_components"].toArray()[4].toObject()["long_name"].toString();
83 
84                 emit newNameFromCoordinates(city, region, country);
85             }
86             else
87             {
88             }
89         }
90     }
91     networkReply->deleteLater();
92 }
93 
initCityList()94 void LocationDialogLite::initCityList()
95 {
96     KStarsData *data = KStarsData::Instance();
97     QStringList cities;
98     foreach (GeoLocation *loc, data->getGeoList())
99     {
100         QString name = loc->fullName();
101         cities.append(name);
102         filteredCityList.insert(name, loc);
103     }
104 
105     //Sort the list of Cities alphabetically...note that filteredCityList may now have a different ordering!
106     m_cityList.setStringList(cities);
107     m_cityList.sort(0);
108 
109     QStringList TZ;
110 
111     for (int i = 0; i < 25; ++i)
112         TZ.append(QLocale().toString((double)(i - 12)));
113     setProperty("TZList", TZ);
114 
115     QStringList DST;
116 
117     foreach (const QString &key, data->getRulebook().keys())
118     {
119         if (!key.isEmpty())
120             DST.append(key);
121     }
122     setProperty("DSTRules", DST);
123 }
124 
filterCity(const QString & city,const QString & province,const QString & country)125 void LocationDialogLite::filterCity(const QString &city, const QString &province, const QString &country)
126 {
127     KStarsData *data = KStarsData::Instance();
128     QStringList cities;
129     filteredCityList.clear();
130 
131     foreach (GeoLocation *loc, data->getGeoList())
132     {
133         QString sc(loc->translatedName());
134         QString ss(loc->translatedCountry());
135         QString sp = "";
136         if (!loc->province().isEmpty())
137             sp = loc->translatedProvince();
138 
139         if (sc.toLower().startsWith(city.toLower()) && sp.toLower().startsWith(province.toLower()) &&
140             ss.toLower().startsWith(country.toLower()))
141         {
142             QString name = loc->fullName();
143             cities.append(name);
144             filteredCityList.insert(name, loc);
145         }
146     }
147     m_cityList.setStringList(cities);
148     m_cityList.sort(0);
149 
150     setProperty("currLocIndex", m_cityList.stringList().indexOf(m_currentLocation));
151 }
152 
addCity(const QString & city,const QString & province,const QString & country,const QString & latitude,const QString & longitude,const QString & TimeZoneString,const QString & TZRule)153 bool LocationDialogLite::addCity(const QString &city, const QString &province, const QString &country,
154                                  const QString &latitude, const QString &longitude,
155                                  const QString &TimeZoneString, const QString &TZRule)
156 {
157     QSqlDatabase mycitydb = getDB();
158 
159     if (mycitydb.isValid())
160     {
161         QString fullName;
162 
163         if (!city.isEmpty())
164         {
165             fullName += city;
166         }
167 
168         if (!province.isEmpty())
169         {
170             fullName += ", " + province;
171         }
172 
173         if (!country.isEmpty())
174         {
175             fullName += ", " + country;
176         }
177 
178         if (m_cityList.stringList().contains(fullName))
179         {
180             return editCity(fullName, city, province, country, latitude, longitude, TimeZoneString, TZRule);
181         }
182 
183         bool latOk(false), lngOk(false), tzOk(false);
184         dms lat = createDms(latitude, true, &latOk);
185         dms lng = createDms(longitude, true, &lngOk);
186         //TimeZoneString.replace( QLocale().decimalPoint(), "." );
187         double TZ = TimeZoneString.toDouble(&tzOk);
188 
189         if (!latOk || !lngOk || !tzOk)
190             return false;
191 
192         //Strip off white space
193         QString City     = city.trimmed();
194         QString Province = province.trimmed();
195         QString Country  = country.trimmed();
196         GeoLocation *g   = nullptr;
197 
198         QSqlQuery add_query(mycitydb);
199         add_query.prepare("INSERT INTO city(Name, Province, Country, Latitude, Longitude, TZ, TZRule) VALUES(:Name, "
200                           ":Province, :Country, :Latitude, :Longitude, :TZ, :TZRule)");
201         add_query.bindValue(":Name", City);
202         add_query.bindValue(":Province", Province);
203         add_query.bindValue(":Country", Country);
204         add_query.bindValue(":Latitude", lat.toDMSString());
205         add_query.bindValue(":Longitude", lng.toDMSString());
206         add_query.bindValue(":TZ", TZ);
207         add_query.bindValue(":TZRule", TZRule);
208         if (add_query.exec() == false)
209         {
210             qWarning() << add_query.lastError() << endl;
211             return false;
212         }
213 
214         //Add city to geoList
215         g = new GeoLocation(lng, lat, City, Province, Country, TZ, &KStarsData::Instance()->Rulebook[TZRule]);
216         KStarsData::Instance()->getGeoList().append(g);
217 
218         mycitydb.commit();
219         mycitydb.close();
220         return true;
221     }
222 
223     return false;
224 }
225 
deleteCity(const QString & fullName)226 bool LocationDialogLite::deleteCity(const QString &fullName)
227 {
228     QSqlDatabase mycitydb = getDB();
229     GeoLocation *geo      = filteredCityList.value(fullName);
230 
231     if (mycitydb.isValid() && geo && !geo->isReadOnly())
232     {
233         QSqlQuery delete_query(mycitydb);
234         delete_query.prepare("DELETE FROM city WHERE Name = :Name AND Province = :Province AND Country = :Country");
235         delete_query.bindValue(":Name", geo->name());
236         delete_query.bindValue(":Province", geo->province());
237         delete_query.bindValue(":Country", geo->country());
238         if (delete_query.exec() == false)
239         {
240             qWarning() << delete_query.lastError() << endl;
241             return false;
242         }
243 
244         filteredCityList.remove(geo->fullName());
245         KStarsData::Instance()->getGeoList().removeOne(geo);
246         delete (geo);
247         mycitydb.commit();
248         mycitydb.close();
249         return true;
250     }
251     return false;
252 }
253 
editCity(const QString & fullName,const QString & city,const QString & province,const QString & country,const QString & latitude,const QString & longitude,const QString & TimeZoneString,const QString & TZRule)254 bool LocationDialogLite::editCity(const QString &fullName, const QString &city, const QString &province,
255                                   const QString &country, const QString &latitude,
256                                   const QString &longitude, const QString &TimeZoneString, const QString &TZRule)
257 {
258     QSqlDatabase mycitydb = getDB();
259     GeoLocation *geo      = filteredCityList.value(fullName);
260 
261     bool latOk(false), lngOk(false), tzOk(false);
262     dms lat   = createDms(latitude, true, &latOk);
263     dms lng   = createDms(longitude, true, &lngOk);
264     double TZ = TimeZoneString.toDouble(&tzOk);
265 
266     if (mycitydb.isValid() && geo && !geo->isReadOnly() && latOk && lngOk && tzOk)
267     {
268         QSqlQuery update_query(mycitydb);
269         update_query.prepare("UPDATE city SET Name = :newName, Province = :newProvince, Country = :newCountry, "
270                              "Latitude = :Latitude, Longitude = :Longitude, TZ = :TZ, TZRule = :TZRule WHERE "
271                              "Name = :Name AND Province = :Province AND Country = :Country");
272         update_query.bindValue(":newName", city);
273         update_query.bindValue(":newProvince", province);
274         update_query.bindValue(":newCountry", country);
275         update_query.bindValue(":Name", geo->name());
276         update_query.bindValue(":Province", geo->province());
277         update_query.bindValue(":Country", geo->country());
278         update_query.bindValue(":Latitude", lat.toDMSString());
279         update_query.bindValue(":Longitude", lng.toDMSString());
280         update_query.bindValue(":TZ", TZ);
281         update_query.bindValue(":TZRule", TZRule);
282         if (update_query.exec() == false)
283         {
284             qWarning() << update_query.lastError() << endl;
285             return false;
286         }
287 
288         geo->setName(city);
289         geo->setProvince(province);
290         geo->setCountry(country);
291         geo->setLat(lat);
292         geo->setLong(lng);
293         geo->setTZ0(TZ);
294         geo->setTZRule(&KStarsData::Instance()->Rulebook[TZRule]);
295 
296         //If we are changing current location update it
297         if (m_currentLocation == fullName)
298         {
299             setLocation(geo->fullName());
300         }
301 
302         mycitydb.commit();
303         mycitydb.close();
304         return true;
305     }
306     return false;
307 }
308 
getCity(const QString & fullName)309 QString LocationDialogLite::getCity(const QString &fullName)
310 {
311     GeoLocation *geo = filteredCityList.value(fullName);
312 
313     if (geo)
314     {
315         return geo->name();
316     }
317     return "";
318 }
319 
getProvince(const QString & fullName)320 QString LocationDialogLite::getProvince(const QString &fullName)
321 {
322     GeoLocation *geo = filteredCityList.value(fullName);
323 
324     if (geo)
325     {
326         return geo->province();
327     }
328     return "";
329 }
330 
getCountry(const QString & fullName)331 QString LocationDialogLite::getCountry(const QString &fullName)
332 {
333     GeoLocation *geo = filteredCityList.value(fullName);
334 
335     if (geo)
336     {
337         return geo->country();
338     }
339     return "";
340 }
341 
getLatitude(const QString & fullName)342 double LocationDialogLite::getLatitude(const QString &fullName)
343 {
344     GeoLocation *geo = filteredCityList.value(fullName);
345 
346     if (geo)
347     {
348         return geo->lat()->Degrees();
349     }
350     return 0;
351 }
352 
getLongitude(const QString & fullName)353 double LocationDialogLite::getLongitude(const QString &fullName)
354 {
355     GeoLocation *geo = filteredCityList.value(fullName);
356 
357     if (geo)
358     {
359         return geo->lng()->Degrees();
360     }
361     return 0;
362 }
363 
getTZ(const QString & fullName)364 int LocationDialogLite::getTZ(const QString &fullName)
365 {
366     GeoLocation *geo = filteredCityList.value(fullName);
367     if (geo)
368     {
369         return m_TZList.indexOf(QString::number(geo->TZ0()));
370     }
371     return -1;
372 }
373 
getDST(const QString & fullName)374 int LocationDialogLite::getDST(const QString &fullName)
375 {
376     GeoLocation *geo                      = filteredCityList.value(fullName);
377     QMap<QString, TimeZoneRule> &Rulebook = KStarsData::Instance()->Rulebook;
378 
379     if (geo)
380     {
381         foreach (const QString &key, Rulebook.keys())
382         {
383             if (!key.isEmpty() && geo->tzrule()->equals(&Rulebook[key]))
384                 return m_DSTRules.indexOf(key);
385         }
386     }
387     return -1;
388 }
389 
isDuplicate(const QString & city,const QString & province,const QString & country)390 bool LocationDialogLite::isDuplicate(const QString &city, const QString &province, const QString &country)
391 {
392     KStarsData *data = KStarsData::Instance();
393 
394     foreach (GeoLocation *loc, data->getGeoList())
395     {
396         QString sc(loc->translatedName());
397         QString ss(loc->translatedCountry());
398         QString sp;
399 
400         if (!loc->province().isEmpty())
401             sp = loc->translatedProvince();
402 
403         if (sc.toLower() == city.toLower() && sp.toLower() == province.toLower() && ss.toLower() == country.toLower())
404         {
405             return true;
406         }
407     }
408     return false;
409 }
410 
isReadOnly(const QString & fullName)411 bool LocationDialogLite::isReadOnly(const QString &fullName)
412 {
413     GeoLocation *geo = filteredCityList.value(fullName);
414 
415     if (geo)
416     {
417         return geo->isReadOnly();
418     }
419     else
420     {
421         return true; //We return true if geolocation wasn't found
422     }
423 }
424 
getDB()425 QSqlDatabase LocationDialogLite::getDB()
426 {
427     QSqlDatabase mycitydb = QSqlDatabase::database("mycitydb");
428     QString dbfile        = QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("mycitydb.sqlite");
429 
430     // If it doesn't exist, create it
431     if (QFile::exists(dbfile) == false)
432     {
433         mycitydb.setDatabaseName(dbfile);
434         mycitydb.open();
435         QSqlQuery create_query(mycitydb);
436         QString query("CREATE TABLE city ( "
437                       "id INTEGER DEFAULT NULL PRIMARY KEY AUTOINCREMENT, "
438                       "Name TEXT DEFAULT NULL, "
439                       "Province TEXT DEFAULT NULL, "
440                       "Country TEXT DEFAULT NULL, "
441                       "Latitude TEXT DEFAULT NULL, "
442                       "Longitude TEXT DEFAULT NULL, "
443                       "TZ REAL DEFAULT NULL, "
444                       "TZRule TEXT DEFAULT NULL)");
445         if (create_query.exec(query) == false)
446         {
447             qWarning() << create_query.lastError() << endl;
448             return QSqlDatabase();
449         }
450     }
451     else if (mycitydb.open() == false)
452     {
453         qWarning() << mycitydb.lastError() << endl;
454         return QSqlDatabase();
455     }
456 
457     return mycitydb;
458 }
459 
checkLongLat(const QString & longitude,const QString & latitude)460 bool LocationDialogLite::checkLongLat(const QString &longitude, const QString &latitude)
461 {
462     if (longitude.isEmpty() || latitude.isEmpty())
463         return false;
464 
465     bool ok = false;
466     double lng = createDms(longitude, true, &ok).Degrees();
467 
468     if (!ok || std::isnan(lng))
469         return false;
470 
471     double lat = createDms(latitude, true, &ok).Degrees();
472 
473     if (!ok || std::isnan(lat))
474         return false;
475 
476     if (fabs(lng) > 180 || fabs(lat) > 90)
477         return false;
478 
479     return true;
480 }
481 
setLocation(const QString & fullName)482 bool LocationDialogLite::setLocation(const QString &fullName)
483 {
484     KStarsData *data = KStarsData::Instance();
485 
486     GeoLocation *geo = filteredCityList.value(fullName);
487     if (!geo)
488     {
489         foreach (GeoLocation *loc, data->getGeoList())
490         {
491             if (loc->fullName() == fullName)
492             {
493                 geo = loc;
494                 break;
495             }
496         }
497     }
498 
499     if (geo)
500     {
501         // set new location in options
502         data->setLocation(*geo);
503 
504         // adjust local time to keep UT the same.
505         // create new LT without DST offset
506         KStarsDateTime ltime = geo->UTtoLT(data->ut());
507 
508         // reset timezonerule to compute next dst change
509         geo->tzrule()->reset_with_ltime(ltime, geo->TZ0(), data->isTimeRunningForward());
510 
511         // reset next dst change time
512         data->setNextDSTChange(geo->tzrule()->nextDSTChange());
513 
514         // reset local sideral time
515         data->syncLST();
516 
517         // Make sure Numbers, Moon, planets, and sky objects are updated immediately
518         data->setFullTimeUpdate();
519 
520         // If the sky is in Horizontal mode and not tracking, reset focus such that
521         // Alt/Az remain constant.
522         if (!Options::isTracking() && Options::useAltAz())
523         {
524             SkyMapLite::Instance()->focus()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
525         }
526 
527         // recalculate new times and objects
528         data->setSnapNextFocus();
529         KStarsLite::Instance()->updateTime();
530         return true;
531     }
532     return false;
533 }
534 
createDms(const QString & degree,bool deg,bool * ok)535 dms LocationDialogLite::createDms(const QString &degree, bool deg, bool *ok)
536 {
537     dms dmsAngle(0.0); // FIXME: Should we change this to NaN?
538     bool check = dmsAngle.setFromString(degree, deg);
539 
540     if (ok)
541     {
542         *ok = check; //ok might be a null pointer!
543     }
544     return dmsAngle;
545 }
546 
setCurrentLocation(const QString & loc)547 void LocationDialogLite::setCurrentLocation(const QString &loc)
548 {
549     if (m_currentLocation != loc)
550     {
551         m_currentLocation = loc;
552         emit currentLocationChanged(loc);
553     }
554 }
555 
updateCurrentLocation()556 void LocationDialogLite::updateCurrentLocation()
557 {
558     currentGeo = KStarsData::Instance()->geo();
559     setCurrentLocation(currentGeo->fullName());
560 }
561