/**************************************************************************** ** ** 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 #include #include #include #include #include #include #include #include #include #include #include #include /* *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; WeatherData now; QList forecast; QQmlListProperty *fcProp; bool ready; bool useGps; QElapsedTimer throttle; int nErrors; int minMsBeforeNewRequest; QTimer delayedCityRequestTimer; QTimer requestNewWeatherTimer; QString app_ident; AppModelPrivate() : src(NULL), nam(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 *prop, WeatherData *val) { Q_UNUSED(val); Q_UNUSED(prop); } static WeatherData *forecastAt(QQmlListProperty *prop, int index) { AppModelPrivate *d = static_cast(prop->data); return d->forecast.at(index); } static int forecastCount(QQmlListProperty *prop) { AppModelPrivate *d = static_cast(prop->data); return d->forecast.size(); } static void forecastClear(QQmlListProperty *prop) { static_cast(prop->data)->forecast.clear(); } //! [0] AppModel::AppModel(QObject *parent) : QObject(parent), d(new AppModelPrivate) { //! [0] d->fcProp = new QQmlListProperty(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] d->nam = new QNetworkAccessManager(this); 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(); } } //! [1] AppModel::~AppModel() { if (d->src) d->src->stopUpdates(); delete d; } //! [2] void AppModel::positionUpdated(QGeoPositionInfo gpsPos) { d->coord = gpsPos.coordinate(); if (!(d->useGps)) return; queryCity(); } //! [2] 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; isetTemperature(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 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(); }