import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_cache/flutter_map_cache.dart'; import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart'; import 'package:path_provider/path_provider.dart'; import 'package:latlong2/latlong.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/core/services/api_service.dart'; // Import du service singleton /// Widget de carte réutilisable utilisant Mapbox /// /// Ce widget encapsule un FlutterMap avec des tuiles Mapbox et fournit /// des fonctionnalités pour afficher des marqueurs, des polygones et des contrôles. class MapboxMap extends StatefulWidget { /// Position initiale de la carte final LatLng initialPosition; /// Niveau de zoom initial final double initialZoom; /// Liste des marqueurs à afficher (au-dessus de tout) final List? markers; /// Liste des marqueurs de labels à afficher (sous les marqueurs principaux) final List? labelMarkers; /// Liste des polygones à afficher final List? polygons; /// Liste des polylines à afficher final List? polylines; /// Contrôleur de carte externe (optionnel) final MapController? mapController; /// Callback appelé lorsque la carte est déplacée final void Function(MapEvent)? onMapEvent; /// Afficher les boutons de contrôle (zoom, localisation) final bool showControls; /// Style de la carte Mapbox (optionnel) /// Si non spécifié, utilise le style par défaut 'mapbox/streets-v12' final String? mapStyle; /// Désactive le drag de la carte final bool disableDrag; /// Utiliser OpenStreetMap au lieu de Mapbox (en cas de problème de token) final bool useOpenStreetMap; const MapboxMap({ super.key, this.initialPosition = const LatLng(48.1173, -1.6778), // Rennes par défaut this.initialZoom = 13.0, this.markers, this.labelMarkers, this.polygons, this.polylines, this.mapController, this.onMapEvent, this.showControls = true, this.mapStyle, this.disableDrag = false, this.useOpenStreetMap = false, }); @override State createState() => _MapboxMapState(); } class _MapboxMapState extends State { /// Contrôleur de carte interne late final MapController _mapController; /// Niveau de zoom actuel (utilisé pour l'affichage futur) // ignore: unused_field double _currentZoom = 13.0; /// Provider de tuiles (peut être NetworkTileProvider ou CachedTileProvider) TileProvider? _tileProvider; /// Indique si le cache est initialisé bool _cacheInitialized = false; @override void initState() { super.initState(); _mapController = widget.mapController ?? MapController(); _currentZoom = widget.initialZoom; _initializeCache(); } /// Initialise le cache des tuiles Future _initializeCache() async { try { if (kIsWeb) { // Pas de cache sur Web (non supporté) setState(() { _cacheInitialized = true; }); return; } final dir = await getTemporaryDirectory(); final cacheDir = '${dir.path}/map_tiles_cache'; // Initialiser le HiveCacheStore final cacheStore = HiveCacheStore( cacheDir, hiveBoxName: 'mapTilesCache', ); // Initialiser le CachedTileProvider _tileProvider = CachedTileProvider( maxStale: const Duration(days: 30), store: cacheStore, ); debugPrint('MapboxMap: Cache initialisé dans $cacheDir'); if (mounted) { setState(() { _cacheInitialized = true; }); } debugPrint('MapboxMap: Cache initialisé avec succès pour ${widget.useOpenStreetMap ? "OpenStreetMap" : "Mapbox"}'); } catch (e) { debugPrint('MapboxMap: Erreur lors de l\'initialisation du cache: $e'); // En cas d'erreur, on continue sans cache if (mounted) { setState(() { _cacheInitialized = true; _tileProvider = null; // Utiliser NetworkTileProvider en fallback }); } } } @override void dispose() { // Ne pas disposer le contrôleur s'il a été fourni de l'extérieur if (widget.mapController == null) { _mapController.dispose(); } super.dispose(); } /// Construit un bouton de contrôle de carte Widget _buildMapButton({ required IconData icon, required VoidCallback onPressed, }) { return Container( width: 40, height: 40, decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.2), blurRadius: 6, offset: const Offset(0, 3), ), ], ), child: IconButton( icon: Icon(icon, size: 20), onPressed: onPressed, padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ); } @override Widget build(BuildContext context) { String urlTemplate; if (widget.useOpenStreetMap) { // Utiliser OpenStreetMap comme alternative urlTemplate = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; debugPrint('MapboxMap: Utilisation d\'OpenStreetMap'); } else { // Déterminer l'URL du template de tuiles Mapbox // Utiliser l'environnement actuel pour obtenir la bonne clé API final String environment = ApiService.instance.getCurrentEnvironment(); final String mapboxToken = AppKeys.getMapboxApiKey(environment); // Essayer différentes API Mapbox selon la plateforme if (kIsWeb) { // Sur web, on peut utiliser l'API styles urlTemplate = 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/256/{z}/{x}/{y}@2x?access_token=$mapboxToken'; } else { // Sur mobile, utiliser l'API v4 qui fonctionne mieux avec les tokens standards // Format: mapbox.streets pour les rues, mapbox.satellite pour satellite urlTemplate = 'https://api.tiles.mapbox.com/v4/mapbox.streets/{z}/{x}/{y}@2x.png?access_token=$mapboxToken'; } } // Afficher un indicateur pendant l'initialisation du cache if (!_cacheInitialized) { return Stack( children: [ // Carte sans cache en attendant _buildMapContent(urlTemplate), // Indicateur discret const Positioned( top: 8, right: 8, child: Card( child: Padding( padding: EdgeInsets.all(8.0), child: Row( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ), SizedBox(width: 8), Text('Initialisation du cache...', style: TextStyle(fontSize: 12)), ], ), ), ), ), ], ); } return _buildMapContent(urlTemplate); } Widget _buildMapContent(String urlTemplate) { return Stack( children: [ // Carte principale FlutterMap( mapController: _mapController, options: MapOptions( initialCenter: widget.initialPosition, initialZoom: widget.initialZoom, minZoom: 7.0, // Zoom minimum pour éviter que les tuiles ne se chargent pas maxZoom: 20.0, // Zoom maximum interactionOptions: InteractionOptions( enableMultiFingerGestureRace: true, flags: widget.disableDrag ? InteractiveFlag.all & ~InteractiveFlag.drag : InteractiveFlag.all, ), onMapEvent: (event) { if (event is MapEventMove) { // Mise à jour du zoom sans rebuild (la variable n'est pas utilisée dans le UI) _currentZoom = _mapController.camera.zoom; } // Appeler le callback externe si fourni if (widget.onMapEvent != null) { widget.onMapEvent!(event); } }, ), children: [ // Tuiles de la carte (Mapbox) TileLayer( urlTemplate: urlTemplate, userAgentPackageName: 'app3.geosector.fr', maxNativeZoom: 19, maxZoom: 20, minZoom: 7, // Utiliser le cache sur mobile, NetworkTileProvider sur Web tileProvider: !kIsWeb && _cacheInitialized && _tileProvider != null ? _tileProvider! : NetworkTileProvider( headers: { 'User-Agent': 'geosector_app/3.3.1', 'Accept': '*/*', }, ), errorTileCallback: (tile, error, stackTrace) { // Réduire les logs d'erreur pour ne pas polluer la console if (!error.toString().contains('abortTrigger')) { debugPrint('MapboxMap: Erreur de chargement de tuile: $error'); } }, ), // Polygones if (widget.polygons != null && widget.polygons!.isNotEmpty) PolygonLayer(polygons: widget.polygons!), // Marqueurs de labels (sous les marqueurs principaux) if (widget.labelMarkers != null && widget.labelMarkers!.isNotEmpty) MarkerLayer(markers: widget.labelMarkers!), // Polylines if (widget.polylines != null && widget.polylines!.isNotEmpty) PolylineLayer(polylines: widget.polylines!), // Marqueurs principaux (au-dessus de tout) if (widget.markers != null && widget.markers!.isNotEmpty) MarkerLayer(markers: widget.markers!), ], ), // Boutons de contrôle if (widget.showControls) Positioned( bottom: 16, right: 16, child: Column( children: [ // Bouton de zoom + _buildMapButton( icon: Icons.add, onPressed: () { _mapController.move( _mapController.camera.center, _mapController.camera.zoom + 1, ); }, ), const SizedBox(height: 8), // Bouton de zoom - _buildMapButton( icon: Icons.remove, onPressed: () { _mapController.move( _mapController.camera.center, _mapController.camera.zoom - 1, ); }, ), const SizedBox(height: 8), // Bouton de localisation _buildMapButton( icon: Icons.my_location, onPressed: () { _mapController.move( widget.initialPosition, 15, ); }, ), ], ), ), ], ); } }