Makina Blog
Générer des tuiles vectorielles sur mesure avec Django
Dans cet article nous allons voir comment générer dynamiquement des tuiles vectorielles utilisables par la bibliothèque de visualisation mapbox-gl-js à partir de données stockées dans un modèle GeoDjango.
Les dépendances
Commençons par installer les dépendances systèmes. L'adaptation aux distributions autres qu'Ubuntu est laissée en exercice au lecteur.
sudo apt-get install virtualenv postgis gdal-bin gcc python-dev libpq-dev wget p7zip
Puis les dépendances python (dans un environnement virtuel).
virtualenv .
./bin/pip install Django psycopg2 mapbox-vector-tile mercantile
La bibliothèque python mapbox-vector-tile va nous servir pour encoder les tuiles au format Mapbox Vector Tiles. Pour sa part, mercantile va nous servir à calculer les coordonnées géographiques des tuiles.
Le projet
Créons notre projet et notre app Django.
./bin/django-admin startproject makina .
./bin/python manage.py startapp tiles
Ajoutons notre app au projet dans makina/settings.py
.
INSTALLED_APPS = [
...
'tiles',
]
Et configurons la base de données dans ce même fichier.
DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': 'makina',
}
}
Enfin, créons la dite base de données.
sudo -u postgres psql -c "CREATE USER $USER SUPERUSER;"
sudo -u postgres psql -c "CREATE DATABASE makina OWNER $USER;"
Le modèle
Créons un `modèle GeoDjango` des limites des départements dans tiles/models.py
.
from django.contrib.gis.db import models
class Departement(models.Model):
code_dept = models.CharField(max_length=2)
nom_dept = models.CharField(max_length=100)
geom = models.MultiPolygonField(srid=3857)
Nous utilisons la projection 3857 (Web Mercator) car c'est elle qui sera utilisée pour la visualisation.
Créons les tables dans la base de donnée.
./bin/python manage.py makemigrations
./bin/python manage.py migrate
Puis téléchargeons les limites de département et importons les.
wget 'https://wxs-telechargement.ign.fr/oikr5jryiph0iwhw36053ptm/telechargement/inspire/GEOFLA_THEME-DEPARTEMENTS_2015_2$GEOFLA_2-1_DEPARTEMENT_SHP_LAMB93_FXX_2015-12-01/file/GEOFLA_2-1_DEPARTEMENT_SHP_LAMB93_FXX_2015-12-01.7z'
p7zip -d GEOFLA_2-1_DEPARTEMENT_SHP_LAMB93_FXX_2015-12-01.7z
ogr2ogr -f PostgreSQL PG:dbname=makina -append -t_srs EPSG:3857 -nlt MULTIPOLYGON -nln tiles_departement -select code_dept,nom_dept GEOFLA_2-1_DEPARTEMENT_SHP_LAMB93_FXX_2015-12-01/GEOFLA/1_DONNEES_LIVRAISON_2015/GEOFLA_2-1_SHP_LAMB93_FR-ED152/DEPARTEMENT/DEPARTEMENT.shp
La vue
Écrivons la vue qui va servir les tuiles dans tiles/views.py
. Commençons par les imports.
import math
import mapbox_vector_tile
import mercantile
from django.contrib.gis.db.models.functions import Intersection
from django.contrib.gis.geos import Polygon
from django.http import HttpResponse
from tiles.models import Departement
Nous aurons besoin d'une fonction qui nous donne la largeur d'un pixel dans la projection utilisée (ici Web Mercator). Pour un niveau de zoom z
, nous avons besoin de 2 ^ z
tuiles pour faire le tour de la terre. Et chaque tuile fait 512 pixels.
def pixel_length(zoom):
RADIUS = 6378137
CIRCUM = 2 * math.pi * RADIUS
SIZE = 512
return CIRCUM / SIZE / 2 ** int(zoom)
Notre vue prend le niveau de zoom est les coordonnées de la tuile en paramètres.
def tile_view(request, zoom, x, y):
Nous calculons les limites géographiques de la tuile, en projection WGS84, puis en projection Web Mercator.
bounds = mercantile.bounds(int(x), int(y), int(zoom))
west, south = mercantile.xy(bounds.west, bounds.south)
east, north = mercantile.xy(bounds.east, bounds.north)
Nous créons la bbox correspondante en ajoutant un buffer de 4 pixels pour gérer les éventuels problèmes de recouvrement entre les tuiles (si vous réglez ce buffer à 0 vous verrez apparaître les frontières des tuiles).
pixel = pixel_length(zoom)
buffer = 4 * pixel
bbox = Polygon.from_bbox((west - buffer, south - buffer, east + buffer, north + buffer))
Nous récupérons toutes les géométries qui intersectent cette bbox et nous découpons ces géométries lorsqu'elles dépassent du buffer autour de la tuile.
departements = Departement.objects.filter(geom__intersects=bbox)
departements = departements.annotate(clipped=Intersection('geom', bbox))
Nous générons la tuile dans une structure de données Python. Nous simplifions au passage la géométrie afin de garder les tuiles les plus légères possible sans dégrader la qualité.
tile = {
"name": "departements",
"features": [
{
"geometry": departement.clipped.simplify(pixel, preserve_topology=True).wkt,
"properties": {
"numero": departement.code_dept,
"nom": departement.nom_dept,
},
}
for departement in departements
],
}
Finalement, nous encodons les tuiles et nous renvoyons la réponse avec le type mime adéquat.
vector_tile = mapbox_vector_tile.encode(tile, quantize_bounds=(west, south, east, north))
return HttpResponse(vector_tile, content_type="application/vnd.mapbox-vector-tile")
La visualisation
Afin de visualiser le résultat de notre travail nous allons ajouter rapidement deux fichiers statiques.
Pour commencer, une page HTML dans tiles/static/index.html
.
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<title></title>
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.18.0/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.18.0/mapbox-gl.css' rel='stylesheet' />
<style>
body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; }
</style>
</head>
<body>
<div id='map'></div>
<script>
mapboxgl.accessToken = 'x';
var map = new mapboxgl.Map({
container: 'map', // container id
style: '/static/style.json', //stylesheet location
center: [2, 46], // starting position
zoom: 5 // starting zoom
});
</script>
</body>
</html>
Et un fichier de style Mapbox dans tiles/static/style.json
.
{
"version": 8,
"sources": {
"ma-source": {
"type": "vector",
"tiles": [
"http://localhost:8000/tiles/{z}/{x}/{y}.mvt"
]
}
},
"layers": [
{
"id": "ma-couche",
"type": "line",
"source": "ma-source",
"source-layer": "departements"
}
]
}
Il ne nous reste plus qu'à lancer le serveur de test et à nous rendre sur http://localhost:8000/static/index.html
pour admirer le résultat.
./bin/python manage.py runserver
La compression
Grâce au codage astucieux du format protocol buffer, la taille des tuiles peut être sensiblement diminuée avec une compression ZIP (codage de Huffmann). Cette compression peut être par exemple configurée à l'aide d'un reverse proxy. Par exemple en ajoutant dans la config de nginx :
gzip on;
gzip_types application/vnd.mapbox-vector-tile;
Et ensuite ?
Vous avez désormais toutes les bases pour servir des tuiles vectorielles en Django. Il ne vous reste plus qu'à laisser libre cours à votre imagination pour tirer profit de la génération dynamique de tuiles.
Formations associées
Formations SIG / Cartographie
Formation Tuiles vectorielles
À distance (FOAD) Du 3 au 4 juin 2025
Voir la formationFormations Django
Formation Django avancé
À distance (FOAD) Du 9 au 13 décembre 2024
Voir la formationActualités en lien
Présentation de django-tracking-fields
Suivi de modification d'objets Django
Internationalisation avec Django
En tant que développeurs nous sommes parfois confronté à la problématique de l'accessibilité des utilisateurs parlant différentes langues. Cet article est à destination des développeurs Django souhaitant découvrir l'internationalisation (i18n) et propose un parcours pas à pas dans cet exercice.
Présentation de Django-Safedelete
Masquage d'objets en base de données une alternative à la suppression définitive.