1 // SPDX-FileCopyrightText: 2008 David Roberts <dvdr18@gmail.com>
2 // SPDX-FileCopyrightText: 2009 Jens-Michael Hoffmann <jensmh@gmx.de>
3 // SPDX-FileCopyrightText: 2011 Bernhard Beschow <bbeschow@cs.tu-berlin.de>
4 //
5 // SPDX-License-Identifier: LGPL-2.1-or-later
6 
7 
8 #include "MergedLayerDecorator.h"
9 
10 #include "blendings/Blending.h"
11 #include "blendings/BlendingFactory.h"
12 #include "SunLocator.h"
13 #include "MarbleMath.h"
14 #include "MarbleDebug.h"
15 #include "GeoDataGroundOverlay.h"
16 #include "GeoSceneTextureTileDataset.h"
17 #include "ImageF.h"
18 #include "StackedTile.h"
19 #include "TileLoaderHelper.h"
20 #include "TextureTile.h"
21 #include "TileLoader.h"
22 #include "RenderState.h"
23 
24 #include "GeoDataCoordinates.h"
25 
26 #include <QPointer>
27 #include <QPainter>
28 #include <QPainterPath>
29 
30 using namespace Marble;
31 
32 class Q_DECL_HIDDEN MergedLayerDecorator::Private
33 {
34 public:
35     Private( TileLoader *tileLoader, const SunLocator *sunLocator );
36 
37     static int maxDivisor( int maximum, int fullLength );
38 
39     StackedTile *createTile( const QVector<QSharedPointer<TextureTile> > &tiles ) const;
40 
41     void renderGroundOverlays( QImage *tileImage, const QVector<QSharedPointer<TextureTile> > &tiles ) const;
42     void paintSunShading( QImage *tileImage, const TileId &id ) const;
43     void paintTileId( QImage *tileImage, const TileId &id ) const;
44 
45     void detectMaxTileLevel();
46     QVector<const GeoSceneTextureTileDataset *> findRelevantTextureLayers( const TileId &stackedTileId ) const;
47 
48     TileLoader *const m_tileLoader;
49     const SunLocator *const m_sunLocator;
50     BlendingFactory m_blendingFactory;
51     QVector<const GeoSceneTextureTileDataset *> m_textureLayers;
52     QList<const GeoDataGroundOverlay *> m_groundOverlays;
53     int m_maxTileLevel;
54     QString m_themeId;
55     int m_levelZeroColumns;
56     int m_levelZeroRows;
57     bool m_showSunShading;
58     bool m_showCityLights;
59     bool m_showTileId;
60 };
61 
Private(TileLoader * tileLoader,const SunLocator * sunLocator)62 MergedLayerDecorator::Private::Private( TileLoader *tileLoader, const SunLocator *sunLocator ) :
63     m_tileLoader( tileLoader ),
64     m_sunLocator( sunLocator ),
65     m_blendingFactory( sunLocator ),
66     m_textureLayers(),
67     m_maxTileLevel( 0 ),
68     m_themeId(),
69     m_levelZeroColumns( 0 ),
70     m_levelZeroRows( 0 ),
71     m_showSunShading( false ),
72     m_showCityLights( false ),
73     m_showTileId( false )
74 {
75 }
76 
MergedLayerDecorator(TileLoader * const tileLoader,const SunLocator * sunLocator)77 MergedLayerDecorator::MergedLayerDecorator( TileLoader * const tileLoader,
78                                             const SunLocator* sunLocator )
79     : d( new Private( tileLoader, sunLocator ) )
80 {
81 }
82 
~MergedLayerDecorator()83 MergedLayerDecorator::~MergedLayerDecorator()
84 {
85     delete d;
86 }
87 
setTextureLayers(const QVector<const GeoSceneTextureTileDataset * > & textureLayers)88 void MergedLayerDecorator::setTextureLayers( const QVector<const GeoSceneTextureTileDataset *> &textureLayers )
89 {
90     if ( textureLayers.count() > 0 ) {
91         const GeoSceneTileDataset *const firstTexture = textureLayers.at( 0 );
92         d->m_levelZeroColumns = firstTexture->levelZeroColumns();
93         d->m_levelZeroRows = firstTexture->levelZeroRows();
94         d->m_blendingFactory.setLevelZeroLayout( d->m_levelZeroColumns, d->m_levelZeroRows );
95         d->m_themeId = QLatin1String("maps/") + firstTexture->sourceDir();
96     }
97 
98     d->m_textureLayers = textureLayers;
99 
100     d->detectMaxTileLevel();
101 }
102 
updateGroundOverlays(const QList<const GeoDataGroundOverlay * > & groundOverlays)103 void MergedLayerDecorator::updateGroundOverlays(const QList<const GeoDataGroundOverlay *> &groundOverlays )
104 {
105     d->m_groundOverlays = groundOverlays;
106 }
107 
108 
textureLayersSize() const109 int MergedLayerDecorator::textureLayersSize() const
110 {
111     return d->m_textureLayers.size();
112 }
113 
maximumTileLevel() const114 int MergedLayerDecorator::maximumTileLevel() const
115 {
116     return d->m_maxTileLevel;
117 }
118 
tileColumnCount(int level) const119 int MergedLayerDecorator::tileColumnCount( int level ) const
120 {
121     Q_ASSERT( !d->m_textureLayers.isEmpty() );
122 
123     const int levelZeroColumns = d->m_textureLayers.at( 0 )->levelZeroColumns();
124 
125     return TileLoaderHelper::levelToColumn( levelZeroColumns, level );
126 }
127 
tileRowCount(int level) const128 int MergedLayerDecorator::tileRowCount( int level ) const
129 {
130     Q_ASSERT( !d->m_textureLayers.isEmpty() );
131 
132     const int levelZeroRows = d->m_textureLayers.at( 0 )->levelZeroRows();
133 
134     return TileLoaderHelper::levelToRow( levelZeroRows, level );
135 }
136 
tileProjection() const137 const GeoSceneAbstractTileProjection *MergedLayerDecorator::tileProjection() const
138 {
139     Q_ASSERT( !d->m_textureLayers.isEmpty() );
140 
141     return d->m_textureLayers.at(0)->tileProjection();
142 }
143 
tileSize() const144 QSize MergedLayerDecorator::tileSize() const
145 {
146     Q_ASSERT( !d->m_textureLayers.isEmpty() );
147 
148     return d->m_textureLayers.at( 0 )->tileSize();
149 }
150 
createTile(const QVector<QSharedPointer<TextureTile>> & tiles) const151 StackedTile *MergedLayerDecorator::Private::createTile( const QVector<QSharedPointer<TextureTile> > &tiles ) const
152 {
153     Q_ASSERT( !tiles.isEmpty() );
154 
155     const TileId firstId = tiles.first()->id();
156     const TileId id( 0, firstId.zoomLevel(), firstId.x(), firstId.y() );
157 
158     // Image for blending all the texture tiles on it
159     QImage resultImage;
160 
161     // if there are more than one active texture layers, we have to convert the
162     // result tile into QImage::Format_ARGB32_Premultiplied to make blending possible
163     const bool withConversion = tiles.count() > 1 || m_showSunShading || m_showTileId || !m_groundOverlays.isEmpty();
164     for ( const QSharedPointer<TextureTile> &tile: tiles ) {
165 
166         // Image blending. If there are several images in the same tile (like clouds
167         // or hillshading images over the map) blend them all into only one image
168 
169         const Blending *const blending =  tile->blending();
170         if ( blending ) {
171 
172             mDebug() << Q_FUNC_INFO << "blending";
173 
174             if ( resultImage.isNull() ) {
175                 resultImage = QImage( tile->image()->size(), QImage::Format_ARGB32_Premultiplied );
176             }
177 
178             blending->blend( &resultImage, tile.data() );
179         }
180         else {
181             mDebug() << Q_FUNC_INFO << "no blending defined => copying top over bottom image";
182             if ( withConversion ) {
183                 resultImage = tile->image()->convertToFormat( QImage::Format_ARGB32_Premultiplied );
184             } else {
185                 resultImage = tile->image()->copy();
186             }
187         }
188     }
189 
190     renderGroundOverlays( &resultImage, tiles );
191 
192     if ( m_showSunShading && !m_showCityLights ) {
193         paintSunShading( &resultImage, id );
194     }
195 
196     if ( m_showTileId ) {
197         paintTileId( &resultImage, id );
198     }
199 
200     return new StackedTile( id, resultImage, tiles );
201 }
202 
renderGroundOverlays(QImage * tileImage,const QVector<QSharedPointer<TextureTile>> & tiles) const203 void MergedLayerDecorator::Private::renderGroundOverlays( QImage *tileImage, const QVector<QSharedPointer<TextureTile> > &tiles ) const
204 {
205 
206     /* All tiles are covering the same area. Pick one. */
207     const TileId tileId = tiles.first()->id();
208 
209     const GeoDataLatLonBox tileLatLonBox = findRelevantTextureLayers(tileId).first()->tileProjection()->geoCoordinates(tileId);
210 
211     /* Map the ground overlay to the image. */
212     for ( int i =  0; i < m_groundOverlays.size(); ++i ) {
213 
214         const GeoDataGroundOverlay* overlay = m_groundOverlays.at( i );
215         if ( !overlay->isGloballyVisible() ) {
216             continue;
217         }
218 
219         const GeoDataLatLonBox overlayLatLonBox = overlay->latLonBox();
220 
221         if ( !tileLatLonBox.intersects( overlayLatLonBox.toCircumscribedRectangle() ) ) {
222             continue;
223         }
224 
225         const qreal pixelToLat = tileLatLonBox.height() / tileImage->height();
226         const qreal pixelToLon = tileLatLonBox.width() / tileImage->width();
227 
228         const qreal latToPixel = overlay->icon().height() / overlayLatLonBox.height();
229         const qreal lonToPixel = overlay->icon().width() / overlayLatLonBox.width();
230 
231         const qreal  global_height = tileImage->height()
232                 * TileLoaderHelper::levelToRow( m_levelZeroRows, tileId.zoomLevel() );
233         const qreal pixel2Rad = M_PI / global_height;
234         const qreal rad2Pixel = global_height / M_PI;
235 
236         qreal latPixelPosition = rad2Pixel/2 * gdInv(tileLatLonBox.north());
237         const bool isMercatorTileProjection = (m_textureLayers.at( 0 )->tileProjectionType() ==  GeoSceneAbstractTileProjection::Mercator);
238 
239         for ( int y = 0; y < tileImage->height(); ++y ) {
240              QRgb *scanLine = ( QRgb* ) ( tileImage->scanLine( y ) );
241 
242              const qreal lat = isMercatorTileProjection
243                      ? gd(2 * (latPixelPosition - y) * pixel2Rad )
244                      : tileLatLonBox.north() - y * pixelToLat;
245 
246              for ( int x = 0; x < tileImage->width(); ++x, ++scanLine ) {
247                  qreal lon = GeoDataCoordinates::normalizeLon( tileLatLonBox.west() + x * pixelToLon );
248 
249                  GeoDataCoordinates coords(lon, lat);
250                  GeoDataCoordinates rotatedCoords(coords);
251 
252                  if (overlay->latLonBox().rotation() != 0) {
253                     // Possible TODO: Make this faster by creating the axisMatrix beforehand
254                     // and just call Quaternion::rotateAroundAxis(const matrix &m) here.
255                     rotatedCoords = coords.rotateAround(overlayLatLonBox.center(), -overlay->latLonBox().rotation());
256                  }
257 
258                  // TODO: The rotated latLonBox is bigger. We need to take this into account.
259                  // (Currently the GroundOverlay sometimes gets clipped because of that)
260                  if ( overlay->latLonBox().contains( rotatedCoords ) ) {
261 
262                      qreal px = GeoDataLatLonBox::width( rotatedCoords.longitude(), overlayLatLonBox.west() ) * lonToPixel;
263                      qreal py = (qreal)( overlay->icon().height() ) - ( GeoDataLatLonBox::height( rotatedCoords.latitude(), overlayLatLonBox.south() ) * latToPixel ) - 1;
264 
265                      if ( px >= 0 && px < overlay->icon().width() && py >= 0 && py < overlay->icon().height() ) {
266                          int alpha = qAlpha( overlay->icon().pixel( px, py ) );
267                          if ( alpha != 0 )
268                          {
269                             QRgb result = ImageF::pixelF( overlay->icon(), px, py );
270 
271                             if (alpha == 255)
272                             {
273                                 *scanLine = result;
274                             }
275                             else
276                             {
277                                 *scanLine = qRgb( ( alpha * qRed(result) + (255 - alpha) * qRed(*scanLine) ) / 255,
278                                             ( alpha * qGreen(result) + (255 - alpha) * qGreen(*scanLine) ) / 255,
279                                             ( alpha * qBlue(result) + (255 - alpha) * qBlue(*scanLine) ) / 255 );
280                             }
281                          }
282                      }
283                  }
284              }
285         }
286     }
287 }
288 
loadTile(const TileId & stackedTileId)289 StackedTile *MergedLayerDecorator::loadTile( const TileId &stackedTileId )
290 {
291     const QVector<const GeoSceneTextureTileDataset *> textureLayers = d->findRelevantTextureLayers( stackedTileId );
292     QVector<QSharedPointer<TextureTile> > tiles;
293     tiles.reserve(textureLayers.size());
294 
295     for ( const GeoSceneTextureTileDataset *layer: textureLayers ) {
296         const TileId tileId( layer->sourceDir(), stackedTileId.zoomLevel(),
297                              stackedTileId.x(), stackedTileId.y() );
298 
299         mDebug() << Q_FUNC_INFO << layer->sourceDir() << tileId << layer->tileSize() << layer->fileFormat();
300 
301         // Blending (how to merge the images into an only image)
302         const Blending *blending = d->m_blendingFactory.findBlending( layer->blending() );
303         if ( blending == nullptr && !layer->blending().isEmpty() ) {
304             mDebug() << Q_FUNC_INFO << "could not find blending" << layer->blending();
305         }
306 
307         const GeoSceneTextureTileDataset *const textureLayer = static_cast<const GeoSceneTextureTileDataset *>( layer );
308         const QImage tileImage = d->m_tileLoader->loadTileImage( textureLayer, tileId, DownloadBrowse );
309 
310         QSharedPointer<TextureTile> tile( new TextureTile( tileId, tileImage, blending ) );
311         tiles.append( tile );
312     }
313 
314     Q_ASSERT( !tiles.isEmpty() );
315 
316     return d->createTile( tiles );
317 }
318 
renderState(const TileId & stackedTileId) const319 RenderState MergedLayerDecorator::renderState( const TileId &stackedTileId ) const
320 {
321     QString const nameTemplate = "Tile %1/%2/%3";
322     RenderState state( nameTemplate.arg( stackedTileId.zoomLevel() )
323                        .arg( stackedTileId.x() )
324                        .arg( stackedTileId.y() ) );
325     const QVector<const GeoSceneTextureTileDataset *> textureLayers = d->findRelevantTextureLayers( stackedTileId );
326     for ( const GeoSceneTextureTileDataset *layer: textureLayers ) {
327         const TileId tileId( layer->sourceDir(), stackedTileId.zoomLevel(),
328                              stackedTileId.x(), stackedTileId.y() );
329         RenderStatus tileStatus = Complete;
330         switch ( TileLoader::tileStatus( layer, tileId ) ) {
331         case TileLoader::Available:
332             tileStatus = Complete;
333             break;
334         case TileLoader::Expired:
335             tileStatus = WaitingForUpdate;
336             break;
337         case TileLoader::Missing:
338             tileStatus = WaitingForData;
339             break;
340         }
341 
342         state.addChild( RenderState( layer->name(), tileStatus ) );
343     }
344 
345     return state;
346 }
347 
hasTextureLayer() const348 bool MergedLayerDecorator::hasTextureLayer() const
349 {
350     return !d->m_textureLayers.isEmpty();
351 }
352 
updateTile(const StackedTile & stackedTile,const TileId & tileId,const QImage & tileImage)353 StackedTile *MergedLayerDecorator::updateTile( const StackedTile &stackedTile, const TileId &tileId, const QImage &tileImage )
354 {
355     Q_ASSERT( !tileImage.isNull() );
356 
357     d->detectMaxTileLevel();
358 
359     QVector<QSharedPointer<TextureTile> > tiles = stackedTile.tiles();
360 
361     for ( int i = 0; i < tiles.count(); ++ i) {
362         if ( tiles[i]->id() == tileId ) {
363             const Blending *blending = tiles[i]->blending();
364 
365             tiles[i] = QSharedPointer<TextureTile>( new TextureTile( tileId, tileImage, blending ) );
366         }
367     }
368 
369     return d->createTile( tiles );
370 }
371 
downloadStackedTile(const TileId & id,DownloadUsage usage)372 void MergedLayerDecorator::downloadStackedTile( const TileId &id, DownloadUsage usage )
373 {
374     const QVector<const GeoSceneTextureTileDataset *> textureLayers = d->findRelevantTextureLayers( id );
375 
376     for ( const GeoSceneTextureTileDataset *textureLayer: textureLayers ) {
377         if ( TileLoader::tileStatus( textureLayer, id ) != TileLoader::Available || usage == DownloadBrowse ) {
378             d->m_tileLoader->downloadTile( textureLayer, id, usage );
379         }
380     }
381 }
382 
setShowSunShading(bool show)383 void MergedLayerDecorator::setShowSunShading( bool show )
384 {
385     d->m_showSunShading = show;
386 }
387 
showSunShading() const388 bool MergedLayerDecorator::showSunShading() const
389 {
390     return d->m_showSunShading;
391 }
392 
setShowCityLights(bool show)393 void MergedLayerDecorator::setShowCityLights( bool show )
394 {
395     d->m_showCityLights = show;
396 }
397 
showCityLights() const398 bool MergedLayerDecorator::showCityLights() const
399 {
400     return d->m_showCityLights;
401 }
402 
setShowTileId(bool visible)403 void MergedLayerDecorator::setShowTileId( bool visible )
404 {
405     d->m_showTileId = visible;
406 }
407 
paintSunShading(QImage * tileImage,const TileId & id) const408 void MergedLayerDecorator::Private::paintSunShading( QImage *tileImage, const TileId &id ) const
409 {
410     if ( tileImage->depth() != 32 )
411         return;
412 
413     // TODO add support for 8-bit maps?
414     // add sun shading
415     const qreal  global_width  = tileImage->width()
416             * TileLoaderHelper::levelToColumn( m_levelZeroColumns, id.zoomLevel() );
417     const qreal  global_height = tileImage->height()
418             * TileLoaderHelper::levelToRow( m_levelZeroRows, id.zoomLevel() );
419     const qreal lon_scale = 2*M_PI / global_width;
420     const qreal lat_scale = -M_PI / global_height;
421     const int tileHeight = tileImage->height();
422     const int tileWidth = tileImage->width();
423 
424     // First we determine the supporting point interval for the interpolation.
425     const int n = maxDivisor( 30, tileWidth );
426     const int ipRight = n * (int)( tileWidth / n );
427 
428     for ( int cur_y = 0; cur_y < tileHeight; ++cur_y ) {
429         const qreal lat = lat_scale * ( id.y() * tileHeight + cur_y ) - 0.5*M_PI;
430         const qreal a = sin( (lat+DEG2RAD * m_sunLocator->getLat() )/2.0 );
431         const qreal c = cos(lat)*cos( -DEG2RAD * m_sunLocator->getLat() );
432 
433         QRgb* scanline = (QRgb*)tileImage->scanLine( cur_y );
434 
435         qreal lastShade = -10.0;
436 
437         int cur_x = 0;
438 
439         while ( cur_x < tileWidth ) {
440 
441             const bool interpolate = ( cur_x != 0 && cur_x < ipRight && cur_x + n < tileWidth );
442 
443             qreal shade = 0;
444 
445             if ( interpolate ) {
446                 const int check = cur_x + n;
447                 const qreal checklon   = lon_scale * ( id.x() * tileWidth + check );
448                 shade = m_sunLocator->shading( checklon, a, c );
449 
450                 // if the shading didn't change across the interpolation
451                 // interval move on and don't change anything.
452                 if ( shade == lastShade && shade == 1.0 ) {
453                     scanline += n;
454                     cur_x += n;
455                     continue;
456                 }
457                 if ( shade == lastShade && shade == 0.0 ) {
458                     for ( int t = 0; t < n; ++t ) {
459                         SunLocator::shadePixel(*scanline, shade);
460                         ++scanline;
461                     }
462                     cur_x += n;
463                     continue;
464                 }
465                 for ( int t = 0; t < n ; ++t ) {
466                     const qreal lon   = lon_scale * ( id.x() * tileWidth + cur_x );
467                     shade = m_sunLocator->shading( lon, a, c );
468                     SunLocator::shadePixel(*scanline, shade);
469                     ++scanline;
470                     ++cur_x;
471                 }
472             }
473 
474             else {
475                 // Make sure we don't exceed the image memory
476                 if ( cur_x < tileWidth ) {
477                     const qreal lon   = lon_scale * ( id.x() * tileWidth + cur_x );
478                     shade = m_sunLocator->shading( lon, a, c );
479                     SunLocator::shadePixel(*scanline, shade);
480                     ++scanline;
481                     ++cur_x;
482                 }
483             }
484             lastShade = shade;
485         }
486     }
487 }
488 
paintTileId(QImage * tileImage,const TileId & id) const489 void MergedLayerDecorator::Private::paintTileId( QImage *tileImage, const TileId &id ) const
490 {
491     QString filename = QString( "%1_%2.jpg" )
492             .arg(id.x(), tileDigits, 10, QLatin1Char('0'))
493             .arg(id.y(), tileDigits, 10, QLatin1Char('0'));
494 
495     QPainter painter( tileImage );
496 
497     QColor foreground;
498     QColor background;
499 
500     if ( ( (qreal)(id.x())/2 == id.x()/2 && (qreal)(id.y())/2 == id.y()/2 )
501          || ( (qreal)(id.x())/2 != id.x()/2 && (qreal)(id.y())/2 != id.y()/2 )
502          )
503     {
504         foreground.setNamedColor( "#FFFFFF" );
505         background.setNamedColor( "#000000" );
506     }
507     else {
508         foreground.setNamedColor( "#000000" );
509         background.setNamedColor( "#FFFFFF" );
510     }
511 
512     int   strokeWidth = 10;
513     QPen  testPen( foreground );
514     testPen.setWidth( strokeWidth );
515     testPen.setJoinStyle( Qt::MiterJoin );
516 
517     painter.setPen( testPen );
518     painter.drawRect( strokeWidth / 2, strokeWidth / 2,
519                       tileImage->width()  - strokeWidth,
520                       tileImage->height() - strokeWidth );
521     QFont testFont(QStringLiteral("Sans Serif"), 12);
522     QFontMetrics testFm( testFont );
523     painter.setFont( testFont );
524 
525     QPen outlinepen( foreground );
526     outlinepen.setWidthF( 6 );
527 
528     painter.setPen( outlinepen );
529     painter.setBrush( background );
530 
531     QPainterPath   outlinepath;
532 
533     QPointF  baseline1( ( tileImage->width() - testFm.boundingRect(filename).width() ) / 2,
534                         ( tileImage->height() * 0.25) );
535     outlinepath.addText( baseline1, testFont, QString( "level: %1" ).arg(id.zoomLevel()) );
536 
537     QPointF  baseline2( ( tileImage->width() - testFm.boundingRect(filename).width() ) / 2,
538                         tileImage->height() * 0.50 );
539     outlinepath.addText( baseline2, testFont, filename );
540 
541     QPointF  baseline3( ( tileImage->width() - testFm.boundingRect(filename).width() ) / 2,
542                         tileImage->height() * 0.75 );
543     outlinepath.addText( baseline3, testFont, m_themeId );
544 
545     painter.drawPath( outlinepath );
546 
547     painter.setPen( Qt::NoPen );
548     painter.drawPath( outlinepath );
549 }
550 
detectMaxTileLevel()551 void MergedLayerDecorator::Private::detectMaxTileLevel()
552 {
553     if ( m_textureLayers.isEmpty() ) {
554         m_maxTileLevel = -1;
555         return;
556     }
557 
558     m_maxTileLevel = TileLoader::maximumTileLevel( *m_textureLayers.at( 0 ) );
559 }
560 
findRelevantTextureLayers(const TileId & stackedTileId) const561 QVector<const GeoSceneTextureTileDataset *> MergedLayerDecorator::Private::findRelevantTextureLayers( const TileId &stackedTileId ) const
562 {
563     QVector<const GeoSceneTextureTileDataset *> result;
564 
565     for ( const GeoSceneTextureTileDataset *candidate: m_textureLayers ) {
566         Q_ASSERT( candidate );
567         // check, if layer provides tiles for the current level
568         if ( !candidate->hasMaximumTileLevel() ||
569              candidate->maximumTileLevel() >= stackedTileId.zoomLevel() ) {
570             //check if the tile intersects with texture bounds
571             if (candidate->latLonBox().isNull()) {
572                 result.append(candidate);
573             }
574             else {
575                 const GeoDataLatLonBox bbox = candidate->tileProjection()->geoCoordinates(stackedTileId);
576 
577                 if (candidate->latLonBox().intersects(bbox)) {
578                     result.append( candidate );
579                 }
580             }
581         }
582     }
583 
584     return result;
585 }
586 
587 // TODO: This should likely go into a math class in the future ...
588 
maxDivisor(int maximum,int fullLength)589 int MergedLayerDecorator::Private::maxDivisor( int maximum, int fullLength )
590 {
591     // Find the optimal interpolation interval n for the
592     // current image canvas width
593     int best = 2;
594 
595     int  nEvalMin = fullLength;
596     for ( int it = 1; it <= maximum; ++it ) {
597         // The optimum is the interval which results in the least amount
598         // supporting points taking into account the rest which can't
599         // get used for interpolation.
600         int nEval = fullLength / it + fullLength % it;
601         if ( nEval < nEvalMin ) {
602             nEvalMin = nEval;
603             best = it;
604         }
605     }
606     return best;
607 }
608