1 /*
2 * Copyright (C) 2008 Fabien Chereau
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
17 */
18
19 #include "StelLocationMgr.hpp"
20 #include "StelLocationMgr_p.hpp"
21
22 #include "StelApp.hpp"
23 #include "StelCore.hpp"
24 #include "StelFileMgr.hpp"
25 #include "StelUtils.hpp"
26 #include "StelJsonParser.hpp"
27 #include "StelLocaleMgr.hpp"
28
29 #include <QStringListModel>
30 #include <QDebug>
31 #include <QFile>
32 #include <QDir>
33 #include <QNetworkInterface>
34 #include <QNetworkAccessManager>
35 #include <QNetworkRequest>
36 #include <QNetworkReply>
37 #include <QUrl>
38 #include <QUrlQuery>
39 #include <QSettings>
40 #include <QTimeZone>
41 #include <QTimer>
42 #include <QApplication>
43 #include <QRegularExpression>
44
45 TimezoneNameMap StelLocationMgr::locationDBToIANAtranslations;
46
47 QList<GeoRegion> StelLocationMgr::regions;
48 QMap<QString, QString> StelLocationMgr::countryCodeToRegionMap;
49 QMap<QString, QString> StelLocationMgr::countryNameToCodeMap;
50
51 #ifdef ENABLE_GPS
52 #ifdef ENABLE_LIBGPS
LibGPSLookupHelper(QObject * parent)53 LibGPSLookupHelper::LibGPSLookupHelper(QObject *parent)
54 : GPSLookupHelper(parent), ready(false)
55 {
56 QSettings* conf = StelApp::getInstance().getSettings();
57
58 QString gpsdHostname=conf->value("gui/gpsd_hostname", "localhost").toString();
59 QString gpsdPort=conf->value("gui/gpsd_port", DEFAULT_GPSD_PORT).toString();
60
61 timer.setSingleShot(false);
62 if (qApp->property("verbose").toBool())
63 qDebug() << "Opening GPSD connection to" << gpsdHostname << ":" << gpsdPort;
64 // Example almost straight from http://www.catb.org/gpsd/client-howto.html
65 gps_rec = new gpsmm(gpsdHostname.toUtf8(), gpsdPort.toUtf8());
66 if(gps_rec->is_open())
67 {
68 ready = gps_rec->stream(WATCH_ENABLE|WATCH_JSON);
69 }
70 if(ready)
71 {
72 connect(&timer, SIGNAL(timeout()), this, SLOT(query()));
73 }
74 else
75 qWarning()<<"libGPS lookup not ready, GPSD probably not running.";
76 }
77
~LibGPSLookupHelper()78 LibGPSLookupHelper::~LibGPSLookupHelper()
79 {
80 delete gps_rec;
81 }
82
isReady()83 bool LibGPSLookupHelper::isReady()
84 {
85 return ready;
86 }
87
setPeriodicQuery(int interval)88 void LibGPSLookupHelper::setPeriodicQuery(int interval)
89 {
90 if (interval==0)
91 timer.stop();
92 else
93 {
94 timer.start(interval);
95 }
96 }
query()97 void LibGPSLookupHelper::query()
98 {
99 bool verbose=qApp->property("verbose").toBool();
100
101 if(!ready)
102 {
103 emit queryError("GPSD helper not ready");
104 return;
105 }
106
107 StelLocation loc;
108
109 int tries=0;
110 int fixmode=0;
111 while (tries<10)
112 {
113 tries++;
114 if (verbose)
115 qDebug() << "query(): tries=" << tries;
116
117 if (!gps_rec->waiting(750000)) // argument usec. wait 0.75 sec. (example had 50s)
118 {
119 qDebug() << " - waiting timed out after 0.75sec.";
120 continue;
121 }
122
123 struct gps_data_t* newdata;
124 if ((newdata = gps_rec->read()) == Q_NULLPTR)
125 {
126 emit queryError("GPSD query: Read error.");
127 return;
128 }
129 else
130 {
131 // It is unclear why some data elements seem to be not filled by gps_rec.read().
132 // if (newdata->status==0) // no fix?
133 // {
134 // // This can happen indoors.
135 // qDebug() << "GPS has no fix.";
136 // emit queryError("GPSD query: No Fix.");
137 // return;
138 // }
139 #if GPSD_API_MAJOR_VERSION < 9
140 if (newdata->online==0.0) // no device?
141 #else
142 if (newdata->online.tv_sec == 0 && newdata->online.tv_nsec == 0) // no device?
143 #endif
144 {
145 // This can happen when unplugging the GPS while running Stellarium,
146 // or running gpsd with no GPS receiver.
147 emit queryError("GPS seems offline. No fix.");
148 return;
149 }
150
151
152 fixmode=newdata->fix.mode; // 0:not_seen, 1:no_fix, 2:2Dfix(no alt), 3:3Dfix(perfect)
153 if (verbose)
154 qDebug() << "GPSD newdata->fix.mode=" << fixmode;
155
156 if (fixmode==0)
157 {
158 // This may come just after creation of the GPSDhelper.
159 // It seems to take some time to fill the data.
160 if (verbose)
161 qDebug() << "GPSD seems not ready yet. Retry.";
162 continue;
163 }
164
165 if (verbose)
166 {
167 //qDebug() << "newdata->online=" << newdata->online;
168 qDebug() << "Solution from " << newdata->satellites_used << "out of " << newdata->satellites_visible << " visible Satellites.";
169 dop_t dop=newdata->dop;
170 #if GPSD_API_MAJOR_VERSION < 9
171 qDebug() << "GPSD data: Long" << newdata->fix.longitude << "Lat" << newdata->fix.latitude << "Alt" << newdata->fix.altitude;
172 #else
173 qDebug() << "GPSD data: Long" << newdata->fix.longitude << "Lat" << newdata->fix.latitude << "Alt" << newdata->fix.altHAE;
174 #endif
175 qDebug() << "Dilution of Precision:";
176 qDebug() << " - xdop:" << dop.xdop << "ydop:" << dop.ydop;
177 qDebug() << " - pdop:" << dop.pdop << "hdop:" << dop.hdop;
178 qDebug() << " - vdop:" << dop.vdop << "tdop:" << dop.tdop << "gdop:" << dop.gdop;
179 // GPSD API 8.0:
180 // * Remove epe from gps_data_t, it duplicates gps_fix_t eph
181 // * Added sep (estimated spherical error, 3D)
182 // Details: https://github.com/Stellarium/stellarium/issues/733
183 // #if GPSD_API_MAJOR_VERSION >= 8
184 // qDebug() << "Spherical Position Error (sep):" << newdata->fix.sep;
185 // #else
186 // qDebug() << "Spherical Position Error (epe):" << newdata->epe;
187 // #endif
188 }
189 loc.longitude = static_cast<float> (newdata->fix.longitude);
190 loc.latitude = static_cast<float> (newdata->fix.latitude);
191 // Frequently hdop, vdop and satellite counts are NaN. Sometimes they show OK. This is minor issue.
192 if ((verbose) && (fixmode<3))
193 {
194 qDebug() << "GPSDfix " << fixmode << ": Location" << QString("lat %1, long %2, alt %3").arg(loc.latitude).arg(loc.longitude).arg(loc.altitude);
195 qDebug() << " Estimated HDOP " << newdata->dop.hdop << "m from " << newdata->satellites_used << "(of" << newdata->satellites_visible << "visible) satellites";
196 }
197 else
198 {
199 #if GPSD_API_MAJOR_VERSION < 9
200 loc.altitude=static_cast<int>(newdata->fix.altitude);
201 #else
202 loc.altitude=static_cast<int>(newdata->fix.altHAE);
203 #endif
204 if (verbose)
205 {
206 qDebug() << "GPSDfix " << fixmode << ": Location" << QString("lat %1, long %2, alt %3").arg(loc.latitude).arg(loc.longitude).arg(loc.altitude);
207 qDebug() << " Estimated HDOP " << newdata->dop.hdop << "m, VDOP " << newdata->dop.vdop << "m from " << newdata->satellites_used << "(of" << newdata->satellites_visible << "visible) satellites";
208 }
209 break; // escape from the tries loop
210 }
211 }
212 }
213
214 if (fixmode <2)
215 {
216 emit queryError("GPSD: Could not get valid position.");
217 return;
218 }
219 if ((verbose) && (fixmode<3))
220 {
221 qDebug() << "Fix only quality " << fixmode << " after " << tries << " tries";
222 }
223 if (verbose)
224 qDebug() << "GPSD location" << QString("lat %1, long %2, alt %3").arg(loc.latitude).arg(loc.longitude).arg(loc.altitude);
225
226 loc.bortleScaleIndex=StelLocation::DEFAULT_BORTLE_SCALE_INDEX;
227 // Usually you don't leave your time zone with GPS.
228 loc.ianaTimeZone=StelApp::getInstance().getCore()->getCurrentTimeZone();
229 loc.isUserLocation=true;
230 loc.planetName="Earth";
231 loc.name=QString("GPS %1%2 %3%4")
232 .arg(loc.latitude<0?"S":"N").arg(floor(loc.latitude))
233 .arg(loc.longitude<0?"W":"E").arg(floor(loc.longitude));
234 emit queryFinished(loc);
235 }
236
237 #endif
238
NMEALookupHelper(QObject * parent)239 NMEALookupHelper::NMEALookupHelper(QObject *parent)
240 : GPSLookupHelper(parent), serial(Q_NULLPTR), nmea(Q_NULLPTR)
241 {
242 //use RAII
243 // Getting a list of ports may enable auto-detection!
244 QList<QSerialPortInfo> portInfoList=QSerialPortInfo::availablePorts();
245
246 if (portInfoList.size()==0)
247 {
248 qDebug() << "No connected devices found. NMEA GPS lookup failed.";
249 return;
250 }
251
252 QSettings* conf = StelApp::getInstance().getSettings();
253
254 // As long as we only have one, this is OK. Else we must do something about COM3, COM4 etc.
255 QSerialPortInfo portInfo;
256 if (portInfoList.size()==1)
257 {
258 portInfo=portInfoList.at(0);
259 qDebug() << "Only one port found at " << portInfo.portName();
260 }
261 else
262 {
263 #ifdef Q_OS_WIN
264 QString portName=conf->value("gui/gps_interface", "COM3").toString();
265 #else
266 QString portName=conf->value("gui/gps_interface", "ttyUSB0").toString();
267 #endif
268 bool portFound=false;
269 for (int i=0; i<portInfoList.size(); ++i)
270 {
271 QSerialPortInfo pi=portInfoList.at(i);
272 qDebug() << "Serial port list. Make sure you are using the right configuration.";
273 qDebug() << "Port: " << pi.portName();
274 qDebug() << " SystemLocation:" << pi.systemLocation();
275 qDebug() << " Description:" << pi.description();
276 qDebug() << " Manufacturer:" << pi.manufacturer();
277 qDebug() << " VendorID:" << pi.vendorIdentifier();
278 qDebug() << " ProductID:" << pi.productIdentifier();
279 qDebug() << " SerialNumber:" << pi.serialNumber();
280 qDebug() << " Busy:" << pi.isBusy();
281 qDebug() << " Null:" << pi.isNull();
282 if (pi.portName()==portName)
283 {
284 portInfo=pi;
285 portFound=true;
286 }
287 }
288 if (!portFound)
289 {
290 qDebug() << "Configured port" << portName << "not found. No GPS query.";
291 return;
292 }
293 }
294
295 // NMEA-0183 specifies device sends at 4800bps, 8N1. Some devices however send at 9600, allow this.
296 // baudrate is configurable via config
297 qint32 baudrate=conf->value("gui/gps_baudrate", 4800).toInt();
298
299 nmea=new QNmeaPositionInfoSource(QNmeaPositionInfoSource::RealTimeMode,this);
300 //serial = new QSerialPort(portInfo, nmea);
301 serial = new QSerialPort(portInfo, this);
302 serial->setBaudRate(baudrate);
303 serial->setDataBits(QSerialPort::Data8);
304 serial->setParity(QSerialPort::NoParity);
305 serial->setStopBits(QSerialPort::OneStop);
306 serial->setFlowControl(QSerialPort::NoFlowControl);
307 if (serial->open(QIODevice::ReadOnly)) // may fail when line used by other program!
308 {
309 nmea->setDevice(serial);
310 qDebug() << "Query GPS NMEA device at port " << serial->portName();
311 connect(nmea, SIGNAL(error(QGeoPositionInfoSource::Error)), this, SLOT(nmeaError(QGeoPositionInfoSource::Error)));
312 connect(nmea, SIGNAL(positionUpdated(const QGeoPositionInfo)),this,SLOT(nmeaUpdated(const QGeoPositionInfo)));
313 connect(nmea, SIGNAL(updateTimeout()),this,SLOT(nmeaTimeout()));
314 }
315 else qWarning() << "Cannot open serial port to NMEA device at port " << serial->portName();
316 // This may leave an un-ready object. Must be cleaned-up later.
317 }
~NMEALookupHelper()318 NMEALookupHelper::~NMEALookupHelper()
319 {
320 if(nmea)
321 {
322 delete nmea;
323 nmea=Q_NULLPTR;
324 }
325 if (serial)
326 {
327 if (serial->isOpen())
328 {
329 //qDebug() << "NMEALookupHelper destructor: Close serial first";
330 serial->clear();
331 serial->close();
332 }
333 delete serial;
334 serial=Q_NULLPTR;
335 }
336 }
337
query()338 void NMEALookupHelper::query()
339 {
340 if(isReady())
341 {
342 //kick off a single update request
343 nmea->requestUpdate(3000);
344 }
345 else
346 emit queryError("NMEA helper not ready");
347 }
setPeriodicQuery(int interval)348 void NMEALookupHelper::setPeriodicQuery(int interval)
349 {
350 if(isReady())
351 {
352 if (interval==0)
353 nmea->stopUpdates();
354 else
355 {
356 nmea->setUpdateInterval(interval);
357 nmea->startUpdates();
358 }
359 }
360 else
361 emit queryError("NMEA helper not ready");
362 }
363
nmeaUpdated(const QGeoPositionInfo & update)364 void NMEALookupHelper::nmeaUpdated(const QGeoPositionInfo &update)
365 {
366 bool verbose=qApp->property("verbose").toBool();
367 if (verbose)
368 qDebug() << "NMEA updated";
369
370 QGeoCoordinate coord=update.coordinate();
371 QDateTime timestamp=update.timestamp();
372
373 if (verbose)
374 {
375 qDebug() << " - time: " << timestamp.toString();
376 qDebug() << " - location: Long=" << coord.longitude() << " Lat=" << coord.latitude() << " Alt=" << coord.altitude();
377 }
378 if (update.isValid()) // emit queryFinished(loc) with new location
379 {
380 StelCore *core=StelApp::getInstance().getCore();
381 StelLocation loc;
382 loc.longitude=static_cast<float>(coord.longitude());
383 loc.latitude=static_cast<float>(coord.latitude());
384 // 2D fix may have only long/lat, invalid altitude.
385 loc.altitude=( qIsNaN(coord.altitude()) ? 0 : static_cast<int>(floor(coord.altitude())));
386 if (verbose)
387 qDebug() << "Location in progress: Long=" << loc.longitude << " Lat=" << loc.latitude << " Alt" << loc.altitude;
388 loc.bortleScaleIndex=StelLocation::DEFAULT_BORTLE_SCALE_INDEX;
389 // Usually you don't leave your time zone with GPS.
390 loc.ianaTimeZone=core->getCurrentTimeZone();
391 loc.isUserLocation=true;
392 loc.planetName="Earth";
393 loc.name=QString("GPS %1%2 %3%4")
394 .arg(loc.longitude<0?"W":"E").arg(floor(loc.longitude))
395 .arg(loc.latitude<0?"S":"N").arg(floor(loc.latitude));
396 if (verbose)
397 qDebug() << "New location named " << loc.name;
398
399 emit queryFinished(loc);
400 }
401 else
402 {
403 if (verbose)
404 qDebug() << "(This position update was an invalid package)";
405 emit queryError("NMEA update: invalid package");
406 }
407 }
408
nmeaError(QGeoPositionInfoSource::Error error)409 void NMEALookupHelper::nmeaError(QGeoPositionInfoSource::Error error)
410 {
411 emit queryError(QString("NMEA general error: %1").arg(error));
412 }
413
nmeaTimeout()414 void NMEALookupHelper::nmeaTimeout()
415 {
416 emit queryError("NMEA timeout");
417 }
418 #endif
419
StelLocationMgr()420 StelLocationMgr::StelLocationMgr()
421 : nmeaHelper(Q_NULLPTR), libGpsHelper(Q_NULLPTR)
422 {
423 // initialize the static QMap first if necessary.
424 // The first entry is the DB name, the second is as we display it in the program.
425 if (locationDBToIANAtranslations.count()==0)
426 {
427 // reported in SF forum on 2017-03-27
428 locationDBToIANAtranslations.insert("Europe/Minsk", "UTC+03:00");
429 locationDBToIANAtranslations.insert("Europe/Samara", "UTC+04:00");
430 locationDBToIANAtranslations.insert("America/Cancun", "UTC-05:00");
431 locationDBToIANAtranslations.insert("Asia/Kamchatka", "UTC+12:00");
432 // Missing on Qt5.7/Win10 as of 2017-03-18.
433 locationDBToIANAtranslations.insert("Europe/Astrakhan", "UTC+04:00");
434 locationDBToIANAtranslations.insert("Europe/Ulyanovsk", "UTC+04:00");
435 locationDBToIANAtranslations.insert("Europe/Kirov", "UTC+03:00");
436 locationDBToIANAtranslations.insert("Asia/Hebron", "Asia/Jerusalem");
437 locationDBToIANAtranslations.insert("Asia/Gaza", "Asia/Jerusalem"); // or use UTC+2:00? (political issue...)
438 locationDBToIANAtranslations.insert("Asia/Kolkata", "Asia/Calcutta");
439 locationDBToIANAtranslations.insert("Asia/Kathmandu", "Asia/Katmandu");
440 locationDBToIANAtranslations.insert("Asia/Tomsk", "Asia/Novosibirsk");
441 locationDBToIANAtranslations.insert("Asia/Barnaul", "UTC+07:00");
442 locationDBToIANAtranslations.insert("Asia/Ho_Chi_Minh", "Asia/Saigon");
443 locationDBToIANAtranslations.insert("Asia/Hovd", "UTC+07:00");
444 locationDBToIANAtranslations.insert("America/Argentina/Buenos_Aires", "America/Buenos_Aires");
445 locationDBToIANAtranslations.insert("America/Argentina/Jujuy", "America/Jujuy");
446 locationDBToIANAtranslations.insert("America/Argentina/Mendoza", "America/Mendoza");
447 locationDBToIANAtranslations.insert("America/Argentina/Catamarca", "America/Catamarca");
448 locationDBToIANAtranslations.insert("America/Argentina/Cordoba", "America/Cordoba");
449 locationDBToIANAtranslations.insert("America/Indiana/Indianapolis", "America/Indianapolis");
450 locationDBToIANAtranslations.insert("America/Kentucky/Louisville", "America/Louisville");
451 locationDBToIANAtranslations.insert("America/Miquelon", "UTC-03:00"); // Small Canadian island.
452 locationDBToIANAtranslations.insert("Africa/Asmara", "Africa/Asmera");
453 locationDBToIANAtranslations.insert("Atlantic/Faroe", "Atlantic/Faeroe");
454 locationDBToIANAtranslations.insert("Pacific/Pohnpei", "Pacific/Ponape");
455 locationDBToIANAtranslations.insert("Pacific/Norfolk", "UTC+11:00");
456 locationDBToIANAtranslations.insert("Pacific/Pitcairn", "UTC-08:00");
457 // Missing on Qt5.5.1/Ubuntu 16.04.1 LTE as of 2017-03-18:
458 // NOTE: We must add these following zones for lookup in both ways: When the binary file is being created for publication on Linux, Rangoon/Yangon is being translated.
459 locationDBToIANAtranslations.insert("Asia/Rangoon", "Asia/Yangon"); // UTC+6:30 Yangon missing on Ubuntu/Qt5.5.1.
460 locationDBToIANAtranslations.insert("Asia/Yangon", "Asia/Rangoon"); // This can translate from the binary location file back to the zone name as known on Windows.
461 locationDBToIANAtranslations.insert( "", "UTC");
462 // Missing on Qt5.9.5/Ubuntu 18.04.4
463 locationDBToIANAtranslations.insert("America/Godthab", "UTC-03:00");
464 // Missing on Qt5.12.10/Win7
465 locationDBToIANAtranslations.insert("Asia/Qostanay", "UTC+06:00"); // no DST; https://www.zeitverschiebung.net/en/timezone/asia--qostanay
466 locationDBToIANAtranslations.insert("Europe/Saratov", "UTC+04:00"); // no DST; https://www.zeitverschiebung.net/en/timezone/europe--saratov
467 locationDBToIANAtranslations.insert("Asia/Atyrau", "UTC+05:00"); // no DST; https://www.zeitverschiebung.net/en/timezone/asia--atyrau
468 locationDBToIANAtranslations.insert("Asia/Famagusta", "Asia/Nicosia"); // Asia/Nicosia has no DST, but Asia/Famagusta has DST!
469 locationDBToIANAtranslations.insert("America/Punta_Arenas", "UTC-03:00"); // no DST; https://www.zeitverschiebung.net/en/timezone/america--punta_arenas
470 // N.B. Further missing TZ names will be printed out in the log.txt. Resolve these by adding into this list.
471 // TODO later: create a text file in user data directory, and auto-update it weekly.
472 }
473
474 QSettings* conf = StelApp::getInstance().getSettings();
475
476 loadCountries();
477 loadRegions();
478 // The line below allows to re-generate the location file, you still need to gunzip it manually afterward.
479 if (conf->value("devel/convert_locations_list", false).toBool())
480 generateBinaryLocationFile("data/base_locations.txt", false, "data/base_locations.bin");
481
482 locations = loadCitiesBin("data/base_locations.bin.gz");
483 locations.unite(loadCities("data/user_locations.txt", true));
484
485 // Init to Paris France because it's the center of the world.
486 lastResortLocation = locationForString(conf->value("init_location/last_location", "Paris, Western Europe").toString());
487 }
488
~StelLocationMgr()489 StelLocationMgr::~StelLocationMgr()
490 {
491 if (nmeaHelper)
492 {
493 delete nmeaHelper;
494 nmeaHelper=Q_NULLPTR;
495 }
496 if (libGpsHelper)
497 {
498 delete libGpsHelper;
499 libGpsHelper=Q_NULLPTR;
500 }
501 }
502
StelLocationMgr(const LocationList & locations)503 StelLocationMgr::StelLocationMgr(const LocationList &locations)
504 : nmeaHelper(Q_NULLPTR), libGpsHelper(Q_NULLPTR)
505 {
506 setLocations(locations);
507
508 QSettings* conf = StelApp::getInstance().getSettings();
509 // Init to Paris France because it's the center of the world.
510 lastResortLocation = locationForString(conf->value("init_location/last_location", "Paris, Western Europe").toString());
511 }
512
setLocations(const LocationList & locations)513 void StelLocationMgr::setLocations(const LocationList &locations)
514 {
515 this->locations.clear();
516 for (const auto& loc : locations)
517 {
518 this->locations.insert(loc.getID(), loc);
519 }
520
521 emit locationListChanged();
522 }
523
generateBinaryLocationFile(const QString & fileName,bool isUserLocation,const QString & binFilePath) const524 void StelLocationMgr::generateBinaryLocationFile(const QString& fileName, bool isUserLocation, const QString& binFilePath) const
525 {
526 qDebug() << "Generating a locations list...";
527 const QMap<QString, StelLocation>& cities = loadCities(fileName, isUserLocation);
528 QFile binfile(StelFileMgr::findFile(binFilePath, StelFileMgr::New));
529 if(binfile.open(QIODevice::WriteOnly))
530 {
531 QDataStream out(&binfile);
532 out.setVersion(QDataStream::Qt_5_2);
533 out << cities;
534 binfile.flush();
535 binfile.close();
536 }
537 qDebug() << "[...] Please use 'gzip -nc base_locations.bin > base_locations.bin.gz' to pack a locations list.";
538 }
539
loadCitiesBin(const QString & fileName)540 LocationMap StelLocationMgr::loadCitiesBin(const QString& fileName)
541 {
542 QMap<QString, StelLocation> res;
543 QString cityDataPath = StelFileMgr::findFile(fileName);
544 if (cityDataPath.isEmpty())
545 return res;
546
547 QFile sourcefile(cityDataPath);
548 if (!sourcefile.open(QIODevice::ReadOnly))
549 {
550 qWarning() << "ERROR: Could not open location data file: " << QDir::toNativeSeparators(cityDataPath);
551 return res;
552 }
553
554 if (fileName.endsWith(".gz"))
555 {
556 QDataStream in(StelUtils::uncompress(sourcefile.readAll()));
557 in.setVersion(QDataStream::Qt_5_2);
558 in >> res;
559 }
560 else
561 {
562 QDataStream in(&sourcefile);
563 in.setVersion(QDataStream::Qt_5_2);
564 in >> res;
565 }
566 // Now res has all location data. However, some timezone names are not available in various versions of Qt.
567 // Sanity checks: It seems we must translate timezone names. Quite a number on Windows, but also still some on Linux.
568 QList<QByteArray> availableTimeZoneList=QTimeZone::availableTimeZoneIds();
569 QStringList unknownTZlist;
570 for (auto& loc : res)
571 {
572 if ((loc.ianaTimeZone!="LMST") && (loc.ianaTimeZone!="LTST") && ( ! availableTimeZoneList.contains(loc.ianaTimeZone.toUtf8())) )
573 {
574 // TZ name which is currently unknown to Qt detected. See if we can translate it, if not: complain to qDebug().
575 QString fixTZname=sanitizeTimezoneStringFromLocationDB(loc.ianaTimeZone);
576 if (availableTimeZoneList.contains(fixTZname.toUtf8()))
577 {
578 loc.ianaTimeZone=fixTZname;
579 }
580 else
581 {
582 qDebug() << "StelLocationMgr::loadCitiesBin(): TimeZone for " << loc.name << " not found: " << loc.ianaTimeZone;
583 unknownTZlist.append(loc.ianaTimeZone);
584 }
585 }
586 }
587 if (unknownTZlist.length()>0)
588 {
589 unknownTZlist.removeDuplicates();
590 qDebug() << "StelLocationMgr::loadCitiesBin(): Summary of unknown TimeZones:";
591 for (const auto& tz : unknownTZlist)
592 {
593 qDebug() << tz;
594 }
595 qDebug() << "Please report these timezone names (this logfile) to the Stellarium developers.";
596 // Note to developers: Fill those names and replacements to the map above.
597 }
598
599 return res;
600 }
601
602 // Done in the following: TZ name sanitizing also for text file!
loadCities(const QString & fileName,bool isUserLocation)603 LocationMap StelLocationMgr::loadCities(const QString& fileName, bool isUserLocation)
604 {
605 // Load the cities from data file
606 QMap<QString, StelLocation> locations;
607 QString cityDataPath = StelFileMgr::findFile(fileName);
608 if (cityDataPath.isEmpty())
609 {
610 // Note it is quite normal not to have a user locations file (e.g. first run)
611 if (!isUserLocation)
612 qWarning() << "WARNING: Failed to locate location data file: " << QDir::toNativeSeparators(fileName);
613 return locations;
614 }
615
616 QFile sourcefile(cityDataPath);
617 if (!sourcefile.open(QIODevice::ReadOnly | QIODevice::Text))
618 {
619 qWarning() << "ERROR: Could not open location data file: " << QDir::toNativeSeparators(cityDataPath);
620 return locations;
621 }
622
623 // Read the data serialized from the file.
624 // Code below borrowed from Marble (http://edu.kde.org/marble/)
625 QTextStream sourcestream(&sourcefile);
626 sourcestream.setCodec("UTF-8");
627 StelLocation loc;
628 while (!sourcestream.atEnd())
629 {
630 const QString& rawline=sourcestream.readLine();
631 if (rawline.isEmpty() || rawline.startsWith('#') || (rawline.split("\t").count() < 8))
632 continue;
633 loc = StelLocation::createFromLine(rawline);
634 loc.isUserLocation = isUserLocation;
635 const QString& locId = loc.getID();
636
637 if (locations.contains(locId))
638 {
639 // Add the state in the name of the existing one and the new one to differentiate
640 StelLocation loc2 = locations[locId];
641 if (!loc2.state.isEmpty())
642 loc2.name += " ("+loc2.state+")";
643 // remove and re-add the fixed version
644 locations.remove(locId);
645 locations.insert(loc2.getID(), loc2);
646
647 if (!loc.state.isEmpty())
648 loc.name += " ("+loc.state+")";
649 locations.insert(loc.getID(), loc);
650 }
651 else
652 {
653 locations.insert(locId, loc);
654 }
655 }
656 sourcefile.close();
657 return locations;
658 }
659
parseAngle(const QString & s,bool * ok)660 static float parseAngle(const QString& s, bool* ok)
661 {
662 float ret;
663 // First try normal decimal value.
664 ret = s.toFloat(ok);
665 if (*ok) return ret;
666 // Try GPS coordinate like +121°33'38.28"
667 QRegularExpression reg("([+-]?[\\d.]+)°(?:([\\d.]+)')?(?:([\\d.]+)\")?");
668 QRegularExpressionMatch match=reg.match(s);
669 if (match.hasMatch())
670 {
671 float deg = match.captured(1).toFloat(ok);
672 if (!*ok) return 0;
673 float min = match.captured(2).isEmpty()? 0 : match.captured(2).toFloat(ok);
674 if (!*ok) return 0;
675 float sec = match.captured(3).isEmpty()? 0 : match.captured(3).toFloat(ok);
676 if (!*ok) return 0;
677 return deg + min / 60 + sec / 3600;
678 }
679 return 0;
680 }
681
locationForString(const QString & s) const682 const StelLocation StelLocationMgr::locationForString(const QString& s) const
683 {
684 auto iter = locations.find(s);
685 if (iter!=locations.end())
686 {
687 return iter.value();
688 }
689 // Maybe this is a city and country names (old format of the data)?
690 QRegularExpression cnreg("(.+),\\s+(.+)$");
691 QRegularExpressionMatch cnMatch=cnreg.match(s);
692 if (cnMatch.hasMatch())
693 {
694 // NOTE: This method will give wrong data for some Russians and U.S. locations
695 // (Asian locations for Russia and for locations on Hawaii for U.S.)
696 QString city = cnMatch.captured(1).trimmed();
697 QString country = cnMatch.captured(2).trimmed();
698 auto iter = locations.find(QString("%1, %2").arg(city, pickRegionFromCountry(country)));
699 if (iter!=locations.end())
700 {
701 return iter.value();
702 }
703 }
704 StelLocation ret;
705 // Maybe it is a coordinate set with elevation?
706 QRegularExpression csreg("(.+),\\s*(.+),\\s*(.+)");
707 QRegularExpressionMatch csMatch=csreg.match(s);
708 if (csMatch.hasMatch())
709 {
710 bool ok;
711 // We have a set of coordinates
712 ret.latitude = parseAngle(csMatch.captured(1).trimmed(), &ok);
713 if (!ok) ret.role = '!';
714 ret.longitude = parseAngle(csMatch.captured(2).trimmed(), &ok);
715 if (!ok) ret.role = '!';
716 ret.altitude = csMatch.captured(3).trimmed().toInt(&ok);
717 if (!ok) ret.role = '!';
718 ret.name = QString("%1, %2").arg(QString::number(ret.latitude, 'f', 2), QString::number(ret.longitude, 'f', 2));
719 ret.planetName = "Earth";
720 return ret;
721 }
722 // Maybe it is a coordinate set without elevation? (e.g. GPS 25.107363,121.558807 )
723 QRegularExpression reg("(?:(.+)\\s+)?(.+),\\s*(.+)"); // FIXME: Seems regexp is not very good
724 QRegularExpressionMatch match=reg.match(s);
725 if (match.hasMatch())
726 {
727 bool ok;
728 // We have a set of coordinates
729 ret.latitude = parseAngle(match.captured(2).trimmed(), &ok);
730 if (!ok) ret.role = '!';
731 ret.longitude = parseAngle(match.captured(3).trimmed(), &ok);
732 if (!ok) ret.role = '!';
733 ret.name = match.captured(1).trimmed();
734 ret.planetName = "Earth";
735 return ret;
736 }
737 ret.role = '!';
738 return ret;
739 }
740
locationFromCLI() const741 const StelLocation StelLocationMgr::locationFromCLI() const
742 {
743 StelLocation ret;
744 QSettings* conf = StelApp::getInstance().getSettings();
745 bool ok;
746 conf->beginGroup("location_run_once");
747 ret.latitude = parseAngle(StelUtils::radToDmsStr(conf->value("latitude").toDouble(), true), &ok);
748 if (!ok) ret.role = '!';
749 ret.longitude = parseAngle(StelUtils::radToDmsStr(conf->value("longitude").toDouble(), true), &ok);
750 if (!ok) ret.role = '!';
751 ret.altitude = conf->value("altitude", 0).toInt(&ok);
752 ret.planetName = conf->value("home_planet", "Earth").toString();
753 ret.landscapeKey = conf->value("landscape_name", "guereins").toString();
754 conf->endGroup();
755 conf->remove("location_run_once");
756 ret.state="CLI"; // flag this location with a marker for handling in LandscapeMgr::init(). state is not displayed anywhere, so I expect no issues from that.
757 return ret;
758 }
759
760 // Get whether a location can be permanently added to the list of user locations
canSaveUserLocation(const StelLocation & loc) const761 bool StelLocationMgr::canSaveUserLocation(const StelLocation& loc) const
762 {
763 return loc.isValid() && locations.find(loc.getID())==locations.end();
764 }
765
766 // Add permanently a location to the list of user locations
saveUserLocation(const StelLocation & loc)767 bool StelLocationMgr::saveUserLocation(const StelLocation& loc)
768 {
769 if (!canSaveUserLocation(loc))
770 return false;
771
772 // Add in the program
773 locations[loc.getID()]=loc;
774
775 //emit before saving the list
776 emit locationListChanged();
777
778 // Append to the user location file
779 QString cityDataPath = StelFileMgr::findFile("data/user_locations.txt", StelFileMgr::Flags(StelFileMgr::Writable|StelFileMgr::File));
780 if (cityDataPath.isEmpty())
781 {
782 if (!StelFileMgr::exists(StelFileMgr::getUserDir()+"/data"))
783 {
784 if (!StelFileMgr::mkDir(StelFileMgr::getUserDir()+"/data"))
785 {
786 qWarning() << "ERROR - cannot create non-existent data directory" << QDir::toNativeSeparators(StelFileMgr::getUserDir()+"/data");
787 qWarning() << "Location cannot be saved";
788 return false;
789 }
790 }
791
792 cityDataPath = StelFileMgr::getUserDir()+"/data/user_locations.txt";
793 qWarning() << "Will create a new user location file: " << QDir::toNativeSeparators(cityDataPath);
794 }
795
796 QFile sourcefile(cityDataPath);
797 if (!sourcefile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append))
798 {
799 qWarning() << "ERROR: Could not open location data file: " << QDir::toNativeSeparators(cityDataPath);
800 return false;
801 }
802
803 QTextStream outstream(&sourcefile);
804 outstream.setCodec("UTF-8");
805 outstream << loc.serializeToLine() << '\n';
806 sourcefile.close();
807
808 return true;
809 }
810
811 // Get whether a location can be deleted from the list of user locations
812 // If the location comes from the base read only list, it cannot be deleted
canDeleteUserLocation(const QString & id) const813 bool StelLocationMgr::canDeleteUserLocation(const QString& id) const
814 {
815 auto iter=locations.find(id);
816
817 // If it's not known at all there is a problem
818 if (iter==locations.end())
819 return false;
820
821 return iter.value().isUserLocation;
822 }
823
824 // Delete permanently the given location from the list of user locations
825 // If the location comes from the base read only list, it cannot be deleted and false is returned
deleteUserLocation(const QString & id)826 bool StelLocationMgr::deleteUserLocation(const QString& id)
827 {
828 if (!canDeleteUserLocation(id))
829 return false;
830
831 locations.remove(id);
832
833 //emit before saving the list
834 emit locationListChanged();
835
836 // Resave the whole remaining user locations file
837 QString cityDataPath = StelFileMgr::findFile("data/user_locations.txt", StelFileMgr::Writable);
838 if (cityDataPath.isEmpty())
839 {
840 if (!StelFileMgr::exists(StelFileMgr::getUserDir()+"/data"))
841 {
842 if (!StelFileMgr::mkDir(StelFileMgr::getUserDir()+"/data"))
843 {
844 qWarning() << "ERROR - cannot create non-existent data directory" << QDir::toNativeSeparators(StelFileMgr::getUserDir()+"/data");
845 qWarning() << "Location cannot be saved";
846 return false;
847 }
848 }
849
850 cityDataPath = StelFileMgr::getUserDir()+"/data/user_locations.txt";
851 qWarning() << "Will create a new user location file: " << QDir::toNativeSeparators(cityDataPath);
852 }
853
854 QFile sourcefile(cityDataPath);
855 if (!sourcefile.open(QIODevice::WriteOnly | QIODevice::Text))
856 {
857 qWarning() << "ERROR: Could not open location data file: " << QDir::toNativeSeparators(cityDataPath);
858 return false;
859 }
860
861 QTextStream outstream(&sourcefile);
862 outstream.setCodec("UTF-8");
863
864 for (QMap<QString, StelLocation>::ConstIterator iter=locations.constBegin();iter!=locations.constEnd();++iter)
865 {
866 if (iter.value().isUserLocation)
867 {
868 outstream << iter.value().serializeToLine() << '\n';
869 }
870 }
871
872 sourcefile.close();
873 return true;
874 }
875
876 // lookup location from IP address.
locationFromIP()877 void StelLocationMgr::locationFromIP()
878 {
879 QSettings* conf = StelApp::getInstance().getSettings();
880 QNetworkRequest req( QUrl( conf->value("main/geoip_api_url", "https://freegeoip.stellarium.org/json/").toString() ) );
881 req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
882 req.setRawHeader("User-Agent", StelUtils::getUserAgentString().toLatin1());
883 QNetworkReply* networkReply=StelApp::getInstance().getNetworkAccessManager()->get(req);
884 connect(networkReply, SIGNAL(finished()), this, SLOT(changeLocationFromNetworkLookup()));
885 }
886
887 #ifdef ENABLE_GPS
locationFromGPS(int interval)888 void StelLocationMgr::locationFromGPS(int interval)
889 {
890 bool verbose=qApp->property("verbose").toBool();
891
892 #ifdef ENABLE_LIBGPS
893 if(!libGpsHelper)
894 {
895 libGpsHelper = new LibGPSLookupHelper(this);
896 connect(libGpsHelper, SIGNAL(queryFinished(StelLocation)), this, SLOT(changeLocationFromGPSQuery(StelLocation)));
897 connect(libGpsHelper, SIGNAL(queryError(QString)), this, SLOT(gpsQueryError(QString)));
898 }
899 if(libGpsHelper->isReady())
900 {
901 if (interval<0)
902 libGpsHelper->query();
903 else
904 {
905 libGpsHelper->setPeriodicQuery(interval);
906 // It seemed possible to leave the LibGPShelper object alive once created, because it does not block
907 // access to the GPS device. However, under pathological circumstances (start/stop of GPSD daemon
908 // after first query and while Stellarium is running, annoying GPSD in other ways, etc.,
909 // the LibGPSLookupHelper may signal ready but still shows problems.
910 // It seems better to also destroy it after finish of queries here and in case of non-readiness below.
911 if (interval==0)
912 {
913 if (verbose)
914 qDebug() << "Deactivating and deleting LibGPShelper...";
915 delete libGpsHelper;
916 libGpsHelper=Q_NULLPTR;
917 emit gpsQueryFinished(true); // signal "successful operation", avoid showing any error in GUI.
918 if (verbose)
919 qDebug() << "Deactivating and deleting LibGPShelper... DONE";
920 }
921 }
922 return;
923 }
924 else
925 {
926 qDebug() << "LibGPSHelper not ready. Attempting a direct NMEA connection instead.";
927 delete libGpsHelper;
928 libGpsHelper=Q_NULLPTR;
929 }
930 #endif
931 if(!nmeaHelper)
932 {
933 if (verbose)
934 qDebug() << "Creating new NMEAhelper...";
935 nmeaHelper = new NMEALookupHelper(this);
936 connect(nmeaHelper, SIGNAL(queryFinished(StelLocation)), this, SLOT(changeLocationFromGPSQuery(StelLocation)));
937 connect(nmeaHelper, SIGNAL(queryError(QString)), this, SLOT(gpsQueryError(QString)));
938 if (verbose)
939 qDebug() << "Creating new NMEAhelper...done";
940 }
941 if(nmeaHelper->isReady())
942 {
943 if (interval<0)
944 nmeaHelper->query();
945 else
946 {
947 nmeaHelper->setPeriodicQuery(interval);
948 if (interval==0)
949 {
950 if (verbose)
951 qDebug() << "Deactivating and deleting NMEAhelper...";
952 delete nmeaHelper;
953 nmeaHelper=Q_NULLPTR;
954 emit gpsQueryFinished(true); // signal "successful operation", avoid showing any error in GUI.
955 if (verbose)
956 qDebug() << "Deactivating and deleting NMEAhelper... DONE";
957 }
958 }
959 }
960 else
961 {
962 // something went wrong. However, a dysfunctional nmeaHelper may still exist, better delete it.
963 if (verbose)
964 qDebug() << "nmeaHelper not ready. Something went wrong.";
965 delete nmeaHelper;
966 nmeaHelper=Q_NULLPTR;
967 emit gpsQueryFinished(false);
968 }
969 }
970
changeLocationFromGPSQuery(const StelLocation & loc)971 void StelLocationMgr::changeLocationFromGPSQuery(const StelLocation &loc)
972 {
973 bool verbose=qApp->property("verbose").toBool();
974
975 StelApp::getInstance().getCore()->moveObserverTo(loc, 0.0, 0.0);
976 if (nmeaHelper)
977 {
978 if (verbose)
979 qDebug() << "Change location from NMEA... successful. NMEAhelper stays active.";
980 }
981 if (verbose)
982 qDebug() << "queryOK, resetting GUI";
983 emit gpsQueryFinished(true);
984 }
985
gpsQueryError(const QString & err)986 void StelLocationMgr::gpsQueryError(const QString &err)
987 {
988 qWarning()<<err;
989 if (nmeaHelper)
990 {
991 nmeaHelper->setPeriodicQuery(0); // stop queries if they came periodically.
992 //qDebug() << "Would Close nmeaHelper during error...";
993 // We should close the serial line to let other programs use the GPS device. (Not needed for the GPSD solution!)
994 //delete nmeaHelper;
995 //nmeaHelper=Q_NULLPTR;
996 //qDebug() << "Would Close nmeaHelper during error.....successful";
997 }
998 qDebug() << "GPS queryError, resetting GUI";
999 emit gpsQueryFinished(false);
1000 }
1001 #endif
1002
1003 // slot that receives IP-based location data from the network.
changeLocationFromNetworkLookup()1004 void StelLocationMgr::changeLocationFromNetworkLookup()
1005 {
1006 StelCore *core=StelApp::getInstance().getCore();
1007 QNetworkReply* networkReply = qobject_cast<QNetworkReply*>(sender());
1008 if (!networkReply)
1009 return;
1010
1011 if (networkReply->error() == QNetworkReply::NoError && networkReply->bytesAvailable()>0)
1012 {
1013 // success
1014 try
1015 {
1016 QVariantMap locMap = StelJsonParser::parse(networkReply->readAll()).toMap();
1017
1018 QString ipRegion = locMap.value("region_name").toString();
1019 if (ipRegion.isEmpty())
1020 ipRegion = locMap.value("region").toString();
1021 QString ipCity = locMap.value("city").toString();
1022 QString ipCountry = locMap.value("country_name").toString(); // NOTE: Got a short name of country
1023 QString ipCountryCode = locMap.value("country_code").toString();
1024 QString ipTimeZone = locMap.value("time_zone").toString();
1025 if (ipTimeZone.isEmpty())
1026 ipTimeZone = locMap.value("timezone").toString();
1027 float latitude=locMap.value("latitude").toFloat();
1028 float longitude=locMap.value("longitude").toFloat();
1029
1030 qDebug() << "Got location" << QString("%1, %2, %3 (%4, %5; %6)").arg(ipCity).arg(ipRegion).arg(ipCountry).arg(latitude).arg(longitude).arg(ipTimeZone) << "for IP" << locMap.value("ip").toString();
1031
1032 StelLocation loc;
1033 loc.name = (ipCity.isEmpty() ? QString("%1, %2").arg(latitude).arg(longitude) : ipCity);
1034 loc.state = (ipRegion.isEmpty() ? "IPregion" : ipRegion);
1035 loc.region = pickRegionFromCountryCode(ipCountryCode.isEmpty() ? "" : ipCountryCode.toLower());
1036 loc.role = QChar(0x0058); // char 'X'
1037 loc.population = 0;
1038 loc.latitude = latitude;
1039 loc.longitude = longitude;
1040 loc.altitude = 0;
1041 loc.bortleScaleIndex = StelLocation::DEFAULT_BORTLE_SCALE_INDEX;
1042 loc.ianaTimeZone = (ipTimeZone.isEmpty() ? "" : ipTimeZone);
1043 loc.planetName = "Earth";
1044 loc.landscapeKey = "";
1045
1046 // Ensure that ipTimeZone is a valid IANA timezone name!
1047 QTimeZone ipTZ(ipTimeZone.toUtf8());
1048 core->setCurrentTimeZone( !ipTZ.isValid() || ipTimeZone.isEmpty() ? "LMST" : ipTimeZone);
1049 core->moveObserverTo(loc, 0.0, 0.0);
1050 QSettings* conf = StelApp::getInstance().getSettings();
1051 conf->setValue("init_location/last_location", QString("%1, %2").arg(latitude).arg(longitude));
1052 }
1053 catch (const std::exception& e)
1054 {
1055 qDebug() << "Failure getting IP-based location: answer is in not acceptable format! Error: " << e.what()
1056 << "\nLet's use Paris, France as default location...";
1057 core->moveObserverTo(getLastResortLocation(), 0.0, 0.0); // Answer is not in JSON format! A possible block by DNS server or firewall
1058 }
1059 }
1060 else
1061 {
1062 qDebug() << "Failure getting IP-based location: \n\t" << networkReply->errorString();
1063 // If there is a problem, this must not change to some other location! Just ignore.
1064 }
1065 networkReply->deleteLater();
1066 }
1067
pickLocationsNearby(const QString planetName,const float longitude,const float latitude,const float radiusDegrees)1068 LocationMap StelLocationMgr::pickLocationsNearby(const QString planetName, const float longitude, const float latitude, const float radiusDegrees)
1069 {
1070 QMap<QString, StelLocation> results;
1071 QMapIterator<QString, StelLocation> iter(locations);
1072 while (iter.hasNext())
1073 {
1074 iter.next();
1075 const StelLocation *loc=&iter.value();
1076 if ( (loc->planetName == planetName) &&
1077 (StelLocation::distanceDegrees(longitude, latitude, loc->longitude, loc->latitude) <= radiusDegrees) )
1078 {
1079 results.insert(iter.key(), iter.value());
1080 }
1081 }
1082 return results;
1083 }
1084
pickLocationsInRegion(const QString region)1085 LocationMap StelLocationMgr::pickLocationsInRegion(const QString region)
1086 {
1087 QMap<QString, StelLocation> results;
1088 QMapIterator<QString, StelLocation> iter(locations);
1089 while (iter.hasNext())
1090 {
1091 iter.next();
1092 const StelLocation *loc=&iter.value();
1093 if (loc->region == region)
1094 {
1095 results.insert(iter.key(), iter.value());
1096 }
1097 }
1098 return results;
1099 }
1100
loadCountries()1101 void StelLocationMgr::loadCountries()
1102 {
1103 // Load ISO 3166-1 two-letter country codes from file
1104 // The format is "[code][tab][country name containing spaces][newline]"
1105 countryNameToCodeMap.clear();
1106 QFile textFile(StelFileMgr::findFile("data/iso3166.tab"));
1107 if(textFile.open(QFile::ReadOnly | QFile::Text))
1108 {
1109 QString line;
1110 int readOk=0;
1111 while(!textFile.atEnd())
1112 {
1113 line = QString::fromUtf8(textFile.readLine());
1114 if (line.startsWith("//") || line.startsWith("#") || line.isEmpty())
1115 continue;
1116
1117 if (!line.isEmpty())
1118 {
1119 #if (QT_VERSION>=QT_VERSION_CHECK(5, 14, 0))
1120 QStringList list=line.split("\t", Qt::KeepEmptyParts);
1121 #else
1122 QStringList list=line.split("\t", QString::KeepEmptyParts);
1123 #endif
1124 QString code = list.at(0).trimmed().toLower();
1125 QString country = list.at(1).trimmed().replace("&", "and");
1126 countryNameToCodeMap.insert(country, code);
1127 readOk++;
1128 }
1129 }
1130 textFile.close();
1131 if (readOk>0)
1132 qDebug() << "Loaded" << readOk << "countries";
1133 else
1134 qDebug() << "ERROR: List of countries was not loaded!";
1135 }
1136 // aliases for some countries to backward compatibility
1137 countryNameToCodeMap.insert("Russian Federation", "ru");
1138 countryNameToCodeMap.insert("Taiwan (Provice of China)", "tw");
1139 }
1140
loadRegions()1141 void StelLocationMgr::loadRegions()
1142 {
1143 QFile geoFile(StelFileMgr::findFile("data/regions-geoscheme.tab"));
1144 if(geoFile.open(QFile::ReadOnly | QFile::Text))
1145 {
1146 QString line;
1147 int readOk=0;
1148 regions.clear();
1149 countryCodeToRegionMap.clear();
1150 while(!geoFile.atEnd())
1151 {
1152 line = QString::fromUtf8(geoFile.readLine());
1153 if (line.startsWith("//") || line.startsWith("#") || line.isEmpty())
1154 continue;
1155
1156 if (!line.isEmpty())
1157 {
1158 #if (QT_VERSION>=QT_VERSION_CHECK(5, 14, 0))
1159 QStringList list=line.split("\t", Qt::KeepEmptyParts);
1160 #else
1161 QStringList list=line.split("\t", QString::KeepEmptyParts);
1162 #endif
1163
1164 QString regionName = list.at(2).trimmed();
1165 QString countries;
1166 if (list.size()>3)
1167 countries = list.at(3).trimmed().toLower();
1168 GeoRegion region;
1169 region.code = list.at(0).trimmed().toInt();
1170 region.planet = list.at(1).trimmed();
1171 region.regionName = regionName;
1172 region.countries = countries;
1173
1174 if (!countries.isEmpty())
1175 {
1176 #if (QT_VERSION>=QT_VERSION_CHECK(5, 14, 0))
1177 QStringList country=countries.split(",", Qt::KeepEmptyParts);
1178 #else
1179 QStringList country=countries.split(",", QString::KeepEmptyParts);
1180 #endif
1181 for (int i = 0; i<country.size(); i++)
1182 {
1183 countryCodeToRegionMap.insert(country.at(i), regionName);
1184 }
1185 }
1186
1187 regions.push_back(region);
1188 readOk++;
1189 }
1190 }
1191 geoFile.close();
1192 if (readOk>0)
1193 qDebug() << "Loaded" << readOk << "regions";
1194 else
1195 qDebug() << "ERROR: List of regions was not loaded!";
1196 }
1197 }
1198
getRegionNames(const QString & planet) const1199 QStringList StelLocationMgr::getRegionNames(const QString& planet) const
1200 {
1201 QStringList allregions;
1202 if (planet.isEmpty())
1203 {
1204 for (int i=0;i<regions.size();i++)
1205 allregions.append(regions.at(i).regionName);
1206 }
1207 else
1208 {
1209 for (int i=0;i<regions.size();i++)
1210 {
1211 if (planet.contains(regions.at(i).planet, Qt::CaseInsensitive) && !planet.contains("Observer", Qt::CaseInsensitive))
1212 allregions.append(regions.at(i).regionName);
1213 }
1214 }
1215
1216 return allregions;
1217 }
1218
pickRegionFromCountryCode(const QString countryCode)1219 QString StelLocationMgr::pickRegionFromCountryCode(const QString countryCode)
1220 {
1221 QMap<QString, QString>::ConstIterator i = countryCodeToRegionMap.find(countryCode);
1222 return (i!=countryCodeToRegionMap.constEnd()) ? i.value() : QString();
1223 }
1224
pickRegionFromCountry(const QString country)1225 QString StelLocationMgr::pickRegionFromCountry(const QString country)
1226 {
1227 QMap<QString, QString>::ConstIterator i = countryNameToCodeMap.find(country);
1228 QString code = (i!=countryNameToCodeMap.constEnd()) ? i.value() : QString();
1229 return pickRegionFromCountryCode(code);
1230 }
1231
pickRegionFromCode(int regionCode)1232 QString StelLocationMgr::pickRegionFromCode(int regionCode)
1233 {
1234 QString region;
1235 for(int i=0;i<regions.size();i++)
1236 {
1237 if (regions.at(i).code == regionCode)
1238 region = regions.at(i).regionName;
1239 }
1240 return region;
1241 }
1242
1243 // Check timezone string and return either the same or the corresponding string that we use in the Stellarium location database.
1244 // If timezone name starts with "UTC", always return unchanged.
1245 // This is required to store timezone names exactly as we know them, and not mix ours and current-iana spelling flavour.
1246 // In practice, reverse lookup to locationDBToIANAtranslations
sanitizeTimezoneStringForLocationDB(QString tzString)1247 QString StelLocationMgr::sanitizeTimezoneStringForLocationDB(QString tzString)
1248 {
1249 if (tzString.startsWith("UTC"))
1250 return tzString;
1251 QByteArray res=locationDBToIANAtranslations.key(tzString.toUtf8(), "---");
1252 if ( res != "---")
1253 return QString(res);
1254 return tzString;
1255 }
1256
1257 // Attempt to translate a timezone name from those used in Stellarium's location database to a name which is valid
1258 // as ckeckable by QTimeZone::availableTimeZoneIds(). That list may be updated anytime and is known to differ
1259 // between OSes. Some spellings may be different, or in some cases some names get simply translated to "UTC+HH:MM" style.
1260 // The empty string gets translated to "UTC".
sanitizeTimezoneStringFromLocationDB(QString dbString)1261 QString StelLocationMgr::sanitizeTimezoneStringFromLocationDB(QString dbString)
1262 {
1263 if (dbString.startsWith("UTC"))
1264 return dbString;
1265 // Maybe silences a debug later:
1266 if (dbString=="")
1267 return "UTC";
1268 QByteArray res=locationDBToIANAtranslations.value(dbString.toUtf8(), "---");
1269 if ( res != "---")
1270 return QString(res);
1271 return dbString;
1272 }
1273
getAllTimezoneNames() const1274 QStringList StelLocationMgr::getAllTimezoneNames() const
1275 {
1276 QStringList ret;
1277
1278 QMapIterator<QString, StelLocation> iter(locations);
1279 while (iter.hasNext())
1280 {
1281 iter.next();
1282 const StelLocation *loc=&iter.value();
1283 QString tz(loc->ianaTimeZone);
1284 if (!ret.contains(tz))
1285 ret.append(tz);
1286 }
1287 // 0.19: So far, this includes the existing names, but QTimeZone also has a few other names.
1288 // Accept others after testing against sanitized names, and especially all UT+/- names!
1289
1290 auto tzList = QTimeZone::availableTimeZoneIds(); // System dependent set of IANA timezone names.
1291 for (const auto& tz : qAsConst(tzList))
1292 {
1293 QString tzcand=sanitizeTimezoneStringFromLocationDB(tz); // try to find name as we use it in the program.
1294 if (!ret.contains(tzcand))
1295 {
1296 //qDebug() << "Extra insert Qt/IANA TZ entry from QTimeZone::availableTimeZoneIds(): " << tz << "as" << tzcand;
1297 ret.append(QString(tzcand));
1298 }
1299 }
1300
1301 // Special cases!
1302 ret.append("LMST");
1303 ret.append("LTST");
1304 ret.append("system_default");
1305 ret.sort();
1306 return ret;
1307 }
1308