1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2007 Torsten Rahn <tackat@kde.org>
4 // SPDX-FileCopyrightText: 2011 Bernhard Beschow <bbeschow@cs.tu-berlin.de>
5 //
6 
7 
8 #include "SphericalScanlineTextureMapper.h"
9 
10 #include <cmath>
11 
12 #include <qmath.h>
13 #include <QRunnable>
14 
15 #include "GeoPainter.h"
16 #include "GeoDataPolygon.h"
17 #include "MarbleDebug.h"
18 #include "Quaternion.h"
19 #include "ScanlineTextureMapperContext.h"
20 #include "StackedTileLoader.h"
21 #include "StackedTile.h"
22 #include "TextureColorizer.h"
23 #include "ViewportParams.h"
24 #include "MathHelper.h"
25 
26 
27 using namespace Marble;
28 
29 class SphericalScanlineTextureMapper::RenderJob : public QRunnable
30 {
31 public:
32     RenderJob( StackedTileLoader *tileLoader, int tileLevel, QImage *canvasImage, const ViewportParams *viewport, MapQuality mapQuality, int yTop, int yBottom );
33 
34     void run() override;
35 
36 private:
37     StackedTileLoader *const m_tileLoader;
38     const int m_tileLevel;
39     QImage *const m_canvasImage;
40     const ViewportParams *const m_viewport;
41     const MapQuality m_mapQuality;
42     int const m_yTop;
43     int const m_yBottom;
44 };
45 
RenderJob(StackedTileLoader * tileLoader,int tileLevel,QImage * canvasImage,const ViewportParams * viewport,MapQuality mapQuality,int yTop,int yBottom)46 SphericalScanlineTextureMapper::RenderJob::RenderJob( StackedTileLoader *tileLoader, int tileLevel, QImage *canvasImage, const ViewportParams *viewport, MapQuality mapQuality, int yTop, int yBottom )
47     : m_tileLoader( tileLoader ),
48       m_tileLevel( tileLevel ),
49       m_canvasImage( canvasImage ),
50       m_viewport( viewport ),
51       m_mapQuality( mapQuality ),
52       m_yTop( yTop ),
53       m_yBottom( yBottom )
54 {
55 }
56 
SphericalScanlineTextureMapper(StackedTileLoader * tileLoader)57 SphericalScanlineTextureMapper::SphericalScanlineTextureMapper( StackedTileLoader *tileLoader )
58     : TextureMapperInterface()
59     , m_tileLoader( tileLoader )
60     , m_radius( 0 )
61     , m_threadPool()
62 {
63 }
64 
mapTexture(GeoPainter * painter,const ViewportParams * viewport,int tileZoomLevel,const QRect & dirtyRect,TextureColorizer * texColorizer)65 void SphericalScanlineTextureMapper::mapTexture( GeoPainter *painter,
66                                                  const ViewportParams *viewport,
67                                                  int tileZoomLevel,
68                                                  const QRect &dirtyRect,
69                                                  TextureColorizer *texColorizer )
70 {
71     if ( m_canvasImage.size() != viewport->size() || m_radius != viewport->radius() ) {
72         const QImage::Format optimalFormat = ScanlineTextureMapperContext::optimalCanvasImageFormat( viewport );
73 
74         if ( m_canvasImage.size() != viewport->size() || m_canvasImage.format() != optimalFormat ) {
75             m_canvasImage = QImage( viewport->size(), optimalFormat );
76         }
77 
78         if ( !viewport->mapCoversViewport() ) {
79             m_canvasImage.fill( 0 );
80         }
81 
82         m_radius = viewport->radius();
83         m_repaintNeeded = true;
84     }
85 
86     if ( m_repaintNeeded ) {
87         mapTexture( viewport, tileZoomLevel, painter->mapQuality() );
88 
89         if ( texColorizer ) {
90             texColorizer->colorize( &m_canvasImage, viewport, painter->mapQuality() );
91         }
92 
93         m_repaintNeeded = false;
94     }
95 
96     const int radius = viewport->radius();
97 
98     QRect rect( viewport->width() / 2 - radius, viewport->height() / 2 - radius,
99                 2 * radius, 2 * radius);
100     rect = rect.intersected( dirtyRect );
101     painter->drawImage( rect, m_canvasImage, rect );
102 }
103 
mapTexture(const ViewportParams * viewport,int tileZoomLevel,MapQuality mapQuality)104 void SphericalScanlineTextureMapper::mapTexture( const ViewportParams *viewport, int tileZoomLevel, MapQuality mapQuality )
105 {
106     // Reset backend
107     m_tileLoader->resetTilehash();
108 
109     // Initialize needed constants:
110 
111     const int imageHeight = m_canvasImage.height();
112     const qint64  radius      = viewport->radius();
113 
114     // Calculate the actual y-range of the map on the screen
115     const int skip = ( mapQuality == LowQuality ) ? 1
116                                                   : 0;
117     const int yTop = ( imageHeight / 2 - radius >= 0 ) ? imageHeight / 2 - radius
118                                                        : 0;
119     const int yBottom = ( yTop == 0 ) ? imageHeight - skip
120                                       : yTop + radius + radius - skip;
121 
122     const int numThreads = m_threadPool.maxThreadCount();
123     const int yStep = qCeil(qreal( yBottom - yTop ) / qreal(numThreads));
124     for ( int i = 0; i < numThreads; ++i ) {
125         const int yStart = yTop +  i      * yStep;
126         const int yEnd   = qMin(yBottom, yTop + (i + 1) * yStep);
127         QRunnable *const job = new RenderJob( m_tileLoader, tileZoomLevel, &m_canvasImage, viewport, mapQuality, yStart, yEnd );
128         m_threadPool.start( job );
129     }
130 
131     m_threadPool.waitForDone();
132 
133     m_tileLoader->cleanupTilehash();
134 }
135 
run()136 void SphericalScanlineTextureMapper::RenderJob::run()
137 {
138     const int imageHeight = m_canvasImage->height();
139     const int imageWidth  = m_canvasImage->width();
140     const qint64  radius  = m_viewport->radius();
141     const qreal  inverseRadius = 1.0 / (qreal)(radius);
142 
143     const bool interlaced   = ( m_mapQuality == LowQuality );
144     const bool highQuality  = ( m_mapQuality == HighQuality
145                              || m_mapQuality == PrintQuality );
146     const bool printQuality = ( m_mapQuality == PrintQuality );
147 
148     // Evaluate the degree of interpolation
149     const int n = ScanlineTextureMapperContext::interpolationStep( m_viewport, m_mapQuality );
150 
151     // Calculate north pole position to decrease pole distortion later on
152     Quaternion northPole = Quaternion::fromSpherical( 0.0, M_PI * 0.5 );
153     northPole.rotateAroundAxis( m_viewport->planetAxis().inverse() );
154     const int northPoleX = imageWidth / 2 + (int)( radius * northPole.v[Q_X] );
155     const int northPoleY = imageHeight / 2 - (int)( radius * northPole.v[Q_Y] );
156 
157     // Calculate axis matrix to represent the planet's rotation.
158     matrix  planetAxisMatrix;
159     m_viewport->planetAxis().toMatrix( planetAxisMatrix );
160 
161     // initialize needed variables that are modified during texture mapping:
162 
163     ScanlineTextureMapperContext context( m_tileLoader, m_tileLevel );
164     qreal  lon = 0.0;
165     qreal  lat = 0.0;
166 
167     // Scanline based algorithm to texture map a sphere
168     for ( int y = m_yTop; y < m_yBottom ; ++y ) {
169 
170         // Evaluate coordinates for the 3D position vector of the current pixel
171         const qreal qy = inverseRadius * (qreal)( imageHeight / 2 - y );
172         const qreal qr = 1.0 - qy * qy;
173 
174         // rx is the radius component in x direction
175         const int rx = (int)sqrt( (qreal)( radius * radius
176                                       - ( ( y - imageHeight / 2 )
177                                           * ( y - imageHeight / 2 ) ) ) );
178 
179         // Calculate the actual x-range of the map within the current scanline.
180         //
181         // If the circular border of the earth disk is still visible then xLeft
182         // equals the scanline position of the most left pixel that gets covered
183         // by the earth disk. In terms of math this equals the half image width minus
184         // the radius component on the current scanline in x direction ("rx").
185         //
186         // If the zoom factor is high enough then the whole screen gets covered
187         // by the earth and the border of the earth disk isn't visible anymore.
188         // In that situation xLeft equals zero.
189         // For xRight the situation is similar.
190 
191         const int xLeft  = ( imageWidth / 2 - rx > 0 ) ? imageWidth / 2 - rx
192                                                        : 0;
193         const int xRight = ( imageWidth / 2 - rx > 0 ) ? xLeft + rx + rx
194                                                        : imageWidth;
195 
196         QRgb * scanLine = (QRgb*)( m_canvasImage->scanLine( y ) ) + xLeft;
197 
198         const int xIpLeft  = ( imageWidth / 2 - rx > 0 ) ? n * (int)( xLeft / n + 1 )
199                                                          : 1;
200         const int xIpRight = ( imageWidth / 2 - rx > 0 ) ? n * (int)( xRight / n - 1 )
201                                                          : n * (int)( xRight / n - 1 ) + 1;
202 
203         // Decrease pole distortion due to linear approximation ( y-axis )
204         bool crossingPoleArea = false;
205         if ( northPole.v[Q_Z] > 0
206              && northPoleY - ( n * 0.75 ) <= y
207              && northPoleY + ( n * 0.75 ) >= y )
208         {
209             crossingPoleArea = true;
210         }
211 
212         int ncount = 0;
213 
214         for ( int x = xLeft; x < xRight; ++x ) {
215             // Prepare for interpolation
216 
217             const int leftInterval = xIpLeft + ncount * n;
218 
219             bool interpolate = false;
220             if ( x >= xIpLeft && x <= xIpRight ) {
221 
222                 // Decrease pole distortion due to linear approximation ( x-axis )
223 //                mDebug() << QString("NorthPole X: %1, LeftInterval: %2").arg( northPoleX ).arg( leftInterval );
224                 if ( crossingPoleArea
225                      && northPoleX >= leftInterval + n
226                      && northPoleX < leftInterval + 2 * n
227                      && x < leftInterval + 3 * n )
228                 {
229                     interpolate = false;
230                 }
231                 else {
232                     x += n - 1;
233                     interpolate = !printQuality;
234                     ++ncount;
235                 }
236             }
237             else
238                 interpolate = false;
239 
240             // Evaluate more coordinates for the 3D position vector of
241             // the current pixel.
242             const qreal qx = (qreal)( x - imageWidth / 2 ) * inverseRadius;
243             const qreal qr2z = qr - qx * qx;
244             const qreal qz = ( qr2z > 0.0 ) ? sqrt( qr2z ) : 0.0;
245 
246             // Create Quaternion from vector coordinates and rotate it
247             // around globe axis
248             Quaternion qpos( 0.0, qx, qy, qz );
249             qpos.rotateAroundAxis( planetAxisMatrix );
250 
251             qpos.getSpherical( lon, lat );
252 //            mDebug() << QString("lon: %1 lat: %2").arg(lon).arg(lat);
253             // Approx for n-1 out of n pixels within the boundary of
254             // xIpLeft to xIpRight
255 
256             if ( interpolate ) {
257                 if (highQuality)
258                     context.pixelValueApproxF( lon, lat, scanLine, n );
259                 else
260                     context.pixelValueApprox( lon, lat, scanLine, n );
261 
262                 scanLine += ( n - 1 );
263             }
264 
265 //          Comment out the pixelValue line and run Marble if you want
266 //          to understand the interpolation:
267 
268 //          Uncomment the crossingPoleArea line to check precise
269 //          rendering around north pole:
270 
271 //            if ( !crossingPoleArea )
272             if ( x < imageWidth ) {
273                 if ( highQuality )
274                     context.pixelValueF( lon, lat, scanLine );
275                 else
276                     context.pixelValue( lon, lat, scanLine );
277             }
278 
279             ++scanLine;
280         }
281 
282         // copy scanline to improve performance
283         if ( interlaced && y + 1 < m_yBottom ) {
284 
285             const int pixelByteSize = m_canvasImage->bytesPerLine() / imageWidth;
286 
287             memcpy( m_canvasImage->scanLine( y + 1 ) + xLeft * pixelByteSize,
288                     m_canvasImage->scanLine( y ) + xLeft * pixelByteSize,
289                     ( xRight - xLeft ) * pixelByteSize );
290             ++y;
291         }
292     }
293 }
294