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();