DOCS/v2/plan-illustre-plan.md
Thomas Fransolet d36394cabd Carte hors ligne : conception du plan illustré, 3e fournisseur
Ni Google ni Mapbox ne donnent un fond hors ligne gratuit, et Mapbox
se facture au nombre de tuiles, pas à la surface. Pour un musée de
plein air comme le Fourneau Saint-Michel, la réponse est son propre
plan dessiné : les repères se posent sur l'image, le plan est une
Resource, donc embarqué par le pipeline hors ligne existant.

Conception seule, rien n'est implémenté. Mapbox est passé fournisseur
par défaut le 04/09.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 12:05:31 +02:00

9.2 KiB

Plan illustré — un 3ᵉ fournisseur de carte, sans tuiles

Contexte : conception du 2026-09-04, déclenchée par la demande du Fourneau Saint-Michel (musée de plein air, plusieurs dizaines de bâtiments, réseau quasi absent). Fait suite à l'analyse carto du même jour, qui a établi que ni Google ni Mapbox ne donnent une carte hors ligne gratuite.

Maquette et diagramme : https://claude.ai/code/artifact/6e8b681e-1697-48e8-b010-d77569c6b9d7

⚠️ Rien n'est implémenté. Ce document est la conception, pas un état d'avancement.


Pourquoi ce troisième fournisseur existe

MapProvider ne connaît que Google et MapBox. Les deux chargent leurs tuiles en réseau, et aucun des deux ne donne un fond hors ligne gratuit :

Fournisseur Hors ligne Coût
Google Aucune API de tuiles hors ligne dans le SDK Android. Les « zones hors connexion » sont une fonction de l'app grand public, pas du SDK. De plus le code passe par flutter_map sur https://mt1.google.com/vt/…, un endpoint non documenté dont la mise en cache est interdite
Mapbox OfflineManager + TileStore, présents dans mapbox_maps_flutter 2.8.0 (version résolue) ⚠️ Un tile pack par visiteur : le compteur monte avec la fréquentation, donc avec le succès du client. Grille à vérifier chez Mapbox
Plan illustré Par construction — l'image est une Resource, elle part avec la visite Aucun tiers. Le coût se déplace sur notre propre stockage : quelques Mo par visite téléchargée, prévisible

Le plan illustré est aussi ce qu'un musée de plein air possède déjà : un plan dessiné, plus lisible qu'un fond routier sur cinquante bâtiments.


La décision qui structure tout le reste

Un plan de musée est stylisé, pas à l'échelle, rarement orienté au nord. Ne pas projeter les repères depuis leurs coordonnées GPS — ils tomberaient à côté des bâtiments dessinés.

Les repères se posent directement sur l'image, à la main, et c'est cette position qui fait foi. Le calage GPS ne sert qu'à afficher le visiteur — la seule donnée qui traverse la frontière entre le terrain et le dessin.

⚠️ Correction par rapport à la première note du 2026-09-04 : celle-ci disait « deux points d'ancrage suffisent pour projeter un GeoPoint sur l'image ». Les deux moitiés étaient fausses — on ne projette pas les repères, et deux points ne suffisent pas (voir plus bas).

Conséquence directe : le calage est facultatif. Un lieu qui ne veut pas de position visiteur téléverse son plan, pose ses repères, et c'est fini. Deux niveaux d'effort, deux produits vendables.

Trois points de calage, pas deux

Deux points donnent une échelle et une translation — suffisant seulement si le plan est à l'échelle et orienté au nord, ce qu'un plan dessiné n'est presque jamais. Trois points donnent une transformation affine, qui absorbe la rotation et l'étirement. Le quatrième n'apporte rien tant qu'on ne veut pas corriger de perspective.

Raison de terrain en plus : trois croix bien réparties permettent de vérifier le calage. Avec deux, toute erreur de relevé se répartit silencieusement sur toute l'image.


Modèle de données — additif, aucune colonne existante modifiée

Champ Note
MapProvider.IllustratedPlan DTOs/SubSection/MapDTO.cs Troisième valeur, position 2. Les lignes existantes gardent 0 et 1, rien à migrer. ⚠️ À répercuter à la main dans manager_api_new/lib/model/map_provider.dart — la règle « pas de regénération du client » s'applique
PlanResourceId / PlanResource Data/SubSection/SectionMap.cs Exactement sur le modèle de IconResourceId / IconResource, déjà présents dans la même classe. C'est ce parallèle qui rend le hors ligne gratuit
PlanX, PlanY GeoPoint (dans SectionMap.cs) double? normalisés 0→1, pas des pixels : le plan peut être ré-encodé ou redimensionné sans invalider un repère
PlanAnchors Data/SubSection/SectionMap.cs [{planX, planY, latitude, longitude}] en jsonb, comme MapCategories l'est déjà. Nul si le lieu ne veut pas de position visiteur

Le geste dans manager-app

