Marble

TileLoader.cpp
1 /*
2  SPDX-FileCopyrightText: 2010 Jens-Michael Hoffmann <[email protected]>
3  SPDX-FileCopyrightText: 2010-2012 Bernhard Beschow <[email protected]>
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 
29 Q_DECLARE_METATYPE( Marble::DownloadUsage )
30 
31 namespace Marble
32 {
33 
34 TileLoader::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 
46 TileLoader::~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,
54 QImage 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() << Q_FUNC_INFO << tileId << "StateUptodate";
64  } else {
65  Q_ASSERT( status == Expired );
66  mDebug() << Q_FUNC_INFO << 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 
89 GeoDataDocument *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() << Q_FUNC_INFO << tileId << "StateUptodate";
101  } else {
102  Q_ASSERT( status == Expired );
103  mDebug() << Q_FUNC_INFO << 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
130 void TileLoader::downloadTile( GeoSceneTileDataset const *tileData, TileId const &tileId, DownloadUsage const usage )
131 {
132  triggerDownload( tileData, tileId, usage );
133 }
134 
135 int 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 
150  QStringList::const_iterator it = leveldirs.constBegin();
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 
165 bool 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 
188 TileLoader::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 
202 void 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 
224 void 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 
244 QString 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 
251 void 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 
267 QImage TileLoader::scaledLowerLevelTile( const GeoSceneTextureTileDataset * textureData, TileId const & id )
268 {
269  mDebug() << Q_FUNC_INFO << 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 
311 GeoDataDocument *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();
322  QString error;
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"
Format_ARGB32_Premultiplied
int height() const const
void fill(uint pixelValue)
QDateTime currentDateTime()
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
DownloadUsage
This enum is used to describe the type of download.
Definition: MarbleGlobal.h:153
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
QStringView level(QStringView ifopt)
QImage scaled(int width, int height, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
QList::const_iterator constBegin() const const
bool exists() const const
int size() const const
qint64 secsTo(const QDateTime &other) const const
bool isEmpty() const const
bool isNull() const const
Q_SCRIPTABLE CaptureState status()
Binds a QML item to a specific geodetic location in screen coordinates.
QSize size() const const
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
QString toLower() const const
QList::const_iterator constEnd() const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QStringList entryList(QDir::Filters filters, QDir::SortFlags sort) const const
QImage fromData(const uchar *data, int size, const char *format)
const QList< QKeySequence > & end()
QImage copy(const QRect &rectangle) const const
QDebug mDebug()
a function to replace qDebug() in Marble library code
Definition: MarbleDebug.cpp:31
int width() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Mon Oct 2 2023 03:52:10 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.