File indexing completed on 2024-04-21 03:44:45

0001 /*
0002     SPDX-FileCopyrightText: 2021 Hy Murveit <hy@murveit.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "terrainrenderer.h"
0008 
0009 #include "kstars_debug.h"
0010 #include "Options.h"
0011 #include "skymap.h"
0012 #include "skyqpainter.h"
0013 #include "projections/projector.h"
0014 #include "projections/equirectangularprojector.h"
0015 #include "skypoint.h"
0016 #include "kstars.h"
0017 
0018 #include <QStatusBar>
0019 
0020 // This is the factory that builds the one-and-only TerrainRenderer.
0021 TerrainRenderer * TerrainRenderer::_terrainRenderer = nullptr;
0022 TerrainRenderer *TerrainRenderer::Instance()
0023 {
0024     if (_terrainRenderer == nullptr)
0025         _terrainRenderer = new TerrainRenderer();
0026 
0027     return _terrainRenderer;
0028 }
0029 
0030 // This class implements a quick 2D float array.
0031 class TerrainLookup
0032 {
0033     public:
0034         TerrainLookup(int width, int height) :
0035             valPtr(new float[width * height]), valWidth(width)
0036         {
0037             memset(valPtr, 0, width * height * sizeof(float));
0038         }
0039         ~TerrainLookup()
0040         {
0041             delete[] valPtr;
0042         }
0043         inline float get(int w, int h)
0044         {
0045             return valPtr[h * valWidth + w];
0046         }
0047         inline void set(int w, int h, float val)
0048         {
0049             valPtr[h * valWidth + w] = val;
0050         }
0051     private:
0052         float *valPtr;
0053         int valWidth = 0;
0054 };
0055 
0056 // Samples 2-D array and returns interpolated values for the unsampled elements.
0057 // Used to speed up the calculation of azimuth and altitude values for all pixels
0058 // of the array to be rendered. Instead we compute every nth pixel's coordinates (e.g. n=4)
0059 // and interpolate the azimuth and alitutde values (so 1/16 the number of calculations).
0060 // This should be more than accurate enough for this rendering, and this part
0061 // of the code definitely needs speed.
0062 class InterpArray
0063 {
0064     public:
0065         // Constructor calculates the downsampled size and allocates the 2D arrays
0066         // for azimuth and altitude.
0067         InterpArray(int width, int height, int samplingFactor) : sampling(samplingFactor)
0068         {
0069             int downsampledWidth = width / sampling;
0070             if (width % sampling != 0)
0071                 downsampledWidth += 1;
0072             lastDownsampledCol = downsampledWidth - 1;
0073 
0074             int downsampledHeight = height / sampling;
0075             if (height % sampling != 0)
0076                 downsampledHeight += 1;
0077             lastDownsampledRow = downsampledHeight - 1;
0078 
0079             azLookup = new TerrainLookup(downsampledWidth, downsampledHeight);
0080             altLookup = new TerrainLookup(downsampledWidth, downsampledHeight);
0081         }
0082 
0083         ~InterpArray()
0084         {
0085             delete azLookup;
0086             delete altLookup;
0087         }
0088 
0089         // Get the azimuth and altitude values from the 2D arrays.
0090         // Inputs are a full-image position
0091         inline void get(int x, int y, float *az, float *alt)
0092         {
0093             const bool rowSampled = y % sampling == 0;
0094             const bool colSampled = x % sampling == 0;
0095             const int xSampled = x / sampling;
0096             const int ySampled = y / sampling;
0097 
0098             // If the requested position has been calculated, the use the stored value.
0099             if (rowSampled && colSampled)
0100             {
0101                 *az = azLookup->get(xSampled, ySampled);
0102                 *alt = altLookup->get(xSampled, ySampled);
0103                 return;
0104             }
0105             // The desired position has not been calculated, but values on its row have been calculated.
0106             // Interpolate between the nearest values on the row.
0107             else if (rowSampled)
0108             {
0109                 if (xSampled >= lastDownsampledCol)
0110                 {
0111                     // If the requested value is on or past the last calculated value,
0112                     // just use that last value.
0113                     *az = azLookup->get(xSampled, ySampled);
0114                     *alt = altLookup->get(xSampled, ySampled);
0115                     return;
0116                 }
0117                 // Weight the contributions of the 2 calculated values on the row by their distance to the desired position.
0118                 const float weight2 = static_cast<float>(x) / sampling - xSampled;
0119                 const float weight1 = 1 - weight2;
0120                 *az  = weight1 *  azLookup->get(xSampled, ySampled) + weight2 *  azLookup->get(xSampled + 1, ySampled);
0121                 *alt = weight1 * altLookup->get(xSampled, ySampled) + weight2 * altLookup->get(xSampled + 1, ySampled);
0122                 return;
0123             }
0124             // The desired position has not been calculated, but values on its column have been calculated.
0125             // Interpolate between the nearest values on the column.
0126             else if (colSampled)
0127             {
0128                 if (ySampled >= lastDownsampledRow)
0129                 {
0130                     // If the requested value is on or past the last calculated value,
0131                     // just use that last value.
0132                     *az = azLookup->get(xSampled, ySampled);
0133                     *alt = altLookup->get(xSampled, ySampled);
0134                     return;
0135                 }
0136                 // Weight the contributions of the 2 calculated values on the column by their distance to the desired position.
0137                 const float weight2 = static_cast<float>(y) / sampling - ySampled;
0138                 const float weight1 = 1 - weight2;
0139                 *az  = weight1 *  azLookup->get(xSampled, ySampled) + weight2 *  azLookup->get(xSampled, ySampled + 1);
0140                 *alt = weight1 * altLookup->get(xSampled, ySampled) + weight2 * altLookup->get(xSampled, ySampled + 1);
0141                 return;
0142             }
0143             else
0144             {
0145                 // The desired position has not been calculated, and no values on its column nor row have been calculated.
0146                 // Interpolate between the nearest 4 values.
0147                 if (xSampled >= lastDownsampledCol || ySampled >= lastDownsampledRow)
0148                 {
0149                     // If we're near the edge, just use a close value. Harder to interpolate and not too important.
0150                     *az = azLookup->get(xSampled, ySampled);
0151                     *alt = altLookup->get(xSampled, ySampled);
0152                     return;
0153                 }
0154                 // The section uses distance along the two nearest calculated rows to come up with an estimate.
0155                 const float weight2 = static_cast<float>(x) / sampling - xSampled;
0156                 const float weight1 = 1 - weight2;
0157                 const float azWval  = (weight1  * ( azLookup->get(xSampled,      ySampled) +  azLookup->get(xSampled,     ySampled + 1)) +
0158                                        weight2  * ( azLookup->get(xSampled + 1,  ySampled) +  azLookup->get(xSampled + 1, ySampled + 1)));
0159                 const float altWval = (weight1  * (altLookup->get(xSampled,      ySampled) + altLookup->get(xSampled,     ySampled + 1)) +
0160                                        weight2  * (altLookup->get(xSampled + 1,  ySampled) + altLookup->get(xSampled + 1, ySampled + 1)));
0161 
0162                 // The section uses distance along the two nearest calculated columns to come up with an estimate.
0163                 const float hweight2 = static_cast<float>(y) / sampling - ySampled;
0164                 const float hweight1 = 1 - hweight2;
0165                 const float azHval =  (hweight2 * ( azLookup->get(xSampled,  ySampled)     +  azLookup->get(xSampled + 1, ySampled)) +
0166                                        hweight1 * ( azLookup->get(xSampled,  ySampled + 1) +  azLookup->get(xSampled + 1, ySampled + 1)));
0167                 const float altHval = (hweight2 * (altLookup->get(xSampled,  ySampled)     + altLookup->get(xSampled + 1, ySampled)) +
0168                                        hweight1 * (altLookup->get(xSampled,  ySampled + 1) + altLookup->get(xSampled + 1, ySampled + 1)));
0169                 // We average the 2 estimates (note above actually estimated twice the value).
0170                 *az  = (azWval + azHval) / 4.0;
0171                 *alt = (altWval + altHval) / 4.0;
0172                 return;
0173             }
0174         }
0175         TerrainLookup *azimuthLookup()
0176         {
0177             return azLookup;
0178         }
0179         TerrainLookup *altitudeLookup()
0180         {
0181             return altLookup;
0182         }
0183 
0184     private:
0185         // These are the indeces of the last rows and columns (in the downsampled sized space) that were filled.
0186         // This is needed because the downsample factor might not be an even multiple of the image size.
0187         int lastDownsampledCol = 0;
0188         int lastDownsampledRow = 0;
0189         // The downsample factor.
0190         int sampling = 0;
0191         // The azimuth and altitude values are stored in these 2D arrays.
0192         TerrainLookup *azLookup = nullptr;
0193         TerrainLookup *altLookup = nullptr;
0194 };
0195 
0196 TerrainRenderer::TerrainRenderer()
0197 {
0198 }
0199 
0200 // Put degrees in the range of 0 -> 359.99999999
0201 double rationalizeAz(double degrees)
0202 {
0203     if (degrees < -1000 || degrees > 1000)
0204         return 0;
0205 
0206     while (degrees < 0)
0207         degrees += 360.0;
0208     while (degrees >= 360.0)
0209         degrees -= 360.0;
0210     return degrees;
0211 }
0212 
0213 // Checks that degrees in the range of -90 -> 90.
0214 double rationalizeAlt(double degrees)
0215 {
0216     if (degrees > 90.0)
0217         return 90.0;
0218     if (degrees < -90)
0219         return -90;
0220     return degrees;
0221 }
0222 
0223 // Assumes the source photosphere has rows which, left-to-right go from AZ=0 to AZ=360
0224 // and columns go from -90 altitude on the bottom to +90 on top.
0225 // Returns the pixel for the desired azimuth and altitude.
0226 QRgb TerrainRenderer::getPixel(double az, double alt) const
0227 {
0228     az = rationalizeAz(az + Options::terrainSourceCorrectAz());
0229     // This may make alt > 90 (due to a negative sourceCorrectAlt).
0230     // If so, it returns 0, which is a transparent pixel.
0231     alt = alt - Options::terrainSourceCorrectAlt();
0232     if (az < 0 || az >= 360 || alt < -90 || alt > 90)
0233         return(0);
0234 
0235     // shift az to be -180 to 180
0236     if (az > 180)
0237         az = az - 360.0;
0238     const int width = sourceImage.width();
0239     const int height = sourceImage.height();
0240 
0241     if (!Options::terrainSmoothPixels())
0242     {
0243         // az=0 should be the middle of the image.
0244         int pixX = width / 2 + (az / 360.0) * width;
0245         if (pixX > width - 1)
0246             pixX = width - 1;
0247         else if (pixX < 0)
0248             pixX = 0;
0249         int pixY = ((alt + 90.0) / 180.0) * height;
0250         if (pixY > height - 1)
0251             pixY = height - 1;
0252         pixY = (height - 1) - pixY;
0253         return sourceImage.pixel(pixX, pixY);
0254     }
0255 
0256     // Get floating point pixel positions so we can interpolate.
0257     float pixX = width / 2 + (az / 360.0) * width;
0258     if (pixX > width - 1)
0259         pixX = width - 1;
0260     else if (pixX < 0)
0261         pixX = 0;
0262     float pixY = ((alt + 90.0) / 180.0) * height;
0263     if (pixY > height - 1)
0264         pixY = height - 1;
0265     pixY = (height - 1) - pixY;
0266 
0267     // Don't bother interpolating for transparent pixels.
0268     constexpr int lowAlpha = 0.1 * 255;
0269     if (qAlpha(sourceImage.pixel(pixX, pixY)) < lowAlpha)
0270         return sourceImage.pixel(pixX, pixY);
0271 
0272     // Instead of just returning the pixel at the truncated position as above,
0273     // below we interpolate the pixel RGBA values based on the floating-point pixel position.
0274     int x1 = static_cast<int>(pixX);
0275     int y1 = static_cast<int>(pixY);
0276 
0277     if ((x1 >= width - 1) || (y1 >= height - 1))
0278         return sourceImage.pixel(x1, y1);
0279 
0280     // weights for the x & x+1, and y & y+1 positions.
0281     float wx2 = pixX - x1;
0282     float wx1 = 1.0 - wx2;
0283     float wy2 = pixY - y1;
0284     float wy1 = 1.0 - wy2;
0285 
0286     // The pixels we'll interpolate.
0287     QRgb c11(qUnpremultiply(sourceImage.pixel(pixX, pixY)));
0288     QRgb c12(qUnpremultiply(sourceImage.pixel(pixX, pixY + 1)));
0289     QRgb c21(qUnpremultiply(sourceImage.pixel(pixX + 1, pixY)));
0290     QRgb c22(qUnpremultiply(sourceImage.pixel(pixX + 1, pixY + 1)));
0291 
0292     // Weights for the above pixels.
0293     float w11 = wx1 * wy1;
0294     float w12 = wx1 * wy2;
0295     float w21 = wx2 * wy1;
0296     float w22 = wx2 * wy2;
0297 
0298     // Finally, interpolate each component.
0299     int red =   w11 * qRed(c11)   + w12 * qRed(c12)   + w21 * qRed(c21)   + w22 * qRed(c22);
0300     int green = w11 * qGreen(c11) + w12 * qGreen(c12) + w21 * qGreen(c21) + w22 * qGreen(c22);
0301     int blue =  w11 * qBlue(c11)  + w12 * qBlue(c12)  + w21 * qBlue(c21)  + w22 * qBlue(c22);
0302     int alpha = w11 * qAlpha(c11)  + w12 * qAlpha(c12)  + w21 * qAlpha(c21)  + w22 * qAlpha(c22);
0303     return qPremultiply(qRgba(red, green, blue, alpha));
0304 }
0305 
0306 // Checks to see if the view is the same as the last call to render.
0307 // If true, render (below) will skip its computations and return the same image
0308 // as was previously calculated.
0309 // If the view is not the same, this method stores away the details of the current view
0310 // so that it may make this comparison again in the future.
0311 bool TerrainRenderer::sameView(const Projector *proj, bool forceRefresh)
0312 {
0313     ViewParams view = proj->viewParams();
0314     SkyPoint point = *(view.focus);
0315     const auto &lst = KStarsData::Instance()->lst();
0316     const auto &lat = KStarsData::Instance()->geo()->lat();
0317 
0318     // We compare az and alt, not RA and DEC, as the RA changes,
0319     // but the rendering is tied to azimuth and altitude which may not change.
0320     // Thus we convert point to horizontal coordinates.
0321     point.EquatorialToHorizontal(lst, lat);
0322     const double az = rationalizeAz(point.az().Degrees());
0323     const double alt = rationalizeAlt(point.alt().Degrees());
0324 
0325     bool ok = view.width == savedViewParams.width &&
0326               view.height == savedViewParams.height &&
0327               view.zoomFactor == savedViewParams.zoomFactor &&
0328               view.rotationAngle == savedViewParams.rotationAngle &&
0329               view.useRefraction == savedViewParams.useRefraction &&
0330               view.useAltAz == savedViewParams.useAltAz &&
0331               view.fillGround == savedViewParams.fillGround;
0332     const double azDiff = fabs(savedAz - az);
0333     const double altDiff = fabs(savedAlt - alt);
0334     if (!forceRefresh && ok && azDiff < .0001 && altDiff < .0001)
0335         return true;
0336 
0337     // Store the view
0338     savedViewParams = view;
0339     savedViewParams.focus = nullptr;
0340     savedAz = az;
0341     savedAlt = alt;
0342     return false;
0343 }
0344 
0345 bool TerrainRenderer::render(uint16_t w, uint16_t h, QImage *terrainImage, const Projector *proj)
0346 {
0347     // This is used to force a re-render, e.g. when the image is changed.
0348     bool dirty = false;
0349 
0350     // Load the image if necessary.
0351     QString filename = Options::terrainSource();
0352     if (!initialized || filename != sourceFilename)
0353     {
0354         dirty = true;
0355         QImage image;
0356         if (image.load(filename))
0357         {
0358             sourceImage = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
0359             qCDebug(KSTARS) << QString("Read terrain file %1 x %2").arg(sourceImage.width()).arg(sourceImage.height());
0360             sourceFilename = filename;
0361             initialized = true;
0362         }
0363         else
0364         {
0365             if (filename.isEmpty())
0366                 KStars::Instance()->statusBar()->showMessage(i18n("Failed to load terrain. Set terrain file in Settings."));
0367             else
0368                 KStars::Instance()->statusBar()->showMessage(i18n("Failed to load terrain image (%1). Set terrain file in Settings.",
0369                         filename));
0370             initialized = false;
0371             Options::setShowTerrain(false);
0372             KStars::Instance()->syncOps();
0373         }
0374     }
0375 
0376     if (!initialized)
0377         return false;
0378 
0379     // Check if parameters have changed.
0380     if ((terrainDownsampling != Options::terrainDownsampling()) ||
0381             (terrainSmoothPixels != Options::terrainSmoothPixels()) ||
0382             (terrainSkipSpeedup != Options::terrainSkipSpeedup()) ||
0383             (terrainTransparencySpeedup != Options::terrainTransparencySpeedup()) ||
0384             (terrainSourceCorrectAz != Options::terrainSourceCorrectAz()) ||
0385             (terrainSourceCorrectAlt != Options::terrainSourceCorrectAlt()))
0386         dirty = true;
0387 
0388     terrainDownsampling = Options::terrainDownsampling();
0389     terrainSmoothPixels = Options::terrainSmoothPixels();
0390     terrainSkipSpeedup = Options::terrainSkipSpeedup();
0391     terrainTransparencySpeedup = Options::terrainTransparencySpeedup();
0392     terrainSourceCorrectAz = Options::terrainSourceCorrectAz();
0393     terrainSourceCorrectAlt = Options::terrainSourceCorrectAlt();
0394 
0395     if (sameView(proj, dirty))
0396     {
0397         // Just return the previous image if the input view hasn't changed.
0398         *terrainImage = savedImage.copy();
0399         return true;
0400     }
0401 
0402     QElapsedTimer timer;
0403     timer.start();
0404 
0405     // Only compute the pixel's az and alt values for every Nth pixel.
0406     // Get the other pixel az and alt values by interpolation.
0407     // This saves a lot of time.
0408     const int sampling = Options::terrainDownsampling();
0409     InterpArray interp(w, h, sampling);
0410     QElapsedTimer setupTimer;
0411     setupTimer.start();
0412     setupLookup(w, h, sampling, proj, interp.azimuthLookup(), interp.altitudeLookup());
0413 
0414     const double setupTime = setupTimer.elapsed() / 1000.0; ///////////////////
0415 
0416     // Another speedup. If true, our calculations are downsampled by 2 in each dimension.
0417     const bool skip = Options::terrainSkipSpeedup() || SkyMap::IsSlewing();
0418     int increment = skip ? 2 : 1;
0419 
0420     // Assign transparent pixels everywhere by default.
0421     terrainImage->fill(0);
0422 
0423     // Go through the image, and for each pixel, using the previously computed az and alt values
0424     // get the corresponding pixel from the terrain image.
0425     bool lastTransparent = false;
0426     for (int j = 0; j < h; j += increment)
0427     {
0428         bool notLastRow = j != h - 1;
0429         for (int i = 0; i < w; i += increment)
0430         {
0431             if (lastTransparent && Options::terrainTransparencySpeedup())
0432             {
0433                 // Speedup--if the last pixel was transparent, then this
0434                 // one is assumed transparent too (but next is calculated).
0435                 lastTransparent = false;
0436                 continue;
0437             }
0438 
0439             const QPointF imgPoint(i, j);
0440             bool equiRectangular = (proj->type() == Projector::Equirectangular);
0441             bool usable = equiRectangular ? !dynamic_cast<const EquirectangularProjector*>(proj)->unusablePoint(imgPoint)
0442                           : !proj->unusablePoint(imgPoint);
0443             if (usable)
0444             {
0445                 float az, alt;
0446                 interp.get(i, j, &az, &alt);
0447                 const QRgb pixel = getPixel(az, alt);
0448                 terrainImage->setPixel(i, j, pixel);
0449                 lastTransparent = (pixel == 0);
0450 
0451                 if (skip)
0452                 {
0453                     // If we've skipped, fill in the missing pixels.
0454                     bool notLastCol = i != w - 1;
0455                     if (notLastCol)
0456                         terrainImage->setPixel(i + 1, j, pixel);
0457                     if (notLastRow)
0458                         terrainImage->setPixel(i, j + 1, pixel);
0459                     if (notLastRow && notLastCol)
0460                         terrainImage->setPixel(i + 1, j + 1, pixel);
0461                 }
0462             }
0463             // Otherwise terrainImage was already filled with transparent pixels
0464             // so i,j will be transparent.
0465         }
0466     }
0467 
0468     savedImage = terrainImage->copy();
0469 
0470     QFile f(sourceFilename);
0471     QFileInfo fileInfo(f.fileName());
0472     QString fName(fileInfo.fileName());
0473     QString dbgMsg(QString("Terrain rendering: %1px, %2s (%3s) %4 ds %5 skip %6 trnsp %7 pan %8 smooth %9")
0474                    .arg(w * h)
0475                    .arg(timer.elapsed() / 1000.0, 5, 'f', 3)
0476                    .arg(setupTime, 5, 'f', 3)
0477                    .arg(fName)
0478                    .arg(Options::terrainDownsampling())
0479                    .arg(Options::terrainSkipSpeedup() ? "T" : "F")
0480                    .arg(Options::terrainTransparencySpeedup() ? "T" : "F")
0481                    .arg(Options::terrainPanning() ? "T" : "F")
0482                    .arg(Options::terrainSmoothPixels() ? "T" : "F"));
0483     //qCDebug(KSTARS) << dbgMsg;
0484     //fprintf(stderr, "%s\n", dbgMsg.toLatin1().data());
0485 
0486     dirty = false;
0487     return true;
0488 }
0489 
0490 // Goes through every Nth input pixel position, finding their azimuth and altitude
0491 // and storing that for future use in the interpolations above.
0492 // This is the most time-costly part of the computation.
0493 void TerrainRenderer::setupLookup(uint16_t w, uint16_t h, int sampling, const Projector *proj, TerrainLookup *azLookup,
0494                                   TerrainLookup *altLookup)
0495 {
0496     const auto &lst = KStarsData::Instance()->lst();
0497     const auto &lat = KStarsData::Instance()->geo()->lat();
0498     for (int j = 0, js = 0; j < h; j += sampling, js++)
0499     {
0500         for (int i = 0, is = 0; i < w; i += sampling, is++)
0501         {
0502             const QPointF imgPoint(i, j);
0503             bool equiRectangular = (proj->type() == Projector::Equirectangular);
0504             bool usable = equiRectangular ? !dynamic_cast<const EquirectangularProjector*>(proj)->unusablePoint(imgPoint)
0505                           : !proj->unusablePoint(imgPoint);
0506             if (usable)
0507             {
0508                 SkyPoint point = equiRectangular ?
0509                                  dynamic_cast<const EquirectangularProjector*>(proj)->fromScreen(imgPoint, lst, lat, true)
0510                                  : proj->fromScreen(imgPoint, lst, lat, true);
0511                 const double az = rationalizeAz(point.az().Degrees());
0512                 const double alt = rationalizeAlt(point.alt().Degrees());
0513                 azLookup->set(is, js, az);
0514                 altLookup->set(is, js, alt);
0515             }
0516         }
0517     }
0518 }
0519