diff --git a/.pipeline/lib/config.js b/.pipeline/lib/config.js index 89982c03..559eb751 100644 --- a/.pipeline/lib/config.js +++ b/.pipeline/lib/config.js @@ -6,10 +6,10 @@ const name = 'hmcr' const phases = { build: {namespace:'txkggj-tools' , name: `${name}`, phase: 'build' , changeId:changeId, suffix: `-build-${changeId}` , instance: `${name}-build-${changeId}` , version:`${version}-${changeId}` , tag:`build-${version}-${changeId}`, transient: true}, - dev: {namespace:'txkggj-dev' , name: `${name}`, phase: 'dev' , changeId:changeId, suffix: `-dev-${changeId}` , instance: `${name}-dev-${changeId}` , version:`${version}-${changeId}` , tag:`dev-${version}-${changeId}` , host: `hmcr-${changeId}-txkggj-dev.pathfinder.gov.bc.ca` , url_prefix: 'dev-', bceid_service: '.test' , export_server: 'devoas1', dotnet_env: 'Development', transient: true}, - test: {namespace:'txkggj-test' , name: `${name}`, phase: 'test' , changeId:changeId, suffix: `-test` , instance: `${name}-test` , version:`${version}` , tag:`test-${version}` , host: `hmcr-txkggj-test.pathfinder.gov.bc.ca` , url_prefix: 'tst-', bceid_service: '.test' , export_server: 'tstoas2', dotnet_env: 'Staging'}, - uat: {namespace:'txkggj-test' , name: `${name}`, phase: 'uat' , changeId:changeId, suffix: `-uat` , instance: `${name}-uat` , version:`${version}` , tag:`uat-${version}` , host: `hmcr-txkggj-uat.pathfinder.gov.bc.ca` , url_prefix: 'uat-', bceid_service: '.test' , export_server: 'tstoas2', dotnet_env: 'UAT'}, - prod: {namespace:'txkggj-prod' , name: `${name}`, phase: 'prod' , changeId:changeId, suffix: `-prod` , instance: `${name}-prod` , version:`${version}` , tag:`prod-${version}` , host: `hmcr-txkggj-prod.pathfinder.gov.bc.ca` , url_prefix: '' , bceid_service: '' , export_server: 'prdoas2', dotnet_env: 'Production'}, + dev: {namespace:'txkggj-dev' , name: `${name}`, phase: 'dev' , changeId:changeId, suffix: `-dev-${changeId}` , instance: `${name}-dev-${changeId}` , version:`${version}-${changeId}` , tag:`dev-${version}-${changeId}` , host: `hmcr-${changeId}-txkggj-dev.pathfinder.gov.bc.ca` , url_prefix: 'dev-', bceid_service: '.test' , oas_server: 'devoas1', dotnet_env: 'Development', transient: true}, + test: {namespace:'txkggj-test' , name: `${name}`, phase: 'test' , changeId:changeId, suffix: `-test` , instance: `${name}-test` , version:`${version}` , tag:`test-${version}` , host: `hmcr-txkggj-test.pathfinder.gov.bc.ca` , url_prefix: 'tst-', bceid_service: '.test' , oas_server: 'tstoas2', dotnet_env: 'Staging'}, + uat: {namespace:'txkggj-test' , name: `${name}`, phase: 'uat' , changeId:changeId, suffix: `-uat` , instance: `${name}-uat` , version:`${version}` , tag:`uat-${version}` , host: `hmcr-txkggj-uat.pathfinder.gov.bc.ca` , url_prefix: 'uat-', bceid_service: '.test' , oas_server: 'tstoas2', dotnet_env: 'UAT'}, + prod: {namespace:'txkggj-prod' , name: `${name}`, phase: 'prod' , changeId:changeId, suffix: `-prod` , instance: `${name}-prod` , version:`${version}` , tag:`prod-${version}` , host: `hmcr-txkggj-prod.pathfinder.gov.bc.ca` , url_prefix: '' , bceid_service: '' , oas_server: 'prdoas2', dotnet_env: 'Production'}, }; // This callback forces the node process to exit as failure. diff --git a/.pipeline/lib/deploy.js b/.pipeline/lib/deploy.js index 5d046865..ca60f4a8 100644 --- a/.pipeline/lib/deploy.js +++ b/.pipeline/lib/deploy.js @@ -76,7 +76,9 @@ module.exports = settings => { ENV: phases[phase].phase, SUBMISSION_URL: `https://${phases[phase].url_prefix}hmcr.th.gov.bc.ca/workreporting?serviceArea={0}&showResult={1}`, BCEID_SERVICE: `https://gws1${phases[phase].bceid_service}.bceid.ca/webservices/client/v10/bceidservice.asmx`, - EXPORT_URL: `https://${phases[phase].export_server}.apps.th.gov.bc.ca` + EXPORT_URL: `https://${phases[phase].oas_server}.apps.th.gov.bc.ca`, + OAS_URL: `https://${phases[phase].oas_server}.apps.th.gov.bc.ca`, + GEOSERVER_TIMEOUT: 120 } } ) diff --git a/api/Hmcr.Api/appsettings.json b/api/Hmcr.Api/appsettings.json index c2b6c846..b8bbacb2 100644 --- a/api/Hmcr.Api/appsettings.json +++ b/api/Hmcr.Api/appsettings.json @@ -16,11 +16,11 @@ { "Name": "Async", "Args": { - "configure": [{ "Name": "Console" }] + "configure": [ { "Name": "Console" } ] } } ], - "Enrich": ["FromLogContext", "WithMachineName"] + "Enrich": [ "FromLogContext", "WithMachineName" ] }, "ConnectionStrings": { "HMCR": "Server=(localdb)\\mssqllocaldb;Database=HMR_DEV;Trusted_Connection=True;MultipleActiveResultSets=true" @@ -40,6 +40,12 @@ "WFSExportPath": "ogs-geoV06/ows?service=WFS&version=2.0.0&request=GetFeature", "KMLExportPath": "ogs-geoV06/wms/kml?mode=download&styles=HMR_GENERIC_FOR_KML" }, + "Timeouts": { + "MapsAPI": 15, + "OasAPI": 15, + "ExportAPI": 15, + "InventoryAPI": 120 + }, "JWT": { "Authority": "https://sso-dev.pathfinder.gov.bc.ca/auth/realms/", "Audience": "" diff --git a/api/Hmcr.Chris/ChrisServiceCollectionExtensions.cs b/api/Hmcr.Chris/ChrisServiceCollectionExtensions.cs index 7b18c49d..4ea3c371 100644 --- a/api/Hmcr.Chris/ChrisServiceCollectionExtensions.cs +++ b/api/Hmcr.Chris/ChrisServiceCollectionExtensions.cs @@ -13,14 +13,14 @@ public static void AddChrisHttpClient(this IServiceCollection services, IConfigu services.AddHttpClient(client => { client.BaseAddress = new Uri(config.GetValue("CHRIS:MapUrl")); - client.Timeout = new TimeSpan(0, 0, 15); + client.Timeout = new TimeSpan(0, 0, config.GetValue("Timeouts:MapsAPI")); client.DefaultRequestHeaders.Clear(); }); services.AddHttpClient(client => { client.BaseAddress = new Uri(config.GetValue("CHRIS:OASUrl")); - client.Timeout = new TimeSpan(0, 0, 15); + client.Timeout = new TimeSpan(0, 0, config.GetValue("Timeouts:OasAPI")); client.DefaultRequestHeaders.Clear(); var userId = config.GetValue("ServiceAccount:User"); @@ -32,7 +32,7 @@ public static void AddChrisHttpClient(this IServiceCollection services, IConfigu services.AddHttpClient(client => { client.BaseAddress = new Uri(config.GetValue("CHRIS:ExportUrl")); - client.Timeout = new TimeSpan(0, 0, 15); + client.Timeout = new TimeSpan(0, 0, config.GetValue("Timeouts:ExportAPI")); client.DefaultRequestHeaders.Clear(); var userId = config.GetValue("ServiceAccount:User"); @@ -44,7 +44,8 @@ public static void AddChrisHttpClient(this IServiceCollection services, IConfigu services.AddHttpClient(client => { client.BaseAddress = new Uri(config.GetValue("CHRIS:OASUrl")); - client.Timeout = new TimeSpan(0, 0, 45); + //TODO: need to set the timeouts to be configurable with the ConfigMap of OCP + client.Timeout = new TimeSpan(0, 0, config.GetValue("Timeouts:InventoryAPI")); client.DefaultRequestHeaders.Clear(); var userId = config.GetValue("ServiceAccount:User"); diff --git a/api/Hmcr.Chris/Hmcr.Chris.csproj b/api/Hmcr.Chris/Hmcr.Chris.csproj index f257989a..fd43f059 100644 --- a/api/Hmcr.Chris/Hmcr.Chris.csproj +++ b/api/Hmcr.Chris/Hmcr.Chris.csproj @@ -26,6 +26,7 @@ + diff --git a/api/Hmcr.Chris/InventoryApi.cs b/api/Hmcr.Chris/InventoryApi.cs index 3f971dfb..73c24bd7 100644 --- a/api/Hmcr.Chris/InventoryApi.cs +++ b/api/Hmcr.Chris/InventoryApi.cs @@ -1,6 +1,10 @@ using Hmcr.Chris.Models; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using NetTopologySuite.Geometries; +using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; @@ -9,11 +13,15 @@ namespace Hmcr.Chris { public interface IInventoryApi { - Task> GetSurfaceTypeAssociatedWithLine(string lineStringCoordinates); - Task GetSurfaceTypeAssociatedWithPoint(string lineStringCoordinates); - - //TODO: implement MC calls to CHRIS - + Task> GetSurfaceTypeAssociatedWithLine(NetTopologySuite.Geometries.Geometry geometry); + Task GetSurfaceTypeAssociatedWithPoint(NetTopologySuite.Geometries.Geometry geometry); + Task> GetMaintenanceClassesAssociatedWithLine(NetTopologySuite.Geometries.Geometry geometry); + Task GetMaintenanceClassesAssociatedWithPoint(NetTopologySuite.Geometries.Geometry geometry); + Task GetHighwayProfileAssociatedWithPoint(NetTopologySuite.Geometries.Geometry geometry); + Task> GetHighwayProfileAssociatedWithLine(NetTopologySuite.Geometries.Geometry geometry); + Task GetGuardrailAssociatedWithPoint(NetTopologySuite.Geometries.Geometry geometry); + Task> GetGuardrailAssociatedWithLine(NetTopologySuite.Geometries.Geometry geometry); + Task> GetBridgeStructure(string rfiSegment); } public class InventoryApi : IInventoryApi @@ -22,6 +30,7 @@ public class InventoryApi : IInventoryApi private InventoryQueries _queries; private IApi _api; private string _path; + private ILogger _logger; /// /// Chris Query typeName values, used to call the Get Inventory @@ -33,53 +42,377 @@ public static class InventoryQueryTypeName public const string SURF_ASSOC_WITH_POINT = "SURF_ASSOCIATED_WITH_POINT"; public const string MC_ASSOC_WITH_LINE = "MC_ASSOCIATED_WITH_LINE"; public const string MC_ASSOC_WITH_POINT = "MC_ASSOCIATED_WITH_POINT"; + public const string HP_ASSOC_WITH_LINE = "HP_ASSOCIATED_WITH_LINE"; + public const string HP_ASSOC_WITH_POINT = "HP_ASSOCIATED_WITH_POINT"; + public const string GR_ASSOC_WITH_LINE = "GR_ASSOCIATED_WITH_POINT"; + public const string GR_ASSOC_WITH_POINT = "GR_ASSOCIATED_WITH_LINE"; + } + public static class InventoryParamType + { + public const string POINT_COORDINATE = "coordinate"; + public const string LINE_COORDINATE = "coordinates"; } - public InventoryApi(HttpClient client, IApi api, IConfiguration config) + public InventoryApi(HttpClient client, IApi api, IConfiguration config, ILogger logger) { _client = client; _queries = new InventoryQueries(); _api = api; _path = config.GetValue("CHRIS:OASPath"); + _logger = logger; } - public async Task GetSurfaceTypeAssociatedWithPoint(string lineStringCoordinates) + /// + /// Utility function takes an array of coordinates and builds them + /// into a string with a limit of 250 coordinate pairs in a group. + /// The group is placed into a string array that is then returned + /// for processing. + /// This is to deal with https://jira.th.gov.bc.ca/browse/HMCR-871 + /// in which GeoServer only accepts a max of 500 coordinate pairs + /// and also ensures we don't hit the 120sec timeout limit or + /// MAX post size. + /// + /// + /// + private List BuildGeometryString(Coordinate[] coordinates) { - var body = string.Format(_queries.SurfaceTypeAssocWithPointQuery, lineStringCoordinates, InventoryQueryTypeName.SURF_ASSOC_WITH_POINT); + var geometryGroup = new List(); + var geometryLineString = ""; + var coordinateCount = 0; - var contents = await (await _api.PostWithRetry(_client, _path, body)).Content.ReadAsStringAsync(); + foreach (Coordinate coordinate in coordinates) + { + geometryLineString += coordinate.X + "\\," + coordinate.Y + "\\,"; + coordinateCount++; + if (coordinateCount == 250 || (coordinate == coordinates.Last())) + { + geometryGroup.Add(geometryLineString.Substring(0, geometryLineString.Length - 2)); + geometryLineString = ""; + coordinateCount = 0; + } + } - var results = JsonSerializer.Deserialize>(contents); + return geometryGroup; + } + public async Task GetSurfaceTypeAssociatedWithPoint(NetTopologySuite.Geometries.Geometry geometry) + { + var body = ""; + var contents = ""; SurfaceType surfaceType = new SurfaceType(); - if (results.features.Length > 0) + + try + { + var geometryGroup = BuildGeometryString(geometry.Coordinates); + + foreach (var lineStringCoordinates in geometryGroup) + { + body = string.Format(_queries.InventoryAssocWithPointQuery, InventoryParamType.POINT_COORDINATE, lineStringCoordinates, InventoryQueryTypeName.SURF_ASSOC_WITH_POINT); + + contents = await (await _api.PostWithRetry(_client, _path, body)).Content.ReadAsStringAsync(); + + var results = JsonSerializer.Deserialize>(contents); + + if (results.features.Length > 0) + { + surfaceType.Type = results.features[0].properties.SURFACE_TYPE; + } + } + + return surfaceType; + } + catch (System.Exception ex) { - surfaceType.Type = results.features[0].properties.SURFACE_TYPE; + _logger.LogError($"Exception - GetSurfaceTypeAssociatedWithPoint: {body} - {contents}"); + throw ex; } + } - return surfaceType; + public async Task> GetSurfaceTypeAssociatedWithLine(NetTopologySuite.Geometries.Geometry geometry) + { + var body = ""; + var contents = ""; + var surfaceTypes = new List(); + + try + { + var geometryGroup = BuildGeometryString(geometry.Coordinates); + + foreach (var lineStringCoordinates in geometryGroup) + { + body = string.Format(_queries.InventoryAssocWithLineQuery, InventoryParamType.LINE_COORDINATE, lineStringCoordinates, InventoryQueryTypeName.SURF_ASSOC_WITH_LINE); + + contents = await (await _api.PostWithRetry(_client, _path, body)).Content.ReadAsStringAsync(); + + var results = JsonSerializer.Deserialize>(contents); + + foreach (var feature in results.features) + { + SurfaceType surfaceType = new SurfaceType(); + surfaceType.Length = feature.properties.CLIPPED_LENGTH_KM; + surfaceType.Type = feature.properties.SURFACE_TYPE; + + surfaceTypes.Add(surfaceType); + } + } + + return surfaceTypes; + } + catch (System.Exception ex) + { + _logger.LogError($"Exception - GetSurfaceTypeAssociatedWithLine: {body} - {contents}"); + throw ex; + } } - public async Task> GetSurfaceTypeAssociatedWithLine(string lineStringCoordinates) + public async Task> GetMaintenanceClassesAssociatedWithLine(NetTopologySuite.Geometries.Geometry geometry) { - var body = string.Format(_queries.SurfaceTypeAssocWithLineQuery, lineStringCoordinates, InventoryQueryTypeName.SURF_ASSOC_WITH_LINE); + var body = ""; + var contents = ""; + var maintenanceClasses = new List(); - var contents = await (await _api.PostWithRetry(_client, _path, body)).Content.ReadAsStringAsync(); + try + { + var geometryGroup = BuildGeometryString(geometry.Coordinates); - var results = JsonSerializer.Deserialize>(contents); + foreach (var lineStringCoordinates in geometryGroup) + { + body = string.Format(_queries.InventoryAssocWithLineQuery, InventoryParamType.LINE_COORDINATE, lineStringCoordinates, InventoryQueryTypeName.MC_ASSOC_WITH_LINE); - var surfaceTypes = new List(); + contents = await (await _api.PostWithRetry(_client, _path, body)).Content.ReadAsStringAsync(); + + var results = JsonSerializer.Deserialize>(contents); + + foreach (var feature in results.features) + { + MaintenanceClass maintenanceClass = new MaintenanceClass(); + maintenanceClass.Length = feature.properties.CLIPPED_LENGTH_KM; + maintenanceClass.SummerRating = feature.properties.SUMMER_CLASS_RATING; + maintenanceClass.WinterRating = feature.properties.WINTER_CLASS_RATING; + + maintenanceClasses.Add(maintenanceClass); + } + } + + return maintenanceClasses; + } + catch (System.Exception ex) + { + _logger.LogError($"Exception - GetMaintenanceClassesAssociatedWithLine: {body} - {contents}"); + throw ex; + } + } + + public async Task GetMaintenanceClassesAssociatedWithPoint(NetTopologySuite.Geometries.Geometry geometry) + { + var body = ""; + var contents = ""; + MaintenanceClass maintenanceClass = new MaintenanceClass(); + + try + { + var geometryGroup = BuildGeometryString(geometry.Coordinates); + + foreach (var lineStringCoordinates in geometryGroup) + { + body = string.Format(_queries.InventoryAssocWithPointQuery, InventoryParamType.POINT_COORDINATE, lineStringCoordinates, InventoryQueryTypeName.MC_ASSOC_WITH_POINT); + + contents = await (await _api.PostWithRetry(_client, _path, body)).Content.ReadAsStringAsync(); + + var results = JsonSerializer.Deserialize>(contents); + + if (results.features.Length > 0) + { + maintenanceClass.SummerRating = results.features[0].properties.SUMMER_CLASS_RATING; + maintenanceClass.WinterRating = results.features[0].properties.WINTER_CLASS_RATING; + } + } + + return maintenanceClass; + } + catch (System.Exception ex) + { + _logger.LogError($"Exception - GetMaintenanceClassesAssociatedWithPoint: {body} - {contents}"); + throw ex; + } + } + + public async Task GetHighwayProfileAssociatedWithPoint(NetTopologySuite.Geometries.Geometry geometry) + { + var body = ""; + var contents = ""; + HighwayProfile highwayProfile = new HighwayProfile(); + + try + { + var geometryGroup = BuildGeometryString(geometry.Coordinates); - foreach (var feature in results.features) + foreach (var lineStringCoordinates in geometryGroup) + { + body = string.Format(_queries.InventoryAssocWithPointQuery, InventoryParamType.POINT_COORDINATE, lineStringCoordinates, InventoryQueryTypeName.HP_ASSOC_WITH_POINT); + + contents = await (await _api.PostWithRetry(_client, _path, body)).Content.ReadAsStringAsync(); + + var results = JsonSerializer.Deserialize>(contents); + + + if (results.features.Length > 0) + { + highwayProfile.NumberOfLanes = results.features[0].properties.NUMBER_OF_LANES; + highwayProfile.DividedHighwayFlag = results.features[0].properties.DIVIDED_HIGHWAY_FLAG; + } + } + + return highwayProfile; + } + catch (System.Exception ex) + { + _logger.LogError($"Exception - GetHighwayProfileAssociatedWithPoint: {body} - {contents}"); + throw ex; + } + } + + public async Task> GetHighwayProfileAssociatedWithLine(NetTopologySuite.Geometries.Geometry geometry) + { + var body = ""; + var contents = ""; + var highwayProfiles = new List(); + + try + { + var geometryGroup = BuildGeometryString(geometry.Coordinates); + + foreach (var lineStringCoordinates in geometryGroup) + { + body = string.Format(_queries.InventoryAssocWithLineQuery, InventoryParamType.LINE_COORDINATE, lineStringCoordinates, InventoryQueryTypeName.HP_ASSOC_WITH_LINE); + + contents = await (await _api.PostWithRetry(_client, _path, body)).Content.ReadAsStringAsync(); + + var results = JsonSerializer.Deserialize>(contents); + + foreach (var feature in results.features) + { + HighwayProfile highwayProfile = new HighwayProfile(); + highwayProfile.Length = feature.properties.CLIPPED_LENGTH_KM; + highwayProfile.NumberOfLanes = feature.properties.NUMBER_OF_LANES; + highwayProfile.DividedHighwayFlag = feature.properties.DIVIDED_HIGHWAY_FLAG; + + highwayProfiles.Add(highwayProfile); + } + } + + return highwayProfiles; + } + catch (System.Exception ex) { - SurfaceType surfaceType = new SurfaceType(); - surfaceType.Length = feature.properties.CLIPPED_LENGTH_KM; - surfaceType.Type = feature.properties.SURFACE_TYPE; - - surfaceTypes.Add(surfaceType); + _logger.LogError($"Exception - GetHighwayProfileAssociatedWithLine: {body} - {contents}"); + throw ex; } + } + + public async Task GetGuardrailAssociatedWithPoint(NetTopologySuite.Geometries.Geometry geometry) + { + var body = ""; + var contents = ""; + Guardrail guardrail = new Guardrail(); - return surfaceTypes; + try + { + var geometryGroup = BuildGeometryString(geometry.Coordinates); + + foreach (var lineStringCoordinates in geometryGroup) + { + body = string.Format(_queries.InventoryAssocWithPointQuery, InventoryParamType.POINT_COORDINATE, lineStringCoordinates, InventoryQueryTypeName.HP_ASSOC_WITH_POINT); + + contents = await (await _api.PostWithRetry(_client, _path, body)).Content.ReadAsStringAsync(); + + var results = JsonSerializer.Deserialize>(contents); + + if (results.features.Length > 0) + { + guardrail.GuardrailType = results.features[0].properties.GUARDRAIL_TYPE; + } + } + + return guardrail; + } + catch (System.Exception ex) + { + _logger.LogError($"Exception - GetGuardrailAssociatedWithPoint: {body} - {contents}"); + throw ex; + } + } + + public async Task> GetGuardrailAssociatedWithLine(NetTopologySuite.Geometries.Geometry geometry) + { + var body = ""; + var contents = ""; + var guardrails = new List(); + + try + { + var geometryGroup = BuildGeometryString(geometry.Coordinates); + + foreach (var lineStringCoordinates in geometryGroup) + { + body = string.Format(_queries.InventoryAssocWithLineQuery, InventoryParamType.LINE_COORDINATE, lineStringCoordinates, InventoryQueryTypeName.HP_ASSOC_WITH_LINE); + + contents = await (await _api.PostWithRetry(_client, _path, body)).Content.ReadAsStringAsync(); + + var results = JsonSerializer.Deserialize>(contents); + + foreach (var feature in results.features) + { + Guardrail guardrail = new Guardrail(); + guardrail.Length = feature.properties.CLIPPED_LENGTH_KM; + guardrail.GuardrailType = feature.properties.GUARDRAIL_TYPE; + + guardrails.Add(guardrail); + } + } + + return guardrails; + } + catch (System.Exception ex) + { + _logger.LogError($"Exception - GetGuardrailAssociatedWithLine: {body} - {contents}"); + throw ex; + } + } + + public async Task> GetBridgeStructure(string rfiSegment) + { + var query = ""; + var content = ""; + + try + { + query = _path + string.Format(_queries.StructureOnRfiSegment, rfiSegment); + + content = await (await _api.GetWithRetry(_client, query)).Content.ReadAsStringAsync(); + + var results = JsonSerializer.Deserialize>(content); + + var structures = new List(); + + foreach (var feature in results.features) + { + Structure structure = new Structure(); + structure.StructureType = feature.properties.BMIS_STRUCTURE_TYPE; //this may possibly be feature.properties.IIT_INV_TYPE + structure.BeginKM = feature.properties.BEGIN_KM; + structure.EndKM = feature.properties.END_KM; + structure.Length = feature.properties.LENGTH_KM; + + structures.Add(structure); + } + + return structures; + } + catch (Exception ex) + { + _logger.LogError($"Exception - GetBridgeStructure: {query} - {content}"); + throw ex; + } } } } diff --git a/api/Hmcr.Chris/InventoryQueries.cs b/api/Hmcr.Chris/InventoryQueries.cs index d4015b52..e61041bf 100644 --- a/api/Hmcr.Chris/InventoryQueries.cs +++ b/api/Hmcr.Chris/InventoryQueries.cs @@ -5,26 +5,28 @@ namespace Hmcr.Chris { public class InventoryQueries { - private string _surfaceTypeAssocWithLineQuery; - private string _surfaceTypeAssocWithPointQuery; + private string _inventoryAssocWithLineQuery; + private string _inventoryAssocWithPointQuery; - public string SurfaceTypeAssocWithLineQuery + public string InventoryAssocWithLineQuery { get { var folder = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "XmlTemplates"); - return _surfaceTypeAssocWithLineQuery ?? (_surfaceTypeAssocWithLineQuery = File.ReadAllText(Path.Combine(folder, "GetInventoryAssocWithWorkActivity.xml"))); + return _inventoryAssocWithLineQuery ?? (_inventoryAssocWithLineQuery = File.ReadAllText(Path.Combine(folder, "GetInventoryAssocWithWorkActivity.xml"))); } } - public string SurfaceTypeAssocWithPointQuery + public string InventoryAssocWithPointQuery { get { var folder = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "XmlTemplates"); - return _surfaceTypeAssocWithPointQuery ?? (_surfaceTypeAssocWithPointQuery = File.ReadAllText(Path.Combine(folder, "GetInventoryAssocWithWorkActivity.xml"))); + return _inventoryAssocWithPointQuery ?? (_inventoryAssocWithPointQuery = File.ReadAllText(Path.Combine(folder, "GetInventoryAssocWithWorkActivity.xml"))); } } + public readonly string StructureOnRfiSegment + = "service=WFS&version=1.1.0&request=GetFeature&typeName=cwr:BSR_BY_RFI&srsName=EPSG:4326&outputFormat=application/json&cql_filter=RFI_UNIQUE='{0}'"; } } diff --git a/api/Hmcr.Chris/Models/Guardrail.cs b/api/Hmcr.Chris/Models/Guardrail.cs new file mode 100644 index 00000000..c7675dab --- /dev/null +++ b/api/Hmcr.Chris/Models/Guardrail.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Hmcr.Chris.Models +{ + public class Guardrail + { + public Geometry geometry { get; set; } + /// + /// Length in Meters + /// + public string GuardrailType { get; set; } + public double Length { get; set; } + } +} diff --git a/api/Hmcr.Chris/Models/HighwayProfile.cs b/api/Hmcr.Chris/Models/HighwayProfile.cs new file mode 100644 index 00000000..4f75182c --- /dev/null +++ b/api/Hmcr.Chris/Models/HighwayProfile.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Hmcr.Chris.Models +{ + public class HighwayProfile + { + public Geometry geometry { get; set; } + /// + /// Length in Meters + /// + public int NumberOfLanes { get; set; } + public string DividedHighwayFlag { get; set; } + public double Length { get; set; } + } +} diff --git a/api/Hmcr.Chris/Models/MaintenanceClass.cs b/api/Hmcr.Chris/Models/MaintenanceClass.cs new file mode 100644 index 00000000..e18fba1b --- /dev/null +++ b/api/Hmcr.Chris/Models/MaintenanceClass.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Hmcr.Chris.Models +{ + public class MaintenanceClass + { + + public Geometry geometry { get; set; } + /// + /// Length in Meters + /// + public string SummerRating { get; set; } + public string WinterRating { get; set; } + public double Length { get; set; } + } +} diff --git a/api/Hmcr.Chris/Models/Property.cs b/api/Hmcr.Chris/Models/Property.cs index e5dc0b39..db3367d0 100644 --- a/api/Hmcr.Chris/Models/Property.cs +++ b/api/Hmcr.Chris/Models/Property.cs @@ -7,7 +7,25 @@ public class Property public string NE_DESCR { get; set; } public float MEASURE { get; set; } public double POINT_VARIANCE { get; set; } - public string SURFACE_TYPE { get; set; } + // clipped length used by all inventory queries public double CLIPPED_LENGTH_KM { get; set; } + // used only by surface type queries + public string SURFACE_TYPE { get; set; } + // used only by maintenace class queries + public string SUMMER_CLASS_RATING { get; set; } + public string WINTER_CLASS_RATING { get; set; } + public string SCHOOL_BUS_ROUTE { get; set; } //future use? + // used only by highway profile queries + public int NUMBER_OF_LANES { get; set; } + public string DIVIDED_HIGHWAY_FLAG { get; set; } + // used only by guardrail queries + public string GUARDRAIL_TYPE { get; set; } + // used only by structure queries + public string RFI_UNIQUE { get; set; } + public string IIT_INV_TYPE { get; set; } + public decimal BEGIN_KM { get; set; } + public decimal END_KM { get; set; } + public double LENGTH_KM { get; set; } + public string BMIS_STRUCTURE_TYPE { get; set; } } } diff --git a/api/Hmcr.Chris/Models/Structure.cs b/api/Hmcr.Chris/Models/Structure.cs new file mode 100644 index 00000000..9456588c --- /dev/null +++ b/api/Hmcr.Chris/Models/Structure.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Hmcr.Chris.Models +{ + public class Structure + { + public Geometry geometry { get; set; } + /// + /// Length in Meters + /// + public string StructureType { get; set; } + public decimal BeginKM { get; set; } + public decimal EndKM { get; set; } + public double Length { get; set; } + } +} diff --git a/api/Hmcr.Chris/Models/SurfaceType.cs b/api/Hmcr.Chris/Models/SurfaceType.cs index 3d4f8ba9..7007da65 100644 --- a/api/Hmcr.Chris/Models/SurfaceType.cs +++ b/api/Hmcr.Chris/Models/SurfaceType.cs @@ -16,16 +16,4 @@ public class SurfaceType public string Type { get; set; } } - public class SurfaceType - { - - public Geometry geometry { get; set; } - /// - /// Length in Meters - /// - public double Length { get; set; } - - public string Type { get; set; } - - } } diff --git a/api/Hmcr.Chris/XmlTemplates/GetInventoryAssocWithWorkActivity.xml b/api/Hmcr.Chris/XmlTemplates/GetInventoryAssocWithWorkActivity.xml index 72be03d3..7b0167f0 100644 --- a/api/Hmcr.Chris/XmlTemplates/GetInventoryAssocWithWorkActivity.xml +++ b/api/Hmcr.Chris/XmlTemplates/GetInventoryAssocWithWorkActivity.xml @@ -2,7 +2,7 @@ version="1.1.0" outputFormat="json" maxFeatures="50" - viewParams="epsg:4326;coordinates:{0}" + viewParams="epsg:4326;{0}:{1}" xmlns:topp="http://www.openplans.org/topp" xmlns:wfs="http://www.opengis.net/wfs" xmlns="http://www.opengis.net/ogc" @@ -10,5 +10,5 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.1.0/WFS-basic.xsd"> - + \ No newline at end of file diff --git a/api/Hmcr.Data/Repositories/ActivityCodeRepository.cs b/api/Hmcr.Data/Repositories/ActivityCodeRepository.cs index 7c182cc1..28303fdc 100644 --- a/api/Hmcr.Data/Repositories/ActivityCodeRepository.cs +++ b/api/Hmcr.Data/Repositories/ActivityCodeRepository.cs @@ -17,6 +17,7 @@ public interface IActivityCodeRepository { Task> GetActiveActivityCodesAsync(); Task> GetActiveActivityCodesLiteAsync(); + Task> GetActiveActivityCodesByActivityNumbersAsync(List actNumbers); Task> GetActivityCodesAsync(string[]? maintenanceTypes, decimal[]? locationCodes, bool? isActive, string searchText, int pageSize, int pageNumber, string orderBy, string direction); Task GetActivityCodeAsync(decimal id); Task CreateActivityCodeAsync(ActivityCodeCreateDto activityCode); @@ -39,10 +40,30 @@ public async Task> GetActiveActivityCodesAsync() { var activities = await DbSet .Include(x => x.LocationCode) + .Include(x => x.HmrServiceAreaActivities) .ToListAsync(); return Mapper.Map>(activities).Where(x => x.IsActive); } + public async Task> GetActiveActivityCodesByActivityNumbersAsync(List actNumbers) + { + var activities = await DbSet + .Include(x => x.LocationCode) + .Include(x => x.HmrServiceAreaActivities) + .Where(s => actNumbers.Contains(s.ActivityNumber)) + .ToListAsync(); + + var activityCodes = Mapper.Map>(activities).Where(x => x.IsActive); + + foreach (var activityCode in activityCodes) + { + HmrActivityCode act = + activities + .Where(x => x.ActivityCodeId == activityCode.ActivityCodeId).FirstOrDefault(); + activityCode.ServiceAreaNumbers = act.HmrServiceAreaActivities.Select(y=>y.ServiceAreaNumber).ToList(); + } + return activityCodes; + } public async Task> GetActiveActivityCodesLiteAsync() { diff --git a/api/Hmcr.Data/Repositories/ActivityRuleRepository.cs b/api/Hmcr.Data/Repositories/ActivityRuleRepository.cs index 142a2ddf..31a8c0c6 100644 --- a/api/Hmcr.Data/Repositories/ActivityRuleRepository.cs +++ b/api/Hmcr.Data/Repositories/ActivityRuleRepository.cs @@ -30,7 +30,7 @@ public ActivityRuleRepository(AppDbContext dbContext, IMapper mapper) public IEnumerable LoadActivityCodeRuleCache() { return DbSet.AsNoTracking() - .Where(x => x.EndDate == null || DateTime.Today < x.EndDate) + .Where(s => s.EndDate == null || s.EndDate > DateTime.Today) .Select(x => new ActivityCodeRuleCache { @@ -45,7 +45,9 @@ public IEnumerable LoadActivityCodeRuleCache() public async Task> GetRoadLengthRulesAsync() { var activityRules = await DbSet.AsNoTracking() + .Where(s => s.EndDate == null || s.EndDate > DateTime.Today) .Where(s => s.ActivityRuleSet.ToUpper() == "ROAD_LENGTH") + .OrderBy(s => s.DisplayOrder) .ToListAsync(); return Mapper.Map>(activityRules); @@ -53,7 +55,9 @@ public async Task> GetRoadLengthRulesAsync() public async Task> GetSurfaceTypeRulesAsync() { var activityRules = await DbSet.AsNoTracking() + .Where(s => s.EndDate == null || s.EndDate > DateTime.Today) .Where(s => s.ActivityRuleSet.ToUpper() == "SURFACE_TYPE") + .OrderBy(s => s.DisplayOrder) .ToListAsync(); return Mapper.Map>(activityRules); @@ -61,7 +65,9 @@ public async Task> GetSurfaceTypeRulesAsync() public async Task> GetRoadClassRulesAsync() { var activityRules = await DbSet.AsNoTracking() + .Where(s => s.EndDate == null || s.EndDate > DateTime.Today) .Where(s => s.ActivityRuleSet.ToUpper() == "ROAD_CLASS") + .OrderBy(s => s.DisplayOrder) .ToListAsync(); return Mapper.Map>(activityRules); @@ -70,7 +76,9 @@ public async Task> GetRoadClassRulesAsync() public async Task> GetDefaultRules() { var activityRules = await DbSet.AsNoTracking() + .Where(s => s.EndDate == null || s.EndDate > DateTime.Today) .Where(s => s.ActivityRuleName.ToUpper() == "NOT APPLICABLE") + .OrderBy(s => s.DisplayOrder) .ToListAsync(); return Mapper.Map>(activityRules); diff --git a/api/Hmcr.Data/Repositories/WorkReportRepository.cs b/api/Hmcr.Data/Repositories/WorkReportRepository.cs index 899a4f4f..56c1a64b 100644 --- a/api/Hmcr.Data/Repositories/WorkReportRepository.cs +++ b/api/Hmcr.Data/Repositories/WorkReportRepository.cs @@ -1,8 +1,10 @@ using AutoMapper; using Hmcr.Data.Database.Entities; using Hmcr.Data.Repositories.Base; +using Hmcr.Model; using Hmcr.Model.Dtos.WorkReport; using Microsoft.EntityFrameworkCore; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -14,6 +16,10 @@ public interface IWorkReportRepository IAsyncEnumerable SaveWorkReportAsnyc(HmrSubmissionObject submission, List workReports); Task> ExportReportAsync(decimal submissionObjectId); Task IsActivityNumberInUseAsync(string activityNumber); + Task IsReportedWorkReportForLocationAAsync(WorkReportTyped workReportTyped); + Task IsReportedWorkReportForLocationBAsync(WorkReportTyped workReportTyped); + Task IsReportedWorkReportForLocationCPointAsync(WorkReportTyped workReportTyped); + Task IsReportedWorkReportForLocationCLineAsync(WorkReportTyped workReportTyped); } public class WorkReportRepository : HmcrRepositoryBase, IWorkReportRepository, IReportExportRepository { @@ -67,5 +73,79 @@ public async Task IsActivityNumberInUseAsync(string activityNumber) { return await DbSet.AnyAsync(wr => wr.ActivityNumber == activityNumber); } + + public async Task IsReportedWorkReportForLocationAAsync(WorkReportTyped workReportTyped) + { + if (workReportTyped.ActivityCodeValidation.ReportingFrequency == null + || workReportTyped.ActivityCodeValidation.ReportingFrequency < 1) return false; + DateTime eDate = workReportTyped.EndDate.GetValueOrDefault(DateTime.Today).Date; + int days = (int)workReportTyped.ActivityCodeValidation.ReportingFrequency; + DateTime sDate = eDate.AddDays(-days); + return await DbContext.HmrWorkReportVws.AsNoTracking() + .Where(x => x.ActivityNumber == workReportTyped.ActivityNumber + && x.ServiceArea == workReportTyped.ServiceArea + && (x.EndDate > sDate && x.EndDate <= eDate) + && x.ValidationStatus.ToUpper().StartsWith(RowStatus.RowSuccess) + ) + .AnyAsync(); + } + public async Task IsReportedWorkReportForLocationBAsync(WorkReportTyped workReportTyped) + { + if (workReportTyped.ActivityCodeValidation.ReportingFrequency == null + || workReportTyped.ActivityCodeValidation.ReportingFrequency < 1) return false; + DateTime eDate = workReportTyped.EndDate.GetValueOrDefault(DateTime.Today).Date; + int days = (int)workReportTyped.ActivityCodeValidation.ReportingFrequency; + DateTime sDate = eDate.AddDays(-days); + return await DbContext.HmrWorkReportVws.AsNoTracking() + .Where(x => x.ActivityNumber == workReportTyped.ActivityNumber + && x.ServiceArea == workReportTyped.ServiceArea + && x.HighwayUnique.ToLower() == workReportTyped.HighwayUnique.ToLower() + && (x.EndDate > sDate && x.EndDate <= eDate) + && x.ValidationStatus.ToUpper().StartsWith(RowStatus.RowSuccess) + ) + .AnyAsync(); + } + public async Task IsReportedWorkReportForLocationCPointAsync(WorkReportTyped workReportTyped) + { + decimal md = (decimal)0.1; //100m + decimal startOffset = (decimal)workReportTyped.StartOffset - md; + decimal endOffset = (decimal)workReportTyped.StartOffset + md; + if (workReportTyped.ActivityCodeValidation.ReportingFrequency == null + || workReportTyped.ActivityCodeValidation.ReportingFrequency < 1) return false; + DateTime eDate = workReportTyped.EndDate.GetValueOrDefault(DateTime.Today).Date; + int days = (int)workReportTyped.ActivityCodeValidation.ReportingFrequency; + DateTime sDate = eDate.AddDays(-days); + return await DbContext.HmrWorkReportVws.AsNoTracking() + .Where(x => x.ActivityNumber == workReportTyped.ActivityNumber + && x.ServiceArea == workReportTyped.ServiceArea + && x.HighwayUnique.ToLower() == workReportTyped.HighwayUnique.ToLower() + && (x.EndDate > sDate && x.EndDate <= eDate) + && (x.StartOffset != null && x.EndOffset == null) + && (x.StartOffset> startOffset && x.StartOffset< endOffset) + && x.ValidationStatus.ToUpper().StartsWith(RowStatus.RowSuccess) + ) + .AnyAsync(); + } + public async Task IsReportedWorkReportForLocationCLineAsync(WorkReportTyped workReportTyped) + { + decimal md = (decimal)0.1; //100m + decimal startOffset = (decimal)workReportTyped.StartOffset - md; + decimal endOffset = (decimal)workReportTyped.EndOffset + md; + if (workReportTyped.ActivityCodeValidation.ReportingFrequency == null + || workReportTyped.ActivityCodeValidation.ReportingFrequency < 1) return false; + DateTime eDate = workReportTyped.EndDate.GetValueOrDefault(DateTime.Today).Date; + int days = (int)workReportTyped.ActivityCodeValidation.ReportingFrequency; + DateTime sDate = eDate.AddDays(-days); + return await DbContext.HmrWorkReportVws.AsNoTracking() + .Where(x => x.ActivityNumber == workReportTyped.ActivityNumber + && x.ServiceArea == workReportTyped.ServiceArea + && x.HighwayUnique.ToLower() == workReportTyped.HighwayUnique.ToLower() + && (x.EndDate > sDate && x.EndDate <= eDate) + && (x.StartOffset != null && x.EndOffset != null) + && (x.StartOffset > startOffset || x.EndOffset < endOffset) + && x.ValidationStatus.ToUpper().StartsWith(RowStatus.RowSuccess) + ) + .AnyAsync(); + } } } diff --git a/api/Hmcr.Domain/Hangfire/WorkReportJobService.cs b/api/Hmcr.Domain/Hangfire/WorkReportJobService.cs index 5f21999d..bcb08c8c 100644 --- a/api/Hmcr.Domain/Hangfire/WorkReportJobService.cs +++ b/api/Hmcr.Domain/Hangfire/WorkReportJobService.cs @@ -61,9 +61,10 @@ public override async Task ProcessSubmission(SubmissionDto submissionDto) if (!await SetSubmissionAsync(submissionDto)) return false; - var activityCodes = await _activityRepo.GetActiveActivityCodesAsync(); - + //var activityCodes = await _activityRepo.GetActiveActivityCodesAsync(); var (untypedRows, headers) = ParseRowsUnTyped(errors); + List untypedActivityNumbers = untypedRows.Select(o => o.ActivityNumber).Distinct().ToList(); + var activityCodes = await _activityRepo.GetActiveActivityCodesByActivityNumbersAsync(untypedActivityNumbers); //text after duplicate lines are removed. Will be used for importing to typed DTO. var (rowCount, text) = await SetRowIdAndRemoveDuplicate(untypedRows, headers); @@ -72,11 +73,11 @@ public override async Task ProcessSubmission(SubmissionDto submissionDto) { errors.AddItem("File", "No new records were found in the file; all records were already processed in the past submission."); _submission.ErrorDetail = errors.GetErrorDetail(); - _submission.SubmissionStatusId = _statusService.FileDuplicate; + _submission.SubmissionStatusId = _statusService.FileDuplicate; // Stage 1 - Duplicate Submission await CommitAndSendEmailAsync(); return true; } - + //stage 2 validation foreach (var untypedRow in untypedRows) { errors = new Dictionary>(); @@ -90,7 +91,7 @@ public override async Task ProcessSubmission(SubmissionDto submissionDto) if (activityCode == null) { errors.AddItem(Fields.ActivityNumber, $"Invalid activity number[{untypedRow.ActivityNumber}]"); - SetErrorDetail(submissionRow, errors, _statusService.FileBasicError); + SetErrorDetail(submissionRow, errors, _statusService.FileBasicError); //Stage 2 - Basic Error continue; } @@ -102,15 +103,15 @@ public override async Task ProcessSubmission(SubmissionDto submissionDto) _validator.Validate(entityName, untypedRow, errors); - //stage 2 validation PerformFieldValidation(errors, untypedRow, activityCode); if (errors.Count > 0) { - SetErrorDetail(submissionRow, errors, _statusService.FileBasicError); + SetErrorDetail(submissionRow, errors, _statusService.FileBasicError); //Stage 2 - Basic Error } } + //stage 3 validation var typedRows = new List(); if (_submission.SubmissionStatusId == _statusService.FileInProgress) @@ -120,7 +121,7 @@ public override async Task ProcessSubmission(SubmissionDto submissionDto) if (rowNum != 0) { var submissionRow = _submissionRows[rowNum]; - SetErrorDetail(submissionRow, errors, _statusService.FileConflictionError); + SetErrorDetail(submissionRow, errors, _statusService.FileConflictionError); //Stage 3 - Confliction Error await CommitAndSendEmailAsync(); return true; } @@ -128,8 +129,6 @@ public override async Task ProcessSubmission(SubmissionDto submissionDto) typedRows = rows; CopyCalculatedFieldsFormUntypedRow(typedRows, untypedRows); - - //stage 3 validation? PerformAdditionalValidation(typedRows); } @@ -140,6 +139,12 @@ public override async Task ProcessSubmission(SubmissionDto submissionDto) } //stage 4 validation starts + PerformFieldServiceAreaValidation(typedRows); + if (_submission.SubmissionStatusId != _statusService.FileInProgress) + { + await CommitAndSendEmailAsync(); + return true; + } var workReports = PerformSpatialValidationAndConversionBatchAsync(typedRows); @@ -151,15 +156,19 @@ public override async Task ProcessSubmission(SubmissionDto submissionDto) return true; } - //perform surfacetype lookup and validation - workReports = PerformSurfaceTypeValidationBatchAsync(workReports); + workReports = PerformAnalyticalValidationBatchAsync(workReports); if (_submission.SubmissionStatusId != _statusService.FileInProgress) { await CommitAndSendEmailAsync(); return true; } - + await PerformReportedWorkReportsValidationAsync(workReports); + if (_submission.SubmissionStatusId != _statusService.FileInProgress) + { + await CommitAndSendEmailAsync(); + return true; + } _submission.SubmissionStatusId = _statusService.FileSuccess; //stage 4 validation ends @@ -170,30 +179,85 @@ public override async Task ProcessSubmission(SubmissionDto submissionDto) return true; } - private List PerformSurfaceTypeValidationBatchAsync(List workReports) + #region Validation Batch Processes + + private List PerformAnalyticalValidationBatchAsync(List workReports) { MethodLogger.LogEntry(_logger, _enableMethodLog, _methodLogHeader, $"Total Record: {workReports.Count}"); + //DateTime StartAt = DateTime.Now; //get surface type not applicable id + var notApplicableRoadLengthId = _validator.ActivityCodeRuleLookup + .Where(x => x.ActivityRuleSet == ActivityRuleType.RoadLength) + .Where(x => x.ActivityRuleExecName == RoadLengthRules.NA) + .FirstOrDefault().ActivityCodeRuleId; + + //get surface type not applicable id + var notApplicableMaintenanceClassId = _validator.ActivityCodeRuleLookup + .Where(x => x.ActivityRuleSet == ActivityRuleType.RoadClass) + .Where(x => x.ActivityRuleExecName == MaintenanceClassRules.NA) + .FirstOrDefault().ActivityCodeRuleId; + var notApplicableSurfaceTypeId = _validator.ActivityCodeRuleLookup .Where(x => x.ActivityRuleSet == ActivityRuleType.SurfaceType) .Where(x => x.ActivityRuleExecName == SurfaceTypeRules.NA) .FirstOrDefault().ActivityCodeRuleId; + var updatedWorkReports = new ConcurrentBag(); + var updatedWorkReportTypeds = new ConcurrentBag(); + var progress = 0; + + foreach (var workReport in workReports.Where(x => x.WorkReportTyped.ActivityCodeValidation.LocationCode == "C")) + { + var tasklist = new List(); + + if ((workReport.WorkReportTyped.ActivityCodeValidation.RoadClassRuleId != 0) + && (workReport.WorkReportTyped.ActivityCodeValidation.RoadClassRuleId != notApplicableMaintenanceClassId)) + { + tasklist.Add(Task.Run(async () => updatedWorkReports.Add(await PerformMaintenanceClassValidationAsync(workReport)))); + } + if ((workReport.WorkReportTyped.ActivityCodeValidation.SurfaceTypeRuleId != 0) + && (workReport.WorkReportTyped.ActivityCodeValidation.SurfaceTypeRuleId != notApplicableSurfaceTypeId)) + { + tasklist.Add(Task.Run(async () => updatedWorkReports.Add(await PerformSurfaceTypeValidationAsync(workReport)))); + } + + Task.WaitAll(tasklist.ToArray()); + + progress += 1; + + if (progress % 100 == 0) + { + _logger.LogInformation($"{_methodLogHeader} PerformAnalyticalValidationBatchAsync {progress}"); + } + } + + /*** Time profiling *** + * DateTime EndAt = DateTime.Now; + TimeSpan TimeDifference = EndAt - StartAt; + Console.WriteLine("Total Duration in milliseconds: {0}", TimeDifference.TotalMilliseconds.ToString());*/ + + return workReports.ToList(); + } + + private List PerformSpatialValidationAndConversionBatchAsync(List typedRows) + { + MethodLogger.LogEntry(_logger, _enableMethodLog, _methodLogHeader, $"Total Record: {typedRows.Count}"); + //grouping the rows - var groups = new List>(); - var currentGroup = new List(); + var groups = new List>(); + var currentGroup = new List(); var count = 0; - foreach (var workReport in workReports) + foreach (var typedRow in typedRows) { - currentGroup.Add(workReport); + currentGroup.Add(typedRow); count++; if (count % 10 == 0) { groups.Add(currentGroup); - currentGroup = new List(); + currentGroup = new List(); } } @@ -202,20 +266,16 @@ private List PerformSurfaceTypeValidationBatchAsync(List(); + var geometries = new ConcurrentBag(); var progress = 0; foreach (var group in groups) { var tasklist = new List(); - - //don't batch if not location code C - foreach (var row in group.Where(x => x.WorkReportTyped.ActivityCodeValidation.LocationCode == "C")) + + foreach (var row in group) { - if (row.WorkReportTyped.ActivityCodeValidation.SurfaceTypeRuleId != notApplicableSurfaceTypeId) - { - tasklist.Add(Task.Run(async () => updatedWorkReports.Add(await PerformSurfaceTypeValidationAsync(row)))); - } + tasklist.Add(Task.Run(async () => geometries.Add(await PerformSpatialValidationAndConversionAsync(row)))); } Task.WaitAll(tasklist.ToArray()); @@ -224,33 +284,141 @@ private List PerformSurfaceTypeValidationBatchAsync(List PerformSurfaceTypeValidationAsync(WorkReportGeometry row) + #endregion + + #region Async Validation Functions + + private async Task> PerformReportedWorkReportsValidationAsync(List workReports) + { + foreach (var workReport in workReports) + { + await PerformReportedWorkReportValidationAsync(workReport.WorkReportTyped); + } + return workReports; + } + private async Task PerformReportedWorkReportValidationAsync(WorkReportTyped typedRow) + { + var warnings = new Dictionary>(); + var submissionRow = _submissionRows[(decimal)typedRow.RowNum]; + string locationCode = typedRow.ActivityCodeValidation.LocationCode.ToUpper(); + if (locationCode == "A" && await _workReportRepo.IsReportedWorkReportForLocationAAsync(typedRow)) + { + warnings.AddItem("Reporting Frequency Validation: End Date" + , $"(Location Code = 'A'): END DATE Should NOT be reported more frequently" + + $" than the Reporting Frequency(days) of [{ typedRow.ActivityCodeValidation.ReportingFrequency}]" + + $" for Activity[{ typedRow.ActivityNumber}]"); + } + else if (locationCode == "B" && await _workReportRepo.IsReportedWorkReportForLocationBAsync(typedRow)) + { + warnings.AddItem("Reporting Frequency Validation: End Date" + , $"(Location Code = 'B'): END DATE Should NOT be reported more frequently" + + $" than the Reporting Frequency(days) of [{ typedRow.ActivityCodeValidation.ReportingFrequency}]" + + $" for Activity[{ typedRow.ActivityNumber}]" + + $" for Highway Unique [{typedRow.HighwayUnique}]"); + } + else if (locationCode == "C" + && typedRow .FeatureType.ToLower() == "point" + && await _workReportRepo.IsReportedWorkReportForLocationCPointAsync(typedRow)) + { + warnings.AddItem("Reporting Frequency Validation: End Date, GPS position" + , $"(Location Code = 'C'; {typedRow.FeatureType}): END DATE Should NOT be reported more frequently" + + $" than the Reporting Frequency(days) of [{ typedRow.ActivityCodeValidation.ReportingFrequency}]" + + $" for Activity[{ typedRow.ActivityNumber}]," + + $" Highway Unique [{typedRow.HighwayUnique}]," + + $" GPS position [{typedRow.StartLatitude},{typedRow.StartLongitude}] with a tolerance of -/+ 100M"); + } + else if (locationCode == "C" + && typedRow.FeatureType.ToLower() == "line" + && await _workReportRepo.IsReportedWorkReportForLocationCLineAsync(typedRow)) + { + warnings.AddItem("Reporting Frequency Validation: End Date, GPS position" + , $"(Location Code = 'C'; {typedRow.FeatureType}): END DATE Should NOT be reported more frequently" + + $" than the Reporting Frequency(days) of [{ typedRow.ActivityCodeValidation.ReportingFrequency}]" + + $" for Activity[{ typedRow.ActivityNumber}]," + + $" Highway Unique [{typedRow.HighwayUnique}]," + + $" GPS position from [{typedRow.StartLatitude},{typedRow.StartLongitude}]" + + $" to [{typedRow.EndLatitude},{typedRow.EndLongitude}] with a tolerance of -/+ 100M"); + } + if (warnings.Count > 0) + { + SetWarningDetail(submissionRow, warnings); + } + return typedRow; + } + private async Task PerformMaintenanceClassValidationAsync(WorkReportGeometry row) { var errors = new Dictionary>(); var typedRow = row.WorkReportTyped; - var geometry = row.Geometry; var submissionRow = _submissionRows[(decimal)typedRow.RowNum]; - typedRow.RoadFeatures = new List(); + typedRow.MaintenanceClasses = new List(); - //build Geometry linestring - var geometryLineString = ""; - foreach (Coordinate coordinate in geometry.Coordinates) + //surface type calls are different between Point & Line + if (typedRow.FeatureType == FeatureType.Point) { - geometryLineString += coordinate.X + "\\," + coordinate.Y; - geometryLineString += (coordinate != geometry.Coordinates.Last()) ? "\\," : ""; + var result = await _spatialService.GetMaintenanceClassAssocWithPointAsync(row.Geometry); + + if (result.result == SpValidationResult.Fail) + { + SetErrorDetail(submissionRow, errors, _statusService.FileLocationError); + } + else if (result.result == SpValidationResult.Success) + { + if (result.maintenanceClass != null) //only add if type was returned + { + WorkReportMaintenanceClass roadClass = new WorkReportMaintenanceClass(); + roadClass.WinterRating = result.maintenanceClass.WinterRating; + roadClass.SummerRating = result.maintenanceClass.SummerRating; + roadClass.RoadLength = result.maintenanceClass.Length; + typedRow.MaintenanceClasses.Add(roadClass); + } + + PerformMaintenanceClassValidation(typedRow); + } + } - + else if (typedRow.FeatureType == FeatureType.Line) + { + var result = await _spatialService.GetMaintenanceClassAssocWithLineAsync(row.Geometry); + if (result.result == SpValidationResult.Fail) + { + SetErrorDetail(submissionRow, errors, _statusService.FileLocationError); + } + else if (result.result == SpValidationResult.Success) + { + foreach (var maintenanceClass in result.maintenanceClasses) + { + WorkReportMaintenanceClass roadClass = new WorkReportMaintenanceClass(); + roadClass.WinterRating = maintenanceClass.WinterRating; + roadClass.SummerRating = maintenanceClass.SummerRating; + roadClass.RoadLength = maintenanceClass.Length; + typedRow.MaintenanceClasses.Add(roadClass); + } + + PerformMaintenanceClassValidation(typedRow); + } + } + + return row; + } + + private async Task PerformSurfaceTypeValidationAsync(WorkReportGeometry row) + { + var errors = new Dictionary>(); + var typedRow = row.WorkReportTyped; + var submissionRow = _submissionRows[(decimal)typedRow.RowNum]; + typedRow.SurfaceTypes = new List(); + //surface type calls are different between Point & Line if (typedRow.FeatureType == FeatureType.Point) { - var result = await _spatialService.GetSurfaceTypeAssocWithPointAsync(geometryLineString); + var result = await _spatialService.GetSurfaceTypeAssocWithPointAsync(row.Geometry); if (result.result == SpValidationResult.Fail) { @@ -260,15 +428,18 @@ private async Task PerformSurfaceTypeValidationAsync(WorkRep { if (result.surfaceType.Type != null) //only add if type was returned { - typedRow.RoadFeatures.Add(new WorkReportRoadFeature(result.surfaceType.Type, result.surfaceType.Length)); + WorkReportSurfaceType roadFeature = new WorkReportSurfaceType(); + roadFeature.SurfaceLength = result.surfaceType.Length; + roadFeature.SurfaceType = result.surfaceType.Type; + typedRow.SurfaceTypes.Add(roadFeature); } - PerformSurfaceTypeValidation(typedRow); + await PerformSurfaceTypeValidation(typedRow); } - } + } else if (typedRow.FeatureType == FeatureType.Line) { - var result = await _spatialService.GetSurfaceTypeAssocWithLineAsync(geometryLineString); + var result = await _spatialService.GetSurfaceTypeAssocWithLineAsync(row.Geometry); if (result.result == SpValidationResult.Fail) { @@ -280,120 +451,297 @@ private async Task PerformSurfaceTypeValidationAsync(WorkRep foreach (string type in distinctTypes) { var totalLengthOfType = result.surfaceTypes.Where(x => x.Type == type).Sum(x => x.Length); - typedRow.RoadFeatures.Add(new WorkReportRoadFeature(type, totalLengthOfType)); + + WorkReportSurfaceType roadFeature = new WorkReportSurfaceType(); + roadFeature.SurfaceLength = totalLengthOfType; + roadFeature.SurfaceType = type; + typedRow.SurfaceTypes.Add(roadFeature); } - - PerformSurfaceTypeValidation(typedRow); + await PerformSurfaceTypeValidation(typedRow); } } return row; } - private void PerformSurfaceTypeValidation(WorkReportTyped typedRow) + private async Task PerformSpatialValidationAndConversionAsync(WorkReportTyped typedRow) + { + var submissionRow = _submissionRows[(decimal)typedRow.RowNum]; + var workReport = new WorkReportGeometry(typedRow, null); + if (typedRow.SpatialData == SpatialData.Gps) + { + await PerformSpatialGpsValidation(workReport, submissionRow); + + SetVarianceWarningDetail(submissionRow, typedRow.HighwayUnique, + GetGpsString(typedRow.StartLatitude, typedRow.StartLongitude), + GetGpsString(typedRow.EndLatitude, typedRow.EndLongitude), + typedRow.SpThresholdLevel); + } + else if (typedRow.SpatialData == SpatialData.Lrs) + { + await PerformSpatialLrsValidation(workReport, submissionRow); + + SetVarianceWarningDetail(submissionRow, typedRow.HighwayUnique, + GetOffsetString(typedRow.StartOffset), + GetOffsetString(typedRow.EndOffset), + typedRow.SpThresholdLevel); + } + + return workReport; + } + + #endregion + + #region Validation Routines + + private void PerformAnalyticalFieldValidation(WorkReportTyped typedRow) + { + var submissionRow = _submissionRows[(decimal)typedRow.RowNum]; + var warnings = new Dictionary>(); + if (typedRow.ActivityCodeValidation.MinValue != null && typedRow.ActivityCodeValidation.MaxValue != null) + { + string accomplishment = typedRow.Accomplishment.ToString(); + string minValue = typedRow.ActivityCodeValidation.MinValue.ConvertDecimalToStringAndRemoveTrailing(); //remove trailing 0 + string maxValue = typedRow.ActivityCodeValidation.MaxValue.ConvertDecimalToStringAndRemoveTrailing(); + if ((new[] { "site", "num", "ea" }).Contains(typedRow.UnitOfMeasure.ToLowerInvariant()) && !accomplishment.IsInteger()) + { + warnings.AddItem("Data Precision Validation: Accomplishment", + $"Accomplishment value of [{accomplishment}] should be a whole number for Unit of Measure [{typedRow.UnitOfMeasure}]" + + $" for Activity Code [{typedRow.ActivityNumber}]"); + } + if (accomplishment.ConvertStrToDecimal() < typedRow.ActivityCodeValidation.MinValue.ConvertNullableDecimal()) + { + warnings.AddItem("Minimum / Maximum Value Validation: Accomplishment", + $"Accomplishment value of [{accomplishment}]" + + $" should be >= the Minimum Value [{minValue}] allowed for the Activity [{typedRow.ActivityNumber}]"); + } + if (accomplishment.ConvertStrToDecimal() > typedRow.ActivityCodeValidation.MaxValue.ConvertNullableDecimal()) + { + warnings.AddItem("Minimum / Maximum Value Validation: Accomplishment", + $"Accomplishment value of [{accomplishment}]" + + $" should be <= the Maximum Value [{maxValue}] allowed for the Activity [{typedRow.ActivityNumber}]"); + } + } + if (warnings.Count > 0) + { + SetWarningDetail(submissionRow, warnings); + } + } + + private void PerformMaintenanceClassValidation(WorkReportTyped typedRow) { var warnings = new Dictionary>(); var submissionRow = _submissionRows[(decimal)typedRow.RowNum]; - //get total length of path - var totalLength = typedRow.RoadFeatures.Sum(x => x.SurfaceLength); - - //determine path length; paved, non-paved, unconstructed, other - var pavedLength = typedRow.RoadFeatures.Where(x => x.SurfaceType == RoadSurface.HOT_MIX || - x.SurfaceType == RoadSurface.COLD_MIX || x.SurfaceType == RoadSurface.CONCRETE || - x.SurfaceType == RoadSurface.SURFACE_TREATED).Sum(x => x.SurfaceLength); - - var unpavedLength = typedRow.RoadFeatures.Where(x => x.SurfaceType == RoadSurface.GRAVEL || - x.SurfaceType == RoadSurface.DIRT).Sum(x => x.SurfaceLength); - - var unconstructedLength = typedRow.RoadFeatures.Where(x => x.SurfaceType == RoadSurface.CLEARED || - x.SurfaceType == RoadSurface.UNCLEARED).Sum(x => x.SurfaceLength); + var summerMaintainedRatings = new[] { "1", "2", "3", "4", "5", "6", "7" }; + var winterMaintainedRatings = new[] { "A", "B", "C", "D", "E" }; + + var winterMaintainedLen = typedRow.MaintenanceClasses.Where(x => winterMaintainedRatings.Contains(x.WinterRating)).Sum(x => x.RoadLength); + var winterUnmaintainedLen = typedRow.MaintenanceClasses.Where(x => x.WinterRating == "F").Sum(x => x.RoadLength); + var winterTotal = winterMaintainedLen + winterUnmaintainedLen; + + var summerMaintainedLen = typedRow.MaintenanceClasses.Where(x => summerMaintainedRatings.Contains(x.SummerRating)).Sum(x => x.RoadLength); + var summerUnmaintainedLen = typedRow.MaintenanceClasses.Where(x => x.SummerRating == "8").Sum(x => x.RoadLength); + var summerTotal = summerMaintainedLen + summerUnmaintainedLen; + + //TODO: the percentages need to be configurable using CODE LOOKUP - var otherLength = typedRow.RoadFeatures.Where(x => x.SurfaceType == RoadSurface.OTHER || - x.SurfaceType == RoadSurface.UNKNOWN).Sum(x => x.SurfaceLength); + if (typedRow.FeatureType == FeatureType.Line) + { + var proportionPercMaintenance = _validator.CodeLookup.Where(x => x.CodeSet == CodeSet.ValidatorProportion) + .Where(x => x.CodeName == ValidatorProportionCode.MAINTENANCE_CLASS).First().CodeValueNum; + + if (typedRow.ActivityCodeValidation.RoadClassRuleExec == MaintenanceClassRules.Class8OrF) + { + //determine proportion of not maintained to total, summer & winter + //if proportion of not maintained < 90% throw warning + if (((summerUnmaintainedLen / summerTotal) < (double)(proportionPercMaintenance / 100)) + || ((winterUnmaintainedLen / winterTotal) < (double)(proportionPercMaintenance / 100))) + { + warnings.AddItem("Road Class Validation" + , $"GPS position from [{typedRow.StartLatitude},{typedRow.StartLongitude}] " + + $"to [{typedRow.EndLatitude},{typedRow.EndLongitude}] should be >= {proportionPercMaintenance}% Maintenance Class 8 or F"); + } + } + else if (typedRow.ActivityCodeValidation.RoadClassRuleExec == MaintenanceClassRules.NotClass8OrF) + { + //determine proportion of maintained to total, summer & winter + //if proportion of maintained < 90% throw warning + if (((summerMaintainedLen / summerTotal) < (double)(proportionPercMaintenance / 100)) + || ((winterMaintainedLen / winterTotal) < (double)(proportionPercMaintenance / 100))) + { + warnings.AddItem("Road Class Validation" + , $"GPS position from [{typedRow.StartLatitude},{typedRow.StartLongitude}] " + + $"to [{typedRow.EndLatitude},{typedRow.EndLongitude}] should NOT be >= {proportionPercMaintenance}% Maintenance Class 8 or F"); + } + } + } + else if (typedRow.FeatureType == FeatureType.Point) + { + + if (typedRow.ActivityCodeValidation.RoadClassRuleExec == MaintenanceClassRules.Class8OrF) + { + if (typedRow.MaintenanceClasses.First().WinterRating != "8" || typedRow.MaintenanceClasses.First().SummerRating != "F") + { + warnings.AddItem("Road Class Validation" + , $"GPS position [{typedRow.StartLatitude},{typedRow.StartLongitude}] should be Maintenance Class 8 or F"); + } + } + else if (typedRow.ActivityCodeValidation.RoadClassRuleExec == MaintenanceClassRules.NotClass8OrF) + { + if (typedRow.MaintenanceClasses.First().WinterRating == "8" || typedRow.MaintenanceClasses.First().SummerRating == "F") + { + warnings.AddItem("Road Class Validation" + , $"GPS position [{typedRow.StartLatitude},{typedRow.StartLongitude}] should NOT be Maintenance Class 8 or F"); + } + } + } + + if (warnings.Count > 0) + { + SetWarningDetail(submissionRow, warnings); + } + } + + private async Task PerformSurfaceTypeValidation(WorkReportTyped typedRow) + { + var warnings = new Dictionary>(); + var submissionRow = _submissionRows[(decimal)typedRow.RowNum]; + + var structureVariance = _validator.CodeLookup.Where(x => x.CodeSet == CodeSet.ValidatorProportion) + .Where(x => x.CodeName == ValidatorProportionCode.STRUCTURE_VARIANCE_M).First().CodeValueNum; + var structureVarianceM = structureVariance / 1000; var surfaceTypeRule = typedRow.ActivityCodeValidation.SurfaceTypeRuleExec; if (typedRow.FeatureType == FeatureType.Line) { + //get total length of path + var totalLength = typedRow.SurfaceTypes.Sum(x => x.SurfaceLength); + + //determine path length; paved, non-paved, unconstructed, other + var pavedLength = typedRow.SurfaceTypes.Where(x => x.SurfaceLength > 0) + .Where(x => x.SurfaceType == RoadSurface.HOT_MIX || + x.SurfaceType == RoadSurface.COLD_MIX || x.SurfaceType == RoadSurface.CONCRETE || + x.SurfaceType == RoadSurface.SURFACE_TREATED).Sum(x => x.SurfaceLength); + + var unpavedLength = typedRow.SurfaceTypes.Where(x => x.SurfaceLength > 0) + .Where(x => x.SurfaceType == RoadSurface.GRAVEL || + x.SurfaceType == RoadSurface.DIRT).Sum(x => x.SurfaceLength); + + var unconstructedLength = typedRow.SurfaceTypes.Where(x => x.SurfaceLength > 0) + .Where(x => x.SurfaceType == RoadSurface.CLEARED || + x.SurfaceType == RoadSurface.UNCLEARED).Sum(x => x.SurfaceLength); + + var otherLength = typedRow.SurfaceTypes.Where(x => x.SurfaceLength > 0) + .Where(x => x.SurfaceType == RoadSurface.OTHER || + x.SurfaceType == RoadSurface.UNKNOWN).Sum(x => x.SurfaceLength); + + //get the proportion percentages from the CODELOOKUP table, turn into double & percent + var proportionPercPaved = _validator.CodeLookup.Where(x => x.CodeSet == CodeSet.ValidatorProportion) + .Where(x => x.CodeName == ValidatorProportionCode.SURFACE_TYPE_PAVED).First().CodeValueNum; + var proportionPercUnpaved = (double)_validator.CodeLookup.Where(x => x.CodeSet == CodeSet.ValidatorProportion) + .Where(x => x.CodeName == ValidatorProportionCode.SURFACE_TYPE_UNPAVED).First().CodeValueNum; + var proportionPercUnconstr = (double)_validator.CodeLookup.Where(x => x.CodeSet == CodeSet.ValidatorProportion) + .Where(x => x.CodeName == ValidatorProportionCode.SURFACE_TYPE_UNCONSTRUCTED).First().CodeValueNum; + switch (surfaceTypeRule) { case SurfaceTypeRules.PavedSurface: case SurfaceTypeRules.PavedStructure: - if ((pavedLength / totalLength) >= .8) - { - //nothing wrong - } - else + if ((pavedLength / totalLength) < (double)(proportionPercPaved / 100)) { if (surfaceTypeRule == SurfaceTypeRules.PavedStructure) { //structure checking + var hasBridgeWithinVariance = await WithinBridgeStructureVariance(typedRow, (decimal)structureVarianceM); + if (!hasBridgeWithinVariance) + { + warnings.AddItem("Surface Type Validation", $"GPS position from [{typedRow.StartLatitude},{typedRow.StartLongitude}] to [{typedRow.EndLatitude},{typedRow.EndLongitude}] should be >= 80% paved surface or or be within {structureVariance}M of a Structure"); + } } else if (surfaceTypeRule == SurfaceTypeRules.PavedSurface) { - warnings.AddItem("Surface Type Validation", $"Rule is {SurfaceTypeRules.PavedSurface} on Line and paved surface less than 80%"); + warnings.AddItem("Surface Type Validation" + , $"GPS position from [{typedRow.StartLatitude},{typedRow.StartLongitude}] " + + $"to [{typedRow.EndLatitude},{typedRow.EndLongitude}] should be >= {proportionPercPaved}% paved surface"); } } break; case SurfaceTypeRules.NonPavedSurface: - if ((unpavedLength / totalLength) >= .8) - { - //nothing wrong - } - else + if ((unpavedLength / totalLength) < (double)(proportionPercUnpaved / 100)) { - warnings.AddItem("Surface Type Validation", $"Rule is {SurfaceTypeRules.NonPavedSurface} on Line and unpaved surface less than 80%"); + warnings.AddItem("Surface Type Validation" + , $"GPS position from [{typedRow.StartLatitude},{typedRow.StartLongitude}] " + + $"to [{typedRow.EndLatitude},{typedRow.EndLongitude}] should be >= {proportionPercUnpaved}% Non-paved surface"); } break; case SurfaceTypeRules.Unconstructed: - if ((unconstructedLength / totalLength) >= .2) + if ((unconstructedLength / totalLength) >= (double)(proportionPercUnconstr / 100)) { - warnings.AddItem("Surface Type Validation", $"Rule is {SurfaceTypeRules.Unconstructed} on Line and unconstructed surface is more than 20%"); + warnings.AddItem("Surface Type Validation" + , $"GPS position from [{typedRow.StartLatitude},{typedRow.StartLongitude}] " + + $"to [{typedRow.EndLatitude},{typedRow.EndLongitude}] should NOT be >= {proportionPercUnconstr }% Unconstructed surface"); } break; } - - } else if (typedRow.FeatureType == FeatureType.Point) + } + else if (typedRow.FeatureType == FeatureType.Point) { + var pointSurfaceType = typedRow.SurfaceTypes.First().SurfaceType; + + var isPaved = (pointSurfaceType == RoadSurface.HOT_MIX + || pointSurfaceType == RoadSurface.COLD_MIX + || pointSurfaceType == RoadSurface.CONCRETE + || pointSurfaceType == RoadSurface.SURFACE_TREATED); + + var isUnpaved = (pointSurfaceType == RoadSurface.GRAVEL + || pointSurfaceType == RoadSurface.DIRT); + + var isUnconstructed = (pointSurfaceType == RoadSurface.CLEARED + || pointSurfaceType == RoadSurface.UNCLEARED); + + var isOther = (pointSurfaceType == RoadSurface.OTHER + || pointSurfaceType == RoadSurface.UNKNOWN); + switch (surfaceTypeRule) { case SurfaceTypeRules.PavedSurface: case SurfaceTypeRules.PavedStructure: - if (pavedLength > 0) + if (!isPaved) { - //nothign wrong - } - else - { - if (unpavedLength > 0 && surfaceTypeRule == SurfaceTypeRules.PavedStructure) + if (surfaceTypeRule == SurfaceTypeRules.PavedStructure) { //structure checking + var hasBridgeWithinVariance = await WithinBridgeStructureVariance(typedRow, (decimal)structureVarianceM); + if (!hasBridgeWithinVariance) + { + warnings.AddItem("Surface Type Validation", $"GPS position [{typedRow.StartLatitude},{typedRow.StartLongitude}] should be paved or be within 100M of a Structure"); + } } - else if (unpavedLength > 0 && surfaceTypeRule == SurfaceTypeRules.PavedSurface) + else { - warnings.AddItem("Surface Type Validation", $"Rule is {SurfaceTypeRules.NonPavedSurface}, unpaved surface found on Point"); + warnings.AddItem("Surface Type Validation" + , $"GPS position [{typedRow.StartLatitude},{typedRow.StartLongitude}] should be paved"); } } break; case SurfaceTypeRules.NonPavedSurface: - if (unpavedLength > 0) + if (!isUnpaved) { - //nothing wrong - } - else if (pavedLength > 0) - { - warnings.AddItem("Surface Type Validation", $"Rule is {SurfaceTypeRules.NonPavedSurface}, paved surface found on Point"); + warnings.AddItem("Surface Type Validation" + , $"GPS position [{typedRow.StartLatitude},{typedRow.StartLongitude}] should be non-paved"); } break; case SurfaceTypeRules.Unconstructed: - if (unconstructedLength > 0) + if (isUnconstructed) { - warnings.AddItem("Surface Type Validation", $"Rule is {SurfaceTypeRules.Unconstructed}, unconstructed surface found on Point"); + warnings.AddItem("Surface Type Validation" + , $"GPS position at [{typedRow.StartLatitude},{typedRow.StartLongitude}] should NOT be Unconstructed"); } break; @@ -406,6 +754,31 @@ private void PerformSurfaceTypeValidation(WorkReportTyped typedRow) } } + private async Task WithinBridgeStructureVariance(WorkReportTyped typedRow, decimal structureVariance) + { + var isBridgeWithinVariance = false; + + var startOffset = typedRow.StartOffset - structureVariance; + var endOffset = ((typedRow.EndOffset == null) ? typedRow.StartOffset : typedRow.EndOffset) + structureVariance; + + var result = await _spatialService.GetBridgeStructureOnRFISegment(typedRow.HighwayUnique); + + var bridgeStructures = result.structures.Where(x => x.StructureType == StructureType.BRIDGE).ToList(); + foreach (var bridge in bridgeStructures) + { + //we need to check if the start of the bridge is between the offset + // or if the end of the bridge is between the offset + if (((bridge.BeginKM >= startOffset) && (bridge.BeginKM <= endOffset)) + || ((bridge.EndKM >= startOffset) && (bridge.EndKM <= endOffset))) + { + isBridgeWithinVariance = true; // if we find one we can stop searching + break; + } + } + + return isBridgeWithinVariance; + } + private void PerformFieldValidation(Dictionary> errors, WorkReportCsvDto untypedRow, ActivityCodeDto activityCode) { if (activityCode.LocationCode.LocationCode == "C" && activityCode.ActivityNumber.StartsWith('6')) @@ -439,6 +812,31 @@ private void PerformFieldValidation(Dictionary> errors, Wor } } + private void PerformFieldServiceAreaValidation(List typedRows) + { + foreach (var typedRow in typedRows) + { + var errors = new Dictionary>(); + var submissionRow = _submissionRows[(decimal)typedRow.RowNum]; + if (string.IsNullOrWhiteSpace(typedRow.ServiceArea.ToString())|| typedRow.ActivityCodeValidation.ServiceAreaNumbers == null) + { + errors.AddItem(Fields.ServiceArea, $"Service area [{typedRow.ServiceArea}] is NOT associated with Activity [{typedRow.ActivityNumber}]"); + } + else + { + if (!typedRow.ActivityCodeValidation.ServiceAreaNumbers.Contains(typedRow.ServiceArea)) + { + errors.AddItem(Fields.ServiceArea, $"Service area [{typedRow.ServiceArea}] is NOT associated with Activity [{typedRow.ActivityNumber}]"); + } + } + + if (errors.Count > 0) + { + SetErrorDetail(submissionRow, errors, _statusService.FileLocationError); + } + } + + } private void PerformAdditionalValidation(List typedRows) { MethodLogger.LogEntry(_logger, _enableMethodLog, _methodLogHeader); @@ -488,7 +886,7 @@ private void PerformAdditionalValidation(List typedRows) { errors.AddItem($"{Fields.EndLongitude}/{Fields.EndLatitude}", "Invalid range of GPS coordinates."); } - + PerformAnalyticalFieldValidation(typedRow); if (errors.Count > 0) { SetErrorDetail(submissionRow, errors, _statusService.FileConflictionError); @@ -496,130 +894,6 @@ private void PerformAdditionalValidation(List typedRows) } } - private void CopyCalculatedFieldsFormUntypedRow(List typedRows, List untypedRows) - { - MethodLogger.LogEntry(_logger, _enableMethodLog, _methodLogHeader); - - foreach (var typedRow in typedRows) - { - var untypedRow = untypedRows.First(x => x.RowNum == typedRow.RowNum); - typedRow.FeatureType = untypedRow.FeatureType; - typedRow.SpatialData = untypedRow.SpatialData; - typedRow.RowId = untypedRow.RowId; - typedRow.SpThresholdLevel = untypedRow.SpThresholdLevel; - - //move activity rules and location code from untyped to typed - typedRow.ActivityCodeValidation.LocationCode = untypedRow.ActivityCodeValidation.LocationCode; - typedRow.ActivityCodeValidation.RoadLengthRuleId = untypedRow.ActivityCodeValidation.RoadLengthRuleId; - typedRow.ActivityCodeValidation.RoadLenghRuleExec = untypedRow.ActivityCodeValidation.RoadLenghRuleExec; - typedRow.ActivityCodeValidation.SurfaceTypeRuleId = untypedRow.ActivityCodeValidation.SurfaceTypeRuleId; - typedRow.ActivityCodeValidation.SurfaceTypeRuleExec = untypedRow.ActivityCodeValidation.SurfaceTypeRuleExec; - typedRow.ActivityCodeValidation.RoadClassRuleId = untypedRow.ActivityCodeValidation.RoadClassRuleId; - typedRow.ActivityCodeValidation.RoadClassRuleExec = untypedRow.ActivityCodeValidation.RoadClassRuleExec; - } - } - - private void SetActivityCodeRulesIntoUntypedRow(WorkReportCsvDto untypedRow, ActivityCodeDto activityCode) - { - untypedRow.FeatureType = activityCode.FeatureType ?? FeatureType.None; - untypedRow.SpThresholdLevel = activityCode.SpThresholdLevel; - //set activity code rules and location code - untypedRow.ActivityCodeValidation.LocationCode = activityCode.LocationCode.LocationCode; - - untypedRow.ActivityCodeValidation.RoadLengthRuleId = activityCode.RoadLengthRule; - untypedRow.ActivityCodeValidation.RoadLenghRuleExec = _validator.ActivityCodeRuleLookup - .Where(x => x.ActivityCodeRuleId == activityCode.RoadLengthRule) - .FirstOrDefault().ActivityRuleExecName; - - untypedRow.ActivityCodeValidation.SurfaceTypeRuleId = activityCode.SurfaceTypeRule; - untypedRow.ActivityCodeValidation.SurfaceTypeRuleExec = _validator.ActivityCodeRuleLookup - .Where(x => x.ActivityCodeRuleId == activityCode.SurfaceTypeRule) - .FirstOrDefault().ActivityRuleExecName; - - untypedRow.ActivityCodeValidation.RoadClassRuleId = activityCode.RoadClassRule; - untypedRow.ActivityCodeValidation.RoadClassRuleExec = _validator.ActivityCodeRuleLookup - .Where(x => x.ActivityCodeRuleId == activityCode.RoadClassRule) - .FirstOrDefault().ActivityRuleExecName; - } - - private List PerformSpatialValidationAndConversionBatchAsync(List typedRows) - { - MethodLogger.LogEntry(_logger, _enableMethodLog, _methodLogHeader, $"Total Record: {typedRows.Count}"); - - //grouping the rows - var groups = new List>(); - var currentGroup = new List(); - - var count = 0; - foreach (var typedRow in typedRows) - { - currentGroup.Add(typedRow); - count++; - - if (count % 10 == 0) - { - groups.Add(currentGroup); - currentGroup = new List(); - } - } - - if (currentGroup.Count > 0) - { - groups.Add(currentGroup); - } - - var geometries = new ConcurrentBag(); - var progress = 0; - - foreach (var group in groups) - { - var tasklist = new List(); - - foreach (var row in group) - { - tasklist.Add(Task.Run(async () => geometries.Add(await PerformSpatialValidationAndConversionAsync(row)))); - } - - Task.WaitAll(tasklist.ToArray()); - - progress += 10; - - if (progress % 500 == 0) - { - _logger.LogInformation($"{_methodLogHeader} PerformSpatialValidationAndConversionAsync {progress}"); - } - } - - return geometries.ToList(); - } - - private async Task PerformSpatialValidationAndConversionAsync(WorkReportTyped typedRow) - { - var submissionRow = _submissionRows[(decimal)typedRow.RowNum]; - var workReport = new WorkReportGeometry(typedRow, null); - - if (typedRow.SpatialData == SpatialData.Gps) - { - await PerformSpatialGpsValidation(workReport, submissionRow); - - SetVarianceWarningDetail(submissionRow, typedRow.HighwayUnique, - GetGpsString(typedRow.StartLatitude, typedRow.StartLongitude), - GetGpsString(typedRow.EndLatitude, typedRow.EndLongitude), - typedRow.SpThresholdLevel); - } - else if (typedRow.SpatialData == SpatialData.Lrs) - { - await PerformSpatialLrsValidation(workReport, submissionRow); - - SetVarianceWarningDetail(submissionRow, typedRow.HighwayUnique, - GetOffsetString(typedRow.StartOffset), - GetOffsetString(typedRow.EndOffset), - typedRow.SpThresholdLevel); - } - - return workReport; - } - private async Task PerformSpatialGpsValidation(WorkReportGeometry workReport, HmrSubmissionRow submissionRow) { var errors = new Dictionary>(); @@ -894,6 +1168,56 @@ private void PerformOffsetEitherLineOrPointValidation(WorkReportTyped typedRow) } } + #endregion + + #region Utility Functions + + private void CopyCalculatedFieldsFormUntypedRow(List typedRows, List untypedRows) + { + MethodLogger.LogEntry(_logger, _enableMethodLog, _methodLogHeader); + + foreach (var typedRow in typedRows) + { + var untypedRow = untypedRows.First(x => x.RowNum == typedRow.RowNum); + typedRow.FeatureType = untypedRow.FeatureType; + typedRow.SpatialData = untypedRow.SpatialData; + typedRow.RowId = untypedRow.RowId; + typedRow.SpThresholdLevel = untypedRow.SpThresholdLevel; + + //move activity rules and location code from untyped to typed + typedRow.ActivityCodeValidation= untypedRow.ActivityCodeValidation; + } + } + + private void SetActivityCodeRulesIntoUntypedRow(WorkReportCsvDto untypedRow, ActivityCodeDto activityCode) + { + untypedRow.FeatureType = activityCode.FeatureType ?? FeatureType.None; + untypedRow.SpThresholdLevel = activityCode.SpThresholdLevel; + //set activity code rules and location code + untypedRow.ActivityCodeValidation.LocationCode = activityCode.LocationCode.LocationCode; + + untypedRow.ActivityCodeValidation.RoadLengthRuleId = activityCode.RoadLengthRule; + untypedRow.ActivityCodeValidation.RoadLenghRuleExec = _validator.ActivityCodeRuleLookup + .Where(x => x.ActivityCodeRuleId == activityCode.RoadLengthRule) + .FirstOrDefault().ActivityRuleExecName; + + untypedRow.ActivityCodeValidation.SurfaceTypeRuleId = activityCode.SurfaceTypeRule; + untypedRow.ActivityCodeValidation.SurfaceTypeRuleExec = _validator.ActivityCodeRuleLookup + .Where(x => x.ActivityCodeRuleId == activityCode.SurfaceTypeRule) + .FirstOrDefault().ActivityRuleExecName; + + untypedRow.ActivityCodeValidation.RoadClassRuleId = activityCode.RoadClassRule; + untypedRow.ActivityCodeValidation.RoadClassRuleExec = _validator.ActivityCodeRuleLookup + .Where(x => x.ActivityCodeRuleId == activityCode.RoadClassRule) + .FirstOrDefault().ActivityRuleExecName; + + untypedRow.ActivityCodeValidation.MinValue = activityCode.MinValue; + untypedRow.ActivityCodeValidation.MaxValue = activityCode.MaxValue; + untypedRow.ActivityCodeValidation.ReportingFrequency = activityCode.ReportingFrequency; + + untypedRow.ActivityCodeValidation.ServiceAreaNumbers = activityCode.ServiceAreaNumbers; + } + private string GetValidationEntityName(WorkReportCsvDto untypedRow, ActivityCodeDto activityCode) { var locationCode = activityCode.LocationCode; @@ -1004,5 +1328,7 @@ private List GetRecords(CsvReader csv) return (0, rows); } + + #endregion } } diff --git a/api/Hmcr.Domain/Services/SpatialService.cs b/api/Hmcr.Domain/Services/SpatialService.cs index 9fce8afa..d328c899 100644 --- a/api/Hmcr.Domain/Services/SpatialService.cs +++ b/api/Hmcr.Domain/Services/SpatialService.cs @@ -19,8 +19,11 @@ public interface ISpatialService (decimal offset, string rfiSegment, string rfiSegmentName, string thresholdLevel, Dictionary> errors); Task<(SpValidationResult result, decimal snappedStartOffset, decimal snappedEndOffset, Point startPoint, Point endPoint, List lines, RfiSegment rfiSegment)> ValidateLrsLineAsync(decimal startOffset, decimal endOffset, string rfiSegment, string rfiSegmentName, string thresholdLevel, Dictionary> errors); - Task<(SpValidationResult result, List surfaceTypes)> GetSurfaceTypeAssocWithLineAsync(string geometryLineString); - Task<(SpValidationResult result, SurfaceType surfaceType)> GetSurfaceTypeAssocWithPointAsync(string geometryLineString); + Task<(SpValidationResult result, List surfaceTypes)> GetSurfaceTypeAssocWithLineAsync(NetTopologySuite.Geometries.Geometry geometry); + Task<(SpValidationResult result, SurfaceType surfaceType)> GetSurfaceTypeAssocWithPointAsync(NetTopologySuite.Geometries.Geometry geometry); + Task<(SpValidationResult result, List maintenanceClasses)> GetMaintenanceClassAssocWithLineAsync(NetTopologySuite.Geometries.Geometry geometry); + Task<(SpValidationResult result, MaintenanceClass maintenanceClass)> GetMaintenanceClassAssocWithPointAsync(NetTopologySuite.Geometries.Geometry geometry); + Task<(SpValidationResult result, List structures)> GetBridgeStructureOnRFISegment(string rfiSegmentName); } public class SpatialService : ISpatialService @@ -245,20 +248,41 @@ public SpatialService(IOasApi oasApi, IFieldValidatorService validator, ILookupC return (SpValidationResult.Success, rfiDetail); } - public async Task<(SpValidationResult result, List surfaceTypes)> GetSurfaceTypeAssocWithLineAsync(string geometryLineString) + public async Task<(SpValidationResult result, List surfaceTypes)> GetSurfaceTypeAssocWithLineAsync(NetTopologySuite.Geometries.Geometry geometry) { - var surfaceTypes = await _inventoryApi.GetSurfaceTypeAssociatedWithLine(geometryLineString); + var surfaceTypes = await _inventoryApi.GetSurfaceTypeAssociatedWithLine(geometry); return (SpValidationResult.Success, surfaceTypes); } - public async Task<(SpValidationResult result, SurfaceType surfaceType)> GetSurfaceTypeAssocWithPointAsync(string geometryLineString) + public async Task<(SpValidationResult result, SurfaceType surfaceType)> GetSurfaceTypeAssocWithPointAsync(NetTopologySuite.Geometries.Geometry geometry) { - var surfaceType = await _inventoryApi.GetSurfaceTypeAssociatedWithPoint(geometryLineString); + var surfaceType = await _inventoryApi.GetSurfaceTypeAssociatedWithPoint(geometry); return (SpValidationResult.Success, surfaceType); } + public async Task<(SpValidationResult result, MaintenanceClass maintenanceClass)> GetMaintenanceClassAssocWithPointAsync(NetTopologySuite.Geometries.Geometry geometry) + { + var maintenanceClass = await _inventoryApi.GetMaintenanceClassesAssociatedWithPoint(geometry); + + return (SpValidationResult.Success, maintenanceClass); + } + + public async Task<(SpValidationResult result, List maintenanceClasses)> GetMaintenanceClassAssocWithLineAsync(NetTopologySuite.Geometries.Geometry geometry) + { + var maintenanceClasses = await _inventoryApi.GetMaintenanceClassesAssociatedWithLine(geometry); + + return (SpValidationResult.Success, maintenanceClasses); + } + + public async Task<(SpValidationResult result, List structures)> GetBridgeStructureOnRFISegment(string rfiSegmentName) + { + var structures = await _inventoryApi.GetBridgeStructure(rfiSegmentName); + + return (SpValidationResult.Success, structures); + } + private (bool withinTolerance, decimal snappedOffset) GetSnappedOffset(RfiSegment segment, decimal offset, string rfiSegment, string rfiSegmentName, string thresholdLevel, Dictionary> errors) { diff --git a/api/Hmcr.Hangfire/appsettings.json b/api/Hmcr.Hangfire/appsettings.json index 18258883..5fa1b0cb 100644 --- a/api/Hmcr.Hangfire/appsettings.json +++ b/api/Hmcr.Hangfire/appsettings.json @@ -43,6 +43,12 @@ "ExportUrl": "http://fortsteele.th.gov.bc.ca:8081", "ExportPath": "ogs-geoV06/wfs?" }, + "Timeouts": { + "MapsAPI": 15, + "OasAPI": 15, + "ExportAPI": 15, + "InventoryAPI": 120 + }, "ServiceAccount": { "User": "", "Password": "" diff --git a/api/Hmcr.Model/Constants.cs b/api/Hmcr.Model/Constants.cs index 1a4e3eb2..2e6dbb7a 100644 --- a/api/Hmcr.Model/Constants.cs +++ b/api/Hmcr.Model/Constants.cs @@ -274,6 +274,8 @@ public static class CodeSet public const string FeatureType = "FEATURE_TYPE"; public const string NonSpHighwayUnique = "NONSP_HIGHWAY_UNIQUE"; public const string ThresholdSp = "THRSHLD_SP_VAR"; + public const string ValidatorProportion = "VALIDATOR_PROPORTION"; + } public static class ThresholdSpLevels @@ -339,6 +341,18 @@ public static class SurfaceTypeRules public const string Unconstructed = "GPS_NOT_UNCONSTRUCTED"; } + public static class MaintenanceClassRules + { + public const string NA = "NOT_APPLICABLE"; + public const string Class8OrF = "CLASS_8_OR_F"; + public const string NotClass8OrF = "NOT_CLASS_8_OR_F"; + } + + public static class RoadLengthRules + { + public const string NA = "NOT_APPLICABLE"; + } + public static class RoadSurface { public const string HOT_MIX = "1"; @@ -353,6 +367,26 @@ public static class RoadSurface public const string UNKNOWN = "Z"; } + public static class ValidatorProportionCode + { + public const string SURFACE_TYPE_PAVED = "SURFACE_TYPE_PAVED"; + public const string SURFACE_TYPE_UNPAVED = "SURFACE_TYPE_UNPAVED"; + public const string SURFACE_TYPE_UNCONSTRUCTED = "SURFACE_TYPE_UNCNSTR"; + public const string MAINTENANCE_CLASS = "MAINTENANCE_CLASS"; + public const string STRUCTURE_VARIANCE_M = "STRUCTURE_VARIANCE_M"; + } + + public static class StructureType + { + public const string BRIDGE = "BRIDGE"; + public const string CULVERT = "CULVERT"; + public const string MARINE = "MARINE"; + public const string RWALL = "RWALL"; + public const string SIGN = "SIGN"; + public const string TUNNEL = "TUNNEL"; + + } + /// /// Spatial Data /// None - Non-Location specific reporting Fields diff --git a/api/Hmcr.Model/Dtos/ActivityCode/ActivityCodeDto.cs b/api/Hmcr.Model/Dtos/ActivityCode/ActivityCodeDto.cs index 98073301..1eeee0c2 100644 --- a/api/Hmcr.Model/Dtos/ActivityCode/ActivityCodeDto.cs +++ b/api/Hmcr.Model/Dtos/ActivityCode/ActivityCodeDto.cs @@ -27,7 +27,6 @@ public class ActivityCodeDto public decimal SurfaceTypeRule { get; set; } public decimal RoadClassRule { get; set; } public IList ServiceAreaNumbers { get; set; } - public decimal? MinValue { get; set; } public decimal? MaxValue { get; set; } public int? ReportingFrequency { get; set; } diff --git a/api/Hmcr.Model/Dtos/ActivityCode/ActivityCodeValidationDto.cs b/api/Hmcr.Model/Dtos/ActivityCode/ActivityCodeValidationDto.cs index f2d2d950..72a35ded 100644 --- a/api/Hmcr.Model/Dtos/ActivityCode/ActivityCodeValidationDto.cs +++ b/api/Hmcr.Model/Dtos/ActivityCode/ActivityCodeValidationDto.cs @@ -17,8 +17,8 @@ public class ActivityCodeValidationDto public string RoadClassRuleExec { get; set; } public virtual IList ServiceAreaNumbers { get; set; } - public decimal? MinimumValue { get; set; } - public decimal? MaximumValue { get; set; } - public decimal? ReportingFrequency { get; set; } + public decimal? MinValue { get; set; } + public decimal? MaxValue { get; set; } + public int? ReportingFrequency { get; set; } } } diff --git a/api/Hmcr.Model/Dtos/WorkReport/WorkReportMaintenanceClass.cs b/api/Hmcr.Model/Dtos/WorkReport/WorkReportMaintenanceClass.cs new file mode 100644 index 00000000..aefcffb3 --- /dev/null +++ b/api/Hmcr.Model/Dtos/WorkReport/WorkReportMaintenanceClass.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Hmcr.Model.Dtos.WorkReport +{ + public class WorkReportMaintenanceClass + { + public string SummerRating { get; set; } + public string WinterRating { get; set; } + public double RoadLength { get; set; } + } +} diff --git a/api/Hmcr.Model/Dtos/WorkReport/WorkReportRoadFeature.cs b/api/Hmcr.Model/Dtos/WorkReport/WorkReportSurfaceType.cs similarity index 50% rename from api/Hmcr.Model/Dtos/WorkReport/WorkReportRoadFeature.cs rename to api/Hmcr.Model/Dtos/WorkReport/WorkReportSurfaceType.cs index b6962876..62b16cb8 100644 --- a/api/Hmcr.Model/Dtos/WorkReport/WorkReportRoadFeature.cs +++ b/api/Hmcr.Model/Dtos/WorkReport/WorkReportSurfaceType.cs @@ -4,15 +4,9 @@ namespace Hmcr.Model.Dtos.WorkReport { - public class WorkReportRoadFeature + public class WorkReportSurfaceType { public string SurfaceType { get; set; } public double SurfaceLength { get; set; } - - public WorkReportRoadFeature(string surfaceType, double surfaceLength) - { - SurfaceType = surfaceType; - SurfaceLength = surfaceLength; - } } } \ No newline at end of file diff --git a/api/Hmcr.Model/Dtos/WorkReport/WorkReportTyped.cs b/api/Hmcr.Model/Dtos/WorkReport/WorkReportTyped.cs index b9457473..d887a9fa 100644 --- a/api/Hmcr.Model/Dtos/WorkReport/WorkReportTyped.cs +++ b/api/Hmcr.Model/Dtos/WorkReport/WorkReportTyped.cs @@ -56,7 +56,9 @@ public class WorkReportTyped /// /// Road feature data retrieved from CHRIS /// - public virtual IList RoadFeatures { get; set; } + public virtual IList SurfaceTypes { get; set; } + public virtual IList MaintenanceClasses { get; set; } + public ActivityCodeValidationDto ActivityCodeValidation { get; set; } } } diff --git a/api/Hmcr.Model/Utils/StringExtensions.cs b/api/Hmcr.Model/Utils/StringExtensions.cs index 3804ede8..dd2f82e0 100644 --- a/api/Hmcr.Model/Utils/StringExtensions.cs +++ b/api/Hmcr.Model/Utils/StringExtensions.cs @@ -200,5 +200,20 @@ public static string RemoveQuestionMark(this string query) { return query.StartsWith("?") ? query.Substring(1) : query; } + + public static decimal ConvertStrToDecimal(this string value) + { + return IsEmpty(value) ? 0 : (decimal) decimal.Parse(value); + } + public static decimal ConvertNullableDecimal(this decimal? value) + { + return value?? 0; + } + public static string ConvertDecimalToStringAndRemoveTrailing(this decimal? value) + { + decimal val = value ?? 0; + return val.ToString("G29"); + } + } } diff --git a/client/src/js/components/forms/EditActivityFormFields.js b/client/src/js/components/forms/EditActivityFormFields.js index b9311f92..dc83ff70 100644 --- a/client/src/js/components/forms/EditActivityFormFields.js +++ b/client/src/js/components/forms/EditActivityFormFields.js @@ -31,8 +31,7 @@ const tipHighwayAttributeValidation = [
    Highway Attribute Validations provide warnings for Location Code C activities when the features of the reported location and/or accomplishment do not meet the defined parameters.
  • Road Length checks the accomplishment against the road length (either Road KM or Lane KM), - or against Guardrail Length [NTD: to confirm Guardrail vs Barrier vernacular based on what types of - guardrail/barrier will be included], as defined in each individual rule. Several rules account for the road length + or against Guardrail Length, as defined in each individual rule. Several rules account for the road length to be multiplied by one or more factors to accommodate non kilometre-based units of measure. Multiplying factors include conversion factors (e.g. 1km=1,000m), application rates (e.g. 2.0 litres/m2) or lane width factors to calculate surface area (e.g. lane width = 3.5m). A 10% tolerance is added to the Total Road KM (to a 200m maximum) and Total Lane KM (to a 500m maximum) diff --git a/database/V26.0/1_HMR_PDM-dml_ACTIVITY_CODE_RULE_UPDATES_V26_IS2.3.sql b/database/V26.0/1_HMR_PDM-dml_ACTIVITY_CODE_RULE_UPDATES_V26_IS2.3.sql new file mode 100644 index 00000000..b13b0ef4 --- /dev/null +++ b/database/V26.0/1_HMR_PDM-dml_ACTIVITY_CODE_RULE_UPDATES_V26_IS2.3.sql @@ -0,0 +1,113 @@ +USE HMR_DEV; -- uncomment appropriate instance +--USE HMR_TST; +--USE HMR_UAT; +--USE HMR_PRD; +GO + +/* logically delete un-used/obsolete rules */ +UPDATE HMR_ACTIVITY_CODE_RULE +SET END_DATE = GETDATE() +, CONCURRENCY_CONTROL_NUMBER = (SELECT CONCURRENCY_CONTROL_NUMBER + FROM HMR_ACTIVITY_CODE_RULE + WHERE ACTIVITY_RULE_EXEC_NAME = 'ROAD_KM_NONPAVED' AND DISPLAY_ORDER = 6) + 1 +WHERE ACTIVITY_RULE_EXEC_NAME = 'ROAD_KM_NONPAVED' AND DISPLAY_ORDER = 6; + +UPDATE HMR_ACTIVITY_CODE_RULE +SET END_DATE = GETDATE() +, CONCURRENCY_CONTROL_NUMBER = (SELECT CONCURRENCY_CONTROL_NUMBER + FROM HMR_ACTIVITY_CODE_RULE + WHERE ACTIVITY_RULE_EXEC_NAME = 'ROAD_KM_20' AND DISPLAY_ORDER = 8) + 1 +WHERE ACTIVITY_RULE_EXEC_NAME = 'ROAD_KM_20' AND DISPLAY_ORDER = 8; + +UPDATE HMR_ACTIVITY_CODE_RULE +SET END_DATE = GETDATE() +, CONCURRENCY_CONTROL_NUMBER = (SELECT CONCURRENCY_CONTROL_NUMBER + FROM HMR_ACTIVITY_CODE_RULE + WHERE ACTIVITY_RULE_EXEC_NAME = 'ALL_CLASSES' AND ACTIVITY_RULE_SET = 'ROAD_CLASS') + 1 +WHERE ACTIVITY_RULE_EXEC_NAME = 'ALL_CLASSES' AND ACTIVITY_RULE_SET = 'ROAD_CLASS'; + +/* update rule name and exec name */ +UPDATE HMR_ACTIVITY_CODE_RULE +SET ACTIVITY_RULE_NAME = '[Qty] ≤ [2.0] * [Road KM * 1000] * [3.5]' +, CONCURRENCY_CONTROL_NUMBER = (SELECT CONCURRENCY_CONTROL_NUMBER + FROM HMR_ACTIVITY_CODE_RULE + WHERE ACTIVITY_RULE_EXEC_NAME = 'RATE_LANE_KM_35' AND DISPLAY_ORDER = 3) + 1 +WHERE ACTIVITY_RULE_EXEC_NAME = 'RATE_LANE_KM_35' AND DISPLAY_ORDER = 3; + +UPDATE HMR_ACTIVITY_CODE_RULE +SET ACTIVITY_RULE_NAME = '[Qty] ≤ [3.0] * [Road KM * 1000] * [6.0]' +, CONCURRENCY_CONTROL_NUMBER = (SELECT CONCURRENCY_CONTROL_NUMBER + FROM HMR_ACTIVITY_CODE_RULE + WHERE ACTIVITY_RULE_EXEC_NAME = 'RATE_LANE_KM_60' AND DISPLAY_ORDER = 4) + 1 +WHERE ACTIVITY_RULE_EXEC_NAME = 'RATE_LANE_KM_60' AND DISPLAY_ORDER = 4; + +UPDATE HMR_ACTIVITY_CODE_RULE +SET ACTIVITY_RULE_NAME = '[Qty] ≤ [Lane KM] * 2.0', ACTIVITY_RULE_EXEC_NAME = 'LANE_KM_20' +, CONCURRENCY_CONTROL_NUMBER = (SELECT CONCURRENCY_CONTROL_NUMBER + FROM HMR_ACTIVITY_CODE_RULE + WHERE ACTIVITY_RULE_EXEC_NAME = 'LANE_KM_PAVED_20' AND DISPLAY_ORDER = 7) + 1 +WHERE ACTIVITY_RULE_EXEC_NAME = 'LANE_KM_PAVED_20' AND DISPLAY_ORDER = 7; + +UPDATE HMR_ACTIVITY_CODE_RULE +SET ACTIVITY_RULE_NAME = '[Qty] ≤ [Guardrail Length * 1000.0]', ACTIVITY_RULE_EXEC_NAME = 'GUARDRAIL_LEN_METERS' +, CONCURRENCY_CONTROL_NUMBER = (SELECT CONCURRENCY_CONTROL_NUMBER + FROM HMR_ACTIVITY_CODE_RULE + WHERE ACTIVITY_RULE_EXEC_NAME = 'BARRIER_LEN_METERS' AND DISPLAY_ORDER = 15) + 1 +WHERE ACTIVITY_RULE_EXEC_NAME = 'BARRIER_LEN_METERS' AND DISPLAY_ORDER = 15; + +/* adjust display order or road length rules */ +UPDATE HMR_ACTIVITY_CODE_RULE +SET DISPLAY_ORDER = 6 +, CONCURRENCY_CONTROL_NUMBER = (SELECT CONCURRENCY_CONTROL_NUMBER + FROM HMR_ACTIVITY_CODE_RULE + WHERE ACTIVITY_RULE_EXEC_NAME = 'LANE_KM') + 1 +WHERE ACTIVITY_RULE_EXEC_NAME = 'LANE_KM'; + +UPDATE HMR_ACTIVITY_CODE_RULE +SET DISPLAY_ORDER = 7 +, CONCURRENCY_CONTROL_NUMBER = (SELECT CONCURRENCY_CONTROL_NUMBER + FROM HMR_ACTIVITY_CODE_RULE + WHERE ACTIVITY_RULE_EXEC_NAME = 'LANE_KM_20') + 1 +WHERE ACTIVITY_RULE_EXEC_NAME = 'LANE_KM_20'; + +UPDATE HMR_ACTIVITY_CODE_RULE +SET DISPLAY_ORDER = 8 +, CONCURRENCY_CONTROL_NUMBER = (SELECT CONCURRENCY_CONTROL_NUMBER + FROM HMR_ACTIVITY_CODE_RULE + WHERE ACTIVITY_RULE_EXEC_NAME = 'LANE_METERS') + 1 +WHERE ACTIVITY_RULE_EXEC_NAME = 'LANE_METERS'; + +UPDATE HMR_ACTIVITY_CODE_RULE +SET DISPLAY_ORDER = 9 +, CONCURRENCY_CONTROL_NUMBER = (SELECT CONCURRENCY_CONTROL_NUMBER + FROM HMR_ACTIVITY_CODE_RULE + WHERE ACTIVITY_RULE_EXEC_NAME = 'ROAD_KM') + 1 +WHERE ACTIVITY_RULE_EXEC_NAME = 'ROAD_KM'; + +UPDATE HMR_ACTIVITY_CODE_RULE +SET DISPLAY_ORDER = 10 +, CONCURRENCY_CONTROL_NUMBER = (SELECT CONCURRENCY_CONTROL_NUMBER + FROM HMR_ACTIVITY_CODE_RULE + WHERE ACTIVITY_RULE_EXEC_NAME = 'ROAD_KM_20' AND END_DATE IS NULL) + 1 +WHERE ACTIVITY_RULE_EXEC_NAME = 'ROAD_KM_20' AND END_DATE IS NULL; + +UPDATE HMR_ACTIVITY_CODE_RULE +SET DISPLAY_ORDER = 11 +, CONCURRENCY_CONTROL_NUMBER = (SELECT CONCURRENCY_CONTROL_NUMBER + FROM HMR_ACTIVITY_CODE_RULE + WHERE ACTIVITY_RULE_EXEC_NAME = 'ROAD_METERS') + 1 +WHERE ACTIVITY_RULE_EXEC_NAME = 'ROAD_METERS'; + +UPDATE HMR_ACTIVITY_CODE_RULE +SET DISPLAY_ORDER = 12 +, CONCURRENCY_CONTROL_NUMBER = (SELECT CONCURRENCY_CONTROL_NUMBER + FROM HMR_ACTIVITY_CODE_RULE + WHERE ACTIVITY_RULE_EXEC_NAME = 'ROAD_METERS_20') + 1 +WHERE ACTIVITY_RULE_EXEC_NAME = 'ROAD_METERS_20'; + +UPDATE HMR_ACTIVITY_CODE_RULE +SET DISPLAY_ORDER = 13 +, CONCURRENCY_CONTROL_NUMBER = (SELECT CONCURRENCY_CONTROL_NUMBER + FROM HMR_ACTIVITY_CODE_RULE + WHERE ACTIVITY_RULE_EXEC_NAME = 'GUARDRAIL_LEN_METERS') + 1 +WHERE ACTIVITY_RULE_EXEC_NAME = 'GUARDRAIL_LEN_METERS'; diff --git a/database/V26.0/2_HMR_PDM-dml_CODE_LOOKUP_INSERTS_V26_IS2.3.sql b/database/V26.0/2_HMR_PDM-dml_CODE_LOOKUP_INSERTS_V26_IS2.3.sql new file mode 100644 index 00000000..b4f03735 --- /dev/null +++ b/database/V26.0/2_HMR_PDM-dml_CODE_LOOKUP_INSERTS_V26_IS2.3.sql @@ -0,0 +1,20 @@ +USE HMR_DEV; -- uncomment appropriate instance +--USE HMR_TST; +--USE HMR_UAT; +--USE HMR_PRD; +GO + +INSERT INTO HMR_CODE_LOOKUP (CODE_SET, CODE_NAME, CODE_VALUE_NUM, CODE_VALUE_FORMAT, CONCURRENCY_CONTROL_NUMBER) +VALUES ('VALIDATOR_PROPORTION', 'SURFACE_TYPE_PAVED', 80, 'NUMBER', 1); + +INSERT INTO HMR_CODE_LOOKUP (CODE_SET, CODE_NAME, CODE_VALUE_NUM, CODE_VALUE_FORMAT, CONCURRENCY_CONTROL_NUMBER) +VALUES ('VALIDATOR_PROPORTION', 'SURFACE_TYPE_UNPAVED', 80, 'NUMBER', 1); + +INSERT INTO HMR_CODE_LOOKUP (CODE_SET, CODE_NAME, CODE_VALUE_NUM, CODE_VALUE_FORMAT, CONCURRENCY_CONTROL_NUMBER) +VALUES ('VALIDATOR_PROPORTION', 'SURFACE_TYPE_UNCNSTR', 20, 'NUMBER', 1); + +INSERT INTO HMR_CODE_LOOKUP (CODE_SET, CODE_NAME, CODE_VALUE_NUM, CODE_VALUE_FORMAT, CONCURRENCY_CONTROL_NUMBER) +VALUES ('VALIDATOR_PROPORTION', 'MAINTENANCE_CLASS', 90, 'NUMBER', 1); + +INSERT INTO HMR_CODE_LOOKUP (CODE_SET, CODE_NAME, CODE_VALUE_NUM, CODE_VALUE_FORMAT, CONCURRENCY_CONTROL_NUMBER) +VALUES ('VALIDATOR_PROPORTION', 'STRUCTURE_VARIANCE_M', 100, 'NUMBER', 1); \ No newline at end of file diff --git a/openshift/configmaps/api-appsettings.yaml b/openshift/configmaps/api-appsettings.yaml index a75275d9..7b737410 100644 --- a/openshift/configmaps/api-appsettings.yaml +++ b/openshift/configmaps/api-appsettings.yaml @@ -9,7 +9,7 @@ objects: { "AllowedHosts": "*", "Constants": { - "Version": "1.3.2.0", + "Version": "1.3.3.0", "SwaggerApiUrl": "/swagger/v1/swagger.json" }, "Serilog": { @@ -55,12 +55,18 @@ objects: "CHRIS": { "MapUrl": "https://prd-maps.th.gov.bc.ca", "MapPath": "geoV05/wfs?", - "OASUrl": "https://prdoas2.apps.th.gov.bc.ca", + "OASUrl": "${OAS_URL}", "OASPath": "ogs-geoV06/wfs?", "ExportUrl": "${EXPORT_URL}", "WFSExportPath": "ogs-geoV06/ows?service=WFS&version=2.0.0&request=GetFeature", "KMLExportPath": "ogs-geoV06/wms/kml?mode=download&styles=HMR_GENERIC_FOR_KML" }, + "Timeouts": { + "MapsAPI": ${GEOSERVER_TIMEOUT}, + "OasAPI": ${GEOSERVER_TIMEOUT}, + "ExportAPI": ${GEOSERVER_TIMEOUT}, + "InventoryAPI": ${GEOSERVER_TIMEOUT} + }, "JWT": { "Authority": "https://sso-dev.pathfinder.gov.bc.ca/auth/realms/", "Audience": "" @@ -114,3 +120,13 @@ parameters: name: EXPORT_URL required: true value: "https://devoas1.apps.th.gov.bc.ca" + - description: GeoServer CHRIS API URL + displayName: OAS_URL + name: OAS_URL + required: true + value: "https://devoas1.apps.th.gov.bc.ca" + - description: Default timeout value for CHRIS/GeoServer API calls + displayName: GEOSERVER_TIMEOUT + name: GEOSERVER_TIMEOUT + required: true + value: "120"