Skip to content

Commit

Permalink
EMBCESSMOD-5034: self-serve eligibility POC (#1963)
Browse files Browse the repository at this point in the history
  • Loading branch information
ytqsl authored Mar 26, 2024
1 parent 146f1ef commit 40bebd4
Show file tree
Hide file tree
Showing 14 changed files with 371 additions and 17 deletions.
36 changes: 20 additions & 16 deletions ess/src/API/EMBC.ESS.Host/appsettings.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft ": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"System.Net.Http.HttpClient": "Warning",
"Microsoft.OData.Extensions.Client": "Warning",
"Grpc.Net.Client": "Warning"
}
"AllowedHosts": "*",
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft ": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"System.Net.Http.HttpClient": "Warning",
"Microsoft.OData.Extensions.Client": "Warning",
"Grpc.Net.Client": "Warning"
}
}
},
"messaging": {
"mode": "both"
},
"Spatial": {
"ArcGISUrl": "https://services1.arcgis.com/xeMpV7tU1t4KD3Ei/ArcGIS",
"GeocoderUrl": "https://geocoder.api.gov.bc.ca/"
}
},
"messaging": {
"mode": "both"
},
"AllowedHosts": "*"
}
35 changes: 35 additions & 0 deletions ess/src/API/EMBC.ESS.Utilities.Spatial/AddressLocator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EMBC.ESS.Utilities.Spatial.ArcGISApi;
using EMBC.ESS.Utilities.Spatial.GeocoderApi;

namespace EMBC.ESS.Utilities.Spatial
{
internal class AddressLocator : IAddressLocator
{
private readonly IGeocoderAdapter geocoder;
private readonly IArcGISAdapter arcGisAdapter;

public AddressLocator(IGeocoderAdapter geocoder, IArcGISAdapter arcGisAdapter)
{
this.geocoder = geocoder;
this.arcGisAdapter = arcGisAdapter;
}

public async Task<AddressInformation> LocateAsync(Location location, CancellationToken ct)
{
if (location is null)
{
throw new ArgumentNullException(nameof(location));
}

var geocode = await geocoder.Resolve(location.FullAddress, ct);
if (geocode == null) return new AddressInformation(location, null, null);
var features = await arcGisAdapter.QueryService(new PointIntersectionQuery("TASK_OA_24/FeatureServer/0", geocode));

return new AddressInformation(location, geocode, features.FirstOrDefault()?.Attributes.Select(a => new LocationAttribute(a.Key, a.Value?.ToString())));
}
}
}
37 changes: 37 additions & 0 deletions ess/src/API/EMBC.ESS.Utilities.Spatial/ArcGISApi/ArcGISAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Anywhere.ArcGIS;
using Anywhere.ArcGIS.Common;
using Anywhere.ArcGIS.Operation;

namespace EMBC.ESS.Utilities.Spatial.ArcGISApi
{
internal class ArcGISAdapter : IArcGISAdapter
{
private readonly PortalGateway portalGateway;

public ArcGISAdapter(PortalGateway portalGateway)
{
this.portalGateway = portalGateway;
}

public async Task<IEnumerable<GISFeature>> QueryService(PointIntersectionQuery query)
{
var arcGisQuery = new Query(query.ServiceName.AsEndpoint())
{
Geometry = new Point
{
X = query.Point.Longitude,
Y = query.Point.Latitude,
SpatialReference = SpatialReference.WGS84
},
SpatialRelationship = SpatialRelationshipTypes.Intersects,
};

var result = await portalGateway.Query(arcGisQuery);

return result.Features.Select(f => new GISFeature(f.Attributes.ToDictionary(a => a.Key, a => a.Value))).ToList();
}
}
}
14 changes: 14 additions & 0 deletions ess/src/API/EMBC.ESS.Utilities.Spatial/ArcGISApi/IArcGISAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Threading.Tasks;

namespace EMBC.ESS.Utilities.Spatial.ArcGISApi
{
internal interface IArcGISAdapter
{
Task<IEnumerable<GISFeature>> QueryService(PointIntersectionQuery query);
}

internal record PointIntersectionQuery(string ServiceName, Geocode Point);

internal record GISFeature(IEnumerable<KeyValuePair<string, object>> Attributes);
}
36 changes: 36 additions & 0 deletions ess/src/API/EMBC.ESS.Utilities.Spatial/Configuration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using EMBC.ESS.Utilities.Spatial.ArcGISApi;
using EMBC.ESS.Utilities.Spatial.GeocoderApi;
using EMBC.Utilities.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Refit;

