| /**************************************************************************** |
| ** |
| ** 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(); |
| } |