Files
fuel-wizard/backend/Services/SuburbBoundariesService.cs
T
2024-11-30 19:58:58 +11:00

186 lines
7.3 KiB
C#

using System.Text.Json;
using backend.Config;
using backend.Models.SuburbBoundariesApi;
using ServiceStack.Data;
using ServiceStack.OrmLite;
namespace backend.Services;
public class SuburbBoundariesService : ISuburbBoundariesService
{
private readonly IDbConnectionFactory _dbConnectionFactory;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<SuburbBoundariesService> _logger;
private readonly SuburbBoundaryApiConfig _suburbBoundaryApiConfig;
public SuburbBoundariesService(IDbConnectionFactory dbConnectionFactory, IHttpClientFactory httpClientFactory,
SuburbBoundaryApiConfig suburbBoundaryApiConfig, ILogger<SuburbBoundariesService> logger)
{
_dbConnectionFactory = dbConnectionFactory;
_httpClientFactory = httpClientFactory;
_suburbBoundaryApiConfig = suburbBoundaryApiConfig;
_logger = logger;
}
public async Task<bool> FetchFeatures()
{
_logger.LogInformation("Started suburb boundary data fetch job");
var client = _httpClientFactory.CreateClient();
var geoJsonData = await client.GetAsync(_suburbBoundaryApiConfig.Url, HttpCompletionOption.ResponseHeadersRead);
if (!geoJsonData.IsSuccessStatusCode)
{
_logger.LogInformation(
"Unsuccessful status code received while fetching suburb boundary data from the API: {StatusCode}",
geoJsonData.StatusCode);
return false;
}
await using var responseStream = await geoJsonData.Content.ReadAsStreamAsync();
using var jsonDocument = await JsonDocument.ParseAsync(responseStream);
var features = jsonDocument.RootElement.GetProperty("features");
if (features.GetArrayLength() != 0) return await UpdateFeatures(features);
_logger.LogError("Suburb boundary Feature Collection is empty - data not refreshed");
return false;
}
public async Task<IEnumerable<Suburb>?> GetAllSuburbs()
{
using var db = _dbConnectionFactory.OpenDbConnection();
//select the name and id from suburb_boundaries
var result = await db.SelectAsync(db.From<SuburbBoundary>()
.Select(sb => new { sb.Name, sb.Id }));
//project to our Suburb type for brevity
var suburbs = result.Select(r => new Suburb { Name = r.Name, Id = r.Id });
return suburbs;
}
public async Task<IEnumerable<Suburb>?> GetSuburbsByName(string name)
{
using var db = _dbConnectionFactory.OpenDbConnection();
//select the name and id from suburb_boundaries, and limit to a maximum of 20 results
var result = await db.SelectAsync(db.From<SuburbBoundary>()
.Where(sb => sb.Name != null && sb.Name.ToUpper().Contains(name.ToUpper()))
.Select(sb => new { sb.Name, sb.Id })
.Limit(20));
//project to our Suburb type for brevity
var suburbs = result.Select(r => new Suburb { Name = r.Name, Id = r.Id });
return suburbs;
}
public async Task<SuburbBoundary?> GetSuburbBoundaryById(string id)
{
using var db = _dbConnectionFactory.OpenDbConnection();
const string sql = """
SELECT id,
name,
dt_create,
created_at,
ST_AsGeoJSON(geometry) AS Geometry
FROM suburb_boundary
WHERE id = @Id
AND ST_GeometryType(geometry) = 'ST_MultiPolygon';
""";
var suburbBoundary = await db.SingleAsync<SuburbBoundary>(sql, new { Id = new Guid(id) });
return suburbBoundary;
}
private async Task<bool> UpdateFeatures(JsonElement features)
{
using var db = _dbConnectionFactory.OpenDbConnection();
try
{
const int batchSize = 50;
var suburbBoundariesToInsert = new List<SuburbBoundary>(batchSize);
var suburbBoundariesToUpdate = new List<SuburbBoundary>(batchSize);
foreach (var feature in features.EnumerateArray())
{
if (!feature.TryGetProperty("geometry", out var geometry) ||
geometry.GetProperty("type").GetString() != "MultiPolygon") continue;
var name = feature.GetProperty("properties").GetProperty("nsw_loca_2").GetString();
var geometryWkt = ConvertMultiPolygonToWkt(geometry.GetProperty("coordinates"));
var dtCreate = feature.GetProperty("properties").TryGetProperty("dt_create", out var dtCreateProp)
? DateTimeOffset.Parse(dtCreateProp.GetString() ?? throw new InvalidOperationException())
: (DateTimeOffset?)null;
//check if an entry with the same name already exists
var existingSuburb = await db.SingleAsync<SuburbBoundary>(x => x.Name == name);
if (existingSuburb != null)
{
//update only if there's a difference in Geometry or DtCreate
if (existingSuburb.Geometry != geometryWkt || existingSuburb.DtCreate != dtCreate)
{
existingSuburb.Geometry = geometryWkt;
existingSuburb.DtCreate = dtCreate;
suburbBoundariesToUpdate.Add(existingSuburb);
}
}
else
{
//if no existing record, add to insert list
suburbBoundariesToInsert.Add(new SuburbBoundary
{
Name = name,
Geometry = geometryWkt,
DtCreate = dtCreate
});
}
if (suburbBoundariesToInsert.Count >= batchSize)
{
await db.InsertAllAsync(suburbBoundariesToInsert);
suburbBoundariesToInsert.Clear();
}
if (suburbBoundariesToUpdate.Count < batchSize) continue;
await db.UpdateAllAsync(suburbBoundariesToUpdate);
suburbBoundariesToUpdate.Clear();
}
//insert or update remaining items in batches
if (suburbBoundariesToInsert.Count > 0) await db.InsertAllAsync(suburbBoundariesToInsert);
if (suburbBoundariesToUpdate.Count > 0) await db.UpdateAllAsync(suburbBoundariesToUpdate);
}
catch (Exception e)
{
_logger.LogError("Error inserting or updating suburb boundary features: {EMessage}", e.Message);
throw;
}
_logger.LogInformation("Successfully updated suburb boundary data in the database");
return true;
}
#region helpers
private static string ConvertMultiPolygonToWkt(JsonElement multiPolygonCoordinates)
{
var polygons = multiPolygonCoordinates.EnumerateArray()
.Select(polygon => string.Join(", ",
polygon[0].EnumerateArray().Select(coord => $"{coord[0].GetDouble()} {coord[1].GetDouble()}")))
.Select(coordinates => $"(({coordinates}))").ToList();
return $"MULTIPOLYGON({string.Join(", ", polygons)})";
}
#endregion
}