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>
165 lines
9.2 KiB
Markdown
165 lines
9.2 KiB
Markdown
# 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 | Où | 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 plan** — `ResourceInputContainer`, 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-service` → `manager_api_new` → `manager-app` → `mymuseum-visitapp` → `visitapp-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.
|