appmodel.cpp Example File | Qt Positioning 5.4

- Qt 5.4
- Qt Positioning
- Weather Info (C++/QML)
- appmodel.cpp Example File
- Qt 5.4.2 Reference Documentation

# appmodel.cpp Example File

weatherinfo/appmodel.cpp

    /****************************************************************************
    **
    ** 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;
        bool useGps;
        QElapsedTimer throttle;
        int nErrors;
        int minMsBeforeNewRequest;
        QTimer delayedCityRequestTimer;
        QTimer requestNewWeatherTimer;

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

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

    AppModel::AppModel(QObject *parent) :
            QObject(parent),
            d(new AppModelPrivate)
    {
        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();

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

    AppModel::~AppModel()
    {
        d->ns->close();
        if (d->src)
            d->src->stopUpdates();
        delete d;
    }

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

    void AppModel::positionUpdated(QGeoPositionInfo gpsPos)
    {
        d->coord = gpsPos.coordinate();

        if (!(d->useGps))
            return;

        queryCity();
    }

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

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