Files
TravelMate/lib/services/place_image_service.dart
Van Leemput Dayron ca28e0a780
Some checks failed
Deploy to Play Store / build_and_deploy (push) Has been cancelled
feat: Implement platform-specific Google Maps API key handling and update app version.
2025-12-06 14:43:22 +01:00

559 lines
17 KiB
Dart

import 'dart:io';
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 String get _apiKey {
if (Platform.isAndroid) {
return dotenv.env['GOOGLE_MAPS_API_KEY_ANDROID'] ??
dotenv.env['GOOGLE_MAPS_API_KEY'] ??
'';
} else if (Platform.isIOS) {
return dotenv.env['GOOGLE_MAPS_API_KEY_IOS'] ??
dotenv.env['GOOGLE_MAPS_API_KEY'] ??
'';
}
return 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<String?> 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<String> _generateSearchTerms(String location) {
final terms = <String>[];
// 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<String> _getCitySpecificTerms(String location) {
final specific = <String>[];
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<String?> _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<String?> _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<String?> _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<String?> _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<Map<String, dynamic>> _sortPhotosByQuality(List photos) {
final photoList = photos.cast<Map<String, dynamic>>();
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<String?> _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<String?> _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<void> cleanupUnusedImages(List<String> usedImageUrls) async {
try {
final listResult = await _storage.ref('trip_images').listAll();
for (final item in listResult.items) {
try {
final url = await item.getDownloadURL();
if (!usedImageUrls.contains(url)) {
await item.delete();
}
} 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<void> cleanupDuplicateImages() async {
try {
final listResult = await _storage.ref('trip_images').listAll();
// Grouper les images par location normalisée
final Map<String, List<Reference>> 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)
for (final entry in locationGroups.entries) {
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();
} 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<String?> 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<void> 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',
);
}
}
}