Requête à un serveur cartographique sous client web OpenLayers3

Etude du code prototype projet OpenLayers 3

Nous avons généré automatiquement dans QGIS un prototype qui permet de visualiser une carte au travers d’un client web, explorons le code…

Le meilleur moyen d’aborder OpenLayers3 est de passer par les exemples et d’explorer la librairie. (Téléchargez la dernière version d’OpenLayers, vous trouverez les codes sources des exemples et la librairie en plus de la version des codes sources au complet)

Concept

La composante de base d’OpenLayers3 est la carte (ol.Map). Cette carte est rendue dans un conteneur cible, en pratique dans un élément div de la page Web qui contient la carte.

Les propriétés de la carte peuvent être configurées au moment de sa construction, ou ultérieurement par des méthodes spécifiques.

Le conteneur cible qui a été généré pour nous est un élément div d’identifiant map (il comporte des sous-divisions pour permettre un affichage en fenêtre modale dont on pourrait se passer pour simplifier l’exemple…).

        <div id="map">
            <div id="popup" class="ol-popup">
                <a href="#" id="popup-closer" class="ol-popup-closer"></a>
                <div id="popup-content"></div>
            </div>  
        </div>

Ce conteneur possède un style d’affichage paramétré en entête du code html, le style est paramétré ici pour un affichage à 100% de la taille de la fenêtre du navigateur.

        <style>
            html, body, #map {
                height: 100%;
                width: 100%;
                overflow: hidden;
            }
         ...
        </style>

Notez que la librairie OpenLayers et ses extensions possèdent leurs propres fichiers de style qui sont chargés en ressources en entête de fichier html.

        <link rel="stylesheet" href="./resources/ol.css">
        <link rel="stylesheet" href="./resources/boundless.css">

ol.Map ne gère pas lui même le niveau de zoom et la projection de la carte, ce sont des propriétés et méthodes d’une instance de vue, ol.View, qui est initialisée dans la carte.

Pour obtenir les données à représenter pour une couche, OpenLayers utilise des sous-classes ol.source.Source. Ces sources permettent d’alimenter la carte au travers de serveurs de tuiles, de serveurs WMS ou WMTS, de données vectorielles dans des formats comme GeoJSON ou KML. Voyons comment sont configurées les sources par défaut dans notre prototype.

Chargement des ressources javascript

Premièrement le prototype charge la librairie OpenLayers 3

<script src="./resources/ol.js"></script>

Puis une il fait appel à une extension qui implémente un composant qui permet de permuter la visibilité des couches…

<script src="./resources/boundless.js"></script>

Le script charge ensuite les données de chaque couche, ces données ont été transformées au format Geojson par l’extension installée sous QGIS, ce sont ces données qui sont les sources de notre prototype par défaut.

Si l’on considère la couche qui comporte les informations départementales, le script va faire appel à deux ressources, la première comporte les données de la couche au format Géojson

<script src="layers/Departement.js"></script>

qui a pour contenu

