Marble

TileLoader.cpp
1/*
2 SPDX-FileCopyrightText: 2010 Jens-Michael Hoffmann <jmho@c-xx.com>
3 SPDX-FileCopyrightText: 2010-2012 Bernhard Beschow <bbeschow@cs.tu-berlin.de>
4
5 SPDX-License-Identifier: LGPL-2.1-or-later
6*/
7
8#include "TileLoader.h"
9
10#include <QDateTime>
11#include <QFileInfo>
12#include <QImage>
13#include <QMetaType>
14#include <QUrl>
15
16#include "GeoDataDocument.h"
17#include "GeoSceneTextureTileDataset.h"
18#include "GeoSceneTileDataset.h"
19#include "GeoSceneTypes.h"
20#include "GeoSceneVectorTileDataset.h"
21#include "HttpDownloadManager.h"
22#include "MarbleDebug.h"
23#include "MarbleDirs.h"
24#include "ParseRunnerPlugin.h"
25#include "ParsingRunner.h"
26#include "TileId.h"
27
28Q_DECLARE_METATYPE(Marble::DownloadUsage)
29
30namespace Marble
31{
32
33TileLoader::TileLoader(HttpDownloadManager *const downloadManager, const PluginManager *pluginManager)
34 : m_pluginManager(pluginManager)
35{
36 qRegisterMetaType<DownloadUsage>("DownloadUsage");
37 connect(this, &TileLoader::tileRequested, downloadManager, &HttpDownloadManager::addJob);
38 connect(downloadManager, SIGNAL(downloadComplete(QString, QString)), SLOT(updateTile(QString, QString)));
39 connect(downloadManager, SIGNAL(downloadComplete(QByteArray, QString)), SLOT(updateTile(QByteArray, QString)));
40}
41
42TileLoader::~TileLoader()
43{
44 // nothing to do
45}
46
47// If the tile image file is locally available:
48// - if not expired: create ImageTile, set state to "uptodate", return it => done
49// - if expired: create TextureTile, state is set to Expired by default, trigger dl,
50QImage TileLoader::loadTileImage(GeoSceneTextureTileDataset const *textureLayer, TileId const &tileId, DownloadUsage const usage)
51{
52 QString const fileName = tileFileName(textureLayer, tileId);
53
54 TileStatus status = tileStatus(textureLayer, tileId);
55 if (status != Missing) {
56 // check if an update should be triggered
57
58 if (status == Available) {
59 mDebug() << tileId << "StateUptodate";
60 } else {
61 Q_ASSERT(status == Expired);
62 mDebug() << tileId << "StateExpired";
63 triggerDownload(textureLayer, tileId, usage);
64 }
65
66 QImage const image(fileName);
67 if (!image.isNull()) {
68 // file is there, so create and return a tile object in any case
69 return image;
70 }
71 }
72
73 // tile was not locally available => trigger download and look for tiles in other levels
74 // for scaling
75
76 QImage replacementTile = scaledLowerLevelTile(textureLayer, tileId);
77 Q_ASSERT(!replacementTile.isNull());
78
79 triggerDownload(textureLayer, tileId, usage);
80
81 return replacementTile;
82}
83
84GeoDataDocument *TileLoader::loadTileVectorData(GeoSceneVectorTileDataset const *textureLayer, TileId const &tileId, DownloadUsage const usage)
85{
86 // FIXME: textureLayer->fileFormat() could be used in the future for use just that parser, instead of all available parsers
87
88 QString const fileName = tileFileName(textureLayer, tileId);
89
90 TileStatus status = tileStatus(textureLayer, tileId);
91 if (status != Missing) {
92 // check if an update should be triggered
93
94 if (status == Available) {
95 mDebug() << tileId << "StateUptodate";
96 } else {
97 Q_ASSERT(status == Expired);
98 mDebug() << tileId << "StateExpired";
99 triggerDownload(textureLayer, tileId, usage);
100 }
101
102 QFile file(fileName);
103 if (file.exists()) {
104 // File is ready, so parse and return the vector data in any case
105 GeoDataDocument *document = openVectorFile(fileName);
106 if (document) {
107 return document;
108 }
109 }
110 } else {
111 // tile was not locally available => trigger download
112 triggerDownload(textureLayer, tileId, usage);
113 }
114
115 return nullptr;
116}
117
118// This method triggers a download of the given tile (without checking
119// expiration). It is called by upper layer (StackedTileLoader) when the tile
120// that should be reloaded is currently loaded in memory.
121//
122// post condition
123// - download is triggered
124void TileLoader::downloadTile(GeoSceneTileDataset const *tileData, TileId const &tileId, DownloadUsage const usage)
125{
126 triggerDownload(tileData, tileId, usage);
127}
128
129int TileLoader::maximumTileLevel(GeoSceneTileDataset const &tileData)
130{
131 // if maximum tile level is configured in the DGML files,
132 // then use it, otherwise use old detection code.
133 if (tileData.maximumTileLevel() >= 0) {
134 return tileData.maximumTileLevel();
135 }
136
137 int maximumTileLevel = -1;
138 const QFileInfo themeStr(tileData.themeStr());
139 const QString tilepath = themeStr.isAbsolute() ? themeStr.absoluteFilePath() : MarbleDirs::path(tileData.themeStr());
140 // mDebug() << "StackedTileLoader::maxPartialTileLevel tilepath" << tilepath;
142
144 QStringList::const_iterator const end = leveldirs.constEnd();
145 for (; it != end; ++it) {
146 bool ok = true;
147 const int value = (*it).toInt(&ok, 10);
148
149 if (ok && value > maximumTileLevel)
150 maximumTileLevel = value;
151 }
152
153 // mDebug() << "Detected maximum tile level that contains data: "
154 // << maxtilelevel;
155 return maximumTileLevel + 1;
156}
157
158bool TileLoader::baseTilesAvailable(GeoSceneTileDataset const &tileData)
159{
160 const int levelZeroColumns = tileData.levelZeroColumns();
161 const int levelZeroRows = tileData.levelZeroRows();
162
163 bool result = true;
164
165 // Check whether the tiles from the lowest texture level are available
166 //
167 for (int column = 0; result && column < levelZeroColumns; ++column) {
168 for (int row = 0; result && row < levelZeroRows; ++row) {
169 const TileId id(0, 0, column, row);
170 const QString tilepath = tileFileName(&tileData, id);
171 result &= QFile::exists(tilepath);
172 if (!result) {
173 mDebug() << "Base tile " << tileData.relativeTileFileName(id) << " is missing for source dir " << tileData.sourceDir();
174 }
175 }
176 }
177
178 return result;
179}
180
181TileLoader::TileStatus TileLoader::tileStatus(GeoSceneTileDataset const *tileData, const TileId &tileId)
182{
183 QString const fileName = tileFileName(tileData, tileId);
184 QFileInfo fileInfo(fileName);
185 if (!fileInfo.exists()) {
186 return Missing;
187 }
188
189 const QDateTime lastModified = fileInfo.lastModified();
190 const int expireSecs = tileData->expire();
191 const bool isExpired = lastModified.secsTo(QDateTime::currentDateTime()) >= expireSecs;
192 return isExpired ? Expired : Available;
193}
194
195void TileLoader::updateTile(QByteArray const &data, QString const &idStr)
196{
197 QStringList const components = idStr.split(QLatin1Char(':'), Qt::SkipEmptyParts);
198 Q_ASSERT(components.size() == 5);
199
200 QString const origin = components[0];
201 QString const sourceDir = components[1];
202 int const zoomLevel = components[2].toInt();
203 int const tileX = components[3].toInt();
204 int const tileY = components[4].toInt();
205
206 TileId const id = TileId(sourceDir, zoomLevel, tileX, tileY);
207
208 if (origin == QString::fromLatin1(GeoSceneTypes::GeoSceneTextureTileType)) {
209 QImage const tileImage = QImage::fromData(data);
210 if (tileImage.isNull())
211 return;
212
213 Q_EMIT tileCompleted(id, tileImage);
214 }
215}
216
217void TileLoader::updateTile(const QString &fileName, const QString &idStr)
218{
219 QStringList const components = idStr.split(QLatin1Char(':'), Qt::SkipEmptyParts);
220 Q_ASSERT(components.size() == 5);
221
222 QString const origin = components[0];
223 QString const sourceDir = components[1];
224 int const zoomLevel = components[2].toInt();
225 int const tileX = components[3].toInt();
226 int const tileY = components[4].toInt();
227
228 TileId const id = TileId(sourceDir, zoomLevel, tileX, tileY);
229 if (origin == QString::fromLatin1(GeoSceneTypes::GeoSceneVectorTileType)) {
230 GeoDataDocument *document = openVectorFile(MarbleDirs::path(fileName));
231 if (document) {
232 Q_EMIT tileCompleted(id, document);
233 }
234 }
235}
236
237QString TileLoader::tileFileName(GeoSceneTileDataset const *tileData, TileId const &tileId)
238{
239 QString const fileName = tileData->relativeTileFileName(tileId);
240 QFileInfo const dirInfo(fileName);
241 return dirInfo.isAbsolute() ? fileName : MarbleDirs::path(fileName);
242}
243
244void TileLoader::triggerDownload(GeoSceneTileDataset const *tileData, TileId const &id, DownloadUsage const usage)
245{
246 if (id.zoomLevel() > 0) {
247 int minValue = tileData->maximumTileLevel() == -1 ? id.zoomLevel() : qMin(id.zoomLevel(), tileData->maximumTileLevel());
248 if (id.zoomLevel() != qMax(tileData->minimumTileLevel(), minValue)) {
249 // Download only level 0 tiles and tiles between minimum and maximum tile level
250 return;
251 }
252 }
253
254 QUrl const sourceUrl = tileData->downloadUrl(id);
255 QString const destFileName = tileData->relativeTileFileName(id);
256 QString const idStr =
257 QStringLiteral("%1:%2:%3:%4:%5").arg(QString::fromLatin1(tileData->nodeType()), tileData->sourceDir()).arg(id.zoomLevel()).arg(id.x()).arg(id.y());
258 Q_EMIT tileRequested(sourceUrl, destFileName, idStr, usage);
259}
260
261QImage TileLoader::scaledLowerLevelTile(const GeoSceneTextureTileDataset *textureData, TileId const &id)
262{
263 mDebug() << id;
264
265 int const minimumLevel = textureData->minimumTileLevel();
266 for (int level = qMax<int>(0, id.zoomLevel() - 1); level >= 0; --level) {
267 if (level > 0 && level < minimumLevel) {
268 continue;
269 }
270 int const deltaLevel = id.zoomLevel() - level;
271
272 TileId const replacementTileId(id.mapThemeIdHash(), level, id.x() >> deltaLevel, id.y() >> deltaLevel);
273 QString const fileName = tileFileName(textureData, replacementTileId);
274 mDebug() << "TileLoader::scaledLowerLevelTile"
275 << "trying" << fileName;
276 QImage toScale = (!fileName.isEmpty() && QFile::exists(fileName)) ? QImage(fileName) : QImage();
277
278 if (level == 0 && toScale.isNull()) {
279 mDebug() << "No level zero tile installed in map theme dir. Falling back to a transparent image for now.";
280 QSize tileSize = textureData->tileSize();
281 Q_ASSERT(!tileSize.isEmpty()); // assured by textureLayer
282 toScale = QImage(tileSize, QImage::Format_ARGB32_Premultiplied);
283 toScale.fill(qRgba(0, 0, 0, 0));
284 }
285
286 if (!toScale.isNull()) {
287 // which rect to scale?
288 int const restTileX = id.x() % (1 << deltaLevel);
289 int const restTileY = id.y() % (1 << deltaLevel);
290 int const partWidth = qMax(1, toScale.width() >> deltaLevel);
291 int const partHeight = qMax(1, toScale.height() >> deltaLevel);
292 int const startX = restTileX * partWidth;
293 int const startY = restTileY * partHeight;
294 mDebug() << "QImage::copy:" << startX << startY << partWidth << partHeight;
295 QImage const part = toScale.copy(startX, startY, partWidth, partHeight);
296 mDebug() << "QImage::scaled:" << toScale.size();
297 return part.scaled(toScale.size());
298 }
299 }
300
301 Q_ASSERT_X(false, "scaled image", "level zero image missing"); // not reached
302 return {};
303}
304
305GeoDataDocument *TileLoader::openVectorFile(const QString &fileName) const
306{
307 QList<const ParseRunnerPlugin *> plugins = m_pluginManager->parsingRunnerPlugins();
308 const QFileInfo fileInfo(fileName);
309 const QString suffix = fileInfo.suffix().toLower();
310 const QString completeSuffix = fileInfo.completeSuffix().toLower();
311
312 for (const ParseRunnerPlugin *plugin : std::as_const(plugins)) {
313 QStringList const extensions = plugin->fileExtensions();
314 if (extensions.contains(suffix) || extensions.contains(completeSuffix)) {
315 ParsingRunner *runner = plugin->newRunner();
317 GeoDataDocument *document = runner->parseFile(fileName, UserDocument, error);
318 if (!document && !error.isEmpty()) {
319 mDebug() << QStringLiteral("Failed to open vector tile %1: %2").arg(fileName, error);
320 }
321 delete runner;
322 return document;
323 }
324 }
325
326 mDebug() << "Unable to open vector tile " << fileName << ": No suitable plugin registered to parse this file format";
327 return nullptr;
328}
329
330}
331
332#include "moc_TileLoader.cpp"
Q_SCRIPTABLE CaptureState status()
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
QStringView level(QStringView ifopt)
const QList< QKeySequence > & end()
Binds a QML item to a specific geodetic location in screen coordinates.
DownloadUsage
This enum is used to describe the type of download.
QDateTime currentDateTime()
qint64 secsTo(const QDateTime &other) const const
QStringList entryList(Filters filters, SortFlags sort) const const
bool exists() const const
QImage fromData(QByteArrayView data, const char *format)
bool isNull() const const
const_iterator constBegin() const const
const_iterator constEnd() const const
qsizetype size() const const
QString arg(Args &&... args) const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QString toLower() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
SkipEmptyParts
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 Mon Nov 18 2024 12:15:46 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.