Webmapping : Affichage en temps réel de données géographiques depuis PostgreSQL

Et un polygone sur la carte, et encore un, puis un autre… le temps passant, vous regardez la carte se remplir de nouvelles données géographiques alors que vous êtes au téléphone avec un collègue, un client. On peut imaginer que ces polygones représentent des parcelles foncières mises en vente, des zones sous surveillance… et bien d’autres applications sont possibles. L’idée de cet article est de représenter des objets géographiques qui se chargent automatiquement sur une cartographie. Alors quels outils du Système d’Informations Géographiques (SIG) Open Sources sont utilisés?

D’un côté la base de données, de l’autre une interface webmapping et entre les deux un fichier PHP qui lit les données  géographiques en format GeoJSON, voilà en substance l’architecture SIG proposée. On note qu’aucun serveur cartographique n’est installé.

Dans un premier temps, on créé une table spatiale dans PostgreSQL (v2.6) / PostGIS puis on sélectionne les données en format GeoJSON. L’interface cartographique et le chargement automatique de la source de données sont réalisées grâce à l’API d’OpenLayers (v4) et de jQuery. Ici, les données géométriques et attributaires associées sont ajoutées dans la table depuis le logiciel QGIS. La figure 1 ci-dessous met en évidence les interactions entre les outils SIG mis en oeuvre.

Figure 1 : Schéma des interactions entre les outils SIG mis en oeuvre.

1. Création d’une table spatiale dans PostgreSQL / PostGIS

Dans cet article, la table spatiale créée est relativement simple sans géométrie particulière ni projection. On met en place une séquence afin d’incrémenter l’identifiant unique gid, ici la seule clé primaire. Un nom, une description, une photo puis le champs relatif à la géométrie sont ajoutés à la table. On a donc :

-- Table: public.demo

-- DROP TABLE public.demo;

CREATE TABLE public.demo
(
  gid integer NOT NULL, -- Identifiant unique
  nom character varying(255), -- Nom 
  description character varying(3000) DEFAULT NULL, -- Description 
  photo character varying(100) DEFAULT NULL, -- Photo 
  geom geometry DEFAULT NULL, -- Géométrie du point
  CONSTRAINT demo_pkey PRIMARY KEY (gid)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE public.demo OWNER TO postgres;

CREATE SEQUENCE demo_gid_seq
    START WITH 1
    INCREMENT BY 1
    NO MAXVALUE
    NO MINVALUE
    CACHE 1;


ALTER TABLE demo ALTER COLUMN gid SET DEFAULT nextval('demo_gid_seq'::regclass);

Dans QGIS, on charge la table vide puis quelques géométries sont dessinées. Des valeurs attributaires pour les différents champs sont associées aux différentes entités géographiques. On “jette un œil” rapidement à la table dans PostgreSQL pour vérifier que cette dernière s’est bien remplie (figure 2).

Figure 2 : Exemple d’entités géographiques dans PostgreSQL

Lisons maintenant les données géographiques avec un script PHP.

2. Lecture des données géographiques stockées en format GeoJSON en PHP

Le script PHP mis en évidence ci-après s’inspire fortement de celui réalisé pour générer des fichiers GeoJSON à la volée depuis PostgreSQL. Le plus important est la requête SQL qui permet de transformer les données géographiques de la table en objet avec la géométrie spatiale et les propriétés. S’il existe une erreur de connexion à la base de données ou une erreur liée à la requête, on retourne le string “erreur”.

<?php // Connection à la base de données
try {
		$dbconn=new PDO("pgsql:host=*;dbname=*","*","*") or die('Connexion impossible');
		$dbconn->exec("SET CHARACTER SET utf8");
		$dbconn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
// S'il existe un problème de connection, on obtient le message d'erreur
} catch(PDOException $erreur) {
		$erreur->getMessage(); // Supprimer en production
		echo 'erreur';
}
if($dbconn){
	// Exécution de la requête SQL GeoJSON
	$sqlGeoJSON="SELECT json_build_object(
		'type', 'FeatureCollection',
		'crs',  json_build_object(
			'type',      'name',
			'properties', json_build_object(
			'name', 'EPSG:4326')),
		'features', json_agg(
			json_build_object(
				'type',       'Feature',
				'id',         gid,
				'geometry',   ST_AsGeoJSON(geom)::json,
				'properties', json_build_object(
					'nom', nom,
					'description', description,
					'photo', photo
				)
			)
		)
	) AS objet_geosjon FROM demo;"; 
	$reqGeoJSON=$dbconn->prepare($sqlGeoJSON);
	$reqGeoJSON->execute(); 
	$dataGeoJSON=$reqGeoJSON->fetch();
	if($dataGeoJSON){
		$objetGeoJSON=$dataGeoJSON['objet_geosjon'];
		echo $objetGeoJSON;
	}else{
		echo 'erreur';
	}
}
?>

