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 | |
76 | WeatherData::WeatherData(QObject *parent) : |
77 | QObject(parent) |
78 | { |
79 | } |
80 | |
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 | |
90 | 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 | */ |
104 | QString WeatherData::weatherIcon() const |
105 | { |
106 | return m_weather; |
107 | } |
108 | |
109 | QString WeatherData::weatherDescription() const |
110 | { |
111 | return m_weatherDescription; |
112 | } |
113 | |
114 | QString WeatherData::temperature() const |
115 | { |
116 | return m_temperature; |
117 | } |
118 | |
119 | void WeatherData::setDayOfWeek(const QString &value) |
120 | { |
121 | m_dayOfWeek = value; |
122 | emit dataChanged(); |
123 | } |
124 | |
125 | void WeatherData::setWeatherIcon(const QString &value) |
126 | { |
127 | m_weather = value; |
128 | emit dataChanged(); |
129 | } |
130 | |
131 | void WeatherData::setWeatherDescription(const QString &value) |
132 | { |
133 | m_weatherDescription = value; |
134 | emit dataChanged(); |
135 | } |
136 | |
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 | |
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 | |
182 | static void forecastAppend(QQmlListProperty<WeatherData> *prop, WeatherData *val) |
183 | { |
184 | Q_UNUSED(val); |
185 | Q_UNUSED(prop); |
186 | } |
187 | |
188 | static WeatherData *forecastAt(QQmlListProperty<WeatherData> *prop, int index) |
189 | { |
190 | AppModelPrivate *d = static_cast<AppModelPrivate*>(prop->data); |
191 | return d->forecast.at(i: index); |
192 | } |
193 | |
194 | static int forecastCount(QQmlListProperty<WeatherData> *prop) |
195 | { |
196 | AppModelPrivate *d = static_cast<AppModelPrivate*>(prop->data); |
197 | return d->forecast.size(); |
198 | } |
199 | |
200 | static void forecastClear(QQmlListProperty<WeatherData> *prop) |
201 | { |
202 | static_cast<AppModelPrivate*>(prop->data)->forecast.clear(); |
203 | } |
204 | |
205 | //! [0] |
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(sender: &d->delayedCityRequestTimer, SIGNAL(timeout()), |
218 | receiver: this, SLOT(queryCity())); |
219 | connect(sender: &d->requestNewWeatherTimer, SIGNAL(timeout()), |
220 | receiver: this, SLOT(refreshWeather())); |
221 | d->requestNewWeatherTimer.start(); |
222 | |
223 | |
224 | //! [1] |
225 | d->nam = new QNetworkAccessManager(this); |
226 | d->src = QGeoPositionInfoSource::createDefaultSource(parent: this); |
227 | |
228 | if (d->src) { |
229 | d->useGps = true; |
230 | connect(sender: d->src, SIGNAL(positionUpdated(QGeoPositionInfo)), |
231 | receiver: this, SLOT(positionUpdated(QGeoPositionInfo))); |
232 | connect(sender: d->src, SIGNAL(error(QGeoPositionInfoSource::Error)), |
233 | receiver: 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 | |
244 | AppModel::~AppModel() |
245 | { |
246 | if (d->src) |
247 | d->src->stopUpdates(); |
248 | delete d; |
249 | } |
250 | |
251 | //! [2] |
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 | |
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(catFunc: 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(key: "lat" , value: latitude); |
284 | query.addQueryItem(key: "lon" , value: longitude); |
285 | query.addQueryItem(key: "mode" , value: "json" ); |
286 | query.addQueryItem(key: "APPID" , value: d->app_ident); |
287 | url.setQuery(query); |
288 | qCDebug(requestsLog) << "submitting request" ; |
289 | |
290 | QNetworkReply *rep = d->nam->get(request: QNetworkRequest(url)); |
291 | // connect up the signal right away |
292 | connect(sender: rep, signal: &QNetworkReply::finished, |
293 | context: this, slot: [this, rep]() { handleGeoNetworkData(networkReply: rep); }); |
294 | } |
295 | |
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 | |
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 | |
323 | void AppModel::handleGeoNetworkData(QNetworkReply *networkReply) |
324 | { |
325 | if (!networkReply) { |
326 | hadError(tryAgain: 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(json: 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(tryAgain: true); |
350 | } |
351 | networkReply->deleteLater(); |
352 | } |
353 | |
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(key: "q" , value: d->city); |
365 | query.addQueryItem(key: "mode" , value: "json" ); |
366 | query.addQueryItem(key: "APPID" , value: d->app_ident); |
367 | url.setQuery(query); |
368 | |
369 | QNetworkReply *rep = d->nam->get(request: QNetworkRequest(url)); |
370 | // connect up the signal right away |
371 | connect(sender: rep, signal: &QNetworkReply::finished, |
372 | context: this, slot: [this, rep]() { handleWeatherNetworkData(networkReply: rep); }); |
373 | } |
374 | |
375 | static QString niceTemperatureString(double t) |
376 | { |
377 | return QString::number(qRound(d: t-ZERO_KELVIN)) + QChar(0xB0); |
378 | } |
379 | |
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(json: 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(i: 0); |
402 | tempObject = val.toObject(); |
403 | d->now.setWeatherDescription(tempObject.value(QStringLiteral("description" )).toString()); |
404 | d->now.setWeatherIcon(tempObject.value(key: "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(t: 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(key: "q" , value: d->city); |
421 | query.addQueryItem(key: "mode" , value: "json" ); |
422 | query.addQueryItem(key: "cnt" , value: "5" ); |
423 | query.addQueryItem(key: "APPID" , value: d->app_ident); |
424 | url.setQuery(query); |
425 | |
426 | QNetworkReply *rep = d->nam->get(request: QNetworkRequest(url)); |
427 | // connect up the signal right away |
428 | connect(sender: rep, signal: &QNetworkReply::finished, |
429 | context: this, slot: [this, rep]() { handleForecastNetworkData(networkReply: rep); }); |
430 | } |
431 | |
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(json: 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(t: jv.toDouble()); |
462 | data += QChar('/'); |
463 | jv = jo.value(QStringLiteral("max" )); |
464 | data += niceTemperatureString(t: jv.toDouble()); |
465 | forecastEntry->setTemperature(data); |
466 | |
467 | //get date |
468 | jv = subtree.value(QStringLiteral("dt" )); |
469 | QDateTime dt = QDateTime::fromMSecsSinceEpoch(msecs: (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(i: 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(t: 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 | |
493 | bool AppModel::hasValidCity() const |
494 | { |
495 | return (!(d->city.isEmpty()) && d->city.size() > 1 && d->city != "" ); |
496 | } |
497 | |
498 | bool AppModel::hasValidWeather() const |
499 | { |
500 | return hasValidCity() && (!(d->now.weatherIcon().isEmpty()) && |
501 | (d->now.weatherIcon().size() > 1) && |
502 | d->now.weatherIcon() != "" ); |
503 | } |
504 | |
505 | WeatherData *AppModel::weather() const |
506 | { |
507 | return &(d->now); |
508 | } |
509 | |
510 | QQmlListProperty<WeatherData> AppModel::forecast() const |
511 | { |
512 | return *(d->fcProp); |
513 | } |
514 | |
515 | bool AppModel::ready() const |
516 | { |
517 | return d->ready; |
518 | } |
519 | |
520 | bool AppModel::hasSource() const |
521 | { |
522 | return (d->src != NULL); |
523 | } |
524 | |
525 | bool AppModel::useGps() const |
526 | { |
527 | return d->useGps; |
528 | } |
529 | |
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 | |
542 | QString AppModel::city() const |
543 | { |
544 | return d->city; |
545 | } |
546 | |
547 | void AppModel::setCity(const QString &value) |
548 | { |
549 | d->city = value; |
550 | emit cityChanged(); |
551 | refreshWeather(); |
552 | } |
553 | |