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

KDE's Doxygen guidelines are available online.