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"