550 lines
16 KiB
C++
550 lines
16 KiB
C++
/****************************************************************************
|
|
**
|
|
** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
|
|
** Contact: http://www.qt-project.org/legal
|
|
**
|
|
** This file is part of the examples of the Qt Toolkit.
|
|
**
|
|
** $QT_BEGIN_LICENSE:BSD$
|
|
** 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 Digia Plc and its Subsidiary(-ies) 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 <QSignalMapper>
|
|
#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;
|
|
QSignalMapper *geoReplyMapper;
|
|
QSignalMapper *weatherReplyMapper, *forecastReplyMapper;
|
|
bool ready;
|
|
QElapsedTimer throttle;
|
|
int nErrors;
|
|
int minMsBeforeNewRequest;
|
|
QTimer delayedCityRequestTimer;
|
|
QTimer requestNewWeatherTimer;
|
|
|
|
AppModelPrivate() :
|
|
src(NULL),
|
|
nam(NULL),
|
|
ns(NULL),
|
|
fcProp(NULL),
|
|
ready(false),
|
|
nErrors(0),
|
|
minMsBeforeNewRequest(baseMsBeforeNewRequest)
|
|
{
|
|
delayedCityRequestTimer.setSingleShot(true);
|
|
delayedCityRequestTimer.setInterval(1000); // 1 s
|
|
requestNewWeatherTimer.setSingleShot(false);
|
|
requestNewWeatherTimer.setInterval(20*60*1000); // 20 min
|
|
throttle.invalidate();
|
|
}
|
|
};
|
|
|
|
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);
|
|
|
|
d->geoReplyMapper = new QSignalMapper(this);
|
|
d->weatherReplyMapper = new QSignalMapper(this);
|
|
d->forecastReplyMapper = new QSignalMapper(this);
|
|
|
|
connect(d->geoReplyMapper, SIGNAL(mapped(QObject*)),
|
|
this, SLOT(handleGeoNetworkData(QObject*)));
|
|
connect(d->weatherReplyMapper, SIGNAL(mapped(QObject*)),
|
|
this, SLOT(handleWeatherNetworkData(QObject*)));
|
|
connect(d->forecastReplyMapper, SIGNAL(mapped(QObject*)),
|
|
this, SLOT(handleForecastNetworkData(QObject*)));
|
|
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) {
|
|
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->city = "Brisbane";
|
|
emit cityChanged();
|
|
this->refreshWeather();
|
|
}
|
|
}
|
|
//! [2]
|
|
|
|
//! [3]
|
|
void AppModel::positionUpdated(QGeoPositionInfo gpsPos)
|
|
{
|
|
d->coord = gpsPos.coordinate();
|
|
|
|
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");
|
|
url.setQuery(query);
|
|
qCDebug(requestsLog) << "submitting request";
|
|
|
|
QNetworkReply *rep = d->nam->get(QNetworkRequest(url));
|
|
// connect up the signal right away
|
|
d->geoReplyMapper->setMapping(rep, rep);
|
|
connect(rep, SIGNAL(finished()),
|
|
d->geoReplyMapper, SLOT(map()));
|
|
}
|
|
|
|
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->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(QObject *replyObj)
|
|
{
|
|
QNetworkReply *networkReply = qobject_cast<QNetworkReply*>(replyObj);
|
|
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");
|
|
url.setQuery(query);
|
|
|
|
QNetworkReply *rep = d->nam->get(QNetworkRequest(url));
|
|
// connect up the signal right away
|
|
d->weatherReplyMapper->setMapping(rep, rep);
|
|
connect(rep, SIGNAL(finished()),
|
|
d->weatherReplyMapper, SLOT(map()));
|
|
}
|
|
|
|
static QString niceTemperatureString(double t)
|
|
{
|
|
return QString::number(qRound(t-ZERO_KELVIN)) + QChar(0xB0);
|
|
}
|
|
|
|
void AppModel::handleWeatherNetworkData(QObject *replyObj)
|
|
{
|
|
qCDebug(requestsLog) << "got weather network data";
|
|
QNetworkReply *networkReply = qobject_cast<QNetworkReply*>(replyObj);
|
|
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");
|
|
url.setQuery(query);
|
|
|
|
QNetworkReply *rep = d->nam->get(QNetworkRequest(url));
|
|
// connect up the signal right away
|
|
d->forecastReplyMapper->setMapping(rep, rep);
|
|
connect(rep, SIGNAL(finished()), d->forecastReplyMapper, SLOT(map()));
|
|
}
|
|
|
|
void AppModel::handleForecastNetworkData(QObject *replyObj)
|
|
{
|
|
qCDebug(requestsLog) << "got forecast";
|
|
QNetworkReply *networkReply = qobject_cast<QNetworkReply*>(replyObj);
|
|
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);
|
|
}
|
|
|
|
QString AppModel::city() const
|
|
{
|
|
return d->city;
|
|
}
|
|
|
|
void AppModel::setCity(const QString &value)
|
|
{
|
|
d->city = value;
|
|
emit cityChanged();
|
|
refreshWeather();
|
|
}
|