mardi 10 janvier 2012

Détecter les changements de position GPS depuis une application hybride Qt/HTML

Bonjour,
Dans cet article, je vais vous présenter une manière simple d'afficher en temps réel la position GPS (latitude et longitude) du téléphone ; cette position sera biensûr mis à jour automatiquement si l'utilisateur se déplace. Il est tentant d'utiliser l'api HTML5 de géolocalisation (navigator.geolocation.watchPosition). Malheurseusement celle-ci n'est pas supportée dans l'implementation Qt Webkit pour Symbian^3 et ses déclinaisons. Je vais donc vous exposer une méthode de contournement répondant à ce besoin.

Tout d'abord je vous invite à créer un projet Qt HTML5 : pour cela cliquer sur "Fichier / Nouveau" sous Qt Creator et sélectionner dans Projets la section "Autre projet" puis "Application HTML5" comme indiqué sur la capture ci-dessous :



Une fois le projet crée, un certain nombre de fichiers a été généré :
  • un ficher html dénommé "index.html" contenant la page web d'entrée de votre application
  • un fichier main.cpp constituant le point d'entrée de l'application Qt
  • un fichier html5applicationviewer.cpp en charge de gérer la webview native utilisée pour piloter votre application web
Pour arriver à mes fins, je vais devoir modifier les fichiers index.html et html5applicationviewer.cpp avec l'idée de créer un objet en C++ faisant le lien entre le monde web (fichier index.html) et le monde natif (fichier html5applicationviewer.cpp). Cet objet natif exposera par le biais de signaux et slots les apis permettant à la partie web de récupérer les notifications de changement de position. Cet objet C++ est défini dans un fichier dénommé cppobject.cpp/.h :

using namespace QtMobility;

class CppObject : public QObject
{
    Q_OBJECT

public:
    CppObject(QWebFrame *frame);

public slots:
    void displayTrace( const QString &param);
    void startWatchingPosition();

signals:
    void positionChanged(double lat, double lng);

private slots:
    void attachObject();
    void positionUpdated(const QGeoPositionInfo &info);

private:
    QWebFrame *m_frame;
    QGeoPositionInfoSource * m_geoPositionInfoSource;

};

