I. L'article original

Qt Quarterly est une revue trimestrielle électronique proposée par Nokia à destination des développeurs et utilisateurs de Qt. Vous pouvez trouver les versions originales.

Nokia, Qt, Qt Quarterly et leurs logos sont des marques déposées de Nokia Corporation en Finlande et/ou dans les autres pays. Les autres marques déposées sont détenues par leurs propriétaires respectifs.

Cet article est la traduction de l'article Location and Mapping Services de David Boddie paru dans la Qt Quarterly Issue 35.

Cet article est une traduction de l'un des tutoriels en anglais écrits par Nokia Corporation and/or its subsidiary(-ies), inclus dans la documentation de Qt. Les éventuels problèmes résultant d'une mauvaise traduction ne sont pas imputables à Nokia.

II. Introduction

Avec la récente sortie de Qt Mobility 1.1, les développeurs ont à leur disposition un nouvel ensemble de classes pour la cartographie et un plug-in pour le service Ovi Maps de Nokia, rendant aisée l'exploration du monde depuis son confortable bureau.

Dans cet article, on va regarder les capacités de base de cartographie de Qt Mobility 1.1, en l'utilisant pour créer des cartes, les annoter et explorer des concepts liés comme les points de repère.

Le code source de l'article.

Image non disponible Image non disponible Image non disponible

III. Trouver un point de départ

QtLocation, un des modules de QtMobility, vient avec une collection de classes liées à divers aspects de la localisation. On y retrouve des classes liées au matériel, comme QGeoPositionInfoSource, qui fournit des informations sur la position de l'utilisateur à l'aide d'un GPS ou un autre périphérique, ou QGeoSatelliteInfoSource, utilisée pour obtenir des informations sur les satellites de positionnement. Bien qu'il soit intéressant de les exploiter, on les laissera pour les développeurs curieux.

Le module inclut aussi un certain nombre de classes générales pour décrire les informations de localisation. QGeoCoordinate contient les latitude, longitude et altitude d'une position sur le globe et est utilisée partout dans l'API QtLocation. QGeoAddress et QGeoPlace fournissent des représentations d'informations de plus haut niveau sur ces positions. Des endroits pourraient aussi correspondre à des points de repère, ils seront explorés plus loin. Il y a aussi des classes pour représenter des aires géographiques, d'une manière abstraite comme QGeoBoundingArea ou plus précisément avec QGeoBoundingBox ou QGeoBoundingCircle.

Cependant, les classes les plus intéressantes dans le cadre de cet article sont celles qui accèdent et affichent les données des cartes. La classe principale est QGraphicsGeoMap, prête pour utilisation dans une QGraphicsScene.

IV. Cartes et services

Avant de démarrer avec les cartes, scènes et vues, on doit tout d'abord trouver une source de données géographiques. La manière générale de procéder est d'utiliser QGeoServiceProvider pour trouver quels services sont disponibles.

 
Sélectionnez
// Des en-têtes sont nécessaires, voir l'exemple complet. 
using namespace QtMobility;

int main(int argc, char *argv[])
{
	QApplication app(argc, argv);
	QStringList services = QGeoServiceProvider::availableServiceProviders();
	
	if (services.isEmpty()) {
	    // Tenir compte de la situation. 
	}

Si aucun service n'est disponible, la QStringList sera vide, les développeurs doivent donc gérer ce cas dans leurs applications et agir en conséquence.

Avec l'hypothèse qu'un service est disponible, on peut construire une instance de QGeoServiceProvider et l'utiliser pour accéder aux fonctionnalités cartographiques. Par défaut, il n'y a pas de support intégré pour des fournisseurs de service dans le module QtLocation : cette responsabilité est déléguée aux plug-ins. Dans le code suivant, on se basera sur le fait qu'un service nokia est disponible, correspondant au service Ovi Maps de Nokia, puisque ce plug-in est fourni dans les compilations par défaut de QtMobility :

 
Sélectionnez
QGeoServiceProvider service("nokia");
if (service.error() != service.NoError) {
    // Tenir compte de la situation. 
}

On devrait aussi couvrir le cas où le plug-in n'est pas disponible, possiblement en fermant ou en demandant à l'utilisateur de choisir un autre fournisseur de services.

Dès qu'un fournisseur a été obtenu, on peut lui demander une instance de QGeoMappingManager, qui permettra de récupérer les images :

 
Sélectionnez
QGeoMappingManager *manager = service.mappingManager();

Tous les fournisseurs ne proposent pas ces fonctionnalités, certains pourraient ne mettre à disposition que la recherche ou le routage, il faut donc bien s'assurer que l'instance obtenue est utilisable. Cependant, dans cet exemple simple, on sait que le fournisseur nokia supporte la cartographie.

La même procédure est utilisée pour la recherche et le routage. Dès qu'on a un fournisseur, on peut appeler ses méthodes searchManager() et routingManager() pour accéder aux gestionnaires.

V. Créer la scène

Avec un gestionnaire de carte en place, on peut télécharger des données cartographiques du service et les présenter à l'utilisateur. Heureusement, on ne doit pas réfléchir de trop pour trouver un moyen de les afficher, car le module QtLocation vient avec un item QGraphicsGeoMap, prêt à être utilisé avec QGraphicsScene. Tout ce que l'on doit faire est de créer une scène et une vue ordinaires et insérer un de ces items :

 
Sélectionnez
QGraphicsScene scene;
QGraphicsView view;
view.setScene(&scene);
 
QGraphicsGeoMap *geoMap = new QGraphicsGeoMap(manager);
scene.addItem(geoMap);

Le gestionnaire de cartes est passé à l'item lors de sa construction, lui permettant d'accéder au service sans autre code. On doit d'abord lui donner une taille, lui dire l'endroit que l'on veut afficher et définir un niveau de zoom :

 
Sélectionnez
    geoMap->resize(300, 300);
    geoMap->setCenter(QGeoCoordinate(52.75, -2.75));
    geoMap->setMapType(QGraphicsGeoMap::TerrainMap);
    geoMap->setZoomLevel(10);
 
    view.show();
    return app.exec();
}
Image non disponible

