- 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>
4138 lines
143 KiB
Dart
Executable File
4138 lines
143 KiB
Dart
Executable File
import 'package:flutter/material.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
import 'package:latlong2/latlong.dart';
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
|
import 'dart:math' as math;
|
|
import 'package:geosector_app/presentation/widgets/mapbox_map.dart';
|
|
import 'package:geosector_app/core/constants/app_keys.dart';
|
|
import 'package:geosector_app/core/services/location_service.dart';
|
|
import 'package:geosector_app/core/data/models/sector_model.dart';
|
|
import 'package:geosector_app/core/data/models/passage_model.dart';
|
|
import 'package:geosector_app/core/data/models/user_sector_model.dart';
|
|
import 'package:geosector_app/presentation/dialogs/sector_dialog.dart';
|
|
import 'package:geosector_app/core/repositories/sector_repository.dart';
|
|
import 'package:geosector_app/core/repositories/passage_repository.dart';
|
|
import 'package:geosector_app/core/services/current_amicale_service.dart';
|
|
import 'package:geosector_app/core/services/current_user_service.dart';
|
|
import 'package:geosector_app/core/repositories/operation_repository.dart';
|
|
import 'package:geosector_app/presentation/widgets/passage_map_dialog.dart';
|
|
|
|
class AdminMapPage extends StatefulWidget {
|
|
const AdminMapPage({super.key});
|
|
|
|
@override
|
|
State<AdminMapPage> createState() => _AdminMapPageState();
|
|
}
|
|
|
|
// Enum pour les modes de la carte
|
|
enum MapMode {
|
|
view, // Mode vue normale
|
|
drawing, // Mode création de secteur
|
|
editing, // Mode modification de secteur
|
|
deleting, // Mode suppression de secteur
|
|
}
|
|
|
|
class _AdminMapPageState extends State<AdminMapPage> {
|
|
// 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 = [];
|
|
|
|
// États
|
|
MapMode _mapMode = MapMode.view;
|
|
int? _selectedSectorId;
|
|
List<DropdownMenuItem<int?>> _sectorItems = [];
|
|
|
|
// États pour le mode dessin
|
|
List<LatLng> _drawingPoints = [];
|
|
int? _draggingPointIndex; // Index du point en cours de déplacement
|
|
LatLng? _originalDragPosition; // Position originale avant le drag
|
|
|
|
// États pour le magnétisme
|
|
LatLng? _snapPoint; // Point d'accrochage détecté
|
|
int? _snapSectorId; // ID du secteur sur lequel on s'accroche
|
|
int? _snapSegmentIndex; // Index du segment d'accrochage
|
|
static const double _snapDistance = 20.0; // Distance de magnétisme en mètres
|
|
|
|
// État pour les midpoints
|
|
int? _hoveredMidpointIndex; // Index du midpoint survolé
|
|
|
|
// États pour le mode édition
|
|
SectorModel? _selectedSectorForEdit;
|
|
List<LatLng> _editingPoints = [];
|
|
Map<int, LatLng> _originalPoints = {}; // Pour annuler les modifications
|
|
int? _hoveredPointIndex; // Index du point principal survolé
|
|
|
|
// État pour le mode suppression
|
|
int? _sectorToDeleteId;
|
|
int? _hoveredSectorId; // ID du secteur survolé en mode suppression
|
|
|
|
// État pour le survol en mode édition
|
|
int? _hoveredSectorIdForEdit; // ID du secteur survolé en mode édition
|
|
|
|
// État pour bloquer le drag de la carte pendant le déplacement des points
|
|
bool _isDraggingPoint = false;
|
|
|
|
// Référence à la boîte Hive pour les paramètres
|
|
late Box _settingsBox;
|
|
|
|
// Listener pour les changements de paramètres
|
|
late ValueListenable<Box<dynamic>> _settingsListenable;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initSettings().then((_) {
|
|
_loadSectors();
|
|
_loadPassages();
|
|
|
|
// Écouter les changements du secteur sélectionné
|
|
_settingsListenable = _settingsBox.listenable(keys: ['selectedSectorId']);
|
|
_settingsListenable.addListener(_onSectorSelectionChanged);
|
|
|
|
// Centrer la carte une seule fois après le chargement initial
|
|
Future.delayed(const Duration(milliseconds: 100), () {
|
|
if (_selectedSectorId != null &&
|
|
_sectors.any((s) => s['id'] == _selectedSectorId)) {
|
|
_centerMapOnSpecificSector(_selectedSectorId!);
|
|
} else if (_sectors.isNotEmpty) {
|
|
_centerMapOnSectors();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// 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 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;
|
|
}
|
|
}
|
|
|
|
// Méthode pour gérer les changements de sélection de secteur
|
|
void _onSectorSelectionChanged() {
|
|
final newSectorId = _settingsBox.get('selectedSectorId');
|
|
if (newSectorId != null && newSectorId != _selectedSectorId) {
|
|
setState(() {
|
|
_selectedSectorId = newSectorId;
|
|
});
|
|
|
|
// Recharger les passages pour le nouveau secteur
|
|
_loadPassages();
|
|
|
|
// Attendre que le build soit fait puis centrer sur le secteur
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (_sectors.any((s) => s['id'] == _selectedSectorId)) {
|
|
_centerMapOnSpecificSector(_selectedSectorId!);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_settingsListenable.removeListener(_onSectorSelectionChanged);
|
|
_mapController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
// Sauvegarder les paramètres utilisateur
|
|
void _saveSettings() {
|
|
// 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 (pour ValueListenableBuilder)
|
|
void _loadSectorsFromBox(Box<SectorModel> sectorsBox) {
|
|
try {
|
|
final sectors = sectorsBox.values.toList();
|
|
|
|
_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
|
|
_updateSectorItemsWithoutSetState();
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du chargement des secteurs: $e');
|
|
}
|
|
}
|
|
|
|
// Charger les secteurs depuis la boîte Hive (avec setState)
|
|
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();
|
|
});
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du chargement des secteurs: $e');
|
|
}
|
|
}
|
|
|
|
// Charger les passages depuis la boîte (pour ValueListenableBuilder)
|
|
void _loadPassagesFromBox(Box<PassageModel> passagesBox) {
|
|
try {
|
|
final List<Map<String, dynamic>> newPassages = [];
|
|
|
|
for (var i = 0; i < passagesBox.length; i++) {
|
|
final passage = passagesBox.getAt(i);
|
|
if (passage != null) {
|
|
final lat = double.tryParse(passage.gpsLat);
|
|
final lng = double.tryParse(passage.gpsLng);
|
|
|
|
if (_selectedSectorId != null &&
|
|
passage.fkSector != _selectedSectorId) {
|
|
continue;
|
|
}
|
|
|
|
if (lat != null && lng != null) {
|
|
Color passageColor = Colors.grey;
|
|
|
|
if (AppKeys.typesPassages.containsKey(passage.fkType)) {
|
|
final colorValue =
|
|
AppKeys.typesPassages[passage.fkType]!['couleur1'] as int;
|
|
passageColor = Color(colorValue);
|
|
|
|
newPassages.add({
|
|
'id': passage.id,
|
|
'position': LatLng(lat, lng),
|
|
'type': passage.fkType,
|
|
'color': passageColor,
|
|
'model': passage,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_passages.clear();
|
|
_passages.addAll(newPassages);
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du chargement des passages: $e');
|
|
}
|
|
}
|
|
|
|
// Charger les passages depuis la boîte Hive (avec setState)
|
|
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
|
|
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);
|
|
});
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du chargement des passages: $e');
|
|
}
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
|
|
// Assombrir une couleur pour les labels
|
|
Color _darkenColor(Color color, [double amount = 0.3]) {
|
|
assert(amount >= 0 && amount <= 1);
|
|
|
|
final hsl = HSLColor.fromColor(color);
|
|
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
|
|
|
|
return hslDark.toColor();
|
|
}
|
|
|
|
// 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');
|
|
}
|
|
|
|
// Mettre à jour les items de la combobox de secteurs (sans setState)
|
|
void _updateSectorItemsWithoutSetState() {
|
|
final List<DropdownMenuItem<int?>> items = [
|
|
const DropdownMenuItem<int?>(
|
|
value: null,
|
|
child: Text('Tous les secteurs'),
|
|
),
|
|
];
|
|
|
|
for (final sector in _sectors) {
|
|
items.add(
|
|
DropdownMenuItem<int?>(
|
|
value: sector['id'] as int,
|
|
child: Text(sector['name'] as String),
|
|
),
|
|
);
|
|
}
|
|
|
|
_sectorItems = items;
|
|
}
|
|
|
|
// Mettre à jour les items de la combobox de secteurs (avec setState)
|
|
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;
|
|
});
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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;
|
|
|
|
// Ajouter un padding minimal aux limites pour s'assurer que le secteur est bien visible
|
|
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;
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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;
|
|
});
|
|
|
|
// Recharger les passages pour appliquer le filtre par secteur
|
|
_loadPassages();
|
|
}
|
|
|
|
// 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) {
|
|
// 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;
|
|
|
|
// 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
|
|
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;
|
|
}
|
|
|
|
return zoom;
|
|
}
|
|
|
|
// 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,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
// Calculer le centre d'un polygone
|
|
LatLng _calculatePolygonCenter(List<LatLng> points) {
|
|
double sumLat = 0;
|
|
double sumLng = 0;
|
|
|
|
for (final point in points) {
|
|
sumLat += point.latitude;
|
|
sumLng += point.longitude;
|
|
}
|
|
|
|
return LatLng(sumLat / points.length, sumLng / points.length);
|
|
}
|
|
|
|
// Compter les passages par secteur
|
|
Map<int, int> _countPassagesBySector() {
|
|
final Map<int, int> passageCount = {};
|
|
|
|
// Initialiser tous les secteurs à 0
|
|
for (final sector in _sectors) {
|
|
passageCount[sector['id'] as int] = 0;
|
|
}
|
|
|
|
// Si un secteur spécifique est sélectionné, on ne compte que ses passages
|
|
if (_selectedSectorId != null) {
|
|
// Les passages sont déjà filtrés dans _passages
|
|
passageCount[_selectedSectorId!] = _passages.length;
|
|
} else {
|
|
// Compter tous les passages de la boîte Hive
|
|
try {
|
|
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
|
for (var i = 0; i < passagesBox.length; i++) {
|
|
final passage = passagesBox.getAt(i);
|
|
if (passage != null && passage.fkSector != null) {
|
|
passageCount[passage.fkSector!] =
|
|
(passageCount[passage.fkSector!] ?? 0) + 1;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du comptage des passages: $e');
|
|
}
|
|
}
|
|
|
|
return passageCount;
|
|
}
|
|
|
|
// Compter les membres par secteur
|
|
Map<int, int> _countMembersBySector() {
|
|
final Map<int, int> memberCount = {};
|
|
|
|
// Initialiser tous les secteurs à 0
|
|
for (final sector in _sectors) {
|
|
memberCount[sector['id'] as int] = 0;
|
|
}
|
|
|
|
// Compter les membres depuis la box UserSector
|
|
try {
|
|
if (Hive.isBoxOpen(AppKeys.userSectorBoxName)) {
|
|
final userSectorBox =
|
|
Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
|
|
for (var i = 0; i < userSectorBox.length; i++) {
|
|
final userSector = userSectorBox.getAt(i);
|
|
if (userSector != null) {
|
|
memberCount[userSector.fkSector] =
|
|
(memberCount[userSector.fkSector] ?? 0) + 1;
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du comptage des membres: $e');
|
|
}
|
|
|
|
return memberCount;
|
|
}
|
|
|
|
// Construire les marqueurs de labels pour les secteurs
|
|
List<Marker> _buildSectorLabels() {
|
|
// Ne pas afficher les labels en mode dessin ou suppression
|
|
if (_sectors.isEmpty || _mapMode != MapMode.view) {
|
|
return [];
|
|
}
|
|
|
|
final passageCount = _countPassagesBySector();
|
|
final memberCount = _countMembersBySector();
|
|
|
|
return _sectors.map((sector) {
|
|
final points = sector['points'] as List<LatLng>;
|
|
final center = _calculatePolygonCenter(points);
|
|
final sectorId = sector['id'] as int;
|
|
final sectorName = sector['name'] as String;
|
|
final sectorColor = sector['color'] as Color;
|
|
final count = passageCount[sectorId] ?? 0;
|
|
final members = memberCount[sectorId] ?? 0;
|
|
|
|
// Utiliser une couleur plus foncée pour le texte
|
|
final textColor = _darkenColor(sectorColor, 0.4);
|
|
|
|
return Marker(
|
|
point: center,
|
|
width: 200,
|
|
height: 75,
|
|
child: IgnorePointer(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
sectorName,
|
|
style: TextStyle(
|
|
color: textColor,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14,
|
|
shadows: [
|
|
Shadow(
|
|
color: Colors.white.withValues(alpha: 0.8),
|
|
offset: const Offset(1, 1),
|
|
blurRadius: 3,
|
|
),
|
|
Shadow(
|
|
color: Colors.white.withValues(alpha: 0.8),
|
|
offset: const Offset(-1, -1),
|
|
blurRadius: 3,
|
|
),
|
|
Shadow(
|
|
color: Colors.white.withValues(alpha: 0.8),
|
|
offset: const Offset(1, -1),
|
|
blurRadius: 3,
|
|
),
|
|
Shadow(
|
|
color: Colors.white.withValues(alpha: 0.8),
|
|
offset: const Offset(-1, 1),
|
|
blurRadius: 3,
|
|
),
|
|
],
|
|
),
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
'$count passage${count > 1 ? 's' : ''}',
|
|
style: TextStyle(
|
|
color: textColor,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
shadows: [
|
|
Shadow(
|
|
color: Colors.white.withValues(alpha: 0.8),
|
|
offset: const Offset(1, 1),
|
|
blurRadius: 3,
|
|
),
|
|
Shadow(
|
|
color: Colors.white.withValues(alpha: 0.8),
|
|
offset: const Offset(-1, -1),
|
|
blurRadius: 3,
|
|
),
|
|
Shadow(
|
|
color: Colors.white.withValues(alpha: 0.8),
|
|
offset: const Offset(1, -1),
|
|
blurRadius: 3,
|
|
),
|
|
Shadow(
|
|
color: Colors.white.withValues(alpha: 0.8),
|
|
offset: const Offset(-1, 1),
|
|
blurRadius: 3,
|
|
),
|
|
],
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
Text(
|
|
'$members membre${members > 1 ? 's' : ''}',
|
|
style: TextStyle(
|
|
color: textColor,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w500,
|
|
shadows: [
|
|
Shadow(
|
|
color: Colors.white.withValues(alpha: 0.8),
|
|
offset: const Offset(1, 1),
|
|
blurRadius: 3,
|
|
),
|
|
Shadow(
|
|
color: Colors.white.withValues(alpha: 0.8),
|
|
offset: const Offset(-1, -1),
|
|
blurRadius: 3,
|
|
),
|
|
Shadow(
|
|
color: Colors.white.withValues(alpha: 0.8),
|
|
offset: const Offset(1, -1),
|
|
blurRadius: 3,
|
|
),
|
|
Shadow(
|
|
color: Colors.white.withValues(alpha: 0.8),
|
|
offset: const Offset(-1, 1),
|
|
blurRadius: 3,
|
|
),
|
|
],
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
// Méthode pour construire les marqueurs des passages
|
|
List<Marker> _buildMarkers() {
|
|
if (_passages.isEmpty) {
|
|
return [];
|
|
}
|
|
|
|
return _passages.map((passage) {
|
|
final int passageType = passage['type'] as int;
|
|
final Color color1 =
|
|
passage['color'] as Color; // couleur1 du type de passage
|
|
final PassageModel passageModel = passage['model'] as PassageModel;
|
|
final bool hasNoSector = passageModel.fkSector == null;
|
|
|
|
// Récupérer la couleur2 du type de passage
|
|
Color color2 = Colors.white; // Couleur par défaut
|
|
if (AppKeys.typesPassages.containsKey(passageType)) {
|
|
final colorValue =
|
|
AppKeys.typesPassages[passageType]!['couleur2'] as int;
|
|
color2 = Color(colorValue);
|
|
}
|
|
|
|
// Si le passage n'a pas de secteur, on met une bordure rouge épaisse
|
|
final Color borderColor = hasNoSector ? Colors.red : color2;
|
|
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: color1,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: borderColor,
|
|
width: borderWidth,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
// Méthode pour construire les polygones des secteurs
|
|
List<Polygon> _buildPolygons() {
|
|
if (_sectors.isEmpty) {
|
|
return [];
|
|
}
|
|
|
|
return _sectors.map((sector) {
|
|
final int sectorId = sector['id'] as int;
|
|
final bool isSelected = _selectedSectorId == sectorId;
|
|
final bool isMarkedForDeletion =
|
|
_mapMode == MapMode.deleting && _sectorToDeleteId == sectorId;
|
|
final bool isHovered =
|
|
_mapMode == MapMode.deleting && _hoveredSectorId == sectorId;
|
|
final bool isHoveredForEdit = _mapMode == MapMode.editing &&
|
|
_hoveredSectorIdForEdit == sectorId &&
|
|
_selectedSectorForEdit == null;
|
|
final bool isSelectedForEdit =
|
|
_mapMode == MapMode.editing && _selectedSectorForEdit?.id == sectorId;
|
|
final Color sectorColor = sector['color'] as Color;
|
|
|
|
// Déterminer la couleur et l'opacité selon l'état
|
|
Color fillColor;
|
|
Color borderColor;
|
|
double borderWidth;
|
|
|
|
if (isMarkedForDeletion) {
|
|
// Secteur marqué pour suppression
|
|
fillColor = Colors.red.withValues(alpha: 0.5);
|
|
borderColor = Colors.red;
|
|
borderWidth = 4.0;
|
|
} else if (isHovered) {
|
|
// Secteur survolé en mode suppression
|
|
fillColor = sectorColor.withValues(alpha: 0.45);
|
|
borderColor = Colors.red.withValues(alpha: 0.8);
|
|
borderWidth = 3.0;
|
|
} else if (isHoveredForEdit) {
|
|
// Secteur survolé en mode édition
|
|
fillColor = sectorColor.withValues(alpha: 0.45);
|
|
borderColor = Colors.green;
|
|
borderWidth = 4.0;
|
|
} else if (isSelectedForEdit) {
|
|
// Secteur sélectionné pour édition
|
|
fillColor = sectorColor.withValues(alpha: 0.5);
|
|
borderColor = Colors.orange;
|
|
borderWidth = 4.0;
|
|
} else if (isSelected) {
|
|
// Secteur sélectionné
|
|
fillColor = sectorColor.withValues(alpha: 0.5);
|
|
borderColor = sectorColor;
|
|
borderWidth = 3.0;
|
|
} else {
|
|
// Secteur normal
|
|
fillColor = sectorColor.withValues(alpha: 0.3);
|
|
borderColor = sectorColor.withValues(alpha: 0.8);
|
|
borderWidth = 2.0;
|
|
}
|
|
|
|
return Polygon(
|
|
points: sector['points'] as List<LatLng>,
|
|
color: fillColor,
|
|
borderColor: borderColor,
|
|
borderStrokeWidth: borderWidth,
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
// 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: true,
|
|
onDeleted: () {
|
|
// Recharger les passages après suppression
|
|
_loadPassages();
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// Démarrer le mode dessin
|
|
void _startDrawingMode() {
|
|
setState(() {
|
|
_mapMode = MapMode.drawing;
|
|
_drawingPoints.clear();
|
|
});
|
|
}
|
|
|
|
// Démarrer le mode suppression
|
|
void _startDeletingMode() {
|
|
setState(() {
|
|
_mapMode = MapMode.deleting;
|
|
_sectorToDeleteId = null;
|
|
});
|
|
}
|
|
|
|
// Démarrer le mode édition
|
|
void _startEditingMode() {
|
|
setState(() {
|
|
_mapMode = MapMode.editing;
|
|
_selectedSectorForEdit = null;
|
|
_editingPoints.clear();
|
|
});
|
|
}
|
|
|
|
// Construire la carte d'aide pour le mode création
|
|
Widget _buildHelpCard() {
|
|
return Material(
|
|
elevation: 4,
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
width: 320,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.95),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: Colors.blue.withValues(alpha: 0.3),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.info_outline,
|
|
color: Colors.blue,
|
|
size: 24,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Création d\'un secteur',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.blue[800],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'Cliquer sur la carte pour créer le 1er point de contour du nouveau secteur. Ensuite créer autant de points nécessaires pour dessiner les contours du secteur, jusqu\'à cliquer une dernière fois sur le 1er point pour finaliser la création du secteur.',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey[700],
|
|
height: 1.4,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'• Clic droit ou Ctrl+clic sur un point pour le supprimer',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: Colors.grey[600],
|
|
fontStyle: FontStyle.italic,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'• Cliquer-glisser sur un point pour le déplacer',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: Colors.grey[600],
|
|
fontStyle: FontStyle.italic,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'• Bouton "Annuler dernier" pour supprimer le dernier point ajouté',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: Colors.grey[600],
|
|
fontStyle: FontStyle.italic,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 20,
|
|
height: 20,
|
|
decoration: BoxDecoration(
|
|
color: Colors.green,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: Colors.white,
|
|
width: 2,
|
|
),
|
|
),
|
|
child: const Center(
|
|
child: Text(
|
|
'1',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Premier point',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Container(
|
|
width: 16,
|
|
height: 16,
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: Colors.white,
|
|
width: 2,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Points suivants',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Construire la carte d'aide pour le mode suppression
|
|
Widget _buildDeleteHelpCard() {
|
|
return Material(
|
|
elevation: 4,
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
width: 360,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.95),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: Colors.red.withValues(alpha: 0.3),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.warning_amber_rounded,
|
|
color: Colors.red,
|
|
size: 24,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Suppression d\'un secteur',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.red[800],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'Vous devez sélectionner le secteur que vous voulez supprimer en cliquant dessus une seule fois. Tous les passages à finaliser et sans infos d\'habitant seront supprimés. Les autres passages seront gardés, mais sans secteur, en attendant que vous recréez un nouveau secteur sur ces passages.',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey[700],
|
|
height: 1.4,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
setState(() {
|
|
_mapMode = MapMode.view;
|
|
_sectorToDeleteId = null;
|
|
});
|
|
},
|
|
icon: const Icon(Icons.cancel, size: 18),
|
|
label: const Text('Annuler'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.grey,
|
|
foregroundColor: Colors.white,
|
|
minimumSize: const Size(100, 36),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Construire la carte d'aide pour le mode édition
|
|
Widget _buildEditHelpCard() {
|
|
return Material(
|
|
elevation: 4,
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
width: 340,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.95),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: Colors.orange.withValues(alpha: 0.3),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.edit_location_alt,
|
|
color: Colors.orange,
|
|
size: 24,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Modification d\'un secteur',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.orange[800],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
if (_selectedSectorForEdit == null) ...[
|
|
Text(
|
|
'Cliquez sur le secteur que vous souhaitez modifier.',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey[700],
|
|
height: 1.4,
|
|
),
|
|
),
|
|
] else ...[
|
|
Text(
|
|
'Secteur sélectionné : ${_selectedSectorForEdit!.libelle}',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.orange[800],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.orange.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(4),
|
|
border:
|
|
Border.all(color: Colors.orange.withValues(alpha: 0.3)),
|
|
),
|
|
child: Text(
|
|
'La modification est verrouillée sur ce secteur.\n'
|
|
'Enregistrez ou annulez avant de modifier un autre secteur.',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.orange[700],
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'• Cliquer-glisser sur un point pour le déplacer\n'
|
|
'• Clic droit ou Ctrl+clic sur un point pour le supprimer\n'
|
|
'• Cliquer sur les points intermédiaires pour en ajouter',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: Colors.grey[600],
|
|
fontStyle: FontStyle.italic,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Annuler le mode dessin
|
|
void _cancelDrawingMode() {
|
|
setState(() {
|
|
_mapMode = MapMode.view;
|
|
_drawingPoints.clear();
|
|
});
|
|
}
|
|
|
|
// Annuler le mode édition
|
|
void _cancelEditingMode() {
|
|
setState(() {
|
|
_mapMode = MapMode.view;
|
|
_selectedSectorForEdit = null;
|
|
_editingPoints.clear();
|
|
_originalPoints.clear();
|
|
});
|
|
}
|
|
|
|
// Sauvegarder le secteur modifié
|
|
Future<void> _saveEditedSector() async {
|
|
if (_selectedSectorForEdit == null || _editingPoints.isEmpty) return;
|
|
|
|
// Vérifier que le polygone est valide
|
|
if (!_isValidPolygon(_editingPoints)) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'Le secteur contient des lignes qui se croisent. Veuillez corriger le tracé.'),
|
|
backgroundColor: Colors.red,
|
|
duration: Duration(seconds: 3),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Appliquer la correction automatique des points adjacents (comme pour la création)
|
|
// En excluant le secteur actuel de la vérification
|
|
final correctedPoints = _correctPointsForAdjacency(_editingPoints,
|
|
excludeSectorId: _selectedSectorForEdit!.id);
|
|
|
|
// Vérifier si des points ont été corrigés
|
|
bool pointsWereCorrected = false;
|
|
for (int i = 0; i < _editingPoints.length; i++) {
|
|
if (_editingPoints[i] != correctedPoints[i]) {
|
|
pointsWereCorrected = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Mettre à jour les points avec la version corrigée
|
|
setState(() {
|
|
_editingPoints = correctedPoints;
|
|
});
|
|
|
|
// Informer l'utilisateur si des corrections ont été appliquées
|
|
if (pointsWereCorrected) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'Les points ont été ajustés automatiquement pour éviter les chevauchements'),
|
|
backgroundColor: Colors.blue,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Vérifier les chevauchements avec les autres secteurs (avec les points corrigés)
|
|
bool hasOverlap = false;
|
|
String? overlappingSectorName;
|
|
|
|
for (final sector in _sectors) {
|
|
// Ne pas vérifier avec lui-même
|
|
if (sector['id'] == _selectedSectorForEdit!.id) continue;
|
|
|
|
final sectorPoints = sector['points'] as List<LatLng>;
|
|
if (_doPolygonsOverlap(correctedPoints, sectorPoints)) {
|
|
hasOverlap = true;
|
|
overlappingSectorName = sector['name'] as String;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasOverlap) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
'Le secteur modifié chevauche avec le secteur "$overlappingSectorName"'),
|
|
backgroundColor: Colors.red,
|
|
duration: const Duration(seconds: 3),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Préparer les coordonnées pour le dialog (avec les points corrigés)
|
|
final coordinates = correctedPoints
|
|
.map((point) => [point.latitude, point.longitude])
|
|
.toList();
|
|
|
|
// Réinitialiser le mode AVANT d'afficher le dialog
|
|
setState(() {
|
|
_mapMode = MapMode.view;
|
|
_editingPoints.clear();
|
|
_originalPoints.clear();
|
|
});
|
|
|
|
// Afficher le dialog de modification (comme pour la création)
|
|
await _showSectorDialog(
|
|
existingSector: _selectedSectorForEdit, coordinates: coordinates);
|
|
|
|
// Réinitialiser la sélection après le dialog
|
|
setState(() {
|
|
_selectedSectorForEdit = null;
|
|
});
|
|
}
|
|
|
|
// Supprimer un point de dessin
|
|
void _removeDrawingPoint(int index) {
|
|
if (_drawingPoints.length <= 1) {
|
|
// Ne pas permettre de supprimer le dernier point
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Un secteur doit avoir au moins un point'),
|
|
backgroundColor: Colors.orange,
|
|
duration: Duration(seconds: 1),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_drawingPoints.removeAt(index);
|
|
// Si on supprime le point sélectionné, désélectionner
|
|
if (_draggingPointIndex == index) {
|
|
_draggingPointIndex = null;
|
|
} else if (_draggingPointIndex != null && _draggingPointIndex! > index) {
|
|
// Ajuster l'index si nécessaire
|
|
_draggingPointIndex = _draggingPointIndex! - 1;
|
|
}
|
|
});
|
|
|
|
// Afficher un message de confirmation
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Point supprimé'),
|
|
backgroundColor: Colors.green,
|
|
duration: Duration(seconds: 1),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Déplacer un point de dessin
|
|
|
|
// Annuler le dernier point ajouté
|
|
void _undoLastPoint() {
|
|
if (_drawingPoints.isEmpty) return;
|
|
|
|
setState(() {
|
|
_drawingPoints.removeLast();
|
|
// Si on avait sélectionné le dernier point, désélectionner
|
|
if (_draggingPointIndex != null &&
|
|
_draggingPointIndex! >= _drawingPoints.length) {
|
|
_draggingPointIndex = null;
|
|
}
|
|
});
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Dernier point annulé'),
|
|
backgroundColor: Colors.orange,
|
|
duration: Duration(seconds: 1),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Gérer le tap sur la carte
|
|
void _handleMapTap(LatLng position) {
|
|
if (_mapMode == MapMode.drawing) {
|
|
// Détecter le magnétisme
|
|
final snapPoint = _detectSnapPoint(position);
|
|
final finalPosition = snapPoint ?? position;
|
|
|
|
// Si c'est le premier point ou un nouveau point
|
|
if (_drawingPoints.isEmpty) {
|
|
// Vérifier que le premier point n'est pas dans un secteur existant
|
|
if (_isPointInsideExistingSector(finalPosition)) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'Impossible de créer un point à l\'intérieur d\'un secteur existant'),
|
|
backgroundColor: Colors.red,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
setState(() {
|
|
_drawingPoints.add(finalPosition);
|
|
});
|
|
} else {
|
|
// Vérifier si on clique sur le premier point pour fermer le polygone
|
|
final firstPoint = _drawingPoints.first;
|
|
final distance = _calculateDistance(position, firstPoint);
|
|
|
|
// Si on clique proche du premier point (dans un rayon de 30 mètres)
|
|
if (_drawingPoints.length >= 3 && distance < 30) {
|
|
// Vérifier que le polygone est valide avant de finaliser
|
|
if (_isValidPolygon(_drawingPoints)) {
|
|
// Finaliser la création du secteur
|
|
_finalizeSectorCreation();
|
|
} else {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'Le secteur contient des lignes qui se croisent. Veuillez corriger le tracé.'),
|
|
backgroundColor: Colors.red,
|
|
duration: Duration(seconds: 3),
|
|
),
|
|
);
|
|
}
|
|
} else {
|
|
// Vérifier que l'ajout du nouveau point ne crée pas de croisement
|
|
if (_drawingPoints.length >= 2) {
|
|
// Vérifier s'il y a un croisement avec la nouvelle ligne
|
|
final lastPoint = _drawingPoints.last;
|
|
bool hasCrossing = false;
|
|
|
|
// Vérifier avec toutes les lignes existantes sauf la dernière
|
|
for (int i = 0; i < _drawingPoints.length - 2; i++) {
|
|
if (_doSegmentsIntersect(lastPoint, finalPosition,
|
|
_drawingPoints[i], _drawingPoints[i + 1])) {
|
|
hasCrossing = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasCrossing) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'Cette ligne croiserait une ligne existante du secteur'),
|
|
backgroundColor: Colors.orange,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Vérifier que le nouveau point n'est pas dans un secteur existant
|
|
if (_isPointInsideExistingSector(finalPosition)) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'Impossible de créer un point à l\'intérieur d\'un secteur existant'),
|
|
backgroundColor: Colors.red,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Ajouter le nouveau point
|
|
setState(() {
|
|
_drawingPoints.add(finalPosition);
|
|
// Réinitialiser le snap après l'ajout
|
|
_snapPoint = null;
|
|
_snapSectorId = null;
|
|
_snapSegmentIndex = null;
|
|
});
|
|
}
|
|
}
|
|
} else if (_mapMode == MapMode.deleting) {
|
|
// En mode suppression, trouver le secteur cliqué
|
|
_handleSectorClick(position);
|
|
} else if (_mapMode == MapMode.editing) {
|
|
// En mode édition, sélectionner le secteur à modifier seulement si aucun n'est déjà sélectionné
|
|
if (_selectedSectorForEdit == null) {
|
|
_handleSectorClickForEdit(position);
|
|
} else {
|
|
// Vérifier si on clique sur un autre secteur
|
|
for (final sector in _sectors) {
|
|
final points = sector['points'] as List<LatLng>;
|
|
if (_isPointInPolygon(position, points) &&
|
|
sector['id'] != _selectedSectorForEdit!.id) {
|
|
// Afficher un message d'avertissement
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'Veuillez enregistrer ou annuler les modifications en cours avant de sélectionner un autre secteur'),
|
|
backgroundColor: Colors.orange,
|
|
duration: Duration(seconds: 3),
|
|
),
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculer la distance entre deux points (en mètres)
|
|
double _calculateDistance(LatLng point1, LatLng point2) {
|
|
final Distance distance = Distance();
|
|
return distance.as(LengthUnit.Meter, point1, point2);
|
|
}
|
|
|
|
// Détecter le point d'accrochage le plus proche
|
|
LatLng? _detectSnapPoint(LatLng currentPoint) {
|
|
if (_sectors.isEmpty) return null;
|
|
|
|
LatLng? bestSnapPoint;
|
|
double bestDistance = _snapDistance;
|
|
int? bestSectorId;
|
|
int? bestSegmentIndex;
|
|
|
|
// Parcourir tous les secteurs
|
|
for (final sector in _sectors) {
|
|
final points = sector['points'] as List<LatLng>;
|
|
final sectorId = sector['id'] as int;
|
|
|
|
// En mode édition, ignorer le secteur en cours d'édition
|
|
if (_mapMode == MapMode.editing &&
|
|
_selectedSectorForEdit != null &&
|
|
sectorId == _selectedSectorForEdit!.id) {
|
|
continue;
|
|
}
|
|
|
|
// Vérifier la proximité avec chaque point du secteur
|
|
for (int i = 0; i < points.length; i++) {
|
|
final distance = _calculateDistance(currentPoint, points[i]);
|
|
if (distance < bestDistance) {
|
|
bestDistance = distance;
|
|
bestSnapPoint = points[i];
|
|
bestSectorId = sectorId;
|
|
bestSegmentIndex = null; // C'est un sommet, pas un segment
|
|
}
|
|
}
|
|
|
|
// Vérifier la proximité avec chaque segment du secteur
|
|
for (int i = 0; i < points.length; i++) {
|
|
final start = points[i];
|
|
final end = points[(i + 1) % points.length];
|
|
|
|
// Calculer le point le plus proche sur le segment
|
|
final snapPoint = _getClosestPointOnSegment(currentPoint, start, end);
|
|
final distance = _calculateDistance(currentPoint, snapPoint);
|
|
|
|
if (distance < bestDistance) {
|
|
bestDistance = distance;
|
|
bestSnapPoint = snapPoint;
|
|
bestSectorId = sectorId;
|
|
bestSegmentIndex = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mettre à jour l'état du snap
|
|
if (bestSnapPoint != null) {
|
|
_snapPoint = bestSnapPoint;
|
|
_snapSectorId = bestSectorId;
|
|
_snapSegmentIndex = bestSegmentIndex;
|
|
} else {
|
|
_snapPoint = null;
|
|
_snapSectorId = null;
|
|
_snapSegmentIndex = null;
|
|
}
|
|
|
|
return bestSnapPoint;
|
|
}
|
|
|
|
// Corriger les points pour éviter les chevauchements avec les secteurs adjacents
|
|
List<LatLng> _correctPointsForAdjacency(List<LatLng> points,
|
|
{int? excludeSectorId}) {
|
|
final correctedPoints = List<LatLng>.from(points);
|
|
const double snapDistance =
|
|
10.0; // Distance pour détecter l'adjacence (10m)
|
|
const double correctionOffset =
|
|
1.0; // Décalage de correction réduit (1m vers l'intérieur)
|
|
|
|
// Première passe : identifier les segments adjacents et corriger
|
|
final Map<int, List<LatLng>> pointCorrections = {};
|
|
|
|
// Pour chaque point, collecter toutes les corrections nécessaires
|
|
for (int i = 0; i < correctedPoints.length; i++) {
|
|
final point = correctedPoints[i];
|
|
final List<LatLng> corrections = [];
|
|
|
|
// Vérifier la proximité avec tous les secteurs existants
|
|
for (final sector in _sectors) {
|
|
// Exclure le secteur en cours d'édition si spécifié
|
|
if (excludeSectorId != null && sector['id'] == excludeSectorId) {
|
|
continue;
|
|
}
|
|
|
|
final sectorPoints = sector['points'] as List<LatLng>;
|
|
|
|
// Vérifier la proximité avec les segments du secteur
|
|
for (int j = 0; j < sectorPoints.length; j++) {
|
|
final segStart = sectorPoints[j];
|
|
final segEnd = sectorPoints[(j + 1) % sectorPoints.length];
|
|
|
|
// Point le plus proche sur le segment
|
|
final closestPoint =
|
|
_getClosestPointOnSegment(point, segStart, segEnd);
|
|
final distance = _calculateDistance(point, closestPoint);
|
|
|
|
// Si le point est proche du segment
|
|
if (distance < snapDistance && distance > 0.1) {
|
|
// Ignorer si déjà sur le segment
|
|
// Ne corriger que si vraiment nécessaire (très proche mais pas exactement sur le segment)
|
|
if (distance < 2.0) {
|
|
// Correction seulement si moins de 2m
|
|
final offsetPoint = _offsetPointInward(
|
|
closestPoint, correctedPoints, i, correctionOffset);
|
|
if (offsetPoint != null) {
|
|
corrections.add(offsetPoint);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Vérifier la proximité avec les sommets du secteur
|
|
for (final sectorPoint in sectorPoints) {
|
|
final distance = _calculateDistance(point, sectorPoint);
|
|
// Tolérer les sommets partagés (très courant pour les secteurs adjacents)
|
|
if (distance < 1.0) {
|
|
// Seulement si TRÈS proche (moins de 1m)
|
|
// Pour les sommets partagés, ne pas corriger ou corriger très peu
|
|
// car c'est normal d'avoir des sommets communs
|
|
continue; // Ignorer les sommets parfaitement alignés
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stocker toutes les corrections pour ce point
|
|
if (corrections.isNotEmpty) {
|
|
pointCorrections[i] = corrections;
|
|
}
|
|
}
|
|
|
|
// Appliquer les corrections en moyennant si plusieurs corrections existent
|
|
pointCorrections.forEach((index, corrections) {
|
|
if (corrections.length == 1) {
|
|
correctedPoints[index] = corrections.first;
|
|
} else {
|
|
// Si plusieurs corrections, utiliser la moyenne pondérée
|
|
double avgLat = 0;
|
|
double avgLng = 0;
|
|
for (final correction in corrections) {
|
|
avgLat += correction.latitude;
|
|
avgLng += correction.longitude;
|
|
}
|
|
correctedPoints[index] = LatLng(
|
|
avgLat / corrections.length,
|
|
avgLng / corrections.length,
|
|
);
|
|
}
|
|
});
|
|
|
|
// Deuxième passe : s'assurer qu'il n'y a pas de chevauchement
|
|
return _ensureNoIntersection(correctedPoints,
|
|
excludeSectorId: excludeSectorId);
|
|
}
|
|
|
|
// Décaler un point vers l'intérieur du polygone
|
|
LatLng? _offsetPointInward(
|
|
LatLng point, List<LatLng> polygon, int pointIndex, double offsetMeters) {
|
|
if (polygon.length < 3) return null;
|
|
|
|
// Obtenir les points adjacents
|
|
final prevIndex = (pointIndex - 1 + polygon.length) % polygon.length;
|
|
final nextIndex = (pointIndex + 1) % polygon.length;
|
|
final prevPoint = polygon[prevIndex];
|
|
final nextPoint = polygon[nextIndex];
|
|
|
|
// Calculer les vecteurs des segments adjacents
|
|
final vec1 = LatLng(
|
|
point.latitude - prevPoint.latitude,
|
|
point.longitude - prevPoint.longitude,
|
|
);
|
|
final vec2 = LatLng(
|
|
nextPoint.latitude - point.latitude,
|
|
nextPoint.longitude - point.longitude,
|
|
);
|
|
|
|
// Calculer la normale (perpendiculaire) au segment pour le décalage
|
|
// On utilise le produit vectoriel pour obtenir la direction perpendiculaire
|
|
final crossProduct =
|
|
vec1.latitude * vec2.longitude - vec1.longitude * vec2.latitude;
|
|
|
|
// Déterminer la direction vers l'intérieur du polygone
|
|
final sign = crossProduct > 0 ? 1.0 : -1.0;
|
|
|
|
// Calculer la bissectrice normalisée
|
|
final bisectorLat = -(vec1.latitude + vec2.latitude) * sign;
|
|
final bisectorLng = -(vec1.longitude + vec2.longitude) * sign;
|
|
|
|
final length =
|
|
math.sqrt(bisectorLat * bisectorLat + bisectorLng * bisectorLng);
|
|
if (length > 0) {
|
|
// Convertir le décalage en degrés avec une meilleure approximation
|
|
final latRad = point.latitude * math.pi / 180;
|
|
final offsetDegreesLat = offsetMeters / 111320.0;
|
|
final offsetDegreesLng = offsetMeters / (111320.0 * math.cos(latRad));
|
|
|
|
return LatLng(
|
|
point.latitude + (bisectorLat / length) * offsetDegreesLat,
|
|
point.longitude + (bisectorLng / length) * offsetDegreesLng,
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// S'assurer qu'il n'y a pas d'intersection après correction
|
|
List<LatLng> _ensureNoIntersection(List<LatLng> points,
|
|
{int? excludeSectorId}) {
|
|
List<LatLng> correctedPoints = List<LatLng>.from(points);
|
|
const int maxIterations = 5; // Éviter les boucles infinies
|
|
int iteration = 0;
|
|
|
|
// Continuer à corriger jusqu'à ce qu'il n'y ait plus de chevauchement
|
|
while (iteration < maxIterations) {
|
|
bool hasOverlap = false;
|
|
List<List<LatLng>> overlappingSectors = [];
|
|
|
|
// Collecter tous les secteurs qui chevauchent
|
|
for (final sector in _sectors) {
|
|
// Exclure le secteur en cours d'édition si spécifié
|
|
if (excludeSectorId != null && sector['id'] == excludeSectorId) {
|
|
continue;
|
|
}
|
|
|
|
final sectorPoints = sector['points'] as List<LatLng>;
|
|
|
|
if (_doPolygonsOverlap(correctedPoints, sectorPoints)) {
|
|
hasOverlap = true;
|
|
overlappingSectors.add(sectorPoints);
|
|
}
|
|
}
|
|
|
|
// Si pas de chevauchement, on a terminé
|
|
if (!hasOverlap) {
|
|
break;
|
|
}
|
|
|
|
// Appliquer une correction pour tous les secteurs qui chevauchent
|
|
correctedPoints =
|
|
_applyMultiSectorCorrection(correctedPoints, overlappingSectors);
|
|
iteration++;
|
|
}
|
|
|
|
return correctedPoints;
|
|
}
|
|
|
|
// Appliquer une correction pour plusieurs secteurs adjacents
|
|
List<LatLng> _applyMultiSectorCorrection(
|
|
List<LatLng> newPoints, List<List<LatLng>> overlappingSectors) {
|
|
final corrected = List<LatLng>.from(newPoints);
|
|
const double strongOffset = 3.0; // Décalage de base (3m)
|
|
const double additionalOffset = 1.0; // Décalage supplémentaire par secteur
|
|
|
|
// Pour chaque point du nouveau polygone
|
|
for (int i = 0; i < corrected.length; i++) {
|
|
final point = corrected[i];
|
|
List<LatLng> corrections = [];
|
|
|
|
// Calculer les corrections nécessaires pour chaque secteur chevauchant
|
|
for (final sectorPoints in overlappingSectors) {
|
|
// Vérifier si le point est proche ou à l'intérieur du secteur
|
|
if (_isPointInPolygon(point, sectorPoints) ||
|
|
_isPointOnPolygonBorder(point, sectorPoints, tolerance: 2.0)) {
|
|
// Trouver le point le plus proche sur le contour
|
|
LatLng? closestBoundaryPoint;
|
|
double minDistance = double.infinity;
|
|
|
|
for (int j = 0; j < sectorPoints.length; j++) {
|
|
final start = sectorPoints[j];
|
|
final end = sectorPoints[(j + 1) % sectorPoints.length];
|
|
final closestPoint = _getClosestPointOnSegment(point, start, end);
|
|
final distance = _calculateDistance(point, closestPoint);
|
|
|
|
if (distance < minDistance) {
|
|
minDistance = distance;
|
|
closestBoundaryPoint = closestPoint;
|
|
}
|
|
}
|
|
|
|
if (closestBoundaryPoint != null) {
|
|
// Calculer la direction d'éloignement
|
|
final direction = LatLng(
|
|
point.latitude - closestBoundaryPoint.latitude,
|
|
point.longitude - closestBoundaryPoint.longitude,
|
|
);
|
|
|
|
final length = math.sqrt(direction.latitude * direction.latitude +
|
|
direction.longitude * direction.longitude);
|
|
|
|
if (length > 0) {
|
|
// Appliquer un décalage proportionnel au nombre de secteurs adjacents
|
|
final totalOffset =
|
|
strongOffset + (additionalOffset * overlappingSectors.length);
|
|
final offsetDegreesLat = totalOffset / 111320.0;
|
|
final offsetDegreesLng = totalOffset /
|
|
(111320.0 * math.cos(point.latitude * math.pi / 180));
|
|
|
|
corrections.add(LatLng(
|
|
closestBoundaryPoint.latitude +
|
|
(direction.latitude / length) * offsetDegreesLat,
|
|
closestBoundaryPoint.longitude +
|
|
(direction.longitude / length) * offsetDegreesLng,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Si des corrections ont été calculées, utiliser la moyenne
|
|
if (corrections.isNotEmpty) {
|
|
double avgLat = 0;
|
|
double avgLng = 0;
|
|
for (final correction in corrections) {
|
|
avgLat += correction.latitude;
|
|
avgLng += correction.longitude;
|
|
}
|
|
corrected[i] = LatLng(
|
|
avgLat / corrections.length,
|
|
avgLng / corrections.length,
|
|
);
|
|
}
|
|
}
|
|
|
|
return corrected;
|
|
}
|
|
|
|
// Appliquer le magnétisme automatique à tous les points d'un secteur
|
|
List<LatLng> _applyAutomaticMagnetism(
|
|
List<LatLng> points, int currentSectorId) {
|
|
final magnetizedPoints = List<LatLng>.from(points);
|
|
const double autoSnapDistance = 10.0; // Distance automatique de 10m
|
|
|
|
// Pour chaque point du secteur
|
|
for (int i = 0; i < magnetizedPoints.length; i++) {
|
|
LatLng? bestSnapPoint;
|
|
double bestDistance = autoSnapDistance;
|
|
|
|
// Parcourir tous les autres secteurs
|
|
for (final sector in _sectors) {
|
|
final sectorId = sector['id'] as int;
|
|
|
|
// Ignorer le secteur actuel
|
|
if (sectorId == currentSectorId) continue;
|
|
|
|
final sectorPoints = sector['points'] as List<LatLng>;
|
|
|
|
// Vérifier la proximité avec chaque point du secteur adjacent
|
|
for (final sectorPoint in sectorPoints) {
|
|
final distance = _calculateDistance(magnetizedPoints[i], sectorPoint);
|
|
if (distance < bestDistance) {
|
|
bestDistance = distance;
|
|
bestSnapPoint = sectorPoint;
|
|
}
|
|
}
|
|
|
|
// Vérifier la proximité avec chaque segment du secteur adjacent
|
|
for (int j = 0; j < sectorPoints.length; j++) {
|
|
final start = sectorPoints[j];
|
|
final end = sectorPoints[(j + 1) % sectorPoints.length];
|
|
|
|
// Calculer le point le plus proche sur le segment
|
|
final snapPoint =
|
|
_getClosestPointOnSegment(magnetizedPoints[i], start, end);
|
|
final distance = _calculateDistance(magnetizedPoints[i], snapPoint);
|
|
|
|
if (distance < bestDistance) {
|
|
bestDistance = distance;
|
|
bestSnapPoint = snapPoint;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Appliquer le magnétisme si un point proche a été trouvé
|
|
if (bestSnapPoint != null) {
|
|
magnetizedPoints[i] = bestSnapPoint;
|
|
}
|
|
}
|
|
|
|
return magnetizedPoints;
|
|
}
|
|
|
|
// Calculer le point le plus proche sur un segment
|
|
LatLng _getClosestPointOnSegment(
|
|
LatLng point, LatLng segStart, LatLng segEnd) {
|
|
final dx = segEnd.longitude - segStart.longitude;
|
|
final dy = segEnd.latitude - segStart.latitude;
|
|
|
|
if (dx == 0 && dy == 0) {
|
|
return segStart; // Le segment est un point
|
|
}
|
|
|
|
// Calculer la projection du point sur la ligne définie par le segment
|
|
final t = ((point.longitude - segStart.longitude) * dx +
|
|
(point.latitude - segStart.latitude) * dy) /
|
|
(dx * dx + dy * dy);
|
|
|
|
// Limiter t entre 0 et 1 pour rester sur le segment
|
|
final tClamped = t.clamp(0.0, 1.0);
|
|
|
|
// Calculer le point projeté
|
|
return LatLng(
|
|
segStart.latitude + tClamped * dy,
|
|
segStart.longitude + tClamped * dx,
|
|
);
|
|
}
|
|
|
|
// Gérer le survol de la souris en mode dessin
|
|
void _handleDrawingHover(PointerHoverEvent event) {
|
|
// Récupérer la taille du widget
|
|
final RenderBox? renderBox = context.findRenderObject() as RenderBox?;
|
|
if (renderBox == null) return;
|
|
|
|
// Convertir la position en coordonnées géographiques
|
|
final localPosition = renderBox.globalToLocal(event.position);
|
|
final mapSize = renderBox.size;
|
|
final camera = _mapController.camera;
|
|
|
|
// Calculer le décalage par rapport au centre
|
|
final dx = localPosition.dx - (mapSize.width / 2);
|
|
final dy = localPosition.dy - (mapSize.height / 2);
|
|
|
|
// Utiliser la projection Web Mercator
|
|
const double tileSize = 256.0;
|
|
final scale = math.pow(2, camera.zoom);
|
|
|
|
final centerX = (camera.center.longitude + 180) / 360 * tileSize * scale;
|
|
final centerLatRad = camera.center.latitude * math.pi / 180;
|
|
final centerY = (1 -
|
|
math.log(math.tan(centerLatRad) + 1 / math.cos(centerLatRad)) /
|
|
math.pi) /
|
|
2 *
|
|
tileSize *
|
|
scale;
|
|
|
|
final mouseX = centerX + dx;
|
|
final mouseY = centerY + dy;
|
|
|
|
final lng = mouseX / (tileSize * scale) * 360 - 180;
|
|
final n = math.pi - 2 * math.pi * mouseY / (tileSize * scale);
|
|
final lat = 180 / math.pi * math.atan(0.5 * (math.exp(n) - math.exp(-n)));
|
|
|
|
final mousePosition = LatLng(lat, lng);
|
|
|
|
// Détecter le magnétisme
|
|
final snapPoint = _detectSnapPoint(mousePosition);
|
|
|
|
// Mettre à jour l'affichage du point d'accrochage
|
|
if (snapPoint != _snapPoint) {
|
|
setState(() {
|
|
_snapPoint = snapPoint;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Gérer le survol de la souris en mode édition
|
|
void _handleEditingHover(PointerHoverEvent event) {
|
|
// Même logique que pour le mode dessin
|
|
_handleDrawingHover(event);
|
|
}
|
|
|
|
// Gérer le survol de la souris en mode suppression et édition
|
|
void _handleMouseHover(PointerHoverEvent event) {
|
|
if (_mapMode != MapMode.deleting && _mapMode != MapMode.editing) {
|
|
// Réinitialiser le survol si on n'est pas en mode suppression ou édition
|
|
if (_hoveredSectorId != null) {
|
|
setState(() {
|
|
_hoveredSectorId = null;
|
|
});
|
|
}
|
|
if (_hoveredSectorIdForEdit != null) {
|
|
setState(() {
|
|
_hoveredSectorIdForEdit = null;
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Récupérer la taille du widget MapboxMap
|
|
final RenderBox? renderBox = context.findRenderObject() as RenderBox?;
|
|
if (renderBox == null) return;
|
|
|
|
// Convertir la position globale en position locale relative au widget
|
|
final localPosition = renderBox.globalToLocal(event.position);
|
|
|
|
// Utiliser une méthode plus précise basée sur la projection Web Mercator
|
|
final mapSize = renderBox.size;
|
|
final camera = _mapController.camera;
|
|
|
|
// Calculer le décalage par rapport au centre de la carte en pixels
|
|
final dx = localPosition.dx - (mapSize.width / 2);
|
|
final dy = localPosition.dy - (mapSize.height / 2);
|
|
|
|
// Constantes pour la projection Web Mercator
|
|
const double tileSize = 256.0;
|
|
final scale = math.pow(2, camera.zoom);
|
|
|
|
// Convertir le centre de la carte en pixels Mercator
|
|
final centerX = (camera.center.longitude + 180) / 360 * tileSize * scale;
|
|
final centerLatRad = camera.center.latitude * math.pi / 180;
|
|
final centerY = (1 -
|
|
math.log(math.tan(centerLatRad) + 1 / math.cos(centerLatRad)) /
|
|
math.pi) /
|
|
2 *
|
|
tileSize *
|
|
scale;
|
|
|
|
// Calculer la position de la souris en pixels Mercator
|
|
final mouseX = centerX + dx;
|
|
final mouseY = centerY + dy;
|
|
|
|
// Convertir les pixels Mercator en coordonnées géographiques
|
|
final lng = mouseX / (tileSize * scale) * 360 - 180;
|
|
final n = math.pi - 2 * math.pi * mouseY / (tileSize * scale);
|
|
final lat = 180 / math.pi * math.atan(0.5 * (math.exp(n) - math.exp(-n)));
|
|
|
|
final latLng = LatLng(lat, lng);
|
|
|
|
// Debug: afficher les coordonnées calculées (moins fréquemment)
|
|
if (kDebugMode && DateTime.now().millisecondsSinceEpoch % 500 < 50) {
|
|
debugPrint('Mouse: local($dx, $dy) -> LatLng($lat, $lng)');
|
|
}
|
|
|
|
// Trouver le secteur sous le curseur
|
|
int? newHoveredSectorId;
|
|
for (final sector in _sectors) {
|
|
final points = sector['points'] as List<LatLng>;
|
|
if (_isPointInPolygon(latLng, points)) {
|
|
newHoveredSectorId = sector['id'] as int;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Mettre à jour l'état selon le mode
|
|
if (_mapMode == MapMode.deleting) {
|
|
// Mode suppression
|
|
if (newHoveredSectorId != _hoveredSectorId) {
|
|
setState(() {
|
|
_hoveredSectorId = newHoveredSectorId;
|
|
});
|
|
|
|
if (kDebugMode && newHoveredSectorId != null) {
|
|
final sector =
|
|
_sectors.firstWhere((s) => s['id'] == newHoveredSectorId);
|
|
debugPrint(
|
|
'Now hovering for delete: ${sector['name']} (ID: $newHoveredSectorId)');
|
|
}
|
|
}
|
|
} else if (_mapMode == MapMode.editing) {
|
|
// Mode édition - ne survoler que si aucun secteur n'est sélectionné
|
|
if (_selectedSectorForEdit == null) {
|
|
if (newHoveredSectorId != _hoveredSectorIdForEdit) {
|
|
setState(() {
|
|
_hoveredSectorIdForEdit = newHoveredSectorId;
|
|
});
|
|
|
|
if (kDebugMode && newHoveredSectorId != null) {
|
|
final sector =
|
|
_sectors.firstWhere((s) => s['id'] == newHoveredSectorId);
|
|
debugPrint(
|
|
'Now hovering for edit: ${sector['name']} (ID: $newHoveredSectorId)');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Gérer le clic sur un secteur pour la suppression
|
|
void _handleSectorClick(LatLng position) {
|
|
// Trouver le secteur qui contient le point cliqué
|
|
for (final sector in _sectors) {
|
|
final points = sector['points'] as List<LatLng>;
|
|
if (_isPointInPolygon(position, points)) {
|
|
setState(() {
|
|
_sectorToDeleteId = sector['id'] as int;
|
|
});
|
|
_showDeleteConfirmationDialog();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Gérer le clic sur un secteur pour l'édition
|
|
void _handleSectorClickForEdit(LatLng position) {
|
|
// Trouver le secteur qui contient le point cliqué
|
|
for (final sector in _sectors) {
|
|
final points = sector['points'] as List<LatLng>;
|
|
if (_isPointInPolygon(position, points)) {
|
|
final sectorId = sector['id'] as int;
|
|
final sectorModel =
|
|
Hive.box<SectorModel>(AppKeys.sectorsBoxName).get(sectorId);
|
|
|
|
if (sectorModel != null) {
|
|
// Copier les points
|
|
List<LatLng> magnetizedPoints = List<LatLng>.from(points);
|
|
|
|
// Appliquer le magnétisme automatique à tous les points
|
|
magnetizedPoints =
|
|
_applyAutomaticMagnetism(magnetizedPoints, sectorId);
|
|
|
|
setState(() {
|
|
_selectedSectorForEdit = sectorModel;
|
|
_editingPoints = magnetizedPoints;
|
|
_originalPoints.clear();
|
|
// Sauvegarder les points originaux (non magnétisés) pour pouvoir annuler
|
|
for (int i = 0; i < points.length; i++) {
|
|
_originalPoints[i] = points[i];
|
|
}
|
|
// Réinitialiser le survol quand un secteur est sélectionné
|
|
_hoveredSectorIdForEdit = null;
|
|
});
|
|
|
|
// Vérifier si des points ont été magnétisés
|
|
bool hasMagnetizedPoints = false;
|
|
for (int i = 0; i < points.length; i++) {
|
|
if (points[i] != magnetizedPoints[i]) {
|
|
hasMagnetizedPoints = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasMagnetizedPoints) {
|
|
_showEditInfoDialogWithMagnetism();
|
|
} else {
|
|
_showEditInfoDialog();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Vérifier si un point est dans un polygone (algorithme ray-casting)
|
|
bool _isPointInPolygon(LatLng point, List<LatLng> polygon) {
|
|
int i, j = polygon.length - 1;
|
|
bool oddNodes = false;
|
|
|
|
for (i = 0; i < polygon.length; i++) {
|
|
if ((polygon[i].latitude < point.latitude &&
|
|
polygon[j].latitude >= point.latitude ||
|
|
polygon[j].latitude < point.latitude &&
|
|
polygon[i].latitude >= point.latitude) &&
|
|
(polygon[i].longitude <= point.longitude ||
|
|
polygon[j].longitude <= point.longitude)) {
|
|
if (polygon[i].longitude +
|
|
(point.latitude - polygon[i].latitude) /
|
|
(polygon[j].latitude - polygon[i].latitude) *
|
|
(polygon[j].longitude - polygon[i].longitude) <
|
|
point.longitude) {
|
|
oddNodes = !oddNodes;
|
|
}
|
|
}
|
|
j = i;
|
|
}
|
|
|
|
return oddNodes;
|
|
}
|
|
|
|
// Vérifier si un point est sur le bord d'un polygone (avec tolérance)
|
|
bool _isPointOnPolygonBorder(LatLng point, List<LatLng> polygon,
|
|
{double tolerance = 5.0}) {
|
|
for (int i = 0; i < polygon.length; i++) {
|
|
final start = polygon[i];
|
|
final end = polygon[(i + 1) % polygon.length];
|
|
|
|
// Calculer la distance du point au segment
|
|
final closestPoint = _getClosestPointOnSegment(point, start, end);
|
|
final distance = _calculateDistance(point, closestPoint);
|
|
|
|
// Si le point est proche du segment (dans la tolérance)
|
|
if (distance <= tolerance) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Vérifier si un point est dans un secteur existant (mais pas sur le bord)
|
|
bool _isPointInsideExistingSector(LatLng point) {
|
|
for (final sector in _sectors) {
|
|
final sectorPoints = sector['points'] as List<LatLng>;
|
|
|
|
// Vérifier si le point est dans le polygone
|
|
if (_isPointInPolygon(point, sectorPoints)) {
|
|
// Vérifier s'il n'est PAS sur le bord (avec tolérance pour le magnétisme)
|
|
if (!_isPointOnPolygonBorder(point, sectorPoints,
|
|
tolerance: _snapDistance)) {
|
|
return true; // Le point est vraiment à l'intérieur
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Vérifier si deux segments de droite se croisent
|
|
bool _doSegmentsIntersect(LatLng p1, LatLng p2, LatLng p3, LatLng p4) {
|
|
double x1 = p1.longitude;
|
|
double y1 = p1.latitude;
|
|
double x2 = p2.longitude;
|
|
double y2 = p2.latitude;
|
|
double x3 = p3.longitude;
|
|
double y3 = p3.latitude;
|
|
double x4 = p4.longitude;
|
|
double y4 = p4.latitude;
|
|
|
|
double denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
|
|
|
|
// Les lignes sont parallèles
|
|
if (denom.abs() < 1e-10) {
|
|
return false;
|
|
}
|
|
|
|
double t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
|
|
double u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
|
|
|
|
// Vérifier si l'intersection est sur les deux segments
|
|
return t > 0 && t < 1 && u > 0 && u < 1;
|
|
}
|
|
|
|
// Vérifier si un polygone est valide (pas de croisements)
|
|
bool _isValidPolygon(List<LatLng> points) {
|
|
if (points.length < 3) return false;
|
|
|
|
// Vérifier tous les segments contre tous les autres
|
|
for (int i = 0; i < points.length; i++) {
|
|
for (int j = i + 2; j < points.length; j++) {
|
|
// Ne pas vérifier les segments adjacents
|
|
if ((i == 0 && j == points.length - 1) || j == i + 1) continue;
|
|
|
|
if (_doSegmentsIntersect(points[i], points[(i + 1) % points.length],
|
|
points[j], points[(j + 1) % points.length])) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Vérifier si deux polygones se chevauchent
|
|
bool _doPolygonsOverlap(List<LatLng> polygon1, List<LatLng> polygon2) {
|
|
// Tolérance pour considérer les secteurs comme adjacents et non chevauchants
|
|
const double borderTolerance =
|
|
5.0; // 5 mètres de tolérance pour les bordures
|
|
const double pointTolerance =
|
|
10.0; // 10 mètres de tolérance pour les points intérieurs
|
|
|
|
// Compter les points vraiment à l'intérieur (pas sur les bords)
|
|
int pointsInsidePolygon2 = 0;
|
|
int pointsInsidePolygon1 = 0;
|
|
|
|
// Vérifier les points du polygon1 dans polygon2
|
|
for (final point in polygon1) {
|
|
if (_isPointInPolygon(point, polygon2)) {
|
|
// Vérifier si le point est vraiment à l'intérieur (pas sur le bord)
|
|
if (!_isPointOnPolygonBorder(point, polygon2,
|
|
tolerance: borderTolerance)) {
|
|
// Vérifier la distance au bord le plus proche
|
|
double minDistanceToBorder = double.infinity;
|
|
for (int i = 0; i < polygon2.length; i++) {
|
|
final start = polygon2[i];
|
|
final end = polygon2[(i + 1) % polygon2.length];
|
|
final closestPoint = _getClosestPointOnSegment(point, start, end);
|
|
final distance = _calculateDistance(point, closestPoint);
|
|
if (distance < minDistanceToBorder) {
|
|
minDistanceToBorder = distance;
|
|
}
|
|
}
|
|
|
|
// Si le point est vraiment à l'intérieur (pas trop proche du bord)
|
|
if (minDistanceToBorder > pointTolerance) {
|
|
pointsInsidePolygon2++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Vérifier les points du polygon2 dans polygon1
|
|
for (final point in polygon2) {
|
|
if (_isPointInPolygon(point, polygon1)) {
|
|
if (!_isPointOnPolygonBorder(point, polygon1,
|
|
tolerance: borderTolerance)) {
|
|
// Vérifier la distance au bord le plus proche
|
|
double minDistanceToBorder = double.infinity;
|
|
for (int i = 0; i < polygon1.length; i++) {
|
|
final start = polygon1[i];
|
|
final end = polygon1[(i + 1) % polygon1.length];
|
|
final closestPoint = _getClosestPointOnSegment(point, start, end);
|
|
final distance = _calculateDistance(point, closestPoint);
|
|
if (distance < minDistanceToBorder) {
|
|
minDistanceToBorder = distance;
|
|
}
|
|
}
|
|
|
|
if (minDistanceToBorder > pointTolerance) {
|
|
pointsInsidePolygon1++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Il y a chevauchement seulement si une proportion significative de points est à l'intérieur
|
|
// Augmenter le seuil pour être plus tolérant
|
|
final threshold1 =
|
|
polygon1.length >= 4 ? 3 : 2; // Au moins 3 points sur 4 (ou 2 sur 3)
|
|
final threshold2 =
|
|
polygon2.length >= 6 ? 3 : 2; // Adaptatif selon la taille
|
|
|
|
if (pointsInsidePolygon2 >= threshold1 ||
|
|
pointsInsidePolygon1 >= threshold2) {
|
|
debugPrint('🚨 CHEVAUCHEMENT DÉTECTÉ - Points à l\'intérieur:');
|
|
debugPrint(
|
|
' Points de polygon1 dans polygon2: $pointsInsidePolygon2 (seuil: $threshold1)');
|
|
debugPrint(
|
|
' Points de polygon2 dans polygon1: $pointsInsidePolygon1 (seuil: $threshold2)');
|
|
debugPrint(
|
|
' Polygon1 (nouveau secteur): ${polygon1.map((p) => '${p.latitude.toStringAsFixed(6)},${p.longitude.toStringAsFixed(6)}').join(' ')}');
|
|
debugPrint(
|
|
' Polygon2 (secteur existant): ${polygon2.map((p) => '${p.latitude.toStringAsFixed(6)},${p.longitude.toStringAsFixed(6)}').join(' ')}');
|
|
return true;
|
|
}
|
|
|
|
// Vérifier les intersections de segments qui ne sont pas des adjacences
|
|
int realIntersections = 0;
|
|
const double vertexTolerance =
|
|
5.0; // Tolérance pour les sommets partagés (5m)
|
|
|
|
for (int i = 0; i < polygon1.length; i++) {
|
|
for (int j = 0; j < polygon2.length; j++) {
|
|
final p1Start = polygon1[i];
|
|
final p1End = polygon1[(i + 1) % polygon1.length];
|
|
final p2Start = polygon2[j];
|
|
final p2End = polygon2[(j + 1) % polygon2.length];
|
|
|
|
// D'abord vérifier si les segments partagent un sommet
|
|
final dist1 = _calculateDistance(p1Start, p2Start);
|
|
final dist2 = _calculateDistance(p1Start, p2End);
|
|
final dist3 = _calculateDistance(p1End, p2Start);
|
|
final dist4 = _calculateDistance(p1End, p2End);
|
|
|
|
// Si les segments partagent un sommet, ils ne peuvent pas se croiser vraiment
|
|
if (dist1 < vertexTolerance ||
|
|
dist2 < vertexTolerance ||
|
|
dist3 < vertexTolerance ||
|
|
dist4 < vertexTolerance) {
|
|
// Les segments sont adjacents (partagent un sommet)
|
|
continue;
|
|
}
|
|
|
|
if (_doSegmentsIntersect(p1Start, p1End, p2Start, p2End)) {
|
|
// Vérifier si les segments sont alignés (partagent une partie)
|
|
if (_areSegmentsAligned(p1Start, p1End, p2Start, p2End)) {
|
|
debugPrint(' Intersection ignorée (segments alignés)');
|
|
continue;
|
|
}
|
|
|
|
// C'est une vraie intersection
|
|
realIntersections++;
|
|
debugPrint(' VRAIE INTERSECTION détectée entre:');
|
|
debugPrint(
|
|
' Seg1: ${p1Start.latitude.toStringAsFixed(6)},${p1Start.longitude.toStringAsFixed(6)} -> ${p1End.latitude.toStringAsFixed(6)},${p1End.longitude.toStringAsFixed(6)}');
|
|
debugPrint(
|
|
' Seg2: ${p2Start.latitude.toStringAsFixed(6)},${p2Start.longitude.toStringAsFixed(6)} -> ${p2End.latitude.toStringAsFixed(6)},${p2End.longitude.toStringAsFixed(6)}');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Il faut plusieurs vraies intersections pour considérer un chevauchement
|
|
// Pour être plus tolérant avec les secteurs adjacents complexes
|
|
if (realIntersections >= 3) {
|
|
debugPrint('🚨 CHEVAUCHEMENT DÉTECTÉ - Intersections de segments:');
|
|
debugPrint(' Nombre d\'intersections réelles: $realIntersections');
|
|
debugPrint(
|
|
' Polygon1 (nouveau secteur): ${polygon1.map((p) => '${p.latitude.toStringAsFixed(6)},${p.longitude.toStringAsFixed(6)}').join(' ')}');
|
|
debugPrint(
|
|
' Polygon2 (secteur existant): ${polygon2.map((p) => '${p.latitude.toStringAsFixed(6)},${p.longitude.toStringAsFixed(6)}').join(' ')}');
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Vérifier si deux segments sont alignés (sur la même ligne)
|
|
bool _areSegmentsAligned(LatLng p1, LatLng p2, LatLng p3, LatLng p4) {
|
|
// Calculer la distance de p3 et p4 à la ligne définie par p1-p2
|
|
final d3 = _pointToLineDistance(p3, p1, p2);
|
|
final d4 = _pointToLineDistance(p4, p1, p2);
|
|
|
|
// Calculer aussi la distance de p1 et p2 à la ligne définie par p3-p4
|
|
final d1 = _pointToLineDistance(p1, p3, p4);
|
|
final d2 = _pointToLineDistance(p2, p3, p4);
|
|
|
|
// Tolérance plus large pour l'alignement (10m)
|
|
const double alignmentTolerance = 10.0;
|
|
|
|
// Les segments sont alignés si tous les points sont proches de la ligne opposée
|
|
return (d3 < alignmentTolerance && d4 < alignmentTolerance) ||
|
|
(d1 < alignmentTolerance && d2 < alignmentTolerance);
|
|
}
|
|
|
|
// Vérifier si deux segments sont adjacents (partagent une partie commune)
|
|
|
|
// Calculer la distance d'un point à une ligne (définie par deux points)
|
|
double _pointToLineDistance(LatLng point, LatLng lineStart, LatLng lineEnd) {
|
|
final closestPoint = _getClosestPointOnSegment(point, lineStart, lineEnd);
|
|
return _calculateDistance(point, closestPoint);
|
|
}
|
|
|
|
// Finaliser la création du secteur
|
|
void _finalizeSectorCreation() {
|
|
if (_drawingPoints.length < 3) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Un secteur doit avoir au moins 3 points'),
|
|
backgroundColor: Colors.orange,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Corriger automatiquement les points pour éviter les chevauchements
|
|
debugPrint('📍 CRÉATION DE SECTEUR - Points originaux:');
|
|
debugPrint(
|
|
' ${_drawingPoints.map((p) => '${p.latitude.toStringAsFixed(6)},${p.longitude.toStringAsFixed(6)}').join(' ')}');
|
|
|
|
final correctedPoints = _correctPointsForAdjacency(_drawingPoints);
|
|
|
|
// Vérifier si des points ont été corrigés
|
|
bool pointsWereCorrected = false;
|
|
for (int i = 0; i < _drawingPoints.length; i++) {
|
|
if (_drawingPoints[i] != correctedPoints[i]) {
|
|
pointsWereCorrected = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (pointsWereCorrected) {
|
|
debugPrint('✏️ CORRECTION APPLIQUÉE - Points corrigés:');
|
|
debugPrint(
|
|
' ${correctedPoints.map((p) => '${p.latitude.toStringAsFixed(6)},${p.longitude.toStringAsFixed(6)}').join(' ')}');
|
|
}
|
|
|
|
// Mettre à jour les points avec la version corrigée
|
|
setState(() {
|
|
_drawingPoints = correctedPoints;
|
|
});
|
|
|
|
// Informer l'utilisateur si des corrections ont été appliquées
|
|
if (pointsWereCorrected) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'Les points ont été ajustés automatiquement pour éviter les chevauchements'),
|
|
backgroundColor: Colors.blue,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Vérifier les chevauchements avec les secteurs existants
|
|
bool hasOverlap = false;
|
|
String? overlappingSectorName;
|
|
|
|
for (final sector in _sectors) {
|
|
final sectorPoints = sector['points'] as List<LatLng>;
|
|
if (_doPolygonsOverlap(correctedPoints, sectorPoints)) {
|
|
hasOverlap = true;
|
|
overlappingSectorName = sector['name'] as String;
|
|
debugPrint('❌ CHEVAUCHEMENT AVEC LE SECTEUR: $overlappingSectorName');
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasOverlap) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
'Le nouveau secteur chevauche avec le secteur "$overlappingSectorName"'),
|
|
backgroundColor: Colors.red,
|
|
duration: const Duration(seconds: 3),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Afficher le dialog de création
|
|
_showSectorDialog();
|
|
}
|
|
|
|
// Afficher le dialog d'information pour l'édition
|
|
void _showEditInfoDialog() {
|
|
if (_selectedSectorForEdit == null) return;
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
'Secteur "${_selectedSectorForEdit!.libelle}" sélectionné. Vous pouvez maintenant déplacer les points.'),
|
|
backgroundColor: Colors.blue,
|
|
duration: const Duration(seconds: 3),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Afficher le dialog d'information pour l'édition avec magnétisme
|
|
void _showEditInfoDialogWithMagnetism() {
|
|
if (_selectedSectorForEdit == null) return;
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Secteur "${_selectedSectorForEdit!.libelle}" sélectionné.'),
|
|
const SizedBox(height: 4),
|
|
const Text(
|
|
'Des points ont été automatiquement ajustés aux secteurs adjacents.',
|
|
style: TextStyle(fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
backgroundColor: Colors.orange,
|
|
duration: const Duration(seconds: 4),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Afficher le dialog de confirmation de suppression
|
|
Future<void> _showDeleteConfirmationDialog() async {
|
|
if (_sectorToDeleteId == null) return;
|
|
|
|
final sectorToDelete =
|
|
_sectors.firstWhere((s) => s['id'] == _sectorToDeleteId);
|
|
final sectorName = sectorToDelete['name'] as String;
|
|
|
|
final result = await showDialog<bool>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => AlertDialog(
|
|
title: Row(
|
|
children: [
|
|
Icon(Icons.warning, color: Colors.red),
|
|
const SizedBox(width: 8),
|
|
Text('Supprimer le secteur'),
|
|
],
|
|
),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Êtes-vous sûr de vouloir supprimer le secteur "$sectorName" ?',
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.orange.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.orange.withValues(alpha: 0.3)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.info_outline,
|
|
color: Colors.orange[700], size: 20),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Attention',
|
|
style: TextStyle(
|
|
color: Colors.orange[700],
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'• Tous les passages à finaliser seront supprimés\n'
|
|
'• Les passages sans infos d\'habitant seront supprimés\n'
|
|
'• Les autres passages seront conservés sans secteur',
|
|
style: TextStyle(
|
|
color: Colors.orange[900],
|
|
fontSize: 13,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop(false);
|
|
setState(() {
|
|
_mapMode = MapMode.view;
|
|
_sectorToDeleteId = null;
|
|
});
|
|
},
|
|
child: const Text('Annuler'),
|
|
),
|
|
ElevatedButton.icon(
|
|
onPressed: () => Navigator.of(context).pop(true),
|
|
icon: const Icon(Icons.delete_forever),
|
|
label: const Text('Supprimer'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (result == true) {
|
|
await _deleteSector();
|
|
} else {
|
|
setState(() {
|
|
_mapMode = MapMode.view;
|
|
_sectorToDeleteId = null;
|
|
_hoveredSectorId = null;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Supprimer le secteur
|
|
Future<void> _deleteSector() async {
|
|
if (_sectorToDeleteId == null) return;
|
|
|
|
try {
|
|
// Afficher un indicateur de chargement
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
),
|
|
SizedBox(width: 16),
|
|
Text('Suppression du secteur en cours...'),
|
|
],
|
|
),
|
|
duration: Duration(seconds: 30),
|
|
),
|
|
);
|
|
|
|
final sectorRepository = SectorRepository();
|
|
final result =
|
|
await sectorRepository.deleteSectorFromApi(_sectorToDeleteId!);
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
|
}
|
|
|
|
if (result['status'] == 'success') {
|
|
// Si le secteur supprimé était sélectionné, réinitialiser la sélection
|
|
if (_selectedSectorId == _sectorToDeleteId) {
|
|
setState(() {
|
|
_selectedSectorId = null;
|
|
});
|
|
}
|
|
|
|
// Les ValueListenableBuilder vont automatiquement recharger les secteurs et passages
|
|
|
|
// Message de succès simple
|
|
if (mounted) {
|
|
final deletedSector =
|
|
_sectors.firstWhere((s) => s['id'] == _sectorToDeleteId);
|
|
final deletedSectorName = deletedSector['name'] as String;
|
|
final passagesDeleted = result['passages_deleted'] ?? 0;
|
|
final passagesReassigned = result['passages_reassigned'] ?? 0;
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
'Secteur "$deletedSectorName" supprimé. $passagesDeleted passages supprimés, $passagesReassigned réassignés.'),
|
|
backgroundColor: Colors.green,
|
|
duration: const Duration(seconds: 4),
|
|
),
|
|
);
|
|
}
|
|
} else {
|
|
final errorMessage =
|
|
result['message'] ?? 'Erreur lors de la suppression du secteur';
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(errorMessage),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Erreur: $e'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
} finally {
|
|
setState(() {
|
|
_mapMode = MapMode.view;
|
|
_sectorToDeleteId = null;
|
|
_hoveredSectorId = null;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Afficher le dialog de création/modification de secteur
|
|
Future<void> _showSectorDialog(
|
|
{SectorModel? existingSector, List<List<double>>? coordinates}) async {
|
|
final currentAmicale = CurrentAmicaleService.instance.currentAmicale;
|
|
if (currentAmicale == null) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Aucune amicale sélectionnée'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Sauvegarder le contexte du widget parent avant d'ouvrir le dialog
|
|
final parentContext = context;
|
|
|
|
// Préparer les coordonnées
|
|
final finalCoordinates = coordinates ??
|
|
(existingSector != null
|
|
? existingSector.getCoordinates()
|
|
: _drawingPoints
|
|
.map((point) => [point.latitude, point.longitude])
|
|
.toList());
|
|
|
|
await showDialog(
|
|
context: parentContext,
|
|
barrierDismissible: false,
|
|
builder: (dialogContext) => SectorDialog(
|
|
existingSector: existingSector,
|
|
coordinates: finalCoordinates,
|
|
onSave: (name, color, memberIds) async {
|
|
// Le dialog se ferme automatiquement dans _handleSave()
|
|
// Attendre un peu pour s'assurer que le dialog est fermé
|
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
|
|
try {
|
|
// Afficher un indicateur de chargement
|
|
if (parentContext.mounted) {
|
|
ScaffoldMessenger.of(parentContext).showSnackBar(
|
|
const SnackBar(
|
|
content: Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
),
|
|
SizedBox(width: 16),
|
|
Text('Enregistrement du secteur...'),
|
|
],
|
|
),
|
|
duration: Duration(seconds: 30),
|
|
),
|
|
);
|
|
}
|
|
|
|
final sectorRepository = SectorRepository();
|
|
int passagesCreated = 0;
|
|
Map<String, dynamic> result;
|
|
|
|
if (existingSector == null) {
|
|
// Création d'un nouveau secteur
|
|
// Convertir les coordonnées au format attendu par l'API : "lat/lng#lat/lng#..."
|
|
final sectorString = finalCoordinates
|
|
.map((coord) => '${coord[0]}/${coord[1]}')
|
|
.join('#') +
|
|
'#'; // Ajouter un # final comme dans les exemples
|
|
|
|
final newSector = SectorModel(
|
|
id: 0, // L'API assignera l'ID
|
|
libelle: name,
|
|
color: color,
|
|
sector: sectorString,
|
|
);
|
|
|
|
// Récupérer les informations nécessaires
|
|
final currentUser = CurrentUserService.instance.currentUser;
|
|
|
|
if (currentUser == null || currentUser.fkEntite == null) {
|
|
throw Exception('Utilisateur non connecté ou sans entité');
|
|
}
|
|
|
|
// Récupérer l'opération courante
|
|
final operationRepository = OperationRepository();
|
|
final currentOperation =
|
|
operationRepository.getCurrentOperation();
|
|
|
|
if (currentOperation == null) {
|
|
throw Exception('Aucune opération active trouvée');
|
|
}
|
|
|
|
result = await sectorRepository.createSector(
|
|
newSector,
|
|
users: memberIds,
|
|
fkEntite: currentUser.fkEntite!,
|
|
operationId: currentOperation.id,
|
|
);
|
|
|
|
debugPrint('📋 RÉPONSE API CREATE:');
|
|
debugPrint(' Status: ${result['status']}');
|
|
debugPrint(' Result keys: ${result.keys.toList()}');
|
|
|
|
if (result['status'] != 'success') {
|
|
throw Exception(result['message'] ??
|
|
'Erreur lors de la création du secteur');
|
|
}
|
|
|
|
// Récupérer les statistiques et informations
|
|
passagesCreated = result['passages_created'] ?? 0;
|
|
|
|
// Traiter les passages retournés par l'API
|
|
if (result['passages_sector'] != null) {
|
|
debugPrint(
|
|
'🔄 Traitement de ${(result['passages_sector'] as List).length} passages retournés par l\'API...');
|
|
await _processPassagesFromSectorApi(
|
|
result['passages_sector'] as List<dynamic>);
|
|
debugPrint('✅ Passages traités avec succès');
|
|
}
|
|
|
|
// Les associations utilisateur-secteur sont déjà traitées dans SectorRepository.createSector()
|
|
// Pas besoin de les traiter à nouveau ici
|
|
|
|
// Recharger les secteurs et passages
|
|
_loadSectors();
|
|
_loadPassages();
|
|
|
|
// Centrer la carte sur le nouveau secteur
|
|
if (result.containsKey('sector') && result['sector'] != null) {
|
|
final newSector = result['sector'] as SectorModel;
|
|
// Attendre un peu que les données soient chargées
|
|
Future.delayed(const Duration(milliseconds: 500), () {
|
|
if (mounted) {
|
|
_centerMapOnSpecificSector(newSector.id);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (parentContext.mounted) {
|
|
ScaffoldMessenger.of(parentContext).hideCurrentSnackBar();
|
|
}
|
|
|
|
// Message de succès simple pour la création
|
|
if (mounted && parentContext.mounted) {
|
|
String message = 'Secteur "$name" créé avec succès. ';
|
|
if (passagesCreated > 0) {
|
|
message += '$passagesCreated passages créés.';
|
|
}
|
|
if (result['warning'] != null) {
|
|
message += ' Attention: ${result['warning']}';
|
|
}
|
|
|
|
ScaffoldMessenger.of(parentContext).showSnackBar(
|
|
SnackBar(
|
|
content: Text(message),
|
|
backgroundColor: result['warning'] != null
|
|
? Colors.orange
|
|
: Colors.green,
|
|
duration: const Duration(seconds: 4),
|
|
),
|
|
);
|
|
}
|
|
} else {
|
|
// Modification d'un secteur existant
|
|
final sectorString = finalCoordinates
|
|
.map((coord) => '${coord[0]}/${coord[1]}')
|
|
.join('#') +
|
|
'#';
|
|
|
|
final updatedSector = existingSector.copyWith(
|
|
libelle: name,
|
|
color: color,
|
|
sector: sectorString,
|
|
);
|
|
|
|
result = await sectorRepository.updateSector(updatedSector,
|
|
users: memberIds);
|
|
|
|
if (result['status'] != 'success') {
|
|
throw Exception(result['message'] ??
|
|
'Erreur lors de la modification du secteur');
|
|
}
|
|
|
|
// Traiter les passages retournés par l'API
|
|
if (result['passages_sector'] != null) {
|
|
debugPrint(
|
|
'🔄 Traitement de ${(result['passages_sector'] as List).length} passages retournés par l\'API après modification...');
|
|
await _processPassagesFromSectorApi(
|
|
result['passages_sector'] as List<dynamic>);
|
|
debugPrint('✅ Passages traités avec succès');
|
|
}
|
|
|
|
// Les associations utilisateur-secteur sont déjà traitées dans SectorRepository.updateSector()
|
|
// Pas besoin de les traiter à nouveau ici
|
|
|
|
// Recharger les secteurs et passages
|
|
_loadSectors();
|
|
_loadPassages();
|
|
|
|
if (parentContext.mounted) {
|
|
ScaffoldMessenger.of(parentContext).hideCurrentSnackBar();
|
|
}
|
|
|
|
// Message de succès simple pour la modification
|
|
if (mounted && parentContext.mounted) {
|
|
String message = 'Secteur "$name" modifié avec succès. ';
|
|
final passagesUpdated = result['passages_updated'] ?? 0;
|
|
final passagesCreated = result['passages_created'] ?? 0;
|
|
final passagesOrphaned = result['passages_orphaned'] ?? 0;
|
|
|
|
if (passagesUpdated > 0) {
|
|
message += '$passagesUpdated passages mis à jour. ';
|
|
}
|
|
if (passagesCreated > 0) {
|
|
message += '$passagesCreated nouveaux passages. ';
|
|
}
|
|
if (passagesOrphaned > 0) {
|
|
message += '$passagesOrphaned passages orphelins. ';
|
|
}
|
|
if (result['warning'] != null) {
|
|
message += ' Attention: ${result['warning']}';
|
|
}
|
|
|
|
ScaffoldMessenger.of(parentContext).showSnackBar(
|
|
SnackBar(
|
|
content: Text(message),
|
|
backgroundColor: result['warning'] != null
|
|
? Colors.orange
|
|
: Colors.green,
|
|
duration: const Duration(seconds: 4),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (parentContext.mounted) {
|
|
ScaffoldMessenger.of(parentContext).hideCurrentSnackBar();
|
|
ScaffoldMessenger.of(parentContext).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Erreur: $e'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
} finally {
|
|
// Réinitialiser le mode après la création/modification
|
|
if (mounted) {
|
|
setState(() {
|
|
_mapMode = MapMode.view;
|
|
_drawingPoints.clear();
|
|
_editingPoints.clear();
|
|
});
|
|
}
|
|
}
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// Widget pour les boutons d'action
|
|
Widget _buildActionButton({
|
|
required IconData icon,
|
|
required String tooltip,
|
|
required VoidCallback? onPressed,
|
|
Color color = Colors.blue,
|
|
Color? iconColor,
|
|
}) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.2),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: FloatingActionButton(
|
|
heroTag: tooltip, // Nécessaire pour éviter les conflits de hero tags
|
|
onPressed: onPressed,
|
|
backgroundColor: onPressed != null ? color : Colors.grey,
|
|
tooltip: tooltip,
|
|
mini: true,
|
|
child: Icon(
|
|
icon,
|
|
color: iconColor ?? Colors.white,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Construire le menu contextuel selon le mode
|
|
Widget _buildContextualMenu() {
|
|
return Material(
|
|
elevation: 4,
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (_mapMode == MapMode.drawing) ...[
|
|
// Menu pour le mode dessin
|
|
if (_drawingPoints.isNotEmpty) ...[
|
|
TextButton.icon(
|
|
onPressed: _undoLastPoint,
|
|
icon: const Icon(Icons.undo, size: 20),
|
|
label: const Text('Annuler dernier'),
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: Colors.orange,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
],
|
|
TextButton.icon(
|
|
onPressed: _cancelDrawingMode,
|
|
icon: const Icon(Icons.cancel, size: 20),
|
|
label: const Text('Tout annuler'),
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: Colors.red,
|
|
),
|
|
),
|
|
] else if (_mapMode == MapMode.editing) ...[
|
|
// Menu pour le mode édition
|
|
if (_selectedSectorForEdit != null) ...[
|
|
TextButton.icon(
|
|
onPressed: _saveEditedSector,
|
|
icon: const Icon(Icons.save, size: 20),
|
|
label: const Text('Enregistrer'),
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: Colors.green,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
],
|
|
TextButton.icon(
|
|
onPressed: _cancelEditingMode,
|
|
icon: const Icon(Icons.cancel, size: 20),
|
|
label: const Text('Annuler'),
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: Colors.red,
|
|
),
|
|
),
|
|
] else if (_mapMode == MapMode.deleting) ...[
|
|
// Menu pour le mode suppression
|
|
TextButton.icon(
|
|
onPressed: () {
|
|
setState(() {
|
|
_mapMode = MapMode.view;
|
|
_sectorToDeleteId = null;
|
|
});
|
|
},
|
|
icon: const Icon(Icons.cancel, size: 20),
|
|
label: const Text('Annuler'),
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: Colors.red,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Construire la layer de dessin (polyline temporaire)
|
|
List<Polyline> _buildDrawingLayer() {
|
|
if (_drawingPoints.isEmpty && _editingPoints.isEmpty) return [];
|
|
|
|
final List<Polyline> polylines = [];
|
|
|
|
// Polyline pour le mode dessin
|
|
if (_drawingPoints.isNotEmpty) {
|
|
polylines.add(
|
|
Polyline(
|
|
points: _drawingPoints,
|
|
strokeWidth: 3.0,
|
|
color: Colors.blue.withValues(alpha: 0.8),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Polyline pour le mode édition
|
|
if (_editingPoints.isNotEmpty && _selectedSectorForEdit != null) {
|
|
polylines.add(
|
|
Polyline(
|
|
points: [
|
|
..._editingPoints,
|
|
_editingPoints.first
|
|
], // Fermer le polygone
|
|
strokeWidth: 3.0,
|
|
color: Colors.orange.withValues(alpha: 0.8),
|
|
),
|
|
);
|
|
}
|
|
|
|
return polylines;
|
|
}
|
|
|
|
// Construire les marqueurs pour les points de dessin
|
|
List<Marker> _buildDrawingMarkers() {
|
|
if (_drawingPoints.isEmpty) return [];
|
|
|
|
final List<Marker> markers = [];
|
|
|
|
// Ajouter les points principaux
|
|
for (int i = 0; i < _drawingPoints.length; i++) {
|
|
final point = _drawingPoints[i];
|
|
final isFirst = i == 0;
|
|
|
|
markers.add(
|
|
Marker(
|
|
point: point,
|
|
width: isFirst ? 35.0 : 25.0,
|
|
height: isFirst ? 35.0 : 25.0,
|
|
child: Listener(
|
|
onPointerDown: (event) {
|
|
// Si c'est un clic droit (bouton 2) ou Ctrl+clic gauche
|
|
if (event.buttons == 2 ||
|
|
(event.buttons == 1 &&
|
|
HardwareKeyboard.instance.isControlPressed)) {
|
|
// Supprimer le point
|
|
_removeDrawingPoint(i);
|
|
} else if (event.buttons == 1) {
|
|
// Clic gauche - commencer le drag du point
|
|
// Note: La fermeture du polygone est gérée par _handleMapTap
|
|
setState(() {
|
|
_draggingPointIndex = i;
|
|
_originalDragPosition = point;
|
|
_isDraggingPoint = true;
|
|
});
|
|
}
|
|
},
|
|
onPointerMove: (event) {
|
|
if (_draggingPointIndex == i && _isDraggingPoint) {
|
|
// Convertir les coordonnées de l'écran en coordonnées géographiques
|
|
final RenderBox? renderBox =
|
|
context.findRenderObject() as RenderBox?;
|
|
if (renderBox == null) return;
|
|
|
|
final localPosition = renderBox.globalToLocal(event.position);
|
|
final mapSize = renderBox.size;
|
|
final camera = _mapController.camera;
|
|
|
|
// Calculer le décalage par rapport au centre de la carte en pixels
|
|
final dx = localPosition.dx - (mapSize.width / 2);
|
|
final dy = localPosition.dy - (mapSize.height / 2);
|
|
|
|
// Constantes pour la projection Web Mercator
|
|
const double tileSize = 256.0;
|
|
final scale = math.pow(2, camera.zoom);
|
|
|
|
// Convertir le centre de la carte en pixels Mercator
|
|
final centerX =
|
|
(camera.center.longitude + 180) / 360 * tileSize * scale;
|
|
final centerY = (1 -
|
|
math.log(math.tan(
|
|
camera.center.latitude * math.pi / 180) +
|
|
1 /
|
|
math.cos(camera.center.latitude *
|
|
math.pi /
|
|
180)) /
|
|
math.pi) /
|
|
2 *
|
|
tileSize *
|
|
scale;
|
|
|
|
// Calculer la nouvelle position en pixels Mercator
|
|
final newX = centerX + dx;
|
|
final newY = centerY + dy;
|
|
|
|
// Convertir en coordonnées géographiques
|
|
final lng = newX / tileSize / scale * 360 - 180;
|
|
final n = math.pi - 2 * math.pi * newY / tileSize / scale;
|
|
final lat = 180 /
|
|
math.pi *
|
|
math.atan(0.5 * (math.exp(n) - math.exp(-n)));
|
|
|
|
final newPosition = LatLng(lat, lng);
|
|
final snapPoint = _detectSnapPoint(newPosition);
|
|
final finalPosition = snapPoint ?? newPosition;
|
|
|
|
setState(() {
|
|
_drawingPoints[i] = finalPosition;
|
|
_snapPoint = snapPoint;
|
|
});
|
|
}
|
|
},
|
|
onPointerUp: (event) {
|
|
if (_draggingPointIndex == i) {
|
|
_handleDragEnd(i);
|
|
}
|
|
},
|
|
child: MouseRegion(
|
|
cursor: _draggingPointIndex == i
|
|
? SystemMouseCursors.grabbing
|
|
: SystemMouseCursors.grab,
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
decoration: BoxDecoration(
|
|
color: _draggingPointIndex == i
|
|
? Colors.yellow
|
|
: (isFirst ? Colors.green : Colors.blue),
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color:
|
|
_draggingPointIndex == i ? Colors.orange : Colors.white,
|
|
width: _draggingPointIndex == i ? 3.0 : 2.0,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.3),
|
|
blurRadius: _draggingPointIndex == i ? 6 : 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: isFirst
|
|
? const Center(
|
|
child: Text(
|
|
'1',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
)
|
|
: null,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Ajouter les midpoints si on a au moins 2 points
|
|
if (_drawingPoints.length >= 2) {
|
|
for (int i = 0; i < _drawingPoints.length; i++) {
|
|
final start = _drawingPoints[i];
|
|
final end = _drawingPoints[(i + 1) % _drawingPoints.length];
|
|
|
|
// Ne pas ajouter de midpoint entre le dernier et le premier point si le polygone n'est pas fermé
|
|
if (i == _drawingPoints.length - 1 && _drawingPoints.length < 3) {
|
|
continue;
|
|
}
|
|
|
|
// Calculer le point milieu
|
|
final midpoint = LatLng(
|
|
(start.latitude + end.latitude) / 2,
|
|
(start.longitude + end.longitude) / 2,
|
|
);
|
|
|
|
markers.add(Marker(
|
|
point: midpoint,
|
|
width: 15.0,
|
|
height: 15.0,
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
onEnter: (_) {
|
|
setState(() {
|
|
_hoveredMidpointIndex = i;
|
|
});
|
|
},
|
|
onExit: (_) {
|
|
setState(() {
|
|
_hoveredMidpointIndex = null;
|
|
});
|
|
},
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
// Insérer un nouveau point au milieu
|
|
setState(() {
|
|
_drawingPoints.insert(i + 1, midpoint);
|
|
_hoveredMidpointIndex = null;
|
|
});
|
|
},
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
decoration: BoxDecoration(
|
|
color: _hoveredMidpointIndex == i
|
|
? Colors.blue.withValues(alpha: 0.8)
|
|
: Colors.grey.withValues(alpha: 0.5),
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color:
|
|
_hoveredMidpointIndex == i ? Colors.blue : Colors.grey,
|
|
width: 2.0,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
));
|
|
}
|
|
}
|
|
|
|
return markers;
|
|
}
|
|
|
|
// Gérer le drag d'un point
|
|
|
|
// Gérer la fin du drag
|
|
void _handleDragEnd(int index) {
|
|
// Vérifier si la position finale est valide
|
|
if (!_isValidPolygon(_drawingPoints)) {
|
|
// Annuler le déplacement
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Le déplacement créerait une intersection'),
|
|
backgroundColor: Colors.red,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
setState(() {
|
|
if (_originalDragPosition != null) {
|
|
_drawingPoints[index] = _originalDragPosition!;
|
|
}
|
|
_draggingPointIndex = null;
|
|
_originalDragPosition = null;
|
|
_isDraggingPoint = false;
|
|
});
|
|
} else {
|
|
setState(() {
|
|
_draggingPointIndex = null;
|
|
_originalDragPosition = null;
|
|
_snapPoint = null;
|
|
});
|
|
|
|
// Réactiver le drag de la carte avec un petit délai pour éviter les conflits
|
|
Future.delayed(const Duration(milliseconds: 100), () {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isDraggingPoint = false;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Supprimer un point d'édition
|
|
void _removeEditingPoint(int index) {
|
|
if (_editingPoints.length <= 3) {
|
|
// Ne pas permettre de supprimer si on a 3 points ou moins
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Un secteur doit avoir au moins 3 points'),
|
|
backgroundColor: Colors.orange,
|
|
duration: Duration(seconds: 1),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_editingPoints.removeAt(index);
|
|
// Si on supprime le point sélectionné, désélectionner
|
|
if (_draggingPointIndex == index) {
|
|
_draggingPointIndex = null;
|
|
} else if (_draggingPointIndex != null && _draggingPointIndex! > index) {
|
|
// Ajuster l'index si nécessaire
|
|
_draggingPointIndex = _draggingPointIndex! - 1;
|
|
}
|
|
});
|
|
|
|
// Afficher un message de confirmation
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Point supprimé'),
|
|
backgroundColor: Colors.green,
|
|
duration: Duration(seconds: 1),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Gérer le drag d'un point d'édition
|
|
|
|
// Gérer la fin du drag d'un point d'édition
|
|
void _handleEditingDragEnd(int index) {
|
|
// Vérifier si la position finale est valide
|
|
if (!_isValidPolygon(_editingPoints)) {
|
|
// Annuler le déplacement
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Le déplacement créerait une intersection'),
|
|
backgroundColor: Colors.red,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
setState(() {
|
|
if (_originalDragPosition != null) {
|
|
_editingPoints[index] = _originalDragPosition!;
|
|
}
|
|
_draggingPointIndex = null;
|
|
_originalDragPosition = null;
|
|
_isDraggingPoint = false;
|
|
});
|
|
} else {
|
|
setState(() {
|
|
_draggingPointIndex = null;
|
|
_originalDragPosition = null;
|
|
_snapPoint = null;
|
|
});
|
|
|
|
// Réactiver le drag de la carte avec un petit délai pour éviter les conflits
|
|
Future.delayed(const Duration(milliseconds: 100), () {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isDraggingPoint = false;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Construire les marqueurs pour le feedback visuel du magnétisme
|
|
List<Marker> _buildSnapMarkers() {
|
|
if (_snapPoint == null ||
|
|
(_mapMode != MapMode.drawing && _mapMode != MapMode.editing)) return [];
|
|
|
|
return [
|
|
Marker(
|
|
point: _snapPoint!,
|
|
width: 20.0,
|
|
height: 20.0,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.orange.withValues(alpha: 0.5),
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: Colors.orange,
|
|
width: 2.0,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.orange.withValues(alpha: 0.5),
|
|
blurRadius: 8,
|
|
spreadRadius: 2,
|
|
),
|
|
],
|
|
),
|
|
child: const Center(
|
|
child: Icon(
|
|
Icons.adjust,
|
|
size: 12,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
];
|
|
}
|
|
|
|
// Construire les marqueurs pour l'édition
|
|
List<Marker> _buildEditingMarkers() {
|
|
if (_editingPoints.isEmpty || _selectedSectorForEdit == null) return [];
|
|
|
|
final List<Marker> markers = [];
|
|
|
|
// Ajouter les points principaux
|
|
for (int i = 0; i < _editingPoints.length; i++) {
|
|
final point = _editingPoints[i];
|
|
final bool isDragging = _draggingPointIndex == i;
|
|
final bool isHovered = _hoveredPointIndex == i;
|
|
|
|
markers.add(Marker(
|
|
point: point,
|
|
width: 50.0, // Zone de détection plus large
|
|
height: 50.0,
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
// Zone de détection invisible mais interactive
|
|
Listener(
|
|
onPointerDown: (event) {
|
|
// Si c'est un clic droit (bouton 2) ou Ctrl+clic gauche
|
|
if (event.buttons == 2 ||
|
|
(event.buttons == 1 &&
|
|
HardwareKeyboard.instance.isControlPressed)) {
|
|
// Supprimer le point
|
|
_removeEditingPoint(i);
|
|
} else if (event.buttons == 1) {
|
|
// Clic gauche : commencer le drag immédiatement
|
|
setState(() {
|
|
_draggingPointIndex = i;
|
|
_originalDragPosition = point;
|
|
_isDraggingPoint = true;
|
|
});
|
|
}
|
|
},
|
|
onPointerMove: (event) {
|
|
if (_draggingPointIndex == i && _isDraggingPoint) {
|
|
// Convertir les coordonnées de l'écran en coordonnées géographiques
|
|
final RenderBox? renderBox =
|
|
context.findRenderObject() as RenderBox?;
|
|
if (renderBox == null) return;
|
|
|
|
final localPosition = renderBox.globalToLocal(event.position);
|
|
final mapSize = renderBox.size;
|
|
final camera = _mapController.camera;
|
|
|
|
// Calculer le décalage par rapport au centre de la carte en pixels
|
|
final dx = localPosition.dx - (mapSize.width / 2);
|
|
final dy = localPosition.dy - (mapSize.height / 2);
|
|
|
|
// Constantes pour la projection Web Mercator
|
|
const double tileSize = 256.0;
|
|
final scale = math.pow(2, camera.zoom);
|
|
|
|
// Convertir le centre de la carte en pixels Mercator
|
|
final centerX =
|
|
(camera.center.longitude + 180) / 360 * tileSize * scale;
|
|
final centerY = (1 -
|
|
math.log(math.tan(
|
|
camera.center.latitude * math.pi / 180) +
|
|
1 /
|
|
math.cos(camera.center.latitude *
|
|
math.pi /
|
|
180)) /
|
|
math.pi) /
|
|
2 *
|
|
tileSize *
|
|
scale;
|
|
|
|
// Calculer la nouvelle position en pixels Mercator
|
|
final newX = centerX + dx;
|
|
final newY = centerY + dy;
|
|
|
|
// Convertir en coordonnées géographiques
|
|
final lng = newX / tileSize / scale * 360 - 180;
|
|
final n = math.pi - 2 * math.pi * newY / tileSize / scale;
|
|
final lat = 180 /
|
|
math.pi *
|
|
math.atan(0.5 * (math.exp(n) - math.exp(-n)));
|
|
|
|
final newPosition = LatLng(lat, lng);
|
|
final snapPoint = _detectSnapPoint(newPosition);
|
|
final finalPosition = snapPoint ?? newPosition;
|
|
|
|
setState(() {
|
|
_editingPoints[i] = finalPosition;
|
|
_snapPoint = snapPoint;
|
|
});
|
|
}
|
|
},
|
|
onPointerUp: (event) {
|
|
if (_draggingPointIndex == i) {
|
|
_handleEditingDragEnd(i);
|
|
}
|
|
},
|
|
child: MouseRegion(
|
|
cursor: isDragging
|
|
? SystemMouseCursors.grabbing
|
|
: SystemMouseCursors.grab,
|
|
onEnter: (_) {
|
|
setState(() {
|
|
_hoveredPointIndex = i;
|
|
});
|
|
},
|
|
onExit: (_) {
|
|
setState(() {
|
|
_hoveredPointIndex = null;
|
|
});
|
|
},
|
|
child: Container(
|
|
width: 50,
|
|
height: 50,
|
|
color: Colors.transparent,
|
|
),
|
|
),
|
|
),
|
|
// Point visible au centre avec animation
|
|
IgnorePointer(
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
width: isDragging ? 30 : (isHovered ? 28 : 25),
|
|
height: isDragging ? 30 : (isHovered ? 28 : 25),
|
|
decoration: BoxDecoration(
|
|
color: isDragging ? Colors.yellow : Colors.orange,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: isDragging ? Colors.orange : Colors.white,
|
|
width: isDragging ? 3.0 : 2.0,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.3),
|
|
blurRadius: isDragging ? 8 : (isHovered ? 6 : 4),
|
|
offset: const Offset(0, 2),
|
|
),
|
|
if (isHovered && !isDragging)
|
|
BoxShadow(
|
|
color: Colors.orange.withValues(alpha: 0.3),
|
|
blurRadius: 15,
|
|
spreadRadius: 2,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
));
|
|
}
|
|
|
|
// Ajouter les midpoints si on a au moins 2 points
|
|
if (_editingPoints.length >= 2) {
|
|
for (int i = 0; i < _editingPoints.length; i++) {
|
|
final start = _editingPoints[i];
|
|
final end = _editingPoints[(i + 1) % _editingPoints.length];
|
|
|
|
// Calculer le point milieu
|
|
final midpoint = LatLng(
|
|
(start.latitude + end.latitude) / 2,
|
|
(start.longitude + end.longitude) / 2,
|
|
);
|
|
|
|
markers.add(Marker(
|
|
point: midpoint,
|
|
width: 15.0,
|
|
height: 15.0,
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
onEnter: (_) {
|
|
setState(() {
|
|
_hoveredMidpointIndex = i;
|
|
});
|
|
},
|
|
onExit: (_) {
|
|
setState(() {
|
|
_hoveredMidpointIndex = null;
|
|
});
|
|
},
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
// Détecter le magnétisme pour le nouveau point
|
|
final snapPoint = _detectSnapPoint(midpoint);
|
|
final finalPosition = snapPoint ?? midpoint;
|
|
|
|
// Insérer un nouveau point au milieu avec magnétisme
|
|
setState(() {
|
|
_editingPoints.insert(i + 1, finalPosition);
|
|
_hoveredMidpointIndex = null;
|
|
_snapPoint = null;
|
|
});
|
|
},
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
decoration: BoxDecoration(
|
|
color: _hoveredMidpointIndex == i
|
|
? Colors.orange.withValues(alpha: 0.8)
|
|
: Colors.grey.withValues(alpha: 0.5),
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: _hoveredMidpointIndex == i
|
|
? Colors.orange
|
|
: Colors.grey,
|
|
width: 2.0,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
));
|
|
}
|
|
}
|
|
|
|
return markers;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ValueListenableBuilder<Box<SectorModel>>(
|
|
valueListenable:
|
|
Hive.box<SectorModel>(AppKeys.sectorsBoxName).listenable(),
|
|
builder: (context, sectorsBox, child) {
|
|
return ValueListenableBuilder<Box<PassageModel>>(
|
|
valueListenable:
|
|
Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
|
|
builder: (context, passagesBox, child) {
|
|
// Charger les données directement depuis les boxes sans setState
|
|
_loadSectorsFromBox(sectorsBox);
|
|
_loadPassagesFromBox(passagesBox);
|
|
|
|
return Stack(
|
|
children: [
|
|
// Carte MapBox avec gestion du tap et du survol
|
|
Positioned.fill(
|
|
child: MouseRegion(
|
|
cursor: (_mapMode == MapMode.deleting &&
|
|
_hoveredSectorId != null) ||
|
|
(_mapMode == MapMode.editing &&
|
|
_hoveredSectorIdForEdit != null &&
|
|
_selectedSectorForEdit == null)
|
|
? SystemMouseCursors.click
|
|
: SystemMouseCursors.basic,
|
|
onHover: (event) {
|
|
// Gérer le survol
|
|
if (_mapMode == MapMode.deleting) {
|
|
_handleMouseHover(event);
|
|
} else if (_mapMode == MapMode.editing) {
|
|
// En mode édition, gérer à la fois le survol des secteurs et le magnétisme
|
|
_handleMouseHover(event);
|
|
if (_editingPoints.isNotEmpty) {
|
|
// Détecter le magnétisme en temps réel pendant l'édition
|
|
_handleEditingHover(event);
|
|
}
|
|
} else if (_mapMode == MapMode.drawing &&
|
|
_drawingPoints.isNotEmpty) {
|
|
// Détecter le magnétisme en temps réel pendant le dessin
|
|
_handleDrawingHover(event);
|
|
}
|
|
},
|
|
onExit: (event) {
|
|
// Réinitialiser le survol quand la souris quitte la carte
|
|
if (_hoveredSectorId != null ||
|
|
_hoveredSectorIdForEdit != null) {
|
|
setState(() {
|
|
_hoveredSectorId = null;
|
|
_hoveredSectorIdForEdit = null;
|
|
});
|
|
}
|
|
},
|
|
child: MapboxMap(
|
|
initialPosition: _currentPosition,
|
|
initialZoom: _currentZoom,
|
|
mapController: _mapController,
|
|
disableDrag: _isDraggingPoint,
|
|
// Utiliser OpenStreetMap temporairement sur mobile si Mapbox échoue
|
|
useOpenStreetMap: !kIsWeb, // true sur mobile, false sur web
|
|
labelMarkers: _buildSectorLabels(),
|
|
markers: [
|
|
..._buildMarkers(),
|
|
..._buildDrawingMarkers(),
|
|
..._buildEditingMarkers(),
|
|
..._buildSnapMarkers(),
|
|
],
|
|
polygons: _buildPolygons(),
|
|
polylines: _buildDrawingLayer(),
|
|
showControls: true,
|
|
onMapEvent: (event) {
|
|
if (event is MapEventMove) {
|
|
setState(() {
|
|
_currentPosition = event.camera.center;
|
|
_currentZoom = event.camera.zoom;
|
|
});
|
|
_saveSettings();
|
|
// Mettre à jour le survol après un mouvement de carte
|
|
if (_mapMode == MapMode.deleting && kIsWeb) {
|
|
// On doit recalculer car la carte a bougé
|
|
// Note: On ne peut pas obtenir la position de la souris ici,
|
|
// elle sera mise à jour au prochain mouvement de souris
|
|
}
|
|
} else if (event is MapEventTap &&
|
|
(_mapMode == MapMode.drawing ||
|
|
_mapMode == MapMode.deleting ||
|
|
_mapMode == MapMode.editing)) {
|
|
_handleMapTap(event.tapPosition);
|
|
}
|
|
},
|
|
),
|
|
)),
|
|
|
|
// Boutons d'action en haut à droite (Web uniquement)
|
|
if (kIsWeb)
|
|
Positioned(
|
|
right: 16,
|
|
top: 16,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
// Bouton Créer
|
|
_buildActionButton(
|
|
icon: Icons.pentagon_outlined,
|
|
tooltip: 'Créer un secteur',
|
|
color: _mapMode == MapMode.drawing
|
|
? Colors.green
|
|
: Colors.blue,
|
|
onPressed: _mapMode == MapMode.view
|
|
? _startDrawingMode
|
|
: null,
|
|
),
|
|
const SizedBox(height: 8),
|
|
// Bouton Modifier
|
|
_buildActionButton(
|
|
icon: Icons.polyline_outlined,
|
|
tooltip: 'Modifier un secteur',
|
|
color: _mapMode == MapMode.editing
|
|
? Colors.orange
|
|
: Colors.blue,
|
|
onPressed: _mapMode == MapMode.view
|
|
? _startEditingMode
|
|
: null,
|
|
),
|
|
const SizedBox(height: 8),
|
|
// Bouton Supprimer
|
|
_buildActionButton(
|
|
icon: Icons.delete_forever,
|
|
tooltip: 'Supprimer un secteur',
|
|
color: _mapMode == MapMode.deleting
|
|
? Colors.red
|
|
: Colors.white,
|
|
iconColor: _mapMode == MapMode.deleting
|
|
? Colors.white
|
|
: Colors.red,
|
|
onPressed: _mapMode == MapMode.view
|
|
? _startDeletingMode
|
|
: null,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Menu contextuel (apparaît selon le mode) - Web uniquement
|
|
if (kIsWeb && _mapMode != MapMode.view)
|
|
Positioned(
|
|
right: 80,
|
|
top: 16,
|
|
child: _buildContextualMenu(),
|
|
),
|
|
|
|
// Bouton Ma position en bas à droite
|
|
Positioned(
|
|
right: 16,
|
|
bottom: 16,
|
|
child: _buildActionButton(
|
|
icon: Icons.my_location,
|
|
tooltip: 'Ma position',
|
|
onPressed: () {
|
|
_getUserLocation();
|
|
},
|
|
),
|
|
),
|
|
|
|
// Combobox de sélection de secteurs
|
|
Positioned(
|
|
left: 16,
|
|
top: 16,
|
|
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();
|
|
}
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Carte d'aide pour le mode création de secteur - Web uniquement
|
|
if (kIsWeb && _mapMode == MapMode.drawing)
|
|
Positioned(
|
|
left: 16,
|
|
bottom: 16,
|
|
child: _buildHelpCard(),
|
|
),
|
|
|
|
// Carte d'aide pour le mode suppression de secteur - Web uniquement
|
|
if (kIsWeb && _mapMode == MapMode.deleting)
|
|
Positioned(
|
|
left: 16,
|
|
bottom: 16,
|
|
child: _buildDeleteHelpCard(),
|
|
),
|
|
|
|
// Carte d'aide pour le mode édition de secteur - Web uniquement
|
|
if (kIsWeb && _mapMode == MapMode.editing)
|
|
Positioned(
|
|
left: 16,
|
|
bottom: 16,
|
|
child: _buildEditHelpCard(),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
// Méthode pour traiter les passages retournés par l'API sans effacer tous les passages existants
|
|
Future<void> _processPassagesFromSectorApi(List<dynamic> passagesData) async {
|
|
try {
|
|
if (passagesData.isEmpty) {
|
|
debugPrint('Aucun passage à traiter');
|
|
return;
|
|
}
|
|
|
|
final passageRepository = PassageRepository();
|
|
final List<PassageModel> passagesToSave = [];
|
|
|
|
for (final passageData in passagesData) {
|
|
try {
|
|
final passage = PassageModel.fromJson(
|
|
Map<String, dynamic>.from(passageData as Map));
|
|
passagesToSave.add(passage);
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du traitement d\'un passage: $e');
|
|
// Continuer avec les autres passages
|
|
}
|
|
}
|
|
|
|
if (passagesToSave.isNotEmpty) {
|
|
await passageRepository.savePassages(passagesToSave);
|
|
debugPrint('${passagesToSave.length} passages sauvegardés dans Hive');
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du traitement des passages: $e');
|
|
}
|
|
}
|
|
}
|