Représenter un résultat de requêtes spatiales sous PostgreSQL PostGIS depuis un client web

Dans l’objectif de développer une application cartographique, nous avons au niveau des données récupéré des jeux de données spatiales, installé la base de données et son extension spatiale et finalement chargé les données en base. Nous avons également mis en place un serveur cartographique qui fournit les données au travers du réseau, et construit les bases de l’application dans un client web pour interroger le serveur cartographique et afficher les premiers éléments de carte.

L’application ne se contente pas d’afficher des données statiques, mais elle doit permettre de représenter l’information dynamiquement en effectuant des requêtes en fonction du contexte d’utilisation. C’est la partie que nous allons développer dans ce document.

Préparation de l’interface cartographique

La première étape est de préparer l’interface graphique et recadrer le contexte.

Nous avons vu comment appeler les services WMS et WFS que nous avons mis en place pour générer une carte simple où la proximité des établissements scolaires aux parcelles cultivées sur la Gironde était affichée de façon statique sur une carte rendue disponible dans un navigateur web par les librairies Leaflet et OpenLayers 3.

Dans la suite de ce document j’utiliserai la librairie Leaflet mais le principe est identique pour OpenLayers 3 et le code peut être adapté.

Question de volumétrie nous n’avons pas sur ce prototype intégré les données parcellaires sur toute les régions. Voyons en premier lieu comment adapter le code pour afficher les parcelles cultivées sur la France métropolitaine, puis explorons les fonctionnalités à paramétrer sur la carte.

Représentations des couches cartographiques depuis le serveur cartographique

Premièrement le programme représente le fond de carte par l’appel aux serveurs de tuiles d’OpenStreetMap, puis il fait appel au serveur QGIS Server pour récupérer les données géométriques des départements français dont il effectue le rendu à la volée. Ces fonctionnalités sont identiques à celles du premier prototype.

Ensuite le programme effectue le rendu des parcelles pour la France métropolitaine. Il interroge le serveur WMS qui génère une image pour la vue en cours.

var wmsLayerParcelles = L.tileLayer.wms(project, {
    layers: 'Parcelles',
    format: 'image/png',
    transparent: true,
    minZoom: 10,
    maxZoom: 18,
    opacity: 0.55,
});
wmsLayerParcelles.addTo(map);

Cette fonctionnalité a été expliquée, la nouveauté est l’apparition des paramètres minZoom et maxZoom qui vont permettre de conditionner le rendu de la couche au niveau d’agrandissement en cours sur la carte. En d’autres termes la librairie ne fera pas appel au serveur pour représenter les parcelles en dessous d’un niveau d’agrandissement fixé à 10. Cela permet de limiter la quantité d’informations de parcelles sur une image, et donc à la fois le travail de représentation des données du serveur et le trafic sur le réseau où seraient transférées des images trop volumineuses.

Si le rendu est effectué côté serveur, on souhaite également accéder aux caractéristiques des parcelles pour indiquer le numéro d’îlot et le type de culture. Cela peut être fait à un niveau d’agrandissement encore supérieur afin de limiter la taille des fichiers échangés, en faisant appel au service WFS.

map.on('zoomend moveend', function() {
...
    if (map.getZoom() > 15)
        getJsonLayerParcelles(map.getBounds());
});

On va préciser au gestionnaire d’événements de mettre en écoute les opérations d’agrandissement et de glisser-déposer.

Lorsque ces événements arrivent, on vérifie le niveau d’agrandissement de la carte et s’il est supérieur à la valeur seuil que l’on a choisi pour faire figurer les données, on charge la couche vectorielle de façon asynchrone pour l’étendue géographique en cours sur la carte.

var jsonLayerParcelles = null;

function getJsonLayerParcelles(bounds) {

    var getFeatureParcelles = project + '?SERVICE=WFS'
        + '&VERSION=1.1.0'
        + '&REQUEST=GetFeature'
        + '&TYPENAME=Parcelles'
        + '&SRSNAME=EPSG:4326'
        + '&outputFormat=GeoJSON'
        + '&BBOX=' + bounds.toBBoxString();

    var proxy = host + '/proxy.php?callback=getGeoJson&url=';

    var proxyURL = proxy + encodeURIComponent(getFeatureParcelles);

    var jsonLayerParcelles = L.geoJson(null, {
        style: function (feature) {
            return {
            fillColor: 'rgb(255,255,255)',
            color: 'rgb(175,179,138)',
            weight: 1,
            opacity: 1,
            fillOpacity: 0,
            dashArray: '1,5',
            };
        },
        onEachFeature: function (feature, layer) {
            var content = 'Parcelle N° ' + feature.properties['num_ilot'] + '<br />';
            content += getCulture(feature.properties['cult_maj']);
            layer.bindPopup(content);
        }
    });

    $.ajax({
        url: proxyURL,
        dataType: 'jsonp',
        jsonpCallback: 'getGeoJson',
        success: function (response) {
            L.geoJson(response, {
                onEachFeature: function (feature, layer) {
                    jsonLayerParcelles.addData(feature)
                }
            });
        }
    });
    jsonLayerParcelles.addTo(map);
}

Le principe de chargement de la couche vectorielle est le même que pour les départements sauf que cette fois-ci on passe l’étendue géographique dans l’url du service. Cela se fait via deux opérations, map.getBounds() et bounds.toBBoxString().

Remarquez que pour chaque objet récupéré on attache un contenu qui s’affichera dans un popup au clic sur la couche. Le contenu est mis en forme par rapports aux propriétés récupérées.

Comme le rendu est déjà effectué sous forme d’image côté serveur, l’opacité est fixée pour rendre transparent le remplissage des polygones, le contour est quand à lui représenté en pointillés grâce à l’option de style dashArray. Cela permet de faire figurer les parcelles qui ont été représentées en blanc opaque ou transparence car ce ne sont pas des cultures mais des prairies ou des gels de cultures.

Finalement par rapport au premier prototype, un contrôle a été ajouté pour utiliser le géocodage d’adresses afin de localiser une destination sur la carte.

        <link rel="stylesheet" href="css/Control.OSMGeocoder.css" />
        <script src="js/Control.OSMGeocoder.js"></script>
var osmGeocoder = new L.Control.OSMGeocoder({
    collapsed: false,
    position: 'topright',
    text: 'Allez!'
});
osmGeocoder.addTo(map);

Paramétrage de fonctionnalités sur la carte

Une extension à la librairie Leaflet permet de faire figurer sur la carte une barre latérale qui a la capacité de se déplier et replier au clic sur les icônes de la barre.

J’ai adapté les styles css pour effectuer un rendu qui laisse entrevoir le fond de carte par transparence.

L’extension est disponible sous Leaflet comme sous Openlayers, elle me semble un bon choix relativement standardisé pour apporter des fonctionnalités à la carte sans développer une multitude de contrôles.

L’extension requiert le chargement de ressources css et javascript, ainsi que du jeux d’icônes vectorielles sous police de caractères font-awesome. Je fais également appel aux polices de caractères Google pour le rendu du contenu de la barre latérale.

        <link href='http://fonts.googleapis.com/css?family=Lato:400,700|PT+Sans:400,700|Roboto:400,500,700' rel='stylesheet' type='text/css'>
        <link href="http://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
...
        <script src="js/leaflet-sidebar.min.js"></script>

L’extension requiert de modifier la classe de style appliquée au conteneur de carte.

<div id="map" class="sidebar-map"></div>

Le contenu de la barre est simplement mis en place par une convention de balises html que je ne reprendrai pas ici, l’extension étant bien documentée.

Un onglet de paramètres permettra à l’utilisateur de configurer la carte. En termes de fonctionnalités on souhaite à minima paramétrer la distance qui caractérise la proximité des établissements aux parcelles, d’autres fonctionnalités peuvent permettre de faire une sélection des établissements à représenter en fonction de la surface des parcelles en contact, des types de cultures, etc…

Ces fonctionnalités requierent d’interroger la base de données après soumission d’un formulaire, l’interrogation se fait au moyen de requêtes spatiales sur Postgis, sachant que l’appel s’effectue en Ajax et que le serveur retournera des informations en Json.

Requêtes spatiales

Avant de passer au mécanisme de communication entre la carte et la base de données, nous allons procéder à l’étude des requêtes spatiales qui feront toute l’importance des données représentées.

La détermination de risques est une problématique complexe, ce que l’application propose n’est pas d’évaluer un risque mais d’exposer des facteurs de risques potentiels qui découlent simplement du bon sens. La démarche est de déterminer des populations d’élèves qui pourraient être impactées par ces critères afin de construire des échantillons de population sur lesquels effectuer des prélèvements pour évaluer l’exposition effective aux pesticides.

