aboutsummaryrefslogtreecommitdiff
path: root/src/Costasdev.Busurbano.Backend/Services/NominatimGeocodingService.cs
blob: 01e57f1b5608db32dca3fd8fb6f9ac3310247bb5 (plain)
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 Costasdev.Busurbano.Backend.Configuration;
using Costasdev.Busurbano.Backend.Types.Nominatim;
using Costasdev.Busurbano.Backend.Types.Planner;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;

namespace Costasdev.Busurbano.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", "Busurbano/1.0 (https://github.com/arielcostas/Busurbano)");
        }
    }

    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
        };
    }
}