1 /****************************************************************************
2 **
3 ** Copyright (C) 2017 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the examples of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:BSD$
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 https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** BSD License Usage
18 ** Alternatively, you may use this file under the terms of the BSD license
19 ** as follows:
20 **
21 ** "Redistribution and use in source and binary forms, with or without
22 ** modification, are permitted provided that the following conditions are
23 ** met:
24 **   * Redistributions of source code must retain the above copyright
25 **     notice, this list of conditions and the following disclaimer.
26 **   * Redistributions in binary form must reproduce the above copyright
27 **     notice, this list of conditions and the following disclaimer in
28 **     the documentation and/or other materials provided with the
29 **     distribution.
30 **   * Neither the name of The Qt Company Ltd nor the names of its
31 **     contributors may be used to endorse or promote products derived
32 **     from this software without specific prior written permission.
33 **
34 **
35 ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
36 ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
37 ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
38 ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
39 ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
40 ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
41 ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
42 ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
43 ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
44 ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
45 ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
46 **
47 ** $QT_END_LICENSE$
48 **
49 ****************************************************************************/
50 
51 #include "appmodel.h"
52 
53 #include <qgeopositioninfosource.h>
54 #include <qgeosatelliteinfosource.h>
55 #include <qnmeapositioninfosource.h>
56 #include <qgeopositioninfo.h>
57 #include <qnetworkconfigmanager.h>
58 
59 #include <QJsonDocument>
60 #include <QJsonObject>
61 #include <QJsonArray>
62 #include <QStringList>
63 #include <QTimer>
64 #include <QUrlQuery>
65 #include <QElapsedTimer>
66 #include <QLoggingCategory>
67 
68 /*
69  *This application uses http://openweathermap.org/api
70  **/
71 
72 #define ZERO_KELVIN 273.15
73 
74 Q_LOGGING_CATEGORY(requestsLog,"wapp.requests")
75 
WeatherData(QObject * parent)76 WeatherData::WeatherData(QObject *parent) :
77         QObject(parent)
78 {
79 }
80 
WeatherData(const WeatherData & other)81 WeatherData::WeatherData(const WeatherData &other) :
82         QObject(0),
83         m_dayOfWeek(other.m_dayOfWeek),
84         m_weather(other.m_weather),
85         m_weatherDescription(other.m_weatherDescription),
86         m_temperature(other.m_temperature)
87 {
88 }
89 
dayOfWeek() const90 QString WeatherData::dayOfWeek() const
91 {
92     return m_dayOfWeek;
93 }
94 
95 /*!
96  * The icon value is based on OpenWeatherMap.org icon set. For details
97  * see http://bugs.openweathermap.org/projects/api/wiki/Weather_Condition_Codes
98  *
99  * e.g. 01d ->sunny day
100  *
101  * The icon string will be translated to
102  * http://openweathermap.org/img/w/01d.png
103  */
weatherIcon() const104 QString WeatherData::weatherIcon() const
105 {
106     return m_weather;
107 }
108 
weatherDescription() const109 QString WeatherData::weatherDescription() const
110 {
111     return m_weatherDescription;
112 }
113 
temperature() const114 QString WeatherData::temperature() const
115 {
116     return m_temperature;
117 }
118 
setDayOfWeek(const QString & value)119 void WeatherData::setDayOfWeek(const QString &value)
120 {
121     m_dayOfWeek = value;
122     emit dataChanged();
123 }
124 
setWeatherIcon(const QString & value)125 void WeatherData::setWeatherIcon(const QString &value)
126 {
127     m_weather = value;
128     emit dataChanged();
129 }
130 
setWeatherDescription(const QString & value)131 void WeatherData::setWeatherDescription(const QString &value)
132 {
133     m_weatherDescription = value;
134     emit dataChanged();
135 }
136 
setTemperature(const QString & value)137 void WeatherData::setTemperature(const QString &value)
138 {
139     m_temperature = value;
140     emit dataChanged();
141 }
142 
143 class AppModelPrivate
144 {
145 public:
146     static const int baseMsBeforeNewRequest = 5 * 1000; // 5 s, increased after each missing answer up to 10x
147     QGeoPositionInfoSource *src;
148     QGeoCoordinate coord;
149     QString longitude, latitude;
150     QString city;
151     QNetworkAccessManager *nam;
152     WeatherData now;
153     QList<WeatherData*> forecast;
154     QQmlListProperty<WeatherData> *fcProp;
155     bool ready;
156     bool useGps;
157     QElapsedTimer throttle;
158     int nErrors;
159     int minMsBeforeNewRequest;
160     QTimer delayedCityRequestTimer;
161     QTimer requestNewWeatherTimer;
162     QString app_ident;
163 
AppModelPrivate()164     AppModelPrivate() :
165             src(NULL),
166             nam(NULL),
167             fcProp(NULL),
168             ready(false),
169             useGps(true),
170             nErrors(0),
171             minMsBeforeNewRequest(baseMsBeforeNewRequest)
172     {
173         delayedCityRequestTimer.setSingleShot(true);
174         delayedCityRequestTimer.setInterval(1000); // 1 s
175         requestNewWeatherTimer.setSingleShot(false);
176         requestNewWeatherTimer.setInterval(20*60*1000); // 20 min
177         throttle.invalidate();
178         app_ident = QStringLiteral("36496bad1955bf3365448965a42b9eac");
179     }
180 };
181 
forecastAppend(QQmlListProperty<WeatherData> * prop,WeatherData * val)182 static void forecastAppend(QQmlListProperty<WeatherData> *prop, WeatherData *val)
183 {
184     Q_UNUSED(val);
185     Q_UNUSED(prop);
186 }
187 
forecastAt(QQmlListProperty<WeatherData> * prop,int index)188 static WeatherData *forecastAt(QQmlListProperty<WeatherData> *prop, int index)
189 {
190     AppModelPrivate *d = static_cast<AppModelPrivate*>(prop->data);
191     return d->forecast.at(index);
192 }
193 
forecastCount(QQmlListProperty<WeatherData> * prop)194 static int forecastCount(QQmlListProperty<WeatherData> *prop)
195 {
196     AppModelPrivate *d = static_cast<AppModelPrivate*>(prop->data);
197     return d->forecast.size();
198 }
199 
forecastClear(QQmlListProperty<WeatherData> * prop)200 static void forecastClear(QQmlListProperty<WeatherData> *prop)
201 {
202     static_cast<AppModelPrivate*>(prop->data)->forecast.clear();
203 }
204 
205 //! [0]
AppModel(QObject * parent)206 AppModel::AppModel(QObject *parent) :
207         QObject(parent),
208         d(new AppModelPrivate)
209 {
210 //! [0]
211     d->fcProp = new QQmlListProperty<WeatherData>(this, d,
212                                                           forecastAppend,
213                                                           forecastCount,
214                                                           forecastAt,
215                                                           forecastClear);
216 
217     connect(&d->delayedCityRequestTimer, SIGNAL(timeout()),
218             this, SLOT(queryCity()));
219     connect(&d->requestNewWeatherTimer, SIGNAL(timeout()),
220             this, SLOT(refreshWeather()));
221     d->requestNewWeatherTimer.start();
222 
223 
224 //! [1]
225     d->nam = new QNetworkAccessManager(this);
226     d->src = QGeoPositionInfoSource::createDefaultSource(this);
227 
228     if (d->src) {
229         d->useGps = true;
230         connect(d->src, SIGNAL(positionUpdated(QGeoPositionInfo)),
231                 this, SLOT(positionUpdated(QGeoPositionInfo)));
232         connect(d->src, SIGNAL(error(QGeoPositionInfoSource::Error)),
233                 this, SLOT(positionError(QGeoPositionInfoSource::Error)));
234         d->src->startUpdates();
235     } else {
236         d->useGps = false;
237         d->city = "Brisbane";
238         emit cityChanged();
239         this->refreshWeather();
240     }
241 }
242 //! [1]
243 
~AppModel()244 AppModel::~AppModel()
245 {
246     if (d->src)
247         d->src->stopUpdates();
248     delete d;
249 }
250 
251 //! [2]
positionUpdated(QGeoPositionInfo gpsPos)252 void AppModel::positionUpdated(QGeoPositionInfo gpsPos)
253 {
254     d->coord = gpsPos.coordinate();
255 
256     if (!(d->useGps))
257         return;
258 
259     queryCity();
260 }
261 //! [2]
262 
queryCity()263 void AppModel::queryCity()
264 {
265     //don't update more often then once a minute
266     //to keep load on server low
267     if (d->throttle.isValid() && d->throttle.elapsed() < d->minMsBeforeNewRequest ) {
268         qCDebug(requestsLog) << "delaying query of city";
269         if (!d->delayedCityRequestTimer.isActive())
270             d->delayedCityRequestTimer.start();
271         return;
272     }
273     qDebug(requestsLog) << "requested query of city";
274     d->throttle.start();
275     d->minMsBeforeNewRequest = (d->nErrors + 1) * d->baseMsBeforeNewRequest;
276 
277     QString latitude, longitude;
278     longitude.setNum(d->coord.longitude());
279     latitude.setNum(d->coord.latitude());
280 
281     QUrl url("http://api.openweathermap.org/data/2.5/weather");
282     QUrlQuery query;
283     query.addQueryItem("lat", latitude);
284     query.addQueryItem("lon", longitude);
285     query.addQueryItem("mode", "json");
286     query.addQueryItem("APPID", d->app_ident);
287     url.setQuery(query);
288     qCDebug(requestsLog) << "submitting request";
289 
290     QNetworkReply *rep = d->nam->get(QNetworkRequest(url));
291     // connect up the signal right away
292     connect(rep, &QNetworkReply::finished,
293             this, [this, rep]() { handleGeoNetworkData(rep); });
294 }
295 
positionError(QGeoPositionInfoSource::Error e)296 void AppModel::positionError(QGeoPositionInfoSource::Error e)
297 {
298     Q_UNUSED(e);
299     qWarning() << "Position source error. Falling back to simulation mode.";
300     // cleanup insufficient QGeoPositionInfoSource instance
301     d->src->stopUpdates();
302     d->src->deleteLater();
303     d->src = 0;
304 
305     // activate simulation mode
306     d->useGps = false;
307     d->city = "Brisbane";
308     emit cityChanged();
309     this->refreshWeather();
310 }
311 
hadError(bool tryAgain)312 void AppModel::hadError(bool tryAgain)
313 {
314     qCDebug(requestsLog) << "hadError, will " << (tryAgain ? "" : "not ") << "rety";
315     d->throttle.start();
316     if (d->nErrors < 10)
317         ++d->nErrors;
318     d->minMsBeforeNewRequest = (d->nErrors + 1) * d->baseMsBeforeNewRequest;
319     if (tryAgain)
320         d->delayedCityRequestTimer.start();
321 }
322 
handleGeoNetworkData(QNetworkReply * networkReply)323 void AppModel::handleGeoNetworkData(QNetworkReply *networkReply)
324 {
325     if (!networkReply) {
326         hadError(false); // should retry?
327         return;
328     }
329 
330     if (!networkReply->error()) {
331         d->nErrors = 0;
332         if (!d->throttle.isValid())
333             d->throttle.start();
334         d->minMsBeforeNewRequest = d->baseMsBeforeNewRequest;
335         //convert coordinates to city name
336         QJsonDocument document = QJsonDocument::fromJson(networkReply->readAll());
337 
338         QJsonObject jo = document.object();
339         QJsonValue jv = jo.value(QStringLiteral("name"));
340 
341         const QString city = jv.toString();
342         qCDebug(requestsLog) << "got city: " << city;
343         if (city != d->city) {
344             d->city = city;
345             emit cityChanged();
346             refreshWeather();
347         }
348     } else {
349         hadError(true);
350     }
351     networkReply->deleteLater();
352 }
353 
refreshWeather()354 void AppModel::refreshWeather()
355 {
356     if (d->city.isEmpty()) {
357         qCDebug(requestsLog) << "refreshing weather skipped (no city)";
358         return;
359     }
360     qCDebug(requestsLog) << "refreshing weather";
361     QUrl url("http://api.openweathermap.org/data/2.5/weather");
362     QUrlQuery query;
363 
364     query.addQueryItem("q", d->city);
365     query.addQueryItem("mode", "json");
366     query.addQueryItem("APPID", d->app_ident);
367     url.setQuery(query);
368 
369     QNetworkReply *rep = d->nam->get(QNetworkRequest(url));
370     // connect up the signal right away
371     connect(rep, &QNetworkReply::finished,
372             this, [this, rep]() { handleWeatherNetworkData(rep); });
373 }
374 
niceTemperatureString(double t)375 static QString niceTemperatureString(double t)
376 {
377     return QString::number(qRound(t-ZERO_KELVIN)) + QChar(0xB0);
378 }
379 
handleWeatherNetworkData(QNetworkReply * networkReply)380 void AppModel::handleWeatherNetworkData(QNetworkReply *networkReply)
381 {
382     qCDebug(requestsLog) << "got weather network data";
383     if (!networkReply)
384         return;
385 
386     if (!networkReply->error()) {
387         foreach (WeatherData *inf, d->forecast)
388             delete inf;
389         d->forecast.clear();
390 
391         QJsonDocument document = QJsonDocument::fromJson(networkReply->readAll());
392 
393         if (document.isObject()) {
394             QJsonObject obj = document.object();
395             QJsonObject tempObject;
396             QJsonValue val;
397 
398             if (obj.contains(QStringLiteral("weather"))) {
399                 val = obj.value(QStringLiteral("weather"));
400                 QJsonArray weatherArray = val.toArray();
401                 val = weatherArray.at(0);
402                 tempObject = val.toObject();
403                 d->now.setWeatherDescription(tempObject.value(QStringLiteral("description")).toString());
404                 d->now.setWeatherIcon(tempObject.value("icon").toString());
405             }
406             if (obj.contains(QStringLiteral("main"))) {
407                 val = obj.value(QStringLiteral("main"));
408                 tempObject = val.toObject();
409                 val = tempObject.value(QStringLiteral("temp"));
410                 d->now.setTemperature(niceTemperatureString(val.toDouble()));
411             }
412         }
413     }
414     networkReply->deleteLater();
415 
416     //retrieve the forecast
417     QUrl url("http://api.openweathermap.org/data/2.5/forecast/daily");
418     QUrlQuery query;
419 
420     query.addQueryItem("q", d->city);
421     query.addQueryItem("mode", "json");
422     query.addQueryItem("cnt", "5");
423     query.addQueryItem("APPID", d->app_ident);
424     url.setQuery(query);
425 
426     QNetworkReply *rep = d->nam->get(QNetworkRequest(url));
427     // connect up the signal right away
428     connect(rep, &QNetworkReply::finished,
429             this, [this, rep]() { handleForecastNetworkData(rep); });
430 }
431 
handleForecastNetworkData(QNetworkReply * networkReply)432 void AppModel::handleForecastNetworkData(QNetworkReply *networkReply)
433 {
434     qCDebug(requestsLog) << "got forecast";
435     if (!networkReply)
436         return;
437 
438     if (!networkReply->error()) {
439         QJsonDocument document = QJsonDocument::fromJson(networkReply->readAll());
440 
441         QJsonObject jo;
442         QJsonValue jv;
443         QJsonObject root = document.object();
444         jv = root.value(QStringLiteral("list"));
445         if (!jv.isArray())
446             qWarning() << "Invalid forecast object";
447         QJsonArray ja = jv.toArray();
448         //we need 4 days of forecast -> first entry is today
449         if (ja.count() != 5)
450             qWarning() << "Invalid forecast object";
451 
452         QString data;
453         for (int i = 1; i<ja.count(); i++) {
454             WeatherData *forecastEntry = new WeatherData();
455 
456             //min/max temperature
457             QJsonObject subtree = ja.at(i).toObject();
458             jo = subtree.value(QStringLiteral("temp")).toObject();
459             jv = jo.value(QStringLiteral("min"));
460             data.clear();
461             data += niceTemperatureString(jv.toDouble());
462             data += QChar('/');
463             jv = jo.value(QStringLiteral("max"));
464             data += niceTemperatureString(jv.toDouble());
465             forecastEntry->setTemperature(data);
466 
467             //get date
468             jv = subtree.value(QStringLiteral("dt"));
469             QDateTime dt = QDateTime::fromMSecsSinceEpoch((qint64)jv.toDouble()*1000);
470             forecastEntry->setDayOfWeek(dt.date().toString(QStringLiteral("ddd")));
471 
472             //get icon
473             QJsonArray weatherArray = subtree.value(QStringLiteral("weather")).toArray();
474             jo = weatherArray.at(0).toObject();
475             forecastEntry->setWeatherIcon(jo.value(QStringLiteral("icon")).toString());
476 
477             //get description
478             forecastEntry->setWeatherDescription(jo.value(QStringLiteral("description")).toString());
479 
480             d->forecast.append(forecastEntry);
481         }
482 
483         if (!(d->ready)) {
484             d->ready = true;
485             emit readyChanged();
486         }
487 
488         emit weatherChanged();
489     }
490     networkReply->deleteLater();
491 }
492 
hasValidCity() const493 bool AppModel::hasValidCity() const
494 {
495     return (!(d->city.isEmpty()) && d->city.size() > 1 && d->city != "");
496 }
497 
hasValidWeather() const498 bool AppModel::hasValidWeather() const
499 {
500     return hasValidCity() && (!(d->now.weatherIcon().isEmpty()) &&
501                               (d->now.weatherIcon().size() > 1) &&
502                               d->now.weatherIcon() != "");
503 }
504 
weather() const505 WeatherData *AppModel::weather() const
506 {
507     return &(d->now);
508 }
509 
forecast() const510 QQmlListProperty<WeatherData> AppModel::forecast() const
511 {
512     return *(d->fcProp);
513 }
514 
ready() const515 bool AppModel::ready() const
516 {
517     return d->ready;
518 }
519 
hasSource() const520 bool AppModel::hasSource() const
521 {
522     return (d->src != NULL);
523 }
524 
useGps() const525 bool AppModel::useGps() const
526 {
527     return d->useGps;
528 }
529 
setUseGps(bool value)530 void AppModel::setUseGps(bool value)
531 {
532     d->useGps = value;
533     if (value) {
534         d->city = "";
535         d->throttle.invalidate();
536         emit cityChanged();
537         emit weatherChanged();
538     }
539     emit useGpsChanged();
540 }
541 
city() const542 QString AppModel::city() const
543 {
544     return d->city;
545 }
546 
setCity(const QString & value)547 void AppModel::setCity(const QString &value)
548 {
549     d->city = value;
550     emit cityChanged();
551     refreshWeather();
552 }
553