SHA256
SomniaGuestLog - Address search/query using Nominatim - Initial implementation
Initial implementation for address search/query using external APIs (OSM Nominatim) & Added required classes for supporting more APIs in the future.
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
@page "/debug/addrSearch"
|
||||||
|
|
||||||
|
@using Somnia.GuestLog.Models.DTOs;
|
||||||
|
@using Somnia.GuestLog.Services.AddressQuery;
|
||||||
|
|
||||||
|
@inject ILogger<AddressQuery> logger;
|
||||||
|
@inject IAddressQueryFactory addressQueryFactory;
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
|
<PageTitle>DEBUG - Address Query</PageTitle>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h3" GutterBottom="true">Address Search</MudText>
|
||||||
|
|
||||||
|
<MudTextField @ref="txtSearch" @bind-Value="SearchText" Label="Search address" Required />
|
||||||
|
|
||||||
|
<MudButton Color="Color.Primary" Variant="Variant.Filled" @onclick="btnSearchAddress">Search</MudButton>
|
||||||
|
|
||||||
|
<MudPaper>
|
||||||
|
@if (addressResults.Count == 0)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body1">No results!</MudText>
|
||||||
|
}
|
||||||
|
@foreach (AddressResultDTO result in addressResults)
|
||||||
|
{
|
||||||
|
<MudItem>
|
||||||
|
<MudStack>
|
||||||
|
<MudText>Address: @result.AddressRoad , @result.HouseNumber</MudText>
|
||||||
|
<MudText>City: @result.AddressCity</MudText>
|
||||||
|
<MudText>Province: @result.AddressProvince</MudText>
|
||||||
|
<MudText>State: @result.AddressState</MudText>
|
||||||
|
<MudText>Country: @result.AddressCountry</MudText>
|
||||||
|
<MudText>ZipCode: @result.ZipCode</MudText>
|
||||||
|
</MudStack>
|
||||||
|
</MudItem>
|
||||||
|
}
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
public string SearchText { get; set; } = "";
|
||||||
|
private List<AddressResultDTO> addressResults = [];
|
||||||
|
|
||||||
|
private MudTextField<string> txtSearch = null!;
|
||||||
|
|
||||||
|
private async Task btnSearchAddress()
|
||||||
|
{
|
||||||
|
if (SearchText.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var queryService = addressQueryFactory.GetService();
|
||||||
|
|
||||||
|
addressResults = await queryService.SearchAddressAsync(SearchText);
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "SearchAddress returned an error!");
|
||||||
|
Snackbar.Add($"Error searching address: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace Somnia.GuestLog.Models.Configuration;
|
||||||
|
|
||||||
|
public class AddressQueryConfig
|
||||||
|
{
|
||||||
|
public const string SectionName = "AddressQuery";
|
||||||
|
|
||||||
|
public bool EnableAddressQuery { get; set; }
|
||||||
|
public AddressQueryProviderEnum? Provider { get; set; }
|
||||||
|
public string? ServiceURL { get; set; }
|
||||||
|
public string? ApiKey { get; set; }
|
||||||
|
public string? UserAgent { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public enum AddressQueryProviderEnum
|
||||||
|
{
|
||||||
|
Nominatim,
|
||||||
|
GoogleApi
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Somnia.GuestLog.Models.DTOs;
|
||||||
|
|
||||||
|
public class AddressResultDTO
|
||||||
|
{
|
||||||
|
public string? HouseNumber { get; set; }
|
||||||
|
public string? AddressRoad { get; set; }
|
||||||
|
public string? AddressCity { get; set; }
|
||||||
|
public string? AddressProvince { get; set; }
|
||||||
|
public string? AddressState { get; set; }
|
||||||
|
public string? AddressCountry { get; set; }
|
||||||
|
public string? ZipCode { get; set; }
|
||||||
|
}
|
||||||
@@ -4,8 +4,11 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using MudBlazor.Services;
|
using MudBlazor.Services;
|
||||||
using Somnia.GuestLog.Components;
|
using Somnia.GuestLog.Components;
|
||||||
using Somnia.GuestLog.Infrastructure;
|
using Somnia.GuestLog.Infrastructure;
|
||||||
|
using Somnia.GuestLog.Models.Configuration;
|
||||||
using Somnia.GuestLog.Models.DTOs;
|
using Somnia.GuestLog.Models.DTOs;
|
||||||
using Somnia.GuestLog.Services;
|
using Somnia.GuestLog.Services;
|
||||||
|
using Somnia.GuestLog.Services.AddressQuery;
|
||||||
|
using Somnia.GuestLog.Utils;
|
||||||
using Somnia.GuestLog.Validators;
|
using Somnia.GuestLog.Validators;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@@ -22,10 +25,13 @@ builder.Services.AddDatabaseDeveloperPageExceptionFilter();
|
|||||||
// Add MudBlazor services
|
// Add MudBlazor services
|
||||||
builder.Services.AddMudServices();
|
builder.Services.AddMudServices();
|
||||||
|
|
||||||
// Add services to the container.
|
// Add components services to the container.
|
||||||
builder.Services.AddRazorComponents()
|
builder.Services.AddRazorComponents()
|
||||||
.AddInteractiveServerComponents();
|
.AddInteractiveServerComponents();
|
||||||
|
|
||||||
|
builder.Services.AddLocalization();
|
||||||
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
// Add Validation Services
|
// Add Validation Services
|
||||||
builder.Services.AddSingleton<IValidator<GuestDocumentDTO>, GuestDocumentDTOValidator>();
|
builder.Services.AddSingleton<IValidator<GuestDocumentDTO>, GuestDocumentDTOValidator>();
|
||||||
builder.Services.AddSingleton<IValidator<GuestDocIdDTO>, GuestDocIdDTOValidator>();
|
builder.Services.AddSingleton<IValidator<GuestDocIdDTO>, GuestDocIdDTOValidator>();
|
||||||
@@ -34,7 +40,12 @@ builder.Services.AddSingleton<IValidator<GuestDocIdDTO>, GuestDocIdDTOValidator>
|
|||||||
builder.Services.AddSingleton<IGuestDocumentService, GuestDocumentService>();
|
builder.Services.AddSingleton<IGuestDocumentService, GuestDocumentService>();
|
||||||
builder.Services.AddSingleton<IDataListService, StaticDataListService>();
|
builder.Services.AddSingleton<IDataListService, StaticDataListService>();
|
||||||
|
|
||||||
builder.Services.AddLocalization();
|
// Add Factories (and it's services)
|
||||||
|
builder.Services.AddFactoryDI<IAddressQueryFactory, ConfigFileAddressQueryFactory>();
|
||||||
|
|
||||||
|
// Add Options config
|
||||||
|
builder.Services.AddOptions<AddressQueryConfig>()
|
||||||
|
.Bind(builder.Configuration.GetSection(AddressQueryConfig.SectionName));
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Somnia.GuestLog.Models.Configuration;
|
||||||
|
using Somnia.GuestLog.Utils.DI;
|
||||||
|
|
||||||
|
namespace Somnia.GuestLog.Services.AddressQuery;
|
||||||
|
|
||||||
|
public class ConfigFileAddressQueryFactory(
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
IOptions<AddressQueryConfig> config
|
||||||
|
) : IAddressQueryFactory, IFactoryDI
|
||||||
|
{
|
||||||
|
public static void RegisterFactoryItems(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddKeyedTransient<IAddressQueryService, NominatimAddressQueryService>(AddressQueryConfig.AddressQueryProviderEnum.Nominatim);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAddressQueryService GetService()
|
||||||
|
{
|
||||||
|
if (config.Value.EnableAddressQuery == false)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("AddressQuery is disabled. Enable and configure it first!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceProvider.GetKeyedService<IAddressQueryService>(config.Value.Provider)
|
||||||
|
?? throw new InvalidOperationException($"AddressQueryService not found for provider: '{config.Value.Provider}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Somnia.GuestLog.Services.AddressQuery;
|
||||||
|
|
||||||
|
public interface IAddressQueryFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the AddressQuery service implementation given current configuration.
|
||||||
|
/// </summary>
|
||||||
|
public IAddressQueryService GetService();
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using Somnia.GuestLog.Models.DTOs;
|
||||||
|
|
||||||
|
namespace Somnia.GuestLog.Services.AddressQuery;
|
||||||
|
|
||||||
|
public interface IAddressQueryService
|
||||||
|
{
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Search an address by plain text query.
|
||||||
|
/// Data is returned as a structured address.
|
||||||
|
/// This service allow to complete and validate addresses.
|
||||||
|
///
|
||||||
|
/// Note: Not all fields are always available for a given query.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">Address to search</param>
|
||||||
|
/// <returns>List of resulting structured addresses</returns>
|
||||||
|
public Task<List<AddressResultDTO>> SearchAddressAsync(string query);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Somnia.GuestLog.Models.Configuration;
|
||||||
|
using Somnia.GuestLog.Models.DTOs;
|
||||||
|
|
||||||
|
namespace Somnia.GuestLog.Services.AddressQuery;
|
||||||
|
|
||||||
|
public class NominatimAddressQueryService(
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
ILogger<NominatimAddressQueryService> logger,
|
||||||
|
IOptions<AddressQueryConfig> configOptions
|
||||||
|
) : IAddressQueryService
|
||||||
|
{
|
||||||
|
private const string defaultBaseAddr = "https://nominatim.openstreetmap.org";
|
||||||
|
private const string defaultUserAgent = "Somnia-GuestLog";
|
||||||
|
private const string apiUri = "/search";
|
||||||
|
|
||||||
|
private readonly AddressQueryConfig config = configOptions.Value;
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<List<AddressResultDTO>> SearchAddressAsync(string query)
|
||||||
|
{
|
||||||
|
var http = GetHttpClient();
|
||||||
|
List<AddressResultDTO> rsList = [];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string uri = GetRequestUri(query);
|
||||||
|
List<ApiResult> apiList = (await http.GetFromJsonAsync<List<ApiResult>>(uri)) ?? [];
|
||||||
|
|
||||||
|
foreach (ApiResult result in apiList)
|
||||||
|
{
|
||||||
|
if (result.Address is null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Skipping result without address details. '{result}'", result);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
rsList.Add(new()
|
||||||
|
{
|
||||||
|
HouseNumber = result.Address.House_number,
|
||||||
|
AddressRoad = result.Address.Road ?? result.Address.Square,
|
||||||
|
AddressCity = result.Address.City ?? result.Address.Municipality ?? result.Address.Village ?? result.Address.Village,
|
||||||
|
AddressProvince = result.Address.Province,
|
||||||
|
AddressState = result.Address.State,
|
||||||
|
AddressCountry = result.Address.Country,
|
||||||
|
ZipCode = result.Address.Postcode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return rsList;
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error getting info from API!");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private HttpClient GetHttpClient()
|
||||||
|
{
|
||||||
|
HttpClient client = httpClientFactory.CreateClient();
|
||||||
|
|
||||||
|
Uri baseAddr = new(config.ServiceURL ?? defaultBaseAddr);
|
||||||
|
|
||||||
|
client.BaseAddress = baseAddr;
|
||||||
|
client.DefaultRequestHeaders.UserAgent.ParseAdd(config.UserAgent ?? defaultUserAgent);
|
||||||
|
client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("es");
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetRequestUri(string query)
|
||||||
|
{
|
||||||
|
return QueryHelpers.AddQueryString(apiUri, [
|
||||||
|
new KeyValuePair<string, string?>("format", "jsonv2"),
|
||||||
|
new KeyValuePair<string, string?>("addressdetails", "1"),
|
||||||
|
new KeyValuePair<string, string?>("layer", "address"),
|
||||||
|
new KeyValuePair<string, string?>("q", query),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record class ApiResultAddress(
|
||||||
|
string? House_number = null,
|
||||||
|
string? Road = null,
|
||||||
|
string? Square = null,
|
||||||
|
string? City = null,
|
||||||
|
string? Municipality = null,
|
||||||
|
string? Village = null,
|
||||||
|
string? Town = null,
|
||||||
|
string? Province = null,
|
||||||
|
string? State = null,
|
||||||
|
string? Country = null,
|
||||||
|
string? Postcode = null
|
||||||
|
);
|
||||||
|
|
||||||
|
private record class ApiResult(
|
||||||
|
string? Osm_type = null,
|
||||||
|
long? Osm_id = null,
|
||||||
|
string? Category = null,
|
||||||
|
string? AddressType = null,
|
||||||
|
string? Name = null,
|
||||||
|
string? Display_name = null,
|
||||||
|
ApiResultAddress? Address = null
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -13,6 +13,4 @@ public interface IGuestDocumentService
|
|||||||
public Task<IEnumerable<GuestDocumentDTO>> GetDtoListByFilterAsync(GuestDocFilterDTO filter, int limit = 0);
|
public Task<IEnumerable<GuestDocumentDTO>> GetDtoListByFilterAsync(GuestDocFilterDTO filter, int limit = 0);
|
||||||
public Task<IEnumerable<GuestDocumentDTO>> GetDtoListAsync();
|
public Task<IEnumerable<GuestDocumentDTO>> GetDtoListAsync();
|
||||||
|
|
||||||
//TODO: Add Filter
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Somnia.GuestLog.Utils.DI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for Factories that need to register DI services.
|
||||||
|
/// <br/><see cref="ServiceCollectionExtensions.AddFactoryDI{TFactory, TFactoryImpl}(IServiceCollection)"/>
|
||||||
|
/// </summary>
|
||||||
|
public interface IFactoryDI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Register required services in the ServiceCollection.
|
||||||
|
/// This method is automaticaly call when the factory is register.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services"></param>
|
||||||
|
public abstract static void RegisterFactoryItems(IServiceCollection services);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using Somnia.GuestLog.Utils.DI;
|
||||||
|
|
||||||
|
namespace Somnia.GuestLog.Utils;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper for registering factories using IFactoryDI interface.
|
||||||
|
/// Those factories register the required services directly to the service collection.
|
||||||
|
/// This helper ensures all required methods are called.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TFactory">Factory Interface</typeparam>
|
||||||
|
/// <typeparam name="TFactoryImpl">Factory Implementation</typeparam>
|
||||||
|
public static IServiceCollection AddFactoryDI<TFactory, TFactoryImpl>(this IServiceCollection services)
|
||||||
|
where TFactory : class
|
||||||
|
where TFactoryImpl : class, TFactory, IFactoryDI
|
||||||
|
{
|
||||||
|
services.AddSingleton<TFactory, TFactoryImpl>();
|
||||||
|
TFactoryImpl.RegisterFactoryItems(services);
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -8,5 +8,12 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*",
|
||||||
|
"AddressQuery": {
|
||||||
|
"EnableAddressQuery": true,
|
||||||
|
"Provider": "Nominatim",
|
||||||
|
"ServiceURL": "https://nominatim.openstreetmap.org/",
|
||||||
|
"ApiKey": "",
|
||||||
|
"UserAgent": "Somnia-GuestLog-Dev"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user