Intuitivement le risque d’exposition aux pesticides augmente quand :

  • L’aire cultivée augmente à proximité de la population. Une requête spatiale doit opérer un calcul d’aire. L’aire peut être représentée par un symbole proportionnel.
  • Il existe plusieurs types de cultures différentes à proximité d’une population. (multiplication de la nature des pesticides, et des horaires d’épandage). Une requête doit permettre de comptabiliser les cultures à proximité d’une population
  • La catégorie de culture est plus ou moins consommatrice de pesticides. Un indicateur existe, l’IFT, qui permet de mesurer la pression phytosanitaire (quantités et fréquences d’épandage) d’un sol mais il ne prend pas en compte la toxicité des pesticides, le type de culture sera par conséquent affiché seulement à titre informatif, il donne quand même une information exploitable car on a connaissance par exemple de la toxicité des traitements en viticulture. Les requêtes doivent éliminer les pâturages et gels de cultures

La carte sera imparfaite. Un certain nombre de paramètres influeront les résultats, par exemple :

  • Il n’est pas possible de prendre en compte la rotation des cultures. Les données de parcelles sont des déclarations relevées en 2010, ces données sont anciennes, et comportent nécessairement des erreurs. Un parallèle pourra être mis en place avec une carte aérienne.
  • Les cultures bio sont négligées faute de données. (De l’ordre de 3% des cultures)
  • Le centroide des établissements n’est pas l’aire des bâtiments, si l’on considère une proximité de 500 mètres aux coordonnées de latitudes et longitude de l’établissement, la proximité réelle aux salles de classe ou à la cours de récréation peut être moindre
  • Les vents dominants et l’age du proviseur ne sont pas pris en compte !

Fonctions spatiales PostGIS, géométrie ou géographie ?

Consultez la référence PostGIS !

PostGIS dispose d’un jeu très complet de fonctions spatiales, et l’on abordera que très peu d’entre elles. Sachez toutefois que PostGIS change de convention de nommage pour standardiser le nom des fonctions par l’ajout du préfixe spatial ST_ à chaque fonction. Cela signifie que si vous recherchez des exemples d’utilisation sur Internet il vous faudra probablement retirer le préfixe…

Les fonctions spatiales peuvent pour certaines fonctionner avec plusieurs types de paramètres. Des conversions (implicites ou explicites) peuvent être effectuées entre certains types. Les types de paramètres sont :

  • box2d : est une boîte composée de coordonnées xmin, ymin, xmax, ymax. Souvent utilisé pour retourner la boîte englobante 2D d’une géométrie.
  • box3d : est une boîte composée de coordonnées xmin, ymin, zmin, xmax, ymax, zmax. Souvent utilisé pour retourner la mesure 3D d’une géométrie ou le recouvrement des géométries.
  • geometry : est le type de données spatiales utilisé par PostGIS pour représenter une fonction dans le système de coordonnées Euclidien. L’unité est celle du SRID.
  • geometry_dump : est un type de données spatiales avec deux champs, geom et path[] qui sont respectivement un objet de géométrie et un tableau qui contient la position de la géométrie dans l’objet de dump
  • geography : est un type de données spatiales utilisée pour représenter une entité dans le système de coordonnées sphériques terrestres. L’unité est le mètre carré.

Nous utiliserons les types geometry et geography. Nos données sont enregistrées dans le type geometry et pour nos besoins nous voudrons calculer des distances en mètres ou des superficies en mètres carrés, cela implique de faire des conversions de type geometry vers type geography et vice versa.

geom::geography
geom::geometry

Sont des exemples de conversions explicites. (Vous pouvez vous référer à la documentation pour savoir quels types peuvent être convertis en autres types)

Dernière remarque concernant les types, l’unité géographique d’une superficie est le mètre carré. Pour des raisons évidentes nous emploieront plus volontiers le kilomètre carré ou l’hectare pour donner une information.

Les conversions d’unités sont les suivantes :

1 mètre carré = 1 * 10^-6 kilomètre carré   (ou encore 1 / 1000000)
1 mètre carré = 1 * 10^-4 hectare   (ou encore 1 / 10000)

Préparation des requêtes

Calcul de l’aire d’un polygone

ST_Area – Retourne l’aire de la surface d’un polygone ou multi-polygone.

Pour l’exemple je vais calculer l’aire de chaque département. La colonne est de type géométrique, il suffit de faire une conversion de type pour retourner la surface en mètres carrés.

SELECT nom_dept, ST_Area(CAST(geom As geography)) * POWER(10, -6) as geom FROM "Departement";

Distance entre géométries

La recherche d’établissements à proximité de zones cultivées peut être réalisée au travers de plusieurs fonctions, le problème qui va se poser est celui des performances.

ST_DWithin – Renvoie la valeur vraie si les géométries sont dans la distance spécifiée l’une à l’autre. Pour les unités géographiques la distance est exprimée en mètres, la mesure s’effectue par défaut autour d’un sphéroïde.

ST_Distance – Pour des champs de type géographie, retourne la distance minimale entre deux géographies sphéroïdale en mètres.

ST_Buffer – Pour un champ de type géométrie, retourne une géométrie qui représente tous les points dont la distance à l’objet géométrique est inférieure ou égale à la distance. Les calculs sont dans le système de référence de l’objet. Pour une géométrie de type point le résultat sera approximativement un cercle ayant pour centre le point et pour rayon la distance.

ST_Intersects – Retourne la valeur vraie si les géométries / géographie « se croisent spatialement en 2D » – (possèdent une partie de l’espace en commun) et la valeur faux sinon (les objets sont disjoints). Pour la géographie – la tolérance est 0,00001 mètres (donc tous les points qui sont considérés fermés retournent la valeur vraie)

Si l’on s’appuie sur les deux premières fonctions, il suffit en théorie de faire une jointure de la table d’établissement avec la table (ou la vue matérialisée) des cultures sur la géométrie des objets trans-typée en géographie sur la distance souhaitée. L’approche est intuitive mais n’exécutez pas la requête qui suit…, pour une proximité de 50 mètres on peut tenter d’évaluer le temps d’exécution :

EXPLAIN SELECT 
e.numero_uai, e.denominati, e.geom, c.num_ilot, c.cult_maj
FROM "Etablissement" e, "master_cultures" c 
WHERE ST_DWithin(e.geom::geography, c.geom::geography, 50);

Le coût d’exécution est estimé à :

"Nested Loop  (cost=0.00..119895256281.96 rows=132693 width=104)"

La requête prendrait un temps indécent à s’exécuter !

Pour une utilisation en ligne il est impératif de compiler au préalable les données. Ma première idée est d’établir un distancier entre le centroide des établissements et les parcelles de cultures à moins de 1500 mètres, le seuil de distance maximale qui sera proposé à l’utilisateur.

En volumétrie, nous avons 64.901 établissements et 6.132.686 parcelles, soit sans seuil potentiellement 398.017.454.086 lignes !

EXPLAIN 
SELECT count(e.numero_uai)
FROM "Etablissement" e, "master_cultures" c
WHERE ST_DWithin(e.geom::geography, c.geom::geography, 1500);
"Aggregate  (cost=119116282174.69..119116282174.70 rows=1 width=9)"

Cette approche permettrait d’enregistrer les distances dans une table indexée mais cette opération est toujours indécente si l’on considère le temps d’exécution initial.

Une telle quantité de parcelles cultivées est très pénalisante, peut-être peut-on éliminer certaines parcelles de l’équation ? Le type de culture est donné par un numéro. On peut éventuellement éliminer certains types, comme les prairies, qui ne sont pas à priori à prendre en considération.

select count(num_ilot) from master_cultures
where cult_maj not in (18, 19);

Retirer les prairies et prairies temporaires permet de diviser par deux le nombre de lignes de la table de cultures, cela reste toutefois très insuffisant.

Il faut envisager une autre approche. Si le coût d’une opération ST_DWithin() est trop important peut être que l’opération ST_Intersects() se révélera plus rapide.

Si l’on se base sur le centroide des établissements pour créer une zone tampon de 1500 mètres autour de l’établissement on peut chercher l’intersection entre la zone tampon et les parcelles, puis on pourra rechercher la distance sur les cas limités où la proximité est avérée.

create table tampon_etas as
select e.numero_uai, e.geom as centre_geom, ST_Buffer(e.geom::geography, 1500)::geometry as geom 
FROM "Etablissement" e;

La première opération est triviale, il suffit d’employer la fonction ST_Buffer() autour de la géométrie de l’établissement, de type POINT, sur un rayon de 1500 mètres pour créer une nouvelle géométrie. Cette opération prend environ 13 secondes, suite à quoi on peut ajouter des indexes spatiaux. La géométrie du point est conservée, elle matérialise le centre de la zone tampon, à partir duquel sera calculée la distance aux cultures.

CREATE INDEX tampon_cgeom_gist ON tampon_etas USING gist (centre_geom) TABLESPACE pg_default;
CREATE INDEX tampon_geom_gist ON tampon_etas USING gist (geom) TABLESPACE pg_default;

Tentons d’évaluer l’opération d’intersection…

EXPLAIN SELECT t.numero_uai
FROM "tampon_etas" t, "master_cultures" c 
WHERE ST_Intersects(t.geom, c.geom);
"Nested Loop  (cost=0.28..176252434.15 rows=132693478 width=9)"

