import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:geolocator/geolocator.dart'; import 'dart:convert'; import 'package:http/http.dart' as http; import 'dart:ui' as ui; import 'package:flutter_dotenv/flutter_dotenv.dart'; class MapContent extends StatefulWidget { final String? initialSearchQuery; const MapContent({super.key, this.initialSearchQuery}); @override State createState() => _MapContentState(); } class _MapContentState extends State { GoogleMapController? _mapController; LatLng _initialPosition = const LatLng(48.8566, 2.3522); final TextEditingController _searchController = TextEditingController(); bool _isLoadingLocation = false; bool _isSearching = false; Position? _currentPosition; final Set _markers = {}; final Set _circles = {}; List _suggestions = []; static final String _apiKey = dotenv.env['GOOGLE_MAPS_API_KEY'] ?? ''; @override void initState() { super.initState(); // Si une recherche initiale est fournie, la pré-remplir et lancer la recherche if (widget.initialSearchQuery != null && widget.initialSearchQuery!.isNotEmpty) { _searchController.text = widget.initialSearchQuery!; // Lancer la recherche automatiquement après un court délai pour laisser l'interface se charger Future.delayed(const Duration(milliseconds: 500), () { _performInitialSearch(widget.initialSearchQuery!); }); } else { // Sinon, obtenir la position actuelle de l'utilisateur _getCurrentLocation(); } } @override void dispose() { _searchController.dispose(); _mapController?.dispose(); super.dispose(); } // Nouvelle méthode pour effectuer la recherche initiale Future _performInitialSearch(String query) async { if (query.isEmpty) return; setState(() { _isSearching = true; }); try { final url = Uri.parse( 'https://maps.googleapis.com/maps/api/place/autocomplete/json' '?input=${Uri.encodeComponent(query)}' '&key=$_apiKey' '&language=fr', ); final response = await http.get(url); if (!mounted) return; if (response.statusCode == 200) { final data = json.decode(response.body); if (data['status'] == 'OK') { final predictions = data['predictions'] as List; if (predictions.isNotEmpty) { // Prendre automatiquement la première suggestion final firstPrediction = predictions.first; final suggestion = PlaceSuggestion( placeId: firstPrediction['place_id'], description: firstPrediction['description'], ); // Effectuer la sélection automatique await _selectPlaceForInitialSearch(suggestion); } else { setState(() { _isSearching = false; }); } } else { setState(() { _isSearching = false; }); } } } catch (e) { _showError('Erreur lors de la recherche initiale: $e'); setState(() { _isSearching = false; }); } } // Version modifiée de _selectPlace pour la recherche initiale Future _selectPlaceForInitialSearch(PlaceSuggestion suggestion) async { try { final url = Uri.parse( 'https://maps.googleapis.com/maps/api/place/details/json' '?place_id=${suggestion.placeId}' '&key=$_apiKey' '&language=fr', ); final response = await http.get(url); if (!mounted) return; if (response.statusCode == 200) { final data = json.decode(response.body); if (data['status'] == 'OK') { final location = data['result']['geometry']['location']; final lat = location['lat']; final lng = location['lng']; final name = data['result']['name']; final newPosition = LatLng(lat, lng); // Ajouter un marqueur pour le lieu recherché setState(() { // Garder le marqueur de position utilisateur s'il existe _markers.removeWhere((m) => m.markerId.value != 'user_location'); // Ajouter le nouveau marqueur de lieu _markers.add( Marker( markerId: MarkerId(suggestion.placeId), position: newPosition, infoWindow: InfoWindow(title: name), ), ); _isSearching = false; }); // Animer la caméra vers le lieu trouvé _mapController?.animateCamera( CameraUpdate.newLatLngZoom(newPosition, 15), ); } } } catch (e) { _showError('Erreur lors de la sélection du lieu initial: $e'); setState(() { _isSearching = false; }); } } Future _getCurrentLocation() async { setState(() { _isLoadingLocation = true; }); try { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { _showError('Veuillez activer les services de localisation'); setState(() { _isLoadingLocation = false; }); return; } LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { _showError('Permission de localisation refusée'); setState(() { _isLoadingLocation = false; }); return; } } if (permission == LocationPermission.deniedForever) { _showError('Permission de localisation refusée définitivement'); setState(() { _isLoadingLocation = false; }); return; } Position position = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, ), ); final newPosition = LatLng(position.latitude, position.longitude); setState(() { _currentPosition = position; _initialPosition = newPosition; _isLoadingLocation = false; }); // Créer l'icône personnalisée et ajouter le marqueur await _addUserLocationMarker(newPosition); if (_mapController != null) { _mapController!.animateCamera( CameraUpdate.newLatLngZoom(_initialPosition, 15), ); } } catch (e) { _showError('Erreur lors de la récupération de la position: $e'); setState(() { _isLoadingLocation = false; }); } } // Créer une icône personnalisée à partir de l'icône Material Future _createCustomMarkerIcon() async { final pictureRecorder = ui.PictureRecorder(); final canvas = Canvas(pictureRecorder); const size = 120.0; // Dessiner l'icône person_pin_circle en bleu final iconPainter = TextPainter(textDirection: TextDirection.ltr); iconPainter.text = TextSpan( text: String.fromCharCode(Icons.person_pin_circle.codePoint), style: TextStyle( fontSize: 100, fontFamily: Icons.person_pin_circle.fontFamily, color: Colors.blue[700], ), ); iconPainter.layout(); iconPainter.paint(canvas, Offset((size - iconPainter.width) / 2, 0)); final picture = pictureRecorder.endRecording(); final image = await picture.toImage(size.toInt(), size.toInt()); final bytes = await image.toByteData(format: ui.ImageByteFormat.png); return BitmapDescriptor.bytes(bytes!.buffer.asUint8List()); } // Ajouter le marqueur avec l'icône personnalisée Future _addUserLocationMarker(LatLng position) async { _markers.clear(); _circles.clear(); // Ajouter un cercle de précision _circles.add( Circle( circleId: const CircleId('user_location_accuracy'), center: position, radius: _currentPosition?.accuracy ?? 50, fillColor: Colors.blue.withValues(alpha: 0.08), strokeColor: Colors.blue.withValues(alpha: 0.3), strokeWidth: 1, ), ); // Créer l'icône personnalisée final icon = await _createCustomMarkerIcon(); // Ajouter le marqueur avec l'icône setState(() { _markers.add( Marker( markerId: const MarkerId('user_location'), position: position, icon: icon, anchor: const Offset( 0.5, 0.85, ), // Ancrer au bas de l'icône (le point du pin) infoWindow: InfoWindow( title: 'Ma position', snippet: 'Lat: ${position.latitude.toStringAsFixed(4)}, Lng: ${position.longitude.toStringAsFixed(4)}', ), ), ); }); } Future _searchPlaces(String query) async { if (query.isEmpty) { setState(() { _suggestions = []; }); return; } setState(() { _isSearching = true; }); try { final url = Uri.parse( 'https://maps.googleapis.com/maps/api/place/autocomplete/json' '?input=${Uri.encodeComponent(query)}' '&key=$_apiKey' '&language=fr', ); final response = await http.get(url); if (!mounted) return; if (response.statusCode == 200) { final data = json.decode(response.body); if (data['status'] == 'OK') { final predictions = data['predictions'] as List; setState(() { _suggestions = predictions .map( (p) => PlaceSuggestion( placeId: p['place_id'], description: p['description'], ), ) .toList(); _isSearching = false; }); } else { setState(() { _suggestions = []; _isSearching = false; }); } } } catch (e) { _showError('Erreur lors de la recherche de lieux: $e'); setState(() { _isSearching = false; }); } } Future _selectPlace(PlaceSuggestion suggestion) async { setState(() { _isSearching = true; _suggestions = []; _searchController.text = suggestion.description; }); try { final url = Uri.parse( 'https://maps.googleapis.com/maps/api/place/details/json' '?place_id=${suggestion.placeId}' '&key=$_apiKey' '&language=fr', ); final response = await http.get(url); if (!mounted) return; if (response.statusCode == 200) { final data = json.decode(response.body); if (data['status'] == 'OK') { final location = data['result']['geometry']['location']; final lat = location['lat']; final lng = location['lng']; final name = data['result']['name']; final newPosition = LatLng(lat, lng); // Ajouter un marqueur pour le lieu recherché (ne pas supprimer le marqueur de position) setState(() { // Garder le marqueur de position utilisateur _markers.removeWhere((m) => m.markerId.value != 'user_location'); // Ajouter le nouveau marqueur de lieu _markers.add( Marker( markerId: MarkerId(suggestion.placeId), position: newPosition, infoWindow: InfoWindow(title: name), ), ); _isSearching = false; }); _mapController?.animateCamera( CameraUpdate.newLatLngZoom(newPosition, 15), ); if (mounted) { FocusScope.of(context).unfocus(); } } } } catch (e) { _showError('Erreur lors de la sélection du lieu: $e'); setState(() { _isSearching = false; }); } } void _showError(String message) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: Colors.red, behavior: SnackBarBehavior.floating, ), ); } } @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Stack( children: [ // Google Maps en arrière-plan Positioned.fill( child: Padding( padding: const EdgeInsets.all(12.0), child: ClipRRect( borderRadius: BorderRadius.circular(16), child: GoogleMap( initialCameraPosition: CameraPosition( target: _initialPosition, zoom: 14, ), onMapCreated: (GoogleMapController controller) { _mapController = controller; }, markers: _markers, circles: _circles, myLocationEnabled: false, myLocationButtonEnabled: false, zoomControlsEnabled: false, mapType: MapType.normal, compassEnabled: true, rotateGesturesEnabled: true, scrollGesturesEnabled: true, tiltGesturesEnabled: true, zoomGesturesEnabled: true, onTap: (_) { setState(() { _suggestions = []; }); FocusScope.of(context).unfocus(); }, ), ), ), ), // Indicateur de chargement if (_isLoadingLocation) Positioned.fill( child: Container( color: Colors.black.withValues(alpha: 0.3), child: Center( child: Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 10, spreadRadius: 2, ), ], ), child: const Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), SizedBox(height: 12), Text( 'Localisation en cours...', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), ], ), ), ), ), ), // Barre de recherche et suggestions Positioned( top: 0, left: 0, right: 0, child: Padding( padding: const EdgeInsets.all(12.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Champ de recherche Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.15), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: TextField( controller: _searchController, style: const TextStyle( color: Colors.black87, fontSize: 16, ), decoration: InputDecoration( hintText: 'Rechercher un lieu...', hintStyle: TextStyle( color: Colors.grey[600], fontSize: 16, ), prefixIcon: _isSearching ? const Padding( padding: EdgeInsets.all(14.0), child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, ), ), ) : Icon(Icons.search, color: Colors.grey[700]), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: Icon( Icons.clear, color: Colors.grey[700], ), onPressed: () { _searchController.clear(); setState(() { _suggestions = []; }); }, ) : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12.0), borderSide: BorderSide.none, ), filled: true, fillColor: Colors.white, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14, ), ), onChanged: (value) { // Ne pas rechercher si c'est juste le remplissage initial if (widget.initialSearchQuery != null && value == widget.initialSearchQuery) { return; } _searchPlaces(value); }, ), ), // Liste des suggestions if (_suggestions.isNotEmpty) Container( margin: const EdgeInsets.only(top: 8), constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.4, ), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.15), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Material( color: Colors.transparent, child: ListView.separated( shrinkWrap: true, padding: EdgeInsets.zero, itemCount: _suggestions.length, separatorBuilder: (context, index) => Divider(height: 1, color: Colors.grey[300]), itemBuilder: (context, index) { final suggestion = _suggestions[index]; return InkWell( onTap: () => _selectPlace(suggestion), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), child: Row( children: [ Icon( Icons.place, color: Colors.grey[600], size: 22, ), const SizedBox(width: 12), Expanded( child: Text( suggestion.description, style: const TextStyle( fontSize: 15, color: Colors.black87, fontWeight: FontWeight.w400, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), ), ); }, ), ), ), ), ], ), ), ), ], ), ), floatingActionButton: FloatingActionButton( heroTag: 'map_fab', onPressed: _getCurrentLocation, tooltip: 'Ma position', child: const Icon(Icons.my_location), ), ); } } class PlaceSuggestion { final String placeId; final String description; PlaceSuggestion({required this.placeId, required this.description}); }