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
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
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)
// Indique si la version courante de Qt Mobility est 1.2.x
bool 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
- addNokiaPoi : pour ajouter sur la carte le point d'intérêt correspondant aux bureaux Nokia France (picto Nokia sur le screenshot)
{
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)
{
// 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
{
// 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é.
- 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é.
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 :
{
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.
{
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.
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.1
import 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
Aucun commentaire:
Enregistrer un commentaire