Écran concerné : lib/Screens/Configurations/Section/SubSection/Map/map_config.dart (481 lignes).

  1. Choisir « Plan illustré » — une valeur de plus dans map_providers, servie par le SingleSelectContainer existant. Le choix masque les réglages Google/Mapbox et fait apparaître la zone de plan.
  2. Téléverser le planResourceInputContainer, déjà utilisé juste à côté pour l'icône. Passe par la médiathèque, le quota et la compression 2560 px livrée en C4.
  3. Poser les repères — le plan s'affiche, on clique, un repère apparaît ; la liste des GeoPoint existants est à côté et on y fait glisser un point déjà rempli. Seul écran vraiment neuf du chantier.
  4. Caler (facultatif) — voir ci-dessous.

Le calage se fait assis, en deux clics par ancre

Personne ne va relever de coordonnées sur le terrain. Le geste est : cliquer le même angle de bâtiment deux fois — une fois sur le plan dessiné, une fois sur une carte réelle affichée juste à côté. L'outil déduit les coordonnées du second clic. Aucun chiffre n'est jamais tapé.

Le composant existe déjà : GeolocInputContainer (lib/Components/geoloc_input_container.dart) ouvre un FlutterLocationPicker — vraie carte, recherche d'adresse, centrée sur Namur (50.429333, 4.891434) par défaut. C'est déjà lui qui sert à poser le point central d'une section carte. Le calage n'invente rien : il appelle deux fois le même composant dans un écran qui les met en regard.

⚠️ i18n obligatoire : tout texte de cet écran passe par AppLocalizations, FR/EN/NL.


Le rendu — aucun SDK de carte

Une image, des repères positionnés en pourcentage par-dessus, un conteneur qui zoome.

  • mymuseum-visitapp : une PlanView à côté de GoogleMapView et MapBoxView (lib/Screens/Sections/Map/). InteractiveViewer enveloppant un Stack donne le pincer-zoomer et le déplacement sans écrire une ligne.
  • visitapp-web : le même rendu en CSS (transform). À écrire après le Flutter, qui reste la référence de comportement.

Ni flutter_map, ni google_maps_flutter, ni mapbox_maps_flutter : donc ni jeton, ni quota, ni compte à surveiller.

La position du visiteur est un repère de plus, calculé à l'affichage par la transformation affine, et masqué dès qu'il sort du cadre du plan — un visiteur au parking ne doit pas voir son point collé au bord de l'image.

Le GPS lui-même ne pose pas de problème : geolocator ^13.0.0 lit le GNSS, autonome sans réseau. Seule réserve à annoncer au client : sans A-GPS, le premier point peut demander 30 à 60 s sous couvert forestier.

Ce que le hors ligne gagne

Le plan est une Resource. Une ligne dans SectionMap.GetReferencedResourceIds(), à côté de ResourceId(IconResourceId), et le pipeline de téléchargement existant l'embarque — aucun mécanisme neuf. SectionType.Map peut alors entrer dans offlineCapableSectionTypes (mymuseum-visitapp/lib/Services/downloadConfiguration.dart) pour ce fournisseur.


⚠️ La conséquence côté web

mapProviderMobileOnlyNote prévient déjà le client que le fournisseur de carte ne vaut que pour le mobile et la borne, parce que visitapp-web reste sur Leaflet (décision du lot E, W1, le 2026-08-11). Pour un plan illustré cette note devient gênante : le plan est le produit, pas un réglage de fond.

Deux issues, à trancher : porter le rendu en web dans la foulée (c'est du CSS, quelques heures), ou assumer que le plan est une fonctionnalité d'app mobile et le dire à la vente. ⚠️ Le plan Essentiel étant web-only, la seconde option lui interdit le plan illustré.


Ordre d'exécution

manager-servicemanager_api_newmanager-appmymuseum-visitappvisitapp-web. Rien ne peut être configuré tant que le modèle n'existe pas.

Repo Travail Ampleur
manager-service Enum, 4 champs, une migration, une ligne dans GetReferencedResourceIds petit
manager_api_new Enum et champs répercutés à la main petit
manager-app Écran de pose des repères et du calage (le picker de carte est déjà écrit) le gros morceau
mymuseum-visitapp PlanView + projection GPS moyen
visitapp-web Le même rendu en CSS moyen

Un raccourci pour valider vite

Une PlanView dans mymuseum-visitapp avec un plan et des repères en dur valide le rendu et le pincer-zoomer en une heure, avant d'engager la migration. À faire si on veut voir quelque chose bouger avant de toucher au schéma.

Ce qui peut être livré en premier et se vendre seul

Le plan sans calage : téléversement, pose des repères, rendu, hors ligne. Ni transformation affine, ni position visiteur — et un musée de plein air a déjà le produit complet que les audioguides vendent depuis trente ans. Le calage GPS se greffe après sans rien casser.