diff --git a/.ci/unit-tests/BHoM_Adapter_Tests/PushTests.cs b/.ci/unit-tests/BHoM_Adapter_Tests/PushTests.cs index 12a11aa3..e0be67d4 100644 --- a/.ci/unit-tests/BHoM_Adapter_Tests/PushTests.cs +++ b/.ci/unit-tests/BHoM_Adapter_Tests/PushTests.cs @@ -33,7 +33,7 @@ using System.Diagnostics.Contracts; using AutoBogus; using Shouldly; - +using BH.oM.Geometry; namespace BH.Tests.Adapter.Structure { @@ -516,5 +516,65 @@ public void Preprocess_PanelLoadsDuplicateIds() } + + [Test] + [Description("Tests that objects being pushed are correctly 'merged' by calls to CopyProperties modules. \n" + + "Two nodes pushed in the same location, one with a support and one without, the adapter should make sure the final node being sent for creation should contain the support.")] + public void CopyProperties_NodesReplaced() + { + //Create bar from line. Nodes will have null-constraints on the Bar + Line line = new Line { Start = new Point { X = 0 }, End = new Point { X = 10 } }; + Bar bar = BH.Engine.Structure.Create.Bar(line, null, 0); + + //Create nodes in the same position + Node node1 = new Node { Position = line.Start, Support = Create.RandomObject() }; + Node node2 = new Node { Position = line.End, Support = Create.RandomObject() }; + + //Push with bar before the nodes + List inputObjs = new List { bar, node1, node2 }; + sa.Push(inputObjs); + + //Make sure the nodes in the model contain the supports + var supports = sa.Created.Where(x => x.Item1 == typeof(Node)).SelectMany(x => x.Item2).Cast().Select(x => x.Support).Where(x => x != null); + supports.ShouldContain(x => x.Name == node1.Support.Name); + supports.ShouldContain(x => x.Name == node2.Support.Name); + } + + [Test] + [Description("Tests that all objects sent to the push have AdapterIds assigned, even though some have been identified as duplicates and hence culled out.")] + public void DuplicateObjects_EnsureAllOutputHaveIds() + { + //Create duplicate elements + Steel steel1 = Create.RandomObject(); + Steel steel2 = Create.RandomObject(); + + steel1.Name = "MatName"; + steel2.Name = steel1.Name; + + SteelSection section1 = Create.RandomObject(); + SteelSection section2 = Create.RandomObject(); + section1.Material = steel1; + section2.Material = steel2; + section1.Name = "SecName"; + section2.Name = section1.Name; + + Line line = new Line { Start = new Point { X = 0 }, End = new Point { X = 10 } }; + Bar bar1 = BH.Engine.Structure.Create.Bar(line, section1, 0); + Bar bar2 = BH.Engine.Structure.Create.Bar(line, section1, 0); + + Node node1 = new Node { Position = line.Start }; + Node node2 = new Node { Position = line.End }; + + //Push duplicates + List inputObjs = new List { bar1, bar2, node1, node2, section1, section2, steel1, steel2 }; + List pushed = sa.Push(inputObjs).OfType().ToList(); + + //Make sure correct number of items has been created to ensure comparers work. + //If this does not work, the check of all objects having assigned Ids is pointless + sa.Created.Where(x => x.Item1 != typeof(Node)).ShouldAllBe(x => x.Item2.Count() == 1); + sa.Created.Where(x => x.Item1 == typeof(Node)).ShouldAllBe(x => x.Item2.Count() == 2); + + pushed.ShouldAllBe(x => BH.Engine.Base.Query.FindFragment(x, typeof(StructuralAdapterId)) != null, "At least one of the pushed objects did not contain an AdapterId Fragment."); + } } } \ No newline at end of file diff --git a/Adapter_Engine/Query/GetCopyPropertiesModules.cs b/Adapter_Engine/Query/GetCopyPropertiesModules.cs index d129287a..ae1254e0 100644 --- a/Adapter_Engine/Query/GetCopyPropertiesModules.cs +++ b/Adapter_Engine/Query/GetCopyPropertiesModules.cs @@ -32,7 +32,8 @@ namespace BH.Engine.Adapter { public static partial class Query { - public static List> GetCopyPropertiesModules(this IBHoMAdapter adapter) where T : class, IBHoMObject + [Description("Gets any adapter module on the adapter for copying properties of an object of type T to another object of type T.")] + public static List> GetCopyPropertiesModules(this IBHoMAdapter adapter) where T : IBHoMObject { return adapter.AdapterModules.OfType>().ToList(); } diff --git a/BHoM_Adapter/AdapterActions/_PushMethods/CRUDDispatchers/CreateOnly.cs b/BHoM_Adapter/AdapterActions/_PushMethods/CRUDDispatchers/CreateOnly.cs index 3feee507..fdfc7d89 100644 --- a/BHoM_Adapter/AdapterActions/_PushMethods/CRUDDispatchers/CreateOnly.cs +++ b/BHoM_Adapter/AdapterActions/_PushMethods/CRUDDispatchers/CreateOnly.cs @@ -47,7 +47,15 @@ protected virtual bool CreateOnly(IEnumerable objectsToPush, string tag = { bool callDistinct = objectLevel == 0 ? m_AdapterSettings.CreateOnly_DistinctObjects : m_AdapterSettings.CreateOnly_DistinctDependencies; - List newObjects = !callDistinct ? objectsToPush.ToList() : objectsToPush.Distinct(Engine.Adapter.Query.GetComparerForType(this, actionConfig)).ToList(); + List newObjects; + IEnumerable> distinctGroups = null; + if (!callDistinct) + newObjects = objectsToPush.ToList(); + else + { + distinctGroups = GroupAndCopyProperties(objectsToPush, actionConfig); + newObjects = distinctGroups.Select(x => x.Key).ToList(); + } // Tag the objects, if tag is given. if (tag != "") @@ -66,20 +74,21 @@ protected virtual bool CreateOnly(IEnumerable objectsToPush, string tag = else if(!ICreate(newObjects, actionConfig)) return false; - if (callDistinct && m_AdapterSettings.UseAdapterId) + if (callDistinct && m_AdapterSettings.UseAdapterId && distinctGroups != null) { // Map Ids to the original set of objects (before we extracted the distincts elements from it). // If some objects of the original set were not Created (because e.g. they were already existing in the external model and had already an id, // therefore no new id was assigned to them) they will not get mapped, so the original set will be left with them intact. - IEqualityComparer comparer = Engine.Adapter.Query.GetComparerForType(this, actionConfig); - foreach (T item in objectsToPush) + foreach (var group in distinctGroups) { - // Fetch any existing IAdapterId fragment and assign it to the item. - // This preserves any additional property other than `Id` that may be in the fragment. - IFragment fragment; - newObjects.First(x => comparer.Equals(x, item)).Fragments.TryGetValue(AdapterIdFragmentType, out fragment); - - item.SetAdapterId(fragment as IAdapterId); + IFragment idFragment; + if (group.Key.Fragments.TryGetValue(AdapterIdFragmentType, out idFragment)) + { + foreach (T item in group.Skip(1)) //Skip 1 as first instance is the key + { + item.SetAdapterId(idFragment as IAdapterId); + } + } } } diff --git a/BHoM_Adapter/AdapterActions/_PushMethods/CRUDDispatchers/FullCRUD.cs b/BHoM_Adapter/AdapterActions/_PushMethods/CRUDDispatchers/FullCRUD.cs index ab7d0769..c9e18fb2 100644 --- a/BHoM_Adapter/AdapterActions/_PushMethods/CRUDDispatchers/FullCRUD.cs +++ b/BHoM_Adapter/AdapterActions/_PushMethods/CRUDDispatchers/FullCRUD.cs @@ -43,13 +43,14 @@ public abstract partial class BHoMAdapter // These methods dispatch calls to different CRUD methods as required by the Push. [Description("Performs the full CRUD, calling the single CRUD methods as appropriate.")] - protected bool FullCRUD(IEnumerable objectsToPush, PushType pushType = PushType.AdapterDefault, string tag = "", ActionConfig actionConfig = null) where T : class, IBHoMObject + protected bool FullCRUD(IEnumerable objectsToPush, PushType pushType = PushType.AdapterDefault, string tag = "", ActionConfig actionConfig = null) where T : IBHoMObject { if (objectsToPush == null || !objectsToPush.Any()) return true; - // Make sure objects are distinct - List newObjects = objectsToPush.Distinct(Engine.Adapter.Query.GetComparerForType(this, actionConfig)).ToList(); + // Make sure objects are distinct and that any copy-proeprty module for the type is run + IEnumerable> distinctGroups = GroupAndCopyProperties(objectsToPush, actionConfig); + List newObjects = distinctGroups.Select(x => x.Key).ToList(); // Add the tag if provided if (!string.IsNullOrWhiteSpace(tag)) @@ -113,15 +114,16 @@ protected bool FullCRUD(IEnumerable objectsToPush, PushType pushType = Pus // Map Ids to the original set of objects (before we extracted the distincts elements from it). // If some objects of the original set were not Created (because e.g. they were already existing in the external model and had already an id, // therefore no new id was assigned to them) they will not get mapped, so the original set will be left with them intact. - IEqualityComparer comparer = Engine.Adapter.Query.GetComparerForType(this, actionConfig); - foreach (T item in objectsToPush) + foreach (var group in distinctGroups) { - // Fetch any existing IAdapterId fragment and assign it to the item. - // This preserves any additional property other than `Id` that may be in the fragment. - IFragment fragment; - newObjects.First(x => comparer.Equals(x, item)).Fragments.TryGetValue(AdapterIdFragmentType, out fragment); - - item.SetAdapterId(fragment as IAdapterId); + IFragment idFragment; + if (group.Key.Fragments.TryGetValue(AdapterIdFragmentType, out idFragment)) + { + foreach (T item in group.Skip(1)) //Skip 1 as first instance is the key + { + item.SetAdapterId(idFragment as IAdapterId); + } + } } } @@ -130,7 +132,7 @@ protected bool FullCRUD(IEnumerable objectsToPush, PushType pushType = Pus /***************************************************/ - protected IEnumerable ReplaceInMemory(IEnumerable newObjects, IEnumerable existingOjects, string tag, ActionConfig actionConfig, bool mergeWithComparer = false) where T : class, IBHoMObject + protected IEnumerable ReplaceInMemory(IEnumerable newObjects, IEnumerable existingOjects, string tag, ActionConfig actionConfig, bool mergeWithComparer = false) where T : IBHoMObject { // Separate objects based on tags List multiTaggedObjects = existingOjects.Where(x => x.Tags.Contains(tag) && x.Tags.Count > 1).ToList(); @@ -164,7 +166,7 @@ protected IEnumerable ReplaceInMemory(IEnumerable newObjects, IEnumerab /***************************************************/ - protected IEnumerable ReplaceThroughAPI(IEnumerable objsToPush, IEnumerable readObjs, string tag, ActionConfig actionConfig, PushType pushType) where T : class, IBHoMObject + protected IEnumerable ReplaceThroughAPI(IEnumerable objsToPush, IEnumerable readObjs, string tag, ActionConfig actionConfig, PushType pushType) where T : IBHoMObject { IEqualityComparer comparer = Engine.Adapter.Query.GetComparerForType(this, actionConfig); VennDiagram diagram = Engine.Data.Create.VennDiagram(objsToPush, readObjs, comparer); diff --git a/BHoM_Adapter/HelperMethods/CopyBHoMObjectProperties.cs b/BHoM_Adapter/HelperMethods/CopyBHoMObjectProperties.cs index d321b870..a2f50132 100644 --- a/BHoM_Adapter/HelperMethods/CopyBHoMObjectProperties.cs +++ b/BHoM_Adapter/HelperMethods/CopyBHoMObjectProperties.cs @@ -36,7 +36,7 @@ namespace BH.Adapter public abstract partial class BHoMAdapter { [Description("Gets called during the Push. Takes properties specified from the source IBHoMObject and assigns them to the target IBHoMObject.")] - public void CopyBHoMObjectProperties(T target, T source) where T : class, IBHoMObject + public void CopyBHoMObjectProperties(T target, T source) where T : IBHoMObject { // Port tags from source to target foreach (string tag in source.Tags) diff --git a/BHoM_Adapter/HelperMethods/DistinctWithCopiedProperties.cs b/BHoM_Adapter/HelperMethods/DistinctWithCopiedProperties.cs new file mode 100644 index 00000000..9e230b3b --- /dev/null +++ b/BHoM_Adapter/HelperMethods/DistinctWithCopiedProperties.cs @@ -0,0 +1,48 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2023, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.Engine.Adapter; +using BH.Engine.Base; +using BH.oM.Adapter; +using BH.oM.Base; +using BH.oM.Data; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace BH.Adapter +{ + + public abstract partial class BHoMAdapter + { + [Description("Gets distinct objects based on implemented AdapterComparer of the particular type, with CopyPropertyModules available for the type run.")] + private List DistinctWithCopiedProperties(IEnumerable objectsToPush, ActionConfig actionConfig = null) where T : IBHoMObject + { + return GroupAndCopyProperties(objectsToPush, actionConfig).Select(x => x.Key).ToList(); + } + } +} + + + + diff --git a/BHoM_Adapter/HelperMethods/GroupAndCopyProperties.cs b/BHoM_Adapter/HelperMethods/GroupAndCopyProperties.cs new file mode 100644 index 00000000..2f07a91a --- /dev/null +++ b/BHoM_Adapter/HelperMethods/GroupAndCopyProperties.cs @@ -0,0 +1,64 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2023, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.Engine.Adapter; +using BH.Engine.Base; +using BH.oM.Adapter; +using BH.oM.Base; +using BH.oM.Data; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace BH.Adapter +{ + public abstract partial class BHoMAdapter + { + [Description("Groups the objects by the coparer for the particular type, and then runs any CopyPropertiesModules available for the type.")] + private IEnumerable> GroupAndCopyProperties(IEnumerable objectsToPush, ActionConfig actionConfig = null) where T : IBHoMObject + { + IEnumerable> grouped = objectsToPush.GroupBy(x => x, Engine.Adapter.Query.GetComparerForType(this, actionConfig)); + + List> copyPropertiesModules = this.GetCopyPropertiesModules(); + + foreach (var group in grouped) + { + T keep = group.Key; + foreach (T item in group.Skip(1)) //Skip 1 as first instance is the key + { + CopyBHoMObjectProperties(keep, item); + foreach (var copyModule in copyPropertiesModules) + { + copyModule.CopyProperties(keep, item); + } + } + } + + return grouped; + } + } +} + + + + diff --git a/Structure_AdapterModules/CopyNodeProperties.cs b/Structure_AdapterModules/CopyNodeProperties.cs index 2a35a4c1..28f6eb56 100644 --- a/Structure_AdapterModules/CopyNodeProperties.cs +++ b/Structure_AdapterModules/CopyNodeProperties.cs @@ -28,6 +28,8 @@ using System.ComponentModel; using BH.oM.Structure.Elements; using BH.oM.Structure.SectionProperties; +using BH.Engine.Structure; +using BH.Engine.Geometry; namespace BH.Adapter.Modules { @@ -38,12 +40,27 @@ public class CopyNodeProperties : ICopyPropertiesModule public void CopyProperties(Node target, Node source) { // If source is constrained and target is not, add source constraint to target - if (source.Support != null && target.Support == null) - target.Support = source.Support; + if (source.Support != null) + { + if (target.Support == null) + target.Support = source.Support; + else + { + string desc1 = target.Support.Description(); + string desc2 = source.Support.Description(); + if(desc1 != desc2) + Engine.Base.Compute.RecordNote($"Node in position ({target.Position.X},{target.Position.Y},{target.Position.Z}) contains conflicting supports. Support {desc1} will be used on the node."); + } + } // If source has a defined orientation and target does not, add local orientation from the source - if (source.Orientation != null && target.Orientation == null) - target.Orientation = source.Orientation; + if (source.Orientation != null) + { + if (target.Orientation == null) + target.Orientation = source.Orientation; + else if(!source.Orientation.IsEqual(target.Orientation)) + BH.Engine.Base.Compute.RecordNote($"Node in position ({target.Position.X}, {target.Position.Y}, {target.Position.Z}) contains conflicting orientaions. Orientation with Normal vector ({target.Orientation.Z.X}, {target.Orientation.Z.Y}, {target.Orientation.Z.Z}) will be used on the node."); + } } } } diff --git a/Structure_AdapterModules/Structure_AdapterModules.csproj b/Structure_AdapterModules/Structure_AdapterModules.csproj index bd1d7473..18b427f6 100644 --- a/Structure_AdapterModules/Structure_AdapterModules.csproj +++ b/Structure_AdapterModules/Structure_AdapterModules.csproj @@ -78,11 +78,21 @@ false false + + C:\ProgramData\BHoM\Assemblies\Spatial_oM.dll + false + false + C:\ProgramData\BHoM\Assemblies\Structure_oM.dll false false + + C:\ProgramData\BHoM\Assemblies\Structure_Engine.dll + false + false +