import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:travel_mate/services/error_service.dart'; class PlaceImageService { static final String _apiKey = dotenv.env['GOOGLE_MAPS_API_KEY'] ?? ''; final FirebaseStorage _storage = FirebaseStorage.instance; final ErrorService _errorService = ErrorService(); /// Récupère l'URL de l'image d'un lieu depuis Google Places API Future getPlaceImageUrl(String location) async { try { // ÉTAPE 1: Vérifier d'abord si une image existe déjà dans le Storage final existingUrl = await _checkExistingImage(location); if (existingUrl != null) { return existingUrl; } if (_apiKey.isEmpty) { _errorService.logError('PlaceImageService', 'Google Maps API key manquante'); return null; } // ÉTAPE 2: Recherche via Google Places API seulement si aucune image n'existe final searchTerms = _generateSearchTerms(location); for (final searchTerm in searchTerms) { // 1. Rechercher le lieu final placeId = await _getPlaceIdForTerm(searchTerm); if (placeId == null) continue; // 2. Récupérer les détails du lieu avec les photos final photoReference = await _getPhotoReference(placeId); if (photoReference == null) continue; // 3. Télécharger et sauvegarder l'image (seulement si pas d'image existante) final imageUrl = await _downloadAndSaveImage(photoReference, location); if (imageUrl != null) { return imageUrl; } } return null; } catch (e) { _errorService.logError('PlaceImageService', 'Erreur lors de la récupération de l\'image: $e'); return null; } } /// Génère différents termes de recherche pour optimiser les résultats List _generateSearchTerms(String location) { final terms = []; // Ajouter des termes spécifiques pour les villes connues final citySpecificTerms = _getCitySpecificTerms(location.toLowerCase()); terms.addAll(citySpecificTerms); // Termes génériques avec attractions terms.addAll([ '$location attractions touristiques monuments', '$location landmarks', '$location tourist attractions', '$location monuments historiques', '$location points d\'intérêt', '$location centre ville', '$location skyline', location, // Terme original en dernier ]); return terms; } /// Retourne des termes spécifiques pour des villes connues List _getCitySpecificTerms(String location) { final specific = []; if (location.contains('paris')) { specific.addAll([ 'Tour Eiffel Paris', 'Arc de Triomphe Paris', 'Notre-Dame Paris', 'Louvre Paris', 'Champs-Élysées Paris', ]); } else if (location.contains('london') || location.contains('londres')) { specific.addAll([ 'Big Ben London', 'Tower Bridge London', 'London Eye', 'Buckingham Palace London', 'Tower of London', ]); } else if (location.contains('rome') || location.contains('roma')) { specific.addAll([ 'Colosseum Rome', 'Trevi Fountain Rome', 'Vatican Rome', 'Pantheon Rome', ]); } else if (location.contains('new york') || location.contains('nyc')) { specific.addAll([ 'Statue of Liberty New York', 'Empire State Building New York', 'Times Square New York', 'Brooklyn Bridge New York', ]); } else if (location.contains('tokyo') || location.contains('japon')) { specific.addAll([ 'Tokyo Tower', 'Senso-ji Temple Tokyo', 'Shibuya Crossing Tokyo', 'Tokyo Skytree', ]); } return specific; } /// Recherche un place ID pour un terme spécifique Future _getPlaceIdForTerm(String searchTerm) async { // Essayer d'abord avec les attractions touristiques String? placeId = await _searchPlaceWithType(searchTerm, 'tourist_attraction'); if (placeId != null) return placeId; // Puis avec les points d'intérêt placeId = await _searchPlaceWithType(searchTerm, 'point_of_interest'); if (placeId != null) return placeId; // Enfin recherche générale return await _searchPlaceGeneral(searchTerm); } /// Recherche un lieu avec un type spécifique Future _searchPlaceWithType(String location, String type) async { try { final url = Uri.parse( 'https://maps.googleapis.com/maps/api/place/textsearch/json' '?query=${Uri.encodeComponent('$location attractions monuments')}' '&type=$type' '&fields=place_id,name,types,rating' '&key=$_apiKey' ); final response = await http.get(url); if (response.statusCode == 200) { final data = json.decode(response.body); if (data['status'] == 'OK' && data['results'].isNotEmpty) { // Prioriser les résultats avec des ratings élevés final results = data['results'] as List; results.sort((a, b) { final aRating = a['rating'] ?? 0.0; final bRating = b['rating'] ?? 0.0; return bRating.compareTo(aRating); }); return results.first['place_id']; } } return null; } catch (e) { return null; } } /// Recherche générale de lieu Future _searchPlaceGeneral(String location) async { try { final url = Uri.parse( 'https://maps.googleapis.com/maps/api/place/findplacefromtext/json' '?input=${Uri.encodeComponent(location)}' '&inputtype=textquery' '&fields=place_id' '&key=$_apiKey' ); final response = await http.get(url); if (response.statusCode == 200) { final data = json.decode(response.body); if (data['status'] == 'OK' && data['candidates'].isNotEmpty) { return data['candidates'][0]['place_id']; } } return null; } catch (e) { return null; } } /// Récupère la référence photo du lieu Future _getPhotoReference(String placeId) async { try { final url = Uri.parse( 'https://maps.googleapis.com/maps/api/place/details/json' '?place_id=$placeId' '&fields=photos' '&key=$_apiKey' ); final response = await http.get(url); if (response.statusCode == 200) { final data = json.decode(response.body); if (data['status'] == 'OK' && data['result'] != null && data['result']['photos'] != null && data['result']['photos'].isNotEmpty) { final photos = data['result']['photos'] as List; // Trier les photos pour obtenir les meilleures final sortedPhotos = _sortPhotosByQuality(photos); if (sortedPhotos.isNotEmpty) { return sortedPhotos.first['photo_reference']; } } } return null; } catch (e) { _errorService.logError('PlaceImageService', 'Erreur lors de la récupération de la référence photo: $e'); return null; } } /// Trie les photos par qualité (largeur/hauteur et popularité) List> _sortPhotosByQuality(List photos) { final photoList = photos.cast>(); photoList.sort((a, b) { // Priorité 1: Photos horizontales (largeur > hauteur) final aWidth = a['width'] ?? 0; final aHeight = a['height'] ?? 0; final bWidth = b['width'] ?? 0; final bHeight = b['height'] ?? 0; final aIsHorizontal = aWidth > aHeight; final bIsHorizontal = bWidth > bHeight; if (aIsHorizontal && !bIsHorizontal) return -1; if (!aIsHorizontal && bIsHorizontal) return 1; // Priorité 2: Résolution plus élevée final aResolution = aWidth * aHeight; final bResolution = bWidth * bHeight; if (aResolution != bResolution) { return bResolution.compareTo(aResolution); } // Priorité 3: Ratio d'aspect optimal pour paysage (1.5-2.0) final aRatio = aWidth > 0 ? aWidth / aHeight : 0; final bRatio = bWidth > 0 ? bWidth / bHeight : 0; final idealRatio = 1.7; // Ratio 16:9 environ final aDiff = (aRatio - idealRatio).abs(); final bDiff = (bRatio - idealRatio).abs(); return aDiff.compareTo(bDiff); }); return photoList; } /// Télécharge l'image et la sauvegarde dans Firebase Storage Future _downloadAndSaveImage(String photoReference, String location) async { try { // URL pour télécharger l'image en haute qualité et format horizontal final imageUrl = 'https://maps.googleapis.com/maps/api/place/photo' '?maxwidth=1200' // Augmenté pour une meilleure qualité '&maxheight=800' // Ratio horizontal ~1.5:1 '&photo_reference=$photoReference' '&key=$_apiKey'; // Télécharger l'image final response = await http.get(Uri.parse(imageUrl)); if (response.statusCode == 200) { // Créer un nom de fichier unique basé sur la localisation normalisée final normalizedLocation = _normalizeLocationName(location); final fileName = '${normalizedLocation}_${DateTime.now().millisecondsSinceEpoch}.jpg'; // Référence vers Firebase Storage final storageRef = _storage.ref().child('trip_images/$fileName'); // Upload de l'image avec métadonnées final uploadTask = await storageRef.putData( response.bodyBytes, SettableMetadata( contentType: 'image/jpeg', customMetadata: { 'location': location, 'normalizedLocation': normalizedLocation, 'source': 'google_places', 'uploadedAt': DateTime.now().toIso8601String(), }, ), ); // Récupérer l'URL de téléchargement final downloadUrl = await uploadTask.ref.getDownloadURL(); return downloadUrl; } else { } return null; } catch (e) { _errorService.logError('PlaceImageService', 'Erreur lors du téléchargement/sauvegarde: $e'); return null; } } /// Vérifie si une image existe déjà pour cette location Future _checkExistingImage(String location) async { try { final normalizedLocation = _normalizeLocationName(location); final listResult = await _storage.ref('trip_images').listAll(); for (final item in listResult.items) { try { final metadata = await item.getMetadata(); final storedNormalizedLocation = metadata.customMetadata?['normalizedLocation']; final storedLocation = metadata.customMetadata?['location']; // Méthode 1: Vérifier avec la location normalisée (nouvelles images) if (storedNormalizedLocation != null && storedNormalizedLocation == normalizedLocation) { final url = await item.getDownloadURL(); return url; } // Méthode 2: Vérifier avec la location originale normalisée (anciennes images) if (storedLocation != null) { final storedLocationNormalized = _normalizeLocationName(storedLocation); if (storedLocationNormalized == normalizedLocation) { final url = await item.getDownloadURL(); return url; } } } catch (e) { // Méthode 3: Essayer de deviner depuis le nom du fichier (fallback) final fileName = item.name; if (fileName.toLowerCase().contains(normalizedLocation.toLowerCase())) { try { final url = await item.getDownloadURL(); return url; } catch (urlError) { _errorService.logError('PlaceImageService', 'Erreur lors de la récupération de l\'URL: $urlError'); } } } } return null; } catch (e) { return null; } } /// Normalise le nom de location pour la comparaison String _normalizeLocationName(String location) { return location .toLowerCase() .replaceAll(RegExp(r'[^a-z0-9]'), '_') .replaceAll(RegExp(r'_+'), '_') .replaceAll(RegExp(r'^_|_$'), ''); } /// Nettoie les images inutilisées (à appeler manuellement si nécessaire) Future cleanupUnusedImages(List usedImageUrls) async { try { final listResult = await _storage.ref('trip_images').listAll(); int deletedCount = 0; for (final item in listResult.items) { try { final url = await item.getDownloadURL(); if (!usedImageUrls.contains(url)) { await item.delete(); deletedCount++; } } catch (e) { _errorService.logError('PlaceImageService', 'Erreur lors du nettoyage: $e'); } } } catch (e) { _errorService.logError('PlaceImageService', 'Erreur lors du nettoyage: $e'); } } /// Nettoie spécifiquement les doublons d'images pour la même location Future cleanupDuplicateImages() async { try { final listResult = await _storage.ref('trip_images').listAll(); // Grouper les images par location normalisée final Map> locationGroups = {}; for (final item in listResult.items) { String locationKey = 'unknown'; try { final metadata = await item.getMetadata(); final storedNormalizedLocation = metadata.customMetadata?['normalizedLocation']; final storedLocation = metadata.customMetadata?['location']; if (storedNormalizedLocation != null) { locationKey = storedNormalizedLocation; } else if (storedLocation != null) { locationKey = _normalizeLocationName(storedLocation); } else { // Deviner depuis le nom du fichier final fileName = item.name; final parts = fileName.split('_'); if (parts.length >= 2) { locationKey = parts.take(parts.length - 1).join('_'); } } } catch (e) { // Utiliser le nom du fichier comme fallback final fileName = item.name; final parts = fileName.split('_'); if (parts.length >= 2) { locationKey = parts.take(parts.length - 1).join('_'); } } if (!locationGroups.containsKey(locationKey)) { locationGroups[locationKey] = []; } locationGroups[locationKey]!.add(item); } // Supprimer les doublons (garder le plus récent) int deletedCount = 0; for (final entry in locationGroups.entries) { final location = entry.key; final images = entry.value; if (images.length > 1) { // Trier par timestamp (garder le plus récent) images.sort((a, b) { final aTimestamp = _extractTimestampFromName(a.name); final bTimestamp = _extractTimestampFromName(b.name); return bTimestamp.compareTo(aTimestamp); // Plus récent en premier }); // Supprimer tous sauf le premier (plus récent) for (int i = 1; i < images.length; i++) { try { await images[i].delete(); deletedCount++; } catch (e) { _errorService.logError('PlaceImageService', 'Erreur lors de la suppression du doublon: $e'); } } } } } catch (e) { _errorService.logError('PlaceImageService', 'Erreur lors du nettoyage des doublons: $e'); } } /// Extrait le timestamp du nom de fichier int _extractTimestampFromName(String fileName) { final parts = fileName.split('_'); if (parts.isNotEmpty) { final lastPart = parts.last.split('.').first; // Enlever l'extension return int.tryParse(lastPart) ?? 0; } return 0; } /// Récupère uniquement l'URL d'une image existante dans le Storage (sans télécharger) Future getExistingImageUrl(String location) async { try { final existingUrl = await _checkExistingImage(location); if (existingUrl != null) { return existingUrl; } return null; } catch (e) { _errorService.logError('PlaceImageService', 'Erreur lors de la recherche d\'image existante: $e'); return null; } } /// Supprimer une image du storage (optionnel, pour le nettoyage) Future deleteImage(String imageUrl) async { try { final ref = _storage.refFromURL(imageUrl); await ref.delete(); } catch (e) { _errorService.logError('PlaceImageService', 'Erreur lors de la suppression de l\'image: $e'); } } }