Bonjour,
Dans ce nouvel article, je vais vous exposer un certain nombre de fonctionnalités en Qt/QML autour de la géolocalisation sur Symbian^3 / Anna / Belle :
- Afficher une carte ovi centrée sur un lieu donné
- Ajouter des points d'intérêts
- Interagir avec les points d'intérêts
- Récupérer les points d'intérêts depuis un flux XML
- Gestion du panning utilisateur
Mon environnement utilisé est le SDK Qt 1.1.4 (Qt 4.7.4 / Qt Mobility 1.2.1) sous Windows 7 et l'application développée a été validée sur Nokia E7 upgradé en Anna.
Le but de l'application est d'afficher sur une carte les stations de vélos sur Paris, avec pour chaque station, le nom et le numéro identifiant. Cet article s'inspire directement d'un
wiki disponible sur le site de développeurs nokia. Les données sur les stations sont accessibles via un flux XML public disponible
ici : comme vous pouvez le constater, il contient une liste de marqueurs :
<marker name="00901 - STATION MOBILE 1" number="901" address="ALLEE DU BELVEDERE PARIS 19 - 0 75000 Paris -" fullAddress="ALLEE DU BELVEDERE PARIS 19 - 0 75000 Paris - 75000 PARIS" lat="48.892745582406675" lng="2.391255159886939" open="1" bonus="0"/>
Dans notre exemple nous nous intéresserons aux champs suivants :
- name : nom de la station
- lat : latitude de la station
- lng : longitude de la station
Je tiens à rappeler que cet article est à usage uniquement pédagogique pour comprendre le fonctionnement des apis Qt/QML de géolocalisation ; l'application peut donc être à tout moment impactée en cas de maintenance de ce service.
Voici une capture représentant le rendu final :
Le picto "Nokia" représente les bureaux de Nokia France et le rectangle gris affiche le nom et le numéro de la station qui a été cliquée par l'utilisateur.
Pour afficher une carte en QML, il est tentant d'utiliser l'élément QML Map : malheureusement cet élément est très limité notamment dans les versions 1.x de Qt Mobility. Or Qt Mobility 1.2 n'étant pas encore déployée sur les terminaux Symbian Anna, je vous propose ici une méthode compatible avec toutes les versions : l'astuce consiste à définir un élément QML Map en C++. Ce nouvel élément personnalisé sera exposé depuis le C++ vers QML via l'appel classique suivant :
qmlRegisterType<MapWidget>("mywidgets", 1, 0, "MyCustomMap");
La classe C++ implémentant l'objet carte personnalisé se nomme
MapWidget : nous aurons l'occasion de la détailler largement par la suite. Depuis un fichier QML, il faudra alors importer de manière classique la version 1.0 du module "mywidgets" :
import mywidgets 1.0
Et dans les faits, l'instantiation de notre carte personnalisée se fera classiquement ainsi :
MyCustomMap {
id: map
anchors.fill: parent
centerLatitude: 48.9018112
centerLongitude: 2.3748493
zoomLevel:15
...
}
Rentrons maintenant dans le vif du sujet ...
Partie C++
J'ai défini une classe dédiée à la gestion de la carte dénommée
MapWidget qui dérive directement de la classe
QGraphicsGeoMap disponible dans Qt Mobility. Et voici le fichier header :
class MapWidget : public QGraphicsGeoMap
{
Q_OBJECT
Q_PROPERTY(int zoomLevel WRITE setCustomZoomLevel)
Q_PROPERTY(double centerLatitude READ centerLatitude WRITE setCenterLatitude)
Q_PROPERTY(double centerLongitude READ centerLongitude WRITE setCenterLongitude)
Q_PROPERTY(bool isQtm12 READ isQtm12)
public:
MapWidget();
~MapWidget();
void setCustomZoomLevel(int aLevel);
double centerLatitude() const;
void setCenterLatitude(double lat);
double centerLongitude() const;
void setCenterLongitude(double lon);
bool isQtm12() const;
Q_INVOKABLE void addNokiaPoi();
Q_INVOKABLE void addPoi(double lat, double lon, int uuid);
Q_INVOKABLE void addPoiGroup();
signals:
void poiClicked(QString uuid, double posX, double posY);
void poiHidden();
protected:
void mousePressEvent(QGraphicsSceneMouseEvent* event);
void mouseReleaseEvent(QGraphicsSceneMouseEvent* event);
void mouseMoveEvent(QGraphicsSceneMouseEvent* event);
private:
static QGeoMappingManager* createManager();
QPointF lastPos;
QList<QGeoMapPixmapObject*> myObjectsList;
QGeoMapGroupObject *poiGroup;
bool m_isQtm12;
};
Cette classe exposera 4 propriétés au monde QML :
- zoomLevel : le niveau de zoom : c'est une valeur décimale (qreal) dont le range dépend de la plateforme. Sur Symbian il va de 0.0 à 20.0. Il existe des bornes définies sous la forme de constante (minimumZoomLevel et maximumZoomLevel)
- centerLatitude : la latitude du centre de la carte
- centerLongitude : la longitude du centre de la carte
- isQtm12 : booléen indiquant si Qt Mobility 1.2.x est installée sur le smartphone (nécessaire pour contourner une régression présente depuis la version 1.2)
La classe s'accompagne donc des méthodes de lecture/écriture associées à ces propriétés :
// Indique si la version courante de Qt Mobility est 1.2.xbool MapWidget::isQtm12() const
{
return m_isQtm12;
}
// Méthode publique permettant d'ajuster le zoom depuis QML
void MapWidget::setCustomZoomLevel(int aLevel)
{
setZoomLevel(aLevel);
}
// Méthode publique pour récupérer la latitude du point surlequel est centrée la carte
double MapWidget::centerLatitude() const
{
return center().latitude();
}
// Méthode publique pour récupérer la longitude du point surlequel est centrée la carte
double MapWidget::centerLongitude() const
{
return center().longitude();
}
// Méthode publique pour modifier la latitude du point surlequel est centrée la carte
void MapWidget::setCenterLatitude(double lat)
{
QGeoCoordinate c = center();
c.setLatitude(lat);
setCenter(c);
}
// Méthode publique pour modifier la longitude du point surlequel est centrée la carte
void MapWidget::setCenterLongitude(double lon)
{
QGeoCoordinate c = center();
c.setLongitude(lon);
setCenter(c);
}
Le constructeur du widget, outre la détection de version de Qt Mobility, fait appel au constructeur parent QGraphicsGeoMap en lui passant une instance QGeoMappingManager crée par une méthode privée de type singleton "createManager" :
MapWidget::MapWidget() :
QGraphicsGeoMap(createManager())
{
m_isQtm12 = detectQTMVersion();
setCenter(QGeoCoordinate(48.89, 2.37));
setZoomLevel(7);
if(m_isQtm12) poiGroup = new QGeoMapGroupObject();
}
La méthode createManager instancie le plugin de géolocalisation "nokia" et renvoie l'instance de QGeoMappingManager associée à l'instance du plugin.
QGeoMappingManager* MapWidget::createManager
()
{
/*QMap<QString, QVariant> parameters;
parameters["app_id"] = "APPID";
parameters["token"] = "TOKEN";*/
// Utilisation du plugin de localisation nokia
QGeoServiceProvider* serviceProvider = new QGeoServiceProvider("nokia" /*, parameters*/);
// Récupération du gestionnaire graphique de carte pour gérer les intéractions
QGeoMappingManager* mappingManager = serviceProvider->mappingManager();
return mappingManager;
}
Note : depuis peu, il est nécessaire de s'enregistrer pour obtenir un jeton (token) afin d'utiliser ce plugin de géolocalisation
Ensuite elle déclare trois méthodes invocables depuis QML :
- addNokiaPoi : pour ajouter sur la carte le point d'intérêt correspondant aux bureaux Nokia France (picto Nokia sur le screenshot)
void MapWidget::addNokiaPoi
()
{
QGeoCoordinate coord(48.9018112, 2.3748493);
QPixmap pixmap = QPixmap(":qml/CustomQmlMap/nokia.png");
QGeoMapPixmapObject *pixMapObject = new QGeoMapPixmapObject(coord, QPoint(-16,-20), pixmap);
addMapObject(pixMapObject);
}
- addPoi : pour ajouter une station de vélo sur la carte. Une station sera vue comme une triplette : longitude / latitude / identifiant. A noter que ce nouveau POI sera mémorisé dans une liste (myObjectsList) en plus d'être ajouté à la carte (ou au groupe de POIs en version QTM 1.2)
void MapWidget::addPoi
(double lat, double lon, int uuid)
{
// Création du point d'intérêt
QGeoCoordinate coord(lat, lon);
QPixmap pixmap = QPixmap(":qml/CustomQmlMap/poi.png");
QGeoMapPixmapObject *pixMapObject = new QGeoMapPixmapObject(coord, QPoint(-16,-20), pixmap);
pixMapObject->setObjectName("poiMarker");
QVariant v(uuid);
pixMapObject->setProperty("uuid", v);
myObjectsList.append(pixMapObject);
if(m_isQtm12)
{
// Utilisation d'un groupe de POIs uniquement pour une version 1.2.x de Qtmobility (bug 1664)
// https://bugreports.qt.nokia.com//browse/QTMOBILITY-1664
poiGroup->addChildObject(pixMapObject);
}
else
addMapObject(pixMapObject);
}
Note : les ressources bitmaps utilisées sont référencées dans un fichier de ressource "images.qrc" dont le chemin relatif est "qml/QmlCustomMap"
- addPoiGroup : pour ajouter un groupement de points d'intérêts sur la carte. Cette méthode est uniquement utilisée pour contourner un bug QTM 1.2
void MapWidget::addPoiGroup
()
{
// Utilisation d'un groupe de POIs uniquement pour une version 1.2.x de Qtmobility (bug 1664)
// https://bugreports.qt.nokia.com//browse/QTMOBILITY-1664
if(poiGroup && m_isQtm12) addMapObject(poiGroup);
}
Cette classe dispose par ailleurs de deux signaux récupérables au niveau QML :
- poiClicked : indiquant que l'utilisateur a cliquer un point d'intérêt particulier. Côté QML cela permettra de dessiner un rectangle gris contenant le nom de la station
- poiHidden : déclenché lorsque l'utilisateur se déplace dans la carte. Côté QML cela permettra de masquer tout rectangle gris précédemment affiché.
Ensuite j'ai surchargé trois gestionnaires d'événements (
mousePressEvent, mouseReleaseEvent et mouseMoveEvent) afin de gérer les interactions utilisateurs avec la carte (principalement le clic sur POI et le pan) :
- mousePressEvent : appelé lorsque l'utilisateur touche l'écran ; j'y mémorise la position (X, Y) dans une propriété de la classe ; cette propriété sera utilisée dans le gestionnaire mouseReleaseEvent afin de déterminer quel POI a été cliqué.
// Mémorisation de la position (cette valeur sera ensuite utilisée dans la méthode mouseReleaseEvent)
void MapWidget::mousePressEvent(QGraphicsSceneMouseEvent* event)
{
lastPos = event->pos();
}
- mouseReleaseEvent : appelé lorsque l'utilisateur retire son doigt de l'écran ; le but de cette méthode est de récupérer les informations du POI que l'utilisateur a ciblé afin de les remonter au niveau QML via l'émission du signal poiClicked :
void MapWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent* event)
{
QPointF releasePos = event->pos();
QPointF diff = lastPos - releasePos;
if (qAbs(diff.x()) > 30 || qAbs(diff.y()) > 30) {
return;
}
int selectedIndex = -1;
double minDiffX = -1;
double minDiffY = -1;
if(myObjectsList.length()>0)
{
// Nous parcourrons la liste des POIs
for(int i=0;i<myObjectsList.length();i++)
{
QGeoMapPixmapObject* obj = myObjectsList[i];
QPointF currentPoiPos = coordinateToScreenPosition(obj->coordinate());
QPointF invalid(0,0);
if(currentPoiPos != invalid)
{
diff = lastPos - currentPoiPos;
double currentDiffX = qAbs(diff.x());
double currentDiffY = qAbs(diff.y());
// Nous mémorisons l'index du POI le plus proche du doigt de l'utilisateur
if(selectedIndex == -1)
{
selectedIndex = i;
minDiffX = currentDiffX;
minDiffY = currentDiffY;
}
else
{
if(minDiffX>=currentDiffX && minDiffY>=currentDiffY)
{
selectedIndex = i;
minDiffX = currentDiffX;
minDiffY = currentDiffY;
}
}
}
}
if(selectedIndex != -1)
{
// Nous récupérons les propriétés du marker correspondant (uuid, position X, position Y) et émettons un signal
QGeoMapPixmapObject* obj = myObjectsList[selectedIndex];
if (obj->objectName() == "poiMarker")
{
QString uuid = obj->property("uuid").toString();
qDebug() << "Emit poiClicked signal with id : " << uuid;
emit poiClicked(uuid, releasePos.x(), releasePos.y());
}
}
}
}
- mouseMouveEvent : appelé lorsque l'utilisateur glisse son doigt sur l'écran ; cette méthode a deux objectifs : déplacer visuellement la carte via l'appel à QGraphicsGeoMap::pan et émettre le signal poiHidden afin de masquer le détail du POI courant qui pourrait potentiellement être affiché dans un rectangle gris en overlay côté QML.
void MapWidget::mouseMoveEvent(QGraphicsSceneMouseEvent* event)
{
QPoint lastPos = event->lastPos().toPoint();
QPoint pos = event->pos().toPoint();
int dx = lastPos.x() - pos.x();
int dy = lastPos.y() - pos.y();
pan(dx, dy);
emit poiHidden();
}
Enfin j'ai défini quelques propriétés privées déjà plus au moins décrites plus haut :
- lastPos : position (X, Y) correspondant à l'endroit où l'utilisateur a toucher l'écran. Cette variable est positionné dans le gestionnaire mousePressEvent
- myObjectsList : contient la liste des POIs (objet de type QGeoMapPixmapObject) ajoutés sur la carte
- poiGroup : groupement de POIs (objet de type QGeoMapGroupObject) contenant tous les POIs. Utilisé avec QTM 1.2 seulement.
Note : pour QTM 1.2 nous ajoutons tous les POIs dans un groupe afin d'ajouter ce groupe à la carte en un seul appel (addPoiGroup) ce qui déclenche un seul "redraw" de la carte. Depuis QTM 1.2, le fait d'ajouter un POI directement dans la carte déclenche un "redraw" complet de la carte (et non de la zone du POI) : comme il y a 1200 POIs, les performances deviennent catastrophiques d'où l'ajout dans un groupe (ce qui ne déclenche pas de "redraw") qui n'est ajouté à la carte qu'une fois.
Partie QML
Maintenant que vous avez une bonne idée de l'implémentation de notre widget de cartographie personnalisé, voyons comment l'utiliser depuis QML. Ayant crée le projet avec le wizard "Application Qt Quick", nous nous retrouvons avec deux fichiers QML : main.qml et MainPage.qml. N'ayant pas modifier le fichier "main.qml" je ne décrirai que le second fichier MainPage.qml.
Commençons par les règles d'importation :
import QtQuick 1.1import QtMobility.location 1.2
import com.nokia.symbian 1.1 // Symbian components
import mywidgets 1.0
Pensez à référencer le module mywidgets afin d'utiliser notre élément personnalisé QmlCustomMap.
La page principale contient tout d'abord une partie pour gérer l'indicateur de chargement :
Page {
id: rootWindow
width: 360
height: 640
// Loading screen
Rectangle {
id:loadingScreen
anchors.fill: parent
opacity: 0.7
color: "white"
visible: true
property alias loadingTxt: bgText.text
state: "showLoadingScreen"
states: [
State {
name: "showLoadingScreen"
PropertyChanges {
target: loadingScreen
visible:true
z:1
}
},
State {
name: "hideLoadingScreen"
PropertyChanges {
target: loadingScreen
visible:false
z:0
}
}
]
Text{
id: bgText
anchors.centerIn: parent
color: "black"
text: "Chargement de la carte"
}
BusyIndicator {
id: indicator
width:40
height:40
anchors.verticalCenter: parent.verticalCenter
anchors.right: bgText.left
anchors.rightMargin: 20
running: true
}
}
Plus important : la création du model de données récupérés initialisée avec un flux XML :
XmlListModel {
id: stations
source: http://www.velib.paris.fr/service/carto
query: "/carto/markers/marker"
XmlRole {name: "number"; query: "@number/string()"}
XmlRole {name: "name"; query: "@name/string()"}
XmlRole {name: "lat"; query: "@lat/string()"}
XmlRole {name: "lng"; query: "@lng/string()"}
onStatusChanged: {
if(status == XmlListModel.Ready) {
console.log("Model Status: ready")
var i = 0
for(i=0;i<stations.count;i++)
{
map.addPoi(stations.get(i).lat, stations.get(i).lng, parseInt(stations.get(i).number));
}
console.log("No more POIs to add")
// Utilisation d'un groupe de POIs uniquement pour une version 1.2.x de Qtmobility (bug 1664)
// https://bugreports.qt.nokia.com//browse/QTMOBILITY-1664
if(map.isQtm12) map.addPoiGroup()
// Ajout du bureau Nokia à Paris
map.addNokiaPoi()
// On masque la mire de chargement
loadingScreen.state = "hideLoadingScreen"
loadingScreen.loadingTxt = ""
}
else if(status == XmlListModel.Error) {
console.log("Model Status: error")
}
else if(status == XmlListModel.Loading) {
console.log("Model Status: loading")
loadingScreen.loadingTxt = "Chargement des POIs"
}
}
}
Le parsing des données XML est un parsing de type XPath (voir
documentation pour plus d'info). C'est très simple à utiliser. Dans notre cas je récupère pour chaque "marker" (i.e. POI) les valeurs de balise "number", "name", "lat" et "lng". A réception des données (c'est à dire quand le status passe à l'état "XmlListModel.Ready"), j'ajoute chacun des POIs sur la carte.
Maintenant je vais créer une instance de l'élément personnalisé QmlCustomMap centré sur les bureaux de Nokia France avec un zoom élevé:
MyCustomMap {
id: map
anchors.fill: parent
centerLatitude: 48.9018112
centerLongitude: 2.3748493
zoomLevel:15
onPoiClicked: {
var i = 0
for(i=0;i<stations.count;i++)
{
if(stations.get(i).number == uuid)
{
poiRect.x = posX-85
poiRect.y = posY-44
poiRect.poiTxt = stations.get(i).name
poiRect.visible = true
break;
}
}
}
onPoiHidden: {
poiRect.visible = false
}
Rectangle {
property alias poiTxt: poiTextElement.text
id: poiRect
visible:false
width:170
height:75
color: "darkgrey"
radius:10
opacity: 0.95
Text {
id:poiTextElement
color:"white"
font.bold: true
font.pointSize: 6
anchors.centerIn: parent
text:""
width:parent.width
wrapMode:Text.WordWrap
}
MouseArea {
anchors.fill: parent
onClicked: {
if(poiRect.visible) poiRect.visible = false
}
}
}
}
Le rectangle identifié par "poiRect" est masqué par défaut et affiché uniquement lorsque l'utilisateur clique sur un POI : il consiste en un rectangle gris arrondi contenant l'identifiant et le nom de la station. Vous pouvez tout à fait utiliser une image à la place. Je vais donc jouer sur la propriété "visible" de cet élément : pour cela je mets en place les gestionnaires de signaux
onPoiClicked et
onPoiHidden.
Et voilà c'est fini : à titre d'exercice je vous invite à afficher plus d'information sur un POI donné en utilisant le second service XML :
http://www.velib.paris.fr/service/stationdetails/ + number
Le code source complet est disponible
ici