Les slots publics représentent les méthodes appelables directement depuis le code html (ici une méthode pour sortir des traces dans la console et une méthode pour s'enregistrer comme listener de changement de position). L'unique signal "positionChanged" sera émis à chaque modification de la position physique du téléphone. Le slot privé "attachObject" permet de déclarer mon objet natif dans le runtime d'éxécution Qt Webkit. Le second slot privé "positionUpdated" est associé à l'api Qt Mobility location afin de récupérer en temps réel la position physique. Enfin cet objet dispose de deux propriétés : "m_frame" représentant la webview dans laquel l'objet C++ est exposé et "m_geoPositionInfoSource" représentant le gestionnaire des mise à jour de position de l'api QTM Location.

Passons maintenant à l'implementation de cet objet :

CppObject::CppObject(QWebFrame *frame)
{
    m_frame = frame;

    if(m_frame)
    {
        attachObject();
        connect( m_frame, SIGNAL(javaScriptWindowObjectCleared()), this, SLOT(attachObject()) );
    }

    mpGeoPositionInfoSource = QGeoPositionInfoSource::createDefaultSource(this);
    mpGeoPositionInfoSource->setPreferredPositioningMethods(QGeoPositionInfoSource::AllPositioningMethods);
    connect(mpGeoPositionInfoSource, SIGNAL(positionUpdated(QGeoPositionInfo)), SLOT(positionUpdated(QGeoPositionInfo)));
}

Je conserve une référence sur la webview parent et je rattache mon instance courante à cet frame web via la méthode privée attachObject :

void CppObject::attachObject()
{
    m_frame->addToJavaScriptWindowObject( QString("CppObject"), this );
}

Ainsi je pourrais utiliser une référence à "CppObject" depuis html. Dans la suite du constructeur, je connecte le signal javascriptWindowObjectCleared à mon slot attachObject. Ceci me permet de garder la référence de mon objet côté javascript y compris après chargement de nouvelles urls. Ensuite je fais appel à l'api QTM Location en utilisant la source de géolocalisation par défaut en indiquant que toutes les méthodes de positionnement peuvent être utilisées. Enfin je connecte un slot privé pour que mon objet soit notifiée dès qu'un changement de position intervient.

Je définis ensuite mes deux slots publics exposables au html :

void CppObject::displayTrace( const QString &param)
{
    qDebug() << param;
}

Simple méthode de sortie console.

void CppObject::startWatchingPosition()
{
    mpGeoPositionInfoSource->startUpdates();
}

Slot public permettant d'initier le monitoring de changement de position.

Enfin mon slot privé pour gérer les notifications de changement de position remontées par QTM Location :

void CppObject::positionUpdated(const QGeoPositionInfo &info)
{
    if (info.isValid())
    {
        if (info.coordinate().isValid())
        {
            emit positionChanged(info.coordinate().latitude(), info.coordinate().longitude());
        }
    }
}

Rien de très compliqué : j'émets mon signal avec les coordonnées physiques à destination des slots connectés. Ensuite vous l'aurez compris, je n'ai plus qu'à créer une instance de mon objet "CppObject" en lui passant une référence sur la frame web parent. Pour cela je vais modifier le constructeur de la classe Html5ApplicationViewerPrivate en y ajoutant mon instantiation d'objet en gras ci-dessous :

Html5ApplicationViewerPrivate::Html5ApplicationViewerPrivate(QWidget *parent)
    : QGraphicsView(parent)
{
    QGraphicsScene *scene = new QGraphicsScene;
    setScene(scene);
    setFrameShape(QFrame::NoFrame);
    setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

    m_webView = new QGraphicsWebView;
    m_webView->setAcceptTouchEvents(true);
    m_webView->setAcceptHoverEvents(false);
    setAttribute(Qt::WA_AcceptTouchEvents, true);
    scene->addItem(m_webView);
    scene->setActiveWindow(m_webView);
#ifdef TOUCH_OPTIMIZED_NAVIGATION
    m_controller = new NavigationController(parent, m_webView);
#endif // TOUCH_OPTIMIZED_NAVIGATION
    connect(m_webView->page()->mainFrame(),
            SIGNAL(javaScriptWindowObjectCleared()), SLOT(addToJavaScript()));


    m_shareObject = new CppObject(m_webView->page()->mainFrame());
}

Reste à voir maintenant comment mettre en oeuvre ce que je viens de créer depuis le code html. Le rendu sera le suivant :


Pour cela, je déclare le body comme suit :

<body>
    <a id="quit">X</a>
    <div id="latitude"></div>
    <div id="longitude"></div>
</body>

Et voici la partie entête (<head>) :

<script type="text/javascript">

   function slot(lat, lng)
   {
      var objectString = "SLOT called with Latitude: "+lat+" Longitude: "+lng;
      CppObject.displayTrace(objectString);


      document.getElementById("latitude").innerHTML="Latitude : "+lat;
      document.getElementById("longitude").innerHTML="Longitude : "+lng;
   }

   window.onload = function()
   {
      /* This connects CppObject signal to our slot */
      CppObject.positionChanged.connect(slot);


      /* This calls a slot which then in turn emits the signal. */
      CppObject.startWatchingPosition();


      document.getElementById("quit").onmousedown = function()
      {
          Qt.quit();                    
      };
   }
</script>


Dans la fonction de chargement de la page, je m'abonne au signal "positionChanged" en définissant le gestionnaire "slot" puis je démarre le monitoring de changement de position via le slot public "startWatchingPostion" exposé par l'object C++ CppObjet. Et dans le gestionnaire d'événement j'affiche les nouvelles coordonnées physiques.

Comme d'ahbitude vous trouverez le code source complet ici.

Attention : Bien que le HTML5 soit correctement géré par Qt Webkit, l'implémentation sur Symbian est très light. Je préconise donc de rester en HTML 4 tout en utilisant le wizard HTML5.

Aucun commentaire:

Enregistrer un commentaire