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 
48 Q_DECLARE_METATYPE(QList<QGeoTileSpec>)
49 Q_DECLARE_METATYPE(QSet<QGeoTileSpec>)
50 
51 QT_BEGIN_NAMESPACE
52 
53 class QGeoCachedTileMemory
54 {
55 public:
~QGeoCachedTileMemory()56     ~QGeoCachedTileMemory()
57     {
58         if (cache)
59             cache->evictFromMemoryCache(this);
60     }
61 
62     QGeoTileSpec spec;
63     QGeoFileTileCache *cache;
64     QByteArray bytes;
65     QString format;
66 };
67 
aboutToBeRemoved(const QGeoTileSpec & key,QSharedPointer<QGeoCachedTileDisk> obj)68 void 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 
aboutToBeEvicted(const QGeoTileSpec & key,QSharedPointer<QGeoCachedTileDisk> obj)75 void 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 
~QGeoCachedTileDisk()82 QGeoCachedTileDisk::~QGeoCachedTileDisk()
83 {
84     if (cache)
85         cache->evictFromDiskCache(this);
86 }
87 
QGeoFileTileCache(const QString & directory,QObject * parent)88 QGeoFileTileCache::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 
init()96 void 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(QDir::Files);
106         foreach (const QString& file, oldCacheFiles)
107             baseDir.remove(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(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 
loadTiles()150 void QGeoFileTileCache::loadTiles()
151 {
152     QStringList formats;
153     formats << QLatin1String("*.*");
154 
155     QDir dir(directory_);
156     QStringList files = dir.entryList(formats, 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(files.at(i));
201         if (spec.zoom() == -1)
202             continue;
203         QString filename = dir.filePath(files.at(i));
204         addToDiskCache(spec, filename);
205     }
206 }
207 
~QGeoFileTileCache()208 QGeoFileTileCache::~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 
printStats()236 void QGeoFileTileCache::printStats()
237 {
238     textureCache_.printStats();
239     memoryCache_.printStats();
240     diskCache_.printStats();
241 }
242 
setMaxDiskUsage(int diskUsage)243 void QGeoFileTileCache::setMaxDiskUsage(int diskUsage)
244 {
245     diskCache_.setMaxCost(diskUsage);
246     isDiskCostSet_ = true;
247 }
248 
maxDiskUsage() const249 int QGeoFileTileCache::maxDiskUsage() const
250 {
251     return diskCache_.maxCost();
252 }
253 
diskUsage() const254 int QGeoFileTileCache::diskUsage() const
255 {
256     return diskCache_.totalCost();
257 }
258 
setMaxMemoryUsage(int memoryUsage)259 void QGeoFileTileCache::setMaxMemoryUsage(int memoryUsage)
260 {
261     memoryCache_.setMaxCost(memoryUsage);
262     isMemoryCostSet_ = true;
263 }
264 
maxMemoryUsage() const265 int QGeoFileTileCache::maxMemoryUsage() const
266 {
267     return memoryCache_.maxCost();
268 }
269 
memoryUsage() const270 int QGeoFileTileCache::memoryUsage() const
271 {
272     return memoryCache_.totalCost();
273 }
274 
setExtraTextureUsage(int textureUsage)275 void QGeoFileTileCache::setExtraTextureUsage(int textureUsage)
276 {
277     extraTextureUsage_ = textureUsage;
278     textureCache_.setMaxCost(minTextureUsage_ + extraTextureUsage_);
279     isTextureCostSet_ = true;
280 }
281 
setMinTextureUsage(int textureUsage)282 void QGeoFileTileCache::setMinTextureUsage(int textureUsage)
283 {
284     minTextureUsage_ = textureUsage;
285     textureCache_.setMaxCost(minTextureUsage_ + extraTextureUsage_);
286 }
287 
maxTextureUsage() const288 int QGeoFileTileCache::maxTextureUsage() const
289 {
290     return textureCache_.maxCost();
291 }
292 
minTextureUsage() const293 int QGeoFileTileCache::minTextureUsage() const
294 {
295     return minTextureUsage_;
296 }
297 
298 
textureUsage() const299 int QGeoFileTileCache::textureUsage() const
300 {
301     return textureCache_.totalCost();
302 }
303 
clearAll()304 void 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(dirFile);
314     }
315 }
316 
clearMapId(const int mapId)317 void QGeoFileTileCache::clearMapId(const int mapId)
318 {
319     for (const QGeoTileSpec &k : diskCache_.keys())
320         if (k.mapId() == mapId)
321             diskCache_.remove(k, true);
322     for (const QGeoTileSpec &k : memoryCache_.keys())
323         if (k.mapId() == mapId)
324             memoryCache_.remove(k);
325     for (const QGeoTileSpec &k : textureCache_.keys())
326         if (k.mapId() == mapId)
327             textureCache_.remove(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(formats, 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(tileFileName);
339         if (spec.mapId() != mapId)
340             continue;
341         QFile::remove(dir.filePath(tileFileName));
342     }
343 }
344 
setCostStrategyDisk(QAbstractGeoTileCache::CostStrategy costStrategy)345 void QGeoFileTileCache::setCostStrategyDisk(QAbstractGeoTileCache::CostStrategy costStrategy)
346 {
347     costStrategyDisk_ = costStrategy;
348 }
349 
costStrategyDisk() const350 QAbstractGeoTileCache::CostStrategy QGeoFileTileCache::costStrategyDisk() const
351 {
352     return costStrategyDisk_;
353 }
354 
setCostStrategyMemory(QAbstractGeoTileCache::CostStrategy costStrategy)355 void QGeoFileTileCache::setCostStrategyMemory(QAbstractGeoTileCache::CostStrategy costStrategy)
356 {
357     costStrategyMemory_ = costStrategy;
358 }
359 
costStrategyMemory() const360 QAbstractGeoTileCache::CostStrategy QGeoFileTileCache::costStrategyMemory() const
361 {
362     return costStrategyMemory_;
363 }
364 
setCostStrategyTexture(QAbstractGeoTileCache::CostStrategy costStrategy)365 void QGeoFileTileCache::setCostStrategyTexture(QAbstractGeoTileCache::CostStrategy costStrategy)
366 {
367     costStrategyTexture_ = costStrategy;
368 }
369 
costStrategyTexture() const370 QAbstractGeoTileCache::CostStrategy QGeoFileTileCache::costStrategyTexture() const
371 {
372     return costStrategyTexture_;
373 }
374 
get(const QGeoTileSpec & spec)375 QSharedPointer<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 
insert(const QGeoTileSpec & spec,const QByteArray & bytes,const QString & format,QAbstractGeoTileCache::CacheAreas areas)383 void 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_);
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 
tileSpecToFilenameDefault(const QGeoTileSpec & spec,const QString & format,const QString & directory)405 QString 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);
429 }
430 
filenameToTileSpecDefault(const QString & filename)431 QGeoTileSpec QGeoFileTileCache::filenameToTileSpecDefault(const QString &filename)
432 {
433     QGeoTileSpec emptySpec;
434 
435     QStringList parts = filename.split('.');
436 
437     if (parts.length() != 2)
438         return emptySpec;
439 
440     QString name = parts.at(0);
441     QStringList fields = name.split('-');
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);
453         if (!ok)
454             return emptySpec;
455         numbers.append(value);
456     }
457 
458     //File name without version, append default
459     if (numbers.length() < 5)
460         numbers.append(-1);
461 
462     return QGeoTileSpec(fields.at(0),
463                     numbers.at(0),
464                     numbers.at(1),
465                     numbers.at(2),
466                     numbers.at(3),
467                     numbers.at(4));
468 }
469 
evictFromDiskCache(QGeoCachedTileDisk * td)470 void QGeoFileTileCache::evictFromDiskCache(QGeoCachedTileDisk *td)
471 {
472     QFile::remove(td->filename);
473 }
474 
evictFromMemoryCache(QGeoCachedTileMemory *)475 void QGeoFileTileCache::evictFromMemoryCache(QGeoCachedTileMemory * /* tm  */)
476 {
477 }
478 
addToDiskCache(const QGeoTileSpec & spec,const QString & filename)479 QSharedPointer<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(spec, td, cost);
492     return td;
493 }
494 
addToDiskCache(const QGeoTileSpec & spec,const QString & filename,const QByteArray & bytes)495 bool 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(spec, td, cost)) {
507         QFile file(filename);
508         file.open(QIODevice::WriteOnly);
509         file.write(bytes);
510         file.close();
511         return true;
512     }
513     return false;
514 }
515 
addToMemoryCache(const QGeoTileSpec & spec,const QByteArray & bytes,const QString & format)516 void 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(spec, tm, cost);
531 }
532 
addToTextureCache(const QGeoTileSpec & spec,const QImage & image)533 QSharedPointer<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(spec, tt, cost);
543 
544     return tt;
545 }
546 
getFromMemory(const QGeoTileSpec & spec)547 QSharedPointer<QGeoTileTexture> QGeoFileTileCache::getFromMemory(const QGeoTileSpec &spec)
548 {
549     QSharedPointer<QGeoTileTexture> tt = textureCache_.object(spec);
550     if (tt)
551         return tt;
552 
553     QSharedPointer<QGeoCachedTileMemory> tm = memoryCache_.object(spec);
554     if (tm) {
555         QImage image;
556         if (!image.loadFromData(tm->bytes)) {
557             handleError(spec, 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 
getFromDisk(const QGeoTileSpec & spec)567 QSharedPointer<QGeoTileTexture> QGeoFileTileCache::getFromDisk(const QGeoTileSpec &spec)
568 {
569     QSharedPointer<QGeoCachedTileDisk> td = diskCache_.object(spec);
570     if (td) {
571         const QString format = QFileInfo(td->filename).suffix();
572         QFile file(td->filename);
573         file.open(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(bytes)) {
590             handleError(spec, 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(QImage::Format_ARGB32_Premultiplied);
597 
598         addToMemoryCache(spec, bytes, format);
599         QSharedPointer<QGeoTileTexture> tt = addToTextureCache(td->spec, image);
600         if (tt)
601             return tt;
602     }
603 
604     return QSharedPointer<QGeoTileTexture>();
605 }
606 
isTileBogus(const QByteArray & bytes) const607 bool QGeoFileTileCache::isTileBogus(const QByteArray &bytes) const
608 {
609     if (bytes.size() == 7 && bytes == QByteArrayLiteral("NoRetry"))
610         return true;
611     return false;
612 }
613 
tileSpecToFilename(const QGeoTileSpec & spec,const QString & format,const QString & directory) const614 QString QGeoFileTileCache::tileSpecToFilename(const QGeoTileSpec &spec, const QString &format, const QString &directory) const
615 {
616     return tileSpecToFilenameDefault(spec, format, directory);
617 }
618 
filenameToTileSpec(const QString & filename) const619 QGeoTileSpec QGeoFileTileCache::filenameToTileSpec(const QString &filename) const
620 {
621     return filenameToTileSpecDefault(filename);
622 }
623 
directory() const624 QString QGeoFileTileCache::directory() const
625 {
626     return directory_;
627 }
628 
629 QT_END_NAMESPACE
630