namespace EMBC.ESS.Utilities.Spatial
{
public class Configuration : IConfigureComponentServices
{
public void ConfigureServices(ConfigurationServices configurationServices)
{
var settings = configurationServices.Configuration.GetSection("Spatial").Get<SpatialSettings>();
if (settings == null || !settings.IsValid())
{
configurationServices.Logger.Report(EMBC.Utilities.Telemetry.ReportType.Warning, "Spatial settings are incomplete, skipping configuration");
return;
}
configurationServices.Services.AddRefitClient<IGeocoderApi>().ConfigureHttpClient(c => c.BaseAddress = settings.GeocoderUrl!);
configurationServices.Services.AddSingleton(new Anywhere.ArcGIS.PortalGateway(settings.ArcGISUrl!.ToString()));
configurationServices.Services.AddTransient<IGeocoderAdapter, GeocoderAdapter>();
configurationServices.Services.AddTransient<IArcGISAdapter, ArcGISAdapter>();
configurationServices.Services.AddTransient<IAddressLocator, AddressLocator>();
}
}

public record SpatialSettings
{
public Uri? ArcGISUrl { get; set; }
public Uri? GeocoderUrl { get; set; }

public bool IsValid() => ArcGISUrl != null && GeocoderUrl != null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6</TargetFramework>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Company>Province of British Columbia</Company>
<Authors>Quartech Systems Limited</Authors>
<Copyright>Copyright 2022 Province of British Columbia</Copyright>
<PackageLicenseExpression></PackageLicenseExpression>
<RepositoryUrl>https://github.com/bcgov/embc-ess-mod</RepositoryUrl>
<RepositoryType>GIT</RepositoryType>
<AnalysisMode>Default</AnalysisMode>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<DebugType>full</DebugType>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Anywhere.ArcGIS" Version="2.0.1" />
<PackageReference Include="Refit.HttpClientFactory" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\EMBC.Utilities\EMBC.Utilities.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Threading;
using System.Threading.Tasks;

namespace EMBC.ESS.Utilities.Spatial.GeocoderApi
{
internal class GeocoderAdapter : IGeocoderAdapter
{
private readonly IGeocoderApi geocoderApi;

public GeocoderAdapter(IGeocoderApi geocoderApi)
{
this.geocoderApi = geocoderApi;
}

public async Task<Geocode?> Resolve(string address, CancellationToken ct)
{
var response = await geocoderApi.GetAddress(new GetAddressRequest { addressString = address });
var coordinates = response?.features?[0].geometry?.coordinates;
var score = response?.features?[0].properties?.score ?? 0;
if (coordinates == null || coordinates.Length != 2) return null;
return new Geocode(coordinates[1], coordinates[0], score);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Threading;
using System.Threading.Tasks;

namespace EMBC.ESS.Utilities.Spatial.GeocoderApi
{
internal interface IGeocoderAdapter
{
Task<Geocode?> Resolve(string address, CancellationToken ct);
}
}
98 changes: 98 additions & 0 deletions ess/src/API/EMBC.ESS.Utilities.Spatial/GeocoderApi/IGeocoderApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System.Threading.Tasks;
using Refit;

namespace EMBC.ESS.Utilities.Spatial.GeocoderApi
{
internal interface IGeocoderApi
{
[Get("/addresses.json?")]
Task<GetAddressResponse> GetAddress([Query] GetAddressRequest request);
}

internal record GetAddressRequest
{
public string? addressString { get; set; }
}

internal class GetAddressResponse
{
public string? baseDataDate { get; set; }
public string? copyrightLicense { get; set; }
public string? copyrightNotice { get; set; }
public Crs? crs { get; set; }
public string? disclaimer { get; set; }
public string? echo { get; set; }
public float? executionTime { get; set; }
public Feature[]? features { get; set; }
public string? interpolation { get; set; }
public string? locationDescriptor { get; set; }
public int? maxResults { get; set; }
public int? minScore { get; set; }
public string? privacyStatement { get; set; }
public string? queryAddress { get; set; }
public string? searchTimestamp { get; set; }
public int? setBack { get; set; }
public string? type { get; set; }
public string? version { get; set; }
}

internal record Crs
{
public Properties? properties { get; set; }
public string? type { get; set; }
}

internal record Properties
{
public int code { get; set; }
}

internal record Feature
{
public Geometry? geometry { get; set; }
public Properties2? properties { get; set; }
public string? type { get; set; }
}

internal record Geometry
{
public double[]? coordinates { get; set; }
public Crs? crs { get; set; }
public string? type { get; set; }
}

internal record Properties2
{
public string? accessNotes { get; set; }
public double? blockID { get; set; }
public string? changeDate { get; set; }
public double? civicNumber { get; set; }
public string? civicNumberSuffix { get; set; }
public string? electoralArea { get; set; }
public object[]? faults { get; set; }
public string? fullAddress { get; set; }
public string? fullSiteDescriptor { get; set; }
public string? isOfficial { get; set; }
public string? isStreetDirectionPrefix { get; set; }
public string? isStreetTypePrefix { get; set; }
public string? localityName { get; set; }
public string? localityType { get; set; }
public string? locationDescriptor { get; set; }
public string? locationPositionalAccuracy { get; set; }
public string? matchPrecision { get; set; }
public int? precisionPoints { get; set; }
public string? provinceCode { get; set; }
public double? score { get; set; }
public string? siteID { get; set; }
public string? siteName { get; set; }
public string? siteRetireDate { get; set; }
public string? siteStatus { get; set; }
public string? streetDirection { get; set; }
public string? streetName { get; set; }
public string? streetQualifier { get; set; }
public string? streetType { get; set; }
public string? unitDesignator { get; set; }
public string? unitNumber { get; set; }
public string? unitNumberSuffix { get; set; }
}
}
20 changes: 20 additions & 0 deletions ess/src/API/EMBC.ESS.Utilities.Spatial/IAddressLocator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace EMBC.ESS.Utilities.Spatial
{
public interface IAddressLocator
{
Task<AddressInformation> LocateAsync(Location location, CancellationToken ct);
}

public record Location(string FullAddress)
{
}

public record AddressInformation(Location Location, Geocode? geocode, IEnumerable<LocationAttribute>? Attributes);
public record Geocode(double Latitude, double Longitude, double score);

public record LocationAttribute(string Name, string? Value);
}
7 changes: 7 additions & 0 deletions ess/src/API/EMBC.ESS.sln
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EMBC.ESS.Utilities.Spatial", "EMBC.ESS.Utilities.Spatial\EMBC.ESS.Utilities.Spatial.csproj", "{6324E308-ACE4-4AC3-B99D-C4A75BDF0DC6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -96,6 +98,10 @@ Global
{C770CDD8-C18C-48D3-A7E8-78797C5CBDEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C770CDD8-C18C-48D3-A7E8-78797C5CBDEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C770CDD8-C18C-48D3-A7E8-78797C5CBDEA}.Release|Any CPU.Build.0 = Release|Any CPU
{6324E308-ACE4-4AC3-B99D-C4A75BDF0DC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6324E308-ACE4-4AC3-B99D-C4A75BDF0DC6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6324E308-ACE4-4AC3-B99D-C4A75BDF0DC6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6324E308-ACE4-4AC3-B99D-C4A75BDF0DC6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -111,6 +117,7 @@ Global
{827BC8E3-E64F-4E0D-A001-AB85DDF461F5} = {E5EDC676-4A91-48C0-BF38-D91F005ECD1A}
{0528280C-DD38-445E-B7AC-9402F4CB00D3} = {E5EDC676-4A91-48C0-BF38-D91F005ECD1A}
{C770CDD8-C18C-48D3-A7E8-78797C5CBDEA} = {E5EDC676-4A91-48C0-BF38-D91F005ECD1A}
{6324E308-ACE4-4AC3-B99D-C4A75BDF0DC6} = {E5EDC676-4A91-48C0-BF38-D91F005ECD1A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7030F633-EA46-4604-948A-65037D5A39B2}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@

<ItemGroup>
<ProjectReference Include="..\EMBC.ESS.Host\EMBC.ESS.Host.csproj" />
<ProjectReference Include="..\EMBC.ESS.Utilities.Spatial\EMBC.ESS.Utilities.Spatial.csproj" />
</ItemGroup>
</Project>
Loading

0 comments on commit 40bebd4

Please sign in to comment.