1 /*
2  * Stellarium Scenery3d Plug-in
3  *
4  * Copyright (C) 2011 Simon Parzer, Peter Neubauer, Georg Zotti
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA  02110-1335, USA.
19  */
20 
21 #include "SceneInfo.hpp"
22 
23 #include "Scenery3d.hpp"
24 #include "StelApp.hpp"
25 #include "StelModuleMgr.hpp"
26 #include "StelFileMgr.hpp"
27 #include "StelIniParser.hpp"
28 #include "StelLocaleMgr.hpp"
29 #include "StelLocationMgr.hpp"
30 #include "VecMath.hpp"
31 
32 #include <QDebug>
33 #include <QDir>
34 #include <QSettings>
35 #include <QFileInfo>
36 
37 Q_LOGGING_CATEGORY(sceneInfo,"stel.plugin.scenery3d.sceneinfo")
38 Q_LOGGING_CATEGORY(storedView,"stel.plugin.scenery3d.storedview")
39 
40 const QString SceneInfo::SCENES_PATH("scenery3d/");
41 const QString StoredView::USERVIEWS_FILE = SceneInfo::SCENES_PATH + "userviews.ini";
42 QSettings* StoredView::userviews = Q_NULLPTR;
43 
44 int SceneInfo::metaTypeId = qRegisterMetaType<SceneInfo>();
45 
loadByID(const QString & id,SceneInfo & info)46 bool SceneInfo::loadByID(const QString &id,SceneInfo& info)
47 {
48 	qCDebug(sceneInfo)<<"Loading scene info for id"<<id;
49 	QString file = StelFileMgr::findFile(SCENES_PATH + id + "/scenery3d.ini", StelFileMgr::File);
50 	if(file.isEmpty())
51 	{
52 		qCCritical(sceneInfo)<<"scenery3d.ini file with id "<<id<<" does not exist!";
53 		return false;
54 	}
55 	//get full directory path
56 	QString path = QFileInfo(file).absolutePath();
57 	qCDebug(sceneInfo)<<"Found scene in"<<path;
58 
59 	QSettings ini(file,StelIniFormat);
60 	if (ini.status() != QSettings::NoError)
61 	{
62 	    qCCritical(sceneInfo) << "ERROR parsing scenery3d.ini file: " << file;
63 	    return false;
64 	}
65 
66 	//load QSettings file
67 	info.id = id;
68 	info.fullPath = path;
69 
70 	//primary description of the scene
71 	ini.beginGroup("model");
72 	info.name = ini.value("name").toString();
73 	info.author = ini.value("author").toString();
74 	info.description = ini.value("description","No description").toString();
75 	info.landscapeName = ini.value("landscape").toString();
76 	info.modelScenery = ini.value("scenery").toString();
77 	info.modelGround = ini.value("ground","").toString();
78 	info.vertexOrder = ini.value("obj_order","XYZ").toString();
79 
80 	info.vertexOrderEnum = StelOBJ::XYZ;
81 	if(!info.vertexOrder.compare("XYZ", Qt::CaseInsensitive)) info.vertexOrderEnum=StelOBJ::XYZ; // no change
82 	else if (!info.vertexOrder.compare("XZY", Qt::CaseInsensitive)) info.vertexOrderEnum=StelOBJ::XZY;
83 	else if (!info.vertexOrder.compare("YXZ", Qt::CaseInsensitive)) info.vertexOrderEnum=StelOBJ::YXZ;
84 	else if (!info.vertexOrder.compare("YZX", Qt::CaseInsensitive)) info.vertexOrderEnum=StelOBJ::YZX;
85 	else if (!info.vertexOrder.compare("ZXY", Qt::CaseInsensitive)) info.vertexOrderEnum=StelOBJ::ZXY;
86 	else if (!info.vertexOrder.compare("ZYX", Qt::CaseInsensitive)) info.vertexOrderEnum=StelOBJ::ZYX;
87 	else qCWarning(sceneInfo)<<"Invalid vertex order statement:"<<info.vertexOrder;
88 
89 	info.camNearZ = ini.value("camNearZ",0.3f).toFloat();
90 	info.camFarZ = ini.value("camFarZ",10000.0f).toFloat();
91 	info.shadowFarZ = ini.value("shadowDistance",info.camFarZ).toFloat();
92 	info.shadowSplitWeight = ini.value("shadowSplitWeight",-1.0f).toFloat();
93 
94 	//constrain shadow range to cam far z, makes no sense to extend it further
95 	if(info.shadowFarZ>info.camFarZ)
96 		info.shadowFarZ = info.camFarZ;
97 
98 	// In case we don't have an axis-aligned OBJ model, this is the chance to correct it.
99 	info.obj2gridMatrix = Mat4d::identity();
100 	if (ini.contains("obj2grid_trafo"))
101 	{
102 		QString str=ini.value("obj2grid_trafo").toString();
103 		QStringList strList=str.split(",");
104 		bool conversionOK[16];
105 		if (strList.length()==16)
106 		{
107 			info.obj2gridMatrix.set(strList.at(0).toDouble(&conversionOK[0]),
108 					strList.at(1).toDouble(&conversionOK[1]),
109 					strList.at(2).toDouble(&conversionOK[2]),
110 					strList.at(3).toDouble(&conversionOK[3]),
111 					strList.at(4).toDouble(&conversionOK[4]),
112 					strList.at(5).toDouble(&conversionOK[5]),
113 					strList.at(6).toDouble(&conversionOK[6]),
114 					strList.at(7).toDouble(&conversionOK[7]),
115 					strList.at(8).toDouble(&conversionOK[8]),
116 					strList.at(9).toDouble(&conversionOK[9]),
117 					strList.at(10).toDouble(&conversionOK[10]),
118 					strList.at(11).toDouble(&conversionOK[11]),
119 					strList.at(12).toDouble(&conversionOK[12]),
120 					strList.at(13).toDouble(&conversionOK[13]),
121 					strList.at(14).toDouble(&conversionOK[14]),
122 					strList.at(15).toDouble(&conversionOK[15])
123 					);
124 			for (int i=0; i<16; ++i)
125 			{
126 				if (!conversionOK[i]) qCWarning(sceneInfo) << "WARNING: scenery3d.ini: element " << i+1 << " of obj2grid_trafo invalid, set zo zero.";
127 			}
128 		}
129 		else qCWarning(sceneInfo) << "obj2grid_trafo invalid: not 16 comma-separated elements";
130 	}
131 	ini.endGroup();
132 
133 	//some importing/rendering params
134 	ini.beginGroup("general");
135 	info.transparencyThreshold = ini.value("transparency_threshold", 0.5f).toFloat();
136 	info.sceneryGenerateNormals = ini.value("scenery_generate_normals", false).toBool();
137 	info.groundGenerateNormals = ini.value("ground_generate_normals", false).toBool();
138 	ini.endGroup();
139 
140 	//load location data
141 	if(ini.childGroups().contains("location"))
142 	{
143 		ini.beginGroup("location");
144 		info.location.reset(new StelLocation());
145 		info.location->name = ini.value("name",info.name).toString();
146 		info.location->planetName = ini.value("planetName","Earth").toString();
147 
148 		if(ini.contains("altitude"))
149 		{
150 			QVariant val = ini.value("altitude");
151 			if(val == "from_model")
152 			{
153 				info.altitudeFromModel = true;
154 				//info.location->altitude = -32766;
155 			}
156 			else
157 			{
158 				info.altitudeFromModel = false;
159 				info.location->altitude = val.toInt();
160 			}
161 		}
162 
163 		if(ini.contains("latitude"))
164 			info.location->latitude = StelUtils::getDecAngle(ini.value("latitude").toString())*M_180_PI;
165 		if (ini.contains("longitude"))
166 			info.location->longitude = StelUtils::getDecAngle(ini.value("longitude").toString())*M_180_PI;
167 		if (ini.contains("country"))
168 			info.location->region = StelLocationMgr::pickRegionFromCountry(ini.value("country").toString());
169 		if (ini.contains("state"))
170 			info.location->state = ini.value("state").toString();
171 		info.location->ianaTimeZone = StelLocationMgr::sanitizeTimezoneStringFromLocationDB(ini.value("timezone", "LMST").toString());
172 
173 		info.location->landscapeKey = info.landscapeName;
174 		ini.endGroup();
175 	}
176 	else
177 		info.location.reset();
178 
179 	//load coord info
180 	ini.beginGroup("coord");
181 	info.gridName = ini.value("grid_name","Unspecified Coordinate Frame").toString();
182 	double orig_x = ini.value("orig_E", 0.0).toDouble();
183 	double orig_y = ini.value("orig_N", 0.0).toDouble();
184 	double orig_z = ini.value("orig_H", 0.0).toDouble();
185 	info.modelWorldOffset=Vec3d(orig_x, orig_y, orig_z); // RealworldGridCoords=objCoords+modelWorldOffset
186 
187 	// Find a rotation around vertical axis, most likely required by meridian convergence.
188 	double rot_z=0.0;
189 	QVariant convAngle = ini.value("convergence_angle",0.0);
190 	if (!convAngle.toString().compare("from_grid"))
191 	{ // compute rot_z from grid_meridian and location. Check their existence!
192 		if (ini.contains("grid_meridian"))
193 		{
194 			double gridCentralMeridian=StelUtils::getDecAngle(ini.value("grid_meridian").toString())*M_180_PI;
195 			if (!info.location.isNull())
196 			{
197 				// Formula from: http://en.wikipedia.org/wiki/Transverse_Mercator_projection, Convergence
198 				//rot_z=std::atan(std::tan((lng-gridCentralMeridian)*M_PI/180.)*std::sin(lat*M_PI/180.));
199 				// or from http://de.wikipedia.org/wiki/Meridiankonvergenz
200 				rot_z=(info.location->longitude - gridCentralMeridian)*M_PI_180*std::sin(info.location->latitude*M_PI_180);
201 
202 				qCDebug(sceneInfo) << "With Longitude " << info.location->longitude
203 					 << ", Latitude " << info.location->latitude << " and CM="
204 					 << gridCentralMeridian << ", ";
205 				qCDebug(sceneInfo) << "setting meridian convergence to " << rot_z*M_180_PI << "degrees";
206 			}
207 			else
208 			{
209 				qCWarning(sceneInfo) << "scenery3d.ini: Convergence angle \"from_grid\" requires location section!";
210 			}
211 		}
212 		else
213 		{
214 			qCWarning(sceneInfo) << "scenery3d.ini: Convergence angle \"from_grid\": cannot compute without grid_meridian!";
215 		}
216 	}
217 	else
218 	{
219 		rot_z = convAngle.toDouble() * M_PI_180;
220 	}
221 	// We must apply also a 90 degree rotation, plus convergence(rot_z)
222 
223 	// Meridian Convergence is negative in north-west quadrant.
224 	// positive MC means True north is "left" of grid north, and model must be rotated clockwise. E.g. Sterngarten (east of UTM CM +15deg) has +0.93, we must rotate clockwise!
225 	// A zRotate rotates counterclockwise, so we must reverse rot_z.
226 	info.zRotateMatrix = Mat4d::zrotation(M_PI_2 - rot_z);
227 
228 	// At last, find start points.
229 	if(ini.contains("start_E") && ini.contains("start_N"))
230 	{
231 		info.startPositionFromModel = false;
232 		info.startWorldOffset[0] = ini.value("start_E").toDouble();
233 		info.startWorldOffset[1] = ini.value("start_N").toDouble();
234 		//FS this is not really used anymore, i think
235 		info.startWorldOffset[2] = ini.value("start_H",0.0).toDouble();
236 	}
237 	else
238 	{
239 		info.startPositionFromModel = true;
240 	}
241 	info.eyeLevel=ini.value("start_Eye", 1.65).toDouble();
242 
243 	//calc pos in model coords
244 	info.relativeStartPosition = info.startWorldOffset - info.modelWorldOffset;
245 	// I love code without comments
246 	info.relativeStartPosition[1]*=-1.0;
247 	info.relativeStartPosition = info.zRotateMatrix.inverse() * info.relativeStartPosition;
248 	info.relativeStartPosition[1]*=-1.0;
249 
250 	if(ini.contains("zero_ground_height"))
251 	{
252 		info.groundNullHeightFromModel=false;
253 		info.groundNullHeight = ini.value("zero_ground_height").toDouble();
254 	}
255 	else
256 	{
257 		info.groundNullHeightFromModel=true;
258 		info.groundNullHeight=0.;
259 	}
260 
261 	if (ini.contains("start_az_alt_fov"))
262 	{
263 		qCDebug(sceneInfo) << "scenery3d.ini: setting initial dir/fov.";
264 		info.lookAt_fov=Vec3f(ini.value("start_az_alt_fov").toString());
265 		info.lookAt_fov[0]=180.0f-info.lookAt_fov[0]; // fix azimuth
266 	}
267 	else
268 	{
269 		info.lookAt_fov=Vec3f(0.f, 0.f, -1000.f);
270 		qCDebug(sceneInfo) << "scenery3d.ini: No initial dir/fov given.";
271 	}
272 	ini.endGroup();
273 
274 	info.isValid = true;
275 	return true;
276 }
277 
getLocalizedHTMLDescription() const278 QString SceneInfo::getLocalizedHTMLDescription() const
279 {
280 	if(!this->isValid)
281 		return QString();
282 
283 	//This is taken from ViewDialog.cpp
284 	QString lang = StelApp::getInstance().getLocaleMgr().getAppLanguage();
285 	if (!QString("pt_BR zh_CN zh_HK zh_TW").contains(lang))
286 	{
287 		lang = lang.split("_").at(0);
288 	}
289 
290 	QString descFile = StelFileMgr::findFile( fullPath + "/description."+lang+".utf8");
291 	if(descFile.isEmpty())
292 	{
293 		//fall back to english
294 		qCWarning(sceneInfo)<<"No scene description found for language"<<lang<<", falling back to english";
295 		descFile = StelFileMgr::findFile( fullPath + "/description.en.utf8");
296 	}
297 
298 	if(descFile.isEmpty())
299 	{
300 		//fall back to stored description
301 		qCWarning(sceneInfo)<<"No external scene description found";
302 		return QString();
303 	}
304 
305 	//load the whole file and return it as string
306 	QFile f(descFile);
307 	QString htmlFile;
308 	if(f.open(QIODevice::ReadOnly))
309 	{
310 		htmlFile = QString::fromUtf8(f.readAll());
311 		f.close();
312 	}
313 
314 	return htmlFile;
315 }
316 
getIDFromName(const QString & name)317 QString SceneInfo::getIDFromName(const QString &name)
318 {
319 	QMap<QString, QString> nameToDirMap = getNameToIDMap();
320 
321 	return nameToDirMap.value(name);
322 }
323 
loadByName(const QString & name,SceneInfo & info)324 bool SceneInfo::loadByName(const QString &name, SceneInfo &info)
325 {
326 	QString id = getIDFromName(name);
327 	if(!id.isEmpty())
328 		return loadByID(id,info);
329 	else
330 	{
331 	    qCWarning(sceneInfo) << "Can't find a 3D scenery with name=" << name;
332 	    return false;
333 	}
334 }
335 
getAllSceneIDs()336 QStringList SceneInfo::getAllSceneIDs()
337 {
338 	QMap<QString,QString> nameToDirMap = getNameToIDMap();
339 	QStringList result;
340 
341 	// We just look over the map of names to IDs and extract the values
342 	for (auto i : nameToDirMap.values())
343 	{
344 		result += i;
345 	}
346 	return result;
347 }
348 
getAllSceneNames()349 QStringList SceneInfo::getAllSceneNames()
350 {
351 	QMap<QString,QString> nameToDirMap = getNameToIDMap();
352 	QStringList result;
353 
354 	// We just look over the map of names to IDs and extract the keys
355 	for (auto i : nameToDirMap.keys())
356 	{
357 		result += i;
358 	}
359 	return result;
360 }
361 
getNameToIDMap()362 QMap<QString, QString> SceneInfo::getNameToIDMap()
363 {
364 	QSet<QString> scenery3dDirs;
365 	QMap<QString, QString> result;
366 
367 	scenery3dDirs = StelFileMgr::listContents(SceneInfo::SCENES_PATH, StelFileMgr::Directory);
368 
369 	for (const auto& dir : scenery3dDirs)
370 	{
371 		QSettings scenery3dIni(StelFileMgr::findFile(SceneInfo::SCENES_PATH + dir + "/scenery3d.ini"), StelIniFormat);
372 		QString k = scenery3dIni.value("model/name").toString();
373 		result[k] = dir;
374 	}
375 	return result;
376 }
377 
getGlobalViewsForScene(const SceneInfo & scene)378 StoredViewList StoredView::getGlobalViewsForScene(const SceneInfo &scene)
379 {
380 	StoredViewList ret;
381 
382 	//return empty
383 	if(!scene.isValid)
384 		return ret;
385 
386 	//load global viewpoints
387 	QFileInfo globalfile( QDir(scene.fullPath), "viewpoints.ini");
388 
389 	if(!globalfile.isFile())
390 	{
391 		qCWarning(storedView)<<globalfile.absoluteFilePath()<<" is not a file";
392 	}
393 	else
394 	{
395 		QSettings ini(globalfile.absoluteFilePath(),StelIniFormat);
396 		if (ini.status() != QSettings::NoError)
397 		{
398 			qCWarning(storedView) << "Error reading global viewpoint file " << globalfile.absoluteFilePath();
399 		}
400 		else
401 		{
402 			int size = ini.beginReadArray("StoredViews");
403 			readArray(ini,ret,size,true);
404 			ini.endArray();
405 		}
406 	}
407 
408 	return ret;
409 }
410 
getUserViewsForScene(const SceneInfo & scene)411 StoredViewList StoredView::getUserViewsForScene(const SceneInfo &scene)
412 {
413 	StoredViewList ret;
414 
415 	//return empty
416 	if(!scene.isValid)
417 		return ret;
418 
419 	QSettings* ini = getUserViews();
420 	if (ini->status() != QSettings::NoError)
421 	{
422 		qCWarning(storedView) << "Error reading user viewpoint file";
423 	}
424 	else
425 	{
426 		int size = ini->beginReadArray(scene.id);
427 		readArray(*ini,ret,size,false);
428 		ini->endArray();
429 	}
430 
431 	return ret;
432 }
433 
saveUserViews(const SceneInfo & scene,const StoredViewList & list)434 void StoredView::saveUserViews(const SceneInfo &scene, const StoredViewList &list)
435 {
436 	//this should never happen
437 	Q_ASSERT(scene.isValid);
438 
439 	QSettings* ini = getUserViews();
440 
441 	if (ini->status() != QSettings::NoError)
442 	{
443 		qCWarning(storedView) << "Error reading user viewpoint file";
444 	}
445 	else
446 	{
447 		//clear old array
448 		ini->remove(scene.id);
449 
450 		ini->beginWriteArray(scene.id,list.size());
451 		//add data
452 		writeArray(*ini,list);
453 		ini->endArray();
454 
455 		//explicit flushing here, may not be necessary
456 		ini->sync();
457 	}
458 }
459 
readArray(QSettings & ini,StoredViewList & list,int size,bool isGlobal)460 void StoredView::readArray(QSettings &ini, StoredViewList &list, int size, bool isGlobal)
461 {
462 	for(int i =0;i<size;++i)
463 	{
464 		ini.setArrayIndex(i);
465 
466 		StoredView sv;
467 		sv.isGlobal = isGlobal;
468 		sv.label = ini.value("label").toString();
469 		sv.description = ini.value("description").toString();
470 		sv.position = Vec4d(ini.value("position").toString());
471 		sv.view_fov = Vec3f(ini.value("view_fov").toString());
472 		if (ini.contains("JD"))
473 		{
474 			sv.jdIsRelevant=true;
475 			sv.jd=ini.value("JD").toDouble();
476 		}
477 		else
478 			sv.jdIsRelevant=false;
479 
480 		list.append(sv);
481 	}
482 }
483 
writeArray(QSettings & ini,const StoredViewList & list)484 void StoredView::writeArray(QSettings &ini, const StoredViewList &list)
485 {
486 	for(int i =0;i<list.size();++i)
487 	{
488 		const StoredView& view = list.at(i);
489 		ini.setArrayIndex(i);
490 
491 		ini.setValue("label", view.label);
492 		ini.setValue("description", view.description);
493 		ini.setValue("position", view.position.toStr());
494 		ini.setValue("view_fov", view.view_fov.toStr());
495 		if (view.jdIsRelevant)
496 			ini.setValue("JD", (view.jd));
497 	}
498 }
499 
getUserViews()500 QSettings* StoredView::getUserViews()
501 {
502 	if(userviews)
503 		return userviews;
504 
505 	//try to find an writable location
506 	QString file = StelFileMgr::findFile(USERVIEWS_FILE, StelFileMgr::Flags(StelFileMgr::Writable|StelFileMgr::File));
507 	if (file.isEmpty())
508 	{
509 		//make sure the new file goes into user dir
510 		file = StelFileMgr::getUserDir() + "/" + USERVIEWS_FILE;
511 	}
512 
513 	if(!StelFileMgr::exists(file))
514 	{
515 		//create an empty file, or QSettings may complain
516 		//is this somehow easier to do in Qt?
517 		QFileInfo f(file);
518 		QDir().mkpath(f.absolutePath());
519 		QFile qfile(file);
520 		if (!qfile.open(QIODevice::WriteOnly))
521 		{
522 			qCWarning(storedView) << "StoredView: cannot create userviews file!";
523 		}
524 		qfile.close();
525 	}
526 
527 	//QSettings gets deleted when plugin is shut down (also saves settings)
528 	//TODO StelIniFormat has bugs with saving HTML! so we use the default Qt format here, no idea if this may cause some problems.
529 	userviews = new QSettings(file,QSettings::IniFormat,GETSTELMODULE(Scenery3d));
530 	return userviews;
531 }
532