diff --git a/.gitignore b/.gitignore index 45a53ff..5c4f875 100644 --- a/.gitignore +++ b/.gitignore @@ -195,3 +195,6 @@ FakesAssemblies/ # Visual Studio 6 workspace options file *.opt + +# Jetbrains Rider +.idea/ diff --git a/ISOv4Plugin/ExtensionMethods/ExtensionMethods.cs b/ISOv4Plugin/ExtensionMethods/Extensions.cs similarity index 95% rename from ISOv4Plugin/ExtensionMethods/ExtensionMethods.cs rename to ISOv4Plugin/ExtensionMethods/Extensions.cs index 0949024..6dd7c0f 100644 --- a/ISOv4Plugin/ExtensionMethods/ExtensionMethods.cs +++ b/ISOv4Plugin/ExtensionMethods/Extensions.cs @@ -3,6 +3,7 @@ */ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -20,9 +21,10 @@ namespace AgGateway.ADAPT.ISOv4Plugin.ExtensionMethods { - public static class ExtensionMethods + public static class Extensions { private static readonly Regex IsoIdPattern = new Regex("^[A-Z]{3,4}-?[0-9]+$", RegexOptions.Compiled); + private static readonly ConcurrentDictionary _directoryFilesCache = new ConcurrentDictionary(); public static string WithTaskDataPath(this string dataPath) { @@ -306,16 +308,23 @@ public static IEnumerable GetDirectoryFiles(this string dataPath, string { if (Directory.Exists(dataPath)) { - //Note! We need to iterate through all files and do a ToLower for this to work in .Net Core in Linux since that filesystem - //is case sensitive and the NetStandard interface for Directory.GetFiles doesn't account for that yet. var fileNameToFind = searchPath.ToLower(); - var allFiles = Directory.GetFiles(dataPath, "*.*", searchOption); + var cacheKey = string.Concat(dataPath, "\0", (int)searchOption); + var allFiles = _directoryFilesCache.GetOrAdd(cacheKey, _ => Directory.GetFiles(dataPath, "*.*", searchOption)); var matchedFiles = allFiles.Where(file => file.ToLower().EndsWith(fileNameToFind)); return matchedFiles; } return new List(); } + /// + /// Clears the cached directory file listings. Call when directory contents may have changed. + /// + public static void ClearDirectoryFilesCache() + { + _directoryFilesCache.Clear(); + } + /// /// Case-insensitive comparison of two strings /// diff --git a/ISOv4Plugin/ISOModels/ISO11783_TaskData.cs b/ISOv4Plugin/ISOModels/ISO11783_TaskData.cs index e3d9609..b9736f9 100644 --- a/ISOv4Plugin/ISOModels/ISO11783_TaskData.cs +++ b/ISOv4Plugin/ISOModels/ISO11783_TaskData.cs @@ -84,16 +84,35 @@ public static ISO11783_TaskData ReadXML(XmlNode taskDataNode, string baseFolder) taskData.DataTransferOriginInt = taskDataNode.GetXmlNodeValueAsInt("@DataTransferOrigin"); taskData.DataTransferLanguage = taskDataNode.GetXmlNodeValue("@DataTransferLanguage"); - //-------------- - //Child Elements - //-------------- + //External file references - select all XFR nodes once and group by prefix + var allXfrNodes = taskDataNode.SelectNodes("XFR"); + var xfrByPrefix = new Dictionary>(); + if (allXfrNodes != null) + { + for (int i = 0; i < allXfrNodes.Count; i++) + { + var xfrNode = allXfrNodes[i]; + var fileName = xfrNode.GetXmlNodeValue("@A"); + if (fileName != null && fileName.Length >= 3) + { + var prefix = fileName.Substring(0, 3); + if (!xfrByPrefix.TryGetValue(prefix, out var list)) + { + list = new List(); + xfrByPrefix[prefix] = list; + } + list.Add(xfrNode); + } + } + } + //Attached Files XmlNodeList afeNodes = taskDataNode.SelectNodes("AFE"); if (afeNodes != null) { taskData.ChildElements.AddRange(ISOAttachedFile.ReadXML(afeNodes)); } - ProcessExternalNodes(taskDataNode, "AFE", baseFolder, taskData, ISOAttachedFile.ReadXML); + ProcessExternalNodes(xfrByPrefix, "AFE", baseFolder, taskData, ISOAttachedFile.ReadXML); //Coded Comments XmlNodeList cctNodes = taskDataNode.SelectNodes("CCT"); @@ -101,7 +120,7 @@ public static ISO11783_TaskData ReadXML(XmlNode taskDataNode, string baseFolder) { taskData.ChildElements.AddRange(ISOCodedComment.ReadXML(cctNodes)); } - ProcessExternalNodes(taskDataNode, "CCT", baseFolder, taskData, ISOCodedComment.ReadXML); + ProcessExternalNodes(xfrByPrefix, "CCT", baseFolder, taskData, ISOCodedComment.ReadXML); //Crop Types XmlNodeList ctpNodes = taskDataNode.SelectNodes("CTP"); @@ -109,7 +128,7 @@ public static ISO11783_TaskData ReadXML(XmlNode taskDataNode, string baseFolder) { taskData.ChildElements.AddRange(ISOCropType.ReadXML(ctpNodes)); } - ProcessExternalNodes(taskDataNode, "CTP", baseFolder, taskData, ISOCropType.ReadXML); + ProcessExternalNodes(xfrByPrefix, "CTP", baseFolder, taskData, ISOCropType.ReadXML); //Cultural Practices XmlNodeList cpcNodes = taskDataNode.SelectNodes("CPC"); @@ -117,7 +136,7 @@ public static ISO11783_TaskData ReadXML(XmlNode taskDataNode, string baseFolder) { taskData.ChildElements.AddRange(ISOCulturalPractice.ReadXML(cpcNodes)); } - ProcessExternalNodes(taskDataNode, "CPC", baseFolder, taskData, ISOCulturalPractice.ReadXML); + ProcessExternalNodes(xfrByPrefix, "CPC", baseFolder, taskData, ISOCulturalPractice.ReadXML); //Customers XmlNodeList ctrNodes = taskDataNode.SelectNodes("CTR"); @@ -125,7 +144,7 @@ public static ISO11783_TaskData ReadXML(XmlNode taskDataNode, string baseFolder) { taskData.ChildElements.AddRange(ISOCustomer.ReadXML(ctrNodes)); } - ProcessExternalNodes(taskDataNode, "CTR", baseFolder, taskData, ISOCustomer.ReadXML); + ProcessExternalNodes(xfrByPrefix, "CTR", baseFolder, taskData, ISOCustomer.ReadXML); //Devices XmlNodeList dvcNodes = taskDataNode.SelectNodes("DVC"); @@ -133,7 +152,7 @@ public static ISO11783_TaskData ReadXML(XmlNode taskDataNode, string baseFolder) { taskData.ChildElements.AddRange(ISODevice.ReadXML(dvcNodes)); } - ProcessExternalNodes(taskDataNode, "DVC", baseFolder, taskData, ISODevice.ReadXML); + ProcessExternalNodes(xfrByPrefix, "DVC", baseFolder, taskData, ISODevice.ReadXML); //Farms XmlNodeList frmNodes = taskDataNode.SelectNodes("FRM"); @@ -141,7 +160,7 @@ public static ISO11783_TaskData ReadXML(XmlNode taskDataNode, string baseFolder) { taskData.ChildElements.AddRange(ISOFarm.ReadXML(frmNodes)); } - ProcessExternalNodes(taskDataNode, "FRM", baseFolder, taskData, ISOFarm.ReadXML); + ProcessExternalNodes(xfrByPrefix, "FRM", baseFolder, taskData, ISOFarm.ReadXML); //Operation Techniques XmlNodeList otqNodes = taskDataNode.SelectNodes("OTQ"); @@ -149,7 +168,7 @@ public static ISO11783_TaskData ReadXML(XmlNode taskDataNode, string baseFolder) { taskData.ChildElements.AddRange(ISOOperationTechnique.ReadXML(otqNodes)); } - ProcessExternalNodes(taskDataNode, "OTQ", baseFolder, taskData, ISOOperationTechnique.ReadXML); + ProcessExternalNodes(xfrByPrefix, "OTQ", baseFolder, taskData, ISOOperationTechnique.ReadXML); //Partfields XmlNodeList pfdNodes = taskDataNode.SelectNodes("PFD"); @@ -157,7 +176,7 @@ public static ISO11783_TaskData ReadXML(XmlNode taskDataNode, string baseFolder) { taskData.ChildElements.AddRange(ISOPartfield.ReadXML(pfdNodes)); } - ProcessExternalNodes(taskDataNode, "PFD", baseFolder, taskData, ISOPartfield.ReadXML); + ProcessExternalNodes(xfrByPrefix, "PFD", baseFolder, taskData, ISOPartfield.ReadXML); //Products XmlNodeList pdtNodes = taskDataNode.SelectNodes("PDT"); @@ -165,7 +184,7 @@ public static ISO11783_TaskData ReadXML(XmlNode taskDataNode, string baseFolder) { taskData.ChildElements.AddRange(ISOProduct.ReadXML(pdtNodes)); } - ProcessExternalNodes(taskDataNode, "PDT", baseFolder, taskData, ISOProduct.ReadXML); + ProcessExternalNodes(xfrByPrefix, "PDT", baseFolder, taskData, ISOProduct.ReadXML); //Product Groups XmlNodeList pgpNodes = taskDataNode.SelectNodes("PGP"); @@ -173,7 +192,7 @@ public static ISO11783_TaskData ReadXML(XmlNode taskDataNode, string baseFolder) { taskData.ChildElements.AddRange(ISOProductGroup.ReadXML(pgpNodes)); } - ProcessExternalNodes(taskDataNode, "PGP", baseFolder, taskData, ISOProductGroup.ReadXML); + ProcessExternalNodes(xfrByPrefix, "PGP", baseFolder, taskData, ISOProductGroup.ReadXML); //Task Controller Capabilities XmlNodeList tccNodes = taskDataNode.SelectNodes("TCC"); @@ -181,7 +200,7 @@ public static ISO11783_TaskData ReadXML(XmlNode taskDataNode, string baseFolder) { taskData.ChildElements.AddRange(ISOTaskControllerCapabilities.ReadXML(tccNodes)); } - ProcessExternalNodes(taskDataNode, "TCC", baseFolder, taskData, ISOTaskControllerCapabilities.ReadXML); + ProcessExternalNodes(xfrByPrefix, "TCC", baseFolder, taskData, ISOTaskControllerCapabilities.ReadXML); //Tasks XmlNodeList tskNodes = taskDataNode.SelectNodes("TSK"); @@ -189,7 +208,7 @@ public static ISO11783_TaskData ReadXML(XmlNode taskDataNode, string baseFolder) { taskData.ChildElements.AddRange(ISOTask.ReadXML(tskNodes)); } - ProcessExternalNodes(taskDataNode, "TSK", baseFolder, taskData, ISOTask.ReadXML); + ProcessExternalNodes(xfrByPrefix, "TSK", baseFolder, taskData, ISOTask.ReadXML); //Value Presentations XmlNodeList vpnNodes = taskDataNode.SelectNodes("VPN"); @@ -197,7 +216,7 @@ public static ISO11783_TaskData ReadXML(XmlNode taskDataNode, string baseFolder) { taskData.ChildElements.AddRange(ISOValuePresentation.ReadXML(vpnNodes)); } - ProcessExternalNodes(taskDataNode, "VPN", baseFolder, taskData, ISOValuePresentation.ReadXML); + ProcessExternalNodes(xfrByPrefix, "VPN", baseFolder, taskData, ISOValuePresentation.ReadXML); //Workers XmlNodeList wkrNodes = taskDataNode.SelectNodes("WKR"); @@ -205,7 +224,7 @@ public static ISO11783_TaskData ReadXML(XmlNode taskDataNode, string baseFolder) { taskData.ChildElements.AddRange(ISOWorker.ReadXML(wkrNodes)); } - ProcessExternalNodes(taskDataNode, "WKR", baseFolder, taskData, ISOWorker.ReadXML); + ProcessExternalNodes(xfrByPrefix, "WKR", baseFolder, taskData, ISOWorker.ReadXML); //LinkList ISOAttachedFile linkListFile = taskData.ChildElements.OfType().SingleOrDefault(afe => afe.FileType == 1); @@ -240,9 +259,11 @@ public override List Validate(List errors) return errors; } - private static void ProcessExternalNodes(XmlNode node, string xmlPrefix, string baseFolder, ISO11783_TaskData taskData, Func> readDelegate) + private static void ProcessExternalNodes(Dictionary> xfrByPrefix, string xmlPrefix, string baseFolder, ISO11783_TaskData taskData, Func> readDelegate) { - var externalNodes = node.SelectNodes($"XFR[starts-with(@A, '{xmlPrefix}')]"); + if (!xfrByPrefix.TryGetValue(xmlPrefix, out var externalNodes)) + return; + for (int i = 0; i < externalNodes.Count; i++) { var inputNodes = externalNodes[i].LoadActualNodes("XFR", baseFolder); diff --git a/ISOv4Plugin/ISOModels/ISOTimeLog.cs b/ISOv4Plugin/ISOModels/ISOTimeLog.cs index f10b85c..d9d6081 100644 --- a/ISOv4Plugin/ISOModels/ISOTimeLog.cs +++ b/ISOv4Plugin/ISOModels/ISOTimeLog.cs @@ -22,6 +22,9 @@ public class ISOTimeLog : ISOElement public uint? Filelength { get; set; } public byte TimeLogType { get; set; } + private ISOTime _cachedTimeElement; + private string _cachedDataPath; + public override XmlWriter WriteXML(XmlWriter xmlBuilder) { xmlBuilder.WriteStartElement("TLG"); @@ -53,6 +56,11 @@ public static IEnumerable ReadXML(XmlNodeList nodes) public ISOTime GetTimeElement(string dataPath) { + if (_cachedTimeElement != null && _cachedDataPath == dataPath) + { + return _cachedTimeElement; + } + string xmlName = string.Concat(Filename, ".xml"); string filePath = dataPath.GetDirectoryFiles(xmlName, SearchOption.TopDirectoryOnly).FirstOrDefault(); if (filePath != null) @@ -61,10 +69,14 @@ public ISOTime GetTimeElement(string dataPath) document.Load(filePath); XmlNode rootNode = document.SelectSingleNode("TIM"); - return ISOTime.ReadXML(rootNode); + var timeElement = ISOTime.ReadXML(rootNode); + _cachedTimeElement = timeElement; + _cachedDataPath = dataPath; + return timeElement; } else { + _cachedDataPath = dataPath; return null; } } diff --git a/ISOv4Plugin/ObjectModel/DeviceElementHierarchy.cs b/ISOv4Plugin/ObjectModel/DeviceElementHierarchy.cs index 2b7fc59..ecc566e 100644 --- a/ISOv4Plugin/ObjectModel/DeviceElementHierarchy.cs +++ b/ISOv4Plugin/ObjectModel/DeviceElementHierarchy.cs @@ -247,31 +247,27 @@ public DeviceHierarchyElement(ISODeviceElement deviceElement, //DeviceProperty assigned Widths & Offsets //DeviceProcessData assigned values will be assigned as the SectionMapper reads timelog data. + //Build a lookup of DeviceProperties by DDI for O(1) access + var propertiesByDDI = deviceElement.DeviceProperties + .GroupBy(dpt => dpt.DDI) + .ToDictionary(g => g.Key, g => g.First()); + //Width - ISODeviceProperty widthProperty = deviceElement.DeviceProperties.FirstOrDefault(dpt => dpt.DDI == "0046"); //Max width - if (widthProperty != null) + ISODeviceProperty widthProperty; + if (propertiesByDDI.TryGetValue("0046", out widthProperty)) //Max width { Width = widthProperty.Value; WidthDDI = "0046"; } - else + else if (propertiesByDDI.TryGetValue("0044", out widthProperty)) //Default working width { - widthProperty = deviceElement.DeviceProperties.FirstOrDefault(dpt => dpt.DDI == "0044"); //Default working width - if (widthProperty != null) - { - Width = widthProperty.Value; - WidthDDI = "0044"; - } - - if (widthProperty == null) - { - widthProperty = deviceElement.DeviceProperties.FirstOrDefault(dpt => dpt.DDI == "0043"); //Actual working width - if (widthProperty != null) - { - Width = widthProperty.Value; - WidthDDI = "0043"; - } - } + Width = widthProperty.Value; + WidthDDI = "0044"; + } + else if (propertiesByDDI.TryGetValue("0043", out widthProperty)) //Actual working width + { + Width = widthProperty.Value; + WidthDDI = "0043"; } if (Width == null) @@ -281,8 +277,8 @@ public DeviceHierarchyElement(ISODeviceElement deviceElement, } //Offsets - ISODeviceProperty xOffsetProperty = deviceElement.DeviceProperties.FirstOrDefault(dpt => dpt.DDI == "0086"); - if (xOffsetProperty != null) + ISODeviceProperty xOffsetProperty; + if (propertiesByDDI.TryGetValue("0086", out xOffsetProperty)) { XOffset = xOffsetProperty.Value; } @@ -291,8 +287,8 @@ public DeviceHierarchyElement(ISODeviceElement deviceElement, AddMissingGeometryDefinition(missingGeometryDefinitions, deviceElement.DeviceElementId, "0086"); } - ISODeviceProperty yOffsetProperty = deviceElement.DeviceProperties.FirstOrDefault(dpt => dpt.DDI == "0087"); - if (yOffsetProperty != null) + ISODeviceProperty yOffsetProperty; + if (propertiesByDDI.TryGetValue("0087", out yOffsetProperty)) { YOffset = yOffsetProperty.Value; } @@ -301,8 +297,8 @@ public DeviceHierarchyElement(ISODeviceElement deviceElement, AddMissingGeometryDefinition(missingGeometryDefinitions, deviceElement.DeviceElementId, "0087"); } - ISODeviceProperty zOffsetProperty = deviceElement.DeviceProperties.FirstOrDefault(dpt => dpt.DDI == "0088"); - if (zOffsetProperty != null) + ISODeviceProperty zOffsetProperty; + if (propertiesByDDI.TryGetValue("0088", out zOffsetProperty)) { ZOffset = zOffsetProperty.Value; } diff --git a/ISOv4Plugin/Plugin.cs b/ISOv4Plugin/Plugin.cs index eb46d62..7ce1c4d 100644 --- a/ISOv4Plugin/Plugin.cs +++ b/ISOv4Plugin/Plugin.cs @@ -34,44 +34,58 @@ public Plugin() public void Export(ApplicationDataModel.ADM.ApplicationDataModel dataModel, string exportPath, Properties properties) { - //Convert the ADAPT model into the ISO model - string outputPath = exportPath.WithTaskDataPath(); - TaskDataMapper taskDataMapper = new TaskDataMapper(outputPath, properties); - Errors = taskDataMapper.Errors; - ISO11783_TaskData taskData = taskDataMapper.Export(dataModel); - - //Serialize the ISO model to XML - using (TaskDocumentWriter writer = new TaskDocumentWriter()) + try { - writer.WriteTaskData(outputPath, taskData); - - //Serialize the Link List - if (taskData.Version > 3) + //Convert the ADAPT model into the ISO model + string outputPath = exportPath.WithTaskDataPath(); + TaskDataMapper taskDataMapper = new TaskDataMapper(outputPath, properties); + Errors = taskDataMapper.Errors; + ISO11783_TaskData taskData = taskDataMapper.Export(dataModel); + + //Serialize the ISO model to XML + using (TaskDocumentWriter writer = new TaskDocumentWriter()) { - writer.WriteLinkList(outputPath, taskData.LinkList); + writer.WriteTaskData(outputPath, taskData); + + //Serialize the Link List + if (taskData.Version > 3) + { + writer.WriteLinkList(outputPath, taskData.LinkList); + } } } + finally + { + Extensions.ClearDirectoryFilesCache(); + } } public IList Import(string dataPath, Properties properties = null) { - var taskDataObjects = ReadDataCard(dataPath); - if (taskDataObjects == null) - return null; - - var adms = new List(); - foreach (var taskData in taskDataObjects) + try { - //Convert the ISO model to ADAPT - TaskDataMapper taskDataMapper = new TaskDataMapper(taskData.DataFolder, properties, taskData.VersionMajor); - ApplicationDataModel.ADM.ApplicationDataModel dataModel = taskDataMapper.Import(taskData); - foreach (var error in taskDataMapper.Errors) + var taskDataObjects = ReadDataCard(dataPath); + if (taskDataObjects == null) + return null; + + var adms = new List(); + foreach (var taskData in taskDataObjects) { - Errors.Add(error); + //Convert the ISO model to ADAPT + TaskDataMapper taskDataMapper = new TaskDataMapper(taskData.DataFolder, properties, taskData.VersionMajor); + ApplicationDataModel.ADM.ApplicationDataModel dataModel = taskDataMapper.Import(taskData); + foreach (var error in taskDataMapper.Errors) + { + Errors.Add(error); + } + adms.Add(dataModel); } - adms.Add(dataModel); + return adms; + } + finally + { + Extensions.ClearDirectoryFilesCache(); } - return adms; } Properties _properties = null; diff --git a/ISOv4Plugin/Representation/RepresentationMapper.cs b/ISOv4Plugin/Representation/RepresentationMapper.cs index 0f6f271..3ddab4f 100644 --- a/ISOv4Plugin/Representation/RepresentationMapper.cs +++ b/ISOv4Plugin/Representation/RepresentationMapper.cs @@ -27,32 +27,46 @@ public interface IRepresentationMapper public class RepresentationMapper : IRepresentationMapper { private readonly Dictionary _ddis; + private readonly Dictionary _ddiToRepresentationCache; public RepresentationMapper() { _ddis = DdiLoader.Ddis; + _ddiToRepresentationCache = BuildDdiToRepresentationCache(); } - public AdaptRepresentation Map(int ddi) + private Dictionary BuildDdiToRepresentationCache() { - if (_ddis.ContainsKey(ddi)) + var cache = new Dictionary(); + foreach (var kvp in _ddis) { - var matchingDdi = _ddis[ddi]; + var ddi = kvp.Key; + var matchingDdi = kvp.Value; var representations = RepresentationManager.Instance.Representations.Where(x => x.Ddi.GetValueOrDefault() == matchingDdi.Id); if (representations.Any()) { - //Default the representation mapping approprately on import var representation = representations.FirstOrDefault(r => r.IsDefaultRepresentationForDDI) ?? representations.First(); - - AdaptRepresentation adaptRep = GetADAPTRepresentation(representation); + var adaptRep = GetADAPTRepresentation(representation); if (adaptRep != null) { - return adaptRep; + cache[ddi] = adaptRep; } } + } + return cache; + } + + public AdaptRepresentation Map(int ddi) + { + if (_ddiToRepresentationCache.TryGetValue(ddi, out var cached)) + { + return cached; + } + if (_ddis.ContainsKey(ddi)) + { return new ApplicationDataModel.Representations.NumericRepresentation { Code = ddi.ToString("X4"), CodeSource = RepresentationCodeSourceEnum.ISO11783_DDI }; } return null;