import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:geosector_app/core/services/location_service.dart'; import 'package:geosector_app/presentation/widgets/mapbox_map.dart'; import '../../core/constants/app_keys.dart'; import '../../core/data/models/sector_model.dart'; import '../../core/data/models/passage_model.dart'; // Extension pour ajouter ln2 (logarithme népérien de 2) comme constante extension MathConstants on math.Random { static const double ln2 = 0.6931471805599453; // ln(2) } class UserMapPage extends StatefulWidget { const UserMapPage({super.key}); @override State createState() => _UserMapPageState(); } class _UserMapPageState extends State { // Contrôleur de carte final MapController _mapController = MapController(); // Position actuelle et zoom LatLng _currentPosition = const LatLng(48.117266, -1.6777926); // Position initiale sur Rennes double _currentZoom = 12.0; // Zoom initial // Données des secteurs et passages final List> _sectors = []; final List> _passages = []; // État du plein écran bool _isFullScreen = false; // Items pour la combobox de secteurs List> _sectorItems = []; // Filtres pour les types de passages bool _showEffectues = true; bool _showAFinaliser = true; bool _showRefuses = true; bool _showDons = true; bool _showLots = true; bool _showMaisonsVides = true; // Référence à la boîte Hive pour les paramètres late Box _settingsBox; // Vérifier si la combobox de secteurs doit être affichée bool get _shouldShowSectorCombobox => _sectors.length > 1; int? _selectedSectorId; @override void initState() { super.initState(); _initSettings().then((_) { _loadSectors(); _loadPassages(); }); } // Initialiser la boîte de paramètres et charger les préférences Future _initSettings() async { // Ouvrir la boîte de paramètres si elle n'est pas déjà ouverte if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) { _settingsBox = await Hive.openBox(AppKeys.settingsBoxName); } else { _settingsBox = Hive.box(AppKeys.settingsBoxName); } // Charger les filtres sauvegardés _showEffectues = _settingsBox.get('showEffectues', defaultValue: true); _showAFinaliser = _settingsBox.get('showAFinaliser', defaultValue: true); _showRefuses = _settingsBox.get('showRefuses', defaultValue: true); _showDons = _settingsBox.get('showDons', defaultValue: true); _showLots = _settingsBox.get('showLots', defaultValue: true); _showMaisonsVides = _settingsBox.get('showMaisonsVides', defaultValue: true); // Charger le secteur sélectionné _selectedSectorId = _settingsBox.get('selectedSectorId'); // Charger la position et le zoom final double? savedLat = _settingsBox.get('mapLat'); final double? savedLng = _settingsBox.get('mapLng'); final double? savedZoom = _settingsBox.get('mapZoom'); if (savedLat != null && savedLng != null) { _currentPosition = LatLng(savedLat, savedLng); } if (savedZoom != null) { _currentZoom = savedZoom; } } // Obtenir la position actuelle de l'utilisateur Future _getUserLocation() async { try { // Afficher un indicateur de chargement ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Recherche de votre position...'), duration: Duration(seconds: 2), ), ); // Obtenir la position actuelle via le service de géolocalisation final position = await LocationService.getCurrentPosition(); if (position != null) { // Mettre à jour la position sur la carte _updateMapPosition(position, zoom: 17); // Sauvegarder la nouvelle position _settingsBox.put('mapLat', position.latitude); _settingsBox.put('mapLng', position.longitude); // Informer l'utilisateur if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Position actualisée'), backgroundColor: Colors.green, duration: Duration(seconds: 1), ), ); } } else { // Informer l'utilisateur en cas d'échec if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Impossible d\'obtenir votre position. Vérifiez vos paramètres de localisation.'), backgroundColor: Colors.red, ), ); } } } catch (e) { // Gérer les erreurs if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur: $e'), backgroundColor: Colors.red, ), ); } } } // Sauvegarder les paramètres utilisateur void _saveSettings() { // Sauvegarder les filtres _settingsBox.put('showEffectues', _showEffectues); _settingsBox.put('showAFinaliser', _showAFinaliser); _settingsBox.put('showRefuses', _showRefuses); _settingsBox.put('showDons', _showDons); _settingsBox.put('showLots', _showLots); _settingsBox.put('showMaisonsVides', _showMaisonsVides); // Sauvegarder le secteur sélectionné if (_selectedSectorId != null) { _settingsBox.put('selectedSectorId', _selectedSectorId); } // Sauvegarder la position et le zoom actuels _settingsBox.put('mapLat', _currentPosition.latitude); _settingsBox.put('mapLng', _currentPosition.longitude); _settingsBox.put('mapZoom', _currentZoom); } // Charger les secteurs depuis la boîte Hive void _loadSectors() { try { final sectorsBox = Hive.box(AppKeys.sectorsBoxName); final sectors = sectorsBox.values.toList(); setState(() { _sectors.clear(); for (final sector in sectors) { final List> coordinates = sector.getCoordinates(); final List points = coordinates.map((coord) => LatLng(coord[0], coord[1])).toList(); if (points.isNotEmpty) { _sectors.add({ 'id': sector.id, 'name': sector.libelle, 'color': _hexToColor(sector.color), 'points': points, }); } } // Mettre à jour les items de la combobox de secteurs _updateSectorItems(); // Si un secteur était sélectionné précédemment, le centrer if (_selectedSectorId != null && _sectors.any((s) => s['id'] == _selectedSectorId)) { _centerMapOnSpecificSector(_selectedSectorId!); } // Sinon, centrer la carte sur tous les secteurs else if (_sectors.isNotEmpty) { _centerMapOnSectors(); } }); } catch (e) { debugPrint('Erreur lors du chargement des secteurs: $e'); } } // Mettre à jour les items de la combobox de secteurs void _updateSectorItems() { // Créer l'item "Tous les secteurs" final List> items = [ const DropdownMenuItem( value: null, child: Text('Tous les secteurs'), ), ]; // Ajouter tous les secteurs for (final sector in _sectors) { items.add( DropdownMenuItem( value: sector['id'] as int, child: Text(sector['name'] as String), ), ); } setState(() { _sectorItems = items; }); } // Charger les passages depuis la boîte Hive void _loadPassages() { try { // Récupérer la boîte des passages final passagesBox = Hive.box(AppKeys.passagesBoxName); // Créer une nouvelle liste temporaire final List> newPassages = []; // Parcourir tous les passages dans la boîte for (var i = 0; i < passagesBox.length; i++) { final passage = passagesBox.getAt(i); if (passage != null) { // Vérifier si les coordonnées GPS sont valides final lat = double.tryParse(passage.gpsLat); final lng = double.tryParse(passage.gpsLng); // Filtrer par secteur si un secteur est sélectionné if (_selectedSectorId != null && passage.fkSector != _selectedSectorId) { continue; } if (lat != null && lng != null) { // Obtenir la couleur du type de passage Color passageColor = Colors.grey; // Couleur par défaut // Vérifier si le type de passage existe dans AppKeys.typesPassages if (AppKeys.typesPassages.containsKey(passage.fkType)) { // Utiliser la couleur1 du type de passage final colorValue = AppKeys.typesPassages[passage.fkType]!['couleur1'] as int; passageColor = Color(colorValue); // Ajouter le passage à la liste temporaire avec filtrage if (_shouldShowPassage(passage.fkType)) { newPassages.add({ 'id': passage.id, 'position': LatLng(lat, lng), 'type': passage.fkType, 'color': passageColor, 'model': passage, // Ajouter le modèle complet }); } } } } } // Mettre à jour la liste des passages dans l'état setState(() { _passages.clear(); _passages.addAll(newPassages); }); // Sauvegarder les paramètres après chargement des passages _saveSettings(); } catch (e) { debugPrint('Erreur lors du chargement des passages: $e'); } } // Vérifier si un passage doit être affiché en fonction de son type bool _shouldShowPassage(int type) { switch (type) { case 1: // Effectué return _showEffectues; case 2: // À finaliser return _showAFinaliser; case 3: // Refusé return _showRefuses; case 4: // Don return _showDons; case 5: // Lot return _showLots; case 6: // Maison vide return _showMaisonsVides; default: return true; } } // Convertir une couleur hexadécimale en Color Color _hexToColor(String hexColor) { // Supprimer le # si présent final String colorStr = hexColor.startsWith('#') ? hexColor.substring(1) : hexColor; // Ajouter FF pour l'opacité si nécessaire (6 caractères -> 8 caractères) final String fullColorStr = colorStr.length == 6 ? 'FF$colorStr' : colorStr; // Convertir en entier et créer la couleur return Color(int.parse(fullColorStr, radix: 16)); } // Centrer la carte sur tous les secteurs void _centerMapOnSectors() { if (_sectors.isEmpty) return; // Trouver les limites de tous les secteurs double minLat = 90.0; double maxLat = -90.0; double minLng = 180.0; double maxLng = -180.0; for (final sector in _sectors) { final points = sector['points'] as List; for (final point in points) { minLat = point.latitude < minLat ? point.latitude : minLat; maxLat = point.latitude > maxLat ? point.latitude : maxLat; minLng = point.longitude < minLng ? point.longitude : minLng; maxLng = point.longitude > maxLng ? point.longitude : maxLng; } } // Ajouter un padding aux limites pour s'assurer que tous les secteurs sont entièrement visibles // avec une marge autour (5% de la taille totale) final latPadding = (maxLat - minLat) * 0.05; final lngPadding = (maxLng - minLng) * 0.05; minLat -= latPadding; maxLat += latPadding; minLng -= lngPadding; maxLng += lngPadding; // Calculer le centre final centerLat = (minLat + maxLat) / 2; final centerLng = (minLng + maxLng) / 2; // Calculer le zoom approprié en tenant compte des dimensions de l'écran final mapWidth = MediaQuery.of(context).size.width; final mapHeight = MediaQuery.of(context).size.height * 0.7; // Estimation de la hauteur de la carte final zoom = _calculateOptimalZoom( minLat, maxLat, minLng, maxLng, mapWidth, mapHeight); // Centrer la carte sur ces limites avec animation _mapController.move(LatLng(centerLat, centerLng), zoom); // Mettre à jour l'état pour refléter la nouvelle position setState(() { _currentPosition = LatLng(centerLat, centerLng); _currentZoom = zoom; }); debugPrint('Carte centrée sur tous les secteurs avec zoom: $zoom'); } // Centrer la carte sur un secteur spécifique void _centerMapOnSpecificSector(int sectorId) { final sectorIndex = _sectors.indexWhere((s) => s['id'] == sectorId); if (sectorIndex == -1) return; // Mettre à jour le secteur sélectionné _selectedSectorId = sectorId; final sector = _sectors[sectorIndex]; final points = sector['points'] as List; final sectorName = sector['name'] as String; debugPrint( 'Centrage sur le secteur: $sectorName (ID: $sectorId) avec ${points.length} points'); if (points.isEmpty) { debugPrint('Aucun point dans ce secteur!'); return; } // Trouver les limites du secteur double minLat = 90.0; double maxLat = -90.0; double minLng = 180.0; double maxLng = -180.0; for (final point in points) { minLat = point.latitude < minLat ? point.latitude : minLat; maxLat = point.latitude > maxLat ? point.latitude : maxLat; minLng = point.longitude < minLng ? point.longitude : minLng; maxLng = point.longitude > maxLng ? point.longitude : maxLng; } debugPrint( 'Limites du secteur: minLat=$minLat, maxLat=$maxLat, minLng=$minLng, maxLng=$maxLng'); // Vérifier si les coordonnées sont valides if (minLat >= maxLat || minLng >= maxLng) { debugPrint('Coordonnées invalides pour le secteur $sectorName'); return; } // Calculer la taille du secteur final latSpan = maxLat - minLat; final lngSpan = maxLng - minLng; debugPrint('Taille du secteur: latSpan=$latSpan, lngSpan=$lngSpan'); // Ajouter un padding minimal aux limites pour s'assurer que le secteur est bien visible // mais prend le maximum de place sur la carte final double latPadding, lngPadding; if (latSpan < 0.01 || lngSpan < 0.01) { // Pour les très petits secteurs, utiliser un padding très réduit latPadding = 0.0003; lngPadding = 0.0003; } else if (latSpan < 0.05 || lngSpan < 0.05) { // Pour les petits secteurs, padding réduit latPadding = 0.0005; lngPadding = 0.0005; } else { // Pour les secteurs plus grands, utiliser un pourcentage minimal latPadding = latSpan * 0.03; // 3% au lieu de 10% lngPadding = lngSpan * 0.03; } minLat -= latPadding; maxLat += latPadding; minLng -= lngPadding; maxLng += lngPadding; debugPrint( 'Limites avec padding: minLat=$minLat, maxLat=$maxLat, minLng=$minLng, maxLng=$maxLng'); // Calculer le centre final centerLat = (minLat + maxLat) / 2; final centerLng = (minLng + maxLng) / 2; // Déterminer le zoom approprié en fonction de la taille du secteur double zoom; // Pour les très petits secteurs (comme des quartiers), utiliser un zoom fixe élevé if (latSpan < 0.01 && lngSpan < 0.01) { zoom = 16.0; // Zoom élevé pour les petits quartiers } else if (latSpan < 0.02 && lngSpan < 0.02) { zoom = 15.0; // Zoom élevé pour les petits quartiers } else if (latSpan < 0.05 && lngSpan < 0.05) { zoom = 13.0; // Zoom pour les secteurs de taille moyenne (quelques quartiers) } else if (latSpan < 0.1 && lngSpan < 0.1) { zoom = 12.0; // Zoom pour les grands secteurs (ville) } else { // Pour les secteurs plus grands, calculer le zoom final mapWidth = MediaQuery.of(context).size.width; final mapHeight = MediaQuery.of(context).size.height * 0.7; zoom = _calculateOptimalZoom( minLat, maxLat, minLng, maxLng, mapWidth, mapHeight); } debugPrint('Zoom calculé pour le secteur $sectorName: $zoom'); // Centrer la carte sur le secteur avec animation _mapController.move(LatLng(centerLat, centerLng), zoom); // Mettre à jour l'état pour refléter la nouvelle position setState(() { _currentPosition = LatLng(centerLat, centerLng); _currentZoom = zoom; }); } // Calculer le zoom optimal pour afficher une zone géographique dans la fenêtre de la carte double _calculateOptimalZoom(double minLat, double maxLat, double minLng, double maxLng, double mapWidth, double mapHeight) { // Méthode simplifiée et plus fiable pour calculer le zoom // Vérifier si les coordonnées sont valides if (minLat >= maxLat || minLng >= maxLng) { debugPrint('Coordonnées invalides pour le calcul du zoom'); return 12.0; // Valeur par défaut raisonnable } // Calculer la taille en degrés final latSpan = maxLat - minLat; final lngSpan = maxLng - minLng; debugPrint( '_calculateOptimalZoom - Taille: latSpan=$latSpan, lngSpan=$lngSpan'); // Ajouter un facteur de sécurité pour éviter les divisions par zéro if (latSpan < 0.0000001 || lngSpan < 0.0000001) { return 15.0; // Zoom élevé pour un point très précis } // Formule simplifiée pour le calcul du zoom // Basée sur l'expérience et adaptée pour les petites zones double zoom; if (latSpan < 0.005 || lngSpan < 0.005) { // Très petite zone (quartier) zoom = 16.0; } else if (latSpan < 0.01 || lngSpan < 0.01) { // Petite zone (quartier) zoom = 15.0; } else if (latSpan < 0.02 || lngSpan < 0.02) { // Petite zone (plusieurs quartiers) zoom = 14.0; } else if (latSpan < 0.05 || lngSpan < 0.05) { // Zone moyenne (ville) zoom = 13.0; } else if (latSpan < 0.2 || lngSpan < 0.2) { // Grande zone (agglomération) zoom = 11.0; } else if (latSpan < 0.5 || lngSpan < 0.5) { // Très grande zone (département) zoom = 9.0; } else if (latSpan < 2.0 || lngSpan < 2.0) { // Région zoom = 7.0; } else if (latSpan < 5.0 || lngSpan < 5.0) { // Pays zoom = 5.0; } else { // Continent ou plus zoom = 3.0; } debugPrint('Zoom calculé: $zoom pour zone: lat $latSpan, lng $lngSpan'); return zoom; } @override Widget build(BuildContext context) { final theme = Theme.of(context); final size = MediaQuery.of(context).size; final isDesktop = size.width > 900; return Scaffold( backgroundColor: Colors.transparent, body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // En-tête - affiché uniquement si pas en plein écran if (!_isFullScreen) Padding( padding: const EdgeInsets.all(16.0), child: Text( 'Carte des passages', style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.primary, ), ), ), // Filtres - affichés uniquement si pas en plein écran if (!_isFullScreen) _buildFilters(theme, isDesktop), // Carte Expanded( child: Stack( children: [ // Carte principale utilisant le widget commun MapboxMap MapboxMap( initialPosition: _currentPosition, initialZoom: _currentZoom, mapController: _mapController, // Utiliser OpenStreetMap sur mobile, Mapbox sur web useOpenStreetMap: !kIsWeb, markers: _buildPassageMarkers(), polygons: _buildSectorPolygons(), showControls: true, onMapEvent: (event) { if (event is MapEventMove) { // Mettre à jour la position et le zoom actuels setState(() { _currentPosition = event.camera.center; _currentZoom = event.camera.zoom; }); } }, ), // Combobox de sélection de secteurs (si plus d'un secteur) if (_shouldShowSectorCombobox) Positioned( left: 16.0, top: 16.0, child: Material( elevation: 4, borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 4), width: 220, // Largeur fixe pour accommoder les noms longs decoration: BoxDecoration( color: Colors.white.withOpacity(0.95), borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.location_on, size: 18, color: Colors.blue), const SizedBox(width: 8), Expanded( child: DropdownButton( value: _selectedSectorId, hint: const Text('Tous les secteurs'), isExpanded: true, underline: Container(), // Supprimer la ligne sous le dropdown icon: const Icon(Icons.arrow_drop_down, color: Colors.blue), items: _sectorItems, onChanged: (int? sectorId) { setState(() { _selectedSectorId = sectorId; }); if (sectorId != null) { _centerMapOnSpecificSector(sectorId); } else { // Si "Tous les secteurs" est sélectionné _centerMapOnSectors(); // Recharger tous les passages sans filtrage par secteur _loadPassages(); } }, ), ), ], ), ), ), ), // Bouton de plein écran (les autres contrôles sont gérés par MapboxMap) Positioned( bottom: 16.0, right: 16.0, child: _buildMapButton( icon: _isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen, onPressed: () { setState(() { _isFullScreen = !_isFullScreen; }); }, ), ), // Bouton de localisation personnalisé (pour utiliser notre propre logique) Positioned( bottom: 80.0, // Positionné au-dessus du bouton plein écran right: 16.0, child: _buildMapButton( icon: Icons.my_location, onPressed: () { _getUserLocation(); }, ), ), ], ), ), ], ), ), ); } // Construire les filtres pour les passages Widget _buildFilters(ThemeData theme, bool isDesktop) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 8.0, runSpacing: 8.0, children: [ // Filtre pour les passages effectués _buildFilterChip( label: AppKeys.typesPassages[1]?['titres'] as String? ?? 'Effectués', selected: _showEffectues, color: Color(AppKeys.typesPassages[1]?['couleur2'] as int), onSelected: (selected) { setState(() { _showEffectues = selected; _loadPassages(); // Recharger les passages avec le nouveau filtre _saveSettings(); // Sauvegarder les préférences }); }, ), // Filtre pour les passages à finaliser _buildFilterChip( label: AppKeys.typesPassages[2]?['titres'] as String? ?? 'À finaliser', selected: _showAFinaliser, color: Color(AppKeys.typesPassages[2]?['couleur2'] as int), onSelected: (selected) { setState(() { _showAFinaliser = selected; _loadPassages(); // Recharger les passages avec le nouveau filtre _saveSettings(); // Sauvegarder les préférences }); }, ), // Filtre pour les passages refusés _buildFilterChip( label: AppKeys.typesPassages[3]?['titres'] as String? ?? 'Refusés', selected: _showRefuses, color: Color(AppKeys.typesPassages[3]?['couleur2'] as int), onSelected: (selected) { setState(() { _showRefuses = selected; _loadPassages(); // Recharger les passages avec le nouveau filtre _saveSettings(); // Sauvegarder les préférences }); }, ), // Filtre pour les dons _buildFilterChip( label: AppKeys.typesPassages[4]?['titres'] as String? ?? 'Dons', selected: _showDons, color: Color(AppKeys.typesPassages[4]?['couleur2'] as int), onSelected: (selected) { setState(() { _showDons = selected; _loadPassages(); // Recharger les passages avec le nouveau filtre _saveSettings(); // Sauvegarder les préférences }); }, ), // Filtre pour les lots _buildFilterChip( label: AppKeys.typesPassages[5]?['titres'] as String? ?? 'Lots', selected: _showLots, color: Color(AppKeys.typesPassages[5]?['couleur2'] as int), onSelected: (selected) { setState(() { _showLots = selected; _loadPassages(); // Recharger les passages avec le nouveau filtre _saveSettings(); // Sauvegarder les préférences }); }, ), // Filtre pour les maisons vides _buildFilterChip( label: AppKeys.typesPassages[6]?['titres'] as String? ?? 'Maisons vides', selected: _showMaisonsVides, color: Color(AppKeys.typesPassages[6]?['couleur2'] as int), onSelected: (selected) { setState(() { _showMaisonsVides = selected; _loadPassages(); // Recharger les passages avec le nouveau filtre _saveSettings(); // Sauvegarder les préférences }); }, ), ], ), ], ), ); } // Construire un chip de filtre Widget _buildFilterChip({ required String label, required bool selected, required Color color, required Function(bool) onSelected, }) { // Utiliser la couleur vive pour les boutons sélectionnés et une version plus terne pour les désélectionnés final Color avatarColor = selected ? color : color.withOpacity(0.4); final Color chipColor = selected ? color.withOpacity(0.2) : Colors.grey.withOpacity(0.1); return FilterChip( label: Text( label, style: TextStyle( fontWeight: selected ? FontWeight.bold : FontWeight.normal, color: selected ? Colors.black : Colors.black54, ), ), selected: selected, showCheckmark: false, avatar: CircleAvatar( backgroundColor: avatarColor, radius: 10.0, ), backgroundColor: Colors.white, selectedColor: chipColor, side: BorderSide( color: selected ? color : Colors.grey.withOpacity(0.3), width: selected ? 1.5 : 1.0, ), padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), onSelected: onSelected, ); } // Construction d'un bouton de carte personnalisé 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(), color: Colors.blue, ), ); } // Construire les marqueurs pour les passages List _buildPassageMarkers() { return _passages.map((passage) { final PassageModel passageModel = passage['model'] as PassageModel; final bool hasNoSector = passageModel.fkSector == null; // Si le passage n'a pas de secteur, on met une bordure rouge épaisse final Color borderColor = hasNoSector ? Colors.red : Colors.white; final double borderWidth = hasNoSector ? 3.0 : 1.0; return Marker( point: passage['position'] as LatLng, width: hasNoSector ? 18.0 : 14.0, // Plus grand si orphelin height: hasNoSector ? 18.0 : 14.0, child: GestureDetector( onTap: () { _showPassageInfo(passage); }, child: Container( decoration: BoxDecoration( color: passage['color'] as Color, shape: BoxShape.circle, border: Border.all( color: borderColor, width: borderWidth, ), ), ), ), ); }).toList(); } // Construire les polygones pour les secteurs List _buildSectorPolygons() { return _sectors.map((sector) { return Polygon( points: sector['points'] as List, color: (sector['color'] as Color).withOpacity(0.3), borderColor: (sector['color'] as Color).withOpacity(1.0), borderStrokeWidth: 2.0, ); }).toList(); } // Méthode pour mettre à jour la position sur la carte void _updateMapPosition(LatLng position, {double? zoom}) { _mapController.move( position, zoom ?? _mapController.camera.zoom, ); // Mettre à jour les variables d'état setState(() { _currentPosition = position; if (zoom != null) { _currentZoom = zoom; } }); // Sauvegarder les paramètres après mise à jour de la position _saveSettings(); } // Afficher les informations d'un passage lorsqu'on clique dessus void _showPassageInfo(Map passage) { final PassageModel passageModel = passage['model'] as PassageModel; final int type = passageModel.fkType; // Construire l'adresse complète final String adresse = '${passageModel.numero}, ${passageModel.rueBis} ${passageModel.rue}'; // Informations sur l'étage, l'appartement et la résidence (si habitat = 2) String? etageInfo; String? apptInfo; String? residenceInfo; if (passageModel.fkHabitat == 2) { if (passageModel.niveau.isNotEmpty) { etageInfo = 'Etage ${passageModel.niveau}'; } if (passageModel.appt.isNotEmpty) { apptInfo = 'appt. ${passageModel.appt}'; } if (passageModel.residence.isNotEmpty) { residenceInfo = passageModel.residence; } } // Formater la date (uniquement si le type n'est pas 2 et si la date existe) String dateInfo = ''; if (type != 2 && passageModel.passedAt != null) { dateInfo = 'Date: ${_formatDate(passageModel.passedAt!)}'; } // Récupérer le nom du passage (si le type n'est pas 6 - Maison vide) String? nomInfo; if (type != 6 && passageModel.name.isNotEmpty) { nomInfo = passageModel.name; } // Récupérer les informations de règlement si le type est 1 (Effectué) ou 5 (Lot) Widget? reglementInfo; if (type == 1 || type == 5) { final int typeReglementId = passageModel.fkTypeReglement; final String montant = passageModel.montant; // Récupérer les informations du type de règlement if (AppKeys.typesReglements.containsKey(typeReglementId)) { final Map typeReglement = AppKeys.typesReglements[typeReglementId]!; final String titre = typeReglement['titre'] as String; final Color couleur = Color(typeReglement['couleur'] as int); final IconData iconData = typeReglement['icon_data'] as IconData; reglementInfo = Padding( padding: const EdgeInsets.only(top: 8.0), child: Row( children: [ Icon(iconData, color: couleur, size: 20), const SizedBox(width: 8), Text('$titre: $montant €', style: TextStyle(color: couleur, fontWeight: FontWeight.bold)), ], ), ); } } // Afficher une bulle d'information showDialog( context: context, builder: (context) => AlertDialog( contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Afficher en premier si le passage n'est pas affecté à un secteur if (passageModel.fkSector == null) ...[ Container( padding: const EdgeInsets.all(8), margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( color: Colors.red.withOpacity(0.1), border: Border.all(color: Colors.red, width: 1), borderRadius: BorderRadius.circular(4), ), child: Row( children: [ const Icon(Icons.warning, color: Colors.red, size: 20), const SizedBox(width: 8), const Expanded( child: Text( 'Ce passage n\'est plus affecté à un secteur', style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold), ), ), ], ), ), ], Text('Adresse: $adresse'), if (residenceInfo != null) ...[ const SizedBox(height: 4), Text(residenceInfo) ], if (etageInfo != null) ...[ const SizedBox(height: 4), Text(etageInfo) ], if (apptInfo != null) ...[ const SizedBox(height: 4), Text(apptInfo) ], if (dateInfo.isNotEmpty) ...[ const SizedBox(height: 8), Text(dateInfo) ], if (nomInfo != null) ...[ const SizedBox(height: 8), Text('Nom: $nomInfo') ], if (reglementInfo != null) reglementInfo, ], ), actionsPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), actions: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ // Bouton d'édition IconButton( onPressed: () { Navigator.of(context).pop(); // Logique pour éditer le passage debugPrint('Éditer le passage ${passageModel.id}'); }, icon: const Icon(Icons.edit), color: Colors.blue, tooltip: 'Modifier', ), // Bouton de suppression IconButton( onPressed: () { Navigator.of(context).pop(); // Logique pour supprimer le passage debugPrint('Supprimer le passage ${passageModel.id}'); }, icon: const Icon(Icons.delete), color: Colors.red, tooltip: 'Supprimer', ), ], ), // Bouton de fermeture TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Fermer'), ), ], ), ], ), ); } // Formater une date String _formatDate(DateTime date) { return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'; } }