feat: enhance global search and map experience
- Global Activity Search: - Allow searching activities globally (not just in destination). - Add distance warning for activities > 50km away. - Create Trip UI: - Fix destination suggestion list overflow. - Prevent suggestion list from reappearing after selection. - Map: - Add generic text search support (e.g., "Restaurants") on 'Enter'. - Display multiple results for generic searches. - Resize markers (User 60.0, Places 50.0). - Standardize place markers to red pin.
This commit is contained in:
@@ -22,6 +22,7 @@ class MapContent extends StatefulWidget {
|
||||
class _MapContentState extends State<MapContent> {
|
||||
GoogleMapController? _mapController;
|
||||
LatLng _initialPosition = const LatLng(48.8566, 2.3522);
|
||||
LatLng? _currentMapCenter;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
bool _isLoadingLocation = false;
|
||||
bool _isSearching = false;
|
||||
@@ -321,20 +322,30 @@ class _MapContentState extends State<MapContent> {
|
||||
}
|
||||
}
|
||||
|
||||
// Créer une icône personnalisée à partir de l'icône Material
|
||||
Future<BitmapDescriptor> _createCustomMarkerIcon() async {
|
||||
// Créer une icône personnalisée
|
||||
Future<BitmapDescriptor> _createMarkerIcon(
|
||||
IconData iconData,
|
||||
Color color, {
|
||||
double size = 60.0,
|
||||
}) async {
|
||||
final pictureRecorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(pictureRecorder);
|
||||
const size = 80.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),
|
||||
text: String.fromCharCode(iconData.codePoint),
|
||||
style: TextStyle(
|
||||
fontSize: 70,
|
||||
fontFamily: Icons.person_pin_circle.fontFamily,
|
||||
color: Colors.blue[700],
|
||||
fontSize: size,
|
||||
fontFamily: iconData.fontFamily,
|
||||
package: iconData.fontPackage,
|
||||
color: color,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: const Offset(1, 1),
|
||||
blurRadius: 2,
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
iconPainter.layout();
|
||||
@@ -364,8 +375,12 @@ class _MapContentState extends State<MapContent> {
|
||||
),
|
||||
);
|
||||
|
||||
// Créer l'icône personnalisée
|
||||
final icon = await _createCustomMarkerIcon();
|
||||
// Créer l'icône personnalisée (plus petite: 60 au lieu de 80)
|
||||
final icon = await _createMarkerIcon(
|
||||
Icons.person_pin_circle,
|
||||
Colors.blue[700]!,
|
||||
size: 60.0,
|
||||
);
|
||||
|
||||
// Ajouter le marqueur avec l'icône
|
||||
setState(() {
|
||||
@@ -374,10 +389,7 @@ class _MapContentState extends State<MapContent> {
|
||||
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)
|
||||
anchor: const Offset(0.5, 0.85), // Ancrer au bas de l'icône
|
||||
infoWindow: InfoWindow(
|
||||
title: 'Ma position',
|
||||
snippet:
|
||||
@@ -401,10 +413,21 @@ class _MapContentState extends State<MapContent> {
|
||||
});
|
||||
|
||||
try {
|
||||
final apiKey = _apiKey;
|
||||
LoggerService.info('MapContent: Searching places with query "$query"');
|
||||
if (apiKey.isEmpty) {
|
||||
LoggerService.error('MapContent: API Key is empty!');
|
||||
} else {
|
||||
// Log first few chars to verify correct key is loaded without leaking full key
|
||||
LoggerService.info(
|
||||
'MapContent: Using API Key starting with ${apiKey.substring(0, 5)}...',
|
||||
);
|
||||
}
|
||||
|
||||
final url = Uri.parse(
|
||||
'https://maps.googleapis.com/maps/api/place/autocomplete/json'
|
||||
'?input=${Uri.encodeComponent(query)}'
|
||||
'&key=$_apiKey'
|
||||
'&key=$apiKey'
|
||||
'&language=fr',
|
||||
);
|
||||
|
||||
@@ -412,11 +435,21 @@ class _MapContentState extends State<MapContent> {
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
LoggerService.info(
|
||||
'MapContent: Response status code: ${response.statusCode}',
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
final status = data['status'];
|
||||
|
||||
if (data['status'] == 'OK') {
|
||||
LoggerService.info('MapContent: API Status: $status');
|
||||
|
||||
if (status == 'OK') {
|
||||
final predictions = data['predictions'] as List;
|
||||
LoggerService.info(
|
||||
'MapContent: Found ${predictions.length} predictions',
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_suggestions = predictions
|
||||
@@ -430,13 +463,19 @@ class _MapContentState extends State<MapContent> {
|
||||
_isSearching = false;
|
||||
});
|
||||
} else {
|
||||
LoggerService.error(
|
||||
'MapContent: API Error: $status - ${data['error_message'] ?? "No error message"}',
|
||||
);
|
||||
setState(() {
|
||||
_suggestions = [];
|
||||
_isSearching = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
LoggerService.error('MapContent: HTTP Error ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
LoggerService.error('MapContent: Exception during search: $e');
|
||||
_showError('Erreur lors de la recherche de lieux: $e');
|
||||
setState(() {
|
||||
_isSearching = false;
|
||||
@@ -444,6 +483,128 @@ class _MapContentState extends State<MapContent> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _performTextSearch(String query) async {
|
||||
if (query.isEmpty) return;
|
||||
|
||||
setState(() {
|
||||
_isSearching = true;
|
||||
_suggestions = []; // Hide suggestions
|
||||
});
|
||||
|
||||
try {
|
||||
final apiKey = _apiKey;
|
||||
// Utiliser le centre actuel de la carte ou la position initiale
|
||||
final center = _currentMapCenter ?? _initialPosition;
|
||||
|
||||
final url = Uri.parse(
|
||||
'https://maps.googleapis.com/maps/api/place/textsearch/json'
|
||||
'?query=${Uri.encodeComponent(query)}'
|
||||
'&location=${center.latitude},${center.longitude}'
|
||||
'&radius=5000' // Rechercher dans un rayon de 5km autour du centre
|
||||
'&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 results = data['results'] as List;
|
||||
final List<Marker> newMarkers = [];
|
||||
|
||||
// Garder le marqueur de position utilisateur
|
||||
final userMarker = _markers
|
||||
.where((m) => m.markerId.value == 'user_location')
|
||||
.toList();
|
||||
|
||||
double minLat = center.latitude;
|
||||
double maxLat = center.latitude;
|
||||
double minLng = center.longitude;
|
||||
double maxLng = center.longitude;
|
||||
|
||||
// Créer le marqueur rouge standard personnalisé
|
||||
final markerIcon = await _createMarkerIcon(
|
||||
Icons.location_on,
|
||||
Colors.red,
|
||||
size: 50.0,
|
||||
);
|
||||
|
||||
for (final place in results) {
|
||||
final geometry = place['geometry']['location'];
|
||||
final lat = geometry['lat'];
|
||||
final lng = geometry['lng'];
|
||||
final name = place['name'];
|
||||
final placeId = place['place_id'];
|
||||
|
||||
final position = LatLng(lat, lng);
|
||||
|
||||
// Mettre à jour les bornes
|
||||
if (lat < minLat) minLat = lat;
|
||||
if (lat > maxLat) maxLat = lat;
|
||||
if (lng < minLng) minLng = lng;
|
||||
if (lng > maxLng) maxLng = lng;
|
||||
|
||||
newMarkers.add(
|
||||
Marker(
|
||||
markerId: MarkerId(placeId),
|
||||
position: position,
|
||||
icon: markerIcon,
|
||||
anchor: const Offset(0.5, 0.85), // Standard anchor for pins
|
||||
infoWindow: InfoWindow(
|
||||
title: name,
|
||||
snippet: place['formatted_address'] ?? 'Lieu trouvé',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_markers.clear();
|
||||
if (userMarker.isNotEmpty) {
|
||||
_markers.add(userMarker.first);
|
||||
}
|
||||
_markers.addAll(newMarkers);
|
||||
_isSearching = false;
|
||||
});
|
||||
|
||||
// Ajuster la caméra pour montrer tous les résultats
|
||||
if (newMarkers.isNotEmpty) {
|
||||
_mapController?.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(
|
||||
LatLngBounds(
|
||||
southwest: LatLng(minLat, minLng),
|
||||
northeast: LatLng(maxLat, maxLng),
|
||||
),
|
||||
50.0, // padding
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
FocusScope.of(context).unfocus();
|
||||
} else {
|
||||
_showError('Aucun résultat trouvé pour "$query"');
|
||||
setState(() {
|
||||
_isSearching = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_showError('Erreur lors de la recherche: ${response.statusCode}');
|
||||
setState(() {
|
||||
_isSearching = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
_showError('Erreur lors de la recherche: $e');
|
||||
setState(() {
|
||||
_isSearching = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectPlace(PlaceSuggestion suggestion) async {
|
||||
setState(() {
|
||||
_isSearching = true;
|
||||
@@ -467,13 +628,21 @@ class _MapContentState extends State<MapContent> {
|
||||
final data = json.decode(response.body);
|
||||
|
||||
if (data['status'] == 'OK') {
|
||||
final location = data['result']['geometry']['location'];
|
||||
final result = data['result'];
|
||||
final location = result['geometry']['location'];
|
||||
final lat = location['lat'];
|
||||
final lng = location['lng'];
|
||||
final name = data['result']['name'];
|
||||
final name = result['name'];
|
||||
|
||||
final newPosition = LatLng(lat, lng);
|
||||
|
||||
// Utiliser le marqueur rouge standard personnalisé
|
||||
final markerIcon = await _createMarkerIcon(
|
||||
Icons.location_on,
|
||||
Colors.red,
|
||||
size: 50.0,
|
||||
);
|
||||
|
||||
// Ajouter un marqueur pour le lieu recherché (ne pas supprimer le marqueur de position)
|
||||
setState(() {
|
||||
// Garder le marqueur de position utilisateur
|
||||
@@ -484,6 +653,8 @@ class _MapContentState extends State<MapContent> {
|
||||
Marker(
|
||||
markerId: MarkerId(suggestion.placeId),
|
||||
position: newPosition,
|
||||
icon: markerIcon,
|
||||
anchor: const Offset(0.5, 0.85),
|
||||
infoWindow: InfoWindow(title: name),
|
||||
),
|
||||
);
|
||||
@@ -533,6 +704,9 @@ class _MapContentState extends State<MapContent> {
|
||||
onMapCreated: (GoogleMapController controller) {
|
||||
_mapController = controller;
|
||||
},
|
||||
onCameraMove: (CameraPosition position) {
|
||||
_currentMapCenter = position.target;
|
||||
},
|
||||
markers: _markers,
|
||||
circles: _circles,
|
||||
myLocationEnabled: false,
|
||||
@@ -664,6 +838,10 @@ class _MapContentState extends State<MapContent> {
|
||||
vertical: 14,
|
||||
),
|
||||
),
|
||||
textInputAction: TextInputAction.search,
|
||||
onSubmitted: (value) {
|
||||
_performTextSearch(value);
|
||||
},
|
||||
onChanged: (value) {
|
||||
// Ne pas rechercher si c'est juste le remplissage initial
|
||||
if (widget.initialSearchQuery != null &&
|
||||
|
||||
Reference in New Issue
Block a user