Tout le monde a déjà ouvert une carte sur un site internet que ce soit :
- pour réserver un logement sur Airbnb ;
- pour choisir son point relais ou son magasin sur un site e-commerce ;
- consulter le marché immobilier sur DVF etalab ;
- ou simplement pour trouver son itinéraire.
A travers cet article, je vais vous montrer simplement comment sauvegarder et restaurer des données Géospatiales avec PostGIS et les afficher sur votre site en utilisant Google Maps ou OpenStreetMap à travers le format de données GeoJSON.
Système d'information géographique
Définition
Un Système d'Information Géographique (SIG) ou en anglais "Geographic information system (GIS)", est un système d'information qui stocke, traite et affiche des données géographiques.
Fournisseurs de carte
L'affichage de données géographiques se fait communément sur une carte.
Il vous faudra choisir un fournisseur de carte pour le fond.
Il en existe plusieurs, des payants comme des libres de droit.
Pour les fournisseurs payants, le leader c'est Google Maps,
ensuite viennent MapBox, Michelin, Bing Maps...
Pour les versions libres, la plus connue est OpenStreetMap (OSM).
Base de donnée Géospatiale
La plupart des systèmes de gestion de base de données (SGBD) ont un plugin GIS. Le plus populaire est PostGIS.
Il existe aussi H2Gis pour les tests en java par exemple.
Format de données GeoJson et KML
Il existe deux formats standards permettant d'échanger des données géographiques, ce sont le GeoJson et le KML.
GeoJson est un format simple et libre basé sur le JSON.
Le format GeoJSON peut encapsuler 3 types d'entité :
- Géométrie : est un type abstrait qui permet de définir des points, des courbes et des surfaces ;
- Particularité (Feature) : l'agrégation d'une géométrie et de propriétés ;
- Collection de Particularités (FeatureCollection).
Il est à noter que seuls les systèmes GIS acceptent que les polygones aient des trous.
Un modèle JAXB est disponible dans la dépendance sf-geojson pour java.
Un modèle TypeScript est disponible pour GeoJson.
KML est une alternative plus ancienne basée sur le format XML créé par Google pour Google Earth.
Il est plus complexe, car il sert aussi à faire de la modélisation en 3D.
GeoJson sera souvent préférable et suffisant avec son format JSON, son côté libre, et sa géométrie orientée planaire.
De manière générale, les fournisseurs de carte et les bases de données géospatiales gèrent de façon native les formats KML et GeoJSON.
Exemple d'utilisation de PostGIS
Créer des colonnes Géospatiales
Dans les bases de données Géospatiale, on peut définir des colonnes de type geometry
ou geography
, et de leurs sous-types.
Les géométries dans PostGIS sont des représentations cartésiennes sans référentiel par défaut (SRID=0).
Quant aux géographies, ce sont des représentations projetées dans un référentiel sphérique terrestre (SRID=4326).
Quand il est question de choisir l'un ou l'autre, le type geography
semble plus pertinent.
Cependant les calculs basés sur le type geometry
sont plus performants et offrent plus de possibilités.
Les géométries sont conseillées par PostGIS quand la rotondité de la Terre entre en jeu, c'est-à-dire au niveau terrestre,
comme calculer la distance d'un vol entre Melbourne et New York.
Exemple : Considérons une table ville.
CREATE TABLE town (
id VARCHAR(255) NOT NULL PRIMARY KEY,
label VARCHAR(255) NOT NULL,
surface geometry NOT NULL,
city_hall_position geometry(Point)
);
ou
CREATE TABLE town (
id VARCHAR(255) NOT NULL PRIMARY KEY,
label VARCHAR(255) NOT NULL,
surface geography NOT NULL,
city_hall_position geography(Point)
);
Index spatiaux
Pour des raisons de performance, il est conseillé d'utiliser les index spatiaux sur les géométries et sur les géographies pour optimiser certains calculs de la base de données.
Les index spatiaux de PostGIS utilisent une structure en R-Arbre.
C'est globalement un index sous la forme d'un arbre découpé en un rectangle très grand à la racine et en rectangles de plus en plus petit quand on tend vers les feuilles.
Ainsi chacune des géométries ou géographies est ainsi reliée au plus petit rectangle la contenant intégralement.
CREATE INDEX town__surface__idx
ON town
USING GIST (surface);
CREATE INDEX town__city_hall_position__idx
ON town
USING GIST (city_hall_position);
Modification de données Géospatiales
Pour modifier les données géospatiales, il faut utiliser de préférence les routines PostGIS ST_GeomFromText
, ST_GeogFromText
, ST_GeomFromGeoJSON
, ST_GeomFromKML
...
INSERT INTO town(id, label, city_hall_position, surface)
VALUES (
'1',
'Vatican',
ST_MakePoint(12.452936, 41.903390),
ST_GeomFromGeoJSON('{
"type": "MultiPolygon",
"coordinates": [[[
[12.458324, 41.902596], [12.458346, 41.901542], [12.457509, 41.901015],
[12.454569, 41.900329], [12.450857, 41.900616], [12.448754,41.900888],
[12.447767,41.900856], [12.445815, 41.901942], [12.447853, 41.903315],
[12.448926, 41.904289], [12.450042, 41.905647], [12.451437, 41.906733],
[12.455385, 41.907324], [12.455728, 41.906349], [12.457638, 41.90579],
[12.457595, 41.903363], [12.458324, 41.902596]
]]]
}')
);
Récupération de géométries ou géographie au format GeoJson
Une géométrie ou une géographie dans PostGIS est un objet limité à 1GB qui peut donc être volumineux.
Plus elle est précise, plus elle est volumineuse, plus elle est longue à transférer.
De manière générale, les jeux de données proviennent de fournisseurs dont la précision est fine, inférieure au mètre prêt.
Cependant quand une carte est affichée dans un navigateur, un pixel peut représenter de 10cm à 20Km selon le zoom et la résolution.
Pour cela, afin d'augmenter les performances, PostGIS fournit des fonctions de dégradation de géométries comme ST_simplify
et ST_reducePrecision
.
Dans le cas de géographie, il faudra convertir en géométrie.
Combiner une fonction de dégradation à une fonction de désérialisation de géométries comme ST_asText
, ST_asGeoJSON
ou ST_asKML
permet de récupérer rapidement des géométries à afficher dans une carte.
SELECT st_asGeoJSON(ST_reducePrecision(surface,0.001))
FROM town
WHERE id = '1';
Ou pour les géographies :
SELECT st_asGeoJSON(ST_reducePrecision(surface::geometry,0.001))
FROM town
WHERE id = '1';
{
"type":"Polygon","coordinates":[[
[12.458,41.902], [12.458,41.901], [12.455,41.9], [12.451,41.901], [12.449,41.901],
[12.448,41.901], [12.446,41.902], [12.448,41.903], [12.449,41.904], [12.45,41.906],
[12.451,41.907], [12.455,41.907], [12.456,41.906], [12.458,41.906], [12.458,41.903],
[12.458,41.902]
]]
}
Il est à noter que le polygone a été réduit de 17 points à 16 points !
Ensuite pour encapsuler la géométrie dans une feature et avoir un GeoJson complet, il faudra passer un enregistrement à st_asGeoJSON
.
SELECT st_asGeoJSON(reduce_town.*)
FROM
(
SELECT id, label, ST_reducePrecision(surface,0.001)
FROM town
WHERE id = '1'
) as reduce_town(id,name,geom);
{
"type": "Feature",
"geometry": {
"type":"Polygon",
"coordinates":[[
[12.458,41.902], [12.458,41.901], [12.455,41.9], [12.451,41.901], [12.449,41.901],
[12.448,41.901], [12.446,41.902], [12.448,41.903], [12.449,41.904], [12.45,41.906],
[12.451,41.907], [12.455,41.907], [12.456,41.906], [12.458,41.906], [12.458,41.903],
[12.458,41.902]
]]
},
"properties": {
"id": "1",
"name": "Vatican"
}
}
L'image ci-dessous illustre la différence entre la géométrie originale aux bords bleus et la géométrie dégradée aux bords marrons.
En utilisant cette technique sur des géométries plus denses, et des zooms ajustés à celles-ci, l'erreur est invisible.
Comment utiliser une carte sur une page web ?
Google Maps
Introduction à Google Maps
Pour faire des tests avec Google Maps, il vous faudra une API key !
Comme Google Maps est un service payant, il est conseillé de l'utiliser en asynchrone.
C'est à dire que la balise script
pointant vers Google Maps dans votre DOM est ajoutée quand la carte doit être affichée.
Voici un module NPM pour Google Maps en asynchrone qui ajoute une Loader
pour charger Google Maps en asynchrone.
npm install --save google-maps
Afficher Google Maps
Pour commencer, je crée ma page HTML dans le fichier google-maps.html
qui contiendra ma carte.
<html lang="fr">
<head>
<title>Demo Google Maps</title>
<link rel="stylesheet" type="text/css" href="map.css" />
</head>
<body>
<div id="map">Map is loading</div>
<script type="module" src="google-maps.ts"></script>
</body>
</html>
Il est important de définir les dimensions de la carte pour qu'elle soit visible à l'écran,
je crée mon fichier map.css pour afficher la carte sur tout l'écran.
html, body {
margin: 0;
}
#map {
width: 100vw;
height: 100vh;
}
Ensuite, je crée le module google-maps.ts
juste en centrant sur Paris pour commercer.
import {Loader, LoaderOptions} from 'google-maps';
const PARIS_CENTER: google.maps.LatLngLiteral = {lat: 48.866667, lng: 2.333333};
const options: LoaderOptions = {};
const loader: Loader = new Loader('YOUR_GOOGLE_MAP_API_KEY', options);
loader.load().then((google) => {
const map = new google.maps.Map(document.querySelector("#map") as HTMLElement, {
center: PARIS_CENTER,
zoom: 8
});
});
Voici la carte que j'affiche dans mon navigateur :
Afficher un GeoJSON dans Google Maps
D'abord, j'ai besoin d'un fichier GeoJSON.
Pour l'exemple, j'ai récupéré un fichier nommé limite-des-quartiers-de-lille-et-de-ses-communes-associees.geojson
sur data.gouv.fr que j'ajoute à la racine de mon projet.
Pour charger le GeoJSon, j'ai besoin d'appeler la méthode loadGeoJson
avec une URL.
map.data.loadGeoJson("./limite-des-quartiers-de-lille-et-de-ses-communes-associees.geojson");
Mais appelée seule, elle ne recentre pas ! Je dois glisser la carte puis zoomer pour voir Lille.
Pour recentrer automatiquement sur le contenu, Google Maps ne propose pas de solution directe, mais propose une méthode fitBounds
qui centre sur des limites données en paramètre.
Pour recentrer, je vais appeler la méthode loadGeoJson
avec un callback.
Ce callback calculera le plus petit rectangle englobant le GeoJSon chargé en parcourant l'ensemble des points définissants les géométries de celui-ci.
Les limites calculées seront finalement utilisées pour recentrer avec fitBounds
.
const map: google.maps.Map = new google.maps.Map(document.querySelector("#map") as HTMLElement);
map.data.loadGeoJson("./limite-des-quartiers-de-lille-et-de-ses-communes-associees.geojson" , null,
(features: Array<google.maps.Data.Feature>) => {
const bounds = new google.maps.LatLngBounds();
features.forEach((feature: google.maps.Data.Feature) => {
feature.getGeometry().forEachLatLng((point: google.maps.LatLng) => {
bounds.extend(point);
});
});
map.fitBounds(bounds);
});
OpenStreetMap avec OpenLayer
Introduction à OpenLayer
OpenLayer (ol) est un framework qui facilite la manipulation de cartes et d'y afficher des données géographiques sur plusieurs fournisseurs tels qu'OpenStreetMap, BingMap, CartoDB...
OpenLayer est basé sur 4 concepts :
- Source : les sources sont les objets permettant de récupérer des ressources externes (Fournisseur de carte, GeoJson, KML...) ;
- Vue (View) : la partie de la carte responsable de la zone à afficher (position, zoom) ;
- Couche (Layer) : les couches sont les représentations graphiques de sources ;
- Carte (Map) : la carte le composant principal qui contient les autres et qui l'affiche dans l'élément donné.
Pour créer une structure de projet OpenLayer
npm create ol-app nom-du-projet
Pour ajouter OpenLayer dans un projet existant
npm install --save ol
Afficher OpenStreetMap avec OpenLayer
Pour commencer, je crée une page open-street-map.html
semblable à celle pour Google Maps.
J'utilise le même CSS pour définir les dimensions de la carte.
Je change le titre de la page et l'URL du module.
<html lang="fr">
<head>
<title>Demo OpenStreetMap avec OpenLayer</title>
<link rel="stylesheet" type="text/css" href="map.css" />
</head>
<body>
<div id="map">Map is loading</div>
<script type="module" src="open-street-map.ts"></script>
</body>
</html>
Ensuite, je crée mon module open-street-map.ts
.
Celui importe le CSS de OpenLayer qui permet de placer les éléments au-dessus de la carte comme les boutons de zoom.
J'y convertis la longitude et la latitude de Paris dans le référentiel spatial d'OpenLayer.
import "ol/ol.css";
import Map from 'ol/Map.js';
import OpenStreetMap from 'ol/source/OSM.js';
import TileLayer from 'ol/layer/Tile.js';
import View from 'ol/View.js';
import {fromLonLat} from 'ol/proj.js';
const PARIS_CENTER: number[] = fromLonLat([2.333333, 48.866667]);
const map = new Map({
target: 'map',
layers: [
new TileLayer({
source: new OpenStreetMap(),
}),
],
view: new View({
center: PARIS_CENTER,
zoom: 8,
}),
});
Le résultat sur mon navigateur est celui-ci :
Afficher un GeoJSON dans OpenStreetMap avec OpenLayer
Pour afficher les quartiers de Lille sur la carte, je vais ajouter à la carte une couche vectorielle utilisant une source de type GeoJson.
Et je recentre plutôt facilement quand le GeoJSON est chargé.
import {Vector as VectorSource} from 'ol/source.js';
import {Vector as VectorLayer} from 'ol/layer.js';
import GeoJSON from 'ol/format/GeoJSON.js';
const vectorSource = new VectorSource({
url: 'limite-des-quartiers-de-lille-et-de-ses-communes-associees.geojson',
format: new GeoJSON(),
});
const vectorLayer = new VectorLayer({
source: vectorSource
});
map.addLayer(vectorLayer);
vectorSource.on('featuresloadend', (vectorSourceEvent:VectorSourceEvent) => {
map.getView().fit(vectorSource.getExtent(), {padding: [20, 20, 20, 20]});
});
Comme le contraste n'est pas assez net par défaut, je vais modifier ma couche vectorielle vectorLayer
qui utilisera une fonction pour définir les styles.
import {Fill, Stroke, Style} from 'ol/style.js';
import * as olColor from 'ol/color.js';
function styleFunction(feature) {
const FILL_COLORS: string[] = [ "red", "indigo" , "black" , "brown", "green", "purple", "grey", "darkorange", "hotpink", "white" , "salmon", "springgreen"];
if(! styleFunction.index) {
styleFunction.index = 0;
}
const colorName: string = FILL_COLORS[styleFunction.index++ % FILL_COLORS.length];
const [r, g, b] = olColor.asArray(colorName);
const colorWithAlpha = olColor.asString([r, g, b, 0.5]);
return new Style({
stroke: new Stroke({
color: 'black',
width: 2,
}),
fill: new Fill({
color: colorWithAlpha,
})
});
}
const vectorLayer = new VectorLayer({
source: vectorSource,
style: styleFunction,
});
Conclusion
En utilisant le format GeoJSON comme pivot, pour faire le lien entre votre frontend et PostGIS, il suffit juste qu'un backend transforme les requêtes HTTP en requêtes SQL géospatiales et qu'il soit un passe plat sur le retour entre PostGIS et le front.
Trouvez votre fournisseur de données public (IGN, ...) ou privé (MBI, GeoPostcodes...)!
Et c'est parti, vous avez toutes les briques qu'il vous faut pour ajouter des cartes sur votre site ou votre application !