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.

Le blog Makina-corpus

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

Django

Django initiation

A distance (foad) Du 28 au 30 septembre 2021

Voir la formation

SIG/Webmapping

Tuiles vectorielles

A distance Du 06 au 07 décembre 2021

Voir la formation

Django

Django avancé

Aucune session de formation n'est prévue pour le moment.

Pour plus d'informations, n'hésitez pas à nous contacter.

Voir la formation

Actualités en lien

Image
Django logo
28/08/2018

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.

Voir l'article
Image
Django logo
08/07/2020

Présentation de django-tracking-fields

Suivi de modification d'objets Django

Voir l'article
09/07/2013

Présentation de Django-Safedelete

Masquage d'objets en base de données une alternative à la suppression définitive.

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus