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