From c1ebd16837aab70d4057a052b6aa649ee6d51a9d Mon Sep 17 00:00:00 2001 From: Eric Pohl Date: Thu, 9 Oct 2025 10:14:24 -0400 Subject: [PATCH 1/7] Construct DateTime objects with Local DateTimeKind --- ISOv4Plugin/Mappers/TimeLogMapper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ISOv4Plugin/Mappers/TimeLogMapper.cs b/ISOv4Plugin/Mappers/TimeLogMapper.cs index 4922ace..5c336a7 100644 --- a/ISOv4Plugin/Mappers/TimeLogMapper.cs +++ b/ISOv4Plugin/Mappers/TimeLogMapper.cs @@ -150,7 +150,7 @@ private class BinaryWriter { // ATTENTION: CoordinateMultiplier and ZMultiplier also exist in Import\SpatialRecordMapper.cs! private const double CoordinateMultiplier = 0.0000001; private const double ZMultiplier = 0.001; // In ISO the PositionUp value is specified in mm. - private readonly DateTime _januaryFirst1980 = new DateTime(1980, 1, 1); + private readonly DateTime _januaryFirst1980 = new DateTime(1980, 1, 1, 0, 0, 0, DateTimeKind.Local); private readonly IEnumeratedValueMapper _enumeratedValueMapper; private readonly INumericValueMapper _numericValueMapper; @@ -753,7 +753,7 @@ internal static Dictionary ReadImplementGeometryValues(IEnumerable ReadImplementGeometryValues(string filePath, ISOTime templateTime, IEnumerable desiredDLVIndices, int version, IList errors) { From 5624d23d0f305cbf3a10093e4d421bf314ac7560 Mon Sep 17 00:00:00 2001 From: Eric Pohl Date: Thu, 9 Oct 2025 10:34:01 -0400 Subject: [PATCH 2/7] Consolidate "first day of 1980" fields --- ISOv4Plugin/Mappers/TimeLogMapper.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ISOv4Plugin/Mappers/TimeLogMapper.cs b/ISOv4Plugin/Mappers/TimeLogMapper.cs index 5c336a7..13a39bb 100644 --- a/ISOv4Plugin/Mappers/TimeLogMapper.cs +++ b/ISOv4Plugin/Mappers/TimeLogMapper.cs @@ -33,6 +33,8 @@ internal TimeLogMapper(TaskDataMapper taskDataMapper) : base(taskDataMapper, "TL { } + private static readonly DateTime _firstDayOf1980 = new DateTime(1980, 1, 1, 0, 0, 0, DateTimeKind.Local); + #region Export private Dictionary _dataLogValueOrdersByWorkingDataID; public IEnumerable ExportTimeLogs(IEnumerable operationDatas, string dataPath) @@ -150,7 +152,6 @@ private class BinaryWriter { // ATTENTION: CoordinateMultiplier and ZMultiplier also exist in Import\SpatialRecordMapper.cs! private const double CoordinateMultiplier = 0.0000001; private const double ZMultiplier = 0.001; // In ISO the PositionUp value is specified in mm. - private readonly DateTime _januaryFirst1980 = new DateTime(1980, 1, 1, 0, 0, 0, DateTimeKind.Local); private readonly IEnumeratedValueMapper _enumeratedValueMapper; private readonly INumericValueMapper _numericValueMapper; @@ -193,7 +194,7 @@ private void WriteSpatialRecord(SpatialRecord spatialRecord, List m var millisecondsSinceMidnight = (UInt32)new TimeSpan(0, spatialRecord.Timestamp.Hour, spatialRecord.Timestamp.Minute, spatialRecord.Timestamp.Second, spatialRecord.Timestamp.Millisecond).TotalMilliseconds; memoryStream.Write(BitConverter.GetBytes(millisecondsSinceMidnight), 0, 4); - var daysSinceJanOne1980 = (UInt16)(spatialRecord.Timestamp - (_januaryFirst1980)).TotalDays; + var daysSinceJanOne1980 = (UInt16)(spatialRecord.Timestamp - _firstDayOf1980).TotalDays; memoryStream.Write(BitConverter.GetBytes(daysSinceJanOne1980), 0, 2); //Position @@ -753,8 +754,6 @@ internal static Dictionary ReadImplementGeometryValues(IEnumerable ReadImplementGeometryValues(string filePath, ISOTime templateTime, IEnumerable desiredDLVIndices, int version, IList errors) { Dictionary output = new Dictionary(); From 59d26e07e3fb035871e97ad16b9545cd09a9b46a Mon Sep 17 00:00:00 2001 From: ericpohl Date: Fri, 5 Dec 2025 11:47:08 -0500 Subject: [PATCH 3/7] More messing with timezones --- ISOv4Plugin/ExtensionMethods/XmlExtensions.cs | 33 ++++++++++++++-- .../Import/SpatialRecordMapper.cs | 39 ++++++++++++------- ISOv4Plugin/Mappers/TaskDataMapper.cs | 1 + ISOv4Plugin/Mappers/TimeLogMapper.cs | 8 +++- 4 files changed, 60 insertions(+), 21 deletions(-) diff --git a/ISOv4Plugin/ExtensionMethods/XmlExtensions.cs b/ISOv4Plugin/ExtensionMethods/XmlExtensions.cs index e99fac4..93ee1b9 100644 --- a/ISOv4Plugin/ExtensionMethods/XmlExtensions.cs +++ b/ISOv4Plugin/ExtensionMethods/XmlExtensions.cs @@ -6,12 +6,15 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Xml; namespace AgGateway.ADAPT.ISOv4Plugin.ExtensionMethods { public static class XmlExtensions { + private static readonly Regex _timezoneOffsetRegex = new Regex(@"(\+|-)\d\d:\d\d|Z$", RegexOptions.Compiled); + public static XmlNodeList LoadActualNodes(this XmlNode xmlNode, string externalNodeTag, string baseFolder) { if (string.Equals(xmlNode.Name, externalNodeTag, StringComparison.OrdinalIgnoreCase)) @@ -112,14 +115,36 @@ public static uint GetXmlNodeValueAsUInt(this XmlNode xmlNode, string xPath) public static DateTime? GetXmlNodeValueAsNullableDateTime(this XmlNode xmlNode, string xPath) { string value = GetXmlNodeValue(xmlNode, xPath); - DateTime outValue; - if (DateTime.TryParse(value, out outValue)) + if (value == null) { - return outValue; + return null; + } + + // The value has timezone info, parse as DateTimeOffset and convert to UTC DateTime + // Otherwise, parse as local DateTime + if (_timezoneOffsetRegex.IsMatch(value)) + { + DateTimeOffset dto; + if (DateTimeOffset.TryParse(value, out dto)) + { + return dto.UtcDateTime; + } + else + { + return null; + } } else { - return null; + DateTime outValue; + if (DateTime.TryParse(value, out outValue)) + { + return outValue; + } + else + { + return null; + } } } diff --git a/ISOv4Plugin/Mappers/LoggedDataMappers/Import/SpatialRecordMapper.cs b/ISOv4Plugin/Mappers/LoggedDataMappers/Import/SpatialRecordMapper.cs index 5b2e90e..4d72f66 100644 --- a/ISOv4Plugin/Mappers/LoggedDataMappers/Import/SpatialRecordMapper.cs +++ b/ISOv4Plugin/Mappers/LoggedDataMappers/Import/SpatialRecordMapper.cs @@ -212,30 +212,39 @@ private DateTime ToUtc(DateTime dateTime) if (dateTime.Kind == DateTimeKind.Utc) return dateTime; - DateTime utc; - if (dateTime.Kind == DateTimeKind.Local) + if (_taskDataMapper.TimezoneOffset.HasValue) { - utc = dateTime.ToUniversalTime(); - } - else if (dateTime.Kind == DateTimeKind.Unspecified && _taskDataMapper.GPSToLocalDelta.HasValue) - { - utc = new DateTime(dateTime.AddHours(-_taskDataMapper.GPSToLocalDelta.Value).Ticks, DateTimeKind.Utc); - } - else - { - // Nothing left to try; return original value - utc = dateTime; + // Convert from local time to UTC using the timezone offset. + var localTime = new DateTimeOffset(dateTime.Year, dateTime.Month, dateTime.Day, + dateTime.Hour, dateTime.Minute, dateTime.Second, dateTime.Millisecond, + _taskDataMapper.TimezoneOffset.Value); + DateTime utc = localTime.UtcDateTime; + return utc; } - return utc; + // Return original value + return dateTime; } private DateTime? Offset(DateTime? input) { - if (_effectiveTimeZoneOffset.HasValue && input.HasValue) + if (!input.HasValue) + return null; + + if (input.Value.Kind == DateTimeKind.Utc) + return input; + + if (_effectiveTimeZoneOffset.HasValue) { - return input.Value.AddHours(_effectiveTimeZoneOffset.Value); + var dateTime = input.Value; + var localTime = new DateTimeOffset(dateTime.Year, dateTime.Month, dateTime.Day, + dateTime.Hour, dateTime.Minute, dateTime.Second, dateTime.Millisecond, + TimeSpan.FromHours(_effectiveTimeZoneOffset.Value)); + DateTime utc = localTime.UtcDateTime; + return utc; + //return input.Value.AddHours(_effectiveTimeZoneOffset.Value); } + return input; } } diff --git a/ISOv4Plugin/Mappers/TaskDataMapper.cs b/ISOv4Plugin/Mappers/TaskDataMapper.cs index 1a39d86..53dac69 100644 --- a/ISOv4Plugin/Mappers/TaskDataMapper.cs +++ b/ISOv4Plugin/Mappers/TaskDataMapper.cs @@ -78,6 +78,7 @@ public TaskDataMapper(string dataPath, Properties properties, int? taskDataVersi internal Dictionary DDIs { get; private set; } internal DeviceOperationTypes DeviceOperationTypes { get; private set; } internal double? GPSToLocalDelta { get; set; } + internal TimeSpan? TimezoneOffset { get; set; } CodedCommentListMapper _commentListMapper; public CodedCommentListMapper CommentListMapper diff --git a/ISOv4Plugin/Mappers/TimeLogMapper.cs b/ISOv4Plugin/Mappers/TimeLogMapper.cs index 13a39bb..eadd4b9 100644 --- a/ISOv4Plugin/Mappers/TimeLogMapper.cs +++ b/ISOv4Plugin/Mappers/TimeLogMapper.cs @@ -330,8 +330,12 @@ protected IEnumerable ImportTimeLog(ISOTask loggedTask, ISOTimeLo var firstRecord = isoRecords.FirstOrDefault(r => r.GpsUtcDateTime.HasValue && r.GpsUtcDate != ushort.MaxValue && r.GpsUtcDate != 0); if (firstRecord != null) { - //Local - UTC = Delta. This value will be rough based on the accuracy of the clock settings but will expose the ability to derive the UTC times from the exported local times. - TaskDataMapper.GPSToLocalDelta = (firstRecord.TimeStart - firstRecord.GpsUtcDateTime.Value).TotalHours; + //Local - UTC = Delta. This value will be rough based on the accuracy of the clock settings + // but will expose the ability to derive the UTC times from the exported local times. + TimeSpan offset = firstRecord.TimeStart - firstRecord.GpsUtcDateTime.Value; + // Round offset to nearest minute for use in timezone offset + TaskDataMapper.TimezoneOffset = TimeSpan.FromMinutes(Math.Round(offset.TotalMinutes)); + TaskDataMapper.GPSToLocalDelta = TaskDataMapper.TimezoneOffset.Value.TotalHours; } } } From cd09b479b38bd46c67d053282f99eacf0c978437 Mon Sep 17 00:00:00 2001 From: ericpohl Date: Fri, 5 Dec 2025 12:08:58 -0500 Subject: [PATCH 4/7] Cleanup --- .../Import/SpatialRecordMapper.cs | 50 ++++++------------- ISOv4Plugin/Mappers/TaskDataMapper.cs | 2 +- ISOv4Plugin/Mappers/TimeLogMapper.cs | 1 - 3 files changed, 15 insertions(+), 38 deletions(-) diff --git a/ISOv4Plugin/Mappers/LoggedDataMappers/Import/SpatialRecordMapper.cs b/ISOv4Plugin/Mappers/LoggedDataMappers/Import/SpatialRecordMapper.cs index 4d72f66..a77f896 100644 --- a/ISOv4Plugin/Mappers/LoggedDataMappers/Import/SpatialRecordMapper.cs +++ b/ISOv4Plugin/Mappers/LoggedDataMappers/Import/SpatialRecordMapper.cs @@ -1,14 +1,11 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using AgGateway.ADAPT.ApplicationDataModel.LoggedData; using AgGateway.ADAPT.ApplicationDataModel.Representations; using AgGateway.ADAPT.ISOv4Plugin.ExtensionMethods; using AgGateway.ADAPT.ISOv4Plugin.ObjectModel; using AgGateway.ADAPT.ISOv4Plugin.ISOModels; -using AgGateway.ADAPT.Representation.UnitSystem; -using AgGateway.ADAPT.ISOv4Plugin.Representation; namespace AgGateway.ADAPT.ISOv4Plugin.Mappers { @@ -27,7 +24,7 @@ public class SpatialRecordMapper : ISpatialRecordMapper private readonly IWorkingDataMapper _workingDataMapper; private readonly ISectionMapper _sectionMapper; private readonly TaskDataMapper _taskDataMapper; - private double? _effectiveTimeZoneOffset; + private TimeSpan? _effectiveTimeZoneOffset; public SpatialRecordMapper(IRepresentationValueInterpolator representationValueInterpolator, ISectionMapper sectionMapper, IWorkingDataMapper workingDataMapper, TaskDataMapper taskDataMapper) { @@ -52,7 +49,7 @@ public IEnumerable Map(IEnumerable isoSpatialRows, pan.AllocationStamp.Start.Value.Minute == firstSpatialRow.TimeStart.Minute && pan.AllocationStamp.Start.Value.Second == firstSpatialRow.TimeStart.Second) { - _effectiveTimeZoneOffset = (firstSpatialRow.TimeStart - pan.AllocationStamp.Start.Value).TotalHours; + _effectiveTimeZoneOffset = firstSpatialRow.TimeStart - pan.AllocationStamp.Start.Value; } } } @@ -191,23 +188,26 @@ private void SetNumericMeterValue(ISOSpatialRow isoSpatialRow, NumericWorkingDat /// private bool GovernsTimestamp(ISOProductAllocation p, SpatialRecord spatialRecord) { - DateTime? allocationStart = Offset(p.AllocationStamp.Start); - DateTime? allocationStop = p.AllocationStamp.Stop != null ? Offset(p.AllocationStamp.Stop) : null; - DateTime spatialRecordTimestampUtc = ToUtc(spatialRecord.Timestamp); + DateTime? allocationStartUtc = ToUtc(p.AllocationStamp.Start, _effectiveTimeZoneOffset); + DateTime? allocationStopUtc = p.AllocationStamp.Stop != null ? + ToUtc(p.AllocationStamp.Stop, _effectiveTimeZoneOffset) : null; + DateTime spatialRecordTimestampUtc = ToUtc(spatialRecord.Timestamp, _taskDataMapper.TimezoneOffset); - return - ToUtc(allocationStart) <= spatialRecordTimestampUtc && - (p.AllocationStamp.Stop == null || ToUtc(allocationStop) >= spatialRecordTimestampUtc); + var returnVal = + allocationStartUtc <= spatialRecordTimestampUtc && + (p.AllocationStamp.Stop == null || allocationStopUtc >= spatialRecordTimestampUtc); + + return returnVal; } // Comparing DateTime values with different Kind values leads to inaccurate results. // Convert DateTimes to UTC if possible before comparing them - private DateTime? ToUtc(DateTime? nullableDateTime) + private DateTime? ToUtc(DateTime? nullableDateTime, TimeSpan? timezoneOffset) { - return nullableDateTime.HasValue ? ToUtc(nullableDateTime.Value) : nullableDateTime; + return nullableDateTime.HasValue ? ToUtc(nullableDateTime.Value, timezoneOffset) : nullableDateTime; } - private DateTime ToUtc(DateTime dateTime) + private DateTime ToUtc(DateTime dateTime, TimeSpan? timezoneOffset) { if (dateTime.Kind == DateTimeKind.Utc) return dateTime; @@ -225,27 +225,5 @@ private DateTime ToUtc(DateTime dateTime) // Return original value return dateTime; } - - private DateTime? Offset(DateTime? input) - { - if (!input.HasValue) - return null; - - if (input.Value.Kind == DateTimeKind.Utc) - return input; - - if (_effectiveTimeZoneOffset.HasValue) - { - var dateTime = input.Value; - var localTime = new DateTimeOffset(dateTime.Year, dateTime.Month, dateTime.Day, - dateTime.Hour, dateTime.Minute, dateTime.Second, dateTime.Millisecond, - TimeSpan.FromHours(_effectiveTimeZoneOffset.Value)); - DateTime utc = localTime.UtcDateTime; - return utc; - //return input.Value.AddHours(_effectiveTimeZoneOffset.Value); - } - - return input; - } } } diff --git a/ISOv4Plugin/Mappers/TaskDataMapper.cs b/ISOv4Plugin/Mappers/TaskDataMapper.cs index 53dac69..99517d7 100644 --- a/ISOv4Plugin/Mappers/TaskDataMapper.cs +++ b/ISOv4Plugin/Mappers/TaskDataMapper.cs @@ -77,7 +77,7 @@ public TaskDataMapper(string dataPath, Properties properties, int? taskDataVersi internal RepresentationMapper RepresentationMapper { get; private set; } internal Dictionary DDIs { get; private set; } internal DeviceOperationTypes DeviceOperationTypes { get; private set; } - internal double? GPSToLocalDelta { get; set; } + internal double? GPSToLocalDelta => TimezoneOffset?.TotalHours; internal TimeSpan? TimezoneOffset { get; set; } CodedCommentListMapper _commentListMapper; diff --git a/ISOv4Plugin/Mappers/TimeLogMapper.cs b/ISOv4Plugin/Mappers/TimeLogMapper.cs index eadd4b9..6bbe43d 100644 --- a/ISOv4Plugin/Mappers/TimeLogMapper.cs +++ b/ISOv4Plugin/Mappers/TimeLogMapper.cs @@ -335,7 +335,6 @@ protected IEnumerable ImportTimeLog(ISOTask loggedTask, ISOTimeLo TimeSpan offset = firstRecord.TimeStart - firstRecord.GpsUtcDateTime.Value; // Round offset to nearest minute for use in timezone offset TaskDataMapper.TimezoneOffset = TimeSpan.FromMinutes(Math.Round(offset.TotalMinutes)); - TaskDataMapper.GPSToLocalDelta = TaskDataMapper.TimezoneOffset.Value.TotalHours; } } } From 919cca119495f364f2cb3912c08e98eb17b85a0b Mon Sep 17 00:00:00 2001 From: ericpohl Date: Fri, 13 Mar 2026 10:30:03 -0400 Subject: [PATCH 5/7] Performance refactorings --- .../{ExtensionMethods.cs => Extensions.cs} | 17 +++-- ISOv4Plugin/ISOModels/ISO11783_TaskData.cs | 61 +++++++++++------ ISOv4Plugin/ISOModels/ISOTimeLog.cs | 14 +++- .../ObjectModel/DeviceElementHierarchy.cs | 46 ++++++------- ISOv4Plugin/Plugin.cs | 66 +++++++++++-------- .../Representation/RepresentationMapper.cs | 28 ++++++-- 6 files changed, 149 insertions(+), 83 deletions(-) rename ISOv4Plugin/ExtensionMethods/{ExtensionMethods.cs => Extensions.cs} (95%) 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..5de4661 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; From c527618f6708feb77a302277995fc84d972529d7 Mon Sep 17 00:00:00 2001 From: ericpohl Date: Fri, 13 Mar 2026 10:32:03 -0400 Subject: [PATCH 6/7] .gitignore JetBrains --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) 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/ From 03ba2a651741d5698bb49fc6a8743df953e6a089 Mon Sep 17 00:00:00 2001 From: ericpohl Date: Fri, 13 Mar 2026 10:36:31 -0400 Subject: [PATCH 7/7] Renaming --- .../ObjectModel/DeviceElementHierarchy.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ISOv4Plugin/ObjectModel/DeviceElementHierarchy.cs b/ISOv4Plugin/ObjectModel/DeviceElementHierarchy.cs index 5de4661..ecc566e 100644 --- a/ISOv4Plugin/ObjectModel/DeviceElementHierarchy.cs +++ b/ISOv4Plugin/ObjectModel/DeviceElementHierarchy.cs @@ -247,24 +247,24 @@ 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 + //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; - if (propertiesByDdi.TryGetValue("0046", out widthProperty)) //Max width + if (propertiesByDDI.TryGetValue("0046", out widthProperty)) //Max width { Width = widthProperty.Value; WidthDDI = "0046"; } - else if (propertiesByDdi.TryGetValue("0044", out widthProperty)) //Default working width + else if (propertiesByDDI.TryGetValue("0044", out widthProperty)) //Default working width { Width = widthProperty.Value; WidthDDI = "0044"; } - else if (propertiesByDdi.TryGetValue("0043", out widthProperty)) //Actual working width + else if (propertiesByDDI.TryGetValue("0043", out widthProperty)) //Actual working width { Width = widthProperty.Value; WidthDDI = "0043"; @@ -278,7 +278,7 @@ public DeviceHierarchyElement(ISODeviceElement deviceElement, //Offsets ISODeviceProperty xOffsetProperty; - if (propertiesByDdi.TryGetValue("0086", out xOffsetProperty)) + if (propertiesByDDI.TryGetValue("0086", out xOffsetProperty)) { XOffset = xOffsetProperty.Value; } @@ -288,7 +288,7 @@ public DeviceHierarchyElement(ISODeviceElement deviceElement, } ISODeviceProperty yOffsetProperty; - if (propertiesByDdi.TryGetValue("0087", out yOffsetProperty)) + if (propertiesByDDI.TryGetValue("0087", out yOffsetProperty)) { YOffset = yOffsetProperty.Value; } @@ -298,7 +298,7 @@ public DeviceHierarchyElement(ISODeviceElement deviceElement, } ISODeviceProperty zOffsetProperty; - if (propertiesByDdi.TryGetValue("0088", out zOffsetProperty)) + if (propertiesByDDI.TryGetValue("0088", out zOffsetProperty)) { ZOffset = zOffsetProperty.Value; }