Marble

MapThemeManager.cpp
1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2008 Torsten Rahn <[email protected]>
4 // SPDX-FileCopyrightText: 2008 Jens-Michael Hoffmann <[email protected]>
5 //
6 
7 
8 // Own
9 #include "MapThemeManager.h"
10 
11 // Qt
12 #include <QDir>
13 #include <QFile>
14 #include <QFileInfo>
15 #include <QFileSystemWatcher>
16 #include <QScopedPointer>
17 #include <QString>
18 #include <QStringList>
19 #include <QStandardItemModel>
20 
21 // Local dir
22 #include "GeoDataPhotoOverlay.h"
23 #include "GeoSceneDocument.h"
24 #include "GeoSceneMap.h"
25 #include "GeoSceneHead.h"
26 #include "GeoSceneIcon.h"
27 #include "GeoSceneParser.h"
28 #include "GeoSceneLayer.h"
29 #include "GeoSceneTileDataset.h"
30 #include "GeoSceneTextureTileDataset.h"
31 #include "GeoSceneProperty.h"
32 #include "GeoSceneZoom.h"
33 #include "GeoSceneSettings.h"
34 #include "MarbleDebug.h"
35 #include "MarbleDirs.h"
36 #include "Planet.h"
37 #include "PlanetFactory.h"
38 
39 // Std
40 #include <limits>
41 
42 namespace
43 {
44  static const QString mapDirName = "maps";
45  static const int columnRelativePath = 1;
46 }
47 
48 namespace Marble
49 {
50 
51 class Q_DECL_HIDDEN MapThemeManager::Private
52 {
53 public:
54  Private( MapThemeManager *parent );
55  ~Private();
56 
57  void directoryChanged( const QString& path );
58  void fileChanged( const QString & path );
59 
60  /**
61  * @brief Updates the map theme model on request.
62  *
63  * This method should usually get invoked on startup or
64  * by a QFileSystemWatcher instance.
65  */
66  void updateMapThemeModel();
67 
68  void watchPaths();
69 
70  /**
71  * @brief Adds directory paths and .dgml file paths to the given QStringList.
72  */
73  static void addMapThemePaths( const QString& mapPathName, QStringList& result );
74 
75  /**
76  * @brief Helper method for findMapThemes(). Searches for .dgml files below
77  * given directory path.
78  */
79  static QStringList findMapThemes( const QString& basePath );
80 
81  /**
82  * @brief Searches for .dgml files below local and system map directory.
83  */
84  static QStringList findMapThemes();
85 
86  static GeoSceneDocument* loadMapThemeFile( const QString& mapThemeId );
87 
88  /**
89  * @brief Helper method for updateMapThemeModel().
90  */
91  static QList<QStandardItem *> createMapThemeRow( const QString& mapThemeID );
92 
93  /**
94  * @brief Deletes any directory with its contents.
95  * @param directory Path to directory
96  * WARNING: Please do not raise this method's visibility in future, keep it private.
97  */
98  static bool deleteDirectory( const QString &directory );
99 
100  MapThemeManager *const q;
101  QStandardItemModel m_mapThemeModel;
102  QStandardItemModel m_celestialList;
103  QFileSystemWatcher m_fileSystemWatcher;
104  bool m_isInitialized;
105 
106 private:
107  /**
108  * @brief Returns all directory paths and .dgml file paths below local and
109  * system map directory.
110  */
111  static QStringList pathsToWatch();
112 };
113 
114 MapThemeManager::Private::Private( MapThemeManager *parent )
115  : q( parent ),
116  m_mapThemeModel( 0, 3 ),
117  m_celestialList(),
118  m_fileSystemWatcher(),
119  m_isInitialized( false )
120 {
121 }
122 
123 MapThemeManager::Private::~Private()
124 {
125 }
126 
127 
129  : QObject( parent ),
130  d( new Private( this ) )
131 {
132  d->watchPaths();
133  connect( &d->m_fileSystemWatcher, SIGNAL(directoryChanged(QString)),
134  this, SLOT(directoryChanged(QString)));
135  connect( &d->m_fileSystemWatcher, SIGNAL(fileChanged(QString)),
136  this, SLOT(fileChanged(QString)));
137 }
138 
139 MapThemeManager::~MapThemeManager()
140 {
141  delete d;
142 }
143 
145 {
146  QStringList result;
147 
148  if ( !d->m_isInitialized ) {
149  d->updateMapThemeModel();
150  d->m_isInitialized = true;
151  }
152 
153  const int mapThemeIdCount = d->m_mapThemeModel.rowCount();
154  result.reserve(mapThemeIdCount);
155  for (int i = 0; i < mapThemeIdCount; ++i) {
156  const QString id = d->m_mapThemeModel.data( d->m_mapThemeModel.index( i, 0 ), Qt::UserRole + 1 ).toString();
157  result << id;
158  }
159 
160  return result;
161 }
162 
164 {
165  if ( mapThemeStringID.isEmpty() )
166  return nullptr;
167 
168  return Private::loadMapThemeFile( mapThemeStringID );
169 }
170 
171 void MapThemeManager::deleteMapTheme( const QString &mapThemeId )
172 {
173  const QString dgmlPath = MarbleDirs::localPath() + QLatin1String("/maps/") + mapThemeId;
174  QFileInfo dgmlFile(dgmlPath);
175 
176  QString themeDir = dgmlFile.dir().absolutePath();
177  Private::deleteDirectory( themeDir );
178 }
179 
180 bool MapThemeManager::Private::deleteDirectory( const QString& directory )
181 {
182  QDir dir( directory );
183  bool result = true;
184 
185  if ( dir.exists() ) {
186  for( const QFileInfo &info: dir.entryInfoList(
189  QDir::DirsFirst ) ) {
190 
191  if ( info.isDir() ) {
192  result = deleteDirectory( info.absoluteFilePath() );
193  } else {
194  result = QFile::remove( info.absoluteFilePath() );
195  }
196 
197  if ( !result ) {
198  return result;
199  }
200  }
201 
202  result = dir.rmdir( directory );
203 
204  if( !result ) {
205  return result;
206  }
207  }
208 
209  return result;
210 }
211 
212 GeoSceneDocument* MapThemeManager::Private::loadMapThemeFile( const QString& mapThemeStringID )
213 {
214  const QString mapThemePath = mapDirName + QLatin1Char('/') + mapThemeStringID;
215  const QString dgmlPath = MarbleDirs::path( mapThemePath );
216 
217  // Check whether file exists
218  QFile file( dgmlPath );
219  if ( !file.exists() ) {
220  qWarning() << "Map theme file does not exist:" << dgmlPath;
221  return nullptr;
222  }
223 
224  // Open file in right mode
225  const bool fileReadable = file.open( QIODevice::ReadOnly );
226 
227  if ( !fileReadable ) {
228  qWarning() << "Map theme file not readable:" << dgmlPath;
229  return nullptr;
230  }
231 
232  GeoSceneParser parser( GeoScene_DGML );
233 
234  if ( !parser.read( &file )) {
235  qWarning() << "Map theme file not well-formed:" << dgmlPath;
236  return nullptr;
237  }
238 
239  mDebug() << "Map theme file successfully loaded:" << dgmlPath;
240 
241  // Get result document
242  GeoSceneDocument* document = static_cast<GeoSceneDocument*>( parser.releaseDocument() );
243  Q_ASSERT( document );
244  return document;
245 }
246 
247 QStringList MapThemeManager::Private::pathsToWatch()
248 {
249  QStringList result;
250  const QString localMapPathName = MarbleDirs::localPath() + QLatin1Char('/') + mapDirName;
251  const QString systemMapPathName = MarbleDirs::systemPath() + QLatin1Char('/') + mapDirName;
252 
253  if( !QDir().exists( localMapPathName ) ) {
254  QDir().mkpath( localMapPathName );
255  }
256 
257  result << localMapPathName;
258  result << systemMapPathName;
259  addMapThemePaths( localMapPathName, result );
260  addMapThemePaths( systemMapPathName, result );
261  return result;
262 }
263 
264 QStringList MapThemeManager::Private::findMapThemes( const QString& basePath )
265 {
266  const QString mapPathName = basePath + QLatin1Char('/') + mapDirName;
267 
268  QDir paths = QDir( mapPathName );
269 
270  QStringList mapPaths = paths.entryList( QStringList( "*" ),
274  QStringList mapDirs;
275 
276  for ( int planet = 0; planet < mapPaths.size(); ++planet ) {
277  QDir themeDir = QDir(mapPathName + QLatin1Char('/') + mapPaths.at(planet));
278  QStringList themeMapPaths = themeDir.entryList(
279  QStringList( "*" ),
280  QDir::AllDirs |
283  for ( int theme = 0; theme < themeMapPaths.size(); ++theme ) {
284  mapDirs << mapPathName + QLatin1Char('/') + mapPaths.at(planet) + QLatin1Char('/')
285  + themeMapPaths.at( theme );
286  }
287  }
288 
289  QStringList mapFiles;
290  QStringListIterator it( mapDirs );
291  while ( it.hasNext() ) {
292  QString themeDir = it.next() + QLatin1Char('/');
293  QString themeDirName = QDir(themeDir).path().section(QLatin1Char('/'), -2, -1);
294  QStringList tmp = QDir( themeDir ).entryList( QStringList( "*.dgml" ),
296  if ( !tmp.isEmpty() ) {
297  QStringListIterator k( tmp );
298  while ( k.hasNext() ) {
299  QString themeXml = k.next();
300  mapFiles << themeDirName + QLatin1Char('/') + themeXml;
301  }
302  }
303  }
304 
305  return mapFiles;
306 }
307 
308 QStringList MapThemeManager::Private::findMapThemes()
309 {
310  QStringList mapFilesLocal = findMapThemes( MarbleDirs::localPath() );
311  QStringList mapFilesSystem = findMapThemes( MarbleDirs::systemPath() );
312  QStringList allMapFiles( mapFilesLocal );
313  allMapFiles << mapFilesSystem;
314 
315  // remove duplicate entries
316  allMapFiles.sort();
317  for ( int i = 1; i < allMapFiles.size(); ++i ) {
318  if ( allMapFiles.at(i) == allMapFiles.at( i-1 ) ) {
319  allMapFiles.removeAt( i );
320  --i;
321  }
322  }
323 
324  return allMapFiles;
325 }
326 
328 {
329  if ( !d->m_isInitialized ) {
330  d->updateMapThemeModel();
331  d->m_isInitialized = true;
332  }
333  return &d->m_mapThemeModel;
334 }
335 
337 {
338  if ( !d->m_isInitialized ) {
339  d->updateMapThemeModel();
340  d->m_isInitialized = true;
341  }
342 
343  return &d->m_celestialList;
344 }
345 
346 QList<QStandardItem *> MapThemeManager::Private::createMapThemeRow( QString const& mapThemeID )
347 {
348  QList<QStandardItem *> itemList;
349 
350  QScopedPointer<GeoSceneDocument> mapTheme( loadMapThemeFile( mapThemeID ) );
351  if ( !mapTheme || !mapTheme->head()->visible() ) {
352  return itemList;
353  }
354 
355  QPixmap themeIconPixmap;
356 
357  QString relativePath = mapDirName + QLatin1Char('/')
358  + mapTheme->head()->target() + QLatin1Char('/') + mapTheme->head()->theme() + QLatin1Char('/')
359  + mapTheme->head()->icon()->pixmap();
360  themeIconPixmap.load( MarbleDirs::path( relativePath ) );
361 
362  if ( themeIconPixmap.isNull() ) {
363  relativePath = "svg/application-x-marble-gray.png";
364  themeIconPixmap.load( MarbleDirs::path( relativePath ) );
365  }
366  else {
367  // Make sure we don't keep excessively large previews in memory
368  // TODO: Scale the icon down to the default icon size in MarbleSelectView.
369  // For now maxIconSize already equals what's expected by the listview.
370  QSize maxIconSize( 136, 136 );
371  if ( themeIconPixmap.size() != maxIconSize ) {
372  mDebug() << "Smooth scaling theme icon";
373  themeIconPixmap = themeIconPixmap.scaled( maxIconSize,
376  }
377  }
378 
379  QIcon mapThemeIcon = QIcon( themeIconPixmap );
380 
381  QString name = mapTheme->head()->name();
382  const QString translatedDescription = QCoreApplication::translate("DGML", mapTheme->head()->description().toUtf8().constData());
383  const QString toolTip = QLatin1String("<span style=\" max-width: 150 px;\"> ") + translatedDescription + QLatin1String(" </span>");
384 
385  QStandardItem *item = new QStandardItem( name );
386  item->setData(QCoreApplication::translate("DGML", name.toUtf8().constData()), Qt::DisplayRole);
387  item->setData( mapThemeIcon, Qt::DecorationRole );
388  item->setData(toolTip, Qt::ToolTipRole);
389  item->setData( mapThemeID, Qt::UserRole + 1 );
390  item->setData(translatedDescription, Qt::UserRole + 2);
391 
392  itemList << item;
393 
394  return itemList;
395 }
396 
397 void MapThemeManager::Private::updateMapThemeModel()
398 {
399  mDebug() << "updateMapThemeModel";
400  m_mapThemeModel.clear();
401 
402  m_mapThemeModel.setHeaderData(0, Qt::Horizontal, QObject::tr("Name"));
403 
404  QStringList stringlist = findMapThemes();
405  QStringListIterator it( stringlist );
406 
407  while ( it.hasNext() ) {
408  QString mapThemeID = it.next();
409 
410  QList<QStandardItem *> itemList = createMapThemeRow( mapThemeID );
411  if ( !itemList.empty() ) {
412  m_mapThemeModel.appendRow( itemList );
413  }
414  }
415 
416  for ( const QString &mapThemeId: stringlist ) {
417  const QString celestialBodyId = mapThemeId.section(QLatin1Char('/'), 0, 0);
418  QString celestialBodyName = PlanetFactory::localizedName( celestialBodyId );
419 
420  QList<QStandardItem*> matchingItems = m_celestialList.findItems( celestialBodyId, Qt::MatchExactly, 1 );
421  if ( matchingItems.isEmpty() ) {
422  m_celestialList.appendRow( QList<QStandardItem*>()
423  << new QStandardItem( celestialBodyName )
424  << new QStandardItem( celestialBodyId ) );
425  }
426  }
427 }
428 
429 void MapThemeManager::Private::watchPaths()
430 {
431  QStringList const paths = pathsToWatch();
432  QStringList const files = m_fileSystemWatcher.files();
433  QStringList const directories = m_fileSystemWatcher.directories();
434  // Check each resource to add that it is not being watched already,
435  // otherwise some qWarning appears
436  for( const QString &resource: paths ) {
437  if ( !directories.contains( resource ) && !files.contains( resource ) ) {
438  m_fileSystemWatcher.addPath( resource );
439  }
440  }
441 }
442 
443 void MapThemeManager::Private::directoryChanged( const QString& path )
444 {
445  mDebug() << "directoryChanged:" << path;
446  watchPaths();
447 
448  mDebug() << "Emitting themesChanged()";
449  updateMapThemeModel();
450  emit q->themesChanged();
451 }
452 
453 void MapThemeManager::Private::fileChanged( const QString& path )
454 {
455  mDebug() << "fileChanged:" << path;
456 
457  // 1. if the file does not (anymore) exist, it got deleted and we
458  // have to delete the corresponding item from the model
459  // 2. if the file exists it is changed and we have to replace
460  // the item with a new one.
461 
462  const QString mapThemeId = path.section(QLatin1Char('/'), -3);
463  mDebug() << "mapThemeId:" << mapThemeId;
464  QList<QStandardItem *> matchingItems = m_mapThemeModel.findItems( mapThemeId,
467  columnRelativePath );
468  mDebug() << "matchingItems:" << matchingItems.size();
469  Q_ASSERT( matchingItems.size() <= 1 );
470  int insertAtRow = 0;
471 
472  if ( matchingItems.size() == 1 ) {
473  const int row = matchingItems.front()->row();
474  insertAtRow = row;
475  QList<QStandardItem *> toBeDeleted = m_mapThemeModel.takeRow( row );
476  while ( !toBeDeleted.isEmpty() ) {
477  delete toBeDeleted.takeFirst();
478  }
479  }
480 
481  QFileInfo fileInfo( path );
482  if ( fileInfo.exists() ) {
483  QList<QStandardItem *> newMapThemeRow = createMapThemeRow( mapThemeId );
484  if ( !newMapThemeRow.empty() ) {
485  m_mapThemeModel.insertRow( insertAtRow, newMapThemeRow );
486  }
487  }
488 
489  emit q->themesChanged();
490 }
491 
492 //
493 // <mapPathName>/<orbDirName>/<themeDirName>
494 //
495 void MapThemeManager::Private::addMapThemePaths( const QString& mapPathName, QStringList& result )
496 {
497  QDir mapPath( mapPathName );
498  QStringList orbDirNames = mapPath.entryList( QStringList( "*" ),
502  QStringListIterator itOrb( orbDirNames );
503  while ( itOrb.hasNext() ) {
504  const QString orbPathName = mapPathName + QLatin1Char('/') + itOrb.next();
505  result << orbPathName;
506 
507  QDir orbPath( orbPathName );
508  QStringList themeDirNames = orbPath.entryList( QStringList( "*" ),
512  QStringListIterator itThemeDir( themeDirNames );
513  while ( itThemeDir.hasNext() ) {
514  const QString themePathName = orbPathName + QLatin1Char('/') + itThemeDir.next();
515  result << themePathName;
516 
517  QDir themePath( themePathName );
518  QStringList themeFileNames = themePath.entryList( QStringList( "*.dgml" ),
520  | QDir::NoSymLinks );
521  QStringListIterator itThemeFile( themeFileNames );
522  while ( itThemeFile.hasNext() ) {
523  const QString themeFilePathName = themePathName + QLatin1Char('/') + itThemeFile.next();
524  result << themeFilePathName;
525  }
526  }
527  }
528 }
529 
530 GeoSceneDocument *MapThemeManager::createMapThemeFromOverlay( const GeoDataPhotoOverlay *overlayData )
531 {
532  GeoSceneDocument * document = new GeoSceneDocument();
533  document->head()->setDescription( overlayData->description() );
534  document->head()->setName( overlayData->name() );
535  document->head()->setTheme( "photo" );
536  document->head()->setTarget( "panorama" );
537  document->head()->setRadius(36000);
538  document->head()->setVisible(true);
539 
540  document->head()->zoom()->setMaximum(3500);
541  document->head()->zoom()->setMinimum(900);
542  document->head()->zoom()->setDiscrete(false);
543 
544  GeoSceneLayer * layer = new GeoSceneLayer( "photo" );
545  layer->setBackend("texture");
546 
547  GeoSceneTextureTileDataset * texture = new GeoSceneTextureTileDataset( "map" );
548  texture->setExpire(std::numeric_limits<int>::max());
549 
550  QString fileName = overlayData->absoluteIconFile();
551  QFileInfo fileInfo( fileName );
552  fileName = fileInfo.fileName();
553 
554  QString sourceDir = fileInfo.absoluteDir().path();
555 
556  QString extension = fileInfo.suffix();
557 
558  texture->setSourceDir( sourceDir );
559  texture->setFileFormat( extension );
560  texture->setInstallMap( fileName );
561  texture->setTileProjection(GeoSceneAbstractTileProjection::Equirectangular);
562 
563  layer->addDataset(texture);
564 
565  document->map()->addLayer(layer);
566 
567  GeoSceneSettings *settings = document->settings();
568 
569  GeoSceneProperty *gridProperty = new GeoSceneProperty( "coordinate-grid" );
570  gridProperty->setValue( false );
571  gridProperty->setAvailable( false );
572  settings->addProperty( gridProperty );
573 
574  GeoSceneProperty *overviewmap = new GeoSceneProperty( "overviewmap" );
575  overviewmap->setValue( false );
576  overviewmap->setAvailable( false );
577  settings->addProperty( overviewmap );
578 
579  GeoSceneProperty *compass = new GeoSceneProperty( "compass" );
580  compass->setValue( false );
581  compass->setAvailable( false );
582  settings->addProperty( compass );
583 
584  GeoSceneProperty *scalebar = new GeoSceneProperty( "scalebar" );
585  scalebar->setValue( true );
586  scalebar->setAvailable( true );
587  settings->addProperty( scalebar );
588 
589  return document;
590 }
591 
592 }
593 
594 #include "moc_MapThemeManager.cpp"
QStandardItemModel * celestialBodiesModel()
Provides a model of all installed planets.
QStringList mapThemeIds() const
Returns a list of all locally available map theme IDs.
QString section(QChar sep, int start, int end, QString::SectionFlags flags) const const
void addDataset(GeoSceneAbstractDataset *)
Add a data set to the legend.
UserRole
static GeoSceneDocument * createMapThemeFromOverlay(const GeoDataPhotoOverlay *overlayData)
Returns a map as a GeoSceneDocument object created from a GeoDataPhotoOverlay.
bool remove()
QSize size() const const
QString translate(const char *context, const char *sourceText, const char *disambiguation, int n)
static QString localizedName(const QString &id)
Returns a localized name of the planet with the given ID.
Provides access to all map themes installed locally.
void addProperty(GeoSceneProperty *property)
Add a property to the settings.
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
NoDotAndDotDot
QString suffix() const const
Layer of a GeoScene document.
Definition: GeoSceneLayer.h:28
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
static void deleteMapTheme(const QString &mapThemeId)
Deletes the map theme with the specified map theme ID.
MapThemeManager(QObject *parent=nullptr)
Constructor.
void reserve(int alloc)
QStandardItemModel * mapThemeModel()
Provides a model of the locally existing themes.
bool empty() const const
int size() const const
KeepAspectRatio
T takeFirst()
Horizontal
bool isEmpty() const const
const T & at(int i) const const
bool mkpath(const QString &dirPath) const const
Settings property within a GeoScene document.
bool isEmpty() const const
Binds a QML item to a specific geodetic location in screen coordinates.
QString absolutePath() const const
Settings of a GeoScene document.
QDir absoluteDir() const const
QString fileName() const const
QString path() const const
bool isNull() const const
bool load(const QString &fileName, const char *format, Qt::ImageConversionFlags flags)
void addLayer(GeoSceneLayer *)
Add a new layer to the map.
Definition: GeoSceneMap.cpp:71
QPixmap scaled(int width, int height, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
KIOFILEWIDGETS_EXPORT QString dir(const QString &fileClass)
QString path(const QString &relativePath)
const char * name(StandardAction id)
virtual void setData(const QVariant &value, int role)
MatchExactly
QStringList entryList(QDir::Filters filters, QDir::SortFlags sort) const const
static GeoSceneDocument * loadMapTheme(const QString &mapThemeStringID)
Returns the map theme as a GeoSceneDocument object.
SmoothTransformation
QString tr(const char *sourceText, const char *disambiguation, int n)
T & front()
A container for features parsed from the DGML file.
QDebug mDebug()
a function to replace qDebug() in Marble library code
Definition: MarbleDebug.cpp:31
void sort(Qt::CaseSensitivity cs)
QDir dir() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Oct 1 2023 04:09:37 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.