1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
using System.Globalization;
using Enmarcha.Backend.Configuration;
using Enmarcha.Backend.Types.Nominatim;
using Enmarcha.Backend.Types.Planner;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace Enmarcha.Backend.Services;
public class NominatimGeocodingService : IGeocodingService
{
private readonly HttpClient _httpClient;
private readonly IMemoryCache _cache;
private readonly ILogger<NominatimGeocodingService> _logger;
private readonly AppConfiguration _config;
private const string GaliciaBounds = "-9.3,43.8,-6.7,41.7";
public NominatimGeocodingService(HttpClient httpClient, IMemoryCache cache, ILogger<NominatimGeocodingService> logger, IOptions<AppConfiguration> config)
{
_httpClient = httpClient;
_cache = cache;
_logger = logger;
_config = config.Value;
// Nominatim 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)");
}
}
public async Task<List<PlannerSearchResult>> GetAutocompleteAsync(string query)
{
if (string.IsNullOrWhiteSpace(query)) return new List<PlannerSearchResult>();
var cacheKey = $"nominatim_autocomplete_{query.ToLowerInvariant()}";
if (_cache.TryGetValue(cacheKey, out List<PlannerSearchResult>? cachedResults) && cachedResults != null)
{
return cachedResults;
}
try
{
var url = $"{_config.NominatimBaseUrl}/search?q={Uri.EscapeDataString(query)}&format=jsonv2&viewbox={GaliciaBounds}&bounded=1&countrycodes=es&addressdetails=1";
var response = await _httpClient.GetFromJsonAsync<List<NominatimSearchResult>>(url);
var results = response?.Select(MapToPlannerSearchResult).ToList() ?? new List<PlannerSearchResult>();
_cache.Set(cacheKey, results, TimeSpan.FromMinutes(30));
return results;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching Nominatim autocomplete results from {Url}", _config.NominatimBaseUrl);
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;
}
try
{
var url = $"{_config.NominatimBaseUrl}/reverse?lat={lat.ToString(CultureInfo.InvariantCulture)}&lon={lon.ToString(CultureInfo.InvariantCulture)}&format=jsonv2&addressdetails=1";
var response = await _httpClient.GetFromJsonAsync<NominatimSearchResult>(url);
if (response == null) return null;
var result = MapToPlannerSearchResult(response);
_cache.Set(cacheKey, result, TimeSpan.FromMinutes(60));
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching Nominatim reverse geocode results from {Url}", _config.NominatimBaseUrl);
return null;
}
}
private PlannerSearchResult MapToPlannerSearchResult(NominatimSearchResult result)
{
var name = result.Address?.Road ?? result.DisplayName?.Split(',').FirstOrDefault();
var label = result.DisplayName;
return new PlannerSearchResult
{
Name = name,
Label = label,
Lat = double.TryParse(result.Lat, CultureInfo.InvariantCulture, out var lat) ? lat : 0,
Lon = double.TryParse(result.Lon, CultureInfo.InvariantCulture, out var lon) ? lon : 0,
Layer = result.Type
};
}
}
|