File indexing completed on 2025-01-05 03:59:37

0001 /*
0002     SPDX-FileCopyrightText: 2010 Jens-Michael Hoffmann <jmho@c-xx.com>
0003     SPDX-FileCopyrightText: 2010-2012 Bernhard Beschow <bbeschow@cs.tu-berlin.de>
0004 
0005     SPDX-License-Identifier: LGPL-2.1-or-later
0006 */
0007 
0008 #include "TileLoader.h"
0009 
0010 #include <QDateTime>
0011 #include <QFileInfo>
0012 #include <QMetaType>
0013 #include <QImage>
0014 #include <QUrl>
0015 
0016 #include "GeoSceneTextureTileDataset.h"
0017 #include "GeoSceneTileDataset.h"
0018 #include "GeoSceneTypes.h"
0019 #include "GeoSceneVectorTileDataset.h"
0020 #include "GeoDataDocument.h"
0021 #include "HttpDownloadManager.h"
0022 #include "MarbleDirs.h"
0023 #include "TileId.h"
0024 #include "TileLoaderHelper.h"
0025 #include "ParseRunnerPlugin.h"
0026 #include "ParsingRunner.h"
0027 
0028 #include "digikam_debug.h"
0029 
0030 Q_DECLARE_METATYPE( Marble::DownloadUsage )
0031 
0032 namespace Marble
0033 {
0034 
0035 TileLoader::TileLoader(HttpDownloadManager * const downloadManager, const PluginManager *pluginManager) :
0036     m_pluginManager(pluginManager)
0037 {
0038     qRegisterMetaType<DownloadUsage>( "DownloadUsage" );
0039     connect( this, SIGNAL(downloadTile(QUrl,QString,QString,DownloadUsage)),
0040              downloadManager, SLOT(addJob(QUrl,QString,QString,DownloadUsage)));
0041     connect( downloadManager, SIGNAL(downloadComplete(QString,QString)),
0042              SLOT(updateTile(QString,QString)));
0043     connect( downloadManager, SIGNAL(downloadComplete(QByteArray,QString)),
0044              SLOT(updateTile(QByteArray,QString)));
0045 }
0046 
0047 TileLoader::~TileLoader()
0048 {
0049     // nothing to do
0050 }
0051 
0052 // If the tile image file is locally available:
0053 //     - if not expired: create ImageTile, set state to "uptodate", return it => done
0054 //     - if expired: create TextureTile, state is set to Expired by default, trigger dl,
0055 QImage TileLoader::loadTileImage( GeoSceneTextureTileDataset const *textureLayer, TileId const & tileId, DownloadUsage const usage )
0056 {
0057     QString const fileName = tileFileName( textureLayer, tileId );
0058 
0059     TileStatus status = tileStatus( textureLayer, tileId );
0060     if ( status != Missing ) {
0061         // check if an update should be triggered
0062 
0063         if ( status == Available ) {
0064             qCDebug(DIGIKAM_MARBLE_LOG) << Q_FUNC_INFO << tileId << "StateUptodate";
0065         } else {
0066             Q_ASSERT( status == Expired );
0067             qCDebug(DIGIKAM_MARBLE_LOG) << Q_FUNC_INFO << tileId << "StateExpired";
0068             triggerDownload( textureLayer, tileId, usage );
0069         }
0070 
0071         QImage const image( fileName );
0072         if ( !image.isNull() ) {
0073             // file is there, so create and return a tile object in any case
0074             return image;
0075         }
0076     }
0077 
0078     // tile was not locally available => trigger download and look for tiles in other levels
0079     // for scaling
0080 
0081     QImage replacementTile = scaledLowerLevelTile( textureLayer, tileId );
0082     Q_ASSERT( !replacementTile.isNull() );
0083 
0084     triggerDownload( textureLayer, tileId, usage );
0085 
0086     return replacementTile;
0087 }
0088 
0089 
0090 GeoDataDocument *TileLoader::loadTileVectorData( GeoSceneVectorTileDataset const *textureLayer, TileId const & tileId, DownloadUsage const usage )
0091 {
0092     // FIXME: textureLayer->fileFormat() could be used in the future for use just that parser, instead of all available parsers
0093 
0094     QString const fileName = tileFileName( textureLayer, tileId );
0095 
0096     TileStatus status = tileStatus( textureLayer, tileId );
0097     if ( status != Missing ) {
0098         // check if an update should be triggered
0099 
0100         if ( status == Available ) {
0101             qCDebug(DIGIKAM_MARBLE_LOG) << Q_FUNC_INFO << tileId << "StateUptodate";
0102         } else {
0103             Q_ASSERT( status == Expired );
0104             qCDebug(DIGIKAM_MARBLE_LOG) << Q_FUNC_INFO << tileId << "StateExpired";
0105             triggerDownload( textureLayer, tileId, usage );
0106         }
0107 
0108         QFile file ( fileName );
0109         if ( file.exists() ) {
0110 
0111             // File is ready, so parse and return the vector data in any case
0112             GeoDataDocument* document = openVectorFile(fileName);
0113             if (document) {
0114                 return document;
0115             }
0116         }
0117     } else {
0118         // tile was not locally available => trigger download
0119         triggerDownload( textureLayer, tileId, usage );
0120     }
0121 
0122     return nullptr;
0123 }
0124 
0125 // This method triggers a download of the given tile (without checking
0126 // expiration). It is called by upper layer (StackedTileLoader) when the tile
0127 // that should be reloaded is currently loaded in memory.
0128 //
0129 // post condition
0130 //     - download is triggered
0131 void TileLoader::downloadTile( GeoSceneTileDataset const *tileData, TileId const &tileId, DownloadUsage const usage )
0132 {
0133     triggerDownload( tileData, tileId, usage );
0134 }
0135 
0136 int TileLoader::maximumTileLevel( GeoSceneTileDataset const & tileData )
0137 {
0138     // if maximum tile level is configured in the DGML files,
0139     // then use it, otherwise use old detection code.
0140     if ( tileData.maximumTileLevel() >= 0 ) {
0141         return tileData.maximumTileLevel();
0142     }
0143 
0144     int maximumTileLevel = -1;
0145     const QFileInfo themeStr( tileData.themeStr() );
0146     const QString tilepath = themeStr.isAbsolute() ? themeStr.absoluteFilePath() : MarbleDirs::path( tileData.themeStr() );
0147     //    qCDebug(DIGIKAM_MARBLE_LOG) << "StackedTileLoader::maxPartialTileLevel tilepath" << tilepath;
0148     QStringList leveldirs = QDir( tilepath ).entryList( QDir::AllDirs | QDir::NoSymLinks
0149                                                         | QDir::NoDotAndDotDot );
0150 
0151     QStringList::const_iterator it = leveldirs.constBegin();
0152     QStringList::const_iterator const end = leveldirs.constEnd();
0153     for (; it != end; ++it ) {
0154         bool ok = true;
0155         const int value = (*it).toInt( &ok, 10 );
0156 
0157         if ( ok && value > maximumTileLevel )
0158             maximumTileLevel = value;
0159     }
0160 
0161     //    qCDebug(DIGIKAM_MARBLE_LOG) << "Detected maximum tile level that contains data: "
0162     //             << maxtilelevel;
0163     return maximumTileLevel + 1;
0164 }
0165 
0166 bool TileLoader::baseTilesAvailable( GeoSceneTileDataset const & tileData )
0167 {
0168     const int  levelZeroColumns = tileData.levelZeroColumns();
0169     const int  levelZeroRows    = tileData.levelZeroRows();
0170 
0171     bool result = true;
0172 
0173     // Check whether the tiles from the lowest texture level are available
0174     //
0175     for ( int column = 0; result && column < levelZeroColumns; ++column ) {
0176         for ( int row = 0; result && row < levelZeroRows; ++row ) {
0177             const TileId id( 0, 0, column, row );
0178             const QString tilepath = tileFileName( &tileData, id );
0179             result &= QFile::exists( tilepath );
0180             if (!result) {
0181                 qCDebug(DIGIKAM_MARBLE_LOG) << "Base tile " << tileData.relativeTileFileName( id ) << " is missing for source dir " << tileData.sourceDir();
0182             }
0183         }
0184     }
0185 
0186     return result;
0187 }
0188 
0189 TileLoader::TileStatus TileLoader::tileStatus( GeoSceneTileDataset const *tileData, const TileId &tileId )
0190 {
0191     QString const fileName = tileFileName( tileData, tileId );
0192     QFileInfo fileInfo( fileName );
0193     if ( !fileInfo.exists() ) {
0194         return Missing;
0195     }
0196 
0197     const QDateTime lastModified = fileInfo.lastModified();
0198     const int expireSecs = tileData->expire();
0199     const bool isExpired = lastModified.secsTo( QDateTime::currentDateTime() ) >= expireSecs;
0200     return isExpired ? Expired : Available;
0201 }
0202 
0203 void TileLoader::updateTile( QByteArray const & data, QString const & idStr )
0204 {
0205     QStringList const components = idStr.split(QLatin1Char(':'), Qt::SkipEmptyParts);
0206     Q_ASSERT( components.size() == 5 );
0207 
0208     QString const origin = components[0];
0209     QString const sourceDir = components[ 1 ];
0210     int const zoomLevel = components[ 2 ].toInt();
0211     int const tileX = components[ 3 ].toInt();
0212     int const tileY = components[ 4 ].toInt();
0213 
0214     TileId const id = TileId( sourceDir, zoomLevel, tileX, tileY );
0215 
0216     if (origin == QString::fromUtf8(GeoSceneTypes::GeoSceneTextureTileType)) {
0217         QImage const tileImage = QImage::fromData( data );
0218         if ( tileImage.isNull() )
0219             return;
0220 
0221         Q_EMIT tileCompleted( id, tileImage );
0222     }
0223 }
0224 
0225 void TileLoader::updateTile(const QString &fileName, const QString &idStr)
0226 {
0227     QStringList const components = idStr.split(QLatin1Char(':'), Qt::SkipEmptyParts);
0228     Q_ASSERT( components.size() == 5 );
0229 
0230     QString const origin = components[0];
0231     QString const sourceDir = components[ 1 ];
0232     int const zoomLevel = components[ 2 ].toInt();
0233     int const tileX = components[ 3 ].toInt();
0234     int const tileY = components[ 4 ].toInt();
0235 
0236     TileId const id = TileId( sourceDir, zoomLevel, tileX, tileY );
0237     if (origin == QString::fromUtf8(GeoSceneTypes::GeoSceneVectorTileType)) {
0238         GeoDataDocument* document = openVectorFile(MarbleDirs::path(fileName));
0239         if (document) {
0240             Q_EMIT tileCompleted(id,  document);
0241         }
0242     }
0243 }
0244 
0245 QString TileLoader::tileFileName( GeoSceneTileDataset const * tileData, TileId const & tileId )
0246 {
0247     QString const fileName = tileData->relativeTileFileName( tileId );
0248     QFileInfo const dirInfo( fileName );
0249     return dirInfo.isAbsolute() ? fileName : MarbleDirs::path( fileName );
0250 }
0251 
0252 void TileLoader::triggerDownload( GeoSceneTileDataset const *tileData, TileId const &id, DownloadUsage const usage )
0253 {
0254     if (id.zoomLevel() > 0) {
0255         int minValue = tileData->maximumTileLevel() == -1 ? id.zoomLevel() : qMin( id.zoomLevel(), tileData->maximumTileLevel() );
0256         if (id.zoomLevel() != qMax(tileData->minimumTileLevel(), minValue) ) {
0257             // Download only level 0 tiles and tiles between minimum and maximum tile level
0258             return;
0259         }
0260     }
0261 
0262     QUrl const sourceUrl = tileData->downloadUrl( id );
0263     QString const destFileName = tileData->relativeTileFileName( id );
0264     QString const idStr = QString::fromUtf8( "%1:%2:%3:%4:%5" ).arg( QString::fromUtf8(tileData->nodeType()), tileData->sourceDir() ).arg( id.zoomLevel() ).arg( id.x() ).arg( id.y() );
0265     Q_EMIT downloadTile( sourceUrl, destFileName, idStr, usage );
0266 }
0267 
0268 QImage TileLoader::scaledLowerLevelTile( const GeoSceneTextureTileDataset * textureData, TileId const & id )
0269 {
0270     qCDebug(DIGIKAM_MARBLE_LOG) << Q_FUNC_INFO << id;
0271 
0272     int const minimumLevel = textureData->minimumTileLevel();
0273     for ( int level = qMax<int>( 0, id.zoomLevel() - 1 ); level >= 0; --level ) {
0274         if (level > 0 && level < minimumLevel) {
0275             continue;
0276         }
0277         int const deltaLevel = id.zoomLevel() - level;
0278 
0279         TileId const replacementTileId( id.mapThemeIdHash(), level,
0280                                         id.x() >> deltaLevel, id.y() >> deltaLevel );
0281         QString const fileName = tileFileName( textureData, replacementTileId );
0282         qCDebug(DIGIKAM_MARBLE_LOG) << "TileLoader::scaledLowerLevelTile" << "trying" << fileName;
0283         QImage toScale = (!fileName.isEmpty() && QFile::exists(fileName)) ? QImage(fileName) : QImage();
0284 
0285         if ( level == 0 && toScale.isNull() ) {
0286             qCDebug(DIGIKAM_MARBLE_LOG) << "No level zero tile installed in map theme dir. Falling back to a transparent image for now.";
0287             QSize tileSize = textureData->tileSize();
0288             Q_ASSERT( !tileSize.isEmpty() ); // assured by textureLayer
0289             toScale = QImage( tileSize, QImage::Format_ARGB32_Premultiplied );
0290             toScale.fill( qRgba( 0, 0, 0, 0 ) );
0291         }
0292 
0293         if ( !toScale.isNull() ) {
0294             // which rect to scale?
0295             int const restTileX = id.x() % ( 1 << deltaLevel );
0296             int const restTileY = id.y() % ( 1 << deltaLevel );
0297             int const partWidth = qMax(1, toScale.width() >> deltaLevel);
0298             int const partHeight = qMax(1, toScale.height() >> deltaLevel);
0299             int const startX = restTileX * partWidth;
0300             int const startY = restTileY * partHeight;
0301             qCDebug(DIGIKAM_MARBLE_LOG) << "QImage::copy:" << startX << startY << partWidth << partHeight;
0302             QImage const part = toScale.copy( startX, startY, partWidth, partHeight );
0303             qCDebug(DIGIKAM_MARBLE_LOG) << "QImage::scaled:" << toScale.size();
0304             return part.scaled( toScale.size() );
0305         }
0306     }
0307 
0308     Q_ASSERT_X( false, "scaled image", "level zero image missing" ); // not reached
0309     return QImage();
0310 }
0311 
0312 GeoDataDocument *TileLoader::openVectorFile(const QString &fileName) const
0313 {
0314     QList<const ParseRunnerPlugin*> plugins = m_pluginManager->parsingRunnerPlugins();
0315     const QFileInfo fileInfo( fileName );
0316     const QString suffix = fileInfo.suffix().toLower();
0317     const QString completeSuffix = fileInfo.completeSuffix().toLower();
0318 
0319     for( const ParseRunnerPlugin *plugin: plugins ) {
0320         QStringList const extensions = plugin->fileExtensions();
0321         if ( extensions.contains( suffix ) || extensions.contains( completeSuffix ) ) {
0322             ParsingRunner* runner = plugin->newRunner();
0323             QString error;
0324             GeoDataDocument* document = runner->parseFile(fileName, UserDocument, error);
0325             if (!document && !error.isEmpty()) {
0326                 qCDebug(DIGIKAM_MARBLE_LOG) << QString::fromUtf8("Failed to open vector tile %1: %2").arg(fileName, error);
0327             }
0328             delete runner;
0329             return document;
0330         }
0331     }
0332 
0333     qCDebug(DIGIKAM_MARBLE_LOG) << "Unable to open vector tile " << fileName << ": No suitable plugin registered to parse this file format";
0334     return nullptr;
0335 }
0336 
0337 }
0338 
0339 #include "moc_TileLoader.cpp"