🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
362 lines
12 KiB
Dart
Executable File
362 lines
12 KiB
Dart
Executable File
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:http_cache_hive_store/http_cache_hive_store.dart'; // Mise à jour v2.0.0 (06/10/2025)
|
|
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<Marker>? markers;
|
|
|
|
/// Liste des marqueurs de labels à afficher (sous les marqueurs principaux)
|
|
final List<Marker>? labelMarkers;
|
|
|
|
/// Liste des polygones à afficher
|
|
final List<Polygon>? polygons;
|
|
|
|
/// Liste des polylines à afficher
|
|
final List<Polyline>? 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<MapboxMap> createState() => _MapboxMapState();
|
|
}
|
|
|
|
class _MapboxMapState extends State<MapboxMap> {
|
|
/// 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<void> _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.withValues(alpha: 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';
|
|
}
|
|
|
|
// Debug pour vérifier la configuration
|
|
debugPrint('MapboxMap: Plateforme: ${kIsWeb ? "Web" : "Mobile"}');
|
|
debugPrint('MapboxMap: Environnement: $environment');
|
|
debugPrint('MapboxMap: Token: ${mapboxToken.substring(0, 10)}...'); // Afficher seulement le début du token
|
|
debugPrint('MapboxMap: URL Template: ${urlTemplate.substring(0, 50)}...');
|
|
}
|
|
|
|
// 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) {
|
|
setState(() {
|
|
// Dans flutter_map 8.1.1, nous devons utiliser le contrôleur pour obtenir le zoom actuel
|
|
_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: 'app.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,
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|