Le coût est très élevé mais déjà divisé par 1000 par rapport aux premières approches. Nous pouvons tenter d’exécuter une requête sur les cultures d’un seul département, et multiplier le temps d’exécution par le nombre de départements pour avoir une estimation du temps d’exécution.

SELECT t.numero_uai
FROM "tampon_etas" t, "cultures_01" c 
WHERE ST_Intersects(t.geom, c.geom)
AND cult_maj not in (18, 19);

Malheureusement l’opération retourne une erreur !

ERREUR:  GEOSIntersects: TopologyException: side location conflict (...)

L’opération n’abouti pas car certaines données de la table de cultures sont invalides, PostGIS possède des fonctions qui permettent de vérifier les données, ou encore de réparer des données.
Si le taux d’erreur n’est pas trop élevé on pourra ignorer les parcelles dont les données sont en erreur.

ST_IsValid – Teste une valeur géométrique et retourne la valeur vrai si la géométrie est bien formée.

ST_IsSimple – Teste une valeur géométrique et retourne la valeur vrai si la géométrie n’a pas de point géométrique invalide comme une auto-intersection ou auto-tangence.

Notez que la géométrie de la table des cultures est de type MULTIPOLYGON, mais que certaines de ses entrées sont de type GeometryCollection, il y a une incohérence de type pour certaines entrées.

SELECT count(*)
FROM "cultures_01" c 
WHERE cult_maj not in (18, 19)
AND NOT ST_IsValid(geom);

142 géométries se révèlent invalides sur 63962 géométries testées, soit environ 2 pour mille. Par rapport à notre besoin on peut se contenter d’ignorer les parcelles dont la géométrie est invalide, le résultat sera négligeable.

SELECT t.numero_uai, c.num_ilot,
t.geom as eta_geom, c.geom as cul_geom,
ST_Distance(t.centre_geom::geography, c.geom::geography) as distance
FROM "tampon_etas" t, "master_cultures" c
WHERE
ST_IsValid(c.geom)
AND ST_Intersects(t.geom, c.geom)
LIMIT 100;

Lorsque l’intersection entre la zone tampon et les cultures retourne la valeur vrai on peut récupérer la distance du centre du tampon à la géométrie de la culture. Cela souffre un peu d’imprécision, mais la fonction ST_DWithin() est contre performante dans ce cas. Il ne reste plus qu’à créer la table qui conservera les distances, c’est cette table que nous interrogerons.

explain CREATE TABLE intersect_etabs AS 
SELECT t.numero_uai, e.code_dept, c.num_ilot,
t.centre_geom as eta_tampon_geom, c.geom as cul_geom,
CAST(floor(ST_Distance(t.centre_geom::geography, c.geom::geography)) as smallint) as distance
FROM 
	"tampon_etas" t INNER JOIN "Etablissement" e on t.numero_uai = e.numero_uai, 
	"master_cultures" c
WHERE
c.cult_maj not in (18, 19) 
AND ST_IsValid(c.geom)
AND ST_Intersects(t.geom, c.geom);

La création prend 25 minutes… J’ai transformé la distance calculée afin d’utiliser un entier de petite dimension qui sera plus rapide à interroger qu’un nombre avec décimales.

Une jointure sur la table d’établissements permet de récupérer au passage le code du département.

select * from intersect_etabs where distance < 300;

La requête prend environ 33 secondes sur l’ensemble de la table sans utiliser d’étendue. Ce résultat n’est pas exploitable sur l’étendue de la France métropolitaine, d’une part parce que le résultat est trop long à retourner, d’autre part parce que cela ferait représenter trop d’établissements sur la carte.

La solution est d’exécuter la requête uniquement lorsque le niveau d’agrandissement est adéquat, on profite alors des capacités de l’indexe spatial. Au niveau de la vue sur la France entière, on pourra faire afficher au niveau de chaque département le nombre d’établissements à proximité de parcelles cultivées dans un symbole de cercle proportionnel. Nous devons au préalable compiler les informations pour les départements pour chaque option de distance proposée à l’utilisateur dans l’interface.

Vérifions en premier lieu la réactivité de la requête spatiale sur une étendue géographique limitée.

Limiter la requête spatiale PostGIS à une étendue géographique

ST_MakeEnvelope – Crée un polygone rectangulaire formé à partir des minimums et maximums donnés. Les valeurs en entrée doivent être spécifiées dans le système de coordonnées géographiques spécifié par le SRID.

Sous PostGIS on peut utiliser la fonction ST_MakeEnvelope(left, bottom, right, top, srid) pour construire l’enveloppe de sélection de l’étendue géographique, qui associée à l’opérateur && sur la géométrie de l’objet permet de trouver l’intersection entre l’enveloppe et la géométrie.

CREATE INDEX intersect_etabs_cul_geom_gist
  ON intersect_etabs
  USING gist
  (cul_geom);
SELECT * FROM intersect_etabs 
WHERE cul_geom && ST_MakeEnvelope(0.23036956787109372,44.9590891448628,0.3277873992919922,45.00419734261587, 4326)
AND distance < 1000;

La requête s’exécute en 11 ms, ce qui est compatible avec une utilisation en temps réel sur la carte.

Compilation des données par département

L’objectif est de créer une table, relativement longue à générer mais très rapide à interroger.

WITH   proximites(a) AS ( VALUES ('{100, 250, 500, 750, 1000, 1250, 1500}'::int[]) )
SELECT generate_subscripts(a, 1) AS idx, unnest(a) AS proximite
FROM   proximites;

Cette première requête permet de générer l’ensemble des proximités aux établissements proposées à l’utilisateur, elle peut être utilisée dans une requête plus complexe de création de table.

CREATE TABLE proximites AS (
WITH   proximites AS (
WITH generateur(a) AS ( VALUES ('{100, 250, 500, 750, 1000, 1250, 1500}'::int[]) )
SELECT generate_subscripts(a, 1) AS idx, unnest(a) AS valeur
FROM   generateur
)
SELECT p.valeur as proximite, count(distinct i.numero_uai) as intersections, i.code_dept 
FROM proximites p
LEFT JOIN intersect_etabs i
ON i.distance < p.valeur
GROUP BY p.valeur, i.code_dept
ORDER BY p.valeur, i.code_dept
)

L’évolution de la table pour d’autres valeurs est simple à réaliser…

INSERT INTO proximites (
SELECT '250'::text as proximite, count(distinct i.numero_uai) as intersections, i.code_dept 
FROM intersect_etabs i 
WHERE i.distance < 250 
GROUP BY i.code_dept
)

Vérifions que les résultats sont conformes aux attentes de réactivité.

select intersections, code_dept from proximites where proximite = '250';

Nous sommes bien sous les 10 ms.

Nous avons compilé les données et examiné les requêtes spatiales qui vont permettre l’interactivité avec l’utilisateur depuis la carte. Nous pouvons désormais utiliser la table des intersections entre établissements et cultures sur une étendue géographique donnée, et à niveau de détail moins élevé la table qui recense le nombre d’établissements en fonction de la proximité aux cultures pour chaque départements.

Voyons comment représenter dynamiquement ces informations sur la carte.

Représenter les résultats JSON de requêtes spatiales

Le principe est simple. Une carte est crée qui constitue la vue par défaut. Une interaction utilisateur va déclencher le changement de la vue pour s’adapter au contexte. Techniquement un écouteur d’événements est mis en place dans le code Javascript qui s’exécute côté client sur le navigateur Internet.

Au déclenchement d’un événement, un appel Javascript asynchrone interroge un script distant qui retourne l’information au format JSON. Le script distant a pour responsabilité de faire des requêtes spatiales sur la base de données et mettre en forme le contenu au format d’échange.

Le contenu retourné peut comporter des informations spatiales à représenter sous la librairie Javascript, dans ce cas le contenu d’échange sera du JSON, qui pourra inclure des objets GeoJSON. Nous utiliserons ce principe pour récupérer les points qui permettrons de localiser et représenter les établissements dans la vue détaillée, toutefois ce n’est pas obligatoire : en effet dans notre cas nous avons déjà interrogé le serveur cartographique depuis le service WFS afin de disposer des éléments de représentation des départements, pourrait nous suffire dans la vue générale de l’information de décompte des établissements par département. Le décompte pourrait très bien être affiché par rapport à la géométrie des départements déjà connue.

Script de proxy à la base de données PostgreSQL

Le script distant agit comme un proxy, il permet de masquer les informations de connexion à la base de données et isoler les traitements sur la base. Pour l’application le script sera développé en langage PHP, mais évidemment n’importe quel autre langage au travers d’un serveur Web peut faire l’affaire.

Afin de simplifier le prototype j’utiliserai un script par requête spatiale. Sur un projet d’envergure on peut évidemment préférer mettre en place un contrôleur frontal.

