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