Cette configuration produit une carte carrée contenant un plan de rue. Le niveau de zoom a été choisi avec soin pour montrer les alentours d'une ville (les valeurs doivent être comprises entre minimumZoomLevel et maximumZoomLevel, qui ont 0 et 18 pour valeur pour le service nokia). Au niveau 0, tout le globe est affiché.

Le code source complet pour cet exemple est inclus dans le dossier graphicsmap de l'archive d'exemples de cet article.

Puisqu'on utilise un item de la vue graphique pour obtenir les images des cartes, on pourrait s'attendre à une extension de cet exemple pour autoriser le défilement. Cependant, cet item est vraiment prévu pour être utilisé comme une zone d'affichage sur une carte plus grande sur laquelle l'utilisateur navigue en touchant et déplaçant l'item. Pour cette raison, il inclut le slot pan(), utilisé pour défiler la carte par distances mesurées à l'aide des coordonnées du widget. On va jeter un coup d'œuil bref aux interactions avec l'utilisateur plus loin dans l'article.

VI. Annoter les cartes

Une des choses que l'on peut vouloir faire avec les cartes obtenues par le service est de les annoter avec des informations utiles pour l'utilisateur, possiblement en utilisant les données d'un autre service. À première vue, il appert qu'il faudrait réfléchir en termes de latitude, longitude et coordonnées de widget. QGraphicsGeoMap fournit des fonctions à cet effet. Cependant, pour de simples labels de texte et formes, on peut utiliser des classes préfabriquées basées sur QGeoMapObject, qui fonctionnent avec des positions spécifiées avec QGeoCoordinate.

 
Sélectionnez
QGraphicsGeoMap *geoMap = new QGraphicsGeoMap(manager);

Avec l'item de la carte défini, on peut placer un label textuel et un cercle sur la carte pour fournir des informations supplémentaires sur un endroit. Pour ces deux objets de carte, on spécifie l'emplacement avec QGeoCoordinate ; pour le cercle, on fournit également un rayon.

 
Sélectionnez
QFont font;
font.setWeight(QFont::Bold);
QGeoMapTextObject *textObj = new QGeoMapTextObject(QGeoCoordinate(46.274, 6.065),
    "Large Hadron Collider\n(approximate location)", font, QPoint(0, 0));
textObj->setPen(QPen(Qt::NoPen));
textObj->setBrush(QBrush(Qt::white));
geoMap->addMapObject(textObj);
 
QGeoMapCircleObject *circleObj = new QGeoMapCircleObject(QGeoCoordinate(46.274, 6.065), 4297.183);
circleObj->setPen(QPen(Qt::white, 2, Qt::DotLine));
circleObj->setBrush(QBrush(Qt::NoBrush));
geoMap->addMapObject(circleObj);

Les objets ajoutés à la carte ne sont pas des objets graphiques usuels, les possibilités d'interaction avec l'utilisateur sont donc limitées. Cependant, ils fournissent une fonctionnalité de sélection basique. Par exemple, leurs positions sont automatiquement mises à jour si la carte est déplacée et on peut contrôler leurs visibilité et position en utilisant les signaux selectedChanged() et visibilityChanged().

Comme avec les items graphiques, chaque type spécialisé d'objet de carte fournit aussi ses propres propriétés et fonctions pour en faciliter l'usage. Les objets de carte peuvent aussi être retirés de l'item carte.

