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