Weather Info (C++/QML)▲
Sélectionnez
/**
**************************************************************************
**
** 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 &
amp;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 &
amp;value)
{
m_dayOfWeek =
value;
emit dataChanged();
}
void
WeatherData::
setWeatherIcon(const
QString &
amp;value)
{
m_weather =
value;
emit dataChanged();
}
void
WeatherData::
setWeatherDescription(const
QString &
amp;value)
{
m_weatherDescription =
value;
emit dataChanged();
}
void
WeatherData::
setTemperature(const
QString &
amp;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&
lt;WeatherData*&
gt; forecast;
QQmlListProperty&
lt;WeatherData&
gt; *
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&
lt;WeatherData&
gt; *
prop, WeatherData *
val)
{
Q_UNUSED(val);
Q_UNUSED(prop);
}
static
WeatherData *
forecastAt(QQmlListProperty&
lt;WeatherData&
gt; *
prop, int
index)
{
AppModelPrivate *
d =
static_cast
&
lt;AppModelPrivate*&
gt;(prop-&
gt;data);
return
d-&
gt;forecast.at(index);
}
static
int
forecastCount(QQmlListProperty&
lt;WeatherData&
gt; *
prop)
{
AppModelPrivate *
d =
static_cast
&
lt;AppModelPrivate*&
gt;(prop-&
gt;data);
return
d-&
gt;forecast.size();
}
static
void
forecastClear(QQmlListProperty&
lt;WeatherData&
gt; *
prop)
{
static_cast
&
lt;AppModelPrivate*&
gt;(prop-&
gt;data)-&
gt;forecast.clear();
}
AppModel::
AppModel(QObject *
parent) :
QObject(parent),
d(new
AppModelPrivate)
{
d-&
gt;fcProp =
new
QQmlListProperty&
lt;WeatherData&
gt;(this
, d,
forecastAppend,
forecastCount,
forecastAt,
forecastClear);
connect(&
amp;d-&
gt;delayedCityRequestTimer, SIGNAL(timeout()),
this
, SLOT(queryCity()));
connect(&
amp;d-&
gt;requestNewWeatherTimer, SIGNAL(timeout()),
this
, SLOT(refreshWeather()));
d-&
gt;requestNewWeatherTimer.start();
// make sure we have an active network session
d-&
gt;nam =
new
QNetworkAccessManager(this
);
QNetworkConfigurationManager ncm;
d-&
gt;ns =
new
QNetworkSession(ncm.defaultConfiguration(), this
);
connect(d-&
gt;ns, SIGNAL(opened()), this
, SLOT(networkSessionOpened()));
// the session may be already open. if it is, run the slot directly
if
(d-&
gt;ns-&
gt;isOpen())
this
-&
gt;networkSessionOpened();
// tell the system we want network
d-&
gt;ns-&
gt;open();
}
AppModel::
~
AppModel()
{
d-&
gt;ns-&
gt;close();
if
(d-&
gt;src)
d-&
gt;src-&
gt;stopUpdates();
delete
d;
}
void
AppModel::
networkSessionOpened()
{
d-&
gt;src =
QGeoPositionInfoSource::
createDefaultSource(this
);
if
(d-&
gt;src) {
d-&
gt;useGps =
true
;
connect(d-&
gt;src, SIGNAL(positionUpdated(QGeoPositionInfo)),
this
, SLOT(positionUpdated(QGeoPositionInfo)));
connect(d-&
gt;src, SIGNAL(error(QGeoPositionInfoSource::
Error)),
this
, SLOT(positionError(QGeoPositionInfoSource::
Error)));
d-&
gt;src-&
gt;startUpdates();
}
else
{
d-&
gt;useGps =
false
;
d-&
gt;city =
"Brisbane"
;
emit cityChanged();
this
-&
gt;refreshWeather();
}
}
void
AppModel::
positionUpdated(QGeoPositionInfo gpsPos)
{
d-&
gt;coord =
gpsPos.coordinate();
if
(!
(d-&
gt;useGps))
return
;
queryCity();
}
void
AppModel::
queryCity()
{
//don't update more often then once a minute
//to keep load on server low
if
(d-&
gt;throttle.isValid() &
amp;&
amp; d-&
gt;throttle.elapsed() &
lt; d-&
gt;minMsBeforeNewRequest ) {
qCDebug(requestsLog) &
lt;&
lt; "delaying query of city"
;
if
(!
d-&
gt;delayedCityRequestTimer.isActive())
d-&
gt;delayedCityRequestTimer.start();
return
;
}
qDebug(requestsLog) &
lt;&
lt; "requested query of city"
;
d-&
gt;throttle.start();
d-&
gt;minMsBeforeNewRequest =
(d-&
gt;nErrors +
1
) *
d-&
gt;baseMsBeforeNewRequest;
QString latitude, longitude;
longitude.setNum(d-&
gt;coord.longitude());
latitude.setNum(d-&
gt;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-&
gt;app_ident);
url.setQuery(query);
qCDebug(requestsLog) &
lt;&
lt; "submitting request"
;
QNetworkReply *
rep =
d-&
gt;nam-&
gt;get(QNetworkRequest(url));
// connect up the signal right away
connect(rep, &
amp;QNetworkReply::
finished,
this
, [this
, rep]() {
handleGeoNetworkData(rep); }
);
}
void
AppModel::
positionError(QGeoPositionInfoSource::
Error e)
{
Q_UNUSED(e);
qWarning() &
lt;&
lt; "Position source error. Falling back to simulation mode."
;
// cleanup insufficient QGeoPositionInfoSource instance
d-&
gt;src-&
gt;stopUpdates();
d-&
gt;src-&
gt;deleteLater();
d-&
gt;src =
0
;
// activate simulation mode
d-&
gt;useGps =
false
;
d-&
gt;city =
"Brisbane"
;
emit cityChanged();
this
-&
gt;refreshWeather();
}
void
AppModel::
hadError(bool
tryAgain)
{
qCDebug(requestsLog) &
lt;&
lt; "hadError, will "
&
lt;&
lt; (tryAgain ? ""
: "not "
) &
lt;&
lt; "rety"
;
d-&
gt;throttle.start();
if
(d-&
gt;nErrors &
lt; 10
)
++
d-&
gt;nErrors;
d-&
gt;minMsBeforeNewRequest =
(d-&
gt;nErrors +
1
) *
d-&
gt;baseMsBeforeNewRequest;
if
(tryAgain)
d-&
gt;delayedCityRequestTimer.start();
}
void
AppModel::
handleGeoNetworkData(QNetworkReply *
networkReply)
{
if
(!
networkReply) {
hadError(false
); // should retry?
return
;
}
if
(!
networkReply-&
gt;error()) {
d-&
gt;nErrors =
0
;
if
(!
d-&
gt;throttle.isValid())
d-&
gt;throttle.start();
d-&
gt;minMsBeforeNewRequest =
d-&
gt;baseMsBeforeNewRequest;
//convert coordinates to city name
QJsonDocument document =
QJsonDocument::
fromJson(networkReply-&
gt;readAll());
QJsonObject jo =
document.object();
QJsonValue jv =
jo.value(QStringLiteral("name"
));
const
QString city =
jv.toString();
qCDebug(requestsLog) &
lt;&
lt; "got city: "
&
lt;&
lt; city;
if
(city !=
d-&
gt;city) {
d-&
gt;city =
city;
emit cityChanged();
refreshWeather();
}
}
else
{
hadError(true
);
}
networkReply-&
gt;deleteLater();
}
void
AppModel::
refreshWeather()
{
if
(d-&
gt;city.isEmpty()) {
qCDebug(requestsLog) &
lt;&
lt; "refreshing weather skipped (no city)"
;
return
;
}
qCDebug(requestsLog) &
lt;&
lt; "refreshing weather"
;
QUrl url("http://api.openweathermap.org/data/2.5/weather"
);
QUrlQuery query;
query.addQueryItem("q"
, d-&
gt;city);
query.addQueryItem("mode"
, "json"
);
query.addQueryItem("APPID"
, d-&
gt;app_ident);
url.setQuery(query);
QNetworkReply *
rep =
d-&
gt;nam-&
gt;get(QNetworkRequest(url));
// connect up the signal right away
connect(rep, &
amp;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) &
lt;&
lt; "got weather network data"
;
if
(!
networkReply)
return
;
if
(!
networkReply-&
gt;error()) {
foreach (WeatherData *
inf, d-&
gt;forecast)
delete
inf;
d-&
gt;forecast.clear();
QJsonDocument document =
QJsonDocument::
fromJson(networkReply-&
gt;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-&
gt;now.setWeatherDescription(tempObject.value(QStringLiteral("description"
)).toString());
d-&
gt;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-&
gt;now.setTemperature(niceTemperatureString(val.toDouble()));
}
}
}
networkReply-&
gt;deleteLater();
//retrieve the forecast
QUrl url("http://api.openweathermap.org/data/2.5/forecast/daily"
);
QUrlQuery query;
query.addQueryItem("q"
, d-&
gt;city);
query.addQueryItem("mode"
, "json"
);
query.addQueryItem("cnt"
, "5"
);
query.addQueryItem("APPID"
, d-&
gt;app_ident);
url.setQuery(query);
QNetworkReply *
rep =
d-&
gt;nam-&
gt;get(QNetworkRequest(url));
// connect up the signal right away
connect(rep, &
amp;QNetworkReply::
finished,
this
, [this
, rep]() {
handleForecastNetworkData(rep); }
);
}
void
AppModel::
handleForecastNetworkData(QNetworkReply *
networkReply)
{
qCDebug(requestsLog) &
lt;&
lt; "got forecast"
;
if
(!
networkReply)
return
;
if
(!
networkReply-&
gt;error()) {
QJsonDocument document =
QJsonDocument::
fromJson(networkReply-&
gt;readAll());
QJsonObject jo;
QJsonValue jv;
QJsonObject root =
document.object();
jv =
root.value(QStringLiteral("list"
));
if
(!
jv.isArray())
qWarning() &
lt;&
lt; "Invalid forecast object"
;
QJsonArray ja =
jv.toArray();
//we need 4 days of forecast -> first entry is today
if
(ja.count() !=
5
)
qWarning() &
lt;&
lt; "Invalid forecast object"
;
QString data;
for
(int
i =
1
; i&
lt;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-&
gt;setTemperature(data);
//get date
jv =
subtree.value(QStringLiteral("dt"
));
QDateTime dt =
QDateTime::
fromMSecsSinceEpoch((qint64)jv.toDouble()*
1000
);
forecastEntry-&
gt;setDayOfWeek(dt.date().toString(QStringLiteral("ddd"
)));
//get icon
QJsonArray weatherArray =
subtree.value(QStringLiteral("weather"
)).toArray();
jo =
weatherArray.at(0
).toObject();
forecastEntry-&
gt;setWeatherIcon(jo.value(QStringLiteral("icon"
)).toString());
//get description
forecastEntry-&
gt;setWeatherDescription(jo.value(QStringLiteral("description"
)).toString());
d-&
gt;forecast.append(forecastEntry);
}
if
(!
(d-&
gt;ready)) {
d-&
gt;ready =
true
;
emit readyChanged();
}
emit weatherChanged();
}
networkReply-&
gt;deleteLater();
}
bool
AppModel::
hasValidCity() const
{
return
(!
(d-&
gt;city.isEmpty()) &
amp;&
amp; d-&
gt;city.size() &
gt; 1
&
amp;&
amp; d-&
gt;city !=
""
);
}
bool
AppModel::
hasValidWeather() const
{
return
hasValidCity() &
amp;&
amp; (!
(d-&
gt;now.weatherIcon().isEmpty()) &
amp;&
amp;
(d-&
gt;now.weatherIcon().size() &
gt; 1
) &
amp;&
amp;
d-&
gt;now.weatherIcon() !=
""
);
}
WeatherData *
AppModel::
weather() const
{
return
&
amp;(d-&
gt;now);
}
QQmlListProperty&
lt;WeatherData&
gt; AppModel::
forecast() const
{
return
*
(d-&
gt;fcProp);
}
bool
AppModel::
ready() const
{
return
d-&
gt;ready;
}
bool
AppModel::
hasSource() const
{
return
(d-&
gt;src !=
NULL
);
}
bool
AppModel::
useGps() const
{
return
d-&
gt;useGps;
}
void
AppModel::
setUseGps(bool
value)
{
d-&
gt;useGps =
value;
if
(value) {
d-&
gt;city =
""
;
d-&
gt;throttle.invalidate();
emit cityChanged();
emit weatherChanged();
}
emit useGpsChanged();
}
QString AppModel::
city() const
{
return
d-&
gt;city;
}
void
AppModel::
setCity(const
QString &
amp;value)
{
d-&
gt;city =
value;
emit cityChanged();
refreshWeather();
}