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 °ree, 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