Le premier script va effectuer une requête qui permettra de représenter le nombre d’établissements à proximité de cultures pour chaque département lorsque le niveau d’agrandissement de la carte laisse apercevoir une étendue géographique relativement large, de l’ordre de dimension d’un quart de la France métropolitaine.

Le rendu sera le suivant

Représentation par cercles proportionnels

L’analyse par symboles proportionnels sert à représenter un indicateur quantitatif en valeurs absolues (nombres, quantités, surfaces…). Ici je ne peux pas afficher en temps réel les 60 000 établissements sur la carte de France, aussi j’utiliserai des cercles proportionnels pour donner une première indication d’un nombre d’établissements adjacents aux cultures pour la distance renseignée dans le formulaire de paramétrage de la carte, pour chaque département. Cette première représentation est minimaliste et peu utile – c’est plutôt le savoir faire qui m’intéresse ici-, elle ne prend pas en compte les autres paramètres afin de pouvoir représenter un minimum d’informations en temps réel.

Chaque valeur de l’indicateur est représentée par un symbole dont la surface est proportionnelle à la valeur représentée, notre indicateur sera le décompte d’établissements adjacents.

Ces symboles proportionnels sont ajoutés sur la carte par ordre décroissant de sorte que les symboles les plus petits soient au-dessus des plus gros.

Interroger POSTGIS en PHP et retourner un résultat JSON / GeoJSON

Le script est enregistré à la racine du répertoire web du serveur Apache, il se nomme action1.php

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

$conn = new PDO('pgsql:host=localhost;dbname=Pesticides', 'user', 'password');
    
$distance = isset($_GET['distance']) ? (int) $_GET['distance'] : 250;

$sth = $conn->prepare('
    select p.intersections, p.code_dept, d.nom_dept, 
    ST_AsGeoJSON(ST_Centroid(d.geom)) as centroid
    from proximites p
    inner join "Departement" d
    ON p.code_dept = d.code_dept 
    where proximite = :proximite  
    order by p.intersections desc'
);
$sth->execute(array(':proximite' => $distance));
$result = $sth->fetchAll(PDO::FETCH_OBJ);

header('content-type: application/javascript; charset=utf-8');
header("access-control-allow-origin: *");
echo filter_var($_GET['callback'], FILTER_SANITIZE_ENCODED), '(', json_encode($result), ');';

Que fait ce script ?

Premièrement il vérifie la présence dans les paramètres d’url d’un paramètre nommé callback. Ce paramètre est simplement le nom de fonction qui encapsule l’objet JSON retourné (format JSONP). J’ai déjà expliqué ce principe, vous pouvez vous référer aux parties 7 ou 8 de ce document.

Le script ouvre une connexion locale sur le serveur PostgreSQL hébergé sur la même machine, puis il exécute une requête préparée sur la connexion ouverte avec le paramètre de distance fourni afin de récupérer les informations qui serviront à la représentation. Les informations de requêtes sont récupérées sous la forme d’un objet PHP grâce au paramètre PDO::FETCH_OBJ qui sera intégralement converti au format JSON par la fonction PHP json_encode().

Finalement le script renvoi les entêtes de contenu javascript et celles d’autorisation d’accès depuis un hôte distant, puis affiche le contenu de retour JSON.

Si l’on s’attarde sur la requête spatiale, vous pouvez constater que les champs code département, nom du département, et décompte sont retournés tels quels en valeurs de chaînes de caractères ou entiers.

Pour la représentation je récupère une géométrie sous forme de POINT, le centre du polygone qui défini un département.

ST_AsGeoJSON(ST_Centroid(d.geom)) as centroid

ST_Centroid – Retourne le centre géométrique d’une entité géométrie, dans le cas du département il retourne un point aux coordonnées du centre de masse de la géométrie (barycentre 2D du polygone qui matérialise les contours du département)

Le point central est transformé en notation GeoJSON pour la commodité du format d’échange.

ST_AsGeoJSON — Retourne une géométrie en tant qu’élément GeoJSON. Notez que cette fonction peut prendre en argument un nombre maxdecimaldigits qui permet de préciser la précision décimale souhaitée, une précision plus faible à notamment pour intérêt d’alléger le volume de données transférées.

Afin de respecter l’ordre de représentation des cercles, la requête effectue un tri sur le décompte, les plus petits cercles seront les derniers représentés dans l’ordre du résultat de recherche.

Exploiter le résultat de requête spatiale POSTGIS depuis un appel Ajax (côté client Javascript)

Le script PHP sert de proxy, il effectue une requête sur la base de données et retourne un résultat au format JSONP où les données spatiales sont embarquées avec les autres éléments de données mais au format GeoJSON.

Côté client Javascript il suffit donc d’appeler le proxy et traiter le résultat lorsque l’interaction avec l’utilisateur requiert de rafraîchir les informations. Nous avons deux possibilités d’interactions : soit l’utilisateur agit sur les contrôles de navigation de la carte afin de changer le niveau d’agrandissement ou de se déplacer vers une autre étendue géographique, soit il agit sur le formulaire de paramétrage pour modifier les conditions de rendu (dans ce cas uniquement la distance de proximité).

Chaque événement va provoquer le rafraîchissement de la vue en cours, à cette fin on va utiliser une fonction qui permettra de réinitialiser les couches dynamiques de la carte.

var dynamicFeatures = new L.featureGroup([]).addTo(map);

Sous Leaflet nous pouvons déclarer chaque couche représentée dynamiquement sous un groupe qui permet de manipuler toutes les couches au travers d’un seul élément. Le groupe est initialisé dans le contexte global et ajouté à la carte.

function refreshView() {
    dynamicFeatures.clearLayers(); 
...
    if (map.getZoom() < 10)
        getDecompteDepartemental();
...
}

La fonction qui rafraîchit la vue en cours commence par supprimer toutes les couches du groupe. Ensuite en fonction du niveau d’agrandissement de la carte on va rechercher les informations souhaitées. L’affichage de cercles proportionnels au niveau départemental est déclenché sous un niveau de zoom à 10, au dessus de ce niveau on choisira un autre mode de représentation.

$("#settings-form").on("change submit", function(event) {
  event.preventDefault();
  refreshView();
});

JQuery permet de créer un écouteur d’événement sur le formulaire de paramètres d’identifiant #settings-form. Les événements change et submit, déclencheront le rafraîchissement des couches dynamiques lors d’un changement sur l’un des champs du formulaire ou lors de sa soumission.

map.on('load zoomend moveend', function() {
    refreshView();
});

L’API Leaflet permet de définir directement un écouteur d’événements sur la carte. Ici on déclenche le rafraîchissement de la vue lors du chargement, lors du changement de niveau d’agrandissement, et lors d’une opération de glisser déposé.

Ces premiers éléments en place, on peut implémenter la fonction qui va représenter les cercles proportionnels.

function getDecompteDepartemental() {

    var distance = $("#distance").val();

    var query = host + '/action1.php?callback=getJson&distance=' 
            + encodeURIComponent(distance);

    $.ajax({
        url: query,
        dataType: 'jsonp',
        jsonpCallback: 'getJson',
        success: function (response) {
...
        }
    });
}

Le mécanisme est toujours le même (voir les documents antérieurs pour adaptation à la librairie OpenLayers 3), premièrement je construis l’url vers le script de proxy, cette url accepte deux paramètres, le nom de la fonction de rappel exploitée par le format JSONP et la distance récupérée en JQuery depuis le champ de sélection du formulaire qui possède l’identifiant #distance.

Un appel asynchrone permet d’obtenir la réponse du script de proxy, et en cas de succès on peut procéder à l’exploitation des résultats dans le format de réponse pour représenter les couches de données sur la carte.

En cas de succès, nous avons besoin de la valeur minimale (ou maximale) parmi les valeurs de décompte, ce minimum est exploité pour le calcul du rayon de chaque cercle.

...
            var values = [];
            for (var key in response)
                values.push(parseInt(response[key].intersections));

            var min = Math.min.apply(null, values);
...

Pour représenter chaque cercle il faut parcourir le résultat de requête, et pour chaque ligne de résultat créer un cercle proportionnel au décompte ayant pour centre le point (la géométrie) retournée au format GeoJSON qui correspond au centre de masse du polygone départemental. Ce cercle est une couche vectorielle qui est ajoutée au groupe d’éléments dynamiques pour être représentée sur la carte.

...
            for (var key in response) {
                var obj = response[key];
                var geometry = JSON.parse(obj.centroid);
                var circle = L.circleMarker([geometry.coordinates[1], geometry.coordinates[0]], {
                    color: 'white',
                    fillColor: 'Orange',
                    fillOpacity: 0.25,
                    opacity: 1
                });
                circle.setRadius(getRadius(parseInt(obj.intersections), min));
                circle.bindPopup(obj.nom_dept + " : " 
                        + obj.intersections + " établissement(s) à proximité" );
                
                dynamicFeatures.addLayer(circle);
            }
...