Le code ci-dessus fait partie d'un exemple complet qui peut être trouvé dans le dossier graphicsmap de l'archive d'exemples de cet article.

Image non disponible

VII. Points de repère

Quand on annote les cartes avec des points intéressants, il devient vite nécessaire de trouver une manière de lister ces emplacements. On pourrait vouloir créer son propre mécanisme de stockage des emplacements, mais le module QtLocation fournit une API que l'on peut utiliser pour stocker et récupérer des emplacements, des points de repère (landmarks), même les catégoriser.

On y accède par une instance de QLandmarkManager, une classe responsable du stockage et de la récupération d'instances de QLandmark. Ces points de repère, eux-mêmes, peuvent contenir plus d'un emplacement. Ils contiennent aussi des informations sur le point lui-même, comme son nom ou une description. On peut les créer de zéro et les donner au gestionnaire, mais on les récupère la majorité du temps de sources externes.

Pour commencer avec ces points de repère, on commence par construire un gestionnaire :

 
Sélectionnez
QLandmarkManager *landmarkManager = new QLandmarkManager();

Il est intéressant de remarquer que ce gestionnaire fournit une interface pour une base de données persistante partagée entre les applications. Ainsi, l'ajout d'un nouveau point à une instance du gestionnaire le rendra visible à toutes les applications qui utilisent cette API. Cela signifie aussi qu'il n'est pas nécessaire d'importer une liste de points à chaque construction d'instance de QLandmarkManager.

Le bout de code suivant de l'exemple landmarks qui accompagne cet article montre la manière la plus simple d'obtenir des points individuels du gestionnaire, en retirant d'abord tout objet de carte préexistant de l'item de la carte avant d'itérer sur les points disponibles, en créant un objet QGeoMapPixmapObject pour chaque point de repère en base de données :

 
Sélectionnez
void MapWindow::updateLandmarks()
{
    landmarkCombo->clear();
    foreach (QGeoMapObject *obj, geoMap->mapObjects())
        geoMap->removeMapObject(obj);
 
    foreach (QLandmark landmark, landmarkManager->landmarks()) {
 
        landmarkCombo->addItem(landmark.name());
        QGeoMapPixmapObject *pixmapObj = new QGeoMapPixmapObject();
        pixmapObj->setCoordinate(landmark.coordinate());
        pixmapObj->setOffset(QPoint(-2, -30));
        pixmapObj->setPixmap(QPixmap(":Resources/landmark.png"));
        geoMap->addMapObject(pixmapObj);
    }
}

Ces points peuvent être ajoutés au gestionnaire et sauvegardés pour usage ultérieur avec la fonction saveLandmark() pour des points individuels ou saveLandmarks() pour une collection. Dans cet exemple, on importe des collections de points depuis des fichiers, en utilisant la fonction importLandmarks() :

 
Sélectionnez
void MapWindow::importLandmarks()
{
	// On commence par récupérer un nom de fichier avec une boîte de dialogue. 

    QFile file(path);
    if (!landmarkManager->importLandmarks(&file))
        QMessageBox::warning(this, tr("Landmarks"),
            tr("Failed to import landmarks: \"%1\"").arg(landmarkManager->errorString()));
    else
        updateLandmarks();
}

Certains formats de fichier de données sont supportés par QtMobility 1.1, dont GPX et LMX. QLandmarkManager importe tous les points qu'il peut trouver de ces fichiers, ainsi toute erreur qui est rapportée correspondra à la dernière erreur arrivée. En réalité, on devrait utiliser la fonction errorMap() pour obtenir un ensemble d'erreurs et construire un message lisible par l'utilisateur à lui afficher.

Le code source complet pour cet exemple est inclus dans le dossier landmarks de l'archive d'exemples de cet article.

VIII. Se déplacer sur la carte

Comme précédemment mentionné, l'item carte utilisé pour afficher des images du service de cartographie est prévu pour être utilisé comme afficheur sur une carte, bien qu'il puisse être utilisé pour fournir des tuiles statiques pour une scène plus grande. Dans cette section, on va regarder un widget qui laisse l'utilisateur naviguer en faisant glisser la carte.

Les développeurs qui ont travaillé avec les événements de la souris précédemment vont probablement savoir ce qui doit être fait pour s'assurer qu'un tel widget fonctionne, les détails ne seront donc pas ici. Cependant, on peut voir le mécanisme de base en regardant les fonctions de gestion d'événements mousePressEvent() et mouseMoveEvent().

 
Sélectionnez
void MapWidget::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        dragging = true;
        dragStartPosition = event->pos();
        setCursor(Qt::ClosedHandCursor);
        event->accept();
    }
}

