- Amélioration des interfaces utilisateur sur mobile - Optimisation de la responsivité des composants Flutter - Mise à jour des widgets de chat et communication - Amélioration des formulaires et tableaux - Ajout de nouveaux composants pour l'administration - Optimisation des thèmes et styles visuels 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
939 lines
33 KiB
Dart
Executable File
939 lines
33 KiB
Dart
Executable File
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';
|
|
import '../../presentation/widgets/passage_map_dialog.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<UserMapPage> createState() => _UserMapPageState();
|
|
}
|
|
|
|
class _UserMapPageState extends State<UserMapPage> {
|
|
// 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<Map<String, dynamic>> _sectors = [];
|
|
final List<Map<String, dynamic>> _passages = [];
|
|
|
|
// Items pour la combobox de secteurs
|
|
List<DropdownMenuItem<int?>> _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<void> _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<void> _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<SectorModel>(AppKeys.sectorsBoxName);
|
|
final sectors = sectorsBox.values.toList();
|
|
|
|
setState(() {
|
|
_sectors.clear();
|
|
|
|
for (final sector in sectors) {
|
|
final List<List<double>> coordinates = sector.getCoordinates();
|
|
final List<LatLng> 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<DropdownMenuItem<int?>> items = [
|
|
const DropdownMenuItem<int?>(
|
|
value: null,
|
|
child: Text('Tous les secteurs'),
|
|
),
|
|
];
|
|
|
|
// Ajouter tous les secteurs
|
|
for (final sector in _sectors) {
|
|
items.add(
|
|
DropdownMenuItem<int?>(
|
|
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<PassageModel>(AppKeys.passagesBoxName);
|
|
|
|
// Créer une nouvelle liste temporaire
|
|
final List<Map<String, dynamic>> 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<LatLng>;
|
|
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<LatLng>;
|
|
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) {
|
|
return Scaffold(
|
|
backgroundColor: Colors.transparent,
|
|
body: SafeArea(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 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: false, // Désactiver les contrôles par défaut pour éviter la duplication
|
|
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.withValues(alpha: 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<int?>(
|
|
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();
|
|
}
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Contrôles de zoom et localisation en bas à droite
|
|
Positioned(
|
|
bottom: 16.0,
|
|
right: 16.0,
|
|
child: Column(
|
|
children: [
|
|
// Bouton zoom +
|
|
_buildMapButton(
|
|
icon: Icons.add,
|
|
onPressed: () {
|
|
final newZoom = _currentZoom + 1;
|
|
_mapController.move(_currentPosition, newZoom);
|
|
setState(() {
|
|
_currentZoom = newZoom;
|
|
});
|
|
_saveSettings();
|
|
},
|
|
),
|
|
const SizedBox(height: 8),
|
|
// Bouton zoom -
|
|
_buildMapButton(
|
|
icon: Icons.remove,
|
|
onPressed: () {
|
|
final newZoom = _currentZoom - 1;
|
|
_mapController.move(_currentPosition, newZoom);
|
|
setState(() {
|
|
_currentZoom = newZoom;
|
|
});
|
|
_saveSettings();
|
|
},
|
|
),
|
|
const SizedBox(height: 8),
|
|
// Bouton de localisation
|
|
_buildMapButton(
|
|
icon: Icons.my_location,
|
|
onPressed: () {
|
|
_getUserLocation();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Filtres de type de passage en bas à gauche
|
|
Positioned(
|
|
bottom: 16.0,
|
|
left: 16.0,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.7),
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.2),
|
|
blurRadius: 6,
|
|
offset: const Offset(0, 3),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Filtre Effectués (type 1)
|
|
_buildFilterDot(
|
|
color: Color(AppKeys.typesPassages[1]?['couleur2'] as int),
|
|
selected: _showEffectues,
|
|
onTap: () {
|
|
setState(() {
|
|
_showEffectues = !_showEffectues;
|
|
_loadPassages();
|
|
_saveSettings();
|
|
});
|
|
},
|
|
),
|
|
const SizedBox(width: 6),
|
|
// Filtre À finaliser (type 2)
|
|
_buildFilterDot(
|
|
color: Color(AppKeys.typesPassages[2]?['couleur2'] as int),
|
|
selected: _showAFinaliser,
|
|
onTap: () {
|
|
setState(() {
|
|
_showAFinaliser = !_showAFinaliser;
|
|
_loadPassages();
|
|
_saveSettings();
|
|
});
|
|
},
|
|
),
|
|
const SizedBox(width: 6),
|
|
// Filtre Refusés (type 3)
|
|
_buildFilterDot(
|
|
color: Color(AppKeys.typesPassages[3]?['couleur2'] as int),
|
|
selected: _showRefuses,
|
|
onTap: () {
|
|
setState(() {
|
|
_showRefuses = !_showRefuses;
|
|
_loadPassages();
|
|
_saveSettings();
|
|
});
|
|
},
|
|
),
|
|
const SizedBox(width: 6),
|
|
// Filtre Dons (type 4)
|
|
_buildFilterDot(
|
|
color: Color(AppKeys.typesPassages[4]?['couleur2'] as int),
|
|
selected: _showDons,
|
|
onTap: () {
|
|
setState(() {
|
|
_showDons = !_showDons;
|
|
_loadPassages();
|
|
_saveSettings();
|
|
});
|
|
},
|
|
),
|
|
const SizedBox(width: 6),
|
|
// Filtre Lots (type 5)
|
|
_buildFilterDot(
|
|
color: Color(AppKeys.typesPassages[5]?['couleur2'] as int),
|
|
selected: _showLots,
|
|
onTap: () {
|
|
setState(() {
|
|
_showLots = !_showLots;
|
|
_loadPassages();
|
|
_saveSettings();
|
|
});
|
|
},
|
|
),
|
|
const SizedBox(width: 6),
|
|
// Filtre Maisons vides (type 6)
|
|
_buildFilterDot(
|
|
color: Color(AppKeys.typesPassages[6]?['couleur2'] as int),
|
|
selected: _showMaisonsVides,
|
|
onTap: () {
|
|
setState(() {
|
|
_showMaisonsVides = !_showMaisonsVides;
|
|
_loadPassages();
|
|
_saveSettings();
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Construire une pastille de filtre pour la carte
|
|
Widget _buildFilterDot({
|
|
required Color color,
|
|
required bool selected,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
width: 24,
|
|
height: 24,
|
|
decoration: BoxDecoration(
|
|
color: selected ? color : color.withValues(alpha: 0.3),
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: selected ? Colors.white : Colors.white.withValues(alpha: 0.5),
|
|
width: 1.5,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 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.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(),
|
|
color: Colors.blue,
|
|
),
|
|
);
|
|
}
|
|
|
|
// Construire les marqueurs pour les passages
|
|
List<Marker> _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<Polygon> _buildSectorPolygons() {
|
|
return _sectors.map((sector) {
|
|
return Polygon(
|
|
points: sector['points'] as List<LatLng>,
|
|
color: (sector['color'] as Color).withValues(alpha: 0.3),
|
|
borderColor: (sector['color'] as Color).withValues(alpha: 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<String, dynamic> passage) {
|
|
final PassageModel passageModel = passage['model'] as PassageModel;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => PassageMapDialog(
|
|
passage: passageModel,
|
|
isAdmin: false, // L'utilisateur n'est pas admin
|
|
onDeleted: () {
|
|
// Recharger les passages après suppression
|
|
_loadPassages();
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|