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

source code of qtlocation/src/location/maps/qgeofiletilecache.cpp