Gérer un appui de souris sur le widget est simple parce qu'on ne veut que supporter le déplacement. Quand le bouton gauche est enfoncé, on considère que la carte va être déplacée, on définit donc un drapeau en interne, enregistre la position de la souris, change la forme du curseur et accepte l'événement. Les autres boutons de la souris sont ignorés.

 
Sélectionnez
void MapWidget::mouseMoveEvent(QMouseEvent *event)
{
    if (dragging) {
        QPoint v = event->pos() - dragStartPosition;
        geoMap->pan(-v.x(), -v.y());
        dragStartPosition = event->pos();
        event->accept();
    }
}

On gère les mouvements dans le gestionnaire d'événements des mouvements de la souris. Les changements dans les coordonnées x et y de la souris sont opposés et passés à la fonction pan() de la carte, car un déplacement de la carte dans une direction requiert que l'origine de la carte soit déplacée dans la direction opposée.

Le code source complet de cet exemple est inclus dans le dossier mapwidget de l'archive d'exemples de cet article.

Image non disponible

IX. Cartes déclaratives

Le module QtLocation inclut aussi le support de QML, en fournissant un certain nombre d'éléments préfabriqués dans le plug-in Location QML. Ils couvrent les mêmes fonctionnalités que les classes C++, mais exposent des API plus adaptées à un langage déclaratif.

Pour afficher simplement des cartes, on peut utiliser l'item Map du module QtMobility.location. On le configure comme suit :

 
Sélectionnez
import QtQuick 1.0
import QtMobility.location 1.1
 
Item {
    width: 400; height: 400
 
    Map {
        anchors.fill: parent
        zoomLevel: 7
        center: Coordinate {
            latitude: 51; longitude: 4
        }
        plugin: Plugin {
            name: "nokia"
        }
    }
}

Comme le code C++ qui utilise la classe QGraphicsGeoMap, on a besoin d'une manière de sélectionner le plug-in de service que l'on veut utiliser, ce que l'on fait avec l'élément Plugin (défini, dans cet exemple, au fournisseur nokia). Au final, le code QML ci-dessus peut être lancé dans qmlviewer sans autre configuration.

Image non disponible

Créer un item QML déplaçable comme le widget de la carte en C++ décrit plus tôt est plus simple que ce que l'on pourrait imaginer. Les seuls changements à faire sont de donner à l'item Map un identifiant, pour que l'endroit montré puisse être changé, et de déclarer un item MouseArea en enfant de la racine Item dans l'interface utilisateur, en utilisant des ancres pour couvrir la même zone de l'écran que l'item Map.

 
Sélectionnez
MouseArea {
    id: area
    anchors.fill: parent
    property real startX: 0
    property real startY: 0
 
    onPressed: {
        startX = mouse.x
        startY = mouse.y
    }
 
    onPositionChanged: {
        var dx = mouse.x - startX
        var dy = mouse.y - startY
        var pos = map.toScreenPosition(map.center)
        pos.x -= dx
        pos.y -= dy
        map.center = map.toCoordinate(pos)
        startX = mouse.x
        startY = mouse.y
    }
}

Comme pour le widget C++, la zone souris gère les clics de souris et met à jour l'item Map en conséquence.

Image non disponible

QML est prévu pour créer rapidement des interfaces utilisateur, on peut facilement ajouter des contrôles simples et familiers à la carte, mais cela nous amène dans la zone de la programmation avec QML, loin des cartes. Le support de la cartographie en utilisant QML est documenté comme incomplet dans QtMobility 1.1, avec des raffinements supplémentaires prévus pour les versions à venir. Pendant ce temps, il y a suffisamment d'items à essayer !

Trois exemples QML sont inclus dans le répertoire QMLde l'archive d'exemples de cet article. map-with-slider.qml utilise les contrôles simples mentionnés ci-dessus.

X. Conclusions

Dans la quête de la révélation des fonctionnalités cartographiques de QtMobility, on n'a fait qu'effleurer la surface du module QtLocation. Par exemple, bien que l'on a mentionné l'utilisation des objets de carte pour les annotations, on n'a pas regardé les chemins et la manière de les représenter ou l'utilisation de la recherche et des services de routage. De même, on n'a couvert que les utilisations les plus simples des points de repère, sans même commencer à explorer les concepts de catégorie et de filtre.

Une autre zone d'intérêt est l'extensibilité du moteur de cartographie lui-même. Toutes les classes qui décrivent l'interface utilisée pour accéder aux services sont exposées, ce qui signifie que les plug-ins pour des services alternatifs peuvent aussi être écrits de la même manière que le plug-in par défaut nokia.

XI. Divers

Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à Nokia pour nous avoir autorisés à traduire cet article !

Merci à Claude Leloup pour sa relecture !