blob: d07a5980091d2846763df60fc3b9dd25f1826358 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2017 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the examples of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of The Qt Company Ltd nor the names of its
** contributors may be used to endorse or promote products derived
** from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "appmodel.h"
#include <qgeopositioninfosource.h>
#include <qgeosatelliteinfosource.h>
#include <qnmeapositioninfosource.h>
#include <qgeopositioninfo.h>
#include <qnetworkconfigmanager.h>
#include <qnetworksession.h>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QStringList>
#include <QTimer>
#include <QUrlQuery>
#include <QElapsedTimer>
#include <QLoggingCategory>
/*
*This application uses http://openweathermap.org/api
**/
#define ZERO_KELVIN 273.15
Q_LOGGING_CATEGORY(requestsLog,"wapp.requests")
WeatherData::WeatherData(QObject *parent) :
QObject(parent)
{
}
WeatherData::WeatherData(const WeatherData &other) :
QObject(0),
m_dayOfWeek(other.m_dayOfWeek),
m_weather(other.m_weather),
m_weatherDescription(other.m_weatherDescription),
m_temperature(other.m_temperature)
{
}
QString WeatherData::dayOfWeek() const
{
return m_dayOfWeek;
}
/*!
* The icon value is based on OpenWeatherMap.org icon set. For details
* see http://bugs.openweathermap.org/projects/api/wiki/Weather_Condition_Codes
*
* e.g. 01d ->sunny day
*
* The icon string will be translated to
* http://openweathermap.org/img/w/01d.png
*/
QString WeatherData::weatherIcon() const
{
return m_weather;
}
QString WeatherData::weatherDescription() const
{
return m_weatherDescription;
}
QString WeatherData::temperature() const
{
return m_temperature;
}
void WeatherData::setDayOfWeek(const QString &value)
{
m_dayOfWeek = value;
emit dataChanged();
}
void WeatherData::setWeatherIcon(const QString &value)
{
m_weather = value;
emit dataChanged();
}
void WeatherData::setWeatherDescription(const QString &value)
{
m_weatherDescription = value;
emit dataChanged();
}
void WeatherData::setTemperature(const QString &value)
{
m_temperature = value;
emit dataChanged();
}
class AppModelPrivate
{
public:
static const int baseMsBeforeNewRequest = 5 * 1000; // 5 s, increased after each missing answer up to 10x
QGeoPositionInfoSource *src;
QGeoCoordinate coord;
QString longitude, latitude;
QString city;
QNetworkAccessManager *nam;
QNetworkSession *ns;
WeatherData now;
QList<WeatherData*> forecast;
QQmlListProperty<WeatherData> *fcProp;
bool ready;
bool useGps;
QElapsedTimer throttle;
int nErrors;
int minMsBeforeNewRequest;
QTimer delayedCityRequestTimer;
QTimer requestNewWeatherTimer;
QString app_ident;
AppModelPrivate() :
src(NULL),
nam(NULL),
ns(NULL),
fcProp(NULL),
ready(false),
useGps(true),
nErrors(0),
minMsBeforeNewRequest(baseMsBeforeNewRequest)
{
delayedCityRequestTimer.setSingleShot(true);
delayedCityRequestTimer.setInterval(1000); // 1 s
requestNewWeatherTimer.setSingleShot(false);
requestNewWeatherTimer.setInterval(20*60*1000); // 20 min
throttle.invalidate();
app_ident = QStringLiteral("36496bad1955bf3365448965a42b9eac");
}
};
static void forecastAppend(QQmlListProperty<WeatherData> *prop, WeatherData *val)
{
Q_UNUSED(val);
Q_UNUSED(prop);
}
static WeatherData *forecastAt(QQmlListProperty<WeatherData> *prop, int index)
{
AppModelPrivate *d = static_cast<AppModelPrivate*>(prop->data);
return d->forecast.at(index);
}
static int forecastCount(QQmlListProperty<WeatherData> *prop)
{
AppModelPrivate *d = static_cast<AppModelPrivate*>(prop->data);
return d->forecast.size();
}
static void forecastClear(QQmlListProperty<WeatherData> *prop)
{
static_cast<AppModelPrivate*>(prop->data)->forecast.clear();
}
//! [0]
AppModel::AppModel(QObject *parent) :
QObject(parent),
d(new AppModelPrivate)
{
//! [0]
d->fcProp = new QQmlListProperty<WeatherData>(this, d,
forecastAppend,
forecastCount,
forecastAt,
forecastClear);
connect(&d->delayedCityRequestTimer, SIGNAL(timeout()),
this, SLOT(queryCity()));
connect(&d->requestNewWeatherTimer, SIGNAL(timeout()),
this, SLOT(refreshWeather()));
d->requestNewWeatherTimer.start();
//! [1]
// make sure we have an active network session
d->nam = new QNetworkAccessManager(this);
QNetworkConfigurationManager ncm;
d->ns = new QNetworkSession(ncm.defaultConfiguration(), this);
connect(d->ns, SIGNAL(opened()), this, SLOT(networkSessionOpened()));
// the session may be already open. if it is, run the slot directly
if (d->ns->isOpen())
this->networkSessionOpened();
// tell the system we want network
d->ns->open();
}
//! [1]
AppModel::~AppModel()
{
d->ns->close();
if (d->src)
d->src->stopUpdates();
delete d;
}
//! [2]
void AppModel::networkSessionOpened()
{
d->src = QGeoPositionInfoSource::createDefaultSource(this);
if (d->src) {
d->useGps = true;
connect(d->src, SIGNAL(positionUpdated(QGeoPositionInfo)),
this, SLOT(positionUpdated(QGeoPositionInfo)));
connect(d->src, SIGNAL(error(QGeoPositionInfoSource::Error)),
this, SLOT(positionError(QGeoPositionInfoSource::Error)));
d->src->startUpdates();
} else {
d->useGps = false;
d->city = "Brisbane";
emit cityChanged();
this->refreshWeather();
}
}
//! [2]
//! [3]
void AppModel::positionUpdated(QGeoPositionInfo gpsPos)
{
d->coord = gpsPos.coordinate();
if (!(d->useGps))
return;
queryCity();
}
//! [3]
void AppModel::queryCity()
{
//don't update more often then once a minute
//to keep load on server low
if (d->throttle.isValid() && d->throttle.elapsed() < d->minMsBeforeNewRequest ) {
qCDebug(requestsLog) << "delaying query of city";
if (!d->delayedCityRequestTimer.isActive())
d->delayedCityRequestTimer.start();
return;
}
qDebug(requestsLog) << "requested query of city";
d->throttle.start();
d->minMsBeforeNewRequest = (d->nErrors + 1) * d->baseMsBeforeNewRequest;
QString latitude, longitude;
longitude.setNum(d->coord.longitude());
latitude.setNum(d->coord.latitude());
QUrl url("http://api.openweathermap.org/data/2.5/weather");
QUrlQuery query;
query.addQueryItem("lat", latitude);
query.addQueryItem("lon", longitude);
query.addQueryItem("mode", "json");
query.addQueryItem("APPID", d->app_ident);
url.setQuery(query);
qCDebug(requestsLog) << "submitting request";
QNetworkReply *rep = d->nam->get(QNetworkRequest(url));
// connect up the signal right away
connect(rep, &QNetworkReply::finished,
this, [this, rep]() { handleGeoNetworkData(rep); });
}
void AppModel::positionError(QGeoPositionInfoSource::Error e)
{
Q_UNUSED(e);
qWarning() << "Position source error. Falling back to simulation mode.";
// cleanup insufficient QGeoPositionInfoSource instance
d->src->stopUpdates();
d->src->deleteLater();
d->src = 0;
// activate simulation mode
d->useGps = false;
d->city = "Brisbane";
emit cityChanged();
this->refreshWeather();
}
void AppModel::hadError(bool tryAgain)
{
qCDebug(requestsLog) << "hadError, will " << (tryAgain ? "" : "not ") << "rety";
d->throttle.start();
if (d->nErrors < 10)
++d->nErrors;
d->minMsBeforeNewRequest = (d->nErrors + 1) * d->baseMsBeforeNewRequest;
if (tryAgain)
d->delayedCityRequestTimer.start();
}
void AppModel::handleGeoNetworkData(QNetworkReply *networkReply)
{
if (!networkReply) {
hadError(false); // should retry?
return;
}
if (!networkReply->error()) {
d->nErrors = 0;
if (!d->throttle.isValid())
d->throttle.start();
d->minMsBeforeNewRequest = d->baseMsBeforeNewRequest;
//convert coordinates to city name
QJsonDocument document = QJsonDocument::fromJson(networkReply->readAll());
QJsonObject jo = document.object();
QJsonValue jv = jo.value(QStringLiteral("name"));
const QString city = jv.toString();
qCDebug(requestsLog) << "got city: " << city;
if (city != d->city) {
d->city = city;
emit cityChanged();
refreshWeather();
}
} else {
hadError(true);
}
networkReply->deleteLater();
}
void AppModel::refreshWeather()
{
if (d->city.isEmpty()) {
qCDebug(requestsLog) << "refreshing weather skipped (no city)";
return;
}
qCDebug(requestsLog) << "refreshing weather";
QUrl url("http://api.openweathermap.org/data/2.5/weather");
QUrlQuery query;
query.addQueryItem("q", d->city);
query.addQueryItem("mode", "json");
query.addQueryItem("APPID", d->app_ident);
url.setQuery(query);
QNetworkReply *rep = d->nam->get(QNetworkRequest(url));
// connect up the signal right away
connect(rep, &QNetworkReply::finished,
this, [this, rep]() { handleWeatherNetworkData(rep); });
}
static QString niceTemperatureString(double t)
{
return QString::number(qRound(t-ZERO_KELVIN)) + QChar(0xB0);
}
void AppModel::handleWeatherNetworkData(QNetworkReply *networkReply)
{
qCDebug(requestsLog) << "got weather network data";
if (!networkReply)
return;
if (!networkReply->error()) {
foreach (WeatherData *inf, d->forecast)
delete inf;
d->forecast.clear();
QJsonDocument document = QJsonDocument::fromJson(networkReply->readAll());
if (document.isObject()) {
QJsonObject obj = document.object();
QJsonObject tempObject;
QJsonValue val;
if (obj.contains(QStringLiteral("weather"))) {
val = obj.value(QStringLiteral("weather"));
QJsonArray weatherArray = val.toArray();
val = weatherArray.at(0);
tempObject = val.toObject();
d->now.setWeatherDescription(tempObject.value(QStringLiteral("description")).toString());
d->now.setWeatherIcon(tempObject.value("icon").toString());
}
if (obj.contains(QStringLiteral("main"))) {
val = obj.value(QStringLiteral("main"));
tempObject = val.toObject();
val = tempObject.value(QStringLiteral("temp"));
d->now.setTemperature(niceTemperatureString(val.toDouble()));
}
}
}
networkReply->deleteLater();
//retrieve the forecast
QUrl url("http://api.openweathermap.org/data/2.5/forecast/daily");
QUrlQuery query;
query.addQueryItem("q", d->city);
query.addQueryItem("mode", "json");
query.addQueryItem("cnt", "5");
query.addQueryItem("APPID", d->app_ident);
url.setQuery(query);
QNetworkReply *rep = d->nam->get(QNetworkRequest(url));
// connect up the signal right away
connect(rep, &QNetworkReply::finished,
this, [this, rep]() { handleForecastNetworkData(rep); });
}
void AppModel::handleForecastNetworkData(QNetworkReply *networkReply)
{
qCDebug(requestsLog) << "got forecast";
if (!networkReply)
return;
if (!networkReply->error()) {
QJsonDocument document = QJsonDocument::fromJson(networkReply->readAll());
QJsonObject jo;
QJsonValue jv;
QJsonObject root = document.object();
jv = root.value(QStringLiteral("list"));
if (!jv.isArray())
qWarning() << "Invalid forecast object";
QJsonArray ja = jv.toArray();
//we need 4 days of forecast -> first entry is today
if (ja.count() != 5)
qWarning() << "Invalid forecast object";
QString data;
for (int i = 1; i<ja.count(); i++) {
WeatherData *forecastEntry = new WeatherData();
//min/max temperature
QJsonObject subtree = ja.at(i).toObject();
jo = subtree.value(QStringLiteral("temp")).toObject();
jv = jo.value(QStringLiteral("min"));
data.clear();
data += niceTemperatureString(jv.toDouble());
data += QChar('/');
jv = jo.value(QStringLiteral("max"));
data += niceTemperatureString(jv.toDouble());
forecastEntry->setTemperature(data);
//get date
jv = subtree.value(QStringLiteral("dt"));
QDateTime dt = QDateTime::fromMSecsSinceEpoch((qint64)jv.toDouble()*1000);
forecastEntry->setDayOfWeek(dt.date().toString(QStringLiteral("ddd")));
//get icon
QJsonArray weatherArray = subtree.value(QStringLiteral("weather")).toArray();
jo = weatherArray.at(0).toObject();
forecastEntry->setWeatherIcon(jo.value(QStringLiteral("icon")).toString());
//get description
forecastEntry->setWeatherDescription(jo.value(QStringLiteral("description")).toString());
d->forecast.append(forecastEntry);
}
if (!(d->ready)) {
d->ready = true;
emit readyChanged();
}
emit weatherChanged();
}
networkReply->deleteLater();
}
bool AppModel::hasValidCity() const
{
return (!(d->city.isEmpty()) && d->city.size() > 1 && d->city != "");
}
bool AppModel::hasValidWeather() const
{
return hasValidCity() && (!(d->now.weatherIcon().isEmpty()) &&
(d->now.weatherIcon().size() > 1) &&
d->now.weatherIcon() != "");
}
WeatherData *AppModel::weather() const
{
return &(d->now);
}
QQmlListProperty<WeatherData> AppModel::forecast() const
{
return *(d->fcProp);
}
bool AppModel::ready() const
{
return d->ready;
}
bool AppModel::hasSource() const
{
return (d->src != NULL);
}
bool AppModel::useGps() const
{
return d->useGps;
}
void AppModel::setUseGps(bool value)
{
d->useGps = value;
if (value) {
d->city = "";
d->throttle.invalidate();
emit cityChanged();
emit weatherChanged();
}
emit useGpsChanged();
}
QString AppModel::city() const
{
return d->city;
}
void AppModel::setCity(const QString &value)
{
d->city = value;
emit cityChanged();
refreshWeather();
}