186 lines
7.3 KiB
C#
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
|
|
} |