588 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using gehGassi.Core.Interfaces;
using gehGassi.Domain.Common;
namespace gehGassi.External.Services
{
/// <summary>
/// Service der zu einer Adresse Längen- und Breitengrad ermittelt, oder zu Längen- und Breitengrad eine Adresse
/// </summary>
public class GeoLocationService : IGeoLocationService
{
private readonly HttpClient _httpClient;
/// <summary>
/// Erstellt eine Instanz
/// </summary>
/// <param name="httpClient">Instanz eines HttpClient</param>
public GeoLocationService(HttpClient httpClient)
{
_httpClient = httpClient;
}
/// <summary>
/// Längen und Breitengrad zu einer Adresse suchen.
/// Zuerst wird versucht die Position mit Strasse, PLZ, Ort und Land zu finden.
/// Wenn das nicht klappt, dann mit PLZ, Ort, Land.
/// Wenn das nicht klappt, dann PLZ Land.
/// </summary>
/// <param name="address">Adresse</param>
/// <param name="language">Gewünschte Sprache ISO2</param>
/// <returns>Geo-Position</returns>
public async Task<GeoLocation> GetLocationAsync(Address address, string language)
{
var result = new GeoLocation()
{
Latitude = 0,
Longitude = 0,
Success = false,
ErrorMessage = "No matching address was found."
};
if (address == null)
{
result.ErrorMessage = "No address provided.";
return result;
}
if (string.IsNullOrEmpty(address.CountryCode))
{
result.ErrorMessage = "No country was specified.";
return result;
}
if (string.IsNullOrEmpty(address.State))
{
result.ErrorMessage = "No state code was specified.";
return result;
}
var innerAddress = new Address
{
AddressLine1 = address.AddressLine1,
AddressLine2 = address.AddressLine2,
Zip = address.Zip,
City = address.City,
State = address.State,
CountryCode = address.CountryCode
};
try
{
_httpClient.DefaultRequestHeaders.AcceptLanguage.Clear();
_httpClient.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue(language));
List<ApiGeoResult> locations = new List<ApiGeoResult>();
var queryLine = GenerateQueryLine(innerAddress);
var query = WebUtility.UrlEncode(queryLine);
var apiResult = await _httpClient.GetAsync($"search?q={query}&format=jsonv2&limit=1&addressdetails=1");
if (apiResult.IsSuccessStatusCode)
{
locations = await apiResult.Content.ReadFromJsonAsync<List<ApiGeoResult>>();
}
if (locations == null || locations.Count == 0)
{
await Task.Delay(1000);
innerAddress.AddressLine1 = string.Empty;
queryLine = GenerateQueryLine(innerAddress);
query = WebUtility.UrlEncode(queryLine);
apiResult = await _httpClient.GetAsync($"search?q={query}&format=jsonv2&limit=1&addressdetails=1");
if (apiResult.IsSuccessStatusCode)
{
locations = await apiResult.Content.ReadFromJsonAsync<List<ApiGeoResult>>();
}
}
if (locations == null || locations.Count == 0)
{
await Task.Delay(1000);
innerAddress.City = string.Empty;
queryLine = GenerateQueryLine(innerAddress);
query = WebUtility.UrlEncode(queryLine);
apiResult = await _httpClient.GetAsync($"search?q={query}&format=jsonv2&limit=1&addressdetails=1");
if (apiResult.IsSuccessStatusCode)
{
locations = await apiResult.Content.ReadFromJsonAsync<List<ApiGeoResult>>();
}
}
if (locations == null || locations.Count == 0)
{
await Task.Delay(1000);
innerAddress.Zip = string.Empty;
queryLine = GenerateQueryLine(innerAddress);
query = WebUtility.UrlEncode(queryLine);
apiResult = await _httpClient.GetAsync($"search?q={query}&format=jsonv2&limit=1&addressdetails=1");
if (apiResult.IsSuccessStatusCode)
{
locations = await apiResult.Content.ReadFromJsonAsync<List<ApiGeoResult>>();
}
}
if (locations != null && locations.Count > 0)
{
if (locations is { Count: 1 })
{
result.Success = true;
result.ErrorMessage = string.Empty;
result.Latitude = double.Parse(locations[0].Latitude, CultureInfo.InvariantCulture);
result.Longitude = double.Parse(locations[0].Longitude, CultureInfo.InvariantCulture);
}
else
{
result.Success = false;
result.ErrorMessage = "No matching address was found.";
}
}
else
{
result.Success = false;
result.ErrorMessage = "Error in communication with geo service. Code: " + apiResult.StatusCode;
}
}
catch (Exception ex)
{
result.Success = false;
result.ErrorMessage = ex.Message;
}
return result;
}
/// <summary>
/// Längen und Breitengrad zu einer Adresse suchen.
/// </summary>
/// <param name="address">Adresse als Text-Zeile</param>
/// <param name="language">Gewünschte Sprache ISO2</param>
/// <returns>Geo-Position</returns>
public async Task<GeoLocation> GetLocationAsync(string address, string language)
{
var result = new GeoLocation()
{
Latitude = 0,
Longitude = 0,
Success = false,
ErrorMessage = "No matching address was found."
};
if (string.IsNullOrWhiteSpace(address))
{
result.ErrorMessage = "No address provided.";
return result;
}
try
{
_httpClient.DefaultRequestHeaders.AcceptLanguage.Clear();
_httpClient.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue(language));
var query = WebUtility.UrlEncode($"{address}");
var apiResult = await _httpClient.GetAsync($"search?q={query}&format=jsonv2&limit=1&addressdetails=1");
if (apiResult.IsSuccessStatusCode)
{
var locations = await apiResult.Content.ReadFromJsonAsync<List<ApiGeoResult>>();
if (locations is { Count: 1 })
{
result.Success = true;
result.ErrorMessage = string.Empty;
result.Latitude = double.Parse(locations[0].Latitude, CultureInfo.InvariantCulture);
result.Longitude = double.Parse(locations[0].Longitude, CultureInfo.InvariantCulture);
}
else
{
result.Success = false;
result.ErrorMessage = "No matching address was found.";
}
}
else
{
result.Success = false;
result.ErrorMessage = "Error in communication with geo service. Code: " + apiResult.StatusCode;
}
}
catch (Exception ex)
{
result.Success = false;
result.ErrorMessage = ex.Message;
}
return result;
}
/// <summary>
/// Längen und Breitengrad zu einer Adresse suchen. Adresse wird mit Details mitgeliefert.
/// Zuerst wird versucht die Position mit Strasse, PLZ, Ort und Land zu finden.
/// Wenn das nicht klappt, dann mit PLZ, Ort, Land.
/// Wenn das nicht klappt, dann PLZ Land.
/// </summary>
/// <param name="address">Adresse</param>
/// <param name="language">Gewünschte Sprache ISO2</param>
/// <returns>Geo-Position</returns>
public async Task<GeoLocationFill> GetLocationFillAsync(Address address, string language)
{
var result = new GeoLocationFill()
{
Latitude = 0,
Longitude = 0,
Success = false,
ErrorMessage = "No matching address was found."
};
if (address == null)
{
result.ErrorMessage = "No address provided.";
return result;
}
if (string.IsNullOrEmpty(address.CountryCode))
{
result.ErrorMessage = "No country was specified.";
return result;
}
if (string.IsNullOrEmpty(address.State))
{
result.ErrorMessage = "No state code was specified.";
return result;
}
try
{
var queryLine = GenerateQueryLine(address);
_httpClient.DefaultRequestHeaders.AcceptLanguage.Clear();
_httpClient.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue(language));
var query = WebUtility.UrlEncode(queryLine);
var apiResult = await _httpClient.GetAsync($"search?q={query}&format=jsonv2&limit=1&addressdetails=1");
if (apiResult.IsSuccessStatusCode)
{
var locations = await apiResult.Content.ReadFromJsonAsync<List<ApiGeoFillResult>>();
if (locations is { Count: 1 })
{
result.Success = true;
result.ErrorMessage = string.Empty;
result.Latitude = double.Parse(locations[0].Latitude, CultureInfo.InvariantCulture);
result.Longitude = double.Parse(locations[0].Longitude, CultureInfo.InvariantCulture);
result.Address.AddressLine1 = $"{locations[0].Address.AddressLine1} {locations[0].Address.HouseNumber}";
result.Address.AddressLine2 = string.Empty;
result.Address.City = locations[0].Address.City;
result.Address.Zip = locations[0].Address.Zip;
result.Address.State = locations[0].Address.StateCode;
result.Address.CountryCode = locations[0].Address.CountryIso;
}
else
{
result.Success = false;
result.ErrorMessage = "No matching address was found.";
}
}
else
{
result.Success = false;
result.ErrorMessage = "Error in communication with geo service. Code: " + apiResult.StatusCode;
}
}
catch (Exception ex)
{
result.Success = false;
result.ErrorMessage = ex.Message;
}
return result;
}
/// <summary>
/// Längen und Breitengrad zu einer Adresse suchen. Adresse wird mit Details mitgeliefert.
/// </summary>
/// <param name="address">Adresse als Text-Zeile</param>
/// <param name="language">Gewünschte Sprache ISO2</param>
/// <returns>Geo-Position</returns>
public async Task<GeoLocationFill> GetLocationFillAsync(string address, string language)
{
var result = new GeoLocationFill()
{
Latitude = 0,
Longitude = 0,
Success = false,
ErrorMessage = "No matching address was found."
};
if (string.IsNullOrWhiteSpace(address))
{
result.ErrorMessage = "No address provided.";
return result;
}
try
{
//Es darf immer nur alle 1 Sekunden eine Anfrage gesendet werden. Daher "emulieren" wir das hier.
await Task.Delay(1000);
_httpClient.DefaultRequestHeaders.AcceptLanguage.Clear();
_httpClient.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue(language));
var query = WebUtility.UrlEncode($"{address}");
var apiResult = await _httpClient.GetAsync($"search?q={query}&format=jsonv2&limit=1addressdetails=1");
if (apiResult.IsSuccessStatusCode)
{
var locations = await apiResult.Content.ReadFromJsonAsync<List<ApiGeoFillResult>>();
if (locations is { Count: 1 })
{
result.Success = true;
result.ErrorMessage = string.Empty;
result.Latitude = double.Parse(locations[0].Latitude, CultureInfo.InvariantCulture);
result.Longitude = double.Parse(locations[0].Longitude, CultureInfo.InvariantCulture);
result.Address.AddressLine1 = $"{locations[0].Address.AddressLine1} {locations[0].Address.HouseNumber}";
result.Address.AddressLine2 = string.Empty;
result.Address.City = locations[0].Address.City;
result.Address.Zip = locations[0].Address.Zip;
result.Address.State = locations[0].Address.StateCode;
result.Address.CountryCode = locations[0].Address.CountryIso;
}
else
{
result.Success = false;
result.ErrorMessage = "No matching address was found.";
}
}
else
{
result.Success = false;
result.ErrorMessage = "Error in communication with geo service. Code: " + apiResult.StatusCode;
}
}
catch (Exception ex)
{
result.Success = false;
result.ErrorMessage = ex.Message;
}
return result;
}
/// <summary>
/// Reverse-Geocoding
/// </summary>
/// <param name="lat">Breitengrad</param>
/// <param name="lng">Längengrad</param>
/// <param name="language">Gewünschte Sprache ISO2</param>
/// <returns>ReverseLocation</returns>
public async Task<ReverseLocation> GetReverseAsync(double lat, double lng, string language)
{
var result = new ReverseLocation()
{
Success = false,
ErrorMessage = "No matching address was found."
};
try
{
_httpClient.DefaultRequestHeaders.AcceptLanguage.Clear();
_httpClient.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue(language));
var apiResult = await _httpClient.GetAsync($"reverse?lat={lat.ToString(CultureInfo.InvariantCulture)}&lon={lng.ToString(CultureInfo.InvariantCulture)}&format=jsonv2");
if (apiResult.IsSuccessStatusCode)
{
var reverseResult = await apiResult.Content.ReadFromJsonAsync<ApiReverseResult>();
if (reverseResult != null)
{
result.Success = true;
result.ErrorMessage = string.Empty;
result.Address.AddressLine1 = $"{reverseResult.Address.AddressLine1} {reverseResult.Address.HouseNumber}";
result.Address.AddressLine2 = string.Empty;
result.Address.City = reverseResult.Address.City;
result.Address.Zip = reverseResult.Address.Zip;
result.Address.State = reverseResult.Address.StateCode;
result.Address.CountryCode = reverseResult.Address.CountryIso;
}
else
{
result.Success = false;
result.ErrorMessage = "No matching address was found.";
}
}
else
{
result.Success = false;
result.ErrorMessage = "Error in communication with geo service. Code: " + apiResult.StatusCode;
}
}
catch (Exception ex)
{
result.Success = false;
result.ErrorMessage = ex.Message;
}
return result;
}
#region Helper
/// <summary>
/// Erstellt aus einer Adresse einen Anfrage-String
/// </summary>
/// <param name="address">Adresse</param>
/// <returns>Anfrage-String</returns>
private static string GenerateQueryLine(Address address)
{
var queryLine = string.Empty;
if (!string.IsNullOrWhiteSpace(address.AddressLine1))
{
if (queryLine.Length > 0)
queryLine += ", ";
queryLine += $"{address.AddressLine1}";
}
if (!string.IsNullOrWhiteSpace(address.Zip))
{
if (queryLine.Length > 0)
queryLine += ", ";
queryLine += $"{address.Zip}";
}
if (!string.IsNullOrWhiteSpace(address.City))
{
if (queryLine.Length > 0)
queryLine += ", ";
queryLine += $"{address.City}";
}
if (queryLine.Length == 0)
queryLine += $"{address.State}";
if (queryLine.Length > 0)
queryLine += ", ";
queryLine += $"{address.CountryCode}";
return queryLine;
}
#endregion
}
/// <summary>
/// Hilfsklasse für die Zuordnung von Tags
/// </summary>
internal class ApiGeoResult
{
/// <summary>
/// Openstreetmap Platz-ID
/// </summary>
[JsonPropertyName("place_id")]
public long PlaceId { get; set; }
/// <summary>
/// Breitengrad
/// </summary>
[JsonPropertyName("lat")]
public string Latitude { get; set; }
/// <summary>
/// Längengrad
/// </summary>
[JsonPropertyName("lon")]
public string Longitude { get; set; }
/// <summary>
/// Voller Name dargestellt
/// </summary>
[JsonPropertyName("display_name")]
public string DisplayName { get; set; }
}
/// <summary>
/// Hilfsklasse für die ERgänzung einer Adresse
/// </summary>
internal class ApiGeoFillResult : ApiGeoResult
{
/// <summary>
/// Adresse
/// </summary>
[JsonPropertyName("address")]
public ApiReverseAddress Address { get; set; }
/// <summary>
/// Erstellt eine Instanz
/// </summary>
public ApiGeoFillResult()
{
Address = new ApiReverseAddress();
}
}
/// <summary>
/// Hilfsklasse für die Zuordnung von Tags
/// </summary>
internal class ApiReverseResult
{
/// <summary>
/// Openstreetmap Platz-ID
/// </summary>
[JsonPropertyName("place_id")]
public long PlaceId { get; set; }
/// <summary>
/// Adresse
/// </summary>
[JsonPropertyName("address")]
public ApiReverseAddress Address { get; set; }
/// <summary>
/// Erstellt eine Instanz
/// </summary>
public ApiReverseResult()
{
Address = new ApiReverseAddress();
}
}
/// <summary>
/// Adress-Mapping
/// </summary>
internal class ApiReverseAddress
{
[JsonPropertyName("road")]
public string AddressLine1 { get; set; }
[JsonPropertyName("house_number")]
public string HouseNumber { get; set; }
[JsonPropertyName("city")]
public string City { get; set; }
[JsonPropertyName("postcode")]
public string Zip { get; set; }
[JsonPropertyName("state")]
public string StateName { get; set; }
[JsonPropertyName("ISO3166-2-lvl4")]
public string StateCode { get; set; }
[JsonPropertyName("country")]
public string CountryName { get; set; }
[JsonPropertyName("country_code")]
public string CountryIso { get; set; }
}
}