From d36394cabd7f5ed710f52d4eaf83027ce73b5ce9 Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Thu, 10 Sep 2026 12:05:31 +0200 Subject: [PATCH] =?UTF-8?q?Carte=20hors=20ligne=20:=20conception=20du=20pl?= =?UTF-8?q?an=20illustr=C3=A9,=203e=20fournisseur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../055-carte-hors-ligne-tile-packs-mapbox.md | 4 +- ...an-illustre-georeference-3e-fournisseur.md | 17 +- v2/plan-illustre-plan.md | 164 ++++++++++++++++++ 3 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 v2/plan-illustre-plan.md diff --git a/kanban/cards/5-planifie/055-carte-hors-ligne-tile-packs-mapbox.md b/kanban/cards/5-planifie/055-carte-hors-ligne-tile-packs-mapbox.md index 9b94e41..504d2c6 100644 --- a/kanban/cards/5-planifie/055-carte-hors-ligne-tile-packs-mapbox.md +++ b/kanban/cards/5-planifie/055-carte-hors-ligne-tile-packs-mapbox.md @@ -8,6 +8,8 @@ src: analyse du code le 2026-09-04 — MapProvider, flutter_map, mapbox_maps_flu ---

Aujourd'hui aucune carte ne fonctionne hors ligne, et ce n'est pas une question de données : MapDTO porte déjà points, centerLatitude, zoom et iconResourceId, et les icônes sont téléchargées avec la visite. C'est le fond qui manque — les tuiles sont chargées en réseau, sans cache.

Le GPS, lui, n'est pas le problème : geolocator ^13.0.0 lit le GNSS, qui est autonome. Seule réserve à annoncer au client : sans A-GPS, le premier point peut demander 30 à 60 s sous couvert forestier.

-

Le chemin court existe déjà dans le pubspec : mapbox_maps_flutter ^2.0.0 expose OfflineManager (style packs) et TileStore (tile regions) — on télécharge une emprise bornée au moment du téléchargement de la visite. Pour un domaine comme le Fourneau Saint-Michel, quelques dizaines de Mo. MapProvider.MapBox existe déjà côté modèle.

+

Le chemin court existe déjà dans le pubspec : la version résolue est 2.8.0 (pas 2.0.0 comme le laisse croire la contrainte), et le paquet installé contient bien OfflineManager, TileStore, StylePackLoadOptions, TileRegionLoadOptions et TileRegionvérifié dans le cache pub, pas supposé. On télécharge une emprise bornée au moment du téléchargement de la visite ; pour un domaine comme le Fourneau, quelques dizaines de Mo.

+

Mapbox est désormais le fournisseur par défaut (04/09) : le null retombait sur Google dans map_page.dart (deux endroits) et map_config.dart. ⚠️ Effet de bord à surveiller — les sections carte existantes de MDLF et Fort Saint-Héribert dont le mapProvider est null basculent silencieusement de Google à Mapbox. Le token est déjà en place, en dur dans main.dart:60.

+

⚠️ Ce n'est pas gratuit, et le compteur est le mauvais : ce qui coûte n'est pas la surface de l'emprise mais le nombre de téléchargements — un tile pack par visiteur. La facture monte donc avec la fréquentation, c'est-à-dire avec le succès du client. Grille à vérifier chez Mapbox, ligne « tile packs » et pas seulement « map loads ». C'est exactement ce calcul que supprime v2/plan-illustre-plan.md.

Google est une impasse, et pas seulement techniquement. Le SDK Maps pour Android n'expose aucune API de tuiles hors ligne — les « zones hors connexion » sont une fonction de l'app Google Maps, pas du SDK intégrable. Et le code n'utilise même pas ce SDK pour les sections carte : il passe par flutter_map sur https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}, un endpoint non documenté dont la mise en cache est explicitement interdite. Conclusion : Mapbox devient le fournisseur du mode hors ligne, Google reste en ligne seulement.

⚠️ Tant que ce chantier n'est pas fait, SectionType.Map reste volontairement hors de offlineCapableSectionTypes (downloadConfiguration.dart) : l'ajouter donnerait une carte grise, ce qui est pire que ne pas la proposer. Une fois les tuiles packagées, c'est une ligne à ajouter dans cette constante.