Pour chaque élément de réponse, on assigne l’élément à un objet nommé obj. Cet objet contient les propriétés telles que définies dans notre requête SQL, c’est à dire : intersections, code_dept, nom_dept, centroid. Les intersections sont un nombre, le décompte, le code département et le nom de département sont des chaînes de caractères, et le centroid un point au format GeoJSON, le format GeoJSON est parcouru pour être transformé en objet javascript à l’aide de la fonction Javascript JSON.parse(). Depuis ce nouvel objet on peut récupérer les coordonnées de latitude et longitude du point pour la représentation sous forme de cercle.

Le rayon du cercle est défini par rapport au décompte et au décompte minimal puis un popup est rattaché au cercle, il s’affichera au clic sur le cercle. Le contenu du popup est au format html, il contient des informations relatives au départements récupérées depuis le résultat de requête.

Dernier point crucial : le cercle est ajouté au groupe que l’on a créé en amont et qui est déjà inclus dans la carte, ce qui provoque l’affichage de l’élément sur la carte.

Concernant le calcul du rayon pour la représentation du cercle je vous communique les fonctions sans entrer dans les détails. Je ne suis pas entièrement satisfait du résultat lors de changements d’échelles de la carte et je vous invite à proposer une solution améliorée.

...
// calcule le rayon du cercle proportionnel
// Retourne le rayon du plus petit cercle

function getMinRadius() {
    var bounds = map.getPixelBounds();
    return Math.floor((bounds.max.x - bounds.min.x) * 0.005);
//    var size = map.getSize();
//    return Math.floor(size.x * 0.005);
}

// Retourne le rayon du cercle
// n est la valeur
// min est la valeur minimale

function getRadius(n, min) {
    return Math.floor(getMinRadius() * Math.sqrt(n / min));
}
...

Sur le même principe nous pouvons récupérer et représenter des géométries plus complexes, c’est exactement ce dont on a besoin à un niveau d’agrandissement élevé pour représenter un établissement.

Un établissement est représenté par un marqueur de position, une icône, la distance de proximité à l’établissement est matérialisée par un cercle opaque autour de ce point, et l’intersection entre les cultures et ce disque est mise en évidence par une couche opaque superposée.

Le script de proxy, nommé action2.php est très similaire au script précédent, seul le traitement des paramètres d’url et la requête à la base changent.

La requête SQL est un cas très interessant, riche en enseignements, et c’est le coeur du projet aussi nous allons la passer en revue. Je vais ignorer le traitement en amont des paramètres pour être plus succint.

...
$bbox = explode(',', $_GET['bbox']);

$sql = '
WITH resultats AS (
SELECT 
e.numero_uai, e.appellatio, e.adresse_ua, e.code_post, e.localite_a, e.nature_uai,
ST_AsGeoJSON(ST_Transform((e.geom), 4326), 6) AS etablissement,
floor(sum(ST_Area(CAST(cul_geom As geography)))) * POWER(10, -4) as surface_totale_cultures,
floor(sum(ST_Area(CAST(ST_Intersection(ST_Buffer(e.geom::geography, :distance)::geometry, cul_geom) As geography)))) * POWER(10, -4) as surface_culture_proximite,
ST_AsGeoJSON(ST_Transform((ST_Union(cul_geom)), 4326), 6) AS cultures,
ST_AsGeoJSON(ST_Transform((ST_Union(ST_Intersection(ST_Buffer(e.geom::geography, :distance)::geometry, cul_geom))), 4326), 6) AS surfaces_proximite,
count(i.num_ilot) as nombre_parcelles,
count(distinct c.cult_maj) as types_culture 
FROM intersect_etabs i 
INNER JOIN "Etablissement" e 
ON i.numero_uai = e.numero_uai 
INNER JOIN master_cultures c
ON i.num_ilot = c.num_ilot  
WHERE cul_geom && ST_MakeEnvelope(:xmin, :ymin, :xmax, :ymax, 4326)
AND distance < :distance 
AND c.cult_maj not in (18, 19)  -- Elimine les prairies
AND e.nature_uai < 800          -- Elimine les etablissements administratifs
AND e.etat_etabl = 1            -- Elimine les etablissements fermes
' . $whereClause . ' 
group by e.numero_uai  
order by e.numero_uai
)
SELECT *
FROM resultats
WHERE surface_culture_proximite > :surface
' . $aggregateWhereClause . ' 
ORDER BY types_culture DESC, surface_culture_proximite DESC, nombre_parcelles DESC
';

$sth = $conn->prepare($sql);
$sth->execute(array(
    ':distance' => $distance,
    ':surface' => ($surface * pow(10, -4)),
    ':xmin' => $bbox[0],
    ':ymin' => $bbox[1],
    ':xmax' => $bbox[2],
    ':ymax' => $bbox[3],
));
...

Cette requête est assez monolithique à première vue, nous pouvons la découper pour appréhender le fonctionnement.

...
WITH resultats AS (
SELECT 
...
ST_AsGeoJSON(ST_Transform((ST_Union(ST_Intersection(ST_Buffer(e.geom::geography, :distance)::geometry, cul_geom))), 4326), 6) AS surfaces_proximite,
...
group by e.numero_uai
)
SELECT *
FROM resultats
WHERE surface_culture_proximite > :surface
...

L’application permet de sélectionner une surface dans le formulaire de paramètres, l’un des objectifs est donc de déterminer la surface totale impactée dans le rayon de proximité autour de l’établissement toutes parcelles confondues pour appliquer un filtre par rapport au paramètre.

Pour calculer la surface il est nécessaire d’agréger les données de parcelles pour chaque établissement. L’opération d’agrégat est réalisée par l’instruction SQL GROUP BY, le résultat d’une requête de type SELECT va retourner les résultats agrégés, une autre requête est alors exécutée sur le résultat de la précédente pour appliquer les paramètres de filtre.

L’imbrication de requêtes est réalisée par les instructions WITH, AS de PostgreSQL, les clauses de conditions stipulent de retourner les résultats agrégés pour lesquels la surface est supérieure à la surface paramétrée (c’est également au niveau de ces clauses SQL que l’on insère si besoin la restriction d’adjacence à plusieurs cultures)

Comment aggréger des objets géométriques ?

Dans notre cas on définit en premier une zone tampon autour du point qui matérialise l’établissement avec la fonction ST_Buffer, que nous avons déjà employé précédemment.

ST_Buffer(e.geom::geography, :distance)::geometry

ST_Intersection — Retourne une géométrie qui représente la portion commune de deux géométries, ici elle est utilisée sur la géométrie de la zone tampon et celle de la parcelle pour ne conserver que la surface en commun.

ST_Union — Retourne une géométrie qui représente le jeu de points d’union ensembliste des géométries, elle va réunifier toutes les intersections de la zone tampon avec les cultures à proximité de l’établissement.

ST_Transform – Retourne une nouvelle géométrie avec ses coordonnées transformées pour le SRID référencé par le paramètre entier, dans notre cas le SRID 4326 permet de travailler selon nos attentes depuis la librairie Javascript.

Une fois ces informations comprises, le reste de la requête est presque trivial. Expliquons les deux derniers points de difficulté.

SELECT ... floor(sum(ST_Area(CAST(cul_geom As geography)))) * POWER(10, -4) as surface_totale_cultures,

L’information retournée comporte une indication de la surface totale des parcelles à partir du moment où la parcelle répond aux paramètres de recherche. La surface est calculée depuis un objet géographie.

L’opération de conversion de type permet de transformer la géométrie de la parcelle en géographie, la fonction ST_Area va retourner l’aire de l’objet, tandis que la fonction SUM va additionner les aires.

Au final la somme des aires est arrondie à l’entier le plus bas et multipliée par la puissance de 10 à l’exposant -4 afin de retourner une quantité exprimée en hectares.

La fonction ST_MakeEnvelope (voir plus haut) restreint la requête aux résultats sur l’étendue géographique passée en paramètres depuis le client Javascript.

    var query = host + '/action2.php?callback=getJson&' + $("#settings-form").serialize()
        + '&bbox=' + bounds.toBBoxString();

Les paramètres sont construits depuis la fonction d’appel, et le résultat de requête est traité à nouveau en cas de succès.

L’Education Nationale a mis à disposition un répertoire des établissements scolaires, l’application permet de renseigner le numéro d’établissement dans le formulaire de recherche.

Pour clôre ce chapitre, voici côté client la fonction de représentation des établissements, suivie d’une copie d’écran du rendu sur la carte.

var premierDegre = L.MakiMarkers.icon({icon: "school", color: "#b0b", size: "m"});
var secondDegre = L.MakiMarkers.icon({icon: "school", color: "#E2492F", size: "m"});

