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