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