File indexing completed on 2024-04-21 03:49:39
0001 // SPDX-License-Identifier: LGPL-2.1-or-later 0002 // 0003 // SPDX-FileCopyrightText: 2008 Torsten Rahn <tackat@kde.org> 0004 // SPDX-FileCopyrightText: 2008 Jens-Michael Hoffmann <jensmh@gmx.de> 0005 // 0006 0007 0008 // Own 0009 #include "MapThemeManager.h" 0010 0011 // Qt 0012 #include <QDir> 0013 #include <QFile> 0014 #include <QFileInfo> 0015 #include <QFileSystemWatcher> 0016 #include <QScopedPointer> 0017 #include <QString> 0018 #include <QStringList> 0019 #include <QStandardItemModel> 0020 0021 // Local dir 0022 #include "GeoDataPhotoOverlay.h" 0023 #include "GeoSceneDocument.h" 0024 #include "GeoSceneMap.h" 0025 #include "GeoSceneHead.h" 0026 #include "GeoSceneIcon.h" 0027 #include "GeoSceneParser.h" 0028 #include "GeoSceneLayer.h" 0029 #include "GeoSceneTileDataset.h" 0030 #include "GeoSceneTextureTileDataset.h" 0031 #include "GeoSceneProperty.h" 0032 #include "GeoSceneZoom.h" 0033 #include "GeoSceneSettings.h" 0034 #include "MarbleDebug.h" 0035 #include "MarbleDirs.h" 0036 #include "Planet.h" 0037 #include "PlanetFactory.h" 0038 0039 // Std 0040 #include <limits> 0041 0042 namespace 0043 { 0044 static const QString mapDirName = "maps"; 0045 static const int columnRelativePath = 1; 0046 } 0047 0048 namespace Marble 0049 { 0050 0051 class Q_DECL_HIDDEN MapThemeManager::Private 0052 { 0053 public: 0054 Private( MapThemeManager *parent ); 0055 ~Private(); 0056 0057 void directoryChanged( const QString& path ); 0058 void fileChanged( const QString & path ); 0059 0060 /** 0061 * @brief Updates the map theme model on request. 0062 * 0063 * This method should usually get invoked on startup or 0064 * by a QFileSystemWatcher instance. 0065 */ 0066 void updateMapThemeModel(); 0067 0068 void watchPaths(); 0069 0070 /** 0071 * @brief Adds directory paths and .dgml file paths to the given QStringList. 0072 */ 0073 static void addMapThemePaths( const QString& mapPathName, QStringList& result ); 0074 0075 /** 0076 * @brief Helper method for findMapThemes(). Searches for .dgml files below 0077 * given directory path. 0078 */ 0079 static QStringList findMapThemes( const QString& basePath ); 0080 0081 /** 0082 * @brief Searches for .dgml files below local and system map directory. 0083 */ 0084 static QStringList findMapThemes(); 0085 0086 static GeoSceneDocument* loadMapThemeFile( const QString& mapThemeId ); 0087 0088 /** 0089 * @brief Helper method for updateMapThemeModel(). 0090 */ 0091 static QList<QStandardItem *> createMapThemeRow( const QString& mapThemeID ); 0092 0093 /** 0094 * @brief Deletes any directory with its contents. 0095 * @param directory Path to directory 0096 * WARNING: Please do not raise this method's visibility in future, keep it private. 0097 */ 0098 static bool deleteDirectory( const QString &directory ); 0099 0100 MapThemeManager *const q; 0101 QStandardItemModel m_mapThemeModel; 0102 QStandardItemModel m_celestialList; 0103 QFileSystemWatcher m_fileSystemWatcher; 0104 bool m_isInitialized; 0105 0106 private: 0107 /** 0108 * @brief Returns all directory paths and .dgml file paths below local and 0109 * system map directory. 0110 */ 0111 static QStringList pathsToWatch(); 0112 }; 0113 0114 MapThemeManager::Private::Private( MapThemeManager *parent ) 0115 : q( parent ), 0116 m_mapThemeModel( 0, 3 ), 0117 m_celestialList(), 0118 m_fileSystemWatcher(), 0119 m_isInitialized( false ) 0120 { 0121 } 0122 0123 MapThemeManager::Private::~Private() 0124 { 0125 } 0126 0127 0128 MapThemeManager::MapThemeManager( QObject *parent ) 0129 : QObject( parent ), 0130 d( new Private( this ) ) 0131 { 0132 d->watchPaths(); 0133 connect( &d->m_fileSystemWatcher, SIGNAL(directoryChanged(QString)), 0134 this, SLOT(directoryChanged(QString))); 0135 connect( &d->m_fileSystemWatcher, SIGNAL(fileChanged(QString)), 0136 this, SLOT(fileChanged(QString))); 0137 } 0138 0139 MapThemeManager::~MapThemeManager() 0140 { 0141 delete d; 0142 } 0143 0144 QStringList MapThemeManager::mapThemeIds() const 0145 { 0146 QStringList result; 0147 0148 if ( !d->m_isInitialized ) { 0149 d->updateMapThemeModel(); 0150 d->m_isInitialized = true; 0151 } 0152 0153 const int mapThemeIdCount = d->m_mapThemeModel.rowCount(); 0154 result.reserve(mapThemeIdCount); 0155 for (int i = 0; i < mapThemeIdCount; ++i) { 0156 const QString id = d->m_mapThemeModel.data( d->m_mapThemeModel.index( i, 0 ), Qt::UserRole + 1 ).toString(); 0157 result << id; 0158 } 0159 0160 return result; 0161 } 0162 0163 GeoSceneDocument* MapThemeManager::loadMapTheme( const QString& mapThemeStringID ) 0164 { 0165 if ( mapThemeStringID.isEmpty() ) 0166 return nullptr; 0167 0168 return Private::loadMapThemeFile( mapThemeStringID ); 0169 } 0170 0171 void MapThemeManager::deleteMapTheme( const QString &mapThemeId ) 0172 { 0173 const QString dgmlPath = MarbleDirs::localPath() + QLatin1String("/maps/") + mapThemeId; 0174 QFileInfo dgmlFile(dgmlPath); 0175 0176 QString themeDir = dgmlFile.dir().absolutePath(); 0177 Private::deleteDirectory( themeDir ); 0178 } 0179 0180 bool MapThemeManager::Private::deleteDirectory( const QString& directory ) 0181 { 0182 QDir dir( directory ); 0183 bool result = true; 0184 0185 if ( dir.exists() ) { 0186 for( const QFileInfo &info: dir.entryInfoList( 0187 QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | 0188 QDir::AllDirs | QDir::Files, 0189 QDir::DirsFirst ) ) { 0190 0191 if ( info.isDir() ) { 0192 result = deleteDirectory( info.absoluteFilePath() ); 0193 } else { 0194 result = QFile::remove( info.absoluteFilePath() ); 0195 } 0196 0197 if ( !result ) { 0198 return result; 0199 } 0200 } 0201 0202 result = dir.rmdir( directory ); 0203 0204 if( !result ) { 0205 return result; 0206 } 0207 } 0208 0209 return result; 0210 } 0211 0212 GeoSceneDocument* MapThemeManager::Private::loadMapThemeFile( const QString& mapThemeStringID ) 0213 { 0214 const QString mapThemePath = mapDirName + QLatin1Char('/') + mapThemeStringID; 0215 const QString dgmlPath = MarbleDirs::path( mapThemePath ); 0216 0217 // Check whether file exists 0218 QFile file( dgmlPath ); 0219 if ( !file.exists() ) { 0220 qWarning() << "Map theme file does not exist:" << dgmlPath; 0221 return nullptr; 0222 } 0223 0224 // Open file in right mode 0225 const bool fileReadable = file.open( QIODevice::ReadOnly ); 0226 0227 if ( !fileReadable ) { 0228 qWarning() << "Map theme file not readable:" << dgmlPath; 0229 return nullptr; 0230 } 0231 0232 GeoSceneParser parser( GeoScene_DGML ); 0233 0234 if ( !parser.read( &file )) { 0235 qWarning() << "Map theme file not well-formed:" << dgmlPath; 0236 return nullptr; 0237 } 0238 0239 mDebug() << "Map theme file successfully loaded:" << dgmlPath; 0240 0241 // Get result document 0242 GeoSceneDocument* document = static_cast<GeoSceneDocument*>( parser.releaseDocument() ); 0243 Q_ASSERT( document ); 0244 return document; 0245 } 0246 0247 QStringList MapThemeManager::Private::pathsToWatch() 0248 { 0249 QStringList result; 0250 const QString localMapPathName = MarbleDirs::localPath() + QLatin1Char('/') + mapDirName; 0251 const QString systemMapPathName = MarbleDirs::systemPath() + QLatin1Char('/') + mapDirName; 0252 0253 if( !QDir().exists( localMapPathName ) ) { 0254 QDir().mkpath( localMapPathName ); 0255 } 0256 0257 result << localMapPathName; 0258 result << systemMapPathName; 0259 addMapThemePaths( localMapPathName, result ); 0260 addMapThemePaths( systemMapPathName, result ); 0261 return result; 0262 } 0263 0264 QStringList MapThemeManager::Private::findMapThemes( const QString& basePath ) 0265 { 0266 const QString mapPathName = basePath + QLatin1Char('/') + mapDirName; 0267 0268 QDir paths = QDir( mapPathName ); 0269 0270 QStringList mapPaths = paths.entryList( QStringList( "*" ), 0271 QDir::AllDirs 0272 | QDir::NoSymLinks 0273 | QDir::NoDotAndDotDot ); 0274 QStringList mapDirs; 0275 0276 for ( int planet = 0; planet < mapPaths.size(); ++planet ) { 0277 QDir themeDir = QDir(mapPathName + QLatin1Char('/') + mapPaths.at(planet)); 0278 QStringList themeMapPaths = themeDir.entryList( 0279 QStringList( "*" ), 0280 QDir::AllDirs | 0281 QDir::NoSymLinks | 0282 QDir::NoDotAndDotDot ); 0283 for ( int theme = 0; theme < themeMapPaths.size(); ++theme ) { 0284 mapDirs << mapPathName + QLatin1Char('/') + mapPaths.at(planet) + QLatin1Char('/') 0285 + themeMapPaths.at( theme ); 0286 } 0287 } 0288 0289 QStringList mapFiles; 0290 QStringListIterator it( mapDirs ); 0291 while ( it.hasNext() ) { 0292 QString themeDir = it.next() + QLatin1Char('/'); 0293 QString themeDirName = QDir(themeDir).path().section(QLatin1Char('/'), -2, -1); 0294 QStringList tmp = QDir( themeDir ).entryList( QStringList( "*.dgml" ), 0295 QDir::Files | QDir::NoSymLinks ); 0296 if ( !tmp.isEmpty() ) { 0297 QStringListIterator k( tmp ); 0298 while ( k.hasNext() ) { 0299 QString themeXml = k.next(); 0300 mapFiles << themeDirName + QLatin1Char('/') + themeXml; 0301 } 0302 } 0303 } 0304 0305 return mapFiles; 0306 } 0307 0308 QStringList MapThemeManager::Private::findMapThemes() 0309 { 0310 QStringList mapFilesLocal = findMapThemes( MarbleDirs::localPath() ); 0311 QStringList mapFilesSystem = findMapThemes( MarbleDirs::systemPath() ); 0312 QStringList allMapFiles( mapFilesLocal ); 0313 allMapFiles << mapFilesSystem; 0314 0315 // remove duplicate entries 0316 allMapFiles.sort(); 0317 for ( int i = 1; i < allMapFiles.size(); ++i ) { 0318 if ( allMapFiles.at(i) == allMapFiles.at( i-1 ) ) { 0319 allMapFiles.removeAt( i ); 0320 --i; 0321 } 0322 } 0323 0324 return allMapFiles; 0325 } 0326 0327 QStandardItemModel* MapThemeManager::mapThemeModel() 0328 { 0329 if ( !d->m_isInitialized ) { 0330 d->updateMapThemeModel(); 0331 d->m_isInitialized = true; 0332 } 0333 return &d->m_mapThemeModel; 0334 } 0335 0336 QStandardItemModel *MapThemeManager::celestialBodiesModel() 0337 { 0338 if ( !d->m_isInitialized ) { 0339 d->updateMapThemeModel(); 0340 d->m_isInitialized = true; 0341 } 0342 0343 return &d->m_celestialList; 0344 } 0345 0346 QList<QStandardItem *> MapThemeManager::Private::createMapThemeRow( QString const& mapThemeID ) 0347 { 0348 QList<QStandardItem *> itemList; 0349 0350 QScopedPointer<GeoSceneDocument> mapTheme( loadMapThemeFile( mapThemeID ) ); 0351 if ( !mapTheme || !mapTheme->head()->visible() ) { 0352 return itemList; 0353 } 0354 0355 QPixmap themeIconPixmap; 0356 0357 QString relativePath = mapDirName + QLatin1Char('/') 0358 + mapTheme->head()->target() + QLatin1Char('/') + mapTheme->head()->theme() + QLatin1Char('/') 0359 + mapTheme->head()->icon()->pixmap(); 0360 themeIconPixmap.load( MarbleDirs::path( relativePath ) ); 0361 0362 if ( themeIconPixmap.isNull() ) { 0363 relativePath = "svg/application-x-marble-gray.png"; 0364 themeIconPixmap.load( MarbleDirs::path( relativePath ) ); 0365 } 0366 else { 0367 // Make sure we don't keep excessively large previews in memory 0368 // TODO: Scale the icon down to the default icon size in MarbleSelectView. 0369 // For now maxIconSize already equals what's expected by the listview. 0370 QSize maxIconSize( 136, 136 ); 0371 if ( themeIconPixmap.size() != maxIconSize ) { 0372 mDebug() << "Smooth scaling theme icon"; 0373 themeIconPixmap = themeIconPixmap.scaled( maxIconSize, 0374 Qt::KeepAspectRatio, 0375 Qt::SmoothTransformation ); 0376 } 0377 } 0378 0379 QIcon mapThemeIcon = QIcon( themeIconPixmap ); 0380 0381 QString name = mapTheme->head()->name(); 0382 const QString translatedDescription = QCoreApplication::translate("DGML", mapTheme->head()->description().toUtf8().constData()); 0383 const QString toolTip = QLatin1String("<span style=\" max-width: 150 px;\"> ") + translatedDescription + QLatin1String(" </span>"); 0384 0385 QStandardItem *item = new QStandardItem( name ); 0386 item->setData(QCoreApplication::translate("DGML", name.toUtf8().constData()), Qt::DisplayRole); 0387 item->setData( mapThemeIcon, Qt::DecorationRole ); 0388 item->setData(toolTip, Qt::ToolTipRole); 0389 item->setData( mapThemeID, Qt::UserRole + 1 ); 0390 item->setData(translatedDescription, Qt::UserRole + 2); 0391 0392 itemList << item; 0393 0394 return itemList; 0395 } 0396 0397 void MapThemeManager::Private::updateMapThemeModel() 0398 { 0399 mDebug(); 0400 m_mapThemeModel.clear(); 0401 0402 m_mapThemeModel.setHeaderData(0, Qt::Horizontal, QObject::tr("Name")); 0403 0404 QStringList stringlist = findMapThemes(); 0405 QStringListIterator it( stringlist ); 0406 0407 while ( it.hasNext() ) { 0408 QString mapThemeID = it.next(); 0409 0410 QList<QStandardItem *> itemList = createMapThemeRow( mapThemeID ); 0411 if ( !itemList.empty() ) { 0412 m_mapThemeModel.appendRow( itemList ); 0413 } 0414 } 0415 0416 for ( const QString &mapThemeId: stringlist ) { 0417 const QString celestialBodyId = mapThemeId.section(QLatin1Char('/'), 0, 0); 0418 QString celestialBodyName = PlanetFactory::localizedName( celestialBodyId ); 0419 0420 QList<QStandardItem*> matchingItems = m_celestialList.findItems( celestialBodyId, Qt::MatchExactly, 1 ); 0421 if ( matchingItems.isEmpty() ) { 0422 m_celestialList.appendRow( QList<QStandardItem*>() 0423 << new QStandardItem( celestialBodyName ) 0424 << new QStandardItem( celestialBodyId ) ); 0425 } 0426 } 0427 } 0428 0429 void MapThemeManager::Private::watchPaths() 0430 { 0431 QStringList const paths = pathsToWatch(); 0432 QStringList const files = m_fileSystemWatcher.files(); 0433 QStringList const directories = m_fileSystemWatcher.directories(); 0434 // Check each resource to add that it is not being watched already, 0435 // otherwise some qWarning appears 0436 for( const QString &resource: paths ) { 0437 if ( !directories.contains( resource ) && !files.contains( resource ) ) { 0438 m_fileSystemWatcher.addPath( resource ); 0439 } 0440 } 0441 } 0442 0443 void MapThemeManager::Private::directoryChanged( const QString& path ) 0444 { 0445 mDebug() << "directoryChanged:" << path; 0446 watchPaths(); 0447 0448 mDebug() << "Emitting themesChanged()"; 0449 updateMapThemeModel(); 0450 emit q->themesChanged(); 0451 } 0452 0453 void MapThemeManager::Private::fileChanged( const QString& path ) 0454 { 0455 mDebug() << "fileChanged:" << path; 0456 0457 // 1. if the file does not (anymore) exist, it got deleted and we 0458 // have to delete the corresponding item from the model 0459 // 2. if the file exists it is changed and we have to replace 0460 // the item with a new one. 0461 0462 const QString mapThemeId = path.section(QLatin1Char('/'), -3); 0463 mDebug() << "mapThemeId:" << mapThemeId; 0464 QList<QStandardItem *> matchingItems = m_mapThemeModel.findItems( mapThemeId, 0465 Qt::MatchFixedString 0466 | Qt::MatchCaseSensitive, 0467 columnRelativePath ); 0468 mDebug() << "matchingItems:" << matchingItems.size(); 0469 Q_ASSERT( matchingItems.size() <= 1 ); 0470 int insertAtRow = 0; 0471 0472 if ( matchingItems.size() == 1 ) { 0473 const int row = matchingItems.front()->row(); 0474 insertAtRow = row; 0475 QList<QStandardItem *> toBeDeleted = m_mapThemeModel.takeRow( row ); 0476 while ( !toBeDeleted.isEmpty() ) { 0477 delete toBeDeleted.takeFirst(); 0478 } 0479 } 0480 0481 QFileInfo fileInfo( path ); 0482 if ( fileInfo.exists() ) { 0483 QList<QStandardItem *> newMapThemeRow = createMapThemeRow( mapThemeId ); 0484 if ( !newMapThemeRow.empty() ) { 0485 m_mapThemeModel.insertRow( insertAtRow, newMapThemeRow ); 0486 } 0487 } 0488 0489 emit q->themesChanged(); 0490 } 0491 0492 // 0493 // <mapPathName>/<orbDirName>/<themeDirName> 0494 // 0495 void MapThemeManager::Private::addMapThemePaths( const QString& mapPathName, QStringList& result ) 0496 { 0497 QDir mapPath( mapPathName ); 0498 QStringList orbDirNames = mapPath.entryList( QStringList( "*" ), 0499 QDir::AllDirs 0500 | QDir::NoSymLinks 0501 | QDir::NoDotAndDotDot ); 0502 QStringListIterator itOrb( orbDirNames ); 0503 while ( itOrb.hasNext() ) { 0504 const QString orbPathName = mapPathName + QLatin1Char('/') + itOrb.next(); 0505 result << orbPathName; 0506 0507 QDir orbPath( orbPathName ); 0508 QStringList themeDirNames = orbPath.entryList( QStringList( "*" ), 0509 QDir::AllDirs 0510 | QDir::NoSymLinks 0511 | QDir::NoDotAndDotDot ); 0512 QStringListIterator itThemeDir( themeDirNames ); 0513 while ( itThemeDir.hasNext() ) { 0514 const QString themePathName = orbPathName + QLatin1Char('/') + itThemeDir.next(); 0515 result << themePathName; 0516 0517 QDir themePath( themePathName ); 0518 QStringList themeFileNames = themePath.entryList( QStringList( "*.dgml" ), 0519 QDir::Files 0520 | QDir::NoSymLinks ); 0521 QStringListIterator itThemeFile( themeFileNames ); 0522 while ( itThemeFile.hasNext() ) { 0523 const QString themeFilePathName = themePathName + QLatin1Char('/') + itThemeFile.next(); 0524 result << themeFilePathName; 0525 } 0526 } 0527 } 0528 } 0529 0530 GeoSceneDocument *MapThemeManager::createMapThemeFromOverlay( const GeoDataPhotoOverlay *overlayData ) 0531 { 0532 GeoSceneDocument * document = new GeoSceneDocument(); 0533 document->head()->setDescription( overlayData->description() ); 0534 document->head()->setName( overlayData->name() ); 0535 document->head()->setTheme( "photo" ); 0536 document->head()->setTarget( "panorama" ); 0537 document->head()->setRadius(36000); 0538 document->head()->setVisible(true); 0539 0540 document->head()->zoom()->setMaximum(3500); 0541 document->head()->zoom()->setMinimum(900); 0542 document->head()->zoom()->setDiscrete(false); 0543 0544 GeoSceneLayer * layer = new GeoSceneLayer( "photo" ); 0545 layer->setBackend("texture"); 0546 0547 GeoSceneTextureTileDataset * texture = new GeoSceneTextureTileDataset( "map" ); 0548 texture->setExpire(std::numeric_limits<int>::max()); 0549 0550 QString fileName = overlayData->absoluteIconFile(); 0551 QFileInfo fileInfo( fileName ); 0552 fileName = fileInfo.fileName(); 0553 0554 QString sourceDir = fileInfo.absoluteDir().path(); 0555 0556 QString extension = fileInfo.suffix(); 0557 0558 texture->setSourceDir( sourceDir ); 0559 texture->setFileFormat( extension ); 0560 texture->setInstallMap( fileName ); 0561 texture->setTileProjection(GeoSceneAbstractTileProjection::Equirectangular); 0562 0563 layer->addDataset(texture); 0564 0565 document->map()->addLayer(layer); 0566 0567 GeoSceneSettings *settings = document->settings(); 0568 0569 GeoSceneProperty *gridProperty = new GeoSceneProperty( "coordinate-grid" ); 0570 gridProperty->setValue( false ); 0571 gridProperty->setAvailable( false ); 0572 settings->addProperty( gridProperty ); 0573 0574 GeoSceneProperty *overviewmap = new GeoSceneProperty( "overviewmap" ); 0575 overviewmap->setValue( false ); 0576 overviewmap->setAvailable( false ); 0577 settings->addProperty( overviewmap ); 0578 0579 GeoSceneProperty *compass = new GeoSceneProperty( "compass" ); 0580 compass->setValue( false ); 0581 compass->setAvailable( false ); 0582 settings->addProperty( compass ); 0583 0584 GeoSceneProperty *scalebar = new GeoSceneProperty( "scalebar" ); 0585 scalebar->setValue( true ); 0586 scalebar->setAvailable( true ); 0587 settings->addProperty( scalebar ); 0588 0589 return document; 0590 } 0591 0592 } 0593 0594 #include "moc_MapThemeManager.cpp"