1 /*
2 * Copyright (C) 2008 Fabien Chereau
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
17 */
18
19 #include "StelSkyImageTile.hpp"
20 #include "StelTextureMgr.hpp"
21 #include "StelApp.hpp"
22 #include "StelFileMgr.hpp"
23 #include "StelUtils.hpp"
24 #include "StelTexture.hpp"
25 #include "StelProjector.hpp"
26 #include "StelToneReproducer.hpp"
27 #include "StelCore.hpp"
28 #include "StelSkyDrawer.hpp"
29 #include "StelPainter.hpp"
30 #include "StelModuleMgr.hpp"
31 #include "SolarSystem.hpp"
32 #include <QDebug>
33
34 #include <cstdio>
35
StelSkyImageTile()36 StelSkyImageTile::StelSkyImageTile()
37 {
38 initCtor();
39 }
40
initCtor()41 void StelSkyImageTile::initCtor()
42 {
43 minResolution = -1;
44 luminance = -1;
45 alphaBlend = false;
46 noTexture = false;
47 texFader = Q_NULLPTR;
48 birthJD = -1e10;
49 withAberration = true;
50 }
51
52 // Constructor
StelSkyImageTile(const QString & url,StelSkyImageTile * parent)53 StelSkyImageTile::StelSkyImageTile(const QString& url, StelSkyImageTile* parent) : MultiLevelJsonBase(parent)
54 {
55 initCtor();
56 if (parent!=Q_NULLPTR)
57 {
58 luminance = parent->luminance;
59 alphaBlend = parent->alphaBlend;
60 }
61 initFromUrl(url);
62 }
63
64 // Constructor from a map used for JSON files with more than 1 level
StelSkyImageTile(const QVariantMap & map,StelSkyImageTile * parent)65 StelSkyImageTile::StelSkyImageTile(const QVariantMap& map, StelSkyImageTile* parent) : MultiLevelJsonBase(parent)
66 {
67 initCtor();
68 if (parent!=Q_NULLPTR)
69 {
70 luminance = parent->luminance;
71 alphaBlend = parent->alphaBlend;
72 }
73 initFromQVariantMap(map);
74 }
75
76 // Destructor
~StelSkyImageTile()77 StelSkyImageTile::~StelSkyImageTile()
78 {
79 }
80
draw(StelCore * core,StelPainter & sPainter,float opacity)81 void StelSkyImageTile::draw(StelCore* core, StelPainter& sPainter, float opacity)
82 {
83 Q_UNUSED(opacity)
84 const StelProjectorP prj = core->getProjection(StelCore::FrameJ2000);
85
86 // compute aberration correction vector
87 Vec3d vel(0.0);
88 if ((core) && (core->getUseAberration()) && (core->getCurrentPlanet()) && (withAberration))
89 {
90 vel=core->getCurrentPlanet()->getHeliocentricEclipticVelocity();
91 vel=StelCore::matVsop87ToJ2000*vel*core->getAberrationFactor()*(AU/(86400.0*SPEED_OF_LIGHT));
92 }
93
94 const float limitLuminance = core->getSkyDrawer()->getLimitLuminance();
95 QMultiMap<double, StelSkyImageTile*> result;
96 // TODO: adjust that viewportconvexpolygon by aberration to select the right tiles.
97 // I thought the viewportpolygon needs to be enlarged just a bit. (I use 20 arcseconds here as estimate of max. aberration from earth.)
98 //getTilesToDraw(result, core, prj->getViewportConvexPolygon(0,0)->getEnlarged(20./3600.*M_PI_180 *core->getAberrationFactor()), limitLuminance, true);
99 // But it seems not even the AllSky region prevents clipping, so it must be caused somewhere else.
100 // const SphericalCap& hp = prj->getBoundingCap();
101 // getTilesToDraw(result, core, SphericalRegionP(new SphericalCap(hp)), limitLuminance, true);
102 getTilesToDraw(result, core, SphericalRegionP(new AllSkySphericalRegion()), limitLuminance, true);
103
104 int numToBeLoaded=0;
105 for (auto* t : result)
106 if (t->isReadyToDisplay()==false)
107 ++numToBeLoaded;
108 updatePercent(result.size(), numToBeLoaded);
109
110 // Draw in the good order
111 sPainter.setBlending(true, GL_ONE, GL_ONE);
112 auto i = result.end();
113 while (i!=result.begin())
114 {
115 --i;
116 i.value()->drawTile(core, sPainter, vel);
117 }
118
119 deleteUnusedSubTiles();
120 }
121
122 // Return the list of tiles which should be drawn.
getTilesToDraw(QMultiMap<double,StelSkyImageTile * > & result,StelCore * core,const SphericalRegionP & viewPortPoly,float limitLuminance,bool recheckIntersect)123 void StelSkyImageTile::getTilesToDraw(QMultiMap<double, StelSkyImageTile*>& result, StelCore* core, const SphericalRegionP& viewPortPoly, float limitLuminance, bool recheckIntersect)
124 {
125 #ifndef NDEBUG
126 // When this method is called, we can assume that:
127 // - the parent tile min resolution was reached
128 // - the parent tile is intersecting FOV
129 // - the parent tile is not scheduled for deletion
130 const StelSkyImageTile* parent = qobject_cast<StelSkyImageTile*>(QObject::parent());
131 if (parent!=Q_NULLPTR)
132 {
133 Q_ASSERT(isDeletionScheduled()==false);
134 const double degPerPixel = 1./core->getProjection(StelCore::FrameJ2000)->getPixelPerRadAtCenter()*M_180_PI;
135 Q_ASSERT(degPerPixel<parent->minResolution);
136
137 Q_ASSERT(parent->isDeletionScheduled()==false);
138 }
139 #endif
140
141 // An error occured during loading
142 if (errorOccured)
143 return;
144
145 // The JSON file is currently being downloaded
146 if (downloading)
147 {
148 //qDebug() << "Downloading " << contructorUrl;
149 return;
150 }
151
152 if (luminance>0 && luminance<limitLuminance)
153 {
154 // Schedule a deletion
155 scheduleChildsDeletion();
156 return;
157 }
158
159 if (birthJD>-1e10 && birthJD>core->getJD())
160 {
161 // Schedule a deletion
162 scheduleChildsDeletion();
163 return;
164 }
165
166 // Check that we are in the screen
167 bool fullInScreen = true;
168 bool intersectScreen = false;
169 if (recheckIntersect)
170 {
171 if (skyConvexPolygons.isEmpty())
172 {
173 // If no polygon is defined, we assume that the tile covers the whole sky
174 fullInScreen=false;
175 intersectScreen=true;
176 }
177 else
178 {
179 for (const auto& poly : skyConvexPolygons)
180 {
181 if (viewPortPoly->contains(poly))
182 {
183 intersectScreen = true;
184 }
185 else
186 {
187 fullInScreen = false;
188 if (viewPortPoly->intersects(poly))
189 intersectScreen = true;
190 }
191 }
192 }
193 }
194 // The tile is outside screen
195 if (fullInScreen==false && intersectScreen==false)
196 {
197 // Schedule a deletion
198 scheduleChildsDeletion();
199 return;
200 }
201
202 // The tile is in screen, and it is a precondition that its resolution is higher than the limit
203 // make sure that it's not going to be deleted
204 cancelDeletion();
205
206 if (noTexture==false)
207 {
208 if (!tex)
209 {
210 // The tile has an associated texture, but it is not yet loaded: load it now
211 StelTextureMgr& texMgr=StelApp::getInstance().getTextureManager();
212 tex = texMgr.createTextureThread(absoluteImageURI, StelTexture::StelTextureParams(true));
213 if (!tex)
214 {
215 qWarning() << "WARNING : Can't create tile: " << absoluteImageURI;
216 errorOccured = true;
217 return;
218 }
219 }
220
221 // The tile is in screen and has a texture: every test passed :) The tile will be displayed
222 result.insert(minResolution, this);
223 }
224
225 // Check if we reach the resolution limit
226 const float degPerPixel = 1.f/core->getProjection(StelCore::FrameJ2000)->getPixelPerRadAtCenter()*M_180_PIf;
227 if (degPerPixel < minResolution)
228 {
229 if (subTiles.isEmpty() && !subTilesUrls.isEmpty())
230 {
231 // Load the sub tiles because we reached the maximum resolution and they are not yet loaded
232 for (const auto& s : subTilesUrls)
233 {
234 StelSkyImageTile* nt;
235 if (s.type()==QVariant::Map)
236 nt = new StelSkyImageTile(s.toMap(), this);
237 else
238 {
239 Q_ASSERT(s.type()==QVariant::String);
240 nt = new StelSkyImageTile(s.toString(), this);
241 }
242 subTiles.append(nt);
243 }
244 }
245 // Try to add the subtiles
246 for (auto* tile : subTiles)
247 {
248 qobject_cast<StelSkyImageTile*>(tile)->getTilesToDraw(result, core, viewPortPoly, limitLuminance, !fullInScreen);
249 }
250 }
251 else
252 {
253 // The subtiles should not be displayed because their resolution is too high
254 scheduleChildsDeletion();
255 }
256 }
257
258 // Draw the image on the screen.
259 // Assume GL_TEXTURE_2D is enabled
drawTile(StelCore * core,StelPainter & sPainter,const Vec3d & vel)260 bool StelSkyImageTile::drawTile(StelCore* core, StelPainter& sPainter, const Vec3d &vel)
261 {
262 if (!tex->bind())
263 return false;
264
265 if (!texFader)
266 {
267 texFader = new QTimeLine(1000, this);
268 texFader->start();
269 }
270
271 // Draw the real texture for this image
272 float ad_lum = (luminance>0) ? qMin(1.0f, core->getToneReproducer()->adaptLuminanceScaled(luminance)) : 1.f;
273 Vec4f color;
274 if (alphaBlend || texFader->state()==QTimeLine::Running)
275 {
276 if (!alphaBlend)
277 sPainter.setBlending(true); // Normal transparency mode
278 else
279 sPainter.setBlending(true, GL_ONE, GL_ONE); // additive blending
280 color.set(ad_lum,ad_lum,ad_lum, static_cast<float>(texFader->currentValue()));
281 }
282 else
283 {
284 sPainter.setBlending(false);
285 color.set(ad_lum,ad_lum,ad_lum, 1.f);
286 }
287
288 const bool withExtinction=(getFrameType()!=StelCore::FrameAltAz && core->getSkyDrawer()->getFlagHasAtmosphere() && core->getSkyDrawer()->getExtinction().getExtinctionCoefficient()>=0.01f);
289
290 for (const auto& poly : skyConvexPolygons)
291 {
292 // Not sure: Are all skyConvexPolygons in J2000 frame? This would also simplify code below...
293 // No, by scripting we can have other frames!
294 //Q_ASSERT(getFrameType() == StelCore::FrameJ2000);
295
296 Vec4f extinctedColor = color;
297 if (withExtinction)
298 {
299 Vec3d bary= poly->getPointInside(); // This is a "frame" vector that points "somewhere" in the first triangle.
300
301 // 2017-03: we need a definite J2000 vector:
302 Vec3d baryJ2000;
303 double lng, lat, ra, dec; // aux. values for coordinate transformations
304 double eclJ2000, eclJDE;
305 //qDebug() << "Frame: " << getFrameType();
306 switch (getFrameType()) // all possible but AzAlt!
307 {
308 case StelCore::FrameJ2000:
309 baryJ2000=bary;
310 break;
311 case StelCore::FrameEquinoxEqu:
312 baryJ2000=core->equinoxEquToJ2000(bary, StelCore::RefractionOff);
313 break;
314 case StelCore::FrameObservercentricEclipticJ2000:
315 // For the ecliptic cases, apply clumsy trafos from StelUtil!
316 eclJ2000=GETSTELMODULE(SolarSystem)->getEarth()->getRotObliquity(2451545.0);
317 StelUtils::rectToSphe(&lng, &lat, bary);
318 StelUtils::eclToEqu(lng, lat, eclJ2000, &ra, &dec);
319 StelUtils::spheToRect(ra, dec, baryJ2000);
320 break;
321 case StelCore::FrameObservercentricEclipticOfDate:
322 // Trafo to eqDate, then as above.
323 eclJDE = GETSTELMODULE(SolarSystem)->getEarth()->getRotObliquity(core->getJDE());
324 StelUtils::rectToSphe(&lng, &lat, bary);
325 StelUtils::eclToEqu(lng, lat, eclJDE, &ra, &dec); // convert to Equatorial/equinox of date
326 StelUtils::spheToRect(ra, dec, bary);
327 baryJ2000=core->equinoxEquToJ2000(bary, StelCore::RefractionOff);
328 break;
329 case StelCore::FrameGalactic:
330 baryJ2000=core->galacticToJ2000(bary);
331 break;
332 case StelCore::FrameSupergalactic:
333 baryJ2000=core->supergalacticToJ2000(bary);
334 break;
335 default:
336 Q_ASSERT(0);
337 qDebug() << "StelSkyImageTile: unknown FrameType. Assume J2000.";
338 baryJ2000=bary;
339 }
340 Vec3d altAz = core->j2000ToAltAz(baryJ2000, StelCore::RefractionOff);
341 float extinctionMagnitude=0.0f;
342 altAz.normalize();
343 core->getSkyDrawer()->getExtinction().forward(altAz, &extinctionMagnitude);
344 // compute a simple factor from magnitude loss.
345 float extinctionFactor=std::pow(0.4f , extinctionMagnitude); // drop of one magnitude: factor 2.5 or 40%
346 extinctedColor[0]*=fabs(extinctionFactor);
347 extinctedColor[1]*=fabs(extinctionFactor);
348 extinctedColor[2]*=fabs(extinctionFactor);
349 }
350 sPainter.setColor(extinctedColor);
351 sPainter.drawSphericalRegion(poly.data(), StelPainter::SphericalPolygonDrawModeTextureFill, Q_NULLPTR, true, 5., vel);
352 }
353
354 #ifdef DEBUG_STELSKYIMAGE_TILE
355 if (debugFont==Q_NULLPTR)
356 {
357 debugFont = &StelApp::getInstance().getFontManager().getStandardFont(StelApp::getInstance().getLocaleMgr().getSkyLanguage(), 12);
358 }
359 color.set(1.0,0.5,0.5,1.0);
360 for (const auto& poly : skyConvexPolygons)
361 {
362 Vec3d win;
363 Vec3d bary = poly->getPointInside();
364 sPainter.getProjector()->project(bary,win);
365 sPainter.drawText(debugFont, win[0], win[1], getAbsoluteImageURI());
366 sPainter.enableTexture2d(false);
367 sPainter.drawSphericalRegion(poly.get(), StelPainter::SphericalPolygonDrawModeBoundary, &color);
368 sPainter.enableTexture2d(true);
369 }
370 #endif
371
372 return true;
373 }
374
375 // Return true if the tile is fully loaded and can be displayed
isReadyToDisplay() const376 bool StelSkyImageTile::isReadyToDisplay() const
377 {
378 return tex && tex->canBind();
379 }
380
381 // Load the tile from a valid QVariantMap
loadFromQVariantMap(const QVariantMap & map)382 void StelSkyImageTile::loadFromQVariantMap(const QVariantMap& map)
383 {
384 if (map.contains("imageCredits"))
385 {
386 QVariantMap dsCredits = map.value("imageCredits").toMap();
387 dataSetCredits.shortCredits = dsCredits.value("short").toString();
388 dataSetCredits.fullCredits = dsCredits.value("full").toString();
389 dataSetCredits.infoURL = dsCredits.value("infoUrl").toString();
390 }
391 if (map.contains("serverCredits"))
392 {
393 QVariantMap sCredits = map.value("serverCredits").toMap();
394 serverCredits.shortCredits = sCredits.value("short").toString();
395 serverCredits.fullCredits = sCredits.value("full").toString();
396 serverCredits.infoURL = sCredits.value("infoUrl").toString();
397 }
398 if (map.contains("description"))
399 {
400 htmlDescription = map.value("description").toString();
401 if (parent()==Q_NULLPTR)
402 {
403 htmlDescription+= "<h3>URL: "+constructorUrl+"</h3>";
404 }
405 }
406 else
407 {
408 if (parent()==Q_NULLPTR)
409 {
410 htmlDescription= "<h3>URL: "+constructorUrl+"</h3>";
411 }
412 }
413
414 shortName = map.value("shortName").toString();
415 if (shortName.isEmpty())
416 shortName = "no name";
417 bool ok=false;
418 if (!map.contains("minResolution"))
419 throw std::runtime_error(qPrintable(QString("minResolution is mandatory")));
420 minResolution = map.value("minResolution").toFloat(&ok);
421 if (!ok)
422 {
423 throw std::runtime_error(qPrintable(QString("minResolution expects a double value, found: \"%1\"").arg(map.value("minResolution").toString())));
424 }
425
426 if (map.contains("luminance"))
427 {
428 luminance = map.value("luminance").toFloat(&ok);
429 if (!ok)
430 throw std::runtime_error("luminance expects a float value");
431 qWarning() << "luminance in preview JSON files is deprecated. Replace with maxBrightness.";
432 }
433
434 if (map.contains("maxBrightness"))
435 {
436 // maxBrightness is the maximum nebula brightness in Vmag/arcmin^2
437 luminance = map.value("maxBrightness").toFloat(&ok);
438 if (!ok)
439 throw std::runtime_error("maxBrightness expects a float value");
440 luminance = StelApp::getInstance().getCore()->getSkyDrawer()->surfaceBrightnessToLuminance(luminance);
441 }
442
443 if (map.contains("alphaBlend"))
444 {
445 alphaBlend = map.value("alphaBlend").toBool();
446 }
447
448 // Load the convex polygons (if any)
449 QVariantList polyList = map.value("skyConvexPolygons").toList();
450 if (polyList.empty())
451 polyList = map.value("worldCoords").toList();
452 else
453 qWarning() << "skyConvexPolygons in preview JSON files is deprecated. Replace with worldCoords.";
454
455 // Load the matching textures positions (if any)
456 QVariantList texCoordList = map.value("textureCoords").toList();
457 if (!texCoordList.isEmpty() && polyList.size()!=texCoordList.size())
458 throw std::runtime_error("the number of convex polygons does not match the number of texture space polygon");
459
460 for (int i=0;i<polyList.size();++i)
461 {
462 const QVariant& polyRaDec = polyList.at(i);
463 QVector<Vec3d> vertices;
464 for (const auto& vRaDec : polyRaDec.toList())
465 {
466 const QVariantList vl = vRaDec.toList();
467 Vec3d v;
468 StelUtils::spheToRect(vl.at(0).toFloat(&ok)*M_PI_180f, vl.at(1).toFloat(&ok)*M_PI_180f, v);
469 if (!ok)
470 throw std::runtime_error("wrong Ra and Dec, expect a double value");
471 vertices.append(v);
472 }
473 Q_ASSERT(vertices.size()==4);
474
475 if (!texCoordList.isEmpty())
476 {
477 const QVariant& polyXY = texCoordList.at(i);
478 QVector<Vec2f> texCoords;
479 for (const auto& vXY : polyXY.toList())
480 {
481 const QVariantList vl = vXY.toList();
482 texCoords.append(Vec2f(vl.at(0).toFloat(&ok), vl.at(1).toFloat(&ok)));
483 if (!ok)
484 throw std::runtime_error("wrong X and Y, expect a double value");
485 }
486 Q_ASSERT(texCoords.size()==4);
487
488 SphericalTexturedConvexPolygon* pol = new SphericalTexturedConvexPolygon(vertices, texCoords);
489 Q_ASSERT(pol->checkValid());
490 skyConvexPolygons.append(SphericalRegionP(pol));
491 }
492 else
493 {
494 SphericalConvexPolygon* pol = new SphericalConvexPolygon(vertices);
495 Q_ASSERT(pol->checkValid());
496 skyConvexPolygons.append(SphericalRegionP(pol));
497 }
498 }
499
500 if (map.contains("imageUrl"))
501 {
502 QString imageUrl = map.value("imageUrl").toString();
503 if (baseUrl.startsWith("http", Qt::CaseInsensitive))
504 {
505 absoluteImageURI = baseUrl+imageUrl;
506 }
507 else
508 {
509 absoluteImageURI = StelFileMgr::findFile(baseUrl+imageUrl);
510 if (absoluteImageURI.isEmpty())
511 {
512 // Maybe the user meant a file in stellarium local files
513 absoluteImageURI = imageUrl;
514 }
515 }
516 }
517 else
518 noTexture = true;
519
520 birthJD = map.value("birthJD", -1e10).toDouble();
521
522 // This is a list of URLs to the child tiles or a list of already loaded map containing child information
523 // (in this later case, the StelSkyImageTile objects will be created later)
524 subTilesUrls = map.value("subTiles").toList();
525 for (auto& variant : subTilesUrls)
526 {
527 if (variant.type() == QVariant::Map)
528 {
529 // Check if the JSON object is a reference, i.e. if it contains a $ref key
530 QVariantMap m = variant.toMap();
531 if (m.size()==1 && m.contains("$ref"))
532 {
533 variant = QString(m["$ref"].toString());
534 }
535 }
536 }
537 // if (subTilesUrls.size()>10)
538 // {
539 // qWarning() << "Large tiles number for " << shortName << ": " << subTilesUrls.size();
540 // }
541
542 withAberration = map.value("withAberration", true).toBool();
543 }
544
545 // Convert the image informations to a map following the JSON structure.
toQVariantMap() const546 QVariantMap StelSkyImageTile::toQVariantMap() const
547 {
548 QVariantMap res;
549
550 // Image credits
551 QVariantMap imCredits;
552 if (!dataSetCredits.shortCredits.isEmpty())
553 imCredits["short"]=dataSetCredits.shortCredits;
554 if (!dataSetCredits.fullCredits.isEmpty())
555 imCredits["full"]=dataSetCredits.fullCredits;
556 if (!dataSetCredits.infoURL.isEmpty())
557 imCredits["infoUrl"]=dataSetCredits.infoURL;
558 if (!imCredits.empty())
559 res["imageCredits"]=imCredits;
560
561 // Server credits
562 QVariantMap serCredits;
563 if (!serverCredits.shortCredits.isEmpty())
564 imCredits["short"]=serverCredits.shortCredits;
565 if (!serverCredits.fullCredits.isEmpty())
566 imCredits["full"]=serverCredits.fullCredits;
567 if (!serverCredits.infoURL.isEmpty())
568 imCredits["infoUrl"]=serverCredits.infoURL;
569 if (!serCredits.empty())
570 res["serverCredits"]=serCredits;
571
572 // Misc
573 if (!shortName.isEmpty())
574 res["shortName"] = shortName;
575 if (minResolution>0)
576 res["minResolution"]=minResolution;
577 if (luminance>0)
578 res["maxBrightness"]=StelApp::getInstance().getCore()->getSkyDrawer()->luminanceToSurfacebrightness(luminance);
579 if (alphaBlend)
580 res["alphaBlend"]=true;
581 if (noTexture==false)
582 res["imageUrl"]=absoluteImageURI;
583 if (birthJD>-1e10)
584 res["birthJD"]=birthJD;
585 res["withAberration"] = withAberration;
586
587 // Polygons
588 // TODO
589
590 // textures positions
591 // TODO
592
593 if (!subTilesUrls.empty())
594 {
595 res["subTiles"] = subTilesUrls;
596 }
597
598 return res;
599 }
600