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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
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)
{
using var activity = Telemetry.Source.StartActivity("GeoapifyAutocomplete");
activity?.SetTag("query", query);
if (string.IsNullOrWhiteSpace(query))
{
return [];
}
var cacheKey = $"nominatim_autocomplete_{query.ToLowerInvariant()}";
var cacheHit = _cache.TryGetValue(cacheKey, out List<PlannerSearchResult>? cachedResults);
activity?.SetTag("cache.hit", cacheHit);
if (cacheHit && 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() ?? [];
activity?.SetTag("results.count", results.Count);
_cache.Set(cacheKey, results, TimeSpan.FromMinutes(60));
return results;
}
catch (Exception ex)
{
activity?.SetStatus(System.Diagnostics.ActivityStatusCode.Error, ex.Message);
_logger.LogError(ex, "Error fetching Geoapify autocomplete results from {Url}", url);
return new List<PlannerSearchResult>();
}
}
public async Task<PlannerSearchResult?> GetReverseGeocodeAsync(double lat, double lon)
{
using var activity = Telemetry.Source.StartActivity("GeoapifyReverseGeocode");
activity?.SetTag("lat", lat);
activity?.SetTag("lon", lon);
var cacheKey = $"nominatim_reverse_{lat:F5}_{lon:F5}";
var cacheHit = _cache.TryGetValue(cacheKey, out PlannerSearchResult? cachedResult);
activity?.SetTag("cache.hit", cacheHit);
if (cacheHit && 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)
{
activity?.SetStatus(System.Diagnostics.ActivityStatusCode.Error, ex.Message);
_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
};
}
}
|