feat: Add geocoding functionality for trips and enhance activity search with coordinates
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
118
lib/services/trip_geocoding_service.dart
Normal file
118
lib/services/trip_geocoding_service.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user