On ajoute une requête afin de s’assurer que la table contient au moins un objet géographique. Certes, cela n’est pas indispensable car la requête de l’objet GeoJSON retourne un résultat valide avec “features : null” mais, dans OpenLayers, lors de l’ajout des objets dans la couche vectorielle, la méthode readFeatures retourne une erreur. Ici, on utilise la fonction PDOStatement::rowCount qui renvoie le nombre de lignes prises en compte par la dernière exécution PDOStatement::execute().

<?php // Connection à la base de données
try {
		$dbconn=new PDO("pgsql:host=*;dbname=*","*","*") or die('Connexion impossible');
		$dbconn->exec("SET CHARACTER SET utf8");
		$dbconn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
// S'il existe un problème de connection, on obtient le message d'erreur
} catch(PDOException $erreur) {
		$erreur->getMessage(); // Supprimer en production
		echo 'erreur';
}
if($dbconn){
	$sqlCount="SELECT gid FROM demo";
	$reqCount=$dbconn->prepare($sqlCount);
	$reqCount->execute();
	$nbObjet=$reqCount->rowCount();
	if($nbObjet>0){
		// Exécution de la requête SQL GeoJSON
		$sqlGeoJSON="SELECT json_build_object(
			'type', 'FeatureCollection',
			'crs',  json_build_object(
				'type',      'name',
				'properties', json_build_object(
				'name', 'EPSG:4326')),
			'features', json_agg(
				json_build_object(
					'type',       'Feature',
					'id',         gid,
					'geometry',   ST_AsGeoJSON(geom)::json,
					'properties', json_build_object(
						'nom', nom,
						'description', description,
						'photo', photo
					)
				)
			)
		) AS objet_geosjon FROM demo;"; 
		$reqGeoJSON=$dbconn->prepare($sqlGeoJSON);
		$reqGeoJSON->execute(); 
		$dataGeoJSON=$reqGeoJSON->fetch();
		if($dataGeoJSON){
			$objetGeoJSON=$dataGeoJSON['objet_geosjon'];
			echo $objetGeoJSON;
		}else{
			echo 'erreur';
		}
	}else{
		echo 'NaN';
	}
}
?>

En production, la connexion à la base de données est évidemment protégée et si de nombreuses actions sont nécessaires sur la table (insert, update, delete, etc), il est alors intéressant de créer plusieurs classes dans un objet.

Les données géographiques sont donc mises à disposition en GeoJSON, format qui est pris en charge par OpenLayers.

3. Construction de l’interface de webmapping avec OpenLayers

Comme d’habitude, après le chargement des différentes bibliothèques d’OpenLayers, de JQuery, de Bootstrap et de Font Awesome dans l’en-tête du script, on divise le corps du script en deux parties : la première contenant la carte et la seconde les attributs des données géographiques ajoutés au fil du temps. Ensuite, dans le code JavaScript, le fond de carte OpenStreetMap et l’objet Map sont instanciés.

<!DOCTYPE html>
<html>
	<head>
		<title>GeoJSON</title>
		<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
		<script src="https://code.jquery.com/jquery-3.1.1.js"></script>
		<link rel="stylesheet" href="http://openlayers.org/en/v4.2.0/css/ol.css" type="text/css">
		<script src="http://openlayers.org/en/v4.2.0/build/ol.js"></script>
		<!-- Latest compiled and minified CSS -->
		<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
		<!-- Latest compiled and minified JavaScript -->
		<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
		<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">	
	</head>
	<body>
		<div class="col-lg-9">
			<div id="map"></div>
		</div>
		<div class="col-lg-3" id="liste">
		<div>
		<script>
			// osm
			var osm = new ol.layer.Tile({
				source: new ol.source.OSM(),
			});
			// déclaration de la carte
			var map = new ol.Map({
				layers: [osm],
				target: 'map',
				view: new ol.View({
					center: [0,0],
					zoom: 2
				}),
			});
		</script>
	</body>
</html>

