1 /*
2 * Stellarium
3 * Copyright (C) 2003 Fabien Chereau
4 * Copyright (C) 2011 Bogdan Marinov
5 * Copyright (C) 2014-17 Georg Zotti
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
20 */
21
22 #include "Landscape.hpp"
23 #include "StelApp.hpp"
24 #include "StelTextureMgr.hpp"
25 #include "StelFileMgr.hpp"
26 #include "StelIniParser.hpp"
27 #include "StelLocation.hpp"
28 #include "StelLocationMgr.hpp"
29 #include "StelCore.hpp"
30 #include "StelPainter.hpp"
31 #include "StelLocaleMgr.hpp"
32 #include "StelModuleMgr.hpp"
33 #include "LandscapeMgr.hpp"
34
35 #include <QDebug>
36 #include <QSettings>
37 #include <QVarLengthArray>
38 #include <QFile>
39 #include <QDir>
40 #include <QtAlgorithms>
41 #include <QRegularExpression>
42
Landscape(float _radius)43 Landscape::Landscape(float _radius)
44 : radius(static_cast<double>(_radius))
45 , id("uninitialized")
46 , minBrightness(-1.)
47 , landscapeBrightness(1.)
48 , lightScapeBrightness(0.)
49 , validLandscape(false)
50 , rows(20)
51 , cols(40)
52 , angleRotateZ(0.)
53 , angleRotateZOffset(0.)
54 , sinMinAltitudeLimit(-0.035) //sin(-2 degrees))
55 , defaultBortleIndex(-1)
56 , defaultFogSetting(-1)
57 , defaultExtinctionCoefficient(-1.)
58 , defaultTemperature(-1000.)
59 , defaultPressure(-2.)
60 , horizonPolygon(Q_NULLPTR)
61 , fontSize(18)
62 , memorySize(sizeof(Landscape))
63 {
64 }
65
~Landscape()66 Landscape::~Landscape()
67 {}
68
69
70 // Load attributes common to all landscapes
loadCommon(const QSettings & landscapeIni,const QString & landscapeId)71 void Landscape::loadCommon(const QSettings& landscapeIni, const QString& landscapeId)
72 {
73 id = landscapeId;
74 name = landscapeIni.value("landscape/name").toString();
75 author = landscapeIni.value("landscape/author").toString();
76 description = landscapeIni.value("landscape/description").toString();
77 description = description.replace(QRegularExpression("\\\\n\\s*\\\\n"), "<br />");
78 description = description.replace("\\n", " ");
79 if (name.isEmpty())
80 {
81 qWarning() << "No valid landscape definition (no name) found for landscape ID "
82 << landscapeId << ". No landscape in use." << StelUtils::getEndLineChar();
83 validLandscape = false;
84 return;
85 }
86 else
87 {
88 validLandscape = true;
89 }
90
91 // Optional data
92 rows = landscapeIni.value("landscape/tesselate_rows", 20).toUInt();
93 cols = landscapeIni.value("landscape/tesselate_cols", 40).toUInt();
94
95 if (landscapeIni.childGroups().contains("location"))
96 {
97 if (landscapeIni.contains("location/planet"))
98 location.planetName = landscapeIni.value("location/planet").toString();
99 else
100 location.planetName = "Earth";
101 // Tolerate decimal values in .ini file, but round to nearest integer
102 if (landscapeIni.contains("location/altitude"))
103 location.altitude = qRound(landscapeIni.value("location/altitude").toDouble());
104 if (landscapeIni.contains("location/latitude"))
105 location.latitude = static_cast<float>(StelUtils::getDecAngle(landscapeIni.value("location/latitude").toString())*M_180_PI);
106 if (landscapeIni.contains("location/longitude"))
107 location.longitude = static_cast<float>(StelUtils::getDecAngle(landscapeIni.value("location/longitude").toString())*M_180_PI);
108 if (landscapeIni.contains("location/country"))
109 location.region = StelLocationMgr::pickRegionFromCountry(landscapeIni.value("location/country").toString());
110 if (landscapeIni.contains("location/state"))
111 location.state = landscapeIni.value("location/state").toString();
112 if (landscapeIni.contains("location/name"))
113 location.name = landscapeIni.value("location/name").toString();
114 else
115 location.name = name;
116 location.landscapeKey = name;
117
118 QString tzString=landscapeIni.value("location/timezone", "").toString();
119 if ((tzString.length() > 0))
120 location.ianaTimeZone=StelLocationMgr::sanitizeTimezoneStringFromLocationDB(tzString);
121
122 defaultBortleIndex = landscapeIni.value("location/light_pollution", -1).toInt();
123 if (defaultBortleIndex<=0) defaultBortleIndex=-1; // neg. values in ini file signal "no change".
124 if (defaultBortleIndex>9) defaultBortleIndex=9; // correct bad values.
125
126 defaultFogSetting = landscapeIni.value("location/display_fog", -1).toInt();
127 defaultExtinctionCoefficient = landscapeIni.value("location/atmospheric_extinction_coefficient", -1.0).toDouble();
128 defaultTemperature = landscapeIni.value("location/atmospheric_temperature", -1000.0).toDouble();
129 defaultPressure = landscapeIni.value("location/atmospheric_pressure", -2.0).toDouble(); // -2=no change! [-1=computeFromAltitude]
130 }
131
132 // Set minimal brightness for landscape
133 minBrightness = landscapeIni.value("landscape/minimal_brightness", -1.0f).toFloat();
134
135 // set a minimal altitude which the landscape covers. (new in 0.14)
136 // This is to allow landscapes with "holes" in the ground (space station?) (Bug lp:1469407)
137 sinMinAltitudeLimit = std::sin(M_PI_180 * landscapeIni.value("landscape/minimal_altitude", -2.0).toDouble());
138
139 // This is now optional for all classes, for mixing with a photo horizon:
140 // they may have different offsets, like a south-centered pano and a grid-aligned polygon.
141 // In case they are aligned, we can use one value angle_rotatez, or define the polygon rotation individually.
142 if (landscapeIni.contains("landscape/polygonal_horizon_list"))
143 {
144 createPolygonalHorizon(
145 StelFileMgr::findFile("landscapes/" + landscapeId + "/" + landscapeIni.value("landscape/polygonal_horizon_list").toString()),
146 landscapeIni.value("landscape/polygonal_angle_rotatez", 0.f).toFloat(),
147 landscapeIni.value("landscape/polygonal_horizon_list_mode", "azDeg_altDeg").toString()
148 );
149 // This line can then be drawn in all classes with the color specified here. If not specified, don't draw it! (flagged by negative red)
150 horizonPolygonLineColor=Vec3f(landscapeIni.value("landscape/horizon_line_color", "-1,0,0" ).toString());
151 }
152 // we must get label color, this is global. (No sense to make that per-landscape!)
153 QSettings *config = StelApp::getInstance().getSettings();
154 labelColor=Vec3f(config->value("landscape/label_color", "0.2,0.8,0.2").toString());
155 fontSize=config->value("landscape/label_font_size", 18).toInt();
156 loadLabels(landscapeId);
157 }
158
createPolygonalHorizon(const QString & lineFileName,const float polyAngleRotateZ,const QString & listMode)159 void Landscape::createPolygonalHorizon(const QString& lineFileName, const float polyAngleRotateZ, const QString &listMode)
160 {
161 // qDebug() << _name << " " << _fullpath << " " << _lineFileName ;
162
163 const QStringList horizonModeList = { "azDeg_altDeg", "azDeg_zdDeg", "azRad_altRad", "azRad_zdRad", "azGrad_zdGrad", "azGrad_zdGrad"};
164 const horizonListMode coordMode=static_cast<horizonListMode>(horizonModeList.indexOf(listMode));
165
166 QVector<Vec3d> horiPoints(0);
167 QFile file(lineFileName);
168
169 if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
170 {
171 qWarning() << "Landscape Horizon line data file" << QDir::toNativeSeparators(lineFileName) << "not found.";
172 return;
173 }
174 QRegularExpression emptyLine("^\\s*$");
175 QTextStream in(&file);
176 while (!in.atEnd())
177 {
178 // Build list of vertices. The checks can certainly become more robust.
179 QString line = in.readLine();
180 if (line.length()==0) continue;
181 if (emptyLine.match(line).hasMatch()) continue;
182 if (line.at(0)=='#') continue; // skip comment lines.
183 const QStringList list = line.trimmed().split(QRegularExpression("\\s+"));
184 if (list.count() < 2)
185 {
186 qWarning() << "Landscape polygon file" << QDir::toNativeSeparators(lineFileName) << "has bad line:" << line << "with" << list.count() << "elements";
187 continue;
188 }
189 //if (list.count() > 2) // use first two elements but give warning.
190 //{
191 // qWarning() << "Landscape polygon file" << QDir::toNativeSeparators(lineFileName) << "has excessive elements in line:" << line << " (" << list.count() << ", not 2 elements)";
192 //}
193 Vec3d point;
194 //qDebug() << "Creating point for az=" << list.at(0) << " alt/zd=" << list.at(1);
195 float az = 0.f, alt = 0.f;
196
197 switch (coordMode)
198 {
199 case azDeg_altDeg:
200 az=(180.0f - polyAngleRotateZ - list.at(0).toFloat())*M_PI_180f;
201 alt=list.at(1).toFloat()*M_PI_180f;
202 break;
203 case azDeg_zdDeg:
204 az=(180.0f - polyAngleRotateZ - list.at(0).toFloat())*M_PI_180f;
205 alt=(90.0f-list.at(1).toFloat())*M_PI_180f;
206 break;
207 case azRad_altRad:
208 az=(M_PIf - polyAngleRotateZ*M_PI_180f - list.at(0).toFloat());
209 alt=list.at(1).toFloat();
210 break;
211 case azRad_zdRad:
212 az=(M_PIf - polyAngleRotateZ*M_PI_180f - list.at(0).toFloat());
213 alt=M_PI_2f-list.at(1).toFloat();
214 break;
215 case azGrad_altGrad:
216 az=(200.0f - list.at(0).toFloat())*M_PIf/200.f - polyAngleRotateZ*M_PI_180f;
217 alt=list.at(1).toFloat()*M_PIf/200.f;
218 break;
219 case azGrad_zdGrad:
220 az=(200.0f - list.at(0).toFloat())*M_PIf/200.f - polyAngleRotateZ*M_PI_180f;
221 alt=(100.0f-list.at(1).toFloat())*M_PIf/200.f;
222 break;
223 case invalid:
224 qWarning() << "invalid polygonal_horizon_list_mode while reading horizon line.";
225 return;
226 }
227
228 StelUtils::spheToRect(az, alt, point);
229 if (horiPoints.isEmpty() || horiPoints.last() != point)
230 horiPoints.append(point);
231 }
232 file.close();
233 //horiPoints.append(horiPoints.at(0)); // close loop? Apparently not necessary.
234
235 //qDebug() << "created horiPoints with " << horiPoints.count() << "points:";
236 //for (int i=0; i<horiPoints.count(); ++i)
237 // qDebug() << horiPoints.at(i)[0] << "/" << horiPoints.at(i)[1] << "/" << horiPoints.at(i)[2] ;
238 AllSkySphericalRegion allskyRegion;
239 SphericalPolygon aboveHorizonPolygon(horiPoints);
240 horizonPolygon = allskyRegion.getSubtraction(aboveHorizonPolygon);
241
242 // If this now contains the zenith, invert the solution:
243 if (horizonPolygon->contains(Vec3d(0.,0.,1.)))
244 {
245 //qDebug() << "Must invert polygon vector!";
246 std::reverse(horiPoints.begin(), horiPoints.end());
247 AllSkySphericalRegion allskyRegion;
248 SphericalPolygon aboveHorizonPolygon(horiPoints);
249 horizonPolygon = allskyRegion.getSubtraction(aboveHorizonPolygon);
250 AllSkySphericalRegion allskyRegion2;
251 horizonPolygon = allskyRegion2.getSubtraction(horizonPolygon);
252 }
253 }
254
255 #include <iostream>
getTexturePath(const QString & basename,const QString & landscapeId)256 const QString Landscape::getTexturePath(const QString& basename, const QString& landscapeId)
257 {
258 // look in the landscape directory first, and if not found default to global textures directory
259 QString path = StelFileMgr::findFile("landscapes/" + landscapeId + "/" + basename);
260 if (path.isEmpty())
261 path = StelFileMgr::findFile("textures/" + basename);
262 if (path.isEmpty())
263 qWarning() << "Warning: Landscape" << landscapeId << ": File" << basename << "does not exist.";
264 return path;
265 }
266
267 // find optional file and fill landscapeLabels list.
loadLabels(const QString & landscapeId)268 void Landscape::loadLabels(const QString& landscapeId)
269 {
270 // in case we have labels and this is called for a retranslation, clean list first.
271 landscapeLabels.clear();
272
273 QString lang, descFileName, locLabelFileName, engLabelFileName;
274
275 lang = StelApp::getInstance().getLocaleMgr().getAppLanguage();
276 locLabelFileName = StelFileMgr::findFile("landscapes/" + landscapeId, StelFileMgr::Directory) + "/gazetteer." + lang + ".utf8";
277 engLabelFileName = StelFileMgr::findFile("landscapes/" + landscapeId, StelFileMgr::Directory) + "/gazetteer.en.utf8";
278
279 // Check the file with full name of locale
280 if (!QFileInfo(locLabelFileName).exists())
281 {
282 // File not found. What about short name of locale?
283 lang = lang.split("_").at(0);
284 locLabelFileName = StelFileMgr::findFile("landscapes/" + landscapeId, StelFileMgr::Directory) + "/gazetteer." + lang + ".utf8";
285 }
286
287 // Get localized or at least English description for landscape
288 if (QFileInfo(locLabelFileName).exists())
289 descFileName = locLabelFileName;
290 else if (QFileInfo(engLabelFileName).exists())
291 descFileName = engLabelFileName;
292 else
293 return;
294
295 // We have found some file now.
296 QFile file(descFileName);
297 if(file.open(QIODevice::ReadOnly | QIODevice::Text))
298 {
299 QTextStream in(&file);
300 in.setCodec("UTF-8");
301 while (!in.atEnd())
302 {
303 QString line=in.readLine();
304
305 // Skip comments and all-empty lines (space allowed and ignored)
306 if (line.startsWith('#') || line.trimmed().isEmpty() )
307 continue;
308 // Read entries, construct vectors, put in list.
309 const QStringList parts=line.split('|');
310 if (parts.count() != 5)
311 {
312 qWarning() << "Invalid line in landscape gazetteer" << descFileName << ":" << line;
313 continue;
314 }
315 LandscapeLabel newLabel;
316 newLabel.name=parts.at(4).trimmed();
317 StelUtils::spheToRect((180.0f-parts.at(0).toFloat()) *M_PI_180f, parts.at(1).toFloat()*M_PI_180f, newLabel.featurePoint);
318 StelUtils::spheToRect((180.0f-parts.at(0).toFloat() - parts.at(3).toFloat())*M_PI_180f, (parts.at(1).toFloat() + parts.at(2).toFloat())*M_PI_180f, newLabel.labelPoint);
319 landscapeLabels.append(newLabel);
320 //qDebug() << "Added landscape label " << newLabel.name;
321 }
322 file.close();
323 }
324 }
325
drawLabels(StelCore * core,StelPainter * painter)326 void Landscape::drawLabels(StelCore* core, StelPainter *painter)
327 {
328 if (landscapeLabels.length()==0) // no labels
329 return;
330 if (labelFader.getInterstate() < 0.0001f) // switched off
331 return;
332
333 // We must reset painter to pure altaz coordinates without pano-based rotation
334 const StelProjectorP prj = core->getProjection(StelCore::FrameAltAz, StelCore::RefractionOff);
335 painter->setProjector(prj);
336 QFont font;
337 font.setPixelSize(fontSize);
338 painter->setFont(font);
339 QFontMetrics fm(font);
340 painter->setColor(labelColor, labelFader.getInterstate()*landFader.getInterstate());
341
342 painter->setBlending(true);
343 painter->setLineSmooth(true);
344
345 for (int i = 0; i < landscapeLabels.size(); ++i)
346 {
347 // in case of gravityLabels, we cannot shift-adjust centered placename, sorry!
348 if (prj->getFlagGravityLabels())
349 {
350 painter->drawText(landscapeLabels.at(i).labelPoint, landscapeLabels.at(i).name, 0, 0, 0, false);
351 }
352 else
353 {
354 int textWidth=fm.boundingRect(landscapeLabels.at(i).name).width();
355 painter->drawText(landscapeLabels.at(i).labelPoint, landscapeLabels.at(i).name, 0, -textWidth/2, 2, true);
356 }
357 painter->drawGreatCircleArc(landscapeLabels.at(i).featurePoint, landscapeLabels.at(i).labelPoint, Q_NULLPTR);
358 }
359
360 painter->setLineSmooth(false);
361 painter->setBlending(false);
362 }
363
364
LandscapeOldStyle(float _radius)365 LandscapeOldStyle::LandscapeOldStyle(float _radius)
366 : Landscape(_radius)
367 , sideTexs(Q_NULLPTR)
368 , nbSideTexs(0)
369 , nbSide(0)
370 , sides(Q_NULLPTR)
371 , nbDecorRepeat(0)
372 , fogAltAngle(0.)
373 , fogAngleShift(0.)
374 , decorAltAngle(0.)
375 , decorAngleShift(0.)
376 , groundAngleShift(0.)
377 , groundAngleRotateZ(0.)
378 , drawGroundFirst(false)
379 , tanMode(false)
380 , calibrated(false) // start with just the known entries.
381 {
382 memorySize=sizeof(LandscapeOldStyle);
383 }
384
~LandscapeOldStyle()385 LandscapeOldStyle::~LandscapeOldStyle()
386 {
387 if (sideTexs)
388 {
389 delete [] sideTexs;
390 sideTexs = Q_NULLPTR;
391 }
392
393 if (sides) delete [] sides;
394 if (sidesImages.size()>0)
395 {
396 qDeleteAll(sidesImages);
397 sidesImages.clear();
398 }
399 landscapeLabels.clear();
400 }
401
load(const QSettings & landscapeIni,const QString & landscapeId)402 void LandscapeOldStyle::load(const QSettings& landscapeIni, const QString& landscapeId)
403 {
404 // TODO: put values into hash and call create() method to consolidate code
405 loadCommon(landscapeIni, landscapeId);
406 // rows, cols have been loaded already, but with different defaults.
407 // GZ Hey, they are not used altogether! Resolution is constant, below!
408 //rows = landscapeIni.value("landscape/tesselate_rows", 8).toInt();
409 //cols = landscapeIni.value("landscape/tesselate_cols", 16).toInt();
410 QString type = landscapeIni.value("landscape/type").toString();
411 if(type != "old_style")
412 {
413 qWarning() << "Landscape type mismatch for landscape " << landscapeId
414 << ", expected old_style, found " << type << ". No landscape in use.";
415 validLandscape = false;
416 return;
417 }
418
419 nbDecorRepeat = static_cast<unsigned short>(landscapeIni.value("landscape/nb_decor_repeat", 1).toUInt());
420 fogAltAngle = landscapeIni.value("landscape/fog_alt_angle", 0.).toFloat();
421 fogAngleShift = landscapeIni.value("landscape/fog_angle_shift", 0.).toFloat();
422 decorAltAngle = landscapeIni.value("landscape/decor_alt_angle", 0.).toFloat();
423 decorAngleShift = landscapeIni.value("landscape/decor_angle_shift", 0.).toFloat();
424 angleRotateZ = landscapeIni.value("landscape/decor_angle_rotatez", 0.).toFloat() * M_PI_180f;
425 groundAngleShift = landscapeIni.value("landscape/ground_angle_shift", 0.).toFloat() * M_PI_180f;
426 groundAngleRotateZ = landscapeIni.value("landscape/ground_angle_rotatez", 0.).toDouble() * M_PI_180;
427 drawGroundFirst = landscapeIni.value("landscape/draw_ground_first", false).toBool();
428 tanMode = landscapeIni.value("landscape/tan_mode", false).toBool();
429 calibrated = landscapeIni.value("landscape/calibrated", false).toBool();
430
431 // Load sides textures
432 nbSideTexs = static_cast<unsigned short>(landscapeIni.value("landscape/nbsidetex", 0).toUInt());
433 sideTexs = new StelTextureSP[static_cast<size_t>(nbSideTexs)*2]; // 0.14: allow upper half for light textures!
434 for (unsigned int i=0; i<nbSideTexs; ++i)
435 {
436 QString textureKey = QString("landscape/tex%1").arg(i);
437 QString textureName = landscapeIni.value(textureKey).toString();
438 const QString texturePath = getTexturePath(textureName, landscapeId);
439 sideTexs[i] = StelApp::getInstance().getTextureManager().createTexture(texturePath);
440 // GZ: To query the textures, also keep an array of QImage*, but only
441 // if that query is not going to be prevented by the polygon that already has been loaded at that point...
442 if ( (!horizonPolygon) && calibrated ) { // for uncalibrated landscapes the texture is currently never queried, so no need to store.
443 QImage *image = new QImage(texturePath);
444 sidesImages.append(image); // indices identical to those in sideTexs
445 #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
446 memorySize+=(image->sizeInBytes());
447 #else
448 memorySize+=static_cast<uint>(image->byteCount());
449 #endif
450 }
451 // Also allow light textures. The light textures must cover the same geometry as the sides. It is allowed that not all or even any light textures are present!
452 textureKey = QString("landscape/light%1").arg(i);
453 textureName = landscapeIni.value(textureKey).toString();
454 if (textureName.length())
455 {
456 const QString lightTexturePath = getTexturePath(textureName, landscapeId);
457 sideTexs[nbSideTexs+i] = StelApp::getInstance().getTextureManager().createTexture(lightTexturePath);
458 if(sideTexs[nbSideTexs+i])
459 memorySize+=sideTexs[nbSideTexs+i]->getGlSize();
460 }
461 else
462 sideTexs[nbSideTexs+i].clear();
463 }
464 if ( (!horizonPolygon) && calibrated )
465 {
466 Q_ASSERT(sidesImages.size()==nbSideTexs);
467 }
468 QMap<unsigned int, unsigned int> texToSide;
469 // Init sides parameters
470 nbSide = static_cast<unsigned short>(landscapeIni.value("landscape/nbside", 0).toUInt());
471 sides = new landscapeTexCoord[static_cast<size_t>(nbSide)];
472 unsigned int texnum;
473 for (unsigned int i=0;i<nbSide;++i)
474 {
475 const QString key = QString("landscape/side%1").arg(i); // e.g. side0
476 //sscanf(s.toLocal8Bit(),"tex%d:%f:%f:%f:%f",&texnum,&a,&b,&c,&d);
477 const QStringList parameters = landscapeIni.value(key).toString().split(':'); // e.g. tex0:0:0:1:1
478 //TODO: How should be handled an invalid texture description?
479 QString textureName = parameters.value(0); // tex0
480 texnum = textureName.right(textureName.length() - 3).toUInt(); // 0
481 sides[i].tex = sideTexs[texnum];
482 sides[i].tex_illum = sideTexs[nbSide+texnum];
483 sides[i].texCoords[0] = parameters.at(1).toFloat();
484 sides[i].texCoords[1] = parameters.at(2).toFloat();
485 sides[i].texCoords[2] = parameters.at(3).toFloat();
486 sides[i].texCoords[3] = parameters.at(4).toFloat();
487 //qDebug() << i << texnum << sides[i].texCoords[0] << sides[i].texCoords[1] << sides[i].texCoords[2] << sides[i].texCoords[3];
488
489 // Prior to precomputing the sides, we used to match E to side0.
490 // In r4598 the precomputing was put in place and caused a problem for
491 // old_style landscapes which had a z rotation on the side textures
492 // and where side0 did not map to tex0
493 // texToSide is a nasty hack to replace the old behaviour.
494 // GZ for V0.13: I put the zrotation to the draw call (like for all other landscapes).
495 // Maybe this can be again simplified?
496 texToSide[i] = texnum;
497 }
498 const QString groundTexName = landscapeIni.value("landscape/groundtex").toString();
499 const QString groundTexPath = getTexturePath(groundTexName, landscapeId);
500 groundTex = StelApp::getInstance().getTextureManager().createTexture(groundTexPath, StelTexture::StelTextureParams(true));
501 if (groundTex)
502 memorySize+=groundTex->getGlSize();
503
504 const QString fogTexName = landscapeIni.value("landscape/fogtex").toString();
505 const QString fogTexPath = getTexturePath(fogTexName, landscapeId);
506 fogTex = StelApp::getInstance().getTextureManager().createTexture(fogTexPath, StelTexture::StelTextureParams(true, GL_LINEAR, GL_REPEAT));
507 if (fogTex)
508 memorySize+=fogTex->getGlSize();
509
510 // Precompute the vertex arrays for ground display
511 // Make slices_per_side=(3<<K) so that the innermost polygon of the fandisk becomes a triangle:
512 //const int slices_per_side = 3*64/(nbDecorRepeat*nbSide);
513 //if (slices_per_side<=0) // GZ: How can negative ever happen?
514 // slices_per_side = 1;
515 const unsigned short int slices_per_side = static_cast<const unsigned short>(qMax(3u*64u/(nbDecorRepeat*nbSide), 1u));
516
517 // draw a fan disk instead of a ordinary disk to that the inner slices
518 // are not so slender. When they are too slender, culling errors occur
519 // in cylinder projection mode.
520 unsigned short int slices_inside = nbSide*slices_per_side*nbDecorRepeat;
521 uint level = 0;
522 while ((slices_inside&1)==0 && slices_inside > 4)
523 {
524 ++level;
525 slices_inside>>=1;
526 }
527 StelPainter::computeFanDisk(static_cast<float>(radius), slices_inside, level, groundVertexArr, groundTexCoordArr); //comaVertexArr, comaTexCoordArr);
528
529
530 // Precompute the vertex arrays for side display. The geometry of the sides is always a cylinder.
531 // The texture is split into regular quads.
532
533 // GZ: the original code for vertical placement makes unfortunately no sense. There are many approximately-fitted landscapes, though.
534 // I added a switch "calibrated" for the ini file. If true, it works as this landscape apparently was originally intended,
535 // if false (or missing) it uses the original code.
536 // I corrected the texture coordinates so that decorAltAngle is the total vertical angle, decorAngleShift the lower angle,
537 // and the texture in between is correctly stretched.
538 // I located an undocumented switch tan_mode, maybe tan_mode=true means cylindrical panorama projection instead of equirectangular.
539 // Since V0.13, calibrated&&tanMode also works!
540 // In calibrated && !tan_mode, the vertical position is computed correctly, so that quads off the horizon are larger.
541 // in calibrated && tan_mode, d_z can become a constant because the texture is already predistorted in cylindrical projection.
542 const unsigned short int stacks = (calibrated ? 16u : 8u); // GZ: 8->16, I need better precision.
543 float z0, d_z;
544 if (calibrated)
545 {
546 if (tanMode) // cylindrical pano: linear in d_z, simpler.
547 {
548 z0= static_cast<float>(radius)*std::tan(decorAngleShift*M_PI_180f);
549 d_z=(static_cast<float>(radius)*std::tan((decorAltAngle+decorAngleShift)*M_PI_180f) - z0)/stacks;
550 }
551 else // equirectangular pano: angular z, requires more work in the loop below!
552 {
553 z0=decorAngleShift;
554 d_z=decorAltAngle/stacks;
555 }
556 }
557 else // buggy code, but legacy.
558 {
559 z0 =static_cast<float>(radius)*(tanMode ? std::tan(decorAngleShift*M_PI_180f) : std::sin(decorAngleShift*M_PI_180f));
560 d_z=static_cast<float>(radius)*(tanMode ? std::tan(decorAltAngle *M_PI_180f)/stacks : std::sin(decorAltAngle *M_PI_180f)/stacks);
561 }
562
563 const float alpha = 2.f*static_cast<float>(M_PI)/(nbDecorRepeat*nbSide*slices_per_side); //delta_azimuth
564 const float ca = std::cos(alpha);
565 const float sa = std::sin(alpha);
566 float y0 = static_cast<float>(radius);
567 float x0 = 0.0f;
568 unsigned short int limit;
569
570 LOSSide precompSide;
571 precompSide.arr.primitiveType=StelVertexArray::Triangles;
572 for (unsigned int n=0;n<nbDecorRepeat;n++)
573 {
574 for (unsigned int i=0;i<nbSide;i++)
575 {
576 unsigned int ti;
577 if (texToSide.contains(i))
578 ti = texToSide[i];
579 else
580 {
581 qDebug() << QString("LandscapeOldStyle::load ERROR: found no corresponding tex value for side%1").arg(i);
582 break;
583 }
584 precompSide.arr.vertex.resize(0);
585 precompSide.arr.texCoords.resize(0);
586 precompSide.arr.indices.resize(0);
587 precompSide.tex=sideTexs[ti];
588 precompSide.light=false;
589
590 float tx0 = sides[ti].texCoords[0];
591 const float d_tx = (sides[ti].texCoords[2]-sides[ti].texCoords[0]) / slices_per_side;
592 const float d_ty = (sides[ti].texCoords[3]-sides[ti].texCoords[1]) / stacks;
593 for (unsigned short int j=0;j<slices_per_side;j++)
594 {
595 const float y1 = y0*ca - x0*sa;
596 const float x1 = y0*sa + x0*ca;
597 const float tx1 = tx0 + d_tx;
598 float z = z0;
599 float ty0 = sides[ti].texCoords[1];
600 limit = static_cast<unsigned short int>(stacks*2u);
601 for (unsigned short int k=0u;k<=limit;k+=2u)
602 {
603 precompSide.arr.texCoords << Vec2f(tx0, ty0) << Vec2f(tx1, ty0);
604 if (calibrated && !tanMode)
605 {
606 double tanZ=radius * static_cast<double>(std::tan(z*M_PI_180f));
607 precompSide.arr.vertex << Vec3d(static_cast<double>(x0), static_cast<double>(y0), tanZ)
608 << Vec3d(static_cast<double>(x1), static_cast<double>(y1), tanZ);
609 } else
610 {
611 precompSide.arr.vertex << Vec3d(static_cast<double>(x0), static_cast<double>(y0), static_cast<double>(z))
612 << Vec3d(static_cast<double>(x1), static_cast<double>(y1), static_cast<double>(z));
613 }
614 z += d_z;
615 ty0 += d_ty;
616 }
617 unsigned short int offset = j*(stacks+1u)*2u;
618 limit = static_cast<unsigned short int>(stacks*2u+2u);
619 for (unsigned short int k = 2;k<limit;k+=2u)
620 {
621 precompSide.arr.indices << offset+k-2 << offset+k-1 << offset+k;
622 precompSide.arr.indices << offset+k << offset+k-1 << offset+k+1;
623 }
624 y0 = y1;
625 x0 = x1;
626 tx0 = tx1;
627 }
628 precomputedSides.append(precompSide);
629 if (sideTexs[ti+nbSide])
630 {
631 precompSide.light=true;
632 precompSide.tex=sideTexs[ti+nbSide];
633 precomputedSides.append(precompSide); // These sides are not called by strict index!
634 // May be 9 for 8 sidetexs plus 1-only light panel
635 }
636 }
637 }
638 //qDebug() << "OldStyleLandscape" << landscapeId << "loaded, mem size:" << memorySize;
639 }
640
draw(StelCore * core,bool onlyPolygon)641 void LandscapeOldStyle::draw(StelCore* core, bool onlyPolygon)
642 {
643 if (!validLandscape)
644 return;
645
646 StelPainter painter(core->getProjection(StelCore::FrameAltAz, StelCore::RefractionOff));
647 const float ppx = static_cast<float>(painter.getProjector()->getDevicePixelsPerPixel());
648 painter.setBlending(true);
649 painter.setCullFace(true);
650
651 if (!onlyPolygon || !horizonPolygon) // Make sure to draw the regular pano when there is no polygon
652 {
653 if (drawGroundFirst)
654 drawGround(core, painter);
655 drawDecor(core, painter, false);
656 if (!drawGroundFirst)
657 drawGround(core, painter);
658 drawFog(core, painter);
659
660 // Self-luminous layer (Light pollution etc). This looks striking!
661 if (lightScapeBrightness>0.0f && (illumFader.getInterstate()>0.f))
662 {
663 painter.setBlending(true, GL_SRC_ALPHA, GL_ONE);
664 drawDecor(core, painter, true);
665 }
666 }
667 // If a horizon line also has been defined, draw it.
668 if (horizonPolygon && (horizonPolygonLineColor != Vec3f(-1.f,0.f,0.f)))
669 {
670 //qDebug() << "drawing line";
671 StelProjector::ModelViewTranformP transfo = core->getAltAzModelViewTransform(StelCore::RefractionOff);
672 transfo->combine(Mat4d::zrotation(static_cast<double>(-angleRotateZOffset)));
673 const StelProjectorP prj = core->getProjection(transfo);
674 painter.setProjector(prj);
675 painter.setBlending(true, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
676 painter.setColor(horizonPolygonLineColor, landFader.getInterstate());
677 const float lineWidth=painter.getLineWidth();
678 painter.setLineWidth(GETSTELMODULE(LandscapeMgr)->getPolyLineThickness()*ppx);
679 painter.drawSphericalRegion(horizonPolygon.data(), StelPainter::SphericalPolygonDrawModeBoundary);
680 painter.setLineWidth(lineWidth);
681 }
682 //else qDebug() << "no polygon defined";
683
684 drawLabels(core, &painter);
685 }
686
687
688 // Draw the horizon fog
drawFog(StelCore * core,StelPainter & sPainter) const689 void LandscapeOldStyle::drawFog(StelCore* core, StelPainter& sPainter) const
690 {
691 if (fogFader.getInterstate()==0.f)
692 return;
693 if(landFader.getInterstate()==0.f)
694 return;
695 if (!(core->getSkyDrawer()->getFlagHasAtmosphere()))
696 return;
697
698 const float vpos = static_cast<float>(radius) * ((tanMode||calibrated) ? std::tan(fogAngleShift*M_PI_180f) : std::sin(fogAngleShift*M_PI_180f));
699 StelProjector::ModelViewTranformP transfo = core->getAltAzModelViewTransform(StelCore::RefractionOff);
700
701 if (calibrated) // new in V0.13: take proper care of the fog layer. This will work perfectly only for calibrated&&tanMode.
702 transfo->combine(Mat4d::zrotation(-static_cast<double>(angleRotateZ+angleRotateZOffset)));
703
704 transfo->combine(Mat4d::translation(Vec3d(0.,0.,static_cast<double>(vpos))));
705 sPainter.setProjector(core->getProjection(transfo));
706 sPainter.setBlending(true, GL_ONE, GL_ONE);
707 sPainter.setColor(Vec3f(landFader.getInterstate()*fogFader.getInterstate()*(0.1f+0.1f*landscapeBrightness)), landFader.getInterstate());
708 fogTex->bind();
709 const double height = radius * static_cast<double>(calibrated?
710 (std::tan((fogAltAngle+fogAngleShift)*M_PI_180f) - std::tan(fogAngleShift*M_PI_180f))
711 : ((tanMode) ? std::tan(fogAltAngle*M_PI_180f) : std::sin(fogAltAngle*M_PI_180f)));
712 sPainter.sCylinder(radius, height, 64, 1);
713 sPainter.setBlending(true);
714 }
715
716 // Draw the side textures
drawDecor(StelCore * core,StelPainter & sPainter,const bool drawLight) const717 void LandscapeOldStyle::drawDecor(StelCore* core, StelPainter& sPainter, const bool drawLight) const
718 {
719 StelProjector::ModelViewTranformP transfo = core->getAltAzModelViewTransform(StelCore::RefractionOff);
720 transfo->combine(Mat4d::zrotation(-static_cast<double>(angleRotateZ+angleRotateZOffset)));
721 sPainter.setProjector(core->getProjection(transfo));
722
723 if (landFader.getInterstate()==0.f)
724 return;
725 if (drawLight)
726 sPainter.setColor(Vec3f(illumFader.getInterstate()*lightScapeBrightness), landFader.getInterstate());
727 else
728 sPainter.setColor(Vec3f(landscapeBrightness), landFader.getInterstate());
729
730 for (const auto& side : precomputedSides)
731 {
732 if (side.light==drawLight)
733 {
734 side.tex->bind();
735 sPainter.drawSphericalTriangles(side.arr, true, false, Q_NULLPTR, false);
736 }
737 }
738 }
739
740 // Draw the ground
drawGround(StelCore * core,StelPainter & sPainter) const741 void LandscapeOldStyle::drawGround(StelCore* core, StelPainter& sPainter) const
742 {
743 if (landFader.getInterstate()==0.f)
744 return;
745 const float vshift = static_cast<float>(radius) * ((tanMode || calibrated) ? std::tan(groundAngleShift) : std::sin(groundAngleShift));
746 StelProjector::ModelViewTranformP transfo = core->getAltAzModelViewTransform(StelCore::RefractionOff);
747 transfo->combine(Mat4d::zrotation(groundAngleRotateZ-static_cast<double>(angleRotateZOffset)) * Mat4d::translation(Vec3d(0,0,static_cast<double>(vshift))));
748
749 sPainter.setProjector(core->getProjection(transfo));
750 sPainter.setColor(landscapeBrightness, landscapeBrightness, landscapeBrightness, landFader.getInterstate());
751
752 if(groundTex.isNull())
753 {
754 qWarning()<<"LandscapeOldStyle groundTex is invalid!";
755 }
756 else
757 {
758 groundTex->bind();
759 }
760 StelVertexArray va(static_cast<const QVector<Vec3d> >(groundVertexArr), StelVertexArray::Triangles, static_cast<const QVector<Vec2f> >(groundTexCoordArr));
761 sPainter.drawStelVertexArray(va, true);
762 }
763
getOpacity(Vec3d azalt) const764 float LandscapeOldStyle::getOpacity(Vec3d azalt) const
765 {
766 if(!validLandscape) return (azalt[2]>0.0 ? 0.0f : 1.0f);
767
768 if (angleRotateZOffset!=0.0f)
769 azalt.transfo4d(Mat4d::zrotation(static_cast<double>(angleRotateZOffset)));
770
771 // in case we also have a horizon polygon defined, this is trivial and fast.
772 if (horizonPolygon)
773 {
774 if (horizonPolygon->contains(azalt)) return 1.0f; else return 0.0f;
775 }
776 // Else, sample the images...
777 float az, alt_rad;
778 StelUtils::rectToSphe(&az, &alt_rad, azalt);
779
780 if (alt_rad < decorAngleShift*M_PI_180f) return 1.0f; // below decor, i.e. certainly opaque ground.
781 if (alt_rad > (decorAltAngle+decorAngleShift)*M_PI_180f) return 0.0f; // above decor, i.e. certainly free sky.
782 if (!calibrated) // the result of this function has no real use here: just complain and return result for math. horizon.
783 {
784 static QString lastLandscapeName;
785 if (lastLandscapeName != name)
786 {
787 qWarning() << "Dubious result: Landscape " << name << " not calibrated. Opacity test represents mathematical horizon only.";
788 lastLandscapeName=name;
789 }
790 return (azalt[2] > 0 ? 0.0f : 1.0f);
791 }
792 az = (M_PIf-az) / M_PIf; // 0..2 = N.E.S.W.N
793 // we go to 0..1 domain, it's easier to think.
794 const float xShift=angleRotateZ /(2.0f*M_PIf); // shift value in -1..1 domain
795 Q_ASSERT(xShift >= -1.0f);
796 Q_ASSERT(xShift <= 1.0f);
797 float az_phot=az*0.5f - 0.25f - xShift; // The 0.25 is caused by regular pano left edge being East. The xShift compensates any configured angleRotateZ
798 az_phot=fmodf(az_phot, 1.0f);
799 if (az_phot<0) az_phot+=1.0f; // 0..1 = image-X for a non-repeating pano photo
800 float az_panel = nbSide*nbDecorRepeat * az_phot; // azimuth in "panel space". Ex for nbS=4, nbDR=3: [0..[12, say 11.4
801 float x_in_panel=fmodf(az_panel, 1.0f);
802 int currentSide = static_cast<int>(floor(fmodf(az_panel, nbSide)));
803 Q_ASSERT(currentSide>=0);
804 Q_ASSERT(currentSide<static_cast<int>(nbSideTexs));
805 if (sidesImages[currentSide]->isNull()) return 0.0f; // can happen if image is misconfigured and failed to load.
806 int x= static_cast<int>(sides[currentSide].texCoords[0] + x_in_panel*(sides[currentSide].texCoords[2]-sides[currentSide].texCoords[0]))
807 * sidesImages[currentSide]->width(); // pixel X from left.
808
809 // QImage has pixel 0/0 in top left corner. We must find image Y for optionally cropped images.
810 // It should no longer be possible that sample position is outside cropped texture. in this case, assert(0) but again assume full transparency and exit early.
811
812 float y_img_1; // y of the sampled altitude in 0..1 visible image height from bottom
813 if (tanMode)
814 {
815 const float tanAlt=std::tan(alt_rad);
816 const float tanTop=std::tan((decorAltAngle+decorAngleShift)*M_PI_180f);
817 const float tanBot=std::tan(decorAngleShift*M_PI_180f);
818 y_img_1=(tanAlt-tanBot)/(tanTop-tanBot); // Y position 0..1 in visible image height from bottom
819 }
820 else
821 { // adapted from spherical...
822 const float alt_pm1 = 2.0f * alt_rad / M_PIf; // sampled altitude, -1...+1 linear in altitude angle
823 const float img_top_pm1 = 1.0f-( (90.0f-decorAltAngle-decorAngleShift) / 90.0f); // the top line in -1..+1 (angular)
824 if (alt_pm1>img_top_pm1) { Q_ASSERT(0); return 0.0f; } // should have been caught above with alt_rad tests
825 const float img_bot_pm1 = 1.0f-((90.0f-decorAngleShift) / 90.0f); // the bottom line in -1..+1 (angular)
826 if (alt_pm1<img_bot_pm1) { Q_ASSERT(0); return 1.0f; } // should have been caught above with alt_rad tests
827
828 y_img_1=(alt_pm1-img_bot_pm1)/(img_top_pm1-img_bot_pm1); // the sampled altitude in 0..1 visible image height from bottom
829 Q_ASSERT(y_img_1<=1.f);
830 Q_ASSERT(y_img_1>=0.f);
831 }
832 // x0/y0 is lower left, x1/y1 upper right corner.
833 float y_baseImg_1 = sides[currentSide].texCoords[1]+ y_img_1*(sides[currentSide].texCoords[3]-sides[currentSide].texCoords[1]);
834 int y=static_cast<int>((1.0f-y_baseImg_1)*static_cast<float>(sidesImages[currentSide]->height())); // pixel Y from top.
835 QRgb pixVal=sidesImages[currentSide]->pixel(x, y);
836 /*
837 #ifndef NDEBUG
838 // GZ: please leave the comment available for further development!
839 qDebug() << "Oldstyle Landscape sampling: az=" << az*180.0 << "° alt=" << alt_rad*180.0f/M_PI
840 << "°, xShift[-1..+1]=" << xShift << " az_phot[0..1]=" << az_phot
841 << " --> current side panel " << currentSide
842 << ", w=" << sidesImages[currentSide]->width() << " h=" << sidesImages[currentSide]->height()
843 << " --> x:" << x << " y:" << y << " alpha:" << qAlpha(pixVal)/255.0f;
844 #endif
845 */
846 return qAlpha(pixVal)/255.0f;
847 }
848
849 ////////////////////////////////////////////////////////////////////////////////////////////////////
850
LandscapePolygonal(float _radius)851 LandscapePolygonal::LandscapePolygonal(float _radius) : Landscape(_radius)
852 {
853 memorySize=(sizeof(LandscapePolygonal));
854 }
855
~LandscapePolygonal()856 LandscapePolygonal::~LandscapePolygonal()
857 {
858 landscapeLabels.clear();
859 }
860
load(const QSettings & landscapeIni,const QString & landscapeId)861 void LandscapePolygonal::load(const QSettings& landscapeIni, const QString& landscapeId)
862 {
863 // loading the polygon has been moved to Landscape::loadCommon(), so that all Landscape classes can use a polygon line.
864 loadCommon(landscapeIni, landscapeId);
865 const QString type = landscapeIni.value("landscape/type").toString();
866 if(type != "polygonal")
867 {
868 qWarning() << "Landscape type mismatch for landscape "<< landscapeId << ", expected polygonal, found " << type << ". No landscape in use.\n";
869 validLandscape = false;
870 return;
871 }
872 if (horizonPolygon.isNull())
873 {
874 qWarning() << "Landscape " << landscapeId << " does not declare a valid polygonal_horizon_list. No landscape in use.\n";
875 validLandscape = false;
876 return;
877 }
878 groundColor=Vec3f( landscapeIni.value("landscape/ground_color", "0,0,0" ).toString() );
879 //flagDrawInFront=landscapeIni.value("landscape/draw_in_foreground", false).toBool(); // manually configured override for a polygonal line plotted into the foreground.
880 validLandscape = true; // assume ok...
881 //qDebug() << "PolygonalLandscape" << landscapeId << "loaded, mem size:" << getMemorySize();
882 }
883
draw(StelCore * core,bool onlyPolygon)884 void LandscapePolygonal::draw(StelCore* core, bool onlyPolygon)
885 {
886 if(!validLandscape) return;
887 if(landFader.getInterstate()==0.f) return;
888
889 StelProjector::ModelViewTranformP transfo = core->getAltAzModelViewTransform(StelCore::RefractionOff);
890 transfo->combine(Mat4d::zrotation(-static_cast<double>(angleRotateZOffset)));
891 const StelProjectorP prj = core->getProjection(transfo);
892 StelPainter sPainter(prj);
893 const float ppx = static_cast<float>(sPainter.getProjector()->getDevicePixelsPerPixel());
894
895 // Normal transparency mode for the transition blending
896 sPainter.setBlending(true);
897 sPainter.setCullFace(true);
898
899 if (!onlyPolygon) // The only useful application of the onlyPolygon is a demo which does not fill the polygon
900 {
901 sPainter.setColor(landscapeBrightness*groundColor, landFader.getInterstate());
902 sPainter.drawSphericalRegion(horizonPolygon.data(), StelPainter::SphericalPolygonDrawModeFill);
903 }
904
905 if (horizonPolygonLineColor != Vec3f(-1.f,0.f,0.f))
906 {
907 sPainter.setLineSmooth(true);
908 sPainter.setColor(horizonPolygonLineColor, landFader.getInterstate());
909 const float lineWidth=sPainter.getLineWidth();
910 sPainter.setLineWidth(GETSTELMODULE(LandscapeMgr)->getPolyLineThickness()*ppx);
911 sPainter.drawSphericalRegion(horizonPolygon.data(), StelPainter::SphericalPolygonDrawModeBoundary);
912 sPainter.setLineWidth(lineWidth);
913 sPainter.setLineSmooth(false);
914 }
915 sPainter.setCullFace(false);
916 drawLabels(core, &sPainter);
917 }
918
getOpacity(Vec3d azalt) const919 float LandscapePolygonal::getOpacity(Vec3d azalt) const
920 {
921 if(!validLandscape) return (azalt[2]>0.0 ? 0.0f : 1.0f);
922 if (angleRotateZOffset!=0.0f)
923 azalt.transfo4d(Mat4d::zrotation(static_cast<double>(angleRotateZOffset)));
924
925 if (horizonPolygon->contains(azalt)) return 1.0f; else return 0.0f;
926 }
927
928 ////////////////////////////////////////////////////////////////////////////////////////
929 // LandscapeFisheye
930 //
931
LandscapeFisheye(float _radius)932 LandscapeFisheye::LandscapeFisheye(float _radius)
933 : Landscape(_radius)
934 , mapTex(StelTextureSP())
935 , mapTexFog(StelTextureSP())
936 , mapTexIllum(StelTextureSP())
937 , mapImage(Q_NULLPTR)
938 , texFov(360.)
939 {
940 memorySize=sizeof(LandscapeFisheye);
941 }
942
~LandscapeFisheye()943 LandscapeFisheye::~LandscapeFisheye()
944 {
945 if (mapImage) delete mapImage;
946 landscapeLabels.clear();
947 }
948
load(const QSettings & landscapeIni,const QString & landscapeId)949 void LandscapeFisheye::load(const QSettings& landscapeIni, const QString& landscapeId)
950 {
951 loadCommon(landscapeIni, landscapeId);
952
953 QString type = landscapeIni.value("landscape/type").toString();
954 if(type != "fisheye")
955 {
956 qWarning() << "Landscape type mismatch for landscape "<< landscapeId << ", expected fisheye, found " << type << ". No landscape in use.\n";
957 validLandscape = false;
958 return;
959 }
960 create(name,
961 landscapeIni.value("landscape/texturefov", 360).toFloat(),
962 getTexturePath(landscapeIni.value("landscape/maptex").toString(), landscapeId),
963 getTexturePath(landscapeIni.value("landscape/maptex_fog").toString(), landscapeId),
964 getTexturePath(landscapeIni.value("landscape/maptex_illum").toString(), landscapeId),
965 landscapeIni.value("landscape/angle_rotatez", 0.f).toFloat());
966 //qDebug() << "FisheyeLandscape" << landscapeId << "loaded, mem size:" << memorySize;
967 }
968
969
create(const QString _name,float _texturefov,const QString & _maptex,const QString & _maptexFog,const QString & _maptexIllum,const float _angleRotateZ)970 void LandscapeFisheye::create(const QString _name, float _texturefov, const QString& _maptex, const QString &_maptexFog, const QString& _maptexIllum, const float _angleRotateZ)
971 {
972 // qDebug() << _name << " " << _fullpath << " " << _maptex << " " << _texturefov;
973 validLandscape = true; // assume ok...
974 name = _name;
975 texFov = _texturefov*M_PI_180f;
976 angleRotateZ = _angleRotateZ*M_PI_180f;
977
978 if (!horizonPolygon)
979 {
980 mapImage = new QImage(_maptex);
981 #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
982 memorySize+=(mapImage->sizeInBytes());
983 #else
984 memorySize+=static_cast<uint>(mapImage->byteCount());
985 #endif
986 }
987 mapTex = StelApp::getInstance().getTextureManager().createTexture(_maptex, StelTexture::StelTextureParams(true));
988 memorySize+=mapTex->getGlSize();
989
990 if (_maptexIllum.length() && (!_maptexIllum.endsWith("/")))
991 {
992 mapTexIllum = StelApp::getInstance().getTextureManager().createTexture(_maptexIllum, StelTexture::StelTextureParams(true));
993 if (mapTexIllum)
994 memorySize+=mapTexIllum->getGlSize();
995 }
996 if (_maptexFog.length() && (!_maptexFog.endsWith("/")))
997 {
998 mapTexFog = StelApp::getInstance().getTextureManager().createTexture(_maptexFog, StelTexture::StelTextureParams(true));
999 if (mapTexFog)
1000 memorySize+=mapTexFog->getGlSize();
1001 }
1002 }
1003
1004
draw(StelCore * core,bool onlyPolygon)1005 void LandscapeFisheye::draw(StelCore* core, bool onlyPolygon)
1006 {
1007 if(!validLandscape) return;
1008 if(landFader.getInterstate()==0.f) return;
1009
1010 StelProjector::ModelViewTranformP transfo = core->getAltAzModelViewTransform(StelCore::RefractionOff);
1011 transfo->combine(Mat4d::zrotation(-static_cast<double>(angleRotateZ+angleRotateZOffset)));
1012 const StelProjectorP prj = core->getProjection(transfo);
1013 StelPainter painter(prj);
1014 const float ppx = static_cast<float>(painter.getProjector()->getDevicePixelsPerPixel());
1015
1016 if (!onlyPolygon || !horizonPolygon) // Make sure to draw the regular pano when there is no polygon
1017 {
1018 // Normal transparency mode
1019 painter.setBlending(true);
1020 painter.setCullFace(true);
1021 painter.setColor(Vec3f(static_cast<float>(landscapeBrightness)), landFader.getInterstate());
1022 mapTex->bind();
1023 painter.sSphereMap(static_cast<double>(radius),cols,rows,texFov,1);
1024 // NEW since 0.13: Fog also for fisheye...
1025 if ((mapTexFog) && (core->getSkyDrawer()->getFlagHasAtmosphere()))
1026 {
1027 //glBlendFunc(GL_ONE, GL_ONE); // GZ: Take blending mode as found in the old_style landscapes...
1028 painter.setBlending(true, GL_ONE, GL_ONE_MINUS_SRC_COLOR); // GZ: better?
1029 painter.setColor(Vec3f(landFader.getInterstate()*fogFader.getInterstate()*(0.1f+0.1f*static_cast<float>(landscapeBrightness))), landFader.getInterstate());
1030 mapTexFog->bind();
1031 painter.sSphereMap(static_cast<double>(radius),cols,rows,texFov,1);
1032 }
1033 if (mapTexIllum && lightScapeBrightness>0.0f && (illumFader.getInterstate()>0.f))
1034 {
1035 painter.setBlending(true, GL_SRC_ALPHA, GL_ONE);
1036 painter.setColor(Vec3f(illumFader.getInterstate()*static_cast<float>(lightScapeBrightness)), landFader.getInterstate());
1037 mapTexIllum->bind();
1038 painter.sSphereMap(static_cast<double>(radius), cols, rows, texFov, 1);
1039 }
1040 painter.setCullFace(false);
1041 }
1042
1043 // If a horizon line also has been defined, draw it.
1044 if (horizonPolygon && (horizonPolygonLineColor != Vec3f(-1.f,0.f,0.f)))
1045 {
1046 //qDebug() << "drawing line";
1047 StelProjector::ModelViewTranformP transfo = core->getAltAzModelViewTransform(StelCore::RefractionOff);
1048 transfo->combine(Mat4d::zrotation(static_cast<double>(-angleRotateZOffset)));
1049 const StelProjectorP prj = core->getProjection(transfo);
1050 painter.setProjector(prj);
1051 painter.setBlending(true, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
1052 painter.setColor(horizonPolygonLineColor, landFader.getInterstate());
1053 const float lineWidth=painter.getLineWidth();
1054 painter.setLineWidth(GETSTELMODULE(LandscapeMgr)->getPolyLineThickness()*ppx);
1055 painter.drawSphericalRegion(horizonPolygon.data(), StelPainter::SphericalPolygonDrawModeBoundary);
1056 painter.setLineWidth(lineWidth);
1057 }
1058
1059 drawLabels(core, &painter);
1060 }
1061
getOpacity(Vec3d azalt) const1062 float LandscapeFisheye::getOpacity(Vec3d azalt) const
1063 {
1064 if(!validLandscape || (!horizonPolygon && (!mapImage || mapImage->isNull()))) return (azalt[2]>0.0 ? 0.0f : 1.0f); // can happen if image is misconfigured and failed to load.
1065
1066 if (angleRotateZOffset!=0.0f)
1067 azalt.transfo4d(Mat4d::zrotation(static_cast<double>(angleRotateZOffset)));
1068
1069 // in case we also have a horizon polygon defined, this is trivial and fast.
1070 if (horizonPolygon)
1071 {
1072 if (horizonPolygon->contains(azalt)) return 1.0f; else return 0.0f;
1073 }
1074 // Else, sample the image...
1075 float az, alt_rad;
1076 StelUtils::rectToSphe(&az, &alt_rad, azalt);
1077
1078 // QImage has pixel 0/0 in top left corner.
1079 // The texture is taken from the center circle in the square texture.
1080 // It is possible that sample position is outside. in this case, assume full opacity and exit early.
1081 if (M_PI_2f-alt_rad > texFov/2.0f ) return 1.0f; // outside fov, in the clamped texture zone: always opaque.
1082
1083 float radius=(M_PI_2f-alt_rad)*2.0f/texFov; // radius in units of mapImage.height/2
1084
1085 az = (M_PIf-az) - static_cast<float>(angleRotateZ); // 0..+2pi -angleRotateZ, real azimuth. NESW
1086 // The texture map has south on top, east at right (if anglerotateZ=0)
1087 int x= static_cast<int>(mapImage->height()/2*(1 + radius*std::sin(az)));
1088 int y= static_cast<int>(mapImage->height()/2*(1 + radius*std::cos(az)));
1089
1090 QRgb pixVal=mapImage->pixel(x, y);
1091 /*
1092 #ifndef NDEBUG
1093 // GZ: please leave the comment available for further development!
1094 qDebug() << "Landscape sampling: az=" << (az+angleRotateZ)/M_PI*180.0f << "° alt=" << alt_rad/M_PI*180.f
1095 << "°, w=" << mapImage->width() << " h=" << mapImage->height()
1096 << " --> x:" << x << " y:" << y << " alpha:" << qAlpha(pixVal)/255.0f;
1097 #endif
1098 */
1099 return qAlpha(pixVal)/255.0f;
1100 }
1101 /////////////////////////////////////////////////////////////////////////////////////////////////
1102 // spherical panoramas
1103
LandscapeSpherical(float _radius)1104 LandscapeSpherical::LandscapeSpherical(float _radius)
1105 : Landscape(_radius)
1106 , mapTex(StelTextureSP())
1107 , mapTexFog(StelTextureSP())
1108 , mapTexIllum(StelTextureSP())
1109 , bottomCap(Vec3d(0.0,0.0,-1.0), 0.0)
1110 , mapTexTop(0.)
1111 , mapTexBottom(0.)
1112 , fogTexTop(0.)
1113 , fogTexBottom(0.)
1114 , illumTexTop(0.)
1115 , illumTexBottom(0.)
1116 , mapImage(Q_NULLPTR)
1117 , bottomCapColor(-1.0f, 0.0f, 0.0f)
1118 {
1119 memorySize=sizeof(LandscapeSpherical);
1120 }
1121
~LandscapeSpherical()1122 LandscapeSpherical::~LandscapeSpherical()
1123 {
1124 if (mapImage) delete mapImage;
1125 landscapeLabels.clear();
1126 }
1127
load(const QSettings & landscapeIni,const QString & landscapeId)1128 void LandscapeSpherical::load(const QSettings& landscapeIni, const QString& landscapeId)
1129 {
1130 loadCommon(landscapeIni, landscapeId);
1131 // if (horizonPolygon)
1132 // qDebug() << "This landscape, " << landscapeId << ", has a polygon defined!" ;
1133 // else
1134 // qDebug() << "This landscape, " << landscapeId << ", has no polygon defined!" ;
1135
1136 QString type = landscapeIni.value("landscape/type").toString();
1137 if (type != "spherical")
1138 {
1139 qWarning() << "Landscape type mismatch for landscape "<< landscapeId
1140 << ", expected spherical, found " << type
1141 << ". No landscape in use.\n";
1142 validLandscape = false;
1143 return;
1144 }
1145
1146 create(name,
1147 getTexturePath(landscapeIni.value("landscape/maptex").toString(), landscapeId),
1148 getTexturePath(landscapeIni.value("landscape/maptex_fog").toString(), landscapeId),
1149 getTexturePath(landscapeIni.value("landscape/maptex_illum").toString(), landscapeId),
1150 landscapeIni.value("landscape/angle_rotatez" , 0.f).toFloat(),
1151 landscapeIni.value("landscape/maptex_top" , 90.f).toFloat(),
1152 landscapeIni.value("landscape/maptex_bottom" , -90.f).toFloat(),
1153 landscapeIni.value("landscape/maptex_fog_top" , 90.f).toFloat(),
1154 landscapeIni.value("landscape/maptex_fog_bottom" , -90.f).toFloat(),
1155 landscapeIni.value("landscape/maptex_illum_top" , 90.f).toFloat(),
1156 landscapeIni.value("landscape/maptex_illum_bottom", -90.f).toFloat(),
1157 Vec3f(landscapeIni.value("landscape/bottom_cap_color", "-1.0,0.0,0.0").toString()));
1158 //qDebug() << "SphericalLandscape" << landscapeId << "loaded, mem size:" << memorySize;
1159 }
1160
1161
1162 //// create a spherical landscape from basic parameters (no ini file needed)
create(const QString _name,const QString & _maptex,const QString & _maptexFog,const QString & _maptexIllum,const float _angleRotateZ,const float _mapTexTop,const float _mapTexBottom,const float _fogTexTop,const float _fogTexBottom,const float _illumTexTop,const float _illumTexBottom,const Vec3f _bottomCapColor)1163 void LandscapeSpherical::create(const QString _name, const QString& _maptex, const QString& _maptexFog, const QString& _maptexIllum,
1164 const float _angleRotateZ,
1165 const float _mapTexTop, const float _mapTexBottom,
1166 const float _fogTexTop, const float _fogTexBottom,
1167 const float _illumTexTop, const float _illumTexBottom, const Vec3f _bottomCapColor)
1168 {
1169 //qDebug() << "LandscapeSpherical::create():"<< _name << " : " << _maptex << " : " << _maptexFog << " : " << _maptexIllum << " : " << _angleRotateZ;
1170 validLandscape = true; // assume ok...
1171 name = _name;
1172 angleRotateZ = _angleRotateZ *M_PI_180f; // Defined in ini --> internal prg value
1173 mapTexTop = (90.f-_mapTexTop) *M_PI_180f; // top 90 --> 0
1174 mapTexBottom = (90.f-_mapTexBottom) *M_PI_180f; // bottom -90 --> pi
1175 fogTexTop = (90.f-_fogTexTop) *M_PI_180f;
1176 fogTexBottom = (90.f-_fogTexBottom) *M_PI_180f;
1177 illumTexTop = (90.f-_illumTexTop) *M_PI_180f;
1178 illumTexBottom= (90.f-_illumTexBottom)*M_PI_180f;
1179 if (!horizonPolygon)
1180 {
1181 mapImage = new QImage(_maptex);
1182 #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
1183 memorySize+=(mapImage->sizeInBytes());
1184 #else
1185 memorySize+=static_cast<uint>(mapImage->byteCount());
1186 #endif
1187 }
1188 mapTex = StelApp::getInstance().getTextureManager().createTexture(_maptex, StelTexture::StelTextureParams(true));
1189 memorySize+=mapTex->getGlSize();
1190
1191 if (_maptexIllum.length() && (!_maptexIllum.endsWith("/")))
1192 {
1193 mapTexIllum = StelApp::getInstance().getTextureManager().createTexture(_maptexIllum, StelTexture::StelTextureParams(true));
1194 if (mapTexIllum)
1195 memorySize+=mapTexIllum->getGlSize();
1196 }
1197 if (_maptexFog.length() && (!_maptexFog.endsWith("/")))
1198 {
1199 mapTexFog = StelApp::getInstance().getTextureManager().createTexture(_maptexFog, StelTexture::StelTextureParams(true));
1200 if (mapTexFog)
1201 memorySize+=mapTexFog->getGlSize();
1202 }
1203
1204 // Add a bottom cap in case of maptex_bottom.
1205 if ((mapTexBottom>-90.f*M_PI_180f) && (_bottomCapColor != Vec3f(-1.0f, 0.0f, 0.0f)))
1206 {
1207 bottomCap = SphericalCap(Vec3d(0.0, 0.0, -1.0), cos(M_PI-static_cast<double>(mapTexBottom)));
1208 bottomCapColor = _bottomCapColor;
1209 }
1210 }
1211
draw(StelCore * core,bool onlyPolygon)1212 void LandscapeSpherical::draw(StelCore* core, bool onlyPolygon)
1213 {
1214 if(!validLandscape) return;
1215 if(landFader.getInterstate()==0.f) return;
1216
1217 StelProjector::ModelViewTranformP transfo = core->getAltAzModelViewTransform(StelCore::RefractionOff);
1218 transfo->combine(Mat4d::zrotation(-(static_cast<double>(angleRotateZ+angleRotateZOffset))));
1219 const StelProjectorP prj = core->getProjection(transfo);
1220 StelPainter sPainter(prj);
1221 const float ppx = static_cast<float>(sPainter.getProjector()->getDevicePixelsPerPixel());
1222
1223 // Normal transparency mode
1224 sPainter.setBlending(true);
1225 sPainter.setCullFace(true);
1226
1227 if (!onlyPolygon || !horizonPolygon) // Make sure to draw the regular pano when there is no polygon
1228 {
1229 if (bottomCap.d>0.0)
1230 {
1231 sPainter.setColor(landscapeBrightness*bottomCapColor, landFader.getInterstate());
1232 sPainter.drawSphericalRegion(&bottomCap, StelPainter::SphericalPolygonDrawModeFill);
1233 }
1234
1235 sPainter.setColor(Vec3f(landscapeBrightness), landFader.getInterstate());
1236 mapTex->bind();
1237
1238 // TODO: verify that this works correctly for custom projections [comment not by GZ]
1239 // seam is at East, except if angleRotateZ has been given.
1240 sPainter.sSphere(radius, 1.0, cols, rows, true, true, mapTexTop, mapTexBottom);
1241 // Since 0.13: Fog also for sphericals...
1242 if ((mapTexFog) && (core->getSkyDrawer()->getFlagHasAtmosphere()))
1243 {
1244 sPainter.setBlending(true, GL_ONE, GL_ONE_MINUS_SRC_COLOR);
1245 sPainter.setColor(Vec3f(landFader.getInterstate()*fogFader.getInterstate()*(0.1f+0.1f*landscapeBrightness)), landFader.getInterstate());
1246 mapTexFog->bind();
1247 sPainter.sSphere(radius, 1.0, cols, static_cast<uint>(ceil(rows*(fogTexTop-fogTexBottom)/(mapTexTop-mapTexBottom))), true, true, fogTexTop, fogTexBottom);
1248 }
1249
1250 // Self-luminous layer (Light pollution etc). This looks striking!
1251 if (mapTexIllum && (lightScapeBrightness>0.0f) && (illumFader.getInterstate()>0.0f))
1252 {
1253 sPainter.setBlending(true, GL_SRC_ALPHA, GL_ONE);
1254 sPainter.setColor(Vec3f(lightScapeBrightness*illumFader.getInterstate()), landFader.getInterstate());
1255 mapTexIllum->bind();
1256 sPainter.sSphere(radius, 1.0, cols, static_cast<uint>(ceil(rows*(illumTexTop-illumTexBottom)/(mapTexTop-mapTexBottom))), true, true, illumTexTop, illumTexBottom);
1257 }
1258 }
1259
1260 // If a horizon line also has been defined, draw it.
1261 if (horizonPolygon && (horizonPolygonLineColor != Vec3f(-1.f,0.f,0.f)))
1262 {
1263 //qDebug() << "drawing line";
1264 transfo = core->getAltAzModelViewTransform(StelCore::RefractionOff);
1265 transfo->combine(Mat4d::zrotation(-static_cast<double>(angleRotateZOffset)));
1266 const StelProjectorP prj = core->getProjection(transfo);
1267 sPainter.setProjector(prj);
1268 sPainter.setBlending(true);
1269 sPainter.setColor(horizonPolygonLineColor, landFader.getInterstate());
1270 const float lineWidth=sPainter.getLineWidth();
1271 sPainter.setLineWidth(GETSTELMODULE(LandscapeMgr)->getPolyLineThickness()*ppx);
1272 sPainter.drawSphericalRegion(horizonPolygon.data(), StelPainter::SphericalPolygonDrawModeBoundary);
1273 sPainter.setLineWidth(lineWidth);
1274 }
1275 //else qDebug() << "no polygon defined";
1276 sPainter.setCullFace(false);
1277 drawLabels(core, &sPainter);
1278 }
1279
1280 //! Sample landscape texture for transparency. May be used for advanced visibility computation like sunrise on the visible horizon etc.
1281 //! @param azalt: normalized direction in alt-az frame
1282 //! @retval alpha (0..1), where 0=fully transparent.
getOpacity(Vec3d azalt) const1283 float LandscapeSpherical::getOpacity(Vec3d azalt) const
1284 {
1285 if(!validLandscape || (!horizonPolygon && (!mapImage || mapImage->isNull()))) return (azalt[2]>0.0 ? 0.0f : 1.0f); // can happen if image is misconfigured and failed to load.
1286
1287 if (angleRotateZOffset!=0.0f)
1288 azalt.transfo4d(Mat4d::zrotation(static_cast<double>(angleRotateZOffset)));
1289
1290 // in case we also have a horizon polygon defined, this is trivial and fast.
1291 if (horizonPolygon)
1292 {
1293 if (horizonPolygon->contains(azalt)) return 1.0f; else return 0.0f;
1294 }
1295 // Else, sample the image...
1296 float az, alt_rad;
1297 StelUtils::rectToSphe(&az, &alt_rad, azalt);
1298
1299 // QImage has pixel 0/0 in top left corner. We must first find image Y for optionally cropped images.
1300 // It is possible that sample position is outside cropped texture. in this case, assume full transparency and exit early.
1301
1302 const float alt_pm1 = 2.0f * alt_rad / M_PIf; // sampled altitude, -1...+1 linear in altitude angle
1303 const float img_top_pm1 = 1.0f-2.0f*(mapTexTop / M_PIf); // the top line in -1..+1
1304 if (alt_pm1>img_top_pm1) return 0.0f;
1305 const float img_bot_pm1 = 1.0f-2.0f*(mapTexBottom / M_PIf); // the bottom line in -1..+1
1306 if (alt_pm1<img_bot_pm1) return 1.0f; // rare case of a hole in the ground. Even though there is a visible hole, play opaque.
1307
1308 float y_img_1=(alt_pm1-img_bot_pm1)/(img_top_pm1-img_bot_pm1); // the sampled altitude in 0..1 image height from bottom
1309 Q_ASSERT(y_img_1<=1.f);
1310 Q_ASSERT(y_img_1>=0.f);
1311
1312 int y=static_cast<int>((1.0f-y_img_1)*mapImage->height()); // pixel Y from top.
1313
1314 az = (M_PIf-az) / M_PIf; // 0..2 = N.E.S.W.N
1315
1316 const float xShift=static_cast<float>(angleRotateZ) /M_PIf; // shift value in -2..2
1317 float az_phot=az - 0.5f - xShift; // The 0.5 is caused by regular pano left edge being East. The xShift compensates any configured angleRotateZ
1318 az_phot=fmodf(az_phot, 2.0f);
1319 if (az_phot<0) az_phot+=2.0f; // 0..2 = image-X
1320
1321 int x=static_cast<int>((az_phot*0.5f) * mapImage->width()); // pixel X from left.
1322
1323 QRgb pixVal=mapImage->pixel(x, y);
1324 /*
1325 #ifndef NDEBUG
1326 // GZ: please leave the comment available for further development!
1327 qDebug() << "Landscape sampling: az=" << az*180.0 << "° alt=" << alt_pm1*90.0f
1328 << "°, xShift[-2..+2]=" << xShift << " az_phot[0..2]=" << az_phot
1329 << ", w=" << mapImage->width() << " h=" << mapImage->height()
1330 << " --> x:" << x << " y:" << y << " alpha:" << qAlpha(pixVal)/255.0f;
1331 #endif
1332 */
1333 return qAlpha(pixVal)/255.0f;
1334 }
1335