using GoogleApi; using GoogleApi.Entities.Maps.Geocoding.Address.Request; using GoogleApi.Entities.Maps.Geocoding.Location.Request; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Svrnty.GeoManagement.Abstractions.Abstractions; using Svrnty.GeoManagement.Abstractions.Models; using Svrnty.GeoManagement.Google.Configuration; using Svrnty.GeoManagement.Google.Mapping; namespace Svrnty.GeoManagement.Google; /// /// Google Geocoding API implementation of IGeoManagementProvider /// public class GeoManagementGoogleProvider : IGeoManagementProvider { private readonly GoogleGeoManagementOptions _options; private readonly ILogger _logger; public GeoManagementGoogleProvider( IOptions options, ILogger logger) { _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); if (string.IsNullOrWhiteSpace(_options.ApiKey)) { throw new ArgumentException("Google API key is required", nameof(options)); } } public async Task GetGeoPointAsync( Address address, CancellationToken cancellationToken = default) { try { var addressString = address.GetFormattedAddress(); _logger.LogDebug("Geocoding address: {Address}", addressString); var request = new AddressGeocodeRequest { Key = _options.ApiKey, Address = addressString }; if (!string.IsNullOrWhiteSpace(_options.Language) && Enum.TryParse(_options.Language, true, out var language)) request.Language = language; if (!string.IsNullOrWhiteSpace(_options.Region)) request.Region = _options.Region; var response = await GoogleMaps.Geocode.AddressGeocode.QueryAsync(request, cancellationToken); if (response.Status != GoogleApi.Entities.Common.Enums.Status.Ok) { _logger.LogWarning("Google Geocoding API returned status: {Status}", response.Status); return null; } var result = response.Results?.FirstOrDefault(); var geoPoint = GoogleAddressMapper.MapToGeoPoint(result); if (geoPoint != null) { _logger.LogDebug("Successfully geocoded address to: {Latitude}, {Longitude}", geoPoint.Latitude, geoPoint.Longitude); } return geoPoint; } catch (Exception ex) { _logger.LogError(ex, "Error geocoding address: {Address}", address.GetFormattedAddress()); return null; } } public async Task ReverseGeocodeAsync( GeoPoint geoPoint, CancellationToken cancellationToken = default) { try { _logger.LogDebug("Reverse geocoding location: {Latitude}, {Longitude}", geoPoint.Latitude, geoPoint.Longitude); var request = new LocationGeocodeRequest { Key = _options.ApiKey, Location = new GoogleApi.Entities.Common.Coordinate(geoPoint.Latitude, geoPoint.Longitude) }; if (!string.IsNullOrWhiteSpace(_options.Language) && Enum.TryParse(_options.Language, true, out var language)) request.Language = language; var response = await GoogleMaps.Geocode.LocationGeocode.QueryAsync(request, cancellationToken); if (response.Status != GoogleApi.Entities.Common.Enums.Status.Ok) { _logger.LogWarning("Google Reverse Geocoding API returned status: {Status}", response.Status); return null; } var result = response.Results?.FirstOrDefault(); var address = GoogleAddressMapper.MapToAddress(result); if (address != null) { _logger.LogDebug("Successfully reverse geocoded to: {Address}", address.GetFormattedAddress()); } return address; } catch (Exception ex) { _logger.LogError(ex, "Error reverse geocoding location: {Latitude}, {Longitude}", geoPoint.Latitude, geoPoint.Longitude); return null; } } public async Task NormalizeAddressAsync( Address address, CancellationToken cancellationToken = default) { try { _logger.LogDebug("Normalizing address: {Address}", address.GetFormattedAddress()); // First geocode to get coordinates var geoPoint = await GetGeoPointAsync(address, cancellationToken); if (geoPoint == null) { _logger.LogWarning("Could not geocode address for normalization: {Address}", address.GetFormattedAddress()); return null; } // Then reverse geocode to get normalized address var normalizedAddress = await ReverseGeocodeAsync(geoPoint, cancellationToken); if (normalizedAddress != null) { _logger.LogDebug("Successfully normalized address from {Original} to {Normalized}", address.GetFormattedAddress(), normalizedAddress.GetFormattedAddress()); } return normalizedAddress; } catch (Exception ex) { _logger.LogError(ex, "Error normalizing address: {Address}", address.GetFormattedAddress()); return null; } } public async Task NormalizeAddressAsync( string address, CancellationToken cancellationToken = default) { try { _logger.LogDebug("Normalizing address string: {Address}", address); var request = new AddressGeocodeRequest { Key = _options.ApiKey, Address = address }; if (!string.IsNullOrWhiteSpace(_options.Language) && Enum.TryParse(_options.Language, true, out var language)) request.Language = language; if (!string.IsNullOrWhiteSpace(_options.Region)) request.Region = _options.Region; var response = await GoogleMaps.Geocode.AddressGeocode.QueryAsync(request, cancellationToken); if (response.Status != GoogleApi.Entities.Common.Enums.Status.Ok) { _logger.LogWarning("Google Geocoding API returned status: {Status} for address: {Address}", response.Status, address); return null; } var result = response.Results?.FirstOrDefault(); var normalizedAddress = GoogleAddressMapper.MapToAddress(result); if (normalizedAddress != null) { _logger.LogDebug("Successfully normalized address string to: {Normalized}", normalizedAddress.GetFormattedAddress()); } return normalizedAddress; } catch (Exception ex) { _logger.LogError(ex, "Error normalizing address string: {Address}", address); return null; } } }