Files
TravelMate/lib/components/map/map_content.dart

670 lines
22 KiB
Dart

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<MapContent> createState() => _MapContentState();
}
class _MapContentState extends State<MapContent> {
GoogleMapController? _mapController;
LatLng _initialPosition = const LatLng(48.8566, 2.3522);
final TextEditingController _searchController = TextEditingController();
bool _isLoadingLocation = false;
bool _isSearching = false;
Position? _currentPosition;
final Set<Marker> _markers = {};
final Set<Circle> _circles = {};
List<PlaceSuggestion> _suggestions = [];
static final String _apiKey = dotenv.env['GOOGLE_MAPS_API_KEY'] ?? '';
@override
void initState() {
super.initState();
_getCurrentLocation();
// 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!);
});
}
}
@override
void dispose() {
_searchController.dispose();
_mapController?.dispose();
super.dispose();
}
// Nouvelle méthode pour effectuer la recherche initiale
Future<void> _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 (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<void> _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 (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<void> _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<BitmapDescriptor> _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.fromBytes(bytes!.buffer.asUint8List());
}
// Ajouter le marqueur avec l'icône personnalisée
Future<void> _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<void> _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 (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<void> _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 (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),
);
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(
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,
});
}