diff --git a/ISOv4Plugin/ExtensionMethods/ExtensionMethods.cs b/ISOv4Plugin/ExtensionMethods/ExtensionMethods.cs index 66a5615..0949024 100644 --- a/ISOv4Plugin/ExtensionMethods/ExtensionMethods.cs +++ b/ISOv4Plugin/ExtensionMethods/ExtensionMethods.cs @@ -95,6 +95,13 @@ public static bool ReverseEquals(this string s1, string s2) return true; } + /// + /// Matches NumericRepresentation by code, accounting for null representation code + /// + public static bool ContainsCode(this ApplicationDataModel.Representations.Representation representation, string code) + { + return representation?.Code != null && representation.Code.Contains(code); + } /// /// Looks up unit, converts and loads representation 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/PartfieldMapper.cs b/ISOv4Plugin/Mappers/PartfieldMapper.cs index 8887a59..ffa211a 100644 --- a/ISOv4Plugin/Mappers/PartfieldMapper.cs +++ b/ISOv4Plugin/Mappers/PartfieldMapper.cs @@ -90,11 +90,16 @@ public ISOPartfield ExportField(Field adaptField) //Boundary PolygonMapper polygonMapper = new PolygonMapper(TaskDataMapper); - FieldBoundary boundary = DataModel.Catalog.FieldBoundaries.SingleOrDefault(b => b.FieldId == adaptField.Id.ReferenceId); + + var boundaries = DataModel.Catalog.FieldBoundaries.Where(b => b.FieldId == adaptField.Id.ReferenceId).ToList(); + var boundary = boundaries.FirstOrDefault(b=> b.Id.ReferenceId == adaptField.ActiveBoundaryId) ?? boundaries.FirstOrDefault(); if (boundary != null) { - IEnumerable isoPolygons = polygonMapper.ExportMultipolygon(boundary.SpatialData, ISOEnumerations.ISOPolygonType.PartfieldBoundary); - isoField.Polygons.AddRange(isoPolygons); + if (boundary.SpatialData != null) + { + IEnumerable isoPolygons = polygonMapper.ExportMultipolygon(boundary.SpatialData, ISOEnumerations.ISOPolygonType.PartfieldBoundary); + isoField.Polygons.AddRange(isoPolygons); + } } //Guidance 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 f0eccf2..6bbe43d 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); 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 @@ -329,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)); } } } @@ -409,8 +413,7 @@ protected IEnumerable ImportTimeLog(ISOTask loggedTask, ISOTimeLo operationData.GetDeviceElementUses = x => operationData.DeviceElementUses.Where(s => s.Depth == x).ToList(); operationData.PrescriptionId = prescriptionID; operationData.OperationType = GetOperationTypeFromProductCategory(productIDs) ?? - GetOperationTypeFromWorkingDatas(workingDatas) ?? - GetOperationTypeFromLoggingDevices(time); + OverrideOperationTypeFromWorkingDatas(GetOperationTypeFromLoggingDevices(time), workingDatas); operationData.ProductIds = productIDs; if (!useDeferredExecution) { @@ -428,22 +431,25 @@ protected IEnumerable ImportTimeLog(ISOTask loggedTask, ISOTimeLo return null; } - private OperationTypeEnum? GetOperationTypeFromWorkingDatas(List workingDatas) + private OperationTypeEnum OverrideOperationTypeFromWorkingDatas(OperationTypeEnum deviceOperationType, List workingDatas) { //Harvest/ForageHarvest omitted intentionally to be determined from machine type vs. working data - if (workingDatas.Any(w => w.Representation.Code.Contains("Seed"))) + if (workingDatas.Any(w => w.Representation.ContainsCode("Seed"))) { return OperationTypeEnum.SowingAndPlanting; } - else if (workingDatas.Any(w => w.Representation.Code.Contains("Tillage"))) + else if (workingDatas.Any(w => w.Representation.ContainsCode("Tillage"))) { return OperationTypeEnum.Tillage; } - if (workingDatas.Any(w => w.Representation.Code.Contains("AppRate"))) + if (workingDatas.Any(w => w.Representation.ContainsCode("AppRate"))) { - return OperationTypeEnum.Unknown; //We can't differentiate CropProtection from Fertilizing, but prefer unkonwn to letting implement type set to SowingAndPlanting + if (deviceOperationType != OperationTypeEnum.Fertilizing && deviceOperationType != OperationTypeEnum.CropProtection) + { + return OperationTypeEnum.Unknown; //We can't differentiate CropProtection from Fertilizing, but prefer unknown to letting implement type set to SowingAndPlanting + } } - return null; + return deviceOperationType; } private List> SplitElementsByProductProperties(Dictionary> productAllocations, HashSet loggedDeviceElementIds, ISODevice dvc) @@ -682,9 +688,6 @@ private void AddProductAllocationsForDeviceElement(Dictionary t.ClientNAMEMachineType >= 2 && t.ClientNAMEMachineType <= 11); + DeviceOperationType deviceType = representedTypes.FirstOrDefault(t => t.ClientNAMEMachineType >= 2 && t.ClientNAMEMachineType <= 11 && + t.OperationType != OperationTypeEnum.Unknown); + if (deviceType != null) { //2-11 represent known types of operations @@ -752,8 +757,6 @@ internal static Dictionary ReadImplementGeometryValues(IEnumerable ReadImplementGeometryValues(string filePath, ISOTime templateTime, IEnumerable desiredDLVIndices, int version, IList errors) { Dictionary output = new Dictionary();