aboutsummaryrefslogtreecommitdiff
path: root/src/Enmarcha.Backend/Services/Geocoding/GeoapifyGeocodingService.cs
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2026-01-02 01:08:41 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2026-01-02 01:08:41 +0100
commita3eb2d0441ae18f75604a4bee64db18391469837 (patch)
tree8994c1987afdd9436ba0699236439d3eb6c3f04d /src/Enmarcha.Backend/Services/Geocoding/GeoapifyGeocodingService.cs
parentdd544d713a2af4713c61ae0d2050f2861cc0892a (diff)
feat: Integrate Geoapify geocoding service and update configuration
Diffstat (limited to 'src/Enmarcha.Backend/Services/Geocoding/GeoapifyGeocodingService.cs')
-rw-r--r--src/Enmarcha.Backend/Services/Geocoding/GeoapifyGeocodingService.cs110
1 files changed, 110 insertions, 0 deletions
diff --git a/src/Enmarcha.Backend/Services/Geocoding/GeoapifyGeocodingService.cs b/src/Enmarcha.Backend/Services/Geocoding/GeoapifyGeocodingService.cs
new file mode 100644
index 0000000..d6cf5f6
--- /dev/null
+++ b/src/Enmarcha.Backend/Services/Geocoding/GeoapifyGeocodingService.cs
@@ -0,0 +1,110 @@
+using System.Globalization;
+using Enmarcha.Backend.Configuration;
+using Enmarcha.Backend.Types.Geoapify;
+using Enmarcha.Backend.Types.Planner;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
+
+namespace Enmarcha.Backend.Services.Geocoding;
+
+public class GeoapifyGeocodingService : IGeocodingService
+{
+ private readonly HttpClient _httpClient;
+ private readonly IMemoryCache _cache;
+ private readonly ILogger<GeoapifyGeocodingService> _logger;
+ private readonly AppConfiguration _config;
+
+ private static readonly string[] ForbiddenResultTypes = ["city", "state", "county", "postcode"];
+
+ public GeoapifyGeocodingService(HttpClient httpClient, IMemoryCache cache, ILogger<GeoapifyGeocodingService> logger, IOptions<AppConfiguration> config)
+ {
+ _httpClient = httpClient;
+ _cache = cache;
+ _logger = logger;
+ _config = config.Value;
+
+ // Geoapify requires a User-Agent
+ if (!_httpClient.DefaultRequestHeaders.Contains("User-Agent"))
+ {
+ _httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Compatible; Enmarcha/0.1; https://enmarcha.app; ariel@costas.dev)");
+ }
+ }
+
+ public async Task<List<PlannerSearchResult>> GetAutocompleteAsync(string query)
+ {
+ if (string.IsNullOrWhiteSpace(query))
+ {
+ return [];
+ }
+
+ var cacheKey = $"nominatim_autocomplete_{query.ToLowerInvariant()}";
+ if (_cache.TryGetValue(cacheKey, out List<PlannerSearchResult>? cachedResults) && cachedResults != null)
+ {
+ return cachedResults;
+ }
+
+ var url = $"https://api.geoapify.com/v1/geocode/autocomplete?text={Uri.EscapeDataString(query)}&lang=gl&limit=5&filter=rect:-9.449497230816405,41.89720361654395,-6.581039728137625,43.92616367306067&format=json";
+
+ try
+ {
+ var response = await _httpClient.GetFromJsonAsync<GeoapifyResult>(url + $"&apiKey={_config.GeoapifyApiKey}");
+
+
+ var results = response?.results
+ .Where(x => !ForbiddenResultTypes.Contains(x.result_type))
+ .Select(MapToPlannerSearchResult)
+ .ToList() ?? [];
+
+ _cache.Set(cacheKey, results, TimeSpan.FromMinutes(60));
+ return results;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error fetching Geoapify autocomplete results from {Url}", url);
+ return new List<PlannerSearchResult>();
+ }
+ }
+
+ public async Task<PlannerSearchResult?> GetReverseGeocodeAsync(double lat, double lon)
+ {
+ var cacheKey = $"nominatim_reverse_{lat:F5}_{lon:F5}";
+ if (_cache.TryGetValue(cacheKey, out PlannerSearchResult? cachedResult) && cachedResult != null)
+ {
+ return cachedResult;
+ }
+
+ var url =
+ $"https://api.geoapify.com/v1/geocode/reverse?lat={lat.ToString(CultureInfo.InvariantCulture)}&lon={lon.ToString(CultureInfo.InvariantCulture)}&lang=gl&format=json";
+ try
+ {
+ var response = await _httpClient.GetFromJsonAsync<GeoapifyResult>(url + $"&apiKey={_config.GeoapifyApiKey}");
+
+ if (response == null) return null;
+
+ var result = MapToPlannerSearchResult(response.results[0]);
+
+ _cache.Set(cacheKey, result, TimeSpan.FromMinutes(60));
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error fetching Geoapify reverse geocode results from {Url}", url);
+ return null;
+ }
+ }
+
+ private PlannerSearchResult MapToPlannerSearchResult(Result result)
+ {
+ var name = result.name ?? result.address_line1;
+ var label = $"{result.street} ({result.postcode} {result.city}, {result.county})";
+
+ return new PlannerSearchResult
+ {
+ Name = name,
+ Label = label,
+ Lat = result.lat,
+ Lon = result.lon,
+ Layer = result.result_type
+ };
+ }
+}