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"