diff --git a/ISOv4Plugin/Mappers/ProductMapper.cs b/ISOv4Plugin/Mappers/ProductMapper.cs index 5362677..26cfe6d 100644 --- a/ISOv4Plugin/Mappers/ProductMapper.cs +++ b/ISOv4Plugin/Mappers/ProductMapper.cs @@ -226,7 +226,9 @@ public Product ImportProduct(ISOProduct isoProduct) product.Form = _manufacturer?.GetProductForm(isoProduct) ?? product.Form; // Category - product.Category = _manufacturer?.GetProductCategory(isoProduct) ?? CategoryEnum.Unknown; + product.Category = _manufacturer?.GetProductCategory(isoProduct) ?? (product.ProductType == ProductTypeEnum.Variety + ? CategoryEnum.Variety + : CategoryEnum.Unknown); // Update ProductType if (product.ProductType == ProductTypeEnum.Generic && product.Category != CategoryEnum.Unknown) diff --git a/ISOv4Plugin/Mappers/TimeLogMapper.cs b/ISOv4Plugin/Mappers/TimeLogMapper.cs index 9aef618..e21cbbf 100644 --- a/ISOv4Plugin/Mappers/TimeLogMapper.cs +++ b/ISOv4Plugin/Mappers/TimeLogMapper.cs @@ -9,6 +9,7 @@ using AgGateway.ADAPT.ApplicationDataModel.Common; using AgGateway.ADAPT.ApplicationDataModel.Equipment; using AgGateway.ADAPT.ApplicationDataModel.LoggedData; +using AgGateway.ADAPT.ApplicationDataModel.Products; using AgGateway.ADAPT.ApplicationDataModel.Shapes; using AgGateway.ADAPT.ISOv4Plugin.ExtensionMethods; using AgGateway.ADAPT.ISOv4Plugin.ISOEnumerations; @@ -355,34 +356,44 @@ protected IEnumerable ImportTimeLog(ISOTask loggedTask, ISOTimeLo List operationDatas = new List(); foreach (ISODevice dvc in loggedDeviceElementsByDevice.Keys) { - OperationData operationData = new OperationData(); - //Determine products - Dictionary> productAllocations = GetProductAllocationsByDeviceElement(loggedTask, dvc); - List productIDs = GetDistinctProductIDs(TaskDataMapper, productAllocations); - - //This line will necessarily invoke a spatial read in order to find - //1)The correct number of CondensedWorkState working datas to create - //2)Any Widths and Offsets stored in the spatial data - IEnumerable sections = sectionMapper.Map(time, - isoRecords, - operationData.Id.ReferenceId, - loggedDeviceElementsByDevice[dvc], - productAllocations); - - var workingDatas = sections != null ? sections.SelectMany(x => x.GetWorkingDatas()).ToList() : new List(); - - operationData.GetSpatialRecords = () => spatialMapper.Map(isoRecords, workingDatas, productAllocations); - operationData.MaxDepth = sections.Count() > 0 ? sections.Select(s => s.Depth).Max() : 0; - operationData.GetDeviceElementUses = x => sectionMapper.ConvertToBaseTypes(sections.Where(s => s.Depth == x).ToList()); - operationData.PrescriptionId = prescriptionID; - operationData.OperationType = GetOperationTypeFromLoggingDevices(time); - operationData.ProductIds = productIDs; - if (!useDeferredExecution) + Dictionary> deviceProductAllocations = GetProductAllocationsByDeviceElement(loggedTask, dvc); + + //Create a separate operation for each product form (liquid, granular or solid). + List> deviceElementGroups = SplitElementsByProductForm(deviceProductAllocations, loggedDeviceElementsByDevice[dvc], dvc); + + foreach (var deviceElementGroup in deviceElementGroups) { - operationData.SpatialRecordCount = isoRecords.Count(); //We will leave this at 0 unless a consumer has overridden deferred execution of spatial data iteration + OperationData operationData = new OperationData(); + + Dictionary> productAllocations = deviceProductAllocations + .Where(x => deviceElementGroup.Contains(x.Key)) + .ToDictionary(x => x.Key, x => x.Value); + List productIDs = GetDistinctProductIDs(TaskDataMapper, productAllocations); + + //This line will necessarily invoke a spatial read in order to find + //1)The correct number of CondensedWorkState working datas to create + //2)Any Widths and Offsets stored in the spatial data + IEnumerable sections = sectionMapper.Map(time, + isoRecords, + operationData.Id.ReferenceId, + deviceElementGroup, + productAllocations); + + var workingDatas = sections != null ? sections.SelectMany(x => x.GetWorkingDatas()).ToList() : new List(); + + operationData.GetSpatialRecords = () => spatialMapper.Map(isoRecords, workingDatas, productAllocations); + operationData.MaxDepth = sections.Count() > 0 ? sections.Select(s => s.Depth).Max() : 0; + operationData.GetDeviceElementUses = x => sectionMapper.ConvertToBaseTypes(sections.Where(s => s.Depth == x).ToList()); + operationData.PrescriptionId = prescriptionID; + operationData.OperationType = GetOperationTypeFromProductCategory(productIDs) ?? GetOperationTypeFromLoggingDevices(time); + operationData.ProductIds = productIDs; + if (!useDeferredExecution) + { + operationData.SpatialRecordCount = isoRecords.Count(); //We will leave this at 0 unless a consumer has overridden deferred execution of spatial data iteration + } + operationDatas.Add(operationData); } - operationDatas.Add(operationData); } //Set the CoincidentOperationDataIds property identifying Operation Datas from the same TimeLog. @@ -393,6 +404,81 @@ protected IEnumerable ImportTimeLog(ISOTask loggedTask, ISOTimeLo return null; } + private List> SplitElementsByProductForm(Dictionary> productAllocations, HashSet loggedDeviceElementIds, ISODevice dvc) + { + //This function splits device elements logged by single TimeLog into groups based + //on product form referenced by these elements. This is done using following logic: + // - determine used products forms and list of device element ids for each form + // - for each product form determine device elements from all other forms + // - remove these device elements and their children from a copy of device hierarchy elements + // - this gives a list of device elements to keep for a product form + var deviceElementIdsByProductForm = productAllocations + .SelectMany(x => x.Value.Select(y => new { Form = GetProductFormByProductAllocation(y), Id = x.Key })) + .Where(x => x.Form.HasValue) + .GroupBy(x => x.Form, x => x.Id) + .Select(x => x.Distinct().ToList()) + .ToList(); + + List> deviceElementGroups = new List>(); + if (deviceElementIdsByProductForm.Count > 1) + { + var deviceHierarchyElement = TaskDataMapper.DeviceElementHierarchies.Items[dvc.DeviceId]; + + var idsWithProduct = deviceElementIdsByProductForm.SelectMany(x => x).ToList(); + foreach (var deviceElementIds in deviceElementIdsByProductForm) + { + var idsToRemove = idsWithProduct.Except(deviceElementIds).ToList(); + var idsToKeep = FilterDeviceElementIds(deviceHierarchyElement, idsToRemove); + + deviceElementGroups.Add(loggedDeviceElementIds.Intersect(idsToKeep).ToList()); + } + } + else + { + deviceElementGroups.Add(loggedDeviceElementIds.ToList()); + } + + return deviceElementGroups; + } + + private ProductFormEnum? GetProductFormByProductAllocation(ISOProductAllocation pan) + { + var adaptProductId = TaskDataMapper.InstanceIDMap.GetADAPTID(pan.ProductIdRef); + var adaptProduct = TaskDataMapper.AdaptDataModel.Catalog.Products.FirstOrDefault(x => x.Id.ReferenceId == adaptProductId); + + // Add an error if ProductAllocation is referencing non-existent product + if (adaptProduct == null) + { + TaskDataMapper.AddError($"ProductAllocation referencing Product={pan.ProductIdRef} skipped since no matching product found"); + } + return adaptProduct?.Form; + } + + private List FilterDeviceElementIds(DeviceHierarchyElement deviceHierarchyElement, List idsToRemove) + { + var elementIdsToKeep = new List(); + if (!idsToRemove.Contains(deviceHierarchyElement.DeviceElement.DeviceElementId)) + { + //By default we need to keep this element - covers scenario of no children elements + bool addThisElement = true; + if (deviceHierarchyElement.Children != null && deviceHierarchyElement.Children.Count > 0) + { + foreach (var c in deviceHierarchyElement.Children) + { + elementIdsToKeep.AddRange(FilterDeviceElementIds(c, idsToRemove)); + } + //Keep this element if at least one child element is kept + addThisElement = elementIdsToKeep.Count > 0; + } + + if (addThisElement) + { + elementIdsToKeep.Add(deviceHierarchyElement.DeviceElement.DeviceElementId); + } + } + return elementIdsToKeep; + } + protected virtual ISOTime GetTimeElementFromTimeLog(ISOTimeLog isoTimeLog) { return isoTimeLog.GetTimeElement(this.TaskDataPath); @@ -500,6 +586,35 @@ private void AddProductAllocationsForDeviceElement(Dictionary productIds) + { + var productCategories = productIds + .Select(x => TaskDataMapper.AdaptDataModel.Catalog.Products.FirstOrDefault(y => y.Id.ReferenceId == x)) + .Where(x => x != null && x.Category != CategoryEnum.Unknown) + .Select(x => x.Category) + .ToList(); + + switch (productCategories.FirstOrDefault()) + { + case CategoryEnum.Variety: + return OperationTypeEnum.SowingAndPlanting; + + case CategoryEnum.Fertilizer: + case CategoryEnum.NitrogenStabilizer: + case CategoryEnum.Manure: + return OperationTypeEnum.Fertilizing; + + case CategoryEnum.Fungicide: + case CategoryEnum.Herbicide: + case CategoryEnum.Insecticide: + case CategoryEnum.Pesticide: + return OperationTypeEnum.CropProtection; + + default: + return null; + } + } + private OperationTypeEnum GetOperationTypeFromLoggingDevices(ISOTime time) { HashSet representedTypes = new HashSet();