function getEtablissements(bounds) {

    var distance = $("#distance").val();

    var query = host + '/action2.php?callback=getJson&' + $("#settings-form").serialize()
        + '&bbox=' + bounds.toBBoxString();

    $.ajax({
        url: query,
        dataType: 'jsonp',
        jsonpCallback: 'getJson',
        success: function (response) {

            for (var key in response) {
                var obj = response[key];

                // Représente l'établissement par un marqueur de position
                
                var etablissement = JSON.parse(obj.etablissement);
                var coordinates = etablissement.coordinates[0];
                var icon = (obj.nature_uai < 300) ? premierDegre : secondDegre;
                var link = "<a href='http://www.education.gouv.fr/bce/index.php?simple_public=" 
                        + obj.numero_uai + "'>" + obj.appellatio + "</a>"
                
                var marker = L.marker([coordinates[1], coordinates[0]], {icon: icon})
                    .bindPopup(
                        link + "<br /> " 
                        + (obj.adresse_ua ? obj.adresse_ua + ', ' : '') 
                        + obj.code_post + ' ' + obj.localite_a + "<br /> " 
                        + 'Surface à proximité : ' + obj.surface_culture_proximite  + " ha<br /> " 
                        + 'Surface totale parcelles : ' + obj.surface_totale_cultures + " ha<br />"
                        + obj.nombre_parcelles + ' parcelle(s)' + "<br />"
                        + obj.types_culture + ' type(s) de cultures' + "<br />"
                        );

                // La zone tampon autour de l'établissement est représentée par un cercle

                var buffer = L.circle([coordinates[1], coordinates[0]], distance, {
                    color: 'red',
                    fillColor: '#f03',
                    fillOpacity: 0.25,
                    opacity: 0.65,
                    weight: 1
                });

                var parcelles = L.geoJson(JSON.parse(obj.cultures), {
                    style: {
                        "color": "#ff7800",
                        "weight": 1,
                        "opacity": 0.65
                    }
                });
                
                var intersections = L.geoJson(JSON.parse(obj.surfaces_proximite), {
                    style: {
                        "color": "red",
                        "weight": 1,
                        "opacity": 0.95
                    }
                });
                
                dynamicFeatures.addLayer(parcelles);
                dynamicFeatures.addLayer(intersections);
                dynamicFeatures.addLayer(buffer);
                dynamicFeatures.addLayer(marker);   
                
                if (map.getZoom() > 15)
                    getJsonLayerParcelles(bounds);
            }
        }
    });
}

Fonctionnalités annexes spécifiques Leaflet

Pour perfectionner un peu la carte, j’ai ajouté un ensemble de fonctionnalités.

Lorsque l’utilisateur arrive sur la carte, j’ai trouvé utile de positionner la carte sur sa localisation.

Cela peut se faire nativement sous Leaflet.

map.locate({setView: true, maxZoom: 15});

La barre latérale est par défaut repliée, ce qui n’est pas intuitif pour l’utilisateur mais si l’on déplie la barre par défaut il n’est pas intuitif de la replier non plus.

En attendant un boutton de fermeture sur cette extension, j’ouvre la barre latérale au chargement et je la referme après quelques secondes. Les opérations sur la barre peuvent s’effectuer depuis les identifiants html des onglets.

setTimeout(function () {
    sidebar.open('home');
}, 1000);
setTimeout(function () {
    sidebar.close('home');
}, 4000);

Finalement, afin de donner à l’utilisateur la possibilité de passer en vue aérienne je fais appel aux serveurs WMS de l’IGN.

Conclusion

Au terme de cette série de documents sur la réalisation d’une application cartographique j’ai détaillé comment réaliser une carte dynamique depuis les données et leur mise à disposition, jusqu’à la représentation dynamique des données.

Techniquement je n’ai pas abordé par manque de temps la représentation d’une carte par chloroplèthe, ce sera l’objet d’un futur document, cette fois ci sous OpenLayers 3.

J’espère que cette application trouvera une utilité publique ou qu’à défaut ce travail motivera ce type de représentations.

Requête à un serveur cartographique sous client web Leaflet

Etude du code prototype projet Leaflet

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 Leaflet est de passer par le tutoriel de démarrage et d’explorer la librairie.

Concept

La composante de base de Leaflet est la carte (L.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

<div id="map"></div>

Ce conteneur possède un style d’affichage paramétré dans le fichier de style css/own_style.css, le style est paramétré ici pour un affichage à 100% de la taille de la fenêtre du navigateur.

...
	html, body, #map {
		height: 100%;
		width: 100%;
		padding: 0;
		margin: 0;
	}
...

Notez que la librairie Leaflet possède son propre fichier de style qui est chargé en ressource en entête de fichier html depuis un CDN.
Les fichiers de style des extensions Leaflet.markercluster et Leaflet.label sont également chargés, ainsi que la feuille de style personnalisée pour le projet.

        <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.3/leaflet.css" />
        <link rel="stylesheet" href="css/MarkerCluster.css" />
        <link rel="stylesheet" href="css/MarkerCluster.Default.css" />
        <link rel="stylesheet" type="text/css" href="css/own_style.css">
        <link rel="stylesheet" href="css/label.css" />

L.Map gère le niveau de zoom et la projection de la carte.

Pour obtenir les données à représenter pour une couche, Leaflet utilise des classes pour chaque type de couche vecteur, ou raster. Ces couches permettent d’alimenter la carte au travers de serveurs de tuiles, serveur WMS, images, données vectorielles et format GeoJSON. Voyons comment sont configurées les sources par défaut dans notre prototype.

Chargement des ressources javascript

Premièrement le prototype charge la librairie JQuery que l’on ne présente plus, puis la librairie Autolinker qui est un utilitaire qui permet de formater proprement des urls en javascript.

        <script src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
        <script src="js/Autolinker.min.js"></script>

Puis une il fait appel à la librairie Leaflet et à ses extensions, leaflet-hash, Leaflet.label et Leaflet.markercluster.

        <script src="http://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.3/leaflet.js"></script>
        <script src="js/leaflet-hash.js"></script>
        <script src="js/label.js"></script>
        <script src="js/leaflet.markercluster.js"></script>

Que font ces extensions ?

  • leaflet-hash va ajouter dynamiquement à l’url du navigateur une ancre qui permet aux utilisateurs de repérer une vue spécifique de la carte (par ex. #11/37.5508/-122.2895
  • Leaflet.label permet d’ajouter des étiquettes de texte aux marqueurs sur la carte et couches vectorielles
  • Leaflet.markercluster permet de manipuler des groupes de marqueurs de façon performante avec des animations Javascript

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 à la couche au format Géojson

<script src="data/exp_Departement.js"></script>

qui a pour contenu

var exp_Departement = {
   "type": "FeatureCollection",
   "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
   "features": [
 { "type": "Feature", "properties": { "color_qgis2leaf": 'none', "border_color_qgis2leaf": '#00ff00', "radius_qgis2leaf": 1.65, "transp_qgis2leaf": 0.75, "transp_fill_qgis2leaf": 1.0,   "id_geofla": 1, "code_dept": "01", "nom_dept": "AIN", "code_chf": "053", "nom_chf": "BOURG-EN-BRESSE", "x_chf_lieu": 8717, "y_chf_lieu": 65696, "x_centroid": 8814, "y_centroid": 65582, "code_reg": "82", "nom_region": "RHONE-ALPES" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 5.831226413621037, 45.938459578293219 ], [ 5.82212102507634, 45.930135953523816 ], [ 5.829154566881813, 45.917237123525148 ], ... } },
                   ...

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.

Une fois les données chargées, le programme passe au travail d’initialisation et de rendu de la carte.

Initialisation et rendu de la carte

var map = L.map('map', {zoomControl: true}).fitBounds([[36.1453640773, -8.27037589111], [54.0946095281, 6.49685600911]]);

Le script commence par créer un objet carte L.map, il passe en paramètre l’identifiant de l’élément div qui va contenir la représentation ainsi qu’un tableau d’options. L’option zoomControl permet d’indiquer que le contrôle de gestion de l’agrandissement sera intégré à la carte.

La méthode fitBounds() est chaînée à la méthode de création, elle définit la vue de carte qui contient les limites géographiques données avec le niveau de zoom maximum possible.
Les limites sont exprimées en couples de points, latitude et longitude.

var hash = new L.Hash(map);

Cette instruction permet d’utiliser l’extension leaflet-hash pour ajouter dynamiquement une ancre nommée à l’url fonction de la vue en cours qui permet de partager facilement l’état de la vue sur la carte. Elle peut être ignorée.

Le script va ensuite construire la liste des couches à afficher, deux instructions se trouvent dans le code, l’une utilise un featureGroup, l’autre un LayerGroup. Seule la première est exploitée.

var feature_group = new L.featureGroup([]);

Va déclarer un groupe qui permet de manipuler plusieurs couche à la fois au sein d’un seul élément. Ce groupe gère les événements souris.

La première couche sert à la représentation de la carte de base, le fond de carte.

            var basemap_0 = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                attribution: additional_attrib + '&copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors,<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'});
            basemap_0.addTo(map);

L.tileLayer indique de charger une source de données depuis un serveur de tuiles (images pré-rendues).
La source de données est configurée pour interroger les serveurs de tuiles d’OpenStreetMap.
Le paramètre attribution est une chaîne de caractère représentatif qui sera affiché dans le contrôle qui gère les attributions
Ces attributions font parties des conditions d’utilisation d’OSM.

