Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qgeofiletilecache.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:critical reason:filesystem-manipulation
5
7
9
10#include <QDir>
11#include <QStandardPaths>
12#include <QMetaType>
13#include <QPixmap>
14#include <QDebug>
15
16#include <QtCore/qsavefile.h>
17
19
20using namespace Qt::StringLiterals;
21
23{
24public:
26 {
27 if (cache)
28 cache->evictFromMemoryCache(this);
29 }
30
32 QGeoFileTileCache *cache;
35};
36
37void QCache3QTileEvictionPolicy::aboutToBeRemoved(const QGeoTileSpec &key, QSharedPointer<QGeoCachedTileDisk> obj)
38{
39 Q_UNUSED(key);
40 // set the cache pointer to zero so we can't call evictFromDiskCache
41 obj->cache = nullptr;
42}
43
44void QCache3QTileEvictionPolicy::aboutToBeEvicted(const QGeoTileSpec &key, QSharedPointer<QGeoCachedTileDisk> obj)
45{
46 Q_UNUSED(key);
47 Q_UNUSED(obj);
48 // leave the pointer set if it's a real eviction
49}
50
52{
53 if (cache)
54 cache->evictFromDiskCache(this);
55}
56
57QGeoFileTileCache::QGeoFileTileCache(const QString &directory, QObject *parent)
58 : QAbstractGeoTileCache(parent), directory_(directory)
59{
60}
61
62void QGeoFileTileCache::init()
63{
64 const QString basePath = baseCacheDirectory() + QLatin1String("QtLocation/");
65
66 // delete old tiles from QtLocation 5.7 or prior
67 // Newer version use plugin-specific subdirectories, versioned with qt version so those are not affected.
68 // TODO Remove cache cleanup in Qt 6
69 QDir baseDir(basePath);
70 if (baseDir.exists()) {
71 const QStringList oldCacheFiles = baseDir.entryList(QDir::Files);
72 for (const QString& file : oldCacheFiles)
73 baseDir.remove(file);
74 const QStringList oldCacheDirs = { QStringLiteral("osm"), QStringLiteral("mapbox"), QStringLiteral("here") };
75 for (const QString& d : oldCacheDirs) {
76 QDir oldCacheDir(basePath + QLatin1Char('/') + d);
77 if (oldCacheDir.exists())
78 oldCacheDir.removeRecursively();
79 }
80 }
81
82 if (directory_.isEmpty()) {
83 directory_ = baseLocationCacheDirectory();
84 qWarning() << "Plugin uses uninitialized QGeoFileTileCache directory which was deleted during startup";
85 }
86
87 const bool directoryCreated = QDir::root().mkpath(directory_);
88 if (!directoryCreated)
89 qWarning() << "Failed to create cache directory " << directory_;
90
91 // default values
92 if (!isDiskCostSet_) { // If setMaxDiskUsage has not been called yet
93 if (costStrategyDisk_ == ByteSize)
94 setMaxDiskUsage(50 * 1024 * 1024);
95 else
96 setMaxDiskUsage(1000);
97 }
98
99 if (!isMemoryCostSet_) { // If setMaxMemoryUsage has not been called yet
100 if (costStrategyMemory_ == ByteSize)
101 setMaxMemoryUsage(3 * 1024 * 1024);
102 else
103 setMaxMemoryUsage(100);
104 }
105
106 if (!isTextureCostSet_) { // If setExtraTextureUsage has not been called yet
107 if (costStrategyTexture_ == ByteSize)
108 setExtraTextureUsage(6 * 1024 * 1024);
109 else
110 setExtraTextureUsage(30); // byte size of texture is >> compressed image, hence unitary cost should be lower
111 }
112
113 loadTiles();
114}
115
116void QGeoFileTileCache::loadTiles()
117{
118 QStringList formats;
119 formats << QLatin1String("*.*");
120
121 QDir dir(directory_);
122 const QStringList files = dir.entryList(formats, QDir::Files);
123#if 0 // workaround for QTBUG-60581
124 // Method:
125 // 1. read each queue file then, if each file exists, deserialize the data into the appropriate
126 // cache queue.
127 for (int i = 1; i<=4; i++) {
128 QString filename = dir.filePath(QString::fromLatin1("queue") + QString::number(i));
129 QFile file(filename);
130 if (!file.open(QIODevice::ReadOnly))
131 continue;
132 QList<QSharedPointer<QGeoCachedTileDisk> > queue;
133 QList<QGeoTileSpec> specs;
134 QList<int> costs;
135 while (!file.atEnd()) {
136 QByteArray line = file.readLine().trimmed();
137 QString filename = QString::fromLatin1(line.constData(), line.length());
138 if (dir.exists(filename)){
139 files.removeOne(filename);
140 QGeoTileSpec spec = filenameToTileSpec(filename);
141 if (spec.zoom() == -1)
142 continue;
143 QSharedPointer<QGeoCachedTileDisk> tileDisk(new QGeoCachedTileDisk);
144 tileDisk->filename = dir.filePath(filename);
145 tileDisk->cache = this;
146 tileDisk->spec = spec;
147 QFileInfo fi(tileDisk->filename);
148 specs.append(spec);
149 queue.append(tileDisk);
150 if (costStrategyDisk_ == ByteSize)
151 costs.append(fi.size());
152 else
153 costs.append(1);
154
155 }
156 }
157
158 diskCache_.deserializeQueue(i, specs, queue, costs);
159 file.close();
160 }
161#endif
162 // 2. remaining tiles that aren't registered in a queue get pushed into cache here
163 // this is a backup, in case the queue manifest files get deleted or out of sync due to
164 // the application not closing down properly
165 for (const auto &file : files) {
166 QGeoTileSpec spec = filenameToTileSpec(file);
167 if (spec.zoom() == -1)
168 continue;
169 QString filename = dir.filePath(file);
170 addToDiskCache(spec, filename);
171 }
172}
173
174QGeoFileTileCache::~QGeoFileTileCache()
175{
176#if 0 // workaround for QTBUG-60581
177 // write disk cache queues to disk
178 QDir dir(directory_);
179 for (int i = 1; i<=4; i++) {
180 QString filename = dir.filePath(QString::fromLatin1("queue") + QString::number(i));
181 QFile file(filename);
182 if (!file.open(QIODevice::WriteOnly)){
183 qWarning() << "Unable to write tile cache file " << filename;
184 continue;
185 }
186 QList<QSharedPointer<QGeoCachedTileDisk> > queue;
187 diskCache_.serializeQueue(i, queue);
188 for (const QSharedPointer<QGeoCachedTileDisk> &tile : queue) {
189 if (tile.isNull())
190 continue;
191
192 // we just want the filename here, not the full path
193 int index = tile->filename.lastIndexOf(QLatin1Char('/'));
194 QByteArray filename = tile->filename.mid(index + 1).toLatin1() + '\n';
195 file.write(filename);
196 }
197 file.close();
198 }
199#endif
200}
201
202void QGeoFileTileCache::printStats()
203{
204 textureCache_.printStats();
205 memoryCache_.printStats();
206 diskCache_.printStats();
207}
208
209void QGeoFileTileCache::setMaxDiskUsage(int diskUsage)
210{
211 diskCache_.setMaxCost(diskUsage);
212 isDiskCostSet_ = true;
213}
214
215int QGeoFileTileCache::maxDiskUsage() const
216{
217 return diskCache_.maxCost();
218}
219
220int QGeoFileTileCache::diskUsage() const
221{
222 return diskCache_.totalCost();
223}
224
225void QGeoFileTileCache::setMaxMemoryUsage(int memoryUsage)
226{
227 memoryCache_.setMaxCost(memoryUsage);
228 isMemoryCostSet_ = true;
229}
230
231int QGeoFileTileCache::maxMemoryUsage() const
232{
233 return memoryCache_.maxCost();
234}
235
236int QGeoFileTileCache::memoryUsage() const
237{
238 return memoryCache_.totalCost();
239}
240
241void QGeoFileTileCache::setExtraTextureUsage(int textureUsage)
242{
243 extraTextureUsage_ = textureUsage;
244 textureCache_.setMaxCost(minTextureUsage_ + extraTextureUsage_);
245 isTextureCostSet_ = true;
246}
247
248void QGeoFileTileCache::setMinTextureUsage(int textureUsage)
249{
250 minTextureUsage_ = textureUsage;
251 textureCache_.setMaxCost(minTextureUsage_ + extraTextureUsage_);
252}
253
254int QGeoFileTileCache::maxTextureUsage() const
255{
256 return textureCache_.maxCost();
257}
258
259int QGeoFileTileCache::minTextureUsage() const
260{
261 return minTextureUsage_;
262}
263
264
265int QGeoFileTileCache::textureUsage() const
266{
267 return textureCache_.totalCost();
268}
269
270void QGeoFileTileCache::clearAll()
271{
272 textureCache_.clear();
273 memoryCache_.clear();
274 diskCache_.clear();
275 QDir dir(directory_);
276 dir.setNameFilters(QStringList() << QLatin1String("*-*-*-*.*"));
277 dir.setFilter(QDir::Files);
278 for (const QString &dirFile : dir.entryList()) {
279 dir.remove(dirFile);
280 }
281}
282
283void QGeoFileTileCache::clearMapId(const int mapId)
284{
285 for (const QGeoTileSpec &k : diskCache_.keys())
286 if (k.mapId() == mapId)
287 diskCache_.remove(k, true);
288 for (const QGeoTileSpec &k : memoryCache_.keys())
289 if (k.mapId() == mapId)
290 memoryCache_.remove(k);
291 for (const QGeoTileSpec &k : textureCache_.keys())
292 if (k.mapId() == mapId)
293 textureCache_.remove(k);
294
295 // TODO: It seems the cache leaves residues, like some tiles do not get picked up.
296 // After the above calls, files that shouldnt be left behind are still on disk.
297 // Do an additional pass and make sure what has to be deleted gets deleted.
298 QDir dir(directory_);
299 QStringList formats;
300 formats << QLatin1String("*.*");
301 const QStringList files = dir.entryList(formats, QDir::Files);
302 qWarning() << "Old tile data detected. Cache eviction left out "<< files.size() << "tiles";
303 for (const QString &tileFileName : files) {
304 QGeoTileSpec spec = filenameToTileSpec(tileFileName);
305 if (spec.mapId() != mapId)
306 continue;
307 QFile::remove(dir.filePath(tileFileName));
308 }
309}
310
311void QGeoFileTileCache::setCostStrategyDisk(QAbstractGeoTileCache::CostStrategy costStrategy)
312{
313 costStrategyDisk_ = costStrategy;
314}
315
316QAbstractGeoTileCache::CostStrategy QGeoFileTileCache::costStrategyDisk() const
317{
318 return costStrategyDisk_;
319}
320
321void QGeoFileTileCache::setCostStrategyMemory(QAbstractGeoTileCache::CostStrategy costStrategy)
322{
323 costStrategyMemory_ = costStrategy;
324}
325
326QAbstractGeoTileCache::CostStrategy QGeoFileTileCache::costStrategyMemory() const
327{
328 return costStrategyMemory_;
329}
330
331void QGeoFileTileCache::setCostStrategyTexture(QAbstractGeoTileCache::CostStrategy costStrategy)
332{
333 costStrategyTexture_ = costStrategy;
334}
335
336QAbstractGeoTileCache::CostStrategy QGeoFileTileCache::costStrategyTexture() const
337{
338 return costStrategyTexture_;
339}
340
341QSharedPointer<QGeoTileTexture> QGeoFileTileCache::get(const QGeoTileSpec &spec)
342{
343 QSharedPointer<QGeoTileTexture> tt = getFromMemory(spec);
344 if (tt)
345 return tt;
346 return getFromDisk(spec);
347}
348
349void QGeoFileTileCache::insert(const QGeoTileSpec &spec,
350 const QByteArray &bytes,
351 const QString &format,
352 QAbstractGeoTileCache::CacheAreas areas)
353{
354 if (bytes.isEmpty())
355 return;
356
357 if (areas & QAbstractGeoTileCache::DiskCache) {
358 QString filename = tileSpecToFilename(spec, format, directory_);
359 addToDiskCache(spec, filename, bytes);
360 }
361
362 if (areas & QAbstractGeoTileCache::MemoryCache) {
363 addToMemoryCache(spec, bytes, format);
364 }
365
366 /* inserts do not hit the texture cache -- this actually reduces overall
367 * cache hit rates because many tiles come too late to be useful
368 * and act as a poison */
369}
370
371QString QGeoFileTileCache::tileSpecToFilenameDefault(const QGeoTileSpec &spec, const QString &format, const QString &directory)
372{
373 QString filename = spec.plugin();
374 filename += QLatin1String("-");
375 filename += QString::number(spec.mapId());
376 filename += QLatin1String("-");
377 filename += QString::number(spec.zoom());
378 filename += QLatin1String("-");
379 filename += QString::number(spec.x());
380 filename += QLatin1String("-");
381 filename += QString::number(spec.y());
382
383 //Append version if real version number to ensure backwards compatibility and eviction of old tiles
384 if (spec.version() != -1) {
385 filename += QLatin1String("-");
386 filename += QString::number(spec.version());
387 }
388
389 filename += QLatin1String(".");
390 filename += format;
391
392 QDir dir = QDir(directory);
393
394 return dir.filePath(filename);
395}
396
397QGeoTileSpec QGeoFileTileCache::filenameToTileSpecDefault(const QString &filename)
398{
399 QGeoTileSpec emptySpec;
400
401 const QStringList parts = filename.split(QLatin1Char('.'));
402
403 if (parts.length() != 2)
404 return emptySpec;
405
406 const QString name = parts.at(0);
407 const QStringList fields = name.split(QLatin1Char('-'));
408
409 qsizetype length = fields.length();
410 if (length != 5 && length != 6)
411 return emptySpec;
412
413 QList<int> numbers;
414
415 bool ok = false;
416 for (qsizetype i = 1; i < length; ++i) {
417 ok = false;
418 int value = fields.at(i).toInt(&ok);
419 if (!ok)
420 return emptySpec;
421 numbers.append(value);
422 }
423
424 //File name without version, append default
425 if (numbers.length() < 5)
426 numbers.append(-1);
427
428 return QGeoTileSpec(fields.at(0),
429 numbers.at(0),
430 numbers.at(1),
431 numbers.at(2),
432 numbers.at(3),
433 numbers.at(4));
434}
435
436void QGeoFileTileCache::evictFromDiskCache(QGeoCachedTileDisk *td)
437{
438 QFile::remove(td->filename);
439}
440
441void QGeoFileTileCache::evictFromMemoryCache(QGeoCachedTileMemory * /* tm */)
442{
443}
444
445QSharedPointer<QGeoCachedTileDisk> QGeoFileTileCache::addToDiskCache(const QGeoTileSpec &spec, const QString &filename)
446{
447 QSharedPointer<QGeoCachedTileDisk> td(new QGeoCachedTileDisk);
448 td->spec = spec;
449 td->filename = filename;
450 td->cache = this;
451
452 int cost = 1;
453 if (costStrategyDisk_ == ByteSize) {
454 QFileInfo fi(filename);
455 cost = fi.size();
456 }
457 diskCache_.insert(spec, td, cost);
458 return td;
459}
460
461bool QGeoFileTileCache::addToDiskCache(const QGeoTileSpec &spec, const QString &filename, const QByteArray &bytes)
462{
463 int cost = 1;
464 if (costStrategyDisk_ == ByteSize)
465 cost = bytes.size();
466
467 if (cost > diskCache_.maxCost())
468 return false; // insert() would fail anyway, so don't even try
469
470 QSaveFile file(filename);
471 if (!file.open(QIODevice::WriteOnly))
472 return false;
473 file.write(bytes);
474 if (!file.commit())
475 return false;
476
477 QSharedPointer<QGeoCachedTileDisk> td(new QGeoCachedTileDisk);
478 td->spec = spec;
479 td->filename = filename;
480 td->cache = this;
481
482 [[maybe_unused]] const bool inserted = diskCache_.insert(spec, td, cost);
483 Q_ASSERT(inserted);
484
485 return true;
486}
487
488void QGeoFileTileCache::addToMemoryCache(const QGeoTileSpec &spec, const QByteArray &bytes, const QString &format)
489{
490 if (isTileBogus(bytes))
491 return;
492
493 QSharedPointer<QGeoCachedTileMemory> tm(new QGeoCachedTileMemory);
494 tm->spec = spec;
495 tm->cache = this;
496 tm->bytes = bytes;
497 tm->format = format;
498
499 int cost = 1;
500 if (costStrategyMemory_ == ByteSize)
501 cost = bytes.size();
502 memoryCache_.insert(spec, tm, cost);
503}
504
505QSharedPointer<QGeoTileTexture> QGeoFileTileCache::addToTextureCache(const QGeoTileSpec &spec, const QImage &image)
506{
507 QSharedPointer<QGeoTileTexture> tt(new QGeoTileTexture);
508 tt->spec = spec;
509 tt->image = image;
510
511 int cost = 1;
512 if (costStrategyTexture_ == ByteSize)
513 cost = image.width() * image.height() * image.depth() / 8;
514 textureCache_.insert(spec, tt, cost);
515
516 return tt;
517}
518
519QSharedPointer<QGeoTileTexture> QGeoFileTileCache::getFromMemory(const QGeoTileSpec &spec)
520{
521 QSharedPointer<QGeoTileTexture> tt = textureCache_.object(spec);
522 if (tt)
523 return tt;
524
525 QSharedPointer<QGeoCachedTileMemory> tm = memoryCache_.object(spec);
526 if (tm) {
527 QImage image;
528 if (!image.loadFromData(tm->bytes)) {
529 handleError(spec, QLatin1String("Problem with tile image"));
530 return QSharedPointer<QGeoTileTexture>();
531 }
532 QSharedPointer<QGeoTileTexture> tt = addToTextureCache(spec, image);
533 if (tt)
534 return tt;
535 }
536 return QSharedPointer<QGeoTileTexture>();
537}
538
539QSharedPointer<QGeoTileTexture> QGeoFileTileCache::getFromDisk(const QGeoTileSpec &spec)
540{
541 QSharedPointer<QGeoCachedTileDisk> td = diskCache_.object(spec);
542 if (td) {
543 const QString format = QFileInfo(td->filename).suffix();
544 QFile file(td->filename);
545 if (!file.open(QIODevice::ReadOnly)) {
546 handleError(spec, "Cannot open file %1: %2"_L1.arg(file.fileName(), file.errorString()));
547 return nullptr;
548 }
549 QByteArray bytes = file.readAll();
550 file.close();
551
552 QImage image;
553 // Some tiles from the servers could be valid images but the tile fetcher
554 // might be able to recognize them as tiles that should not be shown.
555 // If that's the case, the tile fetcher should write "NoRetry" inside the file.
556 if (isTileBogus(bytes)) {
557 QSharedPointer<QGeoTileTexture> tt(new QGeoTileTexture);
558 tt->spec = spec;
559 tt->image = image;
560 return tt;
561 }
562
563 // This is a truly invalid image. The fetcher should try again.
564 if (!image.loadFromData(bytes)) {
565 handleError(spec, QLatin1String("Problem with tile image"));
566 return QSharedPointer<QGeoTileTexture>();
567 }
568
569 // Converting it here, instead of in each QSGTexture::bind()
570 if (image.format() != QImage::Format_RGB32 && image.format() != QImage::Format_ARGB32_Premultiplied)
571 image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
572
573 addToMemoryCache(spec, bytes, format);
574 QSharedPointer<QGeoTileTexture> tt = addToTextureCache(td->spec, image);
575 if (tt)
576 return tt;
577 }
578
579 return QSharedPointer<QGeoTileTexture>();
580}
581
582bool QGeoFileTileCache::isTileBogus(const QByteArray &bytes) const
583{
584 if (bytes.size() == 7 && bytes == QByteArrayLiteral("NoRetry"))
585 return true;
586 return false;
587}
588
589QString QGeoFileTileCache::tileSpecToFilename(const QGeoTileSpec &spec, const QString &format, const QString &directory) const
590{
591 return tileSpecToFilenameDefault(spec, format, directory);
592}
593
594QGeoTileSpec QGeoFileTileCache::filenameToTileSpec(const QString &filename) const
595{
596 return filenameToTileSpecDefault(filename);
597}
598
599QString QGeoFileTileCache::directory() const
600{
601 return directory_;
602}
603
604QT_END_NAMESPACE
QGeoFileTileCache * cache
QGeoFileTileCache * cache
Combined button and popup list for selecting options.