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 const auto entries = dir.entryList();
279 for (const QString &dirFile : entries) {
280 dir.remove(dirFile);
281 }
282}
283
284void QGeoFileTileCache::clearMapId(const int mapId)
285{
286 {
287 const auto keys = diskCache_.keys();
288 for (const QGeoTileSpec &k : keys)
289 if (k.mapId() == mapId)
290 diskCache_.remove(k, true);
291 }
292 {
293 const auto keys = memoryCache_.keys();
294 for (const QGeoTileSpec &k : keys)
295 if (k.mapId() == mapId)
296 memoryCache_.remove(k);
297 }
298 {
299 const auto keys = textureCache_.keys();
300 for (const QGeoTileSpec &k : keys)
301 if (k.mapId() == mapId)
302 textureCache_.remove(k);
303 }
304
305 // TODO: It seems the cache leaves residues, like some tiles do not get picked up.
306 // After the above calls, files that shouldnt be left behind are still on disk.
307 // Do an additional pass and make sure what has to be deleted gets deleted.
308 QDir dir(directory_);
309 QStringList formats;
310 formats << QLatin1String("*.*");
311 const QStringList files = dir.entryList(formats, QDir::Files);
312 qWarning() << "Old tile data detected. Cache eviction left out "<< files.size() << "tiles";
313 for (const QString &tileFileName : files) {
314 QGeoTileSpec spec = filenameToTileSpec(tileFileName);
315 if (spec.mapId() != mapId)
316 continue;
317 QFile::remove(dir.filePath(tileFileName));
318 }
319}
320
321void QGeoFileTileCache::setCostStrategyDisk(QAbstractGeoTileCache::CostStrategy costStrategy)
322{
323 costStrategyDisk_ = costStrategy;
324}
325
326QAbstractGeoTileCache::CostStrategy QGeoFileTileCache::costStrategyDisk() const
327{
328 return costStrategyDisk_;
329}
330
331void QGeoFileTileCache::setCostStrategyMemory(QAbstractGeoTileCache::CostStrategy costStrategy)
332{
333 costStrategyMemory_ = costStrategy;
334}
335
336QAbstractGeoTileCache::CostStrategy QGeoFileTileCache::costStrategyMemory() const
337{
338 return costStrategyMemory_;
339}
340
341void QGeoFileTileCache::setCostStrategyTexture(QAbstractGeoTileCache::CostStrategy costStrategy)
342{
343 costStrategyTexture_ = costStrategy;
344}
345
346QAbstractGeoTileCache::CostStrategy QGeoFileTileCache::costStrategyTexture() const
347{
348 return costStrategyTexture_;
349}
350
351QSharedPointer<QGeoTileTexture> QGeoFileTileCache::get(const QGeoTileSpec &spec)
352{
353 QSharedPointer<QGeoTileTexture> tt = getFromMemory(spec);
354 if (tt)
355 return tt;
356 return getFromDisk(spec);
357}
358
359void QGeoFileTileCache::insert(const QGeoTileSpec &spec,
360 const QByteArray &bytes,
361 const QString &format,
362 QAbstractGeoTileCache::CacheAreas areas)
363{
364 if (bytes.isEmpty())
365 return;
366
367 if (areas & QAbstractGeoTileCache::DiskCache) {
368 QString filename = tileSpecToFilename(spec, format, directory_);
369 addToDiskCache(spec, filename, bytes);
370 }
371
372 if (areas & QAbstractGeoTileCache::MemoryCache) {
373 addToMemoryCache(spec, bytes, format);
374 }
375
376 /* inserts do not hit the texture cache -- this actually reduces overall
377 * cache hit rates because many tiles come too late to be useful
378 * and act as a poison */
379}
380
381QString QGeoFileTileCache::tileSpecToFilenameDefault(const QGeoTileSpec &spec, const QString &format, const QString &directory)
382{
383 QString filename = spec.plugin();
384 filename += QLatin1String("-");
385 filename += QString::number(spec.mapId());
386 filename += QLatin1String("-");
387 filename += QString::number(spec.zoom());
388 filename += QLatin1String("-");
389 filename += QString::number(spec.x());
390 filename += QLatin1String("-");
391 filename += QString::number(spec.y());
392
393 //Append version if real version number to ensure backwards compatibility and eviction of old tiles
394 if (spec.version() != -1) {
395 filename += QLatin1String("-");
396 filename += QString::number(spec.version());
397 }
398
399 filename += QLatin1String(".");
400 filename += format;
401
402 QDir dir = QDir(directory);
403
404 return dir.filePath(filename);
405}
406
407QGeoTileSpec QGeoFileTileCache::filenameToTileSpecDefault(const QString &filename)
408{
409 QGeoTileSpec emptySpec;
410
411 const QStringList parts = filename.split(QLatin1Char('.'));
412
413 if (parts.length() != 2)
414 return emptySpec;
415
416 const QString name = parts.at(0);
417 const QStringList fields = name.split(QLatin1Char('-'));
418
419 qsizetype length = fields.length();
420 if (length != 5 && length != 6)
421 return emptySpec;
422
423 QList<int> numbers;
424
425 bool ok = false;
426 for (qsizetype i = 1; i < length; ++i) {
427 ok = false;
428 int value = fields.at(i).toInt(&ok);
429 if (!ok)
430 return emptySpec;
431 numbers.append(value);
432 }
433
434 //File name without version, append default
435 if (numbers.length() < 5)
436 numbers.append(-1);
437
438 return QGeoTileSpec(fields.at(0),
439 numbers.at(0),
440 numbers.at(1),
441 numbers.at(2),
442 numbers.at(3),
443 numbers.at(4));
444}
445
446void QGeoFileTileCache::evictFromDiskCache(QGeoCachedTileDisk *td)
447{
448 QFile::remove(td->filename);
449}
450
451void QGeoFileTileCache::evictFromMemoryCache(QGeoCachedTileMemory * /* tm */)
452{
453}
454
455QSharedPointer<QGeoCachedTileDisk> QGeoFileTileCache::addToDiskCache(const QGeoTileSpec &spec, const QString &filename)
456{
457 QSharedPointer<QGeoCachedTileDisk> td(new QGeoCachedTileDisk);
458 td->spec = spec;
459 td->filename = filename;
460 td->cache = this;
461
462 int cost = 1;
463 if (costStrategyDisk_ == ByteSize) {
464 QFileInfo fi(filename);
465 cost = fi.size();
466 }
467 diskCache_.insert(spec, td, cost);
468 return td;
469}
470
471bool QGeoFileTileCache::addToDiskCache(const QGeoTileSpec &spec, const QString &filename, const QByteArray &bytes)
472{
473 int cost = 1;
474 if (costStrategyDisk_ == ByteSize)
475 cost = bytes.size();
476
477 if (cost > diskCache_.maxCost())
478 return false; // insert() would fail anyway, so don't even try
479
480 QSaveFile file(filename);
481 if (!file.open(QIODevice::WriteOnly))
482 return false;
483 file.write(bytes);
484 if (!file.commit())
485 return false;
486
487 QSharedPointer<QGeoCachedTileDisk> td(new QGeoCachedTileDisk);
488 td->spec = spec;
489 td->filename = filename;
490 td->cache = this;
491
492 [[maybe_unused]] const bool inserted = diskCache_.insert(spec, td, cost);
493 Q_ASSERT(inserted);
494
495 return true;
496}
497
498void QGeoFileTileCache::addToMemoryCache(const QGeoTileSpec &spec, const QByteArray &bytes, const QString &format)
499{
500 if (isTileBogus(bytes))
501 return;
502
503 QSharedPointer<QGeoCachedTileMemory> tm(new QGeoCachedTileMemory);
504 tm->spec = spec;
505 tm->cache = this;
506 tm->bytes = bytes;
507 tm->format = format;
508
509 int cost = 1;
510 if (costStrategyMemory_ == ByteSize)
511 cost = bytes.size();
512 memoryCache_.insert(spec, tm, cost);
513}
514
515QSharedPointer<QGeoTileTexture> QGeoFileTileCache::addToTextureCache(const QGeoTileSpec &spec, const QImage &image)
516{
517 QSharedPointer<QGeoTileTexture> tt(new QGeoTileTexture);
518 tt->spec = spec;
519 tt->image = image;
520
521 int cost = 1;
522 if (costStrategyTexture_ == ByteSize)
523 cost = image.width() * image.height() * image.depth() / 8;
524 textureCache_.insert(spec, tt, cost);
525
526 return tt;
527}
528
529QSharedPointer<QGeoTileTexture> QGeoFileTileCache::getFromMemory(const QGeoTileSpec &spec)
530{
531 QSharedPointer<QGeoTileTexture> tt = textureCache_.object(spec);
532 if (tt)
533 return tt;
534
535 QSharedPointer<QGeoCachedTileMemory> tm = memoryCache_.object(spec);
536 if (tm) {
537 QImage image;
538 if (!image.loadFromData(tm->bytes)) {
539 handleError(spec, QLatin1String("Problem with tile image"));
540 return QSharedPointer<QGeoTileTexture>();
541 }
542 QSharedPointer<QGeoTileTexture> tt = addToTextureCache(spec, image);
543 if (tt)
544 return tt;
545 }
546 return QSharedPointer<QGeoTileTexture>();
547}
548
549QSharedPointer<QGeoTileTexture> QGeoFileTileCache::getFromDisk(const QGeoTileSpec &spec)
550{
551 QSharedPointer<QGeoCachedTileDisk> td = diskCache_.object(spec);
552 if (td) {
553 const QString format = QFileInfo(td->filename).suffix();
554 QFile file(td->filename);
555 if (!file.open(QIODevice::ReadOnly)) {
556 handleError(spec, "Cannot open file %1: %2"_L1.arg(file.fileName(), file.errorString()));
557 return nullptr;
558 }
559 QByteArray bytes = file.readAll();
560 file.close();
561
562 QImage image;
563 // Some tiles from the servers could be valid images but the tile fetcher
564 // might be able to recognize them as tiles that should not be shown.
565 // If that's the case, the tile fetcher should write "NoRetry" inside the file.
566 if (isTileBogus(bytes)) {
567 QSharedPointer<QGeoTileTexture> tt(new QGeoTileTexture);
568 tt->spec = spec;
569 tt->image = image;
570 return tt;
571 }
572
573 // This is a truly invalid image. The fetcher should try again.
574 if (!image.loadFromData(bytes)) {
575 handleError(spec, QLatin1String("Problem with tile image"));
576 return QSharedPointer<QGeoTileTexture>();
577 }
578
579 // Converting it here, instead of in each QSGTexture::bind()
580 if (image.format() != QImage::Format_RGB32 && image.format() != QImage::Format_ARGB32_Premultiplied)
581 image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
582
583 addToMemoryCache(spec, bytes, format);
584 QSharedPointer<QGeoTileTexture> tt = addToTextureCache(td->spec, image);
585 if (tt)
586 return tt;
587 }
588
589 return QSharedPointer<QGeoTileTexture>();
590}
591
592bool QGeoFileTileCache::isTileBogus(const QByteArray &bytes) const
593{
594 if (bytes.size() == 7 && bytes == QByteArrayLiteral("NoRetry"))
595 return true;
596 return false;
597}
598
599QString QGeoFileTileCache::tileSpecToFilename(const QGeoTileSpec &spec, const QString &format, const QString &directory) const
600{
601 return tileSpecToFilenameDefault(spec, format, directory);
602}
603
604QGeoTileSpec QGeoFileTileCache::filenameToTileSpec(const QString &filename) const
605{
606 return filenameToTileSpecDefault(filename);
607}
608
609QString QGeoFileTileCache::directory() const
610{
611 return directory_;
612}
613
614QT_END_NAMESPACE
QGeoFileTileCache * cache
QGeoFileTileCache * cache
Combined button and popup list for selecting options.