La couche est ajoutée directement à la carte.

Chaque couche de données est alors crée l’une à la suite de l’autre avec une source de données, un style et d’autres paramètres puis elle est ajoutée au groupe… qui au final n’est pas non plus utilisé ! L’extension sous QGIS manque d’aboutissement, et on doit faire abstraction de beaucoup de code généré. A chaque ajout de couche, la couche est temporisée dans un tableau, toutes les couches de la carte sont retirées et réinsérées dans l’ordre du tableau… passons, nous ne conserverons que les instructions d’ajout de couche à la carte.

Si l’on considère la couche des départements.

            var exp_DepartementJSON = new L.geoJson(exp_Departement, {
                onEachFeature: pop_Departement,
                style: function (feature) {
                    return {color: feature.properties.border_color_qgis2leaf,
                        fillColor: feature.properties.color_qgis2leaf,
                        weight: feature.properties.radius_qgis2leaf,
                        opacity: feature.properties.transp_qgis2leaf,
                        fillOpacity: feature.properties.transp_qgis2leaf};
                }
            });
...
map.addLayer(exp_DepartementJSON);

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, exp_Departement, qui a été construit lors de l’export du projet QGIS et chargé en amont dans les ressources Javascript.

L.geoJson() étend la classe featureGroup qui permet de gérer les événements souris.

Le style de rendu de la couche vectorielle est déclaré lors de la construction, il est fonction des éléments de la couche vectorielle, il est donc appliqué individuellement à chaque objet de la couche. Par défaut le code récupère le style depuis les propriétés exportées de l’extension Qgis2leaf, par exemple dans le Geojson vous pouvez trouver une propriété border_color_qgis2leaf positionnée à la couleur #00ff00.

Je peux modifier le style pour impacter tous les éléments avec pour l’exemple une couleur de remplissage rouge et une opacité qui permet de superposer cette couche au fond de carte.

                style: function (feature) {
                    return {color: feature.properties.border_color_qgis2leaf,
                        fillColor: '#FF0000',
                        weight: feature.properties.radius_qgis2leaf,
                        opacity: feature.properties.transp_qgis2leaf,
                        fillOpacity: 0.2};
                }

Remarquez alors le popup qui se déclenche au clic souris sur un département.

L’option onEachFeature permet d’indiquer une fonction qui sera appelée à la création d’une caractéristique vectorielle (chaque objet de la couche déclaré dans le format Geojson), ce qui permet d’attacher des événements aux caractéristiques vectorielles. Dans le code généré, une fonction est définie pour chaque couche, ici pop_Departement. Ce code ne fonctionne sur mon navigateur que si je modifie les styles pour l’affichage des départements. Il permet de manipuler chaque élément, par exemple d’afficher la propriété qui correspond au nom du département au clic sur le département sur la carte.

            function pop_Departement(feature, layer) {
                var popupContent = 'Departement ' + feature.properties['code_dept'];
                layer.bindPopup(popupContent);
            }

La fonction prend en argument les données géométriques et la couche vectorielle. Il est alors possible de récupérer les propriétés de l’objet, la méthode bindPopup() attache un événement au clic qui affichera le contenu html en paramètre dans un popup.

Ajout de contrôles à la carte

