using System.Collections.Generic; using System.Linq; using ZB.MOM.WW.OtOpcUa.Host.Domain; namespace ZB.MOM.WW.OtOpcUa.Host.OpcUa { /// /// Computes the set of changed Galaxy object IDs between two snapshots of hierarchy and attributes. /// public static class AddressSpaceDiff { /// /// Compares old and new hierarchy+attributes and returns the set of gobject IDs that have any difference. /// /// The previously published Galaxy object hierarchy snapshot. /// The previously published Galaxy attribute snapshot keyed to the old hierarchy. /// The latest Galaxy object hierarchy snapshot pulled from the repository. /// The latest Galaxy attribute snapshot that should be reflected in the OPC UA namespace. public static HashSet FindChangedGobjectIds( List oldHierarchy, List oldAttributes, List newHierarchy, List newAttributes) { var changed = new HashSet(); var oldObjects = oldHierarchy.ToDictionary(h => h.GobjectId); var newObjects = newHierarchy.ToDictionary(h => h.GobjectId); // Added objects foreach (var id in newObjects.Keys) if (!oldObjects.ContainsKey(id)) changed.Add(id); // Removed objects foreach (var id in oldObjects.Keys) if (!newObjects.ContainsKey(id)) changed.Add(id); // Modified objects foreach (var kvp in newObjects) if (oldObjects.TryGetValue(kvp.Key, out var oldObj) && !ObjectsEqual(oldObj, kvp.Value)) changed.Add(kvp.Key); // Attribute changes — group by gobject_id and compare var oldAttrsByObj = oldAttributes.GroupBy(a => a.GobjectId) .ToDictionary(g => g.Key, g => g.ToList()); var newAttrsByObj = newAttributes.GroupBy(a => a.GobjectId) .ToDictionary(g => g.Key, g => g.ToList()); // All gobject_ids that have attributes in either old or new var allAttrGobjectIds = new HashSet(oldAttrsByObj.Keys); allAttrGobjectIds.UnionWith(newAttrsByObj.Keys); foreach (var id in allAttrGobjectIds) { if (changed.Contains(id)) continue; oldAttrsByObj.TryGetValue(id, out var oldAttrs); newAttrsByObj.TryGetValue(id, out var newAttrs); if (!AttributeSetsEqual(oldAttrs, newAttrs)) changed.Add(id); } return changed; } /// /// Expands a set of changed gobject IDs to include all descendant gobject IDs in the hierarchy. /// /// The root Galaxy objects that were detected as changed between snapshots. /// The hierarchy used to include descendant objects whose OPC UA nodes must also be rebuilt. public static HashSet ExpandToSubtrees(HashSet changed, List hierarchy) { var childrenByParent = hierarchy.GroupBy(h => h.ParentGobjectId) .ToDictionary(g => g.Key, g => g.Select(h => h.GobjectId).ToList()); var expanded = new HashSet(changed); var queue = new Queue(changed); while (queue.Count > 0) { var id = queue.Dequeue(); if (childrenByParent.TryGetValue(id, out var children)) foreach (var childId in children) if (expanded.Add(childId)) queue.Enqueue(childId); } return expanded; } private static bool ObjectsEqual(GalaxyObjectInfo a, GalaxyObjectInfo b) { return a.TagName == b.TagName && a.BrowseName == b.BrowseName && a.ContainedName == b.ContainedName && a.ParentGobjectId == b.ParentGobjectId && a.IsArea == b.IsArea; } private static bool AttributeSetsEqual(List? a, List? b) { if (a == null && b == null) return true; if (a == null || b == null) return false; if (a.Count != b.Count) return false; // Sort by a stable key and compare pairwise var sortedA = a.OrderBy(x => x.FullTagReference).ThenBy(x => x.PrimitiveName).ToList(); var sortedB = b.OrderBy(x => x.FullTagReference).ThenBy(x => x.PrimitiveName).ToList(); for (var i = 0; i < sortedA.Count; i++) if (!AttributesEqual(sortedA[i], sortedB[i])) return false; return true; } private static bool AttributesEqual(GalaxyAttributeInfo a, GalaxyAttributeInfo b) { return a.AttributeName == b.AttributeName && a.FullTagReference == b.FullTagReference && a.MxDataType == b.MxDataType && a.IsArray == b.IsArray && a.ArrayDimension == b.ArrayDimension && a.PrimitiveName == b.PrimitiveName && a.SecurityClassification == b.SecurityClassification && a.IsHistorized == b.IsHistorized && a.IsAlarm == b.IsAlarm; } } }