1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 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 
37 #include "qgeofiletilecacheosm.h"
38 #include <QtLocation/private/qgeotilespec_p.h>
39 #include <QDir>
40 #include <QDirIterator>
41 #include <QPair>
42 #include <QDateTime>
43 #include <QtConcurrent>
44 #include <QThread>
45 
46 QT_BEGIN_NAMESPACE
47 
QGeoFileTileCacheOsm(const QVector<QGeoTileProviderOsm * > & providers,const QString & offlineDirectory,const QString & directory,QObject * parent)48 QGeoFileTileCacheOsm::QGeoFileTileCacheOsm(const QVector<QGeoTileProviderOsm *> &providers,
49                                            const QString &offlineDirectory,
50                                            const QString &directory,
51                                            QObject *parent)
52 :   QGeoFileTileCache(directory, parent), m_offlineDirectory(offlineDirectory), m_offlineData(false), m_providers(providers)
53 {
54     m_highDpi.resize(providers.size());
55     if (!offlineDirectory.isEmpty()) {
56         m_offlineDirectory = QDir(offlineDirectory);
57         if (m_offlineDirectory.exists())
58             m_offlineData = true;
59     }
60     for (int i = 0; i < providers.size(); i++) {
61         providers[i]->setParent(this);
62         m_highDpi[i] = providers[i]->isHighDpi();
63         connect(providers[i], &QGeoTileProviderOsm::resolutionFinished, this, &QGeoFileTileCacheOsm::onProviderResolutionFinished);
64         connect(providers[i], &QGeoTileProviderOsm::resolutionError, this, &QGeoFileTileCacheOsm::onProviderResolutionFinished);
65     }
66 }
67 
~QGeoFileTileCacheOsm()68 QGeoFileTileCacheOsm::~QGeoFileTileCacheOsm()
69 {
70 }
71 
get(const QGeoTileSpec & spec)72 QSharedPointer<QGeoTileTexture> QGeoFileTileCacheOsm::get(const QGeoTileSpec &spec)
73 {
74     QSharedPointer<QGeoTileTexture> tt = getFromMemory(spec);
75     if (tt)
76         return tt;
77     if ((tt = getFromOfflineStorage(spec)))
78         return tt;
79     return getFromDisk(spec);
80 }
81 
onProviderResolutionFinished(const QGeoTileProviderOsm * provider)82 void QGeoFileTileCacheOsm::onProviderResolutionFinished(const QGeoTileProviderOsm *provider)
83 {
84     clearObsoleteTiles(provider);
85     Q_UNUSED(provider);
86     for (int i = 0; i < m_providers.size(); i++) {
87         if (m_providers[i]->isHighDpi() != m_highDpi[i]) { // e.g., HiDpi was requested but only LoDpi is available
88             int mapId = m_providers[i]->mapType().mapId();
89             m_highDpi[i] = m_providers[i]->isHighDpi();
90 
91             // reload cache for mapId i
92             dropTiles(mapId);
93             loadTiles(mapId);
94 
95             // send signal to clear scene in all maps created through this provider that use the reloaded tiles
96             emit mapDataUpdated(mapId);
97         }
98     }
99 }
100 
101 // On resolution error the provider is removed.
102 // This happens ONLY if there is no enabled hardcoded fallback for the mapId.
103 // Hardcoded fallbacks also have a timestamp, that can get updated with Qt releases.
onProviderResolutionError(const QGeoTileProviderOsm * provider,QNetworkReply::NetworkError error)104 void QGeoFileTileCacheOsm::onProviderResolutionError(const QGeoTileProviderOsm *provider, QNetworkReply::NetworkError error)
105 {
106     Q_UNUSED(error);
107     clearObsoleteTiles(provider); // this still removes tiles who happen to be older than qgeotileproviderosm.cpp defaultTs
108 }
109 
110 // init() is always called before the provider resolution starts
init()111 void QGeoFileTileCacheOsm::init()
112 {
113     if (directory_.isEmpty())
114         directory_ = baseLocationCacheDirectory();
115     QDir::root().mkpath(directory_);
116 
117     // find max mapId
118     int max = 0;
119     for (auto p: m_providers)
120         if (p->mapType().mapId() > max)
121             max = p->mapType().mapId();
122     // Create a mapId to maxTimestamp LUT..
123     m_maxMapIdTimestamps.resize(max+1); // initializes to invalid QDateTime
124 
125     // .. by finding the newest file in each tileset (tileset = mapId).
126     QDir dir(directory_);
127     QStringList formats;
128     formats << QLatin1String("*.*");
129     QStringList files = dir.entryList(formats, QDir::Files);
130 
131     for (const QString &tileFileName : files) {
132         QGeoTileSpec spec = filenameToTileSpec(tileFileName);
133         if (spec.zoom() == -1)
134             continue;
135         QFileInfo fi(dir.filePath(tileFileName));
136         if (fi.lastModified() > m_maxMapIdTimestamps[spec.mapId()])
137             m_maxMapIdTimestamps[spec.mapId()] = fi.lastModified();
138     }
139 
140     // Base class ::init()
141     QGeoFileTileCache::init();
142 
143     for (QGeoTileProviderOsm * p: m_providers)
144         clearObsoleteTiles(p);
145 }
146 
getFromOfflineStorage(const QGeoTileSpec & spec)147 QSharedPointer<QGeoTileTexture> QGeoFileTileCacheOsm::getFromOfflineStorage(const QGeoTileSpec &spec)
148 {
149     if (!m_offlineData)
150         return QSharedPointer<QGeoTileTexture>();
151 
152     int providerId = spec.mapId() - 1;
153     if (providerId < 0 || providerId >= m_providers.size())
154         return QSharedPointer<QGeoTileTexture>();
155 
156     const QString fileName = tileSpecToFilename(spec, QStringLiteral("*"), providerId);
157     QStringList validTiles = m_offlineDirectory.entryList({fileName});
158     if (!validTiles.size())
159         return QSharedPointer<QGeoTileTexture>();
160 
161     QFile file(m_offlineDirectory.absoluteFilePath(validTiles.first()));
162     if (!file.open(QIODevice::ReadOnly))
163         return QSharedPointer<QGeoTileTexture>();
164     QByteArray bytes = file.readAll();
165     file.close();
166 
167     QImage image;
168     if (!image.loadFromData(bytes)) {
169         handleError(spec, QLatin1String("Problem with tile image"));
170         return QSharedPointer<QGeoTileTexture>(0);
171     }
172 
173     addToMemoryCache(spec, bytes, QString());
174     return addToTextureCache(spec, image);
175 }
176 
dropTiles(int mapId)177 void QGeoFileTileCacheOsm::dropTiles(int mapId)
178 {
179     QList<QGeoTileSpec> keys;
180     keys = textureCache_.keys();
181     for (const QGeoTileSpec &k : keys)
182         if (k.mapId() == mapId)
183             textureCache_.remove(k);
184 
185     keys = memoryCache_.keys();
186     for (const QGeoTileSpec &k : keys)
187         if (k.mapId() == mapId)
188             memoryCache_.remove(k);
189 
190     keys = diskCache_.keys();
191     for (const QGeoTileSpec &k : keys)
192         if (k.mapId() == mapId)
193             diskCache_.remove(k);
194 }
195 
loadTiles(int mapId)196 void QGeoFileTileCacheOsm::loadTiles(int mapId)
197 {
198     QStringList formats;
199     formats << QLatin1String("*.*");
200 
201     QDir dir(directory_);
202     QStringList files = dir.entryList(formats, QDir::Files);
203 
204     for (int i = 0; i < files.size(); ++i) {
205         QGeoTileSpec spec = filenameToTileSpec(files.at(i));
206         if (spec.zoom() == -1 || spec.mapId() != mapId)
207             continue;
208         QString filename = dir.filePath(files.at(i));
209         addToDiskCache(spec, filename);
210     }
211 }
212 
tileSpecToFilename(const QGeoTileSpec & spec,const QString & format,const QString & directory) const213 QString QGeoFileTileCacheOsm::tileSpecToFilename(const QGeoTileSpec &spec, const QString &format, const QString &directory) const
214 {
215     int providerId = spec.mapId() - 1;
216     if (providerId < 0 || providerId >= m_providers.size())
217         return QString();
218 
219     QDir dir = QDir(directory);
220     return dir.filePath(tileSpecToFilename(spec, format, providerId));
221 }
222 
tileSpecToFilename(const QGeoTileSpec & spec,const QString & format,int providerId) const223 QString QGeoFileTileCacheOsm::tileSpecToFilename(const QGeoTileSpec &spec, const QString &format, int providerId) const
224 {
225     QString filename = spec.plugin();
226     filename += QLatin1String("-");
227     filename += (m_providers[providerId]->isHighDpi()) ? QLatin1Char('h') : QLatin1Char('l');
228     filename += QLatin1String("-");
229     filename += QString::number(spec.mapId());
230     filename += QLatin1String("-");
231     filename += QString::number(spec.zoom());
232     filename += QLatin1String("-");
233     filename += QString::number(spec.x());
234     filename += QLatin1String("-");
235     filename += QString::number(spec.y());
236 
237     //Append version if real version number to ensure backwards compatibility and eviction of old tiles
238     if (spec.version() != -1) {
239         filename += QLatin1String("-");
240         filename += QString::number(spec.version());
241     }
242 
243     filename += QLatin1String(".");
244     filename += format;
245     return filename;
246 }
247 
filenameToTileSpec(const QString & filename) const248 QGeoTileSpec QGeoFileTileCacheOsm::filenameToTileSpec(const QString &filename) const
249 {
250     QGeoTileSpec emptySpec;
251 
252     QStringList parts = filename.split('.');
253 
254     if (parts.length() != 2)
255         return emptySpec;
256 
257     QString name = parts.at(0);
258     QStringList fields = name.split('-');
259 
260     int length = fields.length();
261     if (length != 6 && length != 7)
262         return emptySpec;
263 
264     QList<int> numbers;
265 
266     bool ok = false;
267     for (int i = 2; i < length; ++i) {
268         ok = false;
269         int value = fields.at(i).toInt(&ok);
270         if (!ok)
271             return emptySpec;
272         numbers.append(value);
273     }
274 
275     if (numbers.at(0) > m_providers.size())
276         return emptySpec;
277 
278     bool highDpi = m_providers[numbers.at(0) - 1]->isHighDpi();
279     if (highDpi && fields.at(1) != QLatin1Char('h'))
280         return emptySpec;
281     else if (!highDpi && fields.at(1) != QLatin1Char('l'))
282         return emptySpec;
283 
284     //File name without version, append default
285     if (numbers.length() < 5)
286         numbers.append(-1);
287 
288     return QGeoTileSpec(fields.at(0),
289                     numbers.at(0),
290                     numbers.at(1),
291                     numbers.at(2),
292                     numbers.at(3),
293                     numbers.at(4));
294 }
295 
clearObsoleteTiles(const QGeoTileProviderOsm * p)296 void QGeoFileTileCacheOsm::clearObsoleteTiles(const QGeoTileProviderOsm *p)
297 {
298     // process initialized providers, and connect the others
299 
300         if (p->isResolved()) {
301             if (m_maxMapIdTimestamps[p->mapType().mapId()].isValid() &&  // there are tiles in the cache
302                 p->timestamp() > m_maxMapIdTimestamps[p->mapType().mapId()]) { // and they are older than the provider
303                 qInfo() << "provider for " << p->mapType().name() << " timestamp: " << p->timestamp()
304                         << " -- data last modified: " << m_maxMapIdTimestamps[p->mapType().mapId()] << ". Clearing.";
305                 clearMapId(p->mapType().mapId());
306                 m_maxMapIdTimestamps[p->mapType().mapId()] = p->timestamp(); // don't do it again.
307             }
308         } else {
309             connect(p, &QGeoTileProviderOsm::resolutionFinished,
310                     this, &QGeoFileTileCacheOsm::onProviderResolutionFinished);
311 #if 0 // If resolution fails, better not try to remove anything. Beside, on error, resolutionFinished is also emitted.
312             connect(p, &QGeoTileProviderOsm::resolutionError,
313                     this, &QGeoFileTileCacheOsm::onProviderResolutionError);
314 #endif
315         }
316 }
317 
318 QT_END_NAMESPACE
319