55b740b4cbe8e4eefef6ae70090a96fb18d02464
[situare] / src / map / mapfetcher.cpp
1 /*
2    Situare - A location system for Facebook
3    Copyright (C) 2010  Ixonos Plc. Authors:
4
5        Jussi Laitinen - jussi.laitinen@ixonos.com
6        Sami Rämö - sami.ramo@ixonos.com
7
8    Situare is free software; you can redistribute it and/or
9    modify it under the terms of the GNU General Public License
10    version 2 as published by the Free Software Foundation.
11
12    Situare is distributed in the hope that it will be useful,
13    but WITHOUT ANY WARRANTY; without even the implied warranty of
14    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15    GNU General Public License for more details.
16
17    You should have received a copy of the GNU General Public License
18    along with Situare; if not, write to the Free Software
19    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
20    USA.
21 */
22
23 #include <QNetworkAccessManager>
24 #include <QNetworkRequest>
25 #include <QNetworkReply>
26 #include <QUrl>
27 #include <QDebug>
28 #include <QPixmap>
29 #include <QNetworkDiskCache>
30 #include <QDesktopServices>
31
32 #include "mapfetcher.h"
33 #include "mapcommon.h"
34 #include "common.h"
35 #include "network/networkaccessmanager.h"
36
37 const int MAX_PARALLEL_DOWNLOADS = 2; ///< Max simultaneous parallel downloads
38 const int NOT_FOUND = -1; ///< Return value if matching request is not found from the list
39
40 MapFetcher::MapFetcher(NetworkAccessManager *manager, QObject *parent)
41     : QObject(parent)
42     , m_pendingRequestsSize(0)
43     , m_fetchMapImagesTimerRunning(false)
44     , m_manager(manager)
45 {
46     qDebug() << __PRETTY_FUNCTION__;
47
48     QNetworkDiskCache *diskCache = new QNetworkDiskCache(this);
49     diskCache->setCacheDirectory(QDesktopServices::storageLocation(
50             QDesktopServices::CacheLocation));
51     m_manager->setCache(diskCache);
52
53     connect(m_manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(
54             downloadFinished(QNetworkReply*)));
55 }
56
57 QUrl MapFetcher::buildURL(int zoomLevel, QPoint tileNumbers)
58 {
59     qDebug() << __PRETTY_FUNCTION__;
60
61     /**
62     * @brief Map server string for building actual URL
63     *
64     * %1 zoom level
65     * %2 x index
66     * %3 y index
67     *
68     * NOTE: If the URL is changed, then the parseURL method must be changed to match
69     *       the new URL structure
70     *
71     * @var MAP_SERVER_URL
72     */
73     const QString MAP_SERVER_URL = QString("http://tile.openstreetmap.org/mapnik/%1/%2/%3.png");
74
75     return QString(MAP_SERVER_URL)
76             .arg(zoomLevel).arg(tileNumbers.x()).arg(tileNumbers.y());
77 }
78
79 void MapFetcher::checkNextRequestFromCache()
80 {
81     qDebug() << __PRETTY_FUNCTION__;
82
83     int i = newestRequestIndex(false);
84
85     if (i != NOT_FOUND) {
86         QUrl url = m_pendingRequests[i].url;
87         if (!url.isEmpty() && url.isValid()) {
88             if (loadImageFromCache(url)) {
89                 // was found, remove from the list
90                 m_pendingRequests.removeAt(i);
91             }
92             else {
93                 // didn't found from cache so mark cache checked and leave to queue
94                 m_pendingRequests[i].cacheChecked = true;
95
96                 if (m_currentDownloads.size() < MAX_PARALLEL_DOWNLOADS)
97                     startNextDownload();
98             }
99         }
100     }
101
102     // schedule checking of the next request if the list is not empty
103     if (newestRequestIndex(false) != NOT_FOUND)
104         QTimer::singleShot(0, this, SLOT(checkNextRequestFromCache()));
105     else
106         m_fetchMapImagesTimerRunning = false;
107 }
108
109 void MapFetcher::downloadFinished(QNetworkReply *reply)
110 {
111     qDebug() << __PRETTY_FUNCTION__;
112
113     if (m_currentDownloads.contains(reply)) {
114
115         if (reply->error() == QNetworkReply::NoError) {
116             QImage image;
117             QUrl url = reply->url();
118
119             if (!image.load(reply, 0))
120                 image = QImage();
121
122             int zoomLevel;
123             int x;
124             int y;
125             parseURL(url, &zoomLevel, &x, &y);
126
127             emit mapImageReceived(zoomLevel, x, y, QPixmap::fromImage(image));
128         }
129         else {
130             emit error(DOWNLOAD_FAILED);
131         }
132
133         m_currentDownloads.removeAll(reply);
134         reply->deleteLater();
135         startNextDownload();
136     }
137 }
138
139 void MapFetcher::enqueueFetchMapImage(int zoomLevel, int x, int y)
140 {
141     qDebug() << __PRETTY_FUNCTION__;
142
143     QUrl url = buildURL(zoomLevel, QPoint(x, y));
144
145     // ignore request if it is already downloading
146     foreach (QNetworkReply *reply, m_currentDownloads) {
147         if (reply->url() == url)
148             return;
149     }
150
151     // check if new request is already in the list and move it to the begin of the list...
152     bool found = false;
153     for (int i = 0; i < m_pendingRequests.size(); i++) {
154         if (m_pendingRequests[i].url == url) {
155             m_pendingRequests.move(i, 0);
156             found = true;
157             break;
158         }
159     }
160     // ...or add new request to the begining of the list
161     if (!found) {
162         MapTileRequest request(url);
163         m_pendingRequests.prepend(request);
164     }
165
166     limitPendingRequestsListSize();
167
168     if (!m_fetchMapImagesTimerRunning) {
169         m_fetchMapImagesTimerRunning = true;
170         QTimer::singleShot(0, this, SLOT(checkNextRequestFromCache()));
171     }
172 }
173
174 void MapFetcher::limitPendingRequestsListSize()
175 {
176     qDebug() << __PRETTY_FUNCTION__;
177
178     while (m_pendingRequests.size() > m_pendingRequestsSize) {
179         m_pendingRequests.removeLast();
180     }
181 }
182
183 bool MapFetcher::loadImageFromCache(const QUrl &url)
184 {
185     qDebug() << __PRETTY_FUNCTION__;
186
187     bool imageFound = false;
188
189     QAbstractNetworkCache *cache = m_manager->cache();
190
191     if (cache) {
192
193         int zoomLevel;
194         int x;
195         int y;
196         parseURL(url, &zoomLevel, &x, &y);
197         int originalZoomLevel = zoomLevel;
198
199         // try to fetch requested and upper level images until found or MAX_UPPER_LEVELS
200         // limit is reached
201         const int MAX_UPPER_LEVELS = 4;
202         do {
203             QIODevice *cacheImage = cache->data(buildURL(zoomLevel, QPoint(x, y)));
204             if (cacheImage) {
205                 QPixmap pixmap;
206                 if (pixmap.loadFromData(cacheImage->readAll())) {
207                     imageFound = true;
208                     emit mapImageReceived(zoomLevel, x, y, pixmap);
209                 }
210
211                 delete cacheImage;
212             }
213         } while (!imageFound
214                  && (originalZoomLevel - zoomLevel) < MAX_UPPER_LEVELS
215                  && translateIndexesToUpperLevel(zoomLevel, x, y));
216
217         // check expiration if image was found from requested level
218         if (imageFound && (originalZoomLevel == zoomLevel)) {
219             // check if image is expired
220             QNetworkCacheMetaData metaData = cache->metaData(url);
221             if ((metaData.expirationDate().isValid()) && (url.isValid())) {
222
223                 if (metaData.expirationDate() < QDateTime::currentDateTime()) {
224                     cache->remove(url);
225                     return false;
226                 }
227             }
228         }
229
230         // if image was found, but from upper level, return false
231         if (imageFound && (originalZoomLevel != zoomLevel))
232             return false;
233     }
234
235     return imageFound;
236 }
237
238 bool MapFetcher::translateIndexesToUpperLevel(int &zoomLevel, int &x, int &y)
239 {
240     qDebug() << __PRETTY_FUNCTION__;
241
242     if (zoomLevel > MIN_MAP_ZOOM_LEVEL) {
243         zoomLevel--;
244         x /= 2;
245         y /= 2;
246
247         return true;
248     }
249
250     return false;
251 }
252
253 int MapFetcher::newestRequestIndex(bool cacheChecked)
254 {
255     qDebug() << __PRETTY_FUNCTION__;
256
257     for (int i = 0; i < m_pendingRequests.size(); i++) {
258         if (m_pendingRequests[i].cacheChecked == cacheChecked) {
259             return i;
260         }
261     }
262
263     return NOT_FOUND;
264 }
265
266 void MapFetcher::parseURL(const QUrl &url, int *zoom, int *x, int *y)
267 {
268     qDebug() << __PRETTY_FUNCTION__;
269
270     QString path = url.path();
271     QStringList pathParts = path.split("/", QString::SkipEmptyParts);
272
273     int size = pathParts.size();
274
275     // Example URL: "http://tile.openstreetmap.org/mapnik/14/9354/4263.png"
276     const int MIN_PATH_SPLITTED_PARTS = 4;
277     const int ZOOM_INDEX = size - 3;
278     const int X_INDEX = size - 2;
279     const int Y_INDEX = size - 1;
280     const int FILE_EXTENSION_LENGTH = 4;
281
282     if (size >= MIN_PATH_SPLITTED_PARTS) {
283         *zoom = (pathParts.at(ZOOM_INDEX)).toInt();
284         *x = (pathParts.at(X_INDEX)).toInt();
285         QString yString = pathParts.at(Y_INDEX);
286         yString.chop(FILE_EXTENSION_LENGTH);
287         *y = yString.toInt();
288     }
289 }
290
291 void MapFetcher::setDownloadQueueSize(int size)
292 {
293     qDebug() << __PRETTY_FUNCTION__ << "size:" << size;
294
295     m_pendingRequestsSize = size;
296     limitPendingRequestsListSize();
297 }
298
299 void MapFetcher::startNextDownload()
300 {
301     qDebug() << __PRETTY_FUNCTION__;
302
303     int i = newestRequestIndex(true);
304
305     if (i != NOT_FOUND) {
306         QUrl url = m_pendingRequests.takeAt(i).url;
307
308         QNetworkRequest request(url);
309         request.setRawHeader("User-Agent", "Situare");
310         QNetworkReply *reply = m_manager->get(request);
311
312         m_currentDownloads.append(reply);
313     }
314 }