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