Aller au contenu

Pourquoi les pistes cyclables de France ne s'affichent pas

9 min de lecture

Je voulais une carte qui montre le réseau cyclable du pays. Le GeoJSON pesait trop lourd, et une fois passé aux tuiles vectorielles il n'y avait toujours rien à l'écran. Le zoom n'est pas un réglage de caméra, c'est un sélecteur de données.

Deux cartes côte à côte dans le même style. À gauche la France au zoom 5, où aucune piste cyclable n'est dessinée. À droite Lyon au zoom 14, où le réseau cyclable apparaît en corail le long des quais du Rhône et de la Saône.

Bikipi sert à trouver chez qui dormir pendant un voyage à vélo, et je m’en occupe. Je voulais que la carte s’ouvre sur la France avec tout le réseau cyclable dessus. L’idée tenait en une phrase, et c’est à peu près tout ce qu’elle avait de simple.

Ça m’a pris un moment de comprendre que ce n’était pas une demande simple. Pas parce que c’est difficile à styler, mais parce que la donnée que je réclamais n’existe pas à l’échelle où je la réclamais. Voici les deux murs que je me suis pris, et ce qu’ils apprennent sur le fonctionnement d’une carte web.

Premier mur : le poids

Le premier réflexe d’un développeur web est le bon réflexe, appliqué au mauvais endroit. Les données géographiques s’échangent en GeoJSON, qui est du JSON ordinaire : une liste d’objets, chacun avec une géométrie et des attributs.

{ "type": "Feature",
  "geometry": { "type": "LineString",
                "coordinates": [[4.8357, 45.7640], [4.8401, 45.7712]] },
  "properties": { "nom": "Berges du Rhône", "surface": "asphalte" } }

Un format qu’on sait lire, un fetch, et on dessine. Sauf que le réseau cyclable français pèse des centaines de mégaoctets dans ce format.

La raison est instructive une fois qu’on la formule. Vous demandez au navigateur de télécharger chaque coordonnée de chaque tronçon d’un pays entier pour peindre une image de mille pixels de large. À cette échelle, une piste de trois kilomètres occupe un pixel. Vous transportez une précision métrique pour en afficher une précision kilométrique, et vous la transportez toute, y compris les neuf dixièmes qui sont hors de l’écran dès que l’utilisateur se déplace.

Au passage, notez l’ordre dans ce bloc : longitude d’abord. Tout le monde dit « latitude, longitude » à l’oral, le GeoJSON range en x, y. Une coordonnée lyonnaise inversée vous envoie par 45° est et 4° nord, au large de la Somalie. Quand un point atterrit dans l’océan Indien, c’est presque toujours ça.

Ce que font les tuiles

La réponse du domaine à ce problème, c’est la tuile vectorielle, et elle mérite qu’on s’y arrête parce qu’elle explique tout le reste.

La surface du globe est découpée en carrés, récursivement : un carré au zoom 0, quatre au zoom 1, seize au zoom 2. À chaque niveau, chaque carré est un fichier. Ce fichier ne contient pas une image mais du protobuf, c’est-à-dire les objets eux-mêmes, avec leurs attributs. Le navigateur ne télécharge que les carrés visibles, au niveau de zoom courant.

C’est la différence qui compte : vous ne recevez plus une photo de la carte, vous recevez ses objets, et c’est vous qui décidez de leur apparence au moment du rendu. D’où la possibilité de recolorer une carte entière sans retélécharger un octet.

Sources, couches, et le champ qui fait perdre une heure

Une fois passé aux tuiles, le style devient le seul fichier que vous éditez. C’est du JSON, et il ressemble à une feuille de style : il décrit comment dessiner, séparément de la donnée.

L’analogie tient assez pour démarrer, et là où elle casse est plus instructif. Il n’y a pas de sélecteur, la règle nomme elle-même sa donnée. Il n’y a pas de cascade : les règles vivent dans un tableau, layers, dessiné dans l’ordre. C’est l’algorithme du peintre, la dernière est au-dessus. Et une règle ne dessine qu’une chose, donc un trait avec un contour demande deux couches.

La distinction à ne pas rater est celle-ci. Une source dit d’où vient la donnée. Une couche est une instruction de dessin posée sur une source. Le rapport n’est pas de un à un : une seule source de routes alimente couramment trois couches, le contour sombre, le remplissage clair, et l’étiquette de rue.

Et une source vectorielle contient elle-même plusieurs tables nommées. C’est là que se cachent les pistes cyclables : pas dans une couche à elles, mais dans la table transportation, avec les autoroutes. Deux attributs les en extraient. class range grossièrement, et met dans path aussi bien les chemins piétons que les pistes cyclables ; c’est subclass qui distingue les seconds.

{ "id": "pistes", "type": "line",
  "source": "openmaptiles", "source-layer": "transportation",
  "filter": ["==", ["get", "subclass"], "cycleway"],
  "paint": { "line-color": "#f87060", "line-width": 2 } }

source dit quel jeu de tuiles, source-layer dit quelle table dedans. Les deux s’appellent « layer » dans le vocabulaire courant, ce qui n’aide personne. Oublier source-layer sur une source vectorielle ne produit aucune erreur, juste une couche parfaitement valide qui ne dessine rien.

Deuxième mur : à l’échelle de la France, il n’y a rien

J’avais donc ma source, ma couche, mon filtre. Sur Lyon, le réseau s’affichait. Sur la France, l’écran restait vide, sans le moindre message.

