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