Refactored to match server v2.0 changes.
[situare] / src / situareservice / situareservice.cpp
1 /*
2    Situare - A location system for Facebook
3    Copyright (C) 2010  Ixonos Plc. Authors:
4
5       Henri Lampela - henri.lampela@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 <qjson/parser.h>
24
25 #include <QDebug>
26 #include <QNetworkReply>
27 #include <QPixmap>
28 #include <QStringList>
29 #include <QtAlgorithms>
30 #include <QtGlobal>
31
32 #include "../error.h"
33 #include "network/networkaccessmanager.h"
34 #include "situarecommon.h"
35 #include "ui/avatarimage.h"
36
37 #include "situareservice.h"
38
39 SituareService::SituareService(QObject *parent)
40         : QObject(parent),
41         m_user(0)
42 {
43     qDebug() << __PRETTY_FUNCTION__;
44
45     m_networkManager = new NetworkAccessManager(this);
46     connect(m_networkManager, SIGNAL(finished(QNetworkReply*)),
47             this, SLOT(requestFinished(QNetworkReply*)), Qt::QueuedConnection);
48
49     m_imageFetcher = new ImageFetcher(new NetworkAccessManager(this), this);
50     connect(this, SIGNAL(fetchImage(QUrl)),
51             m_imageFetcher, SLOT(fetchImage(QUrl)));
52     connect(m_imageFetcher, SIGNAL(imageReceived(QUrl,QPixmap)),
53             this, SLOT(imageReceived(QUrl, QPixmap)));
54     connect(m_imageFetcher, SIGNAL(error(int, int)),
55             this, SIGNAL(error(int, int)));
56 }
57
58 SituareService::~SituareService()
59 {
60     qDebug() << __PRETTY_FUNCTION__;
61
62     if(m_user) {
63         delete m_user;
64         m_user = 0;
65     }
66
67     qDeleteAll(m_friendsList.begin(), m_friendsList.end());
68     m_friendsList.clear();
69 }
70
71 void SituareService::addProfileImages(const QList<QUrl> &imageUrlList)
72 {
73     qDebug() << __PRETTY_FUNCTION__;
74
75     foreach(QUrl url, imageUrlList) {
76         emit fetchImage(url);
77     }
78 }
79
80 void SituareService::appendAccessToken(QString &requestUrl)
81 {
82     qDebug() << __PRETTY_FUNCTION__;
83
84     requestUrl.append(m_session);
85 }
86
87 void SituareService::buildRequest(const QString &script, const QHash<QString, QString> &parameters)
88 {
89     qDebug() << __PRETTY_FUNCTION__;
90
91     const QString PARAMETER_KEY_API = "api";
92     const QString PARAMETER_VALUE_API = "2.0";
93
94     QString url = SITUARE_URL;
95     url.append(script);
96     url.append("?");
97
98     // append default api version parameter if not yet specified
99     if (!parameters.contains(PARAMETER_KEY_API))
100         url.append(PARAMETER_KEY_API + "=" + PARAMETER_VALUE_API + "&");
101
102     // append parameters
103     if (!parameters.isEmpty()) {
104         QHash<QString, QString>::const_iterator i = parameters.constBegin();
105         while (i != parameters.constEnd()) {
106             url.append(i.key());
107             url.append("=");
108             url.append(i.value());
109             url.append("&");
110             i++;
111         }
112     }
113
114     /// @todo BUG: Url parameter strings are not url escaped
115
116 //    qWarning() << __PRETTY_FUNCTION__ << "request url with parameters:" << url;
117
118     if (!m_session.isEmpty()) {
119         appendAccessToken(url);
120         sendRequest(url);
121     } else {
122         emit error(ErrorContext::SITUARE, SituareError::SESSION_EXPIRED);
123     }
124 }
125
126 void SituareService::clearUserData()
127 {
128     qDebug() << __PRETTY_FUNCTION__;
129
130     qDeleteAll(m_friendsList.begin(), m_friendsList.end());
131     m_friendsList.clear();
132
133     if(m_user) {
134         delete m_user;
135         m_user = 0;
136     }
137     emit userDataChanged(m_user, m_friendsList);
138 }
139
140 QString SituareService::degreesToString(double degrees)
141 {
142     qDebug() << __PRETTY_FUNCTION__;
143
144     // one scene pixel is about 5.4e-6 degrees, the integer part is max three digits and one
145     // additional digit is added for maximum precision
146     const int PRECISION = 10;
147
148     return QString::number(degrees, 'f', PRECISION);
149 }
150
151 void SituareService::fetchLocations()
152 {
153     qDebug() << __PRETTY_FUNCTION__;
154
155     QHash<QString, QString> parameters;
156     parameters.insert("extra_user_data", NORMAL_SIZE_PROFILE_IMAGE);
157
158     buildRequest(GET_LOCATIONS, parameters);
159 }
160
161 SituareService::RequestName SituareService::getRequestName(const QUrl &url) const
162 {
163     qDebug() << __PRETTY_FUNCTION__;
164
165     if (url.toString().contains(GET_LOCATIONS))
166         return SituareService::RequestGetLocations;
167     else if (url.toString().contains(UPDATE_LOCATION))
168         return SituareService::RequestUpdateLocation;
169     else if (url.toString().contains(REVERSE_GEO))
170         return SituareService::RequestReverseGeo;
171     else
172         return SituareService::RequestUnknown;
173 }
174
175 void SituareService::imageReceived(const QUrl &url, const QPixmap &image)
176 {
177     qDebug() << __PRETTY_FUNCTION__;
178     qDebug() << "Image URL: " << url << " size :" << image.size();
179
180     // assign facebook silhouette image to all who doesn't have a profile image
181     if(url == QUrl(SILHOUETTE_URL)) {
182         if(m_user->profileImageUrl().isEmpty()) {
183             m_user->setProfileImage(AvatarImage::create(image, AvatarImage::Large));
184             emit imageReady(m_user);
185         }
186         foreach(User *friendItem, m_friendsList) {
187             if(friendItem->profileImageUrl().isEmpty()) {
188                 friendItem->setProfileImage(AvatarImage::create(image, AvatarImage::Small));
189                 emit imageReady(friendItem);
190             }
191         }
192     }
193
194     if (m_user->profileImageUrl() == url) {
195         m_user->setProfileImage(AvatarImage::create(image, AvatarImage::Large));
196         emit imageReady(m_user);
197     }
198
199     foreach(User *friendItem, m_friendsList) {
200         if(friendItem->profileImageUrl() == url) {
201             friendItem->setProfileImage(AvatarImage::create(image, AvatarImage::Small));
202             emit imageReady(friendItem);
203         }
204     }
205 }
206
207 void SituareService::parseReply(const QByteArray &jsonReply, RequestName requestName)
208 {
209     qDebug() << __PRETTY_FUNCTION__;
210
211     QJson::Parser parser;
212     bool ok;
213
214     QVariantMap result = parser.parse(jsonReply, &ok).toMap();
215
216     if (!ok) {
217         emit error(ErrorContext::SITUARE, SituareError::INVALID_JSON);
218     } else {
219         QVariant resultStatus = result["ResultStatus"];
220         QVariant resultData = result["ResultData"];
221
222         if (resultStatus.toString() == "ERROR") {
223             QVariantMap errorData = resultData.toMap();
224             emit error(ErrorContext::SITUARE, errorData["ErrorCode"].toInt());
225         } else if (resultStatus.toString() == "OK") {
226             if (requestName == SituareService::RequestGetLocations)
227                 parseUserData(resultData);
228             else if (requestName == SituareService::RequestUpdateLocation)
229                 emit updateWasSuccessful();
230             else if (requestName == SituareService::RequestReverseGeo)
231                 parseReverseGeoData(resultData);
232         }
233     }
234 }
235
236 void SituareService::parseReverseGeoData(const QVariant &reverseGeoData)
237 {
238     qDebug() << __PRETTY_FUNCTION__;
239
240     QVariantMap result = reverseGeoData.toMap();
241
242     if (result.contains("address") && !result["address"].toString().isEmpty()) {
243         emit reverseGeoReady(result["address"].toString());
244     } else {
245         QStringList coordinates;
246         coordinates.append(result["lat"].toString());
247         coordinates.append(result["lon"].toString());
248         emit error(ErrorContext::SITUARE, SituareError::ADDRESS_RETRIEVAL_FAILED);
249         emit reverseGeoReady(coordinates.join(", "));
250     }
251 }
252
253 void SituareService::parseUserData(const QVariant &userData)
254 {
255     qDebug() << __PRETTY_FUNCTION__;
256
257     m_defaultImage = false;
258
259     QVariantMap result = userData.toMap();
260
261      if (result.contains("user")) {
262
263         QVariant userVariant = result.value("user");
264         QMap<QString, QVariant> userMap = userVariant.toMap();
265
266         GeoCoordinate coordinates(userMap["latitude"].toReal(), userMap["longitude"].toReal());
267
268         QUrl imageUrl = userMap[NORMAL_SIZE_PROFILE_IMAGE].toUrl();
269
270         if(imageUrl.isEmpty()) {
271             // user doesn't have profile image, so we need to get him a silhouette image
272             m_defaultImage = true;
273         }
274
275         QString address = userMap["address"].toString();
276         if(address.isEmpty()) {
277             QStringList location;
278             location.append(QString::number(coordinates.latitude()));
279             location.append(QString::number(coordinates.longitude()));
280             address = location.join(", ");
281         }
282
283         User user = User(address, coordinates, userMap["name"].toString(),
284                       userMap["note"].toString(), imageUrl, userMap["timestamp"].toString(),
285                       true, userMap["uid"].toString());
286
287         QList<User> tmpFriendsList;
288
289         foreach (QVariant friendsVariant, result["friends"].toList()) {
290           QMap<QString, QVariant> friendMap = friendsVariant.toMap();
291           QVariant distance = friendMap["distance"];
292           QMap<QString, QVariant> distanceMap = distance.toMap();
293
294           GeoCoordinate coordinates(friendMap["latitude"].toReal(),friendMap["longitude"].toReal());
295
296           QUrl imageUrl = friendMap["profile_pic"].toUrl();
297
298           if(imageUrl.isEmpty()) {
299               // friend doesn't have profile image, so we need to get him a silhouette image
300               m_defaultImage = true;
301           }
302
303           QString address = friendMap["address"].toString();
304           if(address.isEmpty()) {
305               QStringList location;
306               location.append(QString::number(coordinates.latitude()));
307               location.append(QString::number(coordinates.longitude()));
308               address = location.join(", ");
309           }
310
311           User buddy = User(address, coordinates, friendMap["name"].toString(),
312                            friendMap["note"].toString(), imageUrl,
313                            friendMap["timestamp"].toString(),
314                            false, friendMap["uid"].toString(), distanceMap["units"].toString(),
315                            distanceMap["value"].toDouble());
316
317           tmpFriendsList.append(buddy);
318         }
319
320         QList<QUrl> imageUrlList; // url list for images
321
322         // set unchanged profile images or add new images to imageUrlList for downloading
323         if(m_user) {
324             if(m_user->profileImageUrl() != user.profileImageUrl()) {
325                 if(!user.profileImageUrl().isEmpty())
326                     imageUrlList.append(user.profileImageUrl());
327             } else {
328                 user.setProfileImage(m_user->profileImage());
329             }
330         } else {
331             if(!user.profileImageUrl().isEmpty())
332                 imageUrlList.append(user.profileImageUrl());
333         }
334
335         // clear old user object
336         if(m_user) {
337             delete m_user;
338             m_user = 0;
339         }
340
341         // create new user object from temporary user object
342         m_user = new User(user);
343
344         // set unchanged profile images or add new images to imageUrlList for downloading
345         if(!m_friendsList.isEmpty()) {
346             foreach(User tmpBuddy, tmpFriendsList) {
347                 if(!tmpBuddy.profileImageUrl().isEmpty()) {
348                     bool found = false;
349                     foreach(User *buddy, m_friendsList) {
350                         if(tmpBuddy.profileImageUrl() == buddy->profileImageUrl()) {
351                             tmpBuddy.setProfileImage(buddy->profileImage());
352                             found = true;
353                             break;
354                         }
355                     }
356                     if(!found && !tmpBuddy.profileImageUrl().isEmpty())
357                         imageUrlList.append(tmpBuddy.profileImageUrl());
358                 }
359             }
360         } else {
361             foreach(User buddy, tmpFriendsList) {
362                 if(!buddy.profileImageUrl().isEmpty())
363                     imageUrlList.append(buddy.profileImageUrl());
364             }
365         }
366
367         // clear old friendlist
368         qDeleteAll(m_friendsList.begin(), m_friendsList.end());
369         m_friendsList.clear();
370
371         // populate new friendlist with temporary friendlist's data
372         foreach(User tmpFriendItem, tmpFriendsList) {
373             User *friendItem = new User(tmpFriendItem);
374             m_friendsList.append(friendItem);
375         }
376         tmpFriendsList.clear();
377
378         emit userDataChanged(m_user, m_friendsList);
379
380         // set silhouette image to imageUrlList for downloading
381         if(m_defaultImage)
382             imageUrlList.append(QUrl(SILHOUETTE_URL));
383
384         addProfileImages(imageUrlList);
385         imageUrlList.clear();
386     } else {
387         QVariant address = result.value("address");
388         if(!address.toString().isEmpty()) {
389             emit reverseGeoReady(address.toString());
390         } else {
391             QStringList coordinates;
392             coordinates.append(result.value("lat").toString());
393             coordinates.append(result.value("lon").toString());
394
395             emit error(ErrorContext::SITUARE, SituareError::ADDRESS_RETRIEVAL_FAILED);
396             emit reverseGeoReady(coordinates.join(", "));
397         }
398     }
399 }
400
401 void SituareService::requestFinished(QNetworkReply *reply)
402 {
403     qDebug() << __PRETTY_FUNCTION__;
404
405     //Reply from situare
406     if (m_currentRequests.contains(reply)) {
407
408         if (reply->error())
409             emit error(ErrorContext::NETWORK, reply->error());
410         else
411             parseReply(reply->readAll(), getRequestName(reply->url()));
412
413         m_currentRequests.removeAll(reply);
414         reply->deleteLater();
415     }
416 }
417
418 void SituareService::reverseGeo(const GeoCoordinate &coordinates)
419 {
420     qDebug() << __PRETTY_FUNCTION__;
421
422     QHash<QString, QString> parameters;
423     parameters.insert("lat", degreesToString(coordinates.latitude()));
424     parameters.insert("lon", degreesToString(coordinates.longitude()));
425     parameters.insert("format", "json");
426
427     buildRequest(REVERSE_GEO, parameters);
428 }
429
430 void SituareService::sendRequest(const QString &requestUrl)
431 {
432     qDebug() << __PRETTY_FUNCTION__ << "requestUrl" << requestUrl;
433
434     // make and send the request
435     QNetworkRequest request;
436     request.setUrl(QUrl(requestUrl));
437     request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false);
438     QNetworkReply *reply = m_networkManager->get(request, true);
439     m_currentRequests.append(reply);
440 }
441
442 void SituareService::updateSession(const QString &session)
443 {
444     qDebug() << __PRETTY_FUNCTION__;
445
446     m_session = session;
447
448     if (m_session.isEmpty())
449         clearUserData();
450 }
451
452 void SituareService::updateLocation(const GeoCoordinate &coordinates, const QString &status,
453                                     const bool &publish)
454 {
455     qDebug() << __PRETTY_FUNCTION__;
456
457     QHash<QString, QString> parameters;
458     parameters.insert("lat", degreesToString(coordinates.latitude()));
459     parameters.insert("lon", degreesToString(coordinates.longitude()));
460     parameters.insert("publish", publish ? "true" : "false");
461     parameters.insert("data", status); ///< @todo if !empty ???
462
463     buildRequest(UPDATE_LOCATION, parameters);
464 }