La raison m’a demandé un moment, et c’est le vrai sujet de cet article. Les pistes cyclables ne sont pas dans les tuiles aux zooms lointains. Elles apparaissent à partir du zoom 12 ou 13 environ. En dessous, la donnée n’existe pas. Pas « n’est pas affichée » : n’est pas dans le fichier.

Une fois posé, c’est évident. Une tuile au zoom 5 couvre plusieurs départements. Si elle contenait chaque tronçon cyclable de cette surface, elle pèserait exactement ce que pesait mon GeoJSON, et on aurait réinventé le problème qu’on venait de résoudre. Les producteurs de tuiles écartent donc le détail au fur et à mesure qu’on s’éloigne : les sentiers disparaissent avant les rues, qui disparaissent avant les nationales, qui disparaissent avant les autoroutes. À chaque niveau, on garde ce qui reste lisible et on jette le reste.

C’est la chose qu’un développeur web n’anticipe pas. Vous arrivez avec l’idée que le zoom est un réglage de caméra, une transformation d’échelle appliquée à un contenu fixe. Ici, le zoom est un sélecteur de données. Il fait partie du modèle, pas de la vue. Deux propriétés de couche en découlent directement, minzoom et maxzoom, qui ne sont pas des optimisations mais l’expression normale de « cet objet n’a de sens qu’à cette échelle ».

Ce que ça change pour le produit

La conséquence n’est pas technique, elle est produit, et c’est pour ça qu’il vaut mieux la découvrir tôt.

« Montrer tout le réseau cyclable de France » n’est pas une demande de style, et aucune valeur dans aucun fichier JSON ne la satisfera. C’est une demande de données : soit vous produisez vos propres tuiles en forçant la conservation des pistes aux zooms bas, avec le poids qui va avec, soit vous changez ce que la carte montre selon l’échelle.

Le mur force donc à poser la question utile, qui n’est pas « comment tout afficher » mais « de quoi quelqu’un a besoin à cette distance ». À l’échelle d’un pays, un cyclotouriste ne lit pas un tracé large de trois mètres, même si on parvenait à le lui envoyer. Ce qu’il cherche, c’est où il va pouvoir dormir.

Et ça, c’est un point, pas une ligne. Un point reste lisible à toutes les échelles, il se regroupe quand il y en a trop, et il ne disparaît d’aucune tuile puisque c’est vous qui le fournissez. La règle que j’en tire : gardez les tuiles détaillées pour les échelles où le détail se voit, et changez de propos en vous éloignant, plutôt que de vous acharner à tenir le même à toutes les distances.

Les points d’intérêt, et deux pannes silencieuses

Afficher des points reste plus simple, à deux pièges près qui ont en commun de ne produire aucune erreur.

Les points d’intérêt se dessinent avec une couche de type symbol, qui porte du texte, une icône, ou les deux.

{ "id": "hebergements", "type": "symbol", "source": "spots",
  "layout": { "icon-image": "lit", "text-field": ["get", "nom"],
              "text-font": ["Noto Sans Regular"], "text-offset": [0, 1.2] },
  "paint": { "text-color": "#102542",
             "text-halo-color": "#ffffff", "text-halo-width": 1.2 } }

Le texte exige des glyphes. Une carte ne rend pas les caractères avec une police système. Elle télécharge des atlas précalculés, par plages de caractères, à l’URL glyphs de la racine du style. Si ce serveur ne connaît pas la police demandée dans text-font, vous n’obtenez aucun texte et aucun avertissement, pendant que le reste de la carte s’affiche parfaitement. C’est la panne la plus déroutante du domaine, et la plus fréquente.

L’icône marche pareil avec le sprite : icon-image désigne un nom dans une planche d’images déclarée à la racine. Nom absent, rien à l’écran.

Ces deux mécanismes m’ont assez agacé pour que je finisse par écrire un éditeur de styles qui bascule l’URL des glyphes vers un serveur servant la police choisie, et qui fabrique le sprite à l’export. Il est ouvert et sans clé d’API, tout ce qui est décrit ici s’y colle et s’y manipule.

Quand rien ne s’affiche

Les pannes de ce domaine ont en commun de ne rien dire. Aucune console, aucun avertissement, une carte qui manque simplement quelque chose. Cinq causes couvrent presque tout, à parcourir dans cet ordre.

source-layer absent ou mal orthographié sur une source vectorielle. La couche est valide, elle ne dessine rien.

La donnée n’est pas dans les tuiles à ce zoom. Approchez de quelques niveaux avant de soupçonner le style, et vérifiez le minzoom de la couche.

La police demandée dans text-font n’est pas servie par l’URL glyphs. Aucun texte, le reste de la carte intact.

Le nom donné à icon-image n’est pas dans le sprite. Aucune icône, même silence.

La couche est trop haut dans le tableau layers et se fait recouvrir. Mettez-la à la fin pour trancher.

Et si rien ne charge du tout, c’est le CORS. Pour du PMTiles, le serveur doit en plus accepter les requêtes par plage, puisque le principe est d’aller chercher un morceau d’un gros fichier.

Ce que je retiens

Un style de carte est une donnée comme une autre. Il se lit, se diffe, se génère, se valide. Il n’y a pas de moteur graphique à apprendre, il y a un document à comprendre.

Mais la donnée en dessous a une dimension de plus que celles auxquelles on est habitué. Une base de données répond pareil quelle que soit la taille de la fenêtre ; un jeu de tuiles, non. Ce que vous obtenez dépend d’où vous regardez et de combien vous êtes loin. Tant qu’on n’a pas ça en tête, on cherche dans le style la réponse à une question qui n’y est pas.

Clément Levasseur

© 2026 Clément Levasseur. Tous droits réservés.

LinkedInGitHubEmail