diff --git a/kanban/cards/5-planifie/057-plan-illustre-georeference-3e-fournisseur.md b/kanban/cards/5-planifie/057-plan-illustre-georeference-3e-fournisseur.md index 7d53970..e64beb2 100644 --- a/kanban/cards/5-planifie/057-plan-illustre-georeference-3e-fournisseur.md +++ b/kanban/cards/5-planifie/057-plan-illustre-georeference-3e-fournisseur.md @@ -1,12 +1,15 @@ --- -title: Plan illustré géoréférencé — un 3e fournisseur de carte +title: Plan illustré — un 3e fournisseur de carte, sans tuiles area: manager backend visitapp horizon: v2 tags: carto, offline, manager-app -flag: good | hors ligne par construction, et c'est ce qu'un musée dessine déjà -src: analyse du code le 2026-09-04 — MapProvider n'a que Google et MapBox +flag: good | le seul chemin vers une carte hors ligne sans coût tiers +src: v2/plan-illustre-plan.md — conçu le 2026-09-04 --- -

Un musée de plein air ne distribue pas un fond OpenStreetMap, il distribue son plan dessiné. Aujourd'hui MapProvider ne connaît que Google et MapBox : il n'existe aucun moyen de téléverser une image de plan et d'y placer les points.

-

Ce qu'il faut : une troisième valeur de MapProvider, une ressource image portée par la section, et un calage — deux points d'ancrage suffisent (coin haut-gauche / bas-droit en coordonnées réelles) pour projeter un GeoPoint sur l'image et y afficher la position du visiteur.

-

Pourquoi c'est le meilleur des trois : aucune tuile, donc hors ligne par construction — l'image part avec la visite comme n'importe quelle ressource, et le mécanisme de téléchargement n'a rien de nouveau à apprendre. Le rendu est aussi plus lisible qu'un fond routier sur un site de plusieurs dizaines de bâtiments.

-

Périmètre réel : c'est le plus gros des trois chemins carto — backend (modèle + migration), manager-app (téléversement du plan et pose des ancres), mymuseum-visitapp et visitapp-web (rendu). À faire après Carte hors ligne — tile packs Mapbox, qui débloque le besoin immédiat avec un fond classique.

+

Un musée de plein air ne distribue pas un fond OpenStreetMap, il distribue son plan dessiné. Conception complète, modèle de données et périmètre par repo : DOCS/v2/plan-illustre-plan.md. Rien n'est implémenté.

+

La décision qui structure tout : les repères se posent directement sur l'image, à la main, et c'est cette position qui fait foi — un plan dessiné n'étant ni à l'échelle ni orienté au nord, projeter des GeoPoint depuis leurs coordonnées les ferait tomber à côté des bâtiments. Le calage GPS ne sert qu'à afficher le visiteur, et devient donc facultatif.

+

⚠️ Deux affirmations de la première version de cette carte étaient fausses, corrigées le 04/09 : on ne projette pas les repères, et deux points d'ancrage ne suffisent pas — il en faut trois pour une transformation affine qui absorbe rotation et étirement, et pour pouvoir vérifier le calage au lieu de diluer l'erreur.

+

Le calage se fait assis : cliquer le même angle de bâtiment sur le plan puis sur une carte réelle en regard. GeolocInputContainer ouvre déjà un FlutterLocationPicker (vraie carte, recherche d'adresse) — le composant est écrit, il faut le mettre à côté du plan. Plus besoin de prestation d'installation.

+

Hors ligne gratuit : le plan est une Resource, une ligne dans GetReferencedResourceIds() et le pipeline existant l'embarque. Aucun tiers, aucun quota — contrairement aux tile packs Mapbox, facturés par visiteur.

+

⚠️ Conséquence web à trancher : visitapp-web reste sur Leaflet (lot E, W1), et mapProviderMobileOnlyNote le dit déjà au client. Pour un plan illustré c'est bloquant — le plan est le produit. Soit on porte le rendu en CSS dans la foulée, soit le plan Essentiel, web-only, n'y a pas droit.

+

Livrable qui se vend seul : le plan sans calage — téléversement, pose des repères, rendu, hors ligne. C'est le produit que les audioguides vendent depuis trente ans.

diff --git a/v2/plan-illustre-plan.md b/v2/plan-illustre-plan.md new file mode 100644 index 0000000..47905d1 --- /dev/null +++ b/v2/plan-illustre-plan.md @@ -0,0 +1,164 @@ +# 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 : +> +> ⚠️ **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.