# TODO - Cache Web pour tuiles Mapbox ## Contexte **Problème** : En PROD, les admins travaillent intensivement sur la plateforme web et la carte. Actuellement, les tuiles Mapbox ne sont pas mises en cache sur le web, ce qui provoque : - Téléchargements répétés des mêmes tuiles - Consommation excessive de bande passante - Navigation peu fluide - Coûts API Mapbox potentiellement élevés **Cause** : Mapbox envoie des headers `Cache-Control: no-cache` qui empêchent le cache navigateur par défaut, et `flutter_map_cache` ne supporte pas la plateforme web. **Solution** : Implémenter un cache manuel des tuiles via IndexedDB en Dart, uniquement pour le web. ## Architecture technique ### Flux de données ``` flutter_map demande une tuile ↓ CachedWebTileProvider (nouveau) ↓ WebTileCacheService.getTile() ↓ Vérifie IndexedDB ↓ ┌─────────────┴─────────────┐ │ │ Cache HIT Cache MISS │ │ Retourne image Télécharge depuis Mapbox depuis IndexedDB ↓ Stocke dans IndexedDB ↓ Retourne image ``` ### Composants 1. **WebTileCacheService** (Singleton) - Gère IndexedDB pour stocker les tuiles - Méthodes : `getTile()`, `storeTile()`, `clearCache()`, `clearExpiredTiles()` - Durée de cache : 30 jours (aligné avec mobile/desktop) 2. **CachedWebTileProvider** (Custom TileProvider) - Implémente `TileProvider` de `flutter_map` - Utilise `WebTileCacheService` pour récupérer/stocker - Fallback sur téléchargement direct si erreur 3. **MapboxMap** (Modifié) - Utilise `CachedWebTileProvider` sur web - Garde `HiveCacheStore` sur mobile/desktop ## Fichiers à créer/modifier ### ✅ Fichiers à créer #### 1. `lib/core/services/web_tile_cache_service.dart` Service singleton pour gérer le cache IndexedDB des tuiles Mapbox. **Responsabilités** : - Initialiser la base IndexedDB `mapbox_tiles_cache` - Stocker les tuiles avec clé unique (URL de la tuile) - Récupérer les tuiles depuis le cache - Gérer l'expiration (30 jours) - Nettoyer les tuiles expirées **Structure IndexedDB** : ``` Database: mapbox_tiles_cache ObjectStore: tiles - key: String (URL de la tuile) - value: Object { data: Uint8List (bytes de l'image) timestamp: int (millisecondsSinceEpoch) } ``` **API publique** : ```dart class WebTileCacheService { static final instance = WebTileCacheService._(); Future initialize(); Future getTile(String url); Future storeTile(String url, Uint8List data); Future clearCache(); Future clearExpiredTiles(); } ``` **Dépendances** : - `dart:indexed_db` (natif web) - `dart:typed_data` - `package:flutter/foundation.dart` (kIsWeb) #### 2. `lib/presentation/widgets/cached_web_tile_provider.dart` TileProvider custom qui utilise le cache IndexedDB. **Responsabilités** : - Implémenter `TileProvider` de `flutter_map` - Vérifier le cache avant téléchargement - Télécharger et stocker si absent du cache - Fallback sur comportement par défaut si erreur **API publique** : ```dart class CachedWebTileProvider extends TileProvider { final String accessToken; final String styleId; CachedWebTileProvider({ required this.accessToken, required this.styleId, }); @override ImageProvider getImage(TileCoordinates coordinates, TileLayer options); } ``` **Logique** : 1. Construire l'URL de la tuile 2. Appeler `WebTileCacheService.getTile(url)` 3. Si trouvée → `MemoryImage(cachedData)` 4. Sinon → Télécharger → Stocker → `MemoryImage(downloadedData)` **Dépendances** : - `flutter_map` - `WebTileCacheService` - `dart:typed_data` - `package:http/http.dart` (ou `Dio` si préféré) ### ✅ Fichiers à modifier #### 3. `lib/presentation/widgets/mapbox_map.dart` Modifier pour utiliser le nouveau provider sur web. **Modifications** : - Lignes 96-139 : Refactoriser `_initializeCache()` - Utiliser `CachedWebTileProvider` quand `kIsWeb == true` - Garder `HiveCacheStore` pour mobile/desktop - Initialiser `WebTileCacheService` au démarrage sur web **Avant** (lignes 98-103) : ```dart if (kIsWeb) { // Pas de cache sur Web (non supporté) setState(() { _cacheInitialized = true; }); return; } ``` **Après** : ```dart if (kIsWeb) { // Cache manuel via IndexedDB await WebTileCacheService.instance.initialize(); setState(() { _cacheInitialized = true; }); return; } ``` **TileLayer config** (ligne ~180) : ```dart TileLayer( urlTemplate: 'https://api.mapbox.com/styles/v1/mapbox/$styleId/tiles/256/{z}/{x}/{y}@2x?access_token=$accessToken', tileProvider: kIsWeb ? CachedWebTileProvider( accessToken: accessToken, styleId: styleId, ) : (_cacheStore != null ? CachedTileProvider(store: _cacheStore!) : NetworkTileProvider()), // ... ), ``` ## Étapes d'implémentation ### Phase 1 : Service de cache IndexedDB **Tâche 1.1** : Créer `web_tile_cache_service.dart` - [ ] Créer la classe singleton - [ ] Implémenter `initialize()` avec création de la DB IndexedDB - [ ] Implémenter `getTile(url)` avec vérification d'expiration - [ ] Implémenter `storeTile(url, data)` avec timestamp - [ ] Implémenter `clearCache()` pour vider toute la DB - [ ] Implémenter `clearExpiredTiles()` pour le nettoyage automatique - [ ] Ajouter des logs de debug (`debugPrint`) **Tâche 1.2** : Gestion des erreurs - [ ] Try/catch sur toutes les opérations IndexedDB - [ ] Fallback gracieux si IndexedDB indisponible - [ ] Logs d'erreur explicites **Tâche 1.3** : Tests unitaires - [ ] Tester `initialize()` crée bien la DB - [ ] Tester `storeTile()` + `getTile()` roundtrip - [ ] Tester expiration (mock timestamp) - [ ] Tester `clearCache()` ### Phase 2 : TileProvider custom **Tâche 2.1** : Créer `cached_web_tile_provider.dart` - [ ] Créer la classe qui extend `TileProvider` - [ ] Implémenter `getImage()` avec logique de cache - [ ] Gérer le téléchargement des tuiles manquantes - [ ] Stocker les tuiles téléchargées dans le cache - [ ] Ajouter des logs de performance (cache hit/miss) **Tâche 2.2** : Gestion des erreurs réseau - [ ] Try/catch sur téléchargement - [ ] Retry logic (3 tentatives max) - [ ] Placeholder si échec total - [ ] Logs d'erreur explicites **Tâche 2.3** : Tests - [ ] Tester avec tuile en cache → Pas de requête réseau - [ ] Tester avec tuile absente → Téléchargement + stockage - [ ] Tester avec erreur réseau → Fallback gracieux ### Phase 3 : Intégration dans MapboxMap **Tâche 3.1** : Modifier `mapbox_map.dart` - [ ] Importer `CachedWebTileProvider` et `WebTileCacheService` - [ ] Modifier `_initializeCache()` pour initialiser le service web - [ ] Modifier `TileLayer` pour utiliser le bon provider selon la plateforme - [ ] Tester sur web que le nouveau provider est bien utilisé **Tâche 3.2** : Validation visuelle - [ ] Lancer l'app en mode web DEV - [ ] Ouvrir DevTools → Console pour voir les logs - [ ] Naviguer sur la carte → Vérifier logs "Cache MISS" au premier passage - [ ] Revenir sur la même zone → Vérifier logs "Cache HIT" - [ ] Vérifier dans DevTools → Application → IndexedDB que les tuiles sont stockées **Tâche 3.3** : Tests de performance - [ ] Mesurer le temps de chargement initial (sans cache) - [ ] Mesurer le temps de chargement avec cache - [ ] Vérifier qu'il n'y a plus de requêtes réseau pour les tuiles en cache - [ ] Tester sur une session longue (plusieurs déplacements sur la carte) ### Phase 4 : Nettoyage et documentation **Tâche 4.1** : Nettoyage automatique - [ ] Appeler `clearExpiredTiles()` au démarrage de l'app (dans `initialize()`) - [ ] Ajouter un bouton admin pour vider manuellement le cache (optionnel) **Tâche 4.2** : Logs et monitoring - [ ] Ajouter des statistiques de cache (hit rate, taille totale) - [ ] Logger les performances (temps de chargement moyen) **Tâche 4.3** : Documentation - [ ] Commenter le code (dartdoc) - [ ] Mettre à jour `FLOW-*.md` si nécessaire - [ ] Ajouter section dans README technique ## Tests de validation ### Tests fonctionnels 1. **Cache vide → Premier chargement** - [ ] Ouvrir l'app web en mode incognito - [ ] Naviguer sur la carte - [ ] Vérifier que les tuiles se chargent - [ ] Vérifier les logs "Cache MISS" + "Storing tile" 2. **Cache rempli → Rechargement** - [ ] Recharger la page (F5) - [ ] Revenir sur la même zone de carte - [ ] Vérifier que les tuiles se chargent instantanément - [ ] Vérifier les logs "Cache HIT" - [ ] Vérifier dans Network tab : pas de requêtes Mapbox pour les tuiles en cache 3. **Navigation étendue** - [ ] Se déplacer sur plusieurs zones de la carte - [ ] Revenir sur les zones précédentes - [ ] Vérifier mix de "Cache HIT" et "Cache MISS" 4. **Gestion de l'expiration** - [ ] Modifier temporairement l'expiration à 10 secondes (pour test) - [ ] Charger des tuiles - [ ] Attendre 15 secondes - [ ] Recharger → Vérifier que les tuiles sont re-téléchargées 5. **Gestion d'erreur réseau** - [ ] Désactiver le réseau en plein chargement - [ ] Vérifier que les tuiles en cache s'affichent quand même - [ ] Vérifier que les tuiles manquantes ne bloquent pas l'app ### Tests de performance 6. **Mesure de bande passante** - [ ] Vider le cache - [ ] Naviguer sur une zone avec ~100 tuiles visibles - [ ] Noter la bande passante consommée (DevTools Network) - [ ] Recharger la page et revenir sur la même zone - [ ] Vérifier que la bande passante est ~0 KB 7. **Mesure de vitesse** - [ ] Mesurer le temps de chargement initial : ____ ms - [ ] Mesurer le temps de chargement avec cache : ____ ms - [ ] Objectif : >50% de réduction ### Tests multi-plateformes 8. **Mobile/Desktop non régressé** - [ ] Lancer sur Android → Vérifier que HiveCacheStore fonctionne toujours - [ ] Lancer sur iOS → Vérifier que HiveCacheStore fonctionne toujours - [ ] Vérifier les logs "Cache initialized with HiveCacheStore" 9. **Web multi-navigateurs** - [ ] Tester sur Chrome - [ ] Tester sur Firefox - [ ] Tester sur Safari (si IndexedDB supporté) - [ ] Tester sur Edge ## Checklist de validation finale - [ ] Aucune nouvelle dépendance ajoutée au `pubspec.yaml` - [ ] Code compatible web uniquement (guards `kIsWeb`) - [ ] Mobile/Desktop non impactés (toujours HiveCacheStore) - [ ] Logs de debug présents et clairs - [ ] Gestion d'erreur complète (try/catch) - [ ] Expiration automatique (30 jours) - [ ] Performance mesurée et améliorée - [ ] Tests manuels passés - [ ] Code documenté (dartdoc) - [ ] Pas de régression sur les autres plateformes ## Notes techniques ### Pourquoi IndexedDB et pas localStorage ? - **localStorage** : Limite de 5-10 MB → Insuffisant pour des tuiles (1 tuile = ~60 KB) - **IndexedDB** : Limite de ~50% de l'espace disque disponible → Largement suffisant ### Structure de la clé de cache Format recommandé : `{styleId}_{z}_{x}_{y}@{scale}x` Exemple : `streets-v11_15_16234_11378@2x` Cela permet : - Identification unique de chaque tuile - Support multi-styles (streets, satellite, etc.) - Support multi-résolutions (@1x, @2x) ### Taille estimée du cache - 1 tuile Mapbox @2x : ~60 KB - Carte complète niveau zoom 15 : ~500 tuiles - Total pour une zone : ~30 MB - IndexedDB peut stocker plusieurs GB → Pas de souci ### Maintenance du cache Le nettoyage automatique (`clearExpiredTiles()`) s'exécute : - Au démarrage de l'app - Évite l'accumulation de vieilles tuiles - Garde le cache sous contrôle ## Dépendances ### Packages Flutter (déjà présents) - `flutter_map: ^7.0.2` → Fournit `TileProvider` - `flutter_map_cache: ^1.5.1` → Pour mobile/desktop uniquement ### Bibliothèques Dart natives (aucun ajout) - `dart:indexed_db` → Pour IndexedDB (web seulement) - `dart:typed_data` → Pour manipuler les bytes - `package:flutter/foundation.dart` → Pour `kIsWeb` ### Pas de nouvelle dépendance externe ✅ ## Estimation - **Temps de développement** : 2-3 heures - **Complexité** : Moyenne - **Risque** : Faible (fallback sur comportement actuel) - **Gain** : Important pour les admins PROD ## Prochaine étape **Attente de validation utilisateur** pour démarrer l'implémentation.