Afin d’afficher les données géographiques en format GeoJSON, une stratégie de chargement des données loader est construite dans une fonction Ajax. L’URL du fichier PHP est précisé comme source de donnée de la couche SIG qui est ajoutée à l’objet Map. Pour égayer la carte, un style très coloré et digne d’une Full Moon Party thaïlandaise lui est affecté.

<!DOCTYPE html>
<html>
	<head>
		<title>GeoJSON</title>
		<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
		<script src="https://code.jquery.com/jquery-3.1.1.js" integrity="sha256-16cdPddA6VdVInumRGo6IbivbERE8p7CQR3HzTBuELA=" crossorigin="anonymous"></script>
		<link rel="stylesheet" href="http://openlayers.org/en/v4.2.0/css/ol.css" type="text/css">
		<script src="http://openlayers.org/en/v4.2.0/build/ol.js"></script>
		<!-- Latest compiled and minified CSS -->
		<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
		<!-- Latest compiled and minified JavaScript -->
		<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
		<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">	
		<style>
			#liste{
				height: 600px;
				overflow: auto;
				-webkit-overflow-scrolling: touch;
			}
			#liste p{
				padding: 5px 10px;
				border-bottom: 2px solid #e5e5e5;
			}
		</style>
	</head>
	<body>
		<div class="col-lg-9">
			<div id="map"></div>
		</div>
		<div class="col-lg-3">
			<h1>Réponse du fichier PHP</h1>
			<div id="liste"></div>
		<div>
		<script>
			// osm
			var osm = new ol.layer.Tile({
				source: new ol.source.OSM(),
			});
			// Fonction de chargement des données
			var fGeoJSON = function() {
				$.ajax('../geomatick/geojsonrefreshajx.php').done(function(reponse){
					if(reponse=='erreur'){
						$('#liste').html('<h3>Problème de lecture des données</h3>');
					}else if(reponse=='NaN'){
						$('#liste').html('<h3>Acune donnée</h3>');
					}else{
						var format = new ol.format.GeoJSON();
						var features = format.readFeatures(reponse,{featureProjection: 'EPSG:3857'});
						sourceDemo.addFeatures(features);
					}
				}).fail(function () {
					$('#liste').html('<h3>Problème de lecture des données</h3>');
				});
			}
			// source de la couche GeoJSON
			var sourceDemo = new ol.source.Vector({
				loader: fGeoJSON,
				strategy: ol.loadingstrategy.all
			});
			// style de la couche
			var style = new ol.style.Style({
				fill: new ol.style.Fill({
					color: 'rgba(0,255,0,0.7)',
				}),
				stroke: new ol.style.Stroke({
					width: 5,
					color: 'rgba(255,0,40,1.0)',
				}),
			});
			// déclaration de la couche
			var demo = new ol.layer.Vector({
				source: sourceDemo,
				style: style,
			});
			// déclaration de la carte
			var map = new ol.Map({
				layers: [osm,demo],
				target: 'map',
				view: new ol.View({
					center: [0,0],
					zoom: 2
				}),
			});
		</script>
	</body>
</html>

Ah oui, la cartographie est bien jolie! Mais comment fait-on pour afficher les données de la couche SIG sans recharger la page du navigateur? Pour cela, une nouvelle fonction Ajax s’en charge avec la classe setInterval() de jQuery. A chaque intervalle de temps donné, on efface la source des données GeoJSON précédemment insérées dans l’objet avec la classe clear(). La stratégie de chargement automatique des objets géographiques dans la couche SIG mise en place prend en compte de nouveau l’ensemble des données de la table.

// reload data
function refreshData(){
	sourceDemo.clear();
}
// Intervalle en ms
setInterval(refreshData, 5000);

Ensuite, la liste des attributs de la couche SIG est affichée dans la division prévue à cet effet. Pour cela, dans fGeoJSON, on déclenche une fonction dans laquelle on passe les objets géographiques chargés comme paramètres showData(features); . Dans cette fonction, on liste les propriétés de chaque objet par une boucle puis on les envoie dans la div liste en ordre d’arrivée par la méthode prepend() de jQuery.

function showData(features){
	$('#liste').empty();
	var nbFeatures = features.length;
	for(var i = 0; i < features.length; i++){
		var nom = features[i].S.nom;
		var description = features[i].S.description;
		var photo = features[i].S.photo;
		$('#liste').prepend('<h3><strong><i class="fa fa-caret-right"></i> '+nom+'</strong></h3><p><img src="'+photo+'" width="90%" alt="'+photo+'" class="img-thumbnail"/><br /><br />'+description+'</p>');
	}
}

Et voici le code final et un exemple d’interface de WebMapping (Figure 3):

<!DOCTYPE html>
<html>
	<head>
		<title>GeoJSON</title>
		<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
		<script src="https://code.jquery.com/jquery-3.1.1.js" integrity="sha256-16cdPddA6VdVInumRGo6IbivbERE8p7CQR3HzTBuELA=" crossorigin="anonymous"></script>
		<link rel="stylesheet" href="http://openlayers.org/en/v4.2.0/css/ol.css" type="text/css">
		<script src="http://openlayers.org/en/v4.2.0/build/ol.js"></script>
		<!-- Latest compiled and minified CSS -->
		<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
		<!-- Latest compiled and minified JavaScript -->
		<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
		<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">	
		<style>
			#liste{
				height: 600px;
				overflow: auto;
				-webkit-overflow-scrolling: touch;
			}
			#liste p{
				padding: 5px 10px;
				border-bottom: 2px solid #e5e5e5;
			}
		</style>
	</head>
	<body>
		<div class="col-lg-9">
			<div id="map"></div>
		</div>
		<div class="col-lg-3">
			<h1>Réponse du fichier PHP</h1>
			<div id="liste"></div>
		<div>
		<script>
			// osm
			var osm = new ol.layer.Tile({
				source: new ol.source.OSM(),
			});
			// Fonction de chargement des données
			var fGeoJSON = function() {
				$.ajax('../geomatick/geojsonrefreshajx.php').done(function(reponse){
					if(reponse=='erreur'){
						$('#liste').html('<h3>Problème de lecture des données</h3>');
					}else if(reponse=='NaN'){
						$('#liste').html('<h3>Acune donnée</h3>');
					}else{
						var format = new ol.format.GeoJSON();
						var features = format.readFeatures(reponse,{featureProjection: 'EPSG:3857'});
						sourceDemo.addFeatures(features);
						showData(features);
					}
				}).fail(function () {
					$('#liste').html('<h3>Problème de lecture des données</h3>');
				});
			}
			// source de la couche GeoJSON
			var sourceDemo = new ol.source.Vector({
				loader: fGeoJSON,
				strategy: ol.loadingstrategy.all
			});
			// style de la couche
			var style = new ol.style.Style({
				fill: new ol.style.Fill({
					color: 'rgba(0,255,0,0.7)',
				}),
				stroke: new ol.style.Stroke({
					width: 5,
					color: 'rgba(255,0,40,1.0)',
				}),
			});
			// déclaration de la couche
			var demo = new ol.layer.Vector({
				source: sourceDemo,
				style: style,
			});
			// déclaration de la carte
			var map = new ol.Map({
				layers: [osm,demo],
				target: 'map',
				view: new ol.View({
					center: [0,0],
					zoom: 2
				}),
			});
			// reload data
			function refreshData(){
				sourceDemo.clear();
			}
			// Intervalle en ms
			setInterval(refreshData, 5000);
			// on liste les attributs des données de la couche
			function showData(features){
				$('#liste').empty();
				var nbFeatures = features.length;
				for(var i = 0; i < features.length; i++){
					var nom = features[i].S.nom;
					var description = features[i].S.description;
					var photo = features[i].S.photo;
					$('#liste').prepend('<h3><strong><i class="fa fa-caret-right"></i> '+nom+'</strong></h3><p><img src="'+photo+'" width="90%" alt="'+photo+'" class="img-thumbnail"/><br /><br />'+description+'</p>');
				}
			}
		</script>
	</body>
</html>

Figure 3 : Exemple d’ajout d’entités depuis QGIS (à gauche) et de mise à jour automatique de l’interface de WebMapping (à droite).

En conclusion, on a vu comment rafraîchir automatiquement des données géographiques en webmapping. Ces dernières stockées dans une table PostgreSQL / PostGIS peuvent être ajoutées par différents logiciels tels QGIS, GDAL, etc. On peut aussi imaginer l’enregistrement des objets directement sur l’interface cartographique par :

  • l’ajout de fonctionnalités de dessin;
  • la prise en compte des données utilisateurs localisées comme l’IP et la géolocalisation.

Il est aussi possible d’améliorer ce script en ajoutant des interactions entre la liste des objets et les géométries affichées sur la carte.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *