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