658 lines
25 KiB
C#
658 lines
25 KiB
C#
using LSFE.MobApp.DTO;
|
||
using Microsoft.Extensions.Configuration;
|
||
using Microsoft.Extensions.Logging;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using System.Text.Json;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace LSFE.MobApp.Services
|
||
{
|
||
public class AddressService
|
||
{
|
||
private readonly ILogger<AddressService> _logger;
|
||
private readonly Repository<Entities.AddressCache> _addressCache;
|
||
|
||
// Known location overrides for areas with incorrect OSM data
|
||
private static readonly List<LocationOverride> _locationOverrides = new()
|
||
{
|
||
// Lloyd Laboratories - FBIC area (Tikay, not Sumapang Matanda)
|
||
new LocationOverride
|
||
{
|
||
Name = "First Bulacan Industrial City (FBIC)",
|
||
MinLat = 14.835, MaxLat = 14.842,
|
||
MinLng = 120.850, MaxLng = 120.860,
|
||
Street = "Lloyd Avenue",
|
||
Subdivision = "First Bulacan Industrial City",
|
||
Barangay = "Tikay",
|
||
City = "Malolos",
|
||
Province = "Bulacan",
|
||
Postcode = "3000"
|
||
},
|
||
// Add more known areas here as needed
|
||
};
|
||
|
||
public AddressService(ILogger<AddressService> logger, Repository<Entities.AddressCache> addressCache)
|
||
{
|
||
_logger = logger;
|
||
_addressCache = addressCache;
|
||
}
|
||
|
||
public async Task<(LocationResult result, bool isHumanReadable)> GetCurrentLocationAsync()
|
||
{
|
||
await EnsurePermissionsAsync();
|
||
|
||
var location =
|
||
await Geolocation.Default.GetLastKnownLocationAsync()
|
||
?? await Geolocation.Default.GetLocationAsync(
|
||
new GeolocationRequest(
|
||
GeolocationAccuracy.High,
|
||
TimeSpan.FromSeconds(30)));
|
||
|
||
if (location == null)
|
||
throw new Exception("Unable to get location.");
|
||
|
||
double lat = location.Latitude;
|
||
double lng = location.Longitude;
|
||
|
||
var (address, isHumanReadable) = await GetAddressAsync(lat, lng);
|
||
|
||
return (new LocationResult
|
||
{
|
||
Latitude = lat,
|
||
Longitude = lng,
|
||
Address = address
|
||
}, isHumanReadable);
|
||
}
|
||
|
||
// ================== PERMISSIONS ==================
|
||
|
||
private async Task EnsurePermissionsAsync()
|
||
{
|
||
var status = await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>();
|
||
|
||
if (status != PermissionStatus.Granted)
|
||
status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();
|
||
|
||
if (status != PermissionStatus.Granted)
|
||
throw new Exception("Location permission is required.");
|
||
}
|
||
|
||
// ================== RETRY GEOCODING ==================
|
||
|
||
/// <summary>
|
||
/// Retries geocoding for a coordinate-only address when internet becomes available.
|
||
/// Use this to convert "Lat: X, Lng: Y" back to human-readable addresses.
|
||
/// </summary>
|
||
/// <param name="lat">Latitude</param>
|
||
/// <param name="lng">Longitude</param>
|
||
/// <returns>Tuple of (address, isHumanReadable)</returns>
|
||
public async Task<(string address, bool isHumanReadable)> RetryGeocodingAsync(double lat, double lng)
|
||
{
|
||
_logger.LogInformation($"Retrying geocoding for coordinates: {lat}, {lng}");
|
||
|
||
// Clear cache for this location to force fresh geocoding attempt
|
||
await ClearCacheForLocationAsync(lat, lng);
|
||
|
||
// Try to geocode again
|
||
return await GetAddressAsync(lat, lng);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Clears cached address for a specific location
|
||
/// </summary>
|
||
private async Task ClearCacheForLocationAsync(double lat, double lng)
|
||
{
|
||
const double radius = 0.0002; // ~20 meters
|
||
double latMin = lat - radius;
|
||
double latMax = lat + radius;
|
||
double lngMin = lng - radius;
|
||
double lngMax = lng + radius;
|
||
|
||
var cachedEntries = await _addressCache.GetAllAsync();
|
||
cachedEntries.Where(x =>
|
||
x.Latitude >= latMin &&
|
||
x.Latitude <= latMax &&
|
||
x.Longitude >= lngMin &&
|
||
x.Longitude <= lngMax
|
||
);
|
||
|
||
foreach (var entry in cachedEntries)
|
||
{
|
||
await _addressCache.DeleteAsync(entry);
|
||
}
|
||
}
|
||
|
||
// ================== ADDRESS WITH OVERRIDE ==================
|
||
|
||
private async Task<Entities.AddressCache?> GetNearbyAsync(double lat, double lng)
|
||
{
|
||
const double radius = 0.0025; // mobile-safe radius
|
||
var cutoff = DateTime.UtcNow.AddHours(-24);
|
||
|
||
double latMin = lat - radius;
|
||
double latMax = lat + radius;
|
||
double lngMin = lng - radius;
|
||
double lngMax = lng + radius;
|
||
|
||
var data = await _addressCache.GetAllAsync();
|
||
return await _addressCache.FirstOrDefaultAsync(x =>
|
||
x.Latitude >= latMin &&
|
||
x.Latitude <= latMax &&
|
||
x.Longitude >= lngMin &&
|
||
x.Longitude <= lngMax &&
|
||
x.CachedAt >= cutoff
|
||
);
|
||
}
|
||
|
||
public async Task<(string address, bool isHumanReadable)> GetAddressAsync(double lat, double lng)
|
||
{
|
||
// 1️⃣ Check cache first
|
||
var cached = await GetNearbyAsync(lat, lng);
|
||
if (cached != null)
|
||
return (cached.Address, IsAddressHumanReadable(cached.Address));
|
||
|
||
// 2️⃣ Check for location override (for areas with known bad OSM data)
|
||
var locationOverride = GetLocationOverride(lat, lng);
|
||
|
||
// 3️⃣ Get address from geocoding APIs
|
||
var geoAddress = await GetGeocodedAddressAsync(lat, lng);
|
||
|
||
// 4️⃣ Apply override corrections if found
|
||
string finalAddress;
|
||
bool isHumanReadable;
|
||
|
||
if (locationOverride != null)
|
||
{
|
||
finalAddress = ApplyLocationOverride(geoAddress, locationOverride);
|
||
isHumanReadable = true; // Override always provides human-readable address
|
||
_logger.LogInformation($"Applied location override for {locationOverride.Name}");
|
||
}
|
||
else if (!string.IsNullOrWhiteSpace(geoAddress))
|
||
{
|
||
// 5️⃣ Verify and enhance with nearby POI search
|
||
finalAddress = await EnhanceWithNearbyPOI(lat, lng, geoAddress);
|
||
isHumanReadable = true; // Got address from geocoding API
|
||
}
|
||
else
|
||
{
|
||
// 6️⃣ No internet or geocoding failed - return coordinates
|
||
finalAddress = $"Lat: {lat:F6}, Lng: {lng:F6}";
|
||
isHumanReadable = false;
|
||
_logger.LogWarning($"Failed to geocode address, returning coordinates: {finalAddress}");
|
||
}
|
||
|
||
// Save to cache (even if it's just coordinates)
|
||
await SaveCache(lat, lng, finalAddress);
|
||
|
||
return (finalAddress, isHumanReadable);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Checks if an address is human-readable or just coordinates
|
||
/// </summary>
|
||
private bool IsAddressHumanReadable(string address)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(address))
|
||
return false;
|
||
|
||
// If address starts with "Lat:" or contains only coordinates format, it's not human-readable
|
||
if (address.StartsWith("Lat:", StringComparison.OrdinalIgnoreCase) ||
|
||
address.StartsWith("Latitude:", StringComparison.OrdinalIgnoreCase))
|
||
return false;
|
||
|
||
// Check if it's just a coordinate pattern (numbers, dots, commas, spaces)
|
||
var cleanAddress = address.Replace("Lat:", "").Replace("Lng:", "").Replace("Long:", "")
|
||
.Replace("Latitude:", "").Replace("Longitude:", "").Trim();
|
||
|
||
// If after removing coordinate keywords, we only have numbers/dots/commas, it's not human-readable
|
||
if (cleanAddress.All(c => char.IsDigit(c) || c == '.' || c == ',' || c == ' ' || c == ':' || c == '-'))
|
||
return false;
|
||
|
||
// If we have actual text (city names, street names, etc.), it's human-readable
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Checks if coordinates fall within a known override area
|
||
/// </summary>
|
||
private LocationOverride? GetLocationOverride(double lat, double lng)
|
||
{
|
||
return _locationOverrides.FirstOrDefault(o =>
|
||
lat >= o.MinLat && lat <= o.MaxLat &&
|
||
lng >= o.MinLng && lng <= o.MaxLng);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Applies override data to correct wrong API results
|
||
/// </summary>
|
||
private string ApplyLocationOverride(string? apiAddress, LocationOverride ovr)
|
||
{
|
||
var parts = new List<string>();
|
||
|
||
// Try to extract establishment name from API result
|
||
if (!string.IsNullOrWhiteSpace(apiAddress))
|
||
{
|
||
var apiParts = apiAddress.Split(',').Select(p => p.Trim()).ToArray();
|
||
if (apiParts.Length > 0 && !apiParts[0].Contains("Highway") && !apiParts[0].Contains("Road"))
|
||
{
|
||
parts.Add(apiParts[0]); // Keep establishment name from API
|
||
}
|
||
}
|
||
|
||
// Use override data for everything else
|
||
if (!string.IsNullOrWhiteSpace(ovr.Street))
|
||
parts.Add(ovr.Street);
|
||
|
||
if (!string.IsNullOrWhiteSpace(ovr.Subdivision))
|
||
parts.Add(ovr.Subdivision);
|
||
|
||
if (!string.IsNullOrWhiteSpace(ovr.Barangay))
|
||
parts.Add($"Brgy. {ovr.Barangay}");
|
||
|
||
if (!string.IsNullOrWhiteSpace(ovr.City))
|
||
parts.Add($"City of {ovr.City}");
|
||
|
||
if (!string.IsNullOrWhiteSpace(ovr.Province))
|
||
parts.Add(ovr.Province);
|
||
|
||
if (!string.IsNullOrWhiteSpace(ovr.Postcode))
|
||
parts.Add(ovr.Postcode);
|
||
|
||
parts.Add("Philippines");
|
||
|
||
return string.Join(", ", parts);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Enhances address with nearby POI to get building numbers and names
|
||
/// </summary>
|
||
private async Task<string> EnhanceWithNearbyPOI(double lat, double lng, string baseAddress)
|
||
{
|
||
try
|
||
{
|
||
// Use Overpass API to find exact building/POI at this location
|
||
var poi = await GetNearbyBuildingAsync(lat, lng, radiusMeters: 10);
|
||
|
||
if (poi != null && !string.IsNullOrWhiteSpace(poi.Name))
|
||
{
|
||
// Check if POI name is already in address
|
||
if (!baseAddress.Contains(poi.Name, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// Insert POI at the beginning
|
||
var parts = baseAddress.Split(',').Select(p => p.Trim()).ToList();
|
||
|
||
// Build enhanced address with POI
|
||
var enhanced = new List<string> { poi.Name };
|
||
|
||
// Add house number if available
|
||
if (!string.IsNullOrWhiteSpace(poi.HouseNumber))
|
||
{
|
||
enhanced[0] = $"{poi.Name}, {poi.HouseNumber}";
|
||
}
|
||
|
||
enhanced.AddRange(parts);
|
||
return string.Join(", ", RemoveDuplicatesAndSimilar(enhanced));
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning(ex, "Failed to enhance address with POI");
|
||
}
|
||
|
||
return baseAddress;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets exact building/POI at coordinates using Overpass API
|
||
/// </summary>
|
||
private async Task<POIInfo?> GetNearbyBuildingAsync(double lat, double lng, int radiusMeters = 10)
|
||
{
|
||
try
|
||
{
|
||
using var client = new HttpClient();
|
||
client.Timeout = TimeSpan.FromSeconds(10);
|
||
|
||
// Overpass query to find buildings/POI at exact location
|
||
var query = $@"
|
||
[out:json][timeout:5];
|
||
(
|
||
node[""name""](around:{radiusMeters},{lat},{lng});
|
||
way[""name""](around:{radiusMeters},{lat},{lng});
|
||
node[""building""](around:{radiusMeters},{lat},{lng});
|
||
way[""building""](around:{radiusMeters},{lat},{lng});
|
||
);
|
||
out body;
|
||
";
|
||
|
||
var content = new StringContent($"data={Uri.EscapeDataString(query)}");
|
||
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/x-www-form-urlencoded");
|
||
|
||
var response = await client.PostAsync("https://overpass-api.de/api/interpreter", content);
|
||
var json = await response.Content.ReadAsStringAsync();
|
||
|
||
using var doc = JsonDocument.Parse(json);
|
||
|
||
if (!doc.RootElement.TryGetProperty("elements", out var elements))
|
||
return null;
|
||
|
||
// Find the closest POI with a name
|
||
POIInfo? bestPOI = null;
|
||
double closestDistance = double.MaxValue;
|
||
|
||
foreach (var element in elements.EnumerateArray())
|
||
{
|
||
if (!element.TryGetProperty("tags", out var tags))
|
||
continue;
|
||
|
||
var name = GetJsonString(tags, "name");
|
||
if (string.IsNullOrWhiteSpace(name))
|
||
continue;
|
||
|
||
// Get element coordinates
|
||
double elemLat = lat, elemLng = lng;
|
||
if (element.TryGetProperty("lat", out var latProp) &&
|
||
element.TryGetProperty("lon", out var lonProp))
|
||
{
|
||
elemLat = latProp.GetDouble();
|
||
elemLng = lonProp.GetDouble();
|
||
}
|
||
|
||
// Calculate distance
|
||
var distance = CalculateDistance(lat, lng, elemLat, elemLng);
|
||
|
||
if (distance < closestDistance)
|
||
{
|
||
closestDistance = distance;
|
||
bestPOI = new POIInfo
|
||
{
|
||
Name = name,
|
||
HouseNumber = GetJsonString(tags, "addr:housenumber"),
|
||
Street = GetJsonString(tags, "addr:street"),
|
||
Distance = distance
|
||
};
|
||
}
|
||
}
|
||
|
||
return bestPOI;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning(ex, "Overpass API query failed");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Calculate distance between two coordinates in meters
|
||
/// </summary>
|
||
private double CalculateDistance(double lat1, double lng1, double lat2, double lng2)
|
||
{
|
||
const double R = 6371000; // Earth radius in meters
|
||
var dLat = (lat2 - lat1) * Math.PI / 180;
|
||
var dLng = (lng2 - lng1) * Math.PI / 180;
|
||
var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
|
||
Math.Cos(lat1 * Math.PI / 180) * Math.Cos(lat2 * Math.PI / 180) *
|
||
Math.Sin(dLng / 2) * Math.Sin(dLng / 2);
|
||
var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
|
||
return R * c;
|
||
}
|
||
|
||
// ================== GEOCODING (BEST EFFORT) ==================
|
||
|
||
private async Task<string?> GetGeocodedAddressAsync(double lat, double lng)
|
||
{
|
||
// Try Photon first (unlimited, good for PH)
|
||
var photon = await GetPhotonAddressAsync(lat, lng);
|
||
if (!string.IsNullOrWhiteSpace(photon))
|
||
return photon;
|
||
|
||
// Fallback to Nominatim
|
||
var nominatim = await GetNominatimAddressAsync(lat, lng);
|
||
if (!string.IsNullOrWhiteSpace(nominatim))
|
||
return nominatim;
|
||
|
||
return null;
|
||
}
|
||
|
||
private async Task<string?> GetPhotonAddressAsync(double lat, double lng)
|
||
{
|
||
try
|
||
{
|
||
using var client = new HttpClient();
|
||
client.Timeout = TimeSpan.FromSeconds(10);
|
||
|
||
var url = $"https://photon.komoot.io/reverse?lon={lng}&lat={lat}";
|
||
var json = await client.GetStringAsync(url);
|
||
using var doc = JsonDocument.Parse(json);
|
||
|
||
if (!doc.RootElement.TryGetProperty("features", out var features) ||
|
||
features.GetArrayLength() == 0)
|
||
return null;
|
||
|
||
var props = features[0].GetProperty("properties");
|
||
return FormatPhotonAddress(props);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning(ex, "Photon geocoding failed");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private async Task<string?> GetNominatimAddressAsync(double lat, double lng)
|
||
{
|
||
try
|
||
{
|
||
using var client = new HttpClient();
|
||
client.Timeout = TimeSpan.FromSeconds(10);
|
||
client.DefaultRequestHeaders.Add("User-Agent", "LSFE.MobApp/1.0");
|
||
|
||
var url = $"https://nominatim.openstreetmap.org/reverse?lat={lat}&lon={lng}&format=json&addressdetails=1";
|
||
var json = await client.GetStringAsync(url);
|
||
using var doc = JsonDocument.Parse(json);
|
||
|
||
if (!doc.RootElement.TryGetProperty("address", out var address))
|
||
return null;
|
||
|
||
return FormatNominatimAddress(address);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning(ex, "Nominatim geocoding failed");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private string FormatPhotonAddress(JsonElement props)
|
||
{
|
||
var parts = new List<string>();
|
||
|
||
var name = GetJsonString(props, "name");
|
||
var street = GetJsonString(props, "street");
|
||
var district = GetJsonString(props, "district") ?? GetJsonString(props, "suburb");
|
||
var city = GetJsonString(props, "city");
|
||
var state = GetJsonString(props, "state");
|
||
var postcode = GetJsonString(props, "postcode");
|
||
var country = GetJsonString(props, "country");
|
||
|
||
if (!string.IsNullOrWhiteSpace(name) && name != street)
|
||
parts.Add(name);
|
||
if (!string.IsNullOrWhiteSpace(street))
|
||
parts.Add(street);
|
||
if (!string.IsNullOrWhiteSpace(district))
|
||
parts.Add($"Brgy. {district}");
|
||
if (!string.IsNullOrWhiteSpace(city))
|
||
parts.Add($"City of {city}");
|
||
if (!string.IsNullOrWhiteSpace(state))
|
||
parts.Add(state);
|
||
if (!string.IsNullOrWhiteSpace(postcode))
|
||
parts.Add(postcode);
|
||
if (!string.IsNullOrWhiteSpace(country))
|
||
parts.Add(country);
|
||
|
||
return string.Join(", ", parts.Where(p => !string.IsNullOrWhiteSpace(p)));
|
||
}
|
||
|
||
private string FormatNominatimAddress(JsonElement address)
|
||
{
|
||
var parts = new List<string>();
|
||
|
||
var industrial = GetJsonString(address, "industrial");
|
||
var road = GetJsonString(address, "road");
|
||
var suburb = GetJsonString(address, "suburb") ?? GetJsonString(address, "village");
|
||
var city = GetJsonString(address, "city") ?? GetJsonString(address, "town");
|
||
var state = GetJsonString(address, "state");
|
||
var postcode = GetJsonString(address, "postcode");
|
||
var country = GetJsonString(address, "country");
|
||
|
||
if (!string.IsNullOrWhiteSpace(industrial))
|
||
parts.Add(industrial);
|
||
if (!string.IsNullOrWhiteSpace(road))
|
||
parts.Add(road);
|
||
if (!string.IsNullOrWhiteSpace(suburb))
|
||
parts.Add($"Brgy. {suburb}");
|
||
if (!string.IsNullOrWhiteSpace(city))
|
||
parts.Add($"City of {city}");
|
||
if (!string.IsNullOrWhiteSpace(state))
|
||
parts.Add(state);
|
||
if (!string.IsNullOrWhiteSpace(postcode))
|
||
parts.Add(postcode);
|
||
if (!string.IsNullOrWhiteSpace(country))
|
||
parts.Add(country);
|
||
|
||
return string.Join(", ", parts.Where(p => !string.IsNullOrWhiteSpace(p)));
|
||
}
|
||
|
||
// ================== HELPERS ==================
|
||
|
||
private string? GetJsonString(JsonElement element, string property)
|
||
{
|
||
if (element.TryGetProperty(property, out var value) &&
|
||
value.ValueKind == JsonValueKind.String)
|
||
{
|
||
return value.GetString();
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private List<string> RemoveDuplicatesAndSimilar(List<string> parts)
|
||
{
|
||
var result = new List<string>();
|
||
|
||
foreach (var part in parts)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(part))
|
||
continue;
|
||
|
||
bool isSimilar = false;
|
||
foreach (var existing in result)
|
||
{
|
||
if (IsSimilar(part, existing))
|
||
{
|
||
isSimilar = true;
|
||
if (part.Length > existing.Length)
|
||
{
|
||
result.Remove(existing);
|
||
result.Add(part);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!isSimilar)
|
||
result.Add(part);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
private bool IsSimilar(string a, string b)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b))
|
||
return false;
|
||
|
||
var aLower = a.ToLowerInvariant().Trim();
|
||
var bLower = b.ToLowerInvariant().Trim();
|
||
|
||
if (aLower == bLower)
|
||
return true;
|
||
|
||
var aClean = aLower.Replace(", inc.", "").Replace(" inc.", "").Replace(" corporation", "").Replace(" corp", "").Trim();
|
||
var bClean = bLower.Replace(", inc.", "").Replace(" inc.", "").Replace(" corporation", "").Replace(" corp", "").Trim();
|
||
|
||
if (aClean.Contains(bClean) || bClean.Contains(aClean))
|
||
{
|
||
var minLength = Math.Min(aClean.Length, bClean.Length);
|
||
var maxLength = Math.Max(aClean.Length, bClean.Length);
|
||
return (double)minLength / maxLength >= 0.6;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private async Task SaveCache(double lat, double lng, string address)
|
||
{
|
||
var cacheEntry = new Entities.AddressCache()
|
||
{
|
||
Latitude = lat,
|
||
Longitude = lng,
|
||
Address = address,
|
||
CachedAt = DateTime.UtcNow
|
||
};
|
||
var isSuccess = await _addressCache.SaveAsync(cacheEntry);
|
||
}
|
||
|
||
private async Task<string> GetFromMauiGeocoderAsync(double lat, double lng)
|
||
{
|
||
try
|
||
{
|
||
var placemark = (await Geocoding.Default.GetPlacemarksAsync(lat, lng))?.FirstOrDefault();
|
||
if (placemark == null)
|
||
return $"Lat: {lat:F6}, Long: {lng:F6}";
|
||
|
||
return string.Join(", ", new[]
|
||
{
|
||
placemark.FeatureName,
|
||
placemark.Thoroughfare,
|
||
placemark.SubLocality,
|
||
placemark.Locality,
|
||
placemark.AdminArea,
|
||
placemark.CountryName
|
||
}.Where(x => !string.IsNullOrWhiteSpace(x)));
|
||
}
|
||
catch
|
||
{
|
||
return $"Lat: {lat:F6}, Long: {lng:F6}";
|
||
}
|
||
}
|
||
}
|
||
|
||
// ================== MODELS ==================
|
||
|
||
internal class LocationOverride
|
||
{
|
||
public string Name { get; set; } = "";
|
||
public double MinLat { get; set; }
|
||
public double MaxLat { get; set; }
|
||
public double MinLng { get; set; }
|
||
public double MaxLng { get; set; }
|
||
public string? Street { get; set; }
|
||
public string? Subdivision { get; set; }
|
||
public string? Barangay { get; set; }
|
||
public string? City { get; set; }
|
||
public string? Province { get; set; }
|
||
public string? Postcode { get; set; }
|
||
}
|
||
|
||
internal class POIInfo
|
||
{
|
||
public string Name { get; set; } = "";
|
||
public string? HouseNumber { get; set; }
|
||
public string? Street { get; set; }
|
||
public double Distance { get; set; }
|
||
}
|
||
} |