Creating the UI
Weather2’s UI is split between four QML components:
-
main.qml
: The QML document initially loaded by the application delegate. It defines a tabbed pane containing two tabs (see Listing 7-9).
-
WeatherDetails.qml
: The control responsible for handling user input for weather requests. The control also manages various system prompts for notifying or requesting additional information from the user, when necessary (you will see that the prompts are defined as attached objects).
-
City.qml
: The control responsible for displaying the weather data for a given city. Note that this control is referenced in WeatherDetails.qml
(see Listing 7-10).
-
WeatherMap.qml
: The control responsible for displaying a map with the weather conditions for the selected city (see Listing 7-11).
Listing 7-9. main.qml
import bb.cascades 1.2
TabbedPane {
id: tabbedPane
showTabsOnActionBar: true
Tab {
title: "City weather"
Page {
WeatherDetails {
// control loaded from WeatherDetails.qml
}
}
}
Tab {
title: "Map"
Page {
WeatherMap {
// control loaded from WeatherMap.qml
}
}
}
}
As you can see in Listing 7-9, the WeatherDetails
and WeatherMap
controls are used as content properties for page controls. The QML engine will therefore automatically load the controls from the corresponding files located in the assets folder of your application project (note that WeatherDetails.qml
and WeatherMap.qml
are located in the same folder as main.qml
).
Let us now have a look at the WeatherDetails control implementation (see Listing 7-10).
Listing 7-10. WeatherDetails.qml
import bb.cascades 1.2
import bb.system 1.2
Container {
id: main
background: back.imagePaint
function onError(message) {
errorPrompt.title = message;
errorPrompt.show();
}
function onMultipleCitiesFound(cities) {
citiesDialog.clearList();
for (var i = 0; i < cities.length; i ++) {
citiesDialog.appendItem(cities[i]);
}
citiesDialog.show();
}
function onFinished() {
progress.cancel();
}
onCreationCompleted: {
_app.weather.multipleCitiesFound.connect(main.onMultipleCitiesFound);
_app.weather.error.connect(main.onError);
_app.weather.finished.connect(main.onFinished);
progress.cancelButton.label = "Cancel";
progress.confirmButton.label = "";
}
attachedObjects: [
ImagePaintDefinition {
id: back
repeatPattern: RepeatPattern.XY
imageSource: "asset:///images/background.jpg"
},
SystemListDialog {
id: citiesDialog
onFinished: {
if (value == SystemUiResult.ConfirmButtonSelection) {
_app.weather.cityWeather(citiesDialog.selectedIndices[0]);
progress.show();
}
}
},
SystemPrompt {
id: errorPrompt
onFinished: {
_app.weather.cityWeather(errorPrompt.inputFieldTextEntry());
progress.show();
}
},
SystemProgressDialog {
id: progress
title: "Retrieving city"
onFinished: {
if (value == SystemUiResult.CancelButtonSelection) {
_app.weather.cancel();
}
}
}
]
layout: StackLayout {
orientation: LayoutOrientation.BottomToTop
}
TextField {
id: location
inputMode: TextFieldInputMode.Default
textStyle.textAlign: TextAlign.Center
input {
submitKey: SubmitKey.Go
submitKeyFocusBehavior: SubmitKeyFocusBehavior.Lose
onSubmitted: {
_app.weather.cityWeather(location.text);
progress.show();
}
}
hintText: "Enter city or country name"
}
City{
// control loaded from City.qml
}
}
As you can see, WeatherDetails.qml
mostly contains some JavaScript code responsible for signal handling. Also, an important point to consider is the way the emitted signals from C++ are connected to the JavaScript functions in the main container’s onCreationCompleted
slot (in other words, the onError()
, onMultipleCitiesFound()
, and onFinished()
JavaScript functions or slots for signals emitted by the _app.weather
C++ object). Also note how the location text field’s onSubmitted
slot is used for calling the _app.weather.cityWeather()
slot, which is defined in C++. If the user’s initial query returns multiple cities, a SystemListDialog
is displayed, asking him to further refine the query. In the same manner, if an error occurs because the user’s query is incorrect, a SystemPrompt
is displayed, asking him to correct the query. In both cases, _app.weather.cityWeather()
is called with the user’s updated query.
The City
control is mostly a visual control for displaying the results of a weather request: the control uses labels and an image view for displaying the weather conditions for a given city. All QML properties defined in the control are bound to corresponding C++ properties (for example, Listing 7-11 gives you the binding for the current temperature).
Listing 7-11. City Control, Binding Example
Label {
id: temperature
text: _app.weather.cityinfo.temperature
horizontalAlignment: HorizontalAlignment.Center
textStyle {
fontWeight: FontWeight.W100
color: Color.Black
fontSize: FontSize.PercentageValue
fontSizeValue: 250
}
}
In the example provided in Listing 7-11, the label’s text
property is bound to the _app.weather.cityinfo.temperature
property, which is defined in C++ (as you will see in a moment). Therefore, when the _app.weather.cityinfo.temperature
property is updated in C++, the QML declarative engine automatically updates the label’s text property.
The final QML component to consider is the WeatherMap
component, which appears on the second tab. Listing 7-12 gives you component definition.
Listing 7-12. WeatherMap Control
import bb.cascades 1.2
import ludin.utils 1.0
Container {
layout: DockLayout {
}
onCreationCompleted: {
_app.weather.cityinfo.coordinatesChanged.connect(mapclient.setCoordinates);
scrollview.zoomToPoint(320, 220, 2, ScrollAnimation.Smooth);
}
attachedObjects: [
GoogleMapClient {
id: mapclient
}
]
ScrollView {
id: scrollview
horizontalAlignment: HorizontalAlignment.Fill
verticalAlignment: VerticalAlignment.Fill
scrollViewProperties {
scrollMode: ScrollMode.Both
pinchToZoomEnabled: true
}
ImageView {
id: citymap
image: mapclient.image
}
}
}
Here again, the control is relatively simple. It mainly consists of an image view responsible for displaying a map of the current weather conditions for a given location. The GoogleMapClient
attached object provides the actual weather image. Once again, QML bindings are used to synch the image view and the image map generated by the GoogleMapClient
attached object. Finally, the current map coordinates are provided to the GoogleMapClient
attached object by the _app.weather.cityinfo.coordinatesChanged()
signal (the signal to the slot connection is done in the main container’s onCreationCompleted
slot).
Adding the C++ Implementation
Let us now turn our attention to the C++ implementation. The most important factor to consider is how to organize your code so that you can define classes with specific responsibilities:
-
WeatherClient
: Responsible for performing the REST requests to the Weather Underground service (
www.wunderground.com/weather/api
). The class also handles the parsing of the JSON response.
-
CityInfo
: Encapsulates the weather data once it has been returned by the Weather Underground service. Note that the QML City
control has its properties bound to CityInfo
’s properties.
-
GoogleMapClient
: A client for generating static maps using the Google Maps service. An instance of this class is defined as an attached object property of the WeatherMap
control.
-
ApplicationUI
: The standard application delegate reachable from the QML layer of your application through the QML document context.
The class relationships are also quite simple: the ApplicationUI
object has a WeatherClient weather
property, which in turn has a CityInfo
property. The properties are accessible from QML as _app.weather
and _app.weather.cityinfo
, respectively.
WeatherClient
The WeatherClient
class definition is given in Listing 7-13.
Listing 7-13. WeatherClient Class Definition
#ifndef WEATHERCLIENT_H_
#define WEATHERCLIENT_H_
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include "CityInfo.h"
#include "GoogleMapClient.h"
class WeatherClient : public QObject {
Q_OBJECT
Q_PROPERTY(CityInfo* cityinfo READ city CONSTANT)
public:
WeatherClient(QObject* parent=0);
virtual ∼WeatherClient();
signals:
void multipleCitiesFound(QStringList cities);
void keyError(const QString& message
void error(const QString& message);
void finished();
public slots:
void cityWeather(QString city);
void cityWeather(int selectedIndex);
void cancel();
private slots:
void onCityRequestFinished();
void onCategoriesFinished();
private:
CityInfo* city() const;
void updateCityInfo(const QVariantMap& map);
QString m_apiKey;
QNetworkAccessManager* m_networkManager;
QList<QNetworkReply*> m_networkReplies;
CityInfo* m_cityInfo;
QStringList m_cities;
// static char* constant tags omitted
};
#endif /* WEATHERCLIENT_H_ */
The class definition declares multiple slots and signals. To perform an initial weather request, the
WeatherClient::cityWeather(QString city)
slot has to be called from QML (you might recall from
Chapter 3 that C++ slots and functions marked as
Q_INVOKABLE
can be called from QML). Also note that the signals are the same as those handled in JavaScript by the
WeatherDetails
control (see Listing 7-10). The
multipleCitiesFound
signal is emitted when a user query corresponds to multiple cities. (The cities are stored in a
QStringList
and passed as a parameter to the signal. As soon as the user selects a specific city, the
WeatherClient::cityWeather(int selectedIndex)
slot is called from QML and a new request is sent to the weather service.) The
error
signal is emitted when the Weather Underground service returns an error (the error is passed as a
QString
parameter to the signal), and, finally, the
finished
signal is emitted when a network request has completed.
Let us now turn our attention to the WeatherClient
member function definitions.
Constructor
Listing 7-14 gives you the WeatherClient constructor.
Listing 7-14. WeatherClient Constructor
WeatherClient::WeatherClient(QObject* parent) :
QObject(parent),
m_networkManager(QmlDocument::defaultDeclarativeEngine()->networkAccessManager()),
m_cityInfo(new CityInfo(this))
{
JsonDataAccess jda;
QVariant keyMap = jda.load(
QDir::currentPath() + WeatherClient::m_apiKeyPath);
if (jda.hasError()) {
emit keyError("Error, could not read api key");
} else {
m_apiKey = keyMap.toMap()[WeatherClient::m_keyTag].toString();
}
}
The constructor proceeds by initializing the class members using a member initialization list. The constructor body then tries to load the Weather Underground API key, which is required for each service request. The API key is stored in a JSON
file located in a subfolder of your application’s assets folder. If the constructor fails to load the key, a signal is emitted so that the UI layer can display an error message to the user. WeatherClient::m_apiKeyPath
and Weather::m_keyTag
are string constants that respectively identify the full path to the key file and the corresponding JSON
tag.
REST Service Request
A service request is handled by the WeatherClient::cityWeather(QString city)
member function (see Listing 7-15).
Listing 7-15. WeatherClient::cityWeather(QString city)
void WeatherClient::cityWeather(QString city) {
QString urlString("
http://api.wunderground.com/api/
");
urlString.append(WeatherClient::m_apiKey);
urlString.append("/conditions/q/");
urlString.append(city);
urlString.append(".json");
QNetworkRequest request;
request.setUrl(QUrl(urlString));
QNetworkReply* reply = this->m_networkManager->get(request);
bool result = connect(reply, SIGNAL(finished()), this,
SLOT(onCityRequestFinished()));
Q_ASSERT(result);
this->m_networkReplies.append(reply);
}
The
WeatherClient::cityWeather(QString city)
function dynamically creates a
GET
request
URL
by concatenating the city parameter and the API key previously loaded in the class constructor (the constructed URL will have the following structure:
http://api.wunderground.com/api/
<api key>/conditions/q/<city>.json
). As soon as the
GET
request has been submitted, you will have to connect the
QNetworkReply
’s
finished()
signal to the
WeatherClient
’s
onCityRequestFinished()
slot. Finally, when the request has completed,
WeatherClient::onCityRequestFinished()
will be called (see Listing 7-16).
Working with the Returned JSON
Before actually looking at how the returned JSON document is parsed by the
WeatherClient::onCityRequestFinished()
slot, let us quickly study the structure of the document returned by the Weather Underground service. As a matter of fact, you can conveniently use your browser to perform HTTP requests and study the responses returned by the service. For example, you can use the following URL to retrieve the weather conditions for Los Angeles:
http://api.wunderground.com/api/<key_value>/conditions/q/Los
Angeles, CA.json
.
The corresponding JSON structure is shown in Listing 7-16 (note that in order to save some page space, I have removed the JSON
elements that we will not need to parse or use in our code).
Listing 7-16. Wunderground JSON Response, Single City
{
"response": {
"version": "0.1",
"termsofService": "
http://www.wunderground.com/weather/api/d/terms.html
",
"features": {
"conditions": 1
}
},
"current_observation": {
"display_location": {
"full":"Los Angeles, CA",
"city":"Los Angeles",
"state":"CA",
"state_name":"California",
"country":"US",
"latitude":"33.97457886",
"longitude":"-118.24745941",
},
"observation_time":"Last Updated on October 7, 3:58 AM PDT",
"weather":"Clear",
"temperature_string":"63.1 F (17.3 C)",
"icon_url":"
http://icons-ak.wxug.com/i/c/k/nt_clear.gif
"
}
}
Remembering what I previously told you about parsing
JSON
documents with a JsonDataAccess object, you can see the following:
-
From the structure of the document shown in Listing 7-11, the root object is a QVariantMap
. Supposing that result
is the QVariant
variable obtained with the call to JsonDataAccess::load()
, the root object is therefore obtained with a call to result.toMap()
.
-
One level down, the current_observation
object contained in the root object is retrieved using result.toMap()["current_observation"].toMap()
.
-
Similarly, the latitude attribute is retrieved by chaining method calls as follows: result.toMap()["current_observation"].toMap()["display_location"].toMap()["latitude"].toString()
.
Once you get the hang of chaining the method calls, you will see that you can parse arbitrarily complex JSON structures.
There will be cases where the
JSON
response will return a list of cities instead of a single observation (this will happen when the city request matches multiple values). For example, if your request URL is
http://api.wunderground.com/api/<key_value>/conditions/q/Los
Angeles.json
(note the missing state specification), the returned
JSON
document will be given in Listing 7-17.
Listing 7-17. Wunderground JSON Response, Multiple Results
{
"response": {
"version": "0.1",
"termsofService": "
http://www.wunderground.com/weather/api/d/terms.html
",
"features": {
"conditions": 1
},
"results": [
{
"name": "Los Angeles",
"city": "Los Angeles",
"state": "CA",
"country": "US",
"country_iso3166":"US",
"country_name":"USA",
"zmw": "90001.1.99999",
"l": "/q/zmw:90001.1.99999"
},
{
"name": "Los Angeles",
"city": "Los Angeles",
"state": "",
"country": "CH",
"country_iso3166":"CL",
"country_name":"Chile",
"zmw": "00000.10.85703",
"l": "/q/zmw:00000.10.85703"
},
{
"name": "Los Angeles",
"city": "Los Angeles",
"state": "",
"cojuntry": "PH",
"country_iso3166":"PH",
"country_name":"Philippines",
"zmw": "00000.31.98752",
"l": "/q/zmw:00000.31.98752"
}
]
}
}
Here again, it is quite easy to retrieve the list of cities using the following call chain:
result.toMap()["response"].toMap()["results"].toList()
And finally, if the request contains an error, the returned JSON
document will be similar to Listing 7-18.
Listing 7-18. JSON Response with Error
{
"response": {
"version":"0.1",
"termsofService":"
http://www.wunderground.com/weather/api/d/terms.html
",
"features": {
},
"error": {
"type": "keynotfound",
"description": "this key does not exist"
}
}
}
In other words, you can check for the presence of an error object inside the response in order to make sure that your request was handled correctly by the service (the presence of the error object would be given by the following call chain: result.toMap()["response"].toMap()contains("error")
).
Now that you have a basic understanding of the JSON document structure, you can see how the service response is parsed in the WeatherClient::OnCityRequestFinished()
slot (see Listing 7-19).
Listing 7-19. WeatherClient::onCityRequestFinished( )
void WeatherClient::onCityRequestFinished() {
QNetworkReply* reply = static_cast<QNetworkReply*>(QObject::sender());
if (!reply->error()) {
JsonDataAccess jda;
QVariant response = jda.load(reply);
QVariantMap map = response.toMap();
if (map.contains(WeatherClient::m_currentObservationTag)) {
this->updateCityInfo(map);
} else { // else 1
if (map[WeatherClient::m_responseTag].toMap().contains(
WeatherClient::m_errorTag)) {
emit error(map[WeatherClient::m_responseTag]
.toMap()[WeatherClient::m_errorTag]
.toMap()[WeatherClient::m_descriptionTag].toString());
} else { // else 2
m_cities.clear();
QVariantList results = map[WeatherClient::m_responseTag]
.toMap()[WeatherClient::m_resultsTag].toList();
for (int i = 0; i < results.length(); i++) {
QVariantMap city = results[i].toMap();
if (city[WeatherClient::m_countryTag].toString()
== WeatherClient::m_USATag) {
m_cities.append(city[WeatherClient::m_nameTag].toString()
+ ", "+ city[WeatherClient::m_stateTag].toString());
} else { // else 3
m_cities.append(city[WeatherClient::m_nameTag].toString() + ", "
+ city[WeatherClient::m_countryNameTag].toString());
} // else 3
} // for
emit multipleCitiesFound(m_cities);
} // else 2
} // else 1
}
m_networkReplies.removeOne(reply);
reply->deleteLater();
emit finished();
}
Here is a quick description of the code:
1.
You will need to handle three cases in the response: a response can either contain the current weather conditions for a city, a list of cities, or an error object. Before even handling the response, we first need to check that the request was handled correctly and that there are no errors in the QNetworkReply
object.
2.
We then proceed by parsing the JSON
response.
3.
If the JSON
result contains a current_observation
object, we handle it immediately with a call to WeatherClient::updateCityInfo(const QVariantMap& map)
.
4.
Otherwise, we check if a service error has occurred. If this is the case, we emit the error
signal with the corresponding error message.
5.
If there are no errors, then multiples cities have been returned by the request. In this case, we populate the m_citiesList QStringList
and emit the multipleCitiesFound(m_citiesList)
signal, which will be handled in QML.
6.
Finally, we schedule the QNetworkReply
object for deletion and emit the finished()
signal.
The WeatherService::updateCityInfo(const QVariantMap& map)
method (used in Listing 7-20) is straightforward and is used for updating the m_cityInfo
member variable (which is accessible as the cityinfo
property from QML).
Listing 7-20. WeatherClient::updateCityInfo( )
void WeatherClient::updateCityInfo(const QVariantMap& data) {
QVariantMap currentObservation =
data[WeatherClient::m_currentObservationTag].toMap();
m_cityInfo->setCity(currentObservation[WeatherClient::m_displayLocationTag]
.toMap()[WeatherClient::m_cityTag].toString());
m_cityInfo->setState(currentObservation[WeatherClient::m_displayLocationTag]
.toMap()[WeatherClient::m_stateNameTag].toString());
m_cityInfo->setWeather(currentObservation[WeatherClient::m_weatherTag].toString());
m_cityInfo->setTemperature(currentObservation[WeatherClient::m_temperatureTag]
.toString());
m_cityInfo->setCoordinates(currentObservation[WeatherClient::m_displayLocationTag]
.toMap()[WeatherClient::m_latitudeTag].toString(),
currentObservation[WeatherClient::m_displayLocationTag]
.toMap()[WeatherClient::m_longitudeTag].toString(),
currentObservation[WeatherClient::m_iconUrlTag].toString());
m_cityInfo->setLastObservation(currentObservation[WeatherClient::m_observationTimeTag]
.toString());
}
CityInfo
Listing 7-21 gives you the CityInfo
class definition.
Listing 7-21. CityInfo Class Definition
#ifndef CITY_H_
#define CITY_H_
#include <QObject>
#include <bb/cascades/Image>
#include <QNetworkAccessManager>
class CityInfo : public QObject {
Q_OBJECT
Q_PROPERTY(QString city READ city NOTIFY cityChanged)
Q_PROPERTY(QString state READ state NOTIFY stateChanged)
Q_PROPERTY(QString latitude READ latitude NOTIFY latitudeChanged)
Q_PROPERTY(QString longitude READ longitude NOTIFY longitudeChanged)
Q_PROPERTY(QString weather READ weather NOTIFY weatherChanged)
Q_PROPERTY(QVariant weatherIcon READ weatherIcon NOTIFY weatherIconChanged)
Q_PROPERTY(QString temperature READ temperature NOTIFY temperatureChanged)
Q_PROPERTY(QString lastObservation READ lastObservation NOTIFY lastObservationChanged)
public:
CityInfo(QObject* parent = 0);
virtual ∼CityInfo();
void setCoordinates(const QString& latitude, const QString& longitude,
const QString& weatherIconUrl);
// accessors.
void setCity(const QString& city);
QString city() const;
void setState(const QString& state);
QString state() const;
void setLatitude(const QString& latitude);
QString latitude() const;
void setLongitude(const QString& longitude);
QString longitude() const;
void setWeather(const QString& weather);
QString weather() const;
void setTemperature(const QString& temperature);
QString temperature() const;
void setLastObservation(const QString& lastUpdated);
QString lastObservation() const;
signals:
void cityChanged();
void stateChanged();
void latitudeChanged();
void longitudeChanged();
void coordinatesChanged(const QString& latitude, const QString& longitude,
const QString& markerUrl);
void weatherChanged();
void weatherIconChanged();
void temperatureChanged();
void lastObservationChanged();
private slots:
void onWeatherIconRequestFinished();
private:
QVariant weatherIcon()const;
void setWeatherIconUrl(const QString& iconUrl);
void downloadWeatherIcon(const QString& iconUrl);
QNetworkAccessManager* m_networkManager;
QString m_city;
QString m_state;
QString m_latitude;
QString m_longitude;
QString m_temperature;
QString m_lastObservation;
QString m_weather;
QString m_weatherIconUrl;
bb::cascades::Image m_weatherIcon;
};
Note that the properties declared in the class definition are the ones used by the QML City
control bindings (see Listing 7-11). Also, the Notify
signals are required for updating the QML bindings when the C++ properties change.
If you look at Figure
7-3, you will notice that a small icon is used for representing the current weather conditions. The Weather Underground service provides a
URL
pointing to a downloadable image representing the current conditions (see the
icon_url
element in the
JSON
response in Listing 7-16). The CityInfo class therefore uses the URL to download the icon and display it in QML as an
ImageView
. Listings 7-22 and 7-23 provide the code for downloading the image.
Listing 7-22. CityInfo::downloadWeatherIcon
void CityInfo::downloadWeatherIcon(const QString& iconUrl) {
QNetworkRequest request;
request.setUrl(QUrl(iconUrl));
QNetworkReply* reply = this->m_networkManager->get(request);
bool result = connect(reply, SIGNAL(finished()), this,
SLOT(onWeatherIconRequestFinished()));
Q_ASSERT(result);
}
You should be quite familiar by now with the code shown in Listing 7-22. An HTTP
request for downloading the image is created and submitted to the network access manager. The interesting part of the code is located in Listing 7-23, which handles the HTTP
response.
Listing 7-23. CityInfo::onWeatherIconRequestFinished
void CityInfo::onWeatherIconRequestFinished() {
QNetworkReply* reply = static_cast<QNetworkReply*>(QObject::sender());
if (reply) {
if (reply->error() == QNetworkReply::NoError) {
QByteArray data = reply->readAll();
m_weatherIcon = bb::cascades::Image(bb::utility::ImageConverter::decode(data));
emit weatherIconChanged();
}
reply->deleteLater();
}
}
The code essentially builds a bb::cascades::Image
from the returned data using a bb::utility::ImageConverter
class, and updates the m_weatherIcon
member variable. Note that we also need to emit the weatherIconChanged
signal, which will in turn notify the declarative engine to update the QML binding for the City.weatherImage
property.
The last piece of the puzzle is to access the Image
object as a QVariant from QML using the weatherIcon property (see Listing 7-24).
Listing 7-24. CityInfo::onWeatherIcon( )
QVariant CityInfo::weatherIcon() const {
return QVariant::fromValue(m_weatherIcon);
}
GoogleMapClient
The GoogleMapClient
class generates a static map using the coordinates returned by the Weather Underground service. Here again, the class encapsulates the map generation functionality and exclusively uses properties and signals to communicate with the QML layer. When the GoogleMapClient::setCoordinates()
slot is called, a new request to the Google Maps service is sent. If you look at the WeatherMap
control’s onCreationCompleted
slot, you will notice that the CityInfo::coordinatesChanged()
signal is connected to the GoogleMapClient::setCoordinates()
slot (see Listing 7-12) (in other words, the GoogleMapClient()::setCoordinates()
slot will be called each time the CityInfo
object’s coordinates are updated).
Listing 7-25 shows you the GoogleMapClient::setCoordinates()
slot implementation.
Listing 7-25. CityInfo::setCoordinates( )
void GoogleMapClient::setCoordinates(const QString& latitude,
const QString& longitude, const QString& markerUrl) {
if((m_latitude == latitude) &&
(m_longitude == longitude) &&
(m_markerUrl == markerUrl)) return;
m_latitude = latitude;
m_longitude = longitude;
m_markerUrl = markerUrl;
this->createMap();
}
Finally, the GoogleMapClient::setCoordinates()
method internally calls the GoogleMapClient::createMap()
method, which is responsible for building the network request to the Google Maps service (see Listing 7-26).
Listing 7-26. GoogleMapClient::createMap( )
void GoogleMapClient::createMap() {
QNetworkRequest request;
request.setUrl(QUrl(this->buildUrlString()));
QNetworkReply* reply = this->m_networkManager->get(request);
bool result = connect(reply, SIGNAL(finished()), this, SLOT(onMapReady()));
Q_ASSERT(result);
}
I am going to omit the code for handling the HTTP
response, which is done in GoogleMapClient::onMapReady()
, because it is very similar to WeatherClient::onWeatherIconRequesFinished()
(shown in Listing 7-23). (In retrospect, we could have designed a common base class implementing the image download logic. This is something you could try to refactor.)
The request URL is built with a call to GoogleMapClient::buildUrlString()
(see Listing 7-27).
Listing 7-27. GoogleMapClient::buildUrlString( )
QString GoogleMapClient::buildUrlString() {
QString cityMapUrl("
http://maps.googleapis.com/maps/api/staticmap?center
=");
cityMapUrl.append(m_latitude);
cityMapUrl.append(",");
cityMapUrl.append(m_longitude);
cityMapUrl.append("&");
cityMapUrl.append("zoom=7&size=640x640&sensor=false&");
cityMapUrl.append("maptype=hybrid&");
cityMapUrl.append("markers=");
cityMapUrl.append("icon:");
cityMapUrl.append(m_markerUrl);
cityMapUrl.append("|");
cityMapUrl.append(m_latitude);
cityMapUrl.append(",");
cityMapUrl.append(m_longitude);
cityMapUrl.append("|");
cityMapUrl.append("scale=2");
return cityMapUrl;
}
The code shown in Listing 7-27 essentially creates a new request for a map centered on the m_latitude
and m_longitude
coordinates. The marker parameter for indicating the coordinates is defined as the URL
of the icon returned by the Weather Underground service. (If you specify an image URL as a marker, Google Maps will add it as a marker on your map. By default, when no markers are specified, Google will use its own for the coordinates). This illustrates how you can combine, in practice, multiple services in your own app (we could say that we have built a mashable app).
ApplicationUI
As usual for Cascades applications, the application delegate ties everything together and provides you the access point for the WeatherClient
and CityInfo
instances (note that the delegate itself is set as a QML document context property; see Listings 7-28 and 7-29).
Listing 7-28. ApplicationUI Definition
class ApplicationUI : public QObject
{
Q_OBJECT
Q_PROPERTY(WeatherClient* weather READ weatherClient CONSTANT)
public:
ApplicationUI(bb::cascades::Application *app);
virtual ∼ApplicationUI() { }
private:
WeatherClient* weatherClient();
WeatherClient* m_weatherClient;
};
Listing 7-29. ApplicationUI Constructor
ApplicationUI::ApplicationUI(bb::cascades::Application *app) :
QObject(app), m_weatherClient(new WeatherClient(this)) {
// Create scene document from main.qml asset, the parent is set
// to ensure the document gets destroyed properly at shut down.
QmlDocument *qml = QmlDocument::create("asset:///main.qml").parent(this);
qml->documentContext()->setContextProperty("_app", this);
// Create root object for the UI
AbstractPane *root = qml->createRootObject<AbstractPane>();
// Set created root object as the application scene
app->setScene(root);
}
Finally, to make the WeatherClient
, CityInfo
and GoogleMapClient
classes available as new QML types, you need to register them with the QML type system. This is done in the application’s main
function (see Listing 7-30).
Listing 7-30. main.cpp
Q_DECL_EXPORT int main(int argc, char **argv)
{
qmlRegisterType<CityInfo>("ludin.utils", 1, 0, "CityInfo");
qmlRegisterType<WeatherClient>("ludin.utils", 1, 0, "WeatherClient");
qmlRegisterType<GoogleMapClient>("ludin.utils", 1, 0, "GoogleMapClient");
Application app(argc, argv);
// Create the Application UI object, this is where the main.qml file
// is loaded and the application scene is set.
new ApplicationUI(&app);
// Enter the application main event loop.
return Application::exec();
}
The first two calls to qmlRegisterType()
are required because you are using CityInfo
and WeatherClient
as properties accessible from QML. The last call is required so that you can define the GoogleMapClient
class as an attached object in the WeatherMap
control. (You also need to add the import ludin.utils 1.0
statement at the start of your QML document; see the WeatherMap
control in Listing 7-12.)