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"