var geojson_Departement = {
    "type":"FeatureCollection"
    },
    "crs":{
    "type":"name",
            "properties":{
            "name":"urn:ogc:def:crs:EPSG::3857"}
    }, "features":[
    {"type":"Feature", "properties":{}, "geometry":{"type":"MultiPolygon", "coordinates":
            [[[[649129.155, 5770492.803], [648115.547, 5769160.517],
                    [648898.518, 5767096.316], [648562.967, 5764862.309],
                    [647339.990, 5760692.349], [646490.730, 5758044.713], 
                            ...

GeoJson est un format qui permet d’encoder des structures de données géographiques, nous ne nous attarderons pas sur le format, notre objectif est de remplacer ces données statiques par des données fournies dynamiquement par notre serveur cartographique.

La seconde ressource est un script Javascript qui configure le style de représentation de la couche.

<script src="styles/Departement_style.js"></script>

qui a pour contenu (relativement obscure à cause du code environnant)

var styleCache_Departement = {}
var style_Departement = function (feature, resolution) {
    var value = ""
    var style = [new ol.style.Style({
            stroke: new ol.style.Stroke({color: "rgba(0,255,0,1.0)", lineDash: null, width: 1})
        })
    ];
    var labelText = "";
    var key = value + "_" + labelText

    if (!styleCache_Departement[key]) {
        var text = new ol.style.Text({
            font: '8.25px Calibri,sans-serif',
            text: labelText,
            fill: new ol.style.Fill({
                color: "rgba(0, 0, 0, 255)"
            }),
        });
        styleCache_Departement[key] = new ol.style.Style({"text": text});
    }
    var allStyles = [styleCache_Departement[key]];
    allStyles.push.apply(allStyles, style);
    return allStyles;
};

Finalement le programme charge une dernière ressource qui permet d’initialiser chaque couche depuis les ressources chargées précédemment.

<script src="./layers/layers.js" type="text/javascript"></script>

C’est dans ce script que se passe le plus gros travail d’initialisation.

var baseLayer = new ol.layer.Tile({title: 'OSM', source: new ol.source.OSM()});
...
var lyr_Departement = new ol.layer.Vector({
                source: new ol.source.GeoJSON({object: geojson_Departement}), 
                style: style_Departement,
                title: "Departement"
            });
...
lyr_Departement.setVisible(true);
...
var layersList = [baseLayer,lyr_cultures,lyr_Departement,lyr_DepartementWFS,lyr_etabstamponselec,lyr_etabssel,lyr_etabparcelintersm];

Initialisation des couches

Dans le script layers.js, le script va construire la liste des couches à afficher. Chaque couche est crée l’une à la suite de l’autre avec une source de données, un style et d’autres paramètres. Puis la visibilité de chaque couche est positionnée à la valeur vraie (c’est toutefois la valeur par défaut lors de l’initialisation). La liste des couches est le tableau résultant.

var baseLayer = new ol.layer.Tile({title: 'OSM', source: new ol.source.OSM()});

Ici on construit un objet ol.layer.Tile qui va permettre d’interroger un fournisseur de tuiles (images pré-rendues). L’argument title n’est pas dans les options de l’objet, mais comme cet objet implémente la classe abstraite ol.Object l’argument permet de renseigner un attribut qui possédera des accesseurs (qui pourront être exploités par le composant qui permet de permuter la visibilité des couches)

La source de données ol.source.OSM est pré-configurée dans la librairie et permet d’interroger les serveurs de tuiles d’OpenStreetMap.

var lyr_Departement = new ol.layer.Vector({
                source: new ol.source.GeoJSON({object: geojson_Departement}), 
                style: style_Departement,
                title: "Departement"
            });

Ces instructions permettent de créer une couche de données vecteur, rendue côté client. La source de la couche est au format GeoJSON, elle accepte un objet GeoJSON, geojson_Departement, qui a été construit lors de l’export du projet QGIS et chargé en amont dans les ressources Javascript.

Le style de rendu de la couche vectorielle ol.layer.Vector est également fournit en option lors de la construction, il permet de passer l’objet ol.style.Style également chargé en amont dans les ressources javascript.

lyr_Departement.setVisible(true);

L’objet de couche est déjà défini, le script utilise explicitement un accesseur pour conditionner la visibilité.

var layersList = [baseLayer, ..., lyr_Departement, ...];

La liste des couches est finalement renseignée dans une variable layersList a contexte global.

Initialisation et rendu de la carte

Le script vient d’obtenir les données à représenter pour chaque couche il reste à initialiser l’objet carte mais avant cela il faut encore expliquer un morceau de code généré pour nous.

            var container = document.getElementById('popup');
...
            var overlayPopup = new ol.Overlay({
                element: container
            });

Sous OpenLayers3 il existe une classe de widgets qui s’affiche en superposition, ol.Overlay.

Le code créé une instance de widget qui sera superposée à l’élément du DOM récupéré dans la variable container (qui possède l’identifiant popup)

L’extension disponible sous QGIS génère beaucoup de code superflus pour un usage basique. La gestion de popups et overlays ne m’est d’aucune utilité et sera supprimée du prototype ainsi que toute la gestion d’événements à la souris.
            var map = new ol.Map({
                controls: ol.control.defaults().extend([
                    new ol.control.ScaleLine({}), new Boundless.LayersControl({groups: {default: {title: "Layers"}}})
                ]),
                target: document.getElementById('map'),
                renderer: 'canvas',
                overlays: [overlayPopup],
                layers: layersList,
                view: new ol.View2D({
                    maxZoom: 28, minZoom: 1
                })
            });

La construction de l’objet carte ol.Map est le cœur de la librairie. Elle requiert de renseigner une vue, ajouter une ou plusieurs couches, et préciser un élément conteneur cible.

Les options générées dans l’exemple sont les suivantes :

  • controls : Collection de contrôles. Un contrôle ol.control.Control est un widget représenté en position fixe à l’écran. Un contrôle peut être seulement informatif ou bien peut permettre d’interagir avec l’utilisateur. Une collection de contrôles par défaut est incluse aux cartes, ol.control.defaults (Elle comporte les widgets de Zoom, Rotation et Attributions). Ici le code ajoute deux contrôles à la collection par défaut, en premier l’échelle et en second le widget qui permet de sélectionner les couches à afficher ou masquer. Le rendu des contrôles est paramétrable par application de styles css
  • target : Elément du DOM où la carte sera incluse
  • renderer : Moteur de rendu, seul le canvas a les capacités de rendu vectoriel
  • overlays : Collection de widgets en superposition (contrairement aux contrôles la position des widget n’est pas fixe, elle dépend de la carte)
  • layers : Collection de couches
  • view : Vue associée à la carte.

La vue est un objet qui représente une vue 2D simple de la carte, elle permet de spécifier le centre, la résolution et la rotation de la carte. La façon la plus simple de définir une vue est de définir un point central et un niveau de zoom.

Dans notre exemple la vue est initialisée uniquement avec un niveau de zoom minimal et maximal.

Un objet ol.View possède une projection. La projection détermine le système de coordonnées du centre et ses unités déterminent les unités de résolution (projection d’unités par pixels). La projection par défaut est sphérique Mercator (EPSG: 3857), système de projection utilisé entre autre par OSM et Google Maps, et comme vous pouvez constater elle n’est pas renseignée dans notre cas et correspond bien à la projection utilisée dans les fichiers de données Geojson.

Et ensuite ?

map.getView().fitExtent([-225069.813761, 5267288.926421, 186886.432518, 5746435.175340], map.getSize());

La vue associée à l’objet carte est récupérée. La méthode fitExtent() est au stade expérimental (vous devez décochez la case Stable Only dans l’API pour faire apparaître la documentation !), elle fait s’adapter la vue à l’étendue géographique et à la taille données. La taille est en pixels, elle est déterminée ici par la taille de la carte. L’étendue est un tableau de nombres qui représentent une étendue géographique, il s’agit de [minx, miny, maxx, maxy]. Ces valeurs sont les valeurs récupérées sous QGIS lors de l’export du projet, il est bien entendu possible de les modifier pour s’adapter à une autre étendue par défaut. (Il suffit sous QGIS de copier-coller les valeurs pour la vue qui convient)

Comme les données ne concernent que la Gironde, j’ai basculé le SCR du projet à EPSG:3857 sous QGIS et zoomé la sélection sur la Gironde à partir des attributs de la couche des départements. L’emprise est disponible dans la barre de statut de QGIS en bas à gauche après permutation de l’affichage de position du curseur.

Ce qui donne donc

map.getView().fitExtent([-174152,5464320,85720,5738798], map.getSize());

Le reste du code n’a pas d’incidence sur le rendu de carte, il permet de gérer l’interaction souris avec l’utilisateur. (Et à vrai dire ne semble pas fonctionner)

Nous avons donc expliqué le code et vu comment générer une carte à partir de sources de données vectorielles sous forme de fichiers. Comment adapter le code pour interroger un serveur de données ?

Adaptation du code pour une source de données par serveur cartographique

Nous avons a disposition pour le projet deux services qui tournent sous un serveur cartographique QGIS Server.

Les capacités WFS et WMS des services peuvent être interrogées par les url suivantes

http://92.222.27.205/cgi-bin/projet1/qgis_mapserv.fcgi?SERVICE=WFS&VERSION=1.1.0&REQUEST=GetCapabilities
http://92.222.27.205/cgi-bin/projet1/qgis_mapserv.fcgi?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities

C’est ce dernier service que l’on va exploiter en premier.

Appel à un service WMS QGIS Server sous OpenLayers3

Le nom de la couche des départements nous intéresse particulièrement, on peut le récupérer depuis le fichier xml.

<Layer queryable="1">
<Name>Departement</Name>
<Title>Departement</Title>
<CRS>EPSG:4326</CRS>
<EX_GeographicBoundingBox>
<westBoundLongitude>-5.21251</westBoundLongitude>
<eastBoundLongitude>9.63332</eastBoundLongitude>
<southBoundLatitude>41.3141</southBoundLatitude>
<northBoundLatitude>51.1376</northBoundLatitude>
</EX_GeographicBoundingBox>
<BoundingBox CRS="EPSG:4326" maxx="51.1376" minx="41.3141" maxy="9.63332" miny="-5.21251"/>
<Style>
<Name>default</Name>
<Title>default</Title>
<LegendURL>
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://92.222.27.205/cgi-bin/projet1/qgis_mapserv.fcgi?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetLegendGraphic&LAYER=Departement&FORMAT=image/png&STYLE=default&SLD_VERSION=1.1.0"/>
</LegendURL>
</Style>
</Layer>

Le parcours des sources de données ol.source dans l’API OpenLayers3 nous renseigne sur les possibilités de la librairie.

Il apparaît possible d’utiliser un service WMS pour fournir les images pour une couche donnée, le type est ol.source.ImageWMS.

Il suffit de remplacer la source locale pour la couche des départements.

//var lyr_Departement = new ol.layer.Vector({
//                source: new ol.source.GeoJSON({object: geojson_Departement}), 
//                style: style_Departement,
//                title: "Departement"
//            });

var lyr_Departement = new ol.layer.Image({
                source: new ol.source.ImageWMS({
                               url: "http://92.222.27.205/cgi-bin/projet1/qgis_mapserv.fcgi",
                               params: {
                                   'LAYERS': 'Departement'
                               },
                            }),
                style: style_Departement,
                title: "Departement"
            });

La source ol.source.ImageWMS requiert l’url du service et un tableau des paramètres de la requête WMS en options, dont seul le paramètre layers est obligatoire. (Les autres paramètres sont renseignés dynamiquement par la librairie)

Il est possible de faire la même manipulation pour les autres couches en modifiant le nom de la couche dans l’option layers.

Les requêtes vers le serveur WMS peuvent être contrôlées sous les outils de développement de votre navigateur Web préféré afin d’étudier la requête générée et le temps d’exécution. A titre d’exemple une requête effectuée pour récupérer les parcelles de culture.

http://92.222.27.205/cgi-bin/projet1/qgis_mapserv.fcgi?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&FORMAT=image%2Fpng&TRANSPARENT=true&LAYERS=cultures_33&CRS=EPSG%3A3857&STYLES=&WIDTH=2880&HEIGHT=1529&BBOX=-33130.75605296711%2C5560982.287734281%2C-5613.425870303659%2C5575591.314765286

Notez également que depuis ce type d’appel les paramètres d’affichage en résolution minimale et maximale sont respectés.

var lyr_cultures = new ol.layer.Image({
                source: new ol.source.ImageWMS({
                               url: "http://92.222.27.205/cgi-bin/projet1/qgis_mapserv.fcgi",
                               params: {
                                   'LAYERS': 'cultures_33'
                               },
                            }),
                minResolution:-4.65661009752e-10,
                maxResolution:55785.0,
                style: style_cultures,
                title: "cultures_33"
            });

Dans l’exemple précédent les cultures ne s’affichent que dans la résolution souhaitée par contre les requêtes sont effectuées sur le serveur quelle que soit la résolution dans la vue représentée. Hors limites de représentation elles retournent une image vide (par défaut un png transparent).

Afin de limiter le nombre de requêtes au serveur il peut être intéressant de demander plusieurs couches dans une même requête, par exemple on peut regrouper les trois couches qui servent à représenter les établissements et leur proximité aux cultures.

var lyr_etabs = new ol.layer.Image({
        source: new ol.source.ImageWMS({
            url: "http://92.222.27.205/cgi-bin/projet1/qgis_mapserv.fcgi",
            params: {
                'LAYERS': 'etabs_tampon_selec,etabs_sel,etab_parcel_inters_50m'
            },
        }),
        title: "Etablissements"
    });

En passant par le service WMS le serveur cartographique retourne une image, de ce fait on perd les possibilités offertes par la couche vectorielle Geojson. L’avantage d’une image est souvent sa légèreté sur le réseau par rapport à un fichier de données, mais le format vectoriel nous permet d’interagir en javascript avec les objets, on peut par exemple sélectionner un objet et modifier son style, supprimer ou ajouter un objet.

Interagir avec les objets peut permettre de créer des cartes plus interactives, en créant des composants de formulaire en widget par exemple on peut imaginer de représenter dynamiquement une carte choroplèthe ou des symboles proportionnels en fonction d’un paramètre sélectionné et d’une requête asynchrone qui va chercher les valeurs d’indicateurs à représenter.

Appel à un service WFS QGIS Server sous OpenLayers3

La librairie OpenLayers 3 a la capacité de charger des couches vectorielles depuis une méthode ol.layer.Vector() comme nous l’avons vu précédemment. A plus bas niveau elle permet d’utiliser la méthode ol.source.ServerVector(), toutefois encore en état expérimental, qui va permettre d’alimenter une source dans un format supporté depuis un serveur distant.

Il nous suffit en théorie d’utiliser le format ol.format.GeoJSON() avec notre serveur QGIS Server depuis une requête asynchrone côté client web.

En pratique, si l’on effectue une requête sur notre serveur afin de récupérer des données JSON depuis une page html côté client (en Ajax donc), le navigateur va interdire l’opération si le serveur n’autorise pas explicitement les accès en origine croisée par l’ajout d’une entête de contrôle d’accès.

Notre problème est double. Premièrement QGIS Server possède les capacités pour servir du GeoJSON, qui est simplement du JSON avec une convention, par contre il ne propose pas le format de sortie JSONP qui encapsulerait simplement le GeoJSON dans une fonction Javascript. Il nous resterait comme option de configurer le serveur pour accepter les requêtes d’origine croisées (CORS), cependant le serveur QGIS fonctionne en mode CGI et Apache ne peut pas à ma connaissance ajouter une entête d’autorisation sur la sortie produite par QGIS Server.

La solution lorsque l’on ne peut ni modifier le format de sortie ni configurer le serveur pour ajouter des autorisations est d’utiliser un proxy. Très simplement un script sera hébergé sur le même serveur, il acceptera en paramètres un nom de fonction qui servira à l’encapsulation javascript du format JSON, et l’url qu’il sera en mesure d’interroger depuis le même domaine.

J’ai créé un simple script en langage PHP qui servira de proxy. Vous pourrez probablement trouver des solutions plus complètes…

<?php
if(!isset($_GET['callback']) || !isset($_GET['url'])) {
  header('status: 400 Bad Request', true, 400);
  exit;
}
extract(parse_url($_GET['url']));       

if (! ( in_array($host, array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_ADDR']) )
      || ( basename($path) != 'qgis_mapserv.fcgi' ) )) {
  header('status: 400 Bad Request', true, 400);
  exit;
}
if (!$content = file_get_contents(filter_var($_GET['url'], FILTER_SANITIZE_URL))) {
  header('status: 400 Bad Request', true, 400);
  exit;
}
header('content-type: application/javascript; charset=utf-8');
header("access-control-allow-origin: *");
echo filter_var($_GET['callback'], FILTER_SANITIZE_ENCODED), '(', $content, ');';

Ce script nommé proxy.php est placé à la racine du répertoire web. Il doit être appelé avec deux paramètres, callback et url.

Le paramètre url doit être encodé…

http://io.gchatelier.fr/proxy.php?callback=getDepartementJson&url=...

Le script effectue quelques contrôles, premièrement il n’aboutit pas si les paramètres ne sont pas renseignés. Il vérifie ensuite que l’url à interroger est sur le même domaine que le serveur, puis vérifie que l’on cherche bien à interroger un script CGI qui correspond à QGIS Server. Le script filtre ensuite les paramètres et exécute la requête. Le contenu de la requête est retourné encapsulé dans la fonction de rappel Javascript avec les bons entêtes : premièrement l’entête d’application javascript (et non pas l’entête d’application JSON), puis l’entête de contrôle d’accès qui autorise le proxy à être interrogé depuis un autre domaine.

Une fois ce problème réglé on peut passer au code.

            var DepartementWFSURL = 'http://92.222.27.205/cgi-bin/projet1/qgis_mapserv.fcgi?' + 
                    'SERVICE=WFS&REQUEST=GetFeature&' + 
                    'VERSION=1.1.0&TYPENAME=Departement&' + 
                    'SRSNAME=EPSG:3857&outputFormat=GeoJSON';
            
            var proxy = 'http://io.gchatelier.fr/proxy.php?callback=getDepartementJson&url=';

Construit l’url d’appel à notre proxy en précisant la fonction de rappel et l’url d’origine du service.

            var deptsSource = new ol.source.ServerVector({
                format: new ol.format.GeoJSON(),
                loader: function(extent, resolution, projection) {
                    var url = DepartementWFSURL +
                        '&bbox=' + extent.join(',');

                    $.ajax({
                        url: proxy + encodeURIComponent(url),
                        dataType: 'jsonp',
                        jsonpCallback: 'getDepartementJson',
                    });
                },
                strategy: function() {
                    return [ [-5.21251302, 41.31412406, 9.63331895, 51.13763146] ];
                },
                projection: 'EPSG:3857'
            });

La source récupère les données WFS en GeoJSON en utilisant la technique JSONP.

Remarquez l’option strategy. OpenLayers 3 possède plusieurs types de stratégies ol.loadingstrategy pour le chargement des données. Il peut charger les données par tuiles en multipliant les appels au serveur, tout charger d’un coup, ou charger suivant les coordonnées de la boîte englobante (BBOX). Ici les coordonnées sont fournies sur l’étendue géographique complète [minx, miny, maxx, maxy].

L’option loader correspond à la fonction qui effectue le chargement, elle se voit passer l’étendue et la projection que l’on a paramétrées. Avec la stratégie de tuiles elle serait appelée de multiples fois. C’est que l’appel javascript asynchrone va récupérer les objets de la couche vectorielle depuis le service WFS

L’appel Ajax accepte ces paramètres :

  • url : url vers notre proxy qui interrogera le serveur WFS
  • dataType : notre proxy retourne du javascript au format JSONP, on doit préciser le type de données car les entêtes retournées sont application javascript
  • jsonpCallback : le nom de la fonction de rappel dans le fichier JSONP retournée par le proxy
            var getDepartementJson = function(response) {
                deptsSource.addFeatures(deptsSource.readFeatures(response));
            };

En cas de réussite la fonction anonyme de rappel est exécutée avec la réponse en paramètre, notre GeoJSON… la réponse est parcourue et ajoutée à la source vectorielle.

            var lyr_Departement = new ol.layer.Vector({
                source: deptsSource,
                title: 'Departement',
                style: style_Departement,
            });

Il ne reste plus qu’à exploiter la source vectorielle que nous venons de créer comme nous avons exploité les sources GeoJSON.

Nous avons fait le tour des codes sources générés sous une extension QGIS pour la librairie OpenLayers3. Vous pouvez également vous référer au même document adapté pour la librairie Leaflet.

Nous sommes désormais en mesure de construire des applications interactives pour le web depuis des services cartographiques, ce sera l’objet du prochain document de cette série qui détaillera la création d’une carte interactive sous la librairie Leaflet, le même principe pouvant s’appliquer à OpenLayers3 avec les techniques que nous venons d’expliquer.

Faites découvrir ce billet...Email this to someonePrint this pageShare on FacebookTweet about this on TwitterShare on Google+Share on LinkedIn