Marble

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

KDE's Doxygen guidelines are available online.