Un contrôle L.control( est un widget représenté à l’écran. Un contrôle peut être seulement informatif ou bien peut permettre d’interagir avec l’utilisateur. Nous avons vu lors de la création qu’un contrôle qui permettait de manipuler le niveau d’agrandissement était ajouté lors de la déclaration. Si vous jetez un œil à la carte vous verrez en fait quatre autres contrôles : le titre, l’échelle, les attributions et le contrôle qui permet de sélectionner les couches à afficher ou masquer.

Ces contrôles sont initialisés et ajoutés manuellement à la carte.

Contrôle pour affichage du titre

            var title = new L.Control();
            title.onAdd = function (map) {
                this._div = L.DomUtil.create('div', 'info');
                this.update();
                return this._div;
            };
            title.update = function () {
                this._div.innerHTML = '<h2>Etablissements scolaires de Gironde a 50 m parcelle cultivee</h2>Test client web leaflet'
            };
            title.addTo(map);

Le titre n’est pas un contrôle prédéfini, c’est un contrôle basique qui va implémenter l’interface IControl, qui lui donne accès à la méthode onAdd(). L’aspect visuel du contrôle est construit à l’ajout du contrôle à la carte, le but de la méthode d’ajout est de créer tous les éléments du DOM nécessaire pour le contrôle et d’ajouter des écouteurs d’événements, elle renvoie l’élément contenant le contrôle.

L’instruction L.DomUtil.create(‘div’, ‘info’) va créer dans le DOM un élément DIV de classe info.

L’élément est ensuite manipulé pour insérer un titre html.

Le système est souple et accorde beaucoup de liberté, la position du contrôle est quand à elle fournie en option à la création, à l’ajout à la carte, ou encore précisée post création.

La position est paramétrable dans les angles (haut droit, bas droit, haut gauche, bas gauche). Les contrôles sont rendus par des styles css qui permettent de gérer les marges par exemple, remarquez que le contrôle d’affichage du titre et le contrôle de visualisation des couches sont tous deux dans le coin haut droit, et empilés l’un au dessous de l’autre.

Contrôle pour affichage de l’échelle

L.control.scale({options: {position: 'bottomleft', maxWidth: 100, metric: true, imperial: false, updateWhenIdle: false}}).addTo(map);

L.control.scale() est un contrôle prédéfini qui permet l’affichage d’une échelle.

Contrôle pour visualisation des couches

L.control.layers(baseMaps, {"etabparcelinters50m": exp_etabparcelinters50mJSON, "etabssel": exp_etabsselJSON, "etabstamponselec": exp_etabstamponselecJSON, "DepartementWFS": exp_DepartementWFSJSON, "Departement": exp_DepartementJSON}, {collapsed: false}).addTo(map);

L.control.layers() est un contrôle prédéfini qui permet de sélectionner les couches représentées ou pas. Ce contrôle possède une option collapsed qui permet de représenter le contrôle sous forme d’icône cliquable, le contrôle s’ouvre ou se referme au clic.

Contrôle pour affichage des attributions

Le contrôle est créé et positionné par défaut sur la carte à moins de demander explicitement le contraire lors de la création de carte grâce au paramètre attributionControl positionné à faux. Les attributions sont récupérées des options de chaque couche. (La valeur pour la couche OSM est renseignée à la création)

L.control.attribution() permet de créer le contrôle manuellement, afin par exemple de modifier sa position.

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 Leaflet

Le nom de la couche des départements nous intéresse particulièrement, on peut le récupérer depuis le fichier xml généré par la requête aux capacités du serveur.

<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>

Leaflet est en mesure de charger des sources de données depuis un serveur cartographique via le service WMS, il utilise à cette fin TileLayer.WMS.

Nous pouvons remplacer la source de données Geojson par notre source WMS, la déclaration de la source se fait ainsi :

            var wmsDepts = L.tileLayer.wms("http://92.222.27.205/cgi-bin/projet1/qgis_mapserv.fcgi", {
                layers: 'Departement',
                format: 'image/png',
                transparent: true,
            });

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 vectoriels, 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 Leaflet

Si l’on regarde attentivement le code généré par l’extension sous QGIS, la solution est déjà implémentée mais ne fonctionne pas, le code tente non pas d’accéder au service WMS mais bien au service WFS paramétré sous QGIS. Il va effectuer une requête asynchrone en javascript sur le service WFS (de type GetFeature), le résultat est exploité comme couche vectorielle.

Comment s’y prend-il ?

            var DepartementWFSURL = 'http://92.222.27.205/cgi-bin/projet1/qgis_mapserv.fcgi?SERVICE=WFS&VERSION=1.0.0&REQUEST=GetFeature&TYPENAME=Departement&SRSNAME=EPSG:4326&outputFormat=text%2Fjavascript&format_options=callback%3AgetDepartementWFSJson';
            DepartementWFSURL = DepartementWFSURL.replace(/SRSNAME\=EPSG\:\d+/, 'SRSNAME=EPSG:4326');

Premièrement l’url d’appel au service WFS est construite. Cette url comporte le paramètre format_options qui ne fait pas parti du standard WFS, mais est interprété sur un serveur cartographique Geoserver. Ce paramètre permet au serveur d’encapsuler les données au format JSON dans une fonction javascript, les données retournées sont donc indiquées au format de sortie text-javascript. Ce mécanisme, nommé sous l’appellation JSONP, permet de contourner les appels de requêtes sur domaines croisés.

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.

Corrigée pour le serveur QGIS, l’url qui permet de récupérer les objets de la couche des départements est la suivante :

var DepartementWFSURL = 'http://92.222.27.205/cgi-bin/projet1/qgis_mapserv.fcgi?SERVICE=WFS&VERSION=1.0.0&REQUEST=GetFeature&TYPENAME=Departement&SRSNAME=EPSG:4326&outputFormat=GeoJSON';

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.

Bien… ce problème réglé le code peut fonctionner comme attendu.

            var proxy = 'http://io.gchatelier.fr/proxy.php?callback=getDepartementJson&url=';
            var proxyURL = proxy + encodeURIComponent(DepartementWFSURL);

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

            var exp_DepartementWFSJSON = L.geoJson(null, {
                style: function (feature) {
                    return {color: '#00ff00',
                        fillColor: '#ff0000',
                        weight: 1.65,
                        opacity: 0.75,
                        fillOpacity: 0.10};
                },
                onEachFeature: function (feature, layer) {
                    var popupContent = 'Departement ' + feature.properties['code_dept'];
                    layer.bindPopup(popupContent);
                }
            });

Le code commence par créer une couche GeoJSON vide. Le mécanisme est le même que nous avons rencontré précédemment, avec application d’un style (sur mon navigateur le style de remplissage doit être renseigné pour que le popup fonctionne), et application d’une fonction javascript à exécuter sur chaque objet de la couche vectorielle. Comme précédemment on créé un popup au clic souris sur le département pour afficher son code.

Cette couche vide sera remplie ultérieurement par un appel javascript asynchrone qui va récupérer les objets de la couche vectorielle depuis le service WFS.

            var DepartementWFSajax = $.ajax({
                url: proxyURL,
                dataType: 'jsonp',
                jsonpCallback: 'getDepartementJson',
                success: function (response) {
                    L.geoJson(response, {
                        onEachFeature: function (feature, layer) {
                            exp_DepartementWFSJSON.addData(feature)
                        }
                    });
                }
            });

            exp_DepartementWFSJSON.addTo(map);

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
  • success : En cas de réussite une fonction anonyme est exécutée avec la réponse en paramètre, notre GeoJSON… la réponse est parcourue dans une couche L.geoJson qui duplique chaque objet dans la couche vide

Le développeur de l’extension d’export sous QGIS a fait le choix de dupliquer la couche en cas de réussite ce qui permet d’initialiser une couche vectorielle y compris au cas où la requête Ajax échoue.

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

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.

Utilisation des services WMS et WFS sous un client web Leaflet ou OpenLayers 3

Utilisation des services sous un client web

Nous avons vu comment créer une carte sous le logiciel client QGIS et comment publier le projet sous le serveur cartographique QGIS server au travers de deux types de services, WMS et WFS. Nous sommes donc en mesure de servir des données cartographiques au travers du Web. Ce n’est que la face cachée de l’iceberg, nous allons exploiter ces services pour construire une application de rendu de carte dynamique dans un navigateur web.

Les solutions clientes qui permettent le rendu de cartes interactives sur un navigateur sont des librairies en langage Javascript. Nous couvrirons deux de ces librairies : premièrement la librairie Leaflet qui a été conçue après l’apparition du HTML5 dans un souci de simplicité et d’efficacité, puis on s’attardera sur la référence dans le domaine, la librairie OpenLayers. Pour cette dernière j’emploierai la version 3 qui est une refonte moderne de la version 2, désormais obsolète pour les futur développements.

QGIS possède des extensions pour ces deux librairies qui permettent de générer un squelette de projet web très simplement à partir du projet en cours. Nous allons utiliser ces extensions qui nous facilitent le travail pour initialiser avec les bons paramètres les couches du projet à charger. Ces extensions permettent de générer dans un fichier au format Json les données de couches exploitées par la librairie javascript, alors que l’on souhaite interroger le serveur cartographique. Nous testerons la capacité de ces extensions à générer du code d’appel aux services WFS puis nous verrons en détail le code source nécessaire pour interroger un serveur distant.

Rappels sur le projet de test

Le projet de test est celui que nous avons publié sur le serveur cartographique. Il comporte cinq couches destinées à mettre en évidence les intersections des collèges et lycées avec des parcelles cultivées dans un rayon de 50 mètres, sur le département de la Gironde.

Les couches Departement et cultures_33 servent à représenter les limites administratives, et limites de parcelles. Elles sont récupérées depuis la base de données. Les autres couches sont des couches projet enregistrées au format fichier Shapefile, elles servent à matérialiser les établissements que l’on a identifiés : la couche etabs_sel permet d’afficher un point aux coordonnées de latitude et longitude de l’établissement, la couche etabs_tampon_selec sert à délimiter le périmètre autour de l’établissement et finalement la couche etab_parcel_inters_50m met en évidence l’intersection entre la parcelle de culture et la zone tampon de 50 mètres autour de l’établissement.

Préparation du projet de test

Dupliquons dans le projet la couche des départements chargée dans la base locale avec la même couche mais chargée cette fois depuis le serveur cartographique.
Cette manipulation permettra de tester la capacité des extensions QGIS à générer le code client pour l’exploitation du service.

Ouvrez le projet QGIS que vous avez publié sur le serveur cartographique et enregistrez le sous un autre nom
(ex. C:\SIG\projet_1\projet_1.qgs vers C:\SIG\projet_1\projet_1_distant.qgs)

Ajoutez la couche WFS des départements (Depuis l’icône à gauche ou bien Couche > Ajouter une couche WFS)

Vous devriez avoir désormais deux couches de départements… il vous suffit de copier le style de la première et de l’appliquer à la seconde.

Faites un clic droit dans l’explorateur de couche sur la couche initiale, sélectionnez l’entrée de menu Copier le style.
Procédez de même sur la couche de destination pour Coller le style.

Modifiez le nom de la couche de destination puis enregistrez !

Préparation du projet sous QGIS pour le client Leaflet

Sous QGIS, ouvrez le gestionnaire d’extension depuis le menu principal
Extension > Installer / Gérer les extensions
Recherchez et installez l’extension qgis2leaf
Ouvrez le projet QGIS que vous avez publié sur le serveur cartographique.
(ex. C:\SIG\projet_1\projet_1.qgs)

Depuis le menu principal vous pouvez alors créer une carte web
Internet > qgis2leaf > Export a QGIS project to a working leaflet webmap

Je n’ai renseigné qu’un minimum d’options afin dans un premier temps de générer le moins de code possible :

  • Appuyer sur le bouton Get Layers permet de charger vos couches dans l’extension. Vous pouvez sélectionner les couches à exporter, ici j’ai sélectionné toutes les couches
  • Le champ Frame width / height permet de déterminer les dimensions de la carte sur la page web, j’ai sélectionné le champ Full screen pour utiliser tout l’écran
  • Le champ Extent permet de définir l’étendue géographique initiale à l’arrivée sur l’application, par défaut elle est positionnée à l’étendue du canvas en cours, ici je me suis positionné sur une vue globale centrée sur la Gironde
  • Le champ Basemaps permet de faire une sélection multiple de cartes à importer pour le fond de carte, pour l’habillage j’utilise le fond de carte OpenStreetMap OSM Standard

Le projet est enregistré et ouvert dans un navigateur web, le résultat est fonctionnel on peut masquer des couches et zoomer. Par contre la couche des cultures est affichée quelle que soit l’étendue géographique, alors que l’on avait paramétré dans le projet sa visibilité. Le chargement très long de la page est lié à la quantité de données de cette couche chargée intégralement et sera réduit lorsqu’on chargera la couche depuis le service web en fonction de l’étendue géographique. Notez également que la couche WFS des départements n’est pas rendue…

Pour terminer avec les problèmes relevés : l’étendue initiale du canevas n’est pas respectée.

Ce premier rendu est prometteur, nous rentrerons dans un second temps dans le code qui a été généré, pour le moment voyons en parallèle comment arriver aux mêmes résultats avec une autre librairie.

Préparation du projet sous QGIS pour le client OpenLayer 3

Sous QGIS, ouvrez le gestionnaire d’extension depuis le menu principal
Extension > Installer / Gérer les extensions
Dans l’onglet Paramètres cochez la case pour afficher les extensions expérimentales.
Recherchez et installez l’extension Export to OpenLayers 3
Ouvrez le projet QGIS préparé précédemment. Vous pouvez alors créer une carte web OpenLayers3
Internet > Export to Open Layers > Create OpenLayers map

J’ai laissé les paramètres par défaut devant le peu de choix et l’absence d’aide à l’utilisation. L’extension s’exécute, cela requiert du temps à nouveau en raison de la couche des cultures qui représente 20 Mo de données au format Json. La carte possède les mêmes qualités et défauts que la carte générée pour la librairie Leaflet, c’est à dire que les cultures sont représentées indépendamment de l’échelle, le service WFS n’est pas non plus exploité car les données ont été converties au format Json.

Nous avons vu comment générer les squelettes des codes sources qui permettent d’utiliser des clients web pour l’affichage cartographique. Pour chaque solution nous allons explorer plus en détail le code afin d’expliquer le fonctionnement des librairies et comment nous pouvons remplacer les données générées en dur par des données issues des services que l’on a mis en place.