feat: Add geocoding functionality for trips and enhance activity search with coordinates

This commit is contained in:
Dayron
2025-11-04 20:47:26 +01:00
parent f6c8432335
commit 9cb21c3470
9 changed files with 421 additions and 56 deletions

View File

@@ -23,6 +23,7 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
on<LoadActivities>(_onLoadActivities); on<LoadActivities>(_onLoadActivities);
on<SearchActivities>(_onSearchActivities); on<SearchActivities>(_onSearchActivities);
on<SearchActivitiesWithCoordinates>(_onSearchActivitiesWithCoordinates);
on<SearchActivitiesByText>(_onSearchActivitiesByText); on<SearchActivitiesByText>(_onSearchActivitiesByText);
on<AddActivity>(_onAddActivity); on<AddActivity>(_onAddActivity);
on<AddActivitiesBatch>(_onAddActivitiesBatch); on<AddActivitiesBatch>(_onAddActivitiesBatch);
@@ -98,6 +99,50 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
} }
} }
/// Handles searching activities using coordinates directly (bypasses geocoding)
Future<void> _onSearchActivitiesWithCoordinates(
SearchActivitiesWithCoordinates event,
Emitter<ActivityState> emit,
) async {
try {
// Si c'est un append (charger plus), on garde l'état actuel et on met isLoading à true
if (event.appendToExisting && state is ActivitySearchResults) {
final currentState = state as ActivitySearchResults;
emit(currentState.copyWith(isLoading: true));
} else {
emit(const ActivitySearching());
}
final searchResults = await _placesService.searchActivitiesPaginated(
latitude: event.latitude,
longitude: event.longitude,
tripId: event.tripId,
category: event.category,
pageSize: event.maxResults ?? 20,
);
final activities = searchResults['activities'] as List<Activity>;
List<Activity> finalResults;
// Si on doit ajouter aux résultats existants
if (event.appendToExisting && state is ActivitySearchResults) {
final currentState = state as ActivitySearchResults;
finalResults = [...currentState.searchResults, ...activities];
} else {
finalResults = activities;
}
emit(ActivitySearchResults(
searchResults: finalResults,
query: event.category?.displayName ?? 'Toutes les activités',
isLoading: false,
));
} catch (e) {
_errorService.logError('activity_bloc', 'Erreur recherche activités avec coordonnées: $e');
emit(const ActivityError('Impossible de rechercher les activités'));
}
}
/// Handles text-based activity search /// Handles text-based activity search
Future<void> _onSearchActivitiesByText( Future<void> _onSearchActivitiesByText(
SearchActivitiesByText event, SearchActivitiesByText event,

View File

@@ -43,6 +43,32 @@ class SearchActivities extends ActivityEvent {
List<Object?> get props => [tripId, destination, category, maxResults, offset, reset, appendToExisting]; List<Object?> get props => [tripId, destination, category, maxResults, offset, reset, appendToExisting];
} }
/// Event to search activities using coordinates directly (bypasses geocoding)
class SearchActivitiesWithCoordinates extends ActivityEvent {
final String tripId;
final double latitude;
final double longitude;
final ActivityCategory? category;
final int? maxResults;
final int? offset;
final bool reset;
final bool appendToExisting;
const SearchActivitiesWithCoordinates({
required this.tripId,
required this.latitude,
required this.longitude,
this.category,
this.maxResults,
this.offset,
this.reset = false,
this.appendToExisting = false,
});
@override
List<Object?> get props => [tripId, latitude, longitude, category, maxResults, offset, reset, appendToExisting];
}
/// Event to search activities by text query /// Event to search activities by text query
class SearchActivitiesByText extends ActivityEvent { class SearchActivitiesByText extends ActivityEvent {
final String tripId; final String tripId;

View File

@@ -39,6 +39,8 @@ class GoogleActivityBloc extends Bloc<GoogleActivityEvent, GoogleActivityState>
final result = await _placesService.searchActivitiesPaginated( final result = await _placesService.searchActivitiesPaginated(
destination: event.destination, destination: event.destination,
latitude: event.latitude,
longitude: event.longitude,
tripId: event.tripId, tripId: event.tripId,
category: event.category, category: event.category,
pageSize: 6, pageSize: 6,
@@ -74,6 +76,8 @@ class GoogleActivityBloc extends Bloc<GoogleActivityEvent, GoogleActivityState>
final result = await _placesService.searchActivitiesPaginated( final result = await _placesService.searchActivitiesPaginated(
destination: event.destination, destination: event.destination,
latitude: event.latitude,
longitude: event.longitude,
tripId: event.tripId, tripId: event.tripId,
category: event.category, category: event.category,
pageSize: 6, pageSize: 6,
@@ -143,6 +147,8 @@ class GoogleActivityBloc extends Bloc<GoogleActivityEvent, GoogleActivityState>
add(LoadGoogleActivities( add(LoadGoogleActivities(
tripId: event.tripId, tripId: event.tripId,
destination: event.destination, destination: event.destination,
latitude: event.latitude,
longitude: event.longitude,
category: event.category, category: event.category,
)); ));
} }

View File

@@ -12,35 +12,43 @@ abstract class GoogleActivityEvent extends Equatable {
/// Charger les activités Google Places /// Charger les activités Google Places
class LoadGoogleActivities extends GoogleActivityEvent { class LoadGoogleActivities extends GoogleActivityEvent {
final String tripId; final String tripId;
final String destination; final String? destination;
final double? latitude;
final double? longitude;
final ActivityCategory? category; final ActivityCategory? category;
const LoadGoogleActivities({ const LoadGoogleActivities({
required this.tripId, required this.tripId,
required this.destination, this.destination,
this.latitude,
this.longitude,
this.category, this.category,
}); });
@override @override
List<Object?> get props => [tripId, destination, category]; List<Object?> get props => [tripId, destination, latitude, longitude, category];
} }
/// Charger plus d'activités Google (pagination) /// Charger plus d'activités Google (pagination)
class LoadMoreGoogleActivities extends GoogleActivityEvent { class LoadMoreGoogleActivities extends GoogleActivityEvent {
final String tripId; final String tripId;
final String destination; final String? destination;
final double? latitude;
final double? longitude;
final ActivityCategory? category; final ActivityCategory? category;
final String? nextPageToken; final String? nextPageToken;
const LoadMoreGoogleActivities({ const LoadMoreGoogleActivities({
required this.tripId, required this.tripId,
required this.destination, this.destination,
this.latitude,
this.longitude,
this.category, this.category,
this.nextPageToken, this.nextPageToken,
}); });
@override @override
List<Object?> get props => [tripId, destination, category, nextPageToken]; List<Object?> get props => [tripId, destination, latitude, longitude, category, nextPageToken];
} }
/// Mettre à jour les activités Google /// Mettre à jour les activités Google
@@ -74,17 +82,21 @@ class AddGoogleActivityToDb extends GoogleActivityEvent {
/// Rechercher des activités Google par catégorie /// Rechercher des activités Google par catégorie
class SearchGoogleActivitiesByCategory extends GoogleActivityEvent { class SearchGoogleActivitiesByCategory extends GoogleActivityEvent {
final String tripId; final String tripId;
final String destination; final String? destination;
final double? latitude;
final double? longitude;
final ActivityCategory category; final ActivityCategory category;
const SearchGoogleActivitiesByCategory({ const SearchGoogleActivitiesByCategory({
required this.tripId, required this.tripId,
required this.destination, this.destination,
this.latitude,
this.longitude,
required this.category, required this.category,
}); });
@override @override
List<Object?> get props => [tripId, destination, category]; List<Object?> get props => [tripId, destination, latitude, longitude, category];
} }
/// Effacer les résultats Google /// Effacer les résultats Google

View File

@@ -1282,6 +1282,20 @@ class _ActivitiesPageState extends State<ActivitiesPage>
_totalGoogleActivitiesRequested = 6; // Reset du compteur _totalGoogleActivitiesRequested = 6; // Reset du compteur
_autoReloadInProgress = false; // Reset des protections _autoReloadInProgress = false; // Reset des protections
_lastAutoReloadTriggerCount = 0; _lastAutoReloadTriggerCount = 0;
// Utiliser les coordonnées pré-géolocalisées du voyage si disponibles
if (widget.trip.hasCoordinates) {
print('🌍 [Google Search] Using pre-geocoded coordinates: ${widget.trip.latitude}, ${widget.trip.longitude}');
context.read<ActivityBloc>().add(SearchActivitiesWithCoordinates(
tripId: widget.trip.id!,
latitude: widget.trip.latitude!,
longitude: widget.trip.longitude!,
category: null, // Rechercher dans toutes les catégories
maxResults: 6, // Charger 6 résultats à la fois
reset: true, // Nouveau flag pour reset
));
} else {
print('⚠️ [Google Search] No coordinates available, falling back to destination geocoding');
context.read<ActivityBloc>().add(SearchActivities( context.read<ActivityBloc>().add(SearchActivities(
tripId: widget.trip.id!, tripId: widget.trip.id!,
destination: widget.trip.location, destination: widget.trip.location,
@@ -1289,6 +1303,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
maxResults: 6, // Charger 6 résultats à la fois maxResults: 6, // Charger 6 résultats à la fois
reset: true, // Nouveau flag pour reset reset: true, // Nouveau flag pour reset
)); ));
}
_googleSearchPerformed = true; _googleSearchPerformed = true;
} }
@@ -1297,6 +1312,20 @@ class _ActivitiesPageState extends State<ActivitiesPage>
_totalGoogleActivitiesRequested = 6; // Reset du compteur _totalGoogleActivitiesRequested = 6; // Reset du compteur
_autoReloadInProgress = false; // Reset des protections _autoReloadInProgress = false; // Reset des protections
_lastAutoReloadTriggerCount = 0; _lastAutoReloadTriggerCount = 0;
// Utiliser les coordonnées pré-géolocalisées du voyage si disponibles
if (widget.trip.hasCoordinates) {
print('🌍 [Google Search] Using pre-geocoded coordinates: ${widget.trip.latitude}, ${widget.trip.longitude}');
context.read<ActivityBloc>().add(SearchActivitiesWithCoordinates(
tripId: widget.trip.id!,
latitude: widget.trip.latitude!,
longitude: widget.trip.longitude!,
category: null,
maxResults: 6,
reset: true,
));
} else {
print('⚠️ [Google Search] No coordinates available, falling back to destination geocoding');
context.read<ActivityBloc>().add(SearchActivities( context.read<ActivityBloc>().add(SearchActivities(
tripId: widget.trip.id!, tripId: widget.trip.id!,
destination: widget.trip.location, destination: widget.trip.location,
@@ -1304,6 +1333,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
maxResults: 6, maxResults: 6,
reset: true, reset: true,
)); ));
}
_googleSearchPerformed = true; _googleSearchPerformed = true;
} }
@@ -1317,6 +1347,20 @@ class _ActivitiesPageState extends State<ActivitiesPage>
print('📊 [Google Search] Current results count: $currentCount, requesting total: $newTotal'); print('📊 [Google Search] Current results count: $currentCount, requesting total: $newTotal');
_totalGoogleActivitiesRequested = newTotal; _totalGoogleActivitiesRequested = newTotal;
// Utiliser les coordonnées pré-géolocalisées du voyage si disponibles
if (widget.trip.hasCoordinates) {
print('🌍 [Google Search] Using pre-geocoded coordinates for more results');
context.read<ActivityBloc>().add(SearchActivitiesWithCoordinates(
tripId: widget.trip.id!,
latitude: widget.trip.latitude!,
longitude: widget.trip.longitude!,
category: null,
maxResults: newTotal, // Demander le total cumulé
reset: true, // Reset pour avoir tous les résultats d'un coup
));
} else {
print('⚠️ [Google Search] No coordinates available, falling back to destination geocoding');
context.read<ActivityBloc>().add(SearchActivities( context.read<ActivityBloc>().add(SearchActivities(
tripId: widget.trip.id!, tripId: widget.trip.id!,
destination: widget.trip.location, destination: widget.trip.location,
@@ -1326,6 +1370,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
)); ));
} }
} }
}
void _loadMoreGoogleActivitiesWithTotal(int totalToRequest) { void _loadMoreGoogleActivitiesWithTotal(int totalToRequest) {
print('📈 [Google Search] Loading activities with specific total: $totalToRequest'); print('📈 [Google Search] Loading activities with specific total: $totalToRequest');
@@ -1339,6 +1384,20 @@ class _ActivitiesPageState extends State<ActivitiesPage>
print('📊 [Google Search] Current: $currentCount, Total demandé: $totalToRequest, Additional: $additionalNeeded'); print('📊 [Google Search] Current: $currentCount, Total demandé: $totalToRequest, Additional: $additionalNeeded');
if (additionalNeeded > 0) { if (additionalNeeded > 0) {
// Utiliser les coordonnées pré-géolocalisées du voyage si disponibles
if (widget.trip.hasCoordinates) {
print('🌍 [Google Search] Using pre-geocoded coordinates for additional results');
context.read<ActivityBloc>().add(SearchActivitiesWithCoordinates(
tripId: widget.trip.id!,
latitude: widget.trip.latitude!,
longitude: widget.trip.longitude!,
category: null,
maxResults: additionalNeeded,
offset: currentCount,
appendToExisting: true, // Ajouter aux résultats existants
));
} else {
print('⚠️ [Google Search] No coordinates available, falling back to destination geocoding');
context.read<ActivityBloc>().add(SearchActivities( context.read<ActivityBloc>().add(SearchActivities(
tripId: widget.trip.id!, tripId: widget.trip.id!,
destination: widget.trip.location, destination: widget.trip.location,
@@ -1347,11 +1406,24 @@ class _ActivitiesPageState extends State<ActivitiesPage>
offset: currentCount, offset: currentCount,
appendToExisting: true, // Ajouter aux résultats existants appendToExisting: true, // Ajouter aux résultats existants
)); ));
}
} else { } else {
print('⚠️ [Google Search] Pas besoin de charger plus (déjà suffisant)'); print('⚠️ [Google Search] Pas besoin de charger plus (déjà suffisant)');
} }
} else { } else {
// Si pas de résultats existants, faire une recherche complète // Si pas de résultats existants, faire une recherche complète
if (widget.trip.hasCoordinates) {
print('🌍 [Google Search] Using pre-geocoded coordinates for fresh search');
context.read<ActivityBloc>().add(SearchActivitiesWithCoordinates(
tripId: widget.trip.id!,
latitude: widget.trip.latitude!,
longitude: widget.trip.longitude!,
category: null,
maxResults: totalToRequest,
reset: true,
));
} else {
print('⚠️ [Google Search] No coordinates available, falling back to destination geocoding');
context.read<ActivityBloc>().add(SearchActivities( context.read<ActivityBloc>().add(SearchActivities(
tripId: widget.trip.id!, tripId: widget.trip.id!,
destination: widget.trip.location, destination: widget.trip.location,
@@ -1362,3 +1434,4 @@ class _ActivitiesPageState extends State<ActivitiesPage>
} }
} }
} }
}

View File

@@ -20,6 +20,7 @@ import '../../repositories/group_repository.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../services/place_image_service.dart'; import '../../services/place_image_service.dart';
import '../../services/trip_geocoding_service.dart';
/// Create trip content widget for trip creation and editing functionality. /// Create trip content widget for trip creation and editing functionality.
/// ///
@@ -71,6 +72,7 @@ class _CreateTripContentState extends State<CreateTripContent> {
final _userService = UserService(); final _userService = UserService();
final _groupRepository = GroupRepository(); final _groupRepository = GroupRepository();
final _placeImageService = PlaceImageService(); final _placeImageService = PlaceImageService();
final _tripGeocodingService = TripGeocodingService();
/// Trip date variables /// Trip date variables
DateTime? _startDate; DateTime? _startDate;
@@ -1060,9 +1062,30 @@ class _CreateTripContentState extends State<CreateTripContent> {
imageUrl: _selectedImageUrl, // Ajouter l'URL de l'image imageUrl: _selectedImageUrl, // Ajouter l'URL de l'image
); );
// Géolocaliser le voyage avant de le sauvegarder
Trip tripWithCoordinates;
try {
print('🌍 [CreateTrip] Géolocalisation en cours pour: ${trip.location}');
tripWithCoordinates = await _tripGeocodingService.geocodeTrip(trip);
print('✅ [CreateTrip] Géolocalisation réussie: ${tripWithCoordinates.latitude}, ${tripWithCoordinates.longitude}');
} catch (e) {
print('⚠️ [CreateTrip] Erreur de géolocalisation: $e');
// Continuer sans coordonnées en cas d'erreur
tripWithCoordinates = trip;
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Voyage créé sans géolocalisation (pas d\'impact sur les fonctionnalités)'),
backgroundColor: Colors.orange,
duration: Duration(seconds: 2),
),
);
}
}
if (isEditing) { if (isEditing) {
// Mode mise à jour // Mode mise à jour
tripBloc.add(TripUpdateRequested(trip: trip)); tripBloc.add(TripUpdateRequested(trip: tripWithCoordinates));
await _updateGroupMembers( await _updateGroupMembers(
widget.tripToEdit!.id!, widget.tripToEdit!.id!,
@@ -1072,7 +1095,7 @@ class _CreateTripContentState extends State<CreateTripContent> {
} else { } else {
// Mode création - Le groupe sera créé dans le listener TripCreated // Mode création - Le groupe sera créé dans le listener TripCreated
tripBloc.add(TripCreateRequested(trip: trip)); tripBloc.add(TripCreateRequested(trip: tripWithCoordinates));
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {

View File

@@ -19,6 +19,15 @@ class Trip {
/// Trip destination or location. /// Trip destination or location.
final String location; final String location;
/// Latitude coordinate of the trip location.
final double? latitude;
/// Longitude coordinate of the trip location.
final double? longitude;
/// Timestamp when the location was last geocoded.
final DateTime? lastGeocodingUpdate;
/// Trip start date and time. /// Trip start date and time.
final DateTime startDate; final DateTime startDate;
@@ -54,6 +63,9 @@ class Trip {
required this.title, required this.title,
required this.description, required this.description,
required this.location, required this.location,
this.latitude,
this.longitude,
this.lastGeocodingUpdate,
required this.startDate, required this.startDate,
required this.endDate, required this.endDate,
this.budget, this.budget,
@@ -101,6 +113,11 @@ class Trip {
title: map['title'] as String? ?? '', title: map['title'] as String? ?? '',
description: map['description'] as String? ?? '', description: map['description'] as String? ?? '',
location: map['location'] as String? ?? '', location: map['location'] as String? ?? '',
latitude: (map['latitude'] as num?)?.toDouble(),
longitude: (map['longitude'] as num?)?.toDouble(),
lastGeocodingUpdate: map['lastGeocodingUpdate'] != null
? _parseDateTime(map['lastGeocodingUpdate'])
: null,
startDate: _parseDateTime(map['startDate']), startDate: _parseDateTime(map['startDate']),
endDate: _parseDateTime(map['endDate']), endDate: _parseDateTime(map['endDate']),
budget: (map['budget'] as num?)?.toDouble(), budget: (map['budget'] as num?)?.toDouble(),
@@ -122,6 +139,11 @@ class Trip {
'title': title, 'title': title,
'description': description, 'description': description,
'location': location, 'location': location,
'latitude': latitude,
'longitude': longitude,
'lastGeocodingUpdate': lastGeocodingUpdate != null
? Timestamp.fromDate(lastGeocodingUpdate!)
: null,
'startDate': Timestamp.fromDate(startDate), 'startDate': Timestamp.fromDate(startDate),
'endDate': Timestamp.fromDate(endDate), 'endDate': Timestamp.fromDate(endDate),
'budget': budget, 'budget': budget,
@@ -145,6 +167,9 @@ class Trip {
String? title, String? title,
String? description, String? description,
String? location, String? location,
double? latitude,
double? longitude,
DateTime? lastGeocodingUpdate,
DateTime? startDate, DateTime? startDate,
DateTime? endDate, DateTime? endDate,
double? budget, double? budget,
@@ -160,6 +185,9 @@ class Trip {
title: title ?? this.title, title: title ?? this.title,
description: description ?? this.description, description: description ?? this.description,
location: location ?? this.location, location: location ?? this.location,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
lastGeocodingUpdate: lastGeocodingUpdate ?? this.lastGeocodingUpdate,
startDate: startDate ?? this.startDate, startDate: startDate ?? this.startDate,
endDate: endDate ?? this.endDate, endDate: endDate ?? this.endDate,
budget: budget ?? this.budget, budget: budget ?? this.budget,
@@ -230,9 +258,28 @@ class Trip {
} }
} }
/// Vérifie si le voyage a des coordonnées géographiques valides
bool get hasCoordinates => latitude != null && longitude != null;
/// Vérifie si les coordonnées sont récentes (moins de 30 jours)
bool get hasRecentCoordinates {
if (!hasCoordinates || lastGeocodingUpdate == null) return false;
final thirtyDaysAgo = DateTime.now().subtract(const Duration(days: 30));
return lastGeocodingUpdate!.isAfter(thirtyDaysAgo);
}
/// Retourne un Map avec les coordonnées pour les services externes
Map<String, dynamic>? get coordinatesMap {
if (!hasCoordinates) return null;
return {
'lat': latitude!,
'lng': longitude!,
};
}
@override @override
String toString() { String toString() {
return 'Trip(id: $id, title: $title, location: $location, status: $status)'; return 'Trip(id: $id, title: $title, location: $location, coordinates: ${hasCoordinates ? "($latitude, $longitude)" : "N/A"}, status: $status)';
} }
@override @override

View File

@@ -377,7 +377,9 @@ class ActivityPlacesService {
/// Recherche d'activités avec pagination (6 par page) /// Recherche d'activités avec pagination (6 par page)
Future<Map<String, dynamic>> searchActivitiesPaginated({ Future<Map<String, dynamic>> searchActivitiesPaginated({
required String destination, String? destination,
double? latitude,
double? longitude,
required String tripId, required String tripId,
ActivityCategory? category, ActivityCategory? category,
int pageSize = 6, int pageSize = 6,
@@ -385,16 +387,29 @@ class ActivityPlacesService {
int radius = 5000, int radius = 5000,
}) async { }) async {
try { try {
print('ActivityPlacesService: Recherche paginée pour: $destination (page: ${nextPageToken ?? "première"})'); double lat, lng;
// 1. Géocoder la destination // Utiliser les coordonnées fournies ou géocoder la destination
if (latitude != null && longitude != null) {
lat = latitude;
lng = longitude;
print('ActivityPlacesService: Utilisation des coordonnées pré-géolocalisées: $lat, $lng');
} else if (destination != null) {
print('ActivityPlacesService: Géolocalisation de la destination: $destination');
final coordinates = await _geocodeDestination(destination); final coordinates = await _geocodeDestination(destination);
lat = coordinates['lat']!;
lng = coordinates['lng']!;
} else {
throw Exception('Destination ou coordonnées requises');
}
print('ActivityPlacesService: Recherche paginée aux coordonnées: $lat, $lng (page: ${nextPageToken ?? "première"})');
// 2. Rechercher les activités par catégorie avec pagination // 2. Rechercher les activités par catégorie avec pagination
if (category != null) { if (category != null) {
return await _searchByCategoryPaginated( return await _searchByCategoryPaginated(
coordinates['lat']!, lat,
coordinates['lng']!, lng,
category, category,
tripId, tripId,
radius, radius,
@@ -404,8 +419,8 @@ class ActivityPlacesService {
} else { } else {
// Pour toutes les catégories, faire une recherche générale paginée // Pour toutes les catégories, faire une recherche générale paginée
return await _searchAllCategoriesPaginated( return await _searchAllCategoriesPaginated(
coordinates['lat']!, lat,
coordinates['lng']!, lng,
tripId, tripId,
radius, radius,
pageSize, pageSize,

View File

@@ -0,0 +1,118 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../models/trip.dart';
import 'error_service.dart';
/// Service pour géocoder les destinations des voyages
class TripGeocodingService {
static final TripGeocodingService _instance = TripGeocodingService._internal();
factory TripGeocodingService() => _instance;
TripGeocodingService._internal();
final ErrorService _errorService = ErrorService();
static final String _apiKey = dotenv.env['GOOGLE_MAPS_API_KEY'] ?? '';
/// Géocode la destination d'un voyage et retourne un Trip mis à jour
Future<Trip> geocodeTrip(Trip trip) async {
try {
print('🌍 [TripGeocoding] Géocodage de "${trip.location}"');
// Vérifier si on a déjà des coordonnées récentes
if (trip.hasRecentCoordinates) {
print('✅ [TripGeocoding] Coordonnées récentes trouvées, pas de géocodage nécessaire');
return trip;
}
if (_apiKey.isEmpty) {
print('❌ [TripGeocoding] Clé API Google Maps manquante');
throw Exception('Clé API Google Maps non configurée');
}
final coordinates = await _geocodeDestination(trip.location);
if (coordinates != null) {
print('✅ [TripGeocoding] Coordonnées trouvées: ${coordinates['lat']}, ${coordinates['lng']}');
return trip.copyWith(
latitude: coordinates['lat'],
longitude: coordinates['lng'],
lastGeocodingUpdate: DateTime.now(),
);
} else {
print('⚠️ [TripGeocoding] Impossible de géocoder "${trip.location}"');
return trip;
}
} catch (e) {
print('❌ [TripGeocoding] Erreur lors du géocodage: $e');
_errorService.logError('trip_geocoding_service', e);
return trip; // Retourner le voyage original en cas d'erreur
}
}
/// Géocode une destination et retourne les coordonnées
Future<Map<String, double>?> _geocodeDestination(String destination) async {
try {
final url = 'https://maps.googleapis.com/maps/api/geocode/json'
'?address=${Uri.encodeComponent(destination)}'
'&key=$_apiKey';
print('🌐 [TripGeocoding] URL = $url');
final response = await http.get(Uri.parse(url));
print('📡 [TripGeocoding] Status code = ${response.statusCode}');
if (response.statusCode == 200) {
final data = json.decode(response.body);
print('📋 [TripGeocoding] Réponse géocodage = ${data['status']}');
if (data['status'] == 'OK' && data['results'].isNotEmpty) {
final location = data['results'][0]['geometry']['location'];
final coordinates = {
'lat': (location['lat'] as num).toDouble(),
'lng': (location['lng'] as num).toDouble(),
};
print('📍 [TripGeocoding] Coordonnées trouvées = $coordinates');
return coordinates;
} else {
print('⚠️ [TripGeocoding] Erreur API = ${data['error_message'] ?? data['status']}');
return null;
}
} else {
print('❌ [TripGeocoding] Erreur HTTP ${response.statusCode}');
return null;
}
} catch (e) {
print('❌ [TripGeocoding] Exception lors du géocodage: $e');
_errorService.logError('trip_geocoding_service', e);
return null;
}
}
/// Vérifie si un voyage a besoin d'être géocodé
bool needsGeocoding(Trip trip) {
return !trip.hasRecentCoordinates;
}
/// Géocode plusieurs voyages en batch
Future<List<Trip>> geocodeTrips(List<Trip> trips) async {
print('🔄 [TripGeocoding] Géocodage de ${trips.length} voyages');
final List<Trip> geocodedTrips = [];
for (final trip in trips) {
if (needsGeocoding(trip)) {
final geocodedTrip = await geocodeTrip(trip);
geocodedTrips.add(geocodedTrip);
// Petit délai pour éviter de saturer l'API Google
await Future.delayed(const Duration(milliseconds: 200));
} else {
geocodedTrips.add(trip);
}
}
print('✅ [TripGeocoding] Géocodage terminé');
return geocodedTrips;
}
}