Skip to content
7 changes: 7 additions & 0 deletions ISOv4Plugin/ExtensionMethods/ExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ public static bool ReverseEquals(this string s1, string s2)
return true;
}

/// <summary>
/// Matches NumericRepresentation by code, accounting for null representation code
/// </summary>
public static bool ContainsCode(this ApplicationDataModel.Representations.Representation representation, string code)
{
return representation?.Code != null && representation.Code.Contains(code);
}

/// <summary>
/// Looks up unit, converts and loads representation
Expand Down
33 changes: 29 additions & 4 deletions ISOv4Plugin/ExtensionMethods/XmlExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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;
}
}
}

Expand Down
59 changes: 23 additions & 36 deletions ISOv4Plugin/Mappers/LoggedDataMappers/Import/SpatialRecordMapper.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -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)
{
Expand All @@ -52,7 +49,7 @@ public IEnumerable<SpatialRecord> Map(IEnumerable<ISOSpatialRow> 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;
}
}
}
Expand Down Expand Up @@ -191,52 +188,42 @@ private void SetNumericMeterValue(ISOSpatialRow isoSpatialRow, NumericWorkingDat
/// </summary>
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;
}
}
}
11 changes: 8 additions & 3 deletions ISOv4Plugin/Mappers/PartfieldMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ISOPolygon> isoPolygons = polygonMapper.ExportMultipolygon(boundary.SpatialData, ISOEnumerations.ISOPolygonType.PartfieldBoundary);
isoField.Polygons.AddRange(isoPolygons);
if (boundary.SpatialData != null)
{
IEnumerable<ISOPolygon> isoPolygons = polygonMapper.ExportMultipolygon(boundary.SpatialData, ISOEnumerations.ISOPolygonType.PartfieldBoundary);
isoField.Polygons.AddRange(isoPolygons);
}
}

//Guidance
Expand Down
3 changes: 2 additions & 1 deletion ISOv4Plugin/Mappers/TaskDataMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ public TaskDataMapper(string dataPath, Properties properties, int? taskDataVersi
internal RepresentationMapper RepresentationMapper { get; private set; }
internal Dictionary<int, DdiDefinition> 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
Expand Down
39 changes: 21 additions & 18 deletions ISOv4Plugin/Mappers/TimeLogMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, int> _dataLogValueOrdersByWorkingDataID;
public IEnumerable<ISOTimeLog> ExportTimeLogs(IEnumerable<OperationData> operationDatas, string dataPath)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -193,7 +194,7 @@ private void WriteSpatialRecord(SpatialRecord spatialRecord, List<WorkingData> 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
Expand Down Expand Up @@ -329,8 +330,11 @@ protected IEnumerable<OperationData> 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));
}
}
}
Expand Down Expand Up @@ -409,8 +413,7 @@ protected IEnumerable<OperationData> 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)
{
Expand All @@ -428,22 +431,25 @@ protected IEnumerable<OperationData> ImportTimeLog(ISOTask loggedTask, ISOTimeLo
return null;
}

private OperationTypeEnum? GetOperationTypeFromWorkingDatas(List<WorkingData> workingDatas)
private OperationTypeEnum OverrideOperationTypeFromWorkingDatas(OperationTypeEnum deviceOperationType, List<WorkingData> 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<List<string>> SplitElementsByProductProperties(Dictionary<string, List<ISOProductAllocation>> productAllocations, HashSet<string> loggedDeviceElementIds, ISODevice dvc)
Expand Down Expand Up @@ -682,9 +688,6 @@ private void AddProductAllocationsForDeviceElement(Dictionary<string, Dictionary

switch (productCategories.FirstOrDefault())
{
case CategoryEnum.Variety:
return OperationTypeEnum.SowingAndPlanting;

case CategoryEnum.Fertilizer:
case CategoryEnum.NitrogenStabilizer:
case CategoryEnum.Manure:
Expand Down Expand Up @@ -722,7 +725,9 @@ private OperationTypeEnum GetOperationTypeFromLoggingDevices(ISOTime time)
}
}

DeviceOperationType deviceType = representedTypes.FirstOrDefault(t => 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
Expand Down Expand Up @@ -752,8 +757,6 @@ internal static Dictionary<byte, int> ReadImplementGeometryValues(IEnumerable<by

protected class BinaryReader
{
private static readonly DateTime _firstDayOf1980 = new DateTime(1980, 01, 01);

public static Dictionary<byte, int> ReadImplementGeometryValues(string filePath, ISOTime templateTime, IEnumerable<byte> desiredDLVIndices, int version, IList<IError> errors)
{
Dictionary<byte, int> output = new Dictionary<byte, int>();
Expand Down
Loading