File indexing completed on 2024-04-14 03:47:57

0001 // SPDX-FileCopyrightText: 2008 David Roberts <dvdr18@gmail.com>
0002 // SPDX-FileCopyrightText: 2009 Jens-Michael Hoffmann <jensmh@gmx.de>
0003 // SPDX-FileCopyrightText: 2011 Bernhard Beschow <bbeschow@cs.tu-berlin.de>
0004 //
0005 // SPDX-License-Identifier: LGPL-2.1-or-later
0006 
0007 
0008 #include "MergedLayerDecorator.h"
0009 
0010 #include "blendings/Blending.h"
0011 #include "blendings/BlendingFactory.h"
0012 #include "SunLocator.h"
0013 #include "MarbleMath.h"
0014 #include "MarbleDebug.h"
0015 #include "GeoDataGroundOverlay.h"
0016 #include "GeoSceneTextureTileDataset.h"
0017 #include "ImageF.h"
0018 #include "StackedTile.h"
0019 #include "TileLoaderHelper.h"
0020 #include "TextureTile.h"
0021 #include "TileLoader.h"
0022 #include "RenderState.h"
0023 
0024 #include "GeoDataCoordinates.h"
0025 
0026 #include <QPointer>
0027 #include <QPainter>
0028 #include <QPainterPath>
0029 
0030 using namespace Marble;
0031 
0032 class Q_DECL_HIDDEN MergedLayerDecorator::Private
0033 {
0034 public:
0035     Private( TileLoader *tileLoader, const SunLocator *sunLocator );
0036 
0037     static int maxDivisor( int maximum, int fullLength );
0038 
0039     StackedTile *createTile( const QVector<QSharedPointer<TextureTile> > &tiles ) const;
0040 
0041     void renderGroundOverlays( QImage *tileImage, const QVector<QSharedPointer<TextureTile> > &tiles ) const;
0042     void paintSunShading( QImage *tileImage, const TileId &id ) const;
0043     void paintTileId( QImage *tileImage, const TileId &id ) const;
0044 
0045     void detectMaxTileLevel();
0046     QVector<const GeoSceneTextureTileDataset *> findRelevantTextureLayers( const TileId &stackedTileId ) const;
0047 
0048     TileLoader *const m_tileLoader;
0049     const SunLocator *const m_sunLocator;
0050     BlendingFactory m_blendingFactory;
0051     QVector<const GeoSceneTextureTileDataset *> m_textureLayers;
0052     QList<const GeoDataGroundOverlay *> m_groundOverlays;
0053     int m_maxTileLevel;
0054     QString m_themeId;
0055     int m_levelZeroColumns;
0056     int m_levelZeroRows;
0057     bool m_showSunShading;
0058     bool m_showCityLights;
0059     bool m_showTileId;
0060 };
0061 
0062 MergedLayerDecorator::Private::Private( TileLoader *tileLoader, const SunLocator *sunLocator ) :
0063     m_tileLoader( tileLoader ),
0064     m_sunLocator( sunLocator ),
0065     m_blendingFactory( sunLocator ),
0066     m_textureLayers(),
0067     m_maxTileLevel( 0 ),
0068     m_themeId(),
0069     m_levelZeroColumns( 0 ),
0070     m_levelZeroRows( 0 ),
0071     m_showSunShading( false ),
0072     m_showCityLights( false ),
0073     m_showTileId( false )
0074 {
0075 }
0076 
0077 MergedLayerDecorator::MergedLayerDecorator( TileLoader * const tileLoader,
0078                                             const SunLocator* sunLocator )
0079     : d( new Private( tileLoader, sunLocator ) )
0080 {
0081 }
0082 
0083 MergedLayerDecorator::~MergedLayerDecorator()
0084 {
0085     delete d;
0086 }
0087 
0088 void MergedLayerDecorator::setTextureLayers( const QVector<const GeoSceneTextureTileDataset *> &textureLayers )
0089 {
0090     if ( textureLayers.count() > 0 ) {
0091         const GeoSceneTileDataset *const firstTexture = textureLayers.at( 0 );
0092         d->m_levelZeroColumns = firstTexture->levelZeroColumns();
0093         d->m_levelZeroRows = firstTexture->levelZeroRows();
0094         d->m_blendingFactory.setLevelZeroLayout( d->m_levelZeroColumns, d->m_levelZeroRows );
0095         d->m_themeId = QLatin1String("maps/") + firstTexture->sourceDir();
0096     }
0097 
0098     d->m_textureLayers = textureLayers;
0099 
0100     d->detectMaxTileLevel();
0101 }
0102 
0103 void MergedLayerDecorator::updateGroundOverlays(const QList<const GeoDataGroundOverlay *> &groundOverlays )
0104 {
0105     d->m_groundOverlays = groundOverlays;
0106 }
0107 
0108 
0109 int MergedLayerDecorator::textureLayersSize() const
0110 {
0111     return d->m_textureLayers.size();
0112 }
0113 
0114 int MergedLayerDecorator::maximumTileLevel() const
0115 {
0116     return d->m_maxTileLevel;
0117 }
0118 
0119 int MergedLayerDecorator::tileColumnCount( int level ) const
0120 {
0121     Q_ASSERT( !d->m_textureLayers.isEmpty() );
0122 
0123     const int levelZeroColumns = d->m_textureLayers.at( 0 )->levelZeroColumns();
0124 
0125     return TileLoaderHelper::levelToColumn( levelZeroColumns, level );
0126 }
0127 
0128 int MergedLayerDecorator::tileRowCount( int level ) const
0129 {
0130     Q_ASSERT( !d->m_textureLayers.isEmpty() );
0131 
0132     const int levelZeroRows = d->m_textureLayers.at( 0 )->levelZeroRows();
0133 
0134     return TileLoaderHelper::levelToRow( levelZeroRows, level );
0135 }
0136 
0137 const GeoSceneAbstractTileProjection *MergedLayerDecorator::tileProjection() const
0138 {
0139     Q_ASSERT( !d->m_textureLayers.isEmpty() );
0140 
0141     return d->m_textureLayers.at(0)->tileProjection();
0142 }
0143 
0144 QSize MergedLayerDecorator::tileSize() const
0145 {
0146     Q_ASSERT( !d->m_textureLayers.isEmpty() );
0147 
0148     return d->m_textureLayers.at( 0 )->tileSize();
0149 }
0150 
0151 StackedTile *MergedLayerDecorator::Private::createTile( const QVector<QSharedPointer<TextureTile> > &tiles ) const
0152 {
0153     Q_ASSERT( !tiles.isEmpty() );
0154 
0155     const TileId firstId = tiles.first()->id();
0156     const TileId id( 0, firstId.zoomLevel(), firstId.x(), firstId.y() );
0157 
0158     // Image for blending all the texture tiles on it
0159     QImage resultImage;
0160 
0161     // if there are more than one active texture layers, we have to convert the
0162     // result tile into QImage::Format_ARGB32_Premultiplied to make blending possible
0163     const bool withConversion = tiles.count() > 1 || m_showSunShading || m_showTileId || !m_groundOverlays.isEmpty();
0164     for ( const QSharedPointer<TextureTile> &tile: tiles ) {
0165 
0166         // Image blending. If there are several images in the same tile (like clouds
0167         // or hillshading images over the map) blend them all into only one image
0168 
0169         const Blending *const blending =  tile->blending();
0170         if ( blending ) {
0171 
0172             mDebug() << "blending";
0173 
0174             if ( resultImage.isNull() ) {
0175                 resultImage = QImage( tile->image()->size(), QImage::Format_ARGB32_Premultiplied );
0176             }
0177 
0178             blending->blend( &resultImage, tile.data() );
0179         }
0180         else {
0181             mDebug() << "no blending defined => copying top over bottom image";
0182             if ( withConversion ) {
0183                 resultImage = tile->image()->convertToFormat( QImage::Format_ARGB32_Premultiplied );
0184             } else {
0185                 resultImage = tile->image()->copy();
0186             }
0187         }
0188     }
0189 
0190     renderGroundOverlays( &resultImage, tiles );
0191 
0192     if ( m_showSunShading && !m_showCityLights ) {
0193         paintSunShading( &resultImage, id );
0194     }
0195 
0196     if ( m_showTileId ) {
0197         paintTileId( &resultImage, id );
0198     }
0199 
0200     return new StackedTile( id, resultImage, tiles );
0201 }
0202 
0203 void MergedLayerDecorator::Private::renderGroundOverlays( QImage *tileImage, const QVector<QSharedPointer<TextureTile> > &tiles ) const
0204 {
0205 
0206     /* All tiles are covering the same area. Pick one. */
0207     const TileId tileId = tiles.first()->id();
0208 
0209     const GeoDataLatLonBox tileLatLonBox = findRelevantTextureLayers(tileId).first()->tileProjection()->geoCoordinates(tileId);
0210 
0211     /* Map the ground overlay to the image. */
0212     for ( int i =  0; i < m_groundOverlays.size(); ++i ) {
0213 
0214         const GeoDataGroundOverlay* overlay = m_groundOverlays.at( i );
0215         if ( !overlay->isGloballyVisible() ) {
0216             continue;
0217         }
0218 
0219         const GeoDataLatLonBox overlayLatLonBox = overlay->latLonBox();
0220 
0221         if ( !tileLatLonBox.intersects( overlayLatLonBox.toCircumscribedRectangle() ) ) {
0222             continue;
0223         }
0224 
0225         const qreal pixelToLat = tileLatLonBox.height() / tileImage->height();
0226         const qreal pixelToLon = tileLatLonBox.width() / tileImage->width();
0227 
0228         const qreal latToPixel = overlay->icon().height() / overlayLatLonBox.height();
0229         const qreal lonToPixel = overlay->icon().width() / overlayLatLonBox.width();
0230 
0231         const qreal  global_height = tileImage->height()
0232                 * TileLoaderHelper::levelToRow( m_levelZeroRows, tileId.zoomLevel() );
0233         const qreal pixel2Rad = M_PI / global_height;
0234         const qreal rad2Pixel = global_height / M_PI;
0235 
0236         qreal latPixelPosition = rad2Pixel/2 * gdInv(tileLatLonBox.north());
0237         const bool isMercatorTileProjection = (m_textureLayers.at( 0 )->tileProjectionType() ==  GeoSceneAbstractTileProjection::Mercator);
0238 
0239         for ( int y = 0; y < tileImage->height(); ++y ) {
0240              QRgb *scanLine = ( QRgb* ) ( tileImage->scanLine( y ) );
0241 
0242              const qreal lat = isMercatorTileProjection
0243                      ? gd(2 * (latPixelPosition - y) * pixel2Rad )
0244                      : tileLatLonBox.north() - y * pixelToLat;
0245 
0246              for ( int x = 0; x < tileImage->width(); ++x, ++scanLine ) {
0247                  qreal lon = GeoDataCoordinates::normalizeLon( tileLatLonBox.west() + x * pixelToLon );
0248 
0249                  GeoDataCoordinates coords(lon, lat);
0250                  GeoDataCoordinates rotatedCoords(coords);
0251 
0252                  if (overlay->latLonBox().rotation() != 0) {
0253                     // Possible TODO: Make this faster by creating the axisMatrix beforehand
0254                     // and just call Quaternion::rotateAroundAxis(const matrix &m) here.
0255                     rotatedCoords = coords.rotateAround(overlayLatLonBox.center(), -overlay->latLonBox().rotation());
0256                  }
0257 
0258                  // TODO: The rotated latLonBox is bigger. We need to take this into account.
0259                  // (Currently the GroundOverlay sometimes gets clipped because of that)
0260                  if ( overlay->latLonBox().contains( rotatedCoords ) ) {
0261 
0262                      qreal px = GeoDataLatLonBox::width( rotatedCoords.longitude(), overlayLatLonBox.west() ) * lonToPixel;
0263                      qreal py = (qreal)( overlay->icon().height() ) - ( GeoDataLatLonBox::height( rotatedCoords.latitude(), overlayLatLonBox.south() ) * latToPixel ) - 1;
0264 
0265                      if ( px >= 0 && px < overlay->icon().width() && py >= 0 && py < overlay->icon().height() ) {
0266                          int alpha = qAlpha( overlay->icon().pixel( px, py ) );
0267                          if ( alpha != 0 )
0268                          {
0269                             QRgb result = ImageF::pixelF( overlay->icon(), px, py );
0270 
0271                             if (alpha == 255)
0272                             {
0273                                 *scanLine = result;
0274                             }
0275                             else
0276                             {
0277                                 *scanLine = qRgb( ( alpha * qRed(result) + (255 - alpha) * qRed(*scanLine) ) / 255,
0278                                             ( alpha * qGreen(result) + (255 - alpha) * qGreen(*scanLine) ) / 255,
0279                                             ( alpha * qBlue(result) + (255 - alpha) * qBlue(*scanLine) ) / 255 );
0280                             }
0281                          }
0282                      }
0283                  }
0284              }
0285         }
0286     }
0287 }
0288 
0289 StackedTile *MergedLayerDecorator::loadTile( const TileId &stackedTileId )
0290 {
0291     const QVector<const GeoSceneTextureTileDataset *> textureLayers = d->findRelevantTextureLayers( stackedTileId );
0292     QVector<QSharedPointer<TextureTile> > tiles;
0293     tiles.reserve(textureLayers.size());
0294 
0295     for ( const GeoSceneTextureTileDataset *layer: textureLayers ) {
0296         const TileId tileId( layer->sourceDir(), stackedTileId.zoomLevel(),
0297                              stackedTileId.x(), stackedTileId.y() );
0298 
0299         mDebug() << layer->sourceDir() << tileId << layer->tileSize() << layer->fileFormat();
0300 
0301         // Blending (how to merge the images into an only image)
0302         const Blending *blending = d->m_blendingFactory.findBlending( layer->blending() );
0303         if ( blending == nullptr && !layer->blending().isEmpty() ) {
0304             mDebug() << "could not find blending" << layer->blending();
0305         }
0306 
0307         const GeoSceneTextureTileDataset *const textureLayer = static_cast<const GeoSceneTextureTileDataset *>( layer );
0308         const QImage tileImage = d->m_tileLoader->loadTileImage( textureLayer, tileId, DownloadBrowse );
0309 
0310         QSharedPointer<TextureTile> tile( new TextureTile( tileId, tileImage, blending ) );
0311         tiles.append( tile );
0312     }
0313 
0314     Q_ASSERT( !tiles.isEmpty() );
0315 
0316     return d->createTile( tiles );
0317 }
0318 
0319 RenderState MergedLayerDecorator::renderState( const TileId &stackedTileId ) const
0320 {
0321     QString const nameTemplate = "Tile %1/%2/%3";
0322     RenderState state( nameTemplate.arg( stackedTileId.zoomLevel() )
0323                        .arg( stackedTileId.x() )
0324                        .arg( stackedTileId.y() ) );
0325     const QVector<const GeoSceneTextureTileDataset *> textureLayers = d->findRelevantTextureLayers( stackedTileId );
0326     for ( const GeoSceneTextureTileDataset *layer: textureLayers ) {
0327         const TileId tileId( layer->sourceDir(), stackedTileId.zoomLevel(),
0328                              stackedTileId.x(), stackedTileId.y() );
0329         RenderStatus tileStatus = Complete;
0330         switch ( TileLoader::tileStatus( layer, tileId ) ) {
0331         case TileLoader::Available:
0332             tileStatus = Complete;
0333             break;
0334         case TileLoader::Expired:
0335             tileStatus = WaitingForUpdate;
0336             break;
0337         case TileLoader::Missing:
0338             tileStatus = WaitingForData;
0339             break;
0340         }
0341 
0342         state.addChild( RenderState( layer->name(), tileStatus ) );
0343     }
0344 
0345     return state;
0346 }
0347 
0348 bool MergedLayerDecorator::hasTextureLayer() const
0349 {
0350     return !d->m_textureLayers.isEmpty();
0351 }
0352 
0353 StackedTile *MergedLayerDecorator::updateTile( const StackedTile &stackedTile, const TileId &tileId, const QImage &tileImage )
0354 {
0355     Q_ASSERT( !tileImage.isNull() );
0356 
0357     d->detectMaxTileLevel();
0358 
0359     QVector<QSharedPointer<TextureTile> > tiles = stackedTile.tiles();
0360 
0361     for ( int i = 0; i < tiles.count(); ++ i) {
0362         if ( tiles[i]->id() == tileId ) {
0363             const Blending *blending = tiles[i]->blending();
0364 
0365             tiles[i] = QSharedPointer<TextureTile>( new TextureTile( tileId, tileImage, blending ) );
0366         }
0367     }
0368 
0369     return d->createTile( tiles );
0370 }
0371 
0372 void MergedLayerDecorator::downloadStackedTile( const TileId &id, DownloadUsage usage )
0373 {
0374     const QVector<const GeoSceneTextureTileDataset *> textureLayers = d->findRelevantTextureLayers( id );
0375 
0376     for ( const GeoSceneTextureTileDataset *textureLayer: textureLayers ) {
0377         if (textureLayer->tileLevels().isEmpty() || textureLayer->tileLevels().contains(id.zoomLevel())) {
0378             if ( TileLoader::tileStatus( textureLayer, id ) != TileLoader::Available || usage == DownloadBrowse ) {
0379                 d->m_tileLoader->downloadTile( textureLayer, id, usage );
0380             }
0381         }
0382     }
0383 }
0384 
0385 void MergedLayerDecorator::setShowSunShading( bool show )
0386 {
0387     d->m_showSunShading = show;
0388 }
0389 
0390 bool MergedLayerDecorator::showSunShading() const
0391 {
0392     return d->m_showSunShading;
0393 }
0394 
0395 void MergedLayerDecorator::setShowCityLights( bool show )
0396 {
0397     d->m_showCityLights = show;
0398 }
0399 
0400 bool MergedLayerDecorator::showCityLights() const
0401 {
0402     return d->m_showCityLights;
0403 }
0404 
0405 void MergedLayerDecorator::setShowTileId( bool visible )
0406 {
0407     d->m_showTileId = visible;
0408 }
0409 
0410 void MergedLayerDecorator::Private::paintSunShading( QImage *tileImage, const TileId &id ) const
0411 {
0412     if ( tileImage->depth() != 32 )
0413         return;
0414 
0415     // TODO add support for 8-bit maps?
0416     // add sun shading
0417     const qreal  global_width  = tileImage->width()
0418             * TileLoaderHelper::levelToColumn( m_levelZeroColumns, id.zoomLevel() );
0419     const qreal  global_height = tileImage->height()
0420             * TileLoaderHelper::levelToRow( m_levelZeroRows, id.zoomLevel() );
0421     const qreal lon_scale = 2*M_PI / global_width;
0422     const qreal lat_scale = -M_PI / global_height;
0423     const int tileHeight = tileImage->height();
0424     const int tileWidth = tileImage->width();
0425 
0426     // First we determine the supporting point interval for the interpolation.
0427     const int n = maxDivisor( 30, tileWidth );
0428     const int ipRight = n * (int)( tileWidth / n );
0429 
0430     for ( int cur_y = 0; cur_y < tileHeight; ++cur_y ) {
0431         const qreal lat = lat_scale * ( id.y() * tileHeight + cur_y ) - 0.5*M_PI;
0432         const qreal a = sin( (lat+DEG2RAD * m_sunLocator->getLat() )/2.0 );
0433         const qreal c = cos(lat)*cos( -DEG2RAD * m_sunLocator->getLat() );
0434 
0435         QRgb* scanline = (QRgb*)tileImage->scanLine( cur_y );
0436 
0437         qreal lastShade = -10.0;
0438 
0439         int cur_x = 0;
0440 
0441         while ( cur_x < tileWidth ) {
0442 
0443             const bool interpolate = ( cur_x != 0 && cur_x < ipRight && cur_x + n < tileWidth );
0444 
0445             qreal shade = 0;
0446 
0447             if ( interpolate ) {
0448                 const int check = cur_x + n;
0449                 const qreal checklon   = lon_scale * ( id.x() * tileWidth + check );
0450                 shade = m_sunLocator->shading( checklon, a, c );
0451 
0452                 // if the shading didn't change across the interpolation
0453                 // interval move on and don't change anything.
0454                 if ( shade == lastShade && shade == 1.0 ) {
0455                     scanline += n;
0456                     cur_x += n;
0457                     continue;
0458                 }
0459                 if ( shade == lastShade && shade == 0.0 ) {
0460                     for ( int t = 0; t < n; ++t ) {
0461                         SunLocator::shadePixel(*scanline, shade);
0462                         ++scanline;
0463                     }
0464                     cur_x += n;
0465                     continue;
0466                 }
0467                 for ( int t = 0; t < n ; ++t ) {
0468                     const qreal lon   = lon_scale * ( id.x() * tileWidth + cur_x );
0469                     shade = m_sunLocator->shading( lon, a, c );
0470                     SunLocator::shadePixel(*scanline, shade);
0471                     ++scanline;
0472                     ++cur_x;
0473                 }
0474             }
0475 
0476             else {
0477                 // Make sure we don't exceed the image memory
0478                 if ( cur_x < tileWidth ) {
0479                     const qreal lon   = lon_scale * ( id.x() * tileWidth + cur_x );
0480                     shade = m_sunLocator->shading( lon, a, c );
0481                     SunLocator::shadePixel(*scanline, shade);
0482                     ++scanline;
0483                     ++cur_x;
0484                 }
0485             }
0486             lastShade = shade;
0487         }
0488     }
0489 }
0490 
0491 void MergedLayerDecorator::Private::paintTileId( QImage *tileImage, const TileId &id ) const
0492 {
0493     QString filename = QString( "%1_%2.jpg" )
0494             .arg(id.x(), tileDigits, 10, QLatin1Char('0'))
0495             .arg(id.y(), tileDigits, 10, QLatin1Char('0'));
0496 
0497     QPainter painter( tileImage );
0498 
0499     QColor foreground;
0500     QColor background;
0501 
0502     if ( ( (qreal)(id.x())/2 == id.x()/2 && (qreal)(id.y())/2 == id.y()/2 )
0503          || ( (qreal)(id.x())/2 != id.x()/2 && (qreal)(id.y())/2 != id.y()/2 )
0504          )
0505     {
0506         foreground.setNamedColor( "#FFFFFF" );
0507         background.setNamedColor( "#000000" );
0508     }
0509     else {
0510         foreground.setNamedColor( "#000000" );
0511         background.setNamedColor( "#FFFFFF" );
0512     }
0513 
0514     int   strokeWidth = 10;
0515     QPen  testPen( foreground );
0516     testPen.setWidth( strokeWidth );
0517     testPen.setJoinStyle( Qt::MiterJoin );
0518 
0519     painter.setPen( testPen );
0520     painter.drawRect( strokeWidth / 2, strokeWidth / 2,
0521                       tileImage->width()  - strokeWidth,
0522                       tileImage->height() - strokeWidth );
0523     QFont testFont(QStringLiteral("Sans Serif"), 12);
0524     QFontMetrics testFm( testFont );
0525     painter.setFont( testFont );
0526 
0527     QPen outlinepen( foreground );
0528     outlinepen.setWidthF( 6 );
0529 
0530     painter.setPen( outlinepen );
0531     painter.setBrush( background );
0532 
0533     QPainterPath   outlinepath;
0534 
0535     QPointF  baseline1( ( tileImage->width() - testFm.boundingRect(filename).width() ) / 2,
0536                         ( tileImage->height() * 0.25) );
0537     outlinepath.addText( baseline1, testFont, QString( "level: %1" ).arg(id.zoomLevel()) );
0538 
0539     QPointF  baseline2( ( tileImage->width() - testFm.boundingRect(filename).width() ) / 2,
0540                         tileImage->height() * 0.50 );
0541     outlinepath.addText( baseline2, testFont, filename );
0542 
0543     QPointF  baseline3( ( tileImage->width() - testFm.boundingRect(filename).width() ) / 2,
0544                         tileImage->height() * 0.75 );
0545     outlinepath.addText( baseline3, testFont, m_themeId );
0546 
0547     painter.drawPath( outlinepath );
0548 
0549     painter.setPen( Qt::NoPen );
0550     painter.drawPath( outlinepath );
0551 }
0552 
0553 void MergedLayerDecorator::Private::detectMaxTileLevel()
0554 {
0555     if ( m_textureLayers.isEmpty() ) {
0556         m_maxTileLevel = -1;
0557         return;
0558     }
0559 
0560     m_maxTileLevel = TileLoader::maximumTileLevel( *m_textureLayers.at( 0 ) );
0561 }
0562 
0563 QVector<const GeoSceneTextureTileDataset *> MergedLayerDecorator::Private::findRelevantTextureLayers( const TileId &stackedTileId ) const
0564 {
0565     QVector<const GeoSceneTextureTileDataset *> result;
0566 
0567     for ( const GeoSceneTextureTileDataset *candidate: m_textureLayers ) {
0568         Q_ASSERT( candidate );
0569         // check, if layer provides tiles for the current level
0570         if ( !candidate->hasMaximumTileLevel() ||
0571              candidate->maximumTileLevel() >= stackedTileId.zoomLevel() ) {
0572             //check if the tile intersects with texture bounds
0573             if (candidate->latLonBox().isNull()) {
0574                 result.append(candidate);
0575             }
0576             else {
0577                 const GeoDataLatLonBox bbox = candidate->tileProjection()->geoCoordinates(stackedTileId);
0578 
0579                 if (candidate->latLonBox().intersects(bbox)) {
0580                     result.append( candidate );
0581                 }
0582             }
0583         }
0584     }
0585 
0586     return result;
0587 }
0588 
0589 // TODO: This should likely go into a math class in the future ...
0590 
0591 int MergedLayerDecorator::Private::maxDivisor( int maximum, int fullLength )
0592 {
0593     // Find the optimal interpolation interval n for the
0594     // current image canvas width
0595     int best = 2;
0596 
0597     int  nEvalMin = fullLength;
0598     for ( int it = 1; it <= maximum; ++it ) {
0599         // The optimum is the interval which results in the least amount
0600         // supporting points taking into account the rest which can't
0601         // get used for interpolation.
0602         int nEval = fullLength / it + fullLength % it;
0603         if ( nEval < nEvalMin ) {
0604             nEvalMin = nEval;
0605             best = it;
0606         }
0607     }
0608     return best;
0609 }