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 
  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 public.demo ALTER COLUMN gid SET DEFAULT nextval('demo_gid_seq'::regclass);
CREATE INDEX ON public.demo USING GIST(geom);

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, lorsque la source du vecteur change, 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.

sourceDemo.once('change', function(evt){
	$('#liste').empty();
	var features = sourceDemo.getFeatures();
	var nbFeatures = features.length;
	for(var i = 0; i < features.length; i++){
		var nom = features[i].get('nom');
		var description = features[i].get('description');
		var photo = features[i].get('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
			sourceDemo.once('change', function(evt){
				$('#liste').empty();
				var features = sourceDemo.getFeatures();
				var nbFeatures = features.length;
				for(var i = 0; i < features.length; i++){
					var nom = features[i].get('nom');
					var description = features[i].get('description');
					var photo = features[i].get('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.

Partager l'article
Taggé , , , .Mettre en favori le Permaliens.

A propos Florian Delahaye

Passionné de Géomatique

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

  1. Hanaa KHOJ dit :

    Bonjour! Tout d’abord, je vous remercie beaucoup pour ce tutoriel.
    En effet, j’ai un projet d’étude sur le webmapping, et je n’ai pas trouvé comment enregistrer des entités dessinées dans postgresql (postgis) sous format geojson? Pourriez-vous m’aider?

    Merci d’avance

    • Florian Delahaye dit :

      Bonjour,

      Je vous remercie pour votre commentaire. Pour vous répondre, la création de modules de dessin et l’enregistrement des data dans la bdd via WFS ou directement en GeoJSON sont expliquées dans mes formations GeoServer et OpenLayers de niveau avancé. Ces contenus ne sont pour le moment pas délivrés gratuitement sur les tutoriels. Si vous avez un intérêt à suivre les formations, vous pouvez me contacter par email.

      Cordialement,

  2. Grégoire Coulon dit :

    Bonjour Florian,
    super tuto !

    En voyant le résultat, je me dis que c’est exactement ce que je cherche…toutefois, la reproduction (même en créant la même table postgis) n’aboutit pas. est-il possible de récupéeer le projet?

    En tout cas merci pour ces tutos ils sont rares et précieux. En attendant, je vais m’obstiner encore un peu !

    Bonne journée.

    • Florian Delahaye dit :

      Bonjour Grégoire,

      L’article date de 2017 et toutes les librairies prises en charge ici (OpenLayers, Bootstrap, etc.) ont évolué sur des nouvelles versions. Il faudrait donc adapter le code à ces nouvelles versions mais je vous conseille d’apprendre JavaScript avec NodeJS.

      Cdlt

  3. Grégoire Coulon dit :

    Re-bonjour, j’ai actualisés les liens ci-dessous, mais ça ne fonctionne toujours pas…je continue à chercher !

    Liens actualisés :

    A la place de :

    • emma dit :

      Bonjour Grégorie!
      Je me demande si vous avez trouvé une solution. Car j’ai le même problème. J’ai actualisé tous les liens mais je n’ai pas aboutit aux bons résultats.

  4. Sam dit :

    Bonjour.. Est-ce que cette méthode est-elle encore utilsable maintenant ??

    • Florian Delahaye dit :

      Bonjour Sam,

      Oui il est à possible d’appeler les données bancarisées en GeoJSON dans PG et de les afficher via une API de WebMapping comme OL.

      FD

Laisser un commentaire

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