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..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,52 +188,42 @@ 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; - DateTime utc; - if (dateTime.Kind == DateTimeKind.Local) - { - utc = dateTime.ToUniversalTime(); - } - else if (dateTime.Kind == DateTimeKind.Unspecified && _taskDataMapper.GPSToLocalDelta.HasValue) - { - utc = new DateTime(dateTime.AddHours(-_taskDataMapper.GPSToLocalDelta.Value).Ticks, DateTimeKind.Utc); - } - else + if (_taskDataMapper.TimezoneOffset.HasValue) { - // 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; - } - - private DateTime? Offset(DateTime? input) - { - if (_effectiveTimeZoneOffset.HasValue && input.HasValue) - { - return input.Value.AddHours(_effectiveTimeZoneOffset.Value); - } - return input; + // Return original value + return dateTime; } } } diff --git a/ISOv4Plugin/Mappers/TaskDataMapper.cs b/ISOv4Plugin/Mappers/TaskDataMapper.cs index 1a39d86..99517d7 100644 --- a/ISOv4Plugin/Mappers/TaskDataMapper.cs +++ b/ISOv4Plugin/Mappers/TaskDataMapper.cs @@ -77,7 +77,8 @@ 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; public CodedCommentListMapper CommentListMapper diff --git a/ISOv4Plugin/Mappers/TimeLogMapper.cs b/ISOv4Plugin/Mappers/TimeLogMapper.cs index 13a39bb..6bbe43d 100644 --- a/ISOv4Plugin/Mappers/TimeLogMapper.cs +++ b/ISOv4Plugin/Mappers/TimeLogMapper.cs @@ -330,8 +330,11 @@ 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)); } } }