Multiprocotol Terminalprogram (BAT)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MultiTerm/MultiTerm.Core/ViewModel/MultiFormatDataViewModel.cs

350 lines
14 KiB

using Common;
using Common.Helpers;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MultiTerm.Core.Types;
using MultiTerm.Protocols.Model;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Text;
namespace MultiTerm.Core.ViewModel;
public partial class MultiFormatDataViewModel : ObservableObject
{
private readonly IContext uiContext;
private int dataCharacterCount = 0;
private List<byte>? listOfPreviousCharacters = null;
#region ICommunicationDataViewModel Implementation
[ObservableProperty]
private ObservableCollection<ByteDataViewModel> data = new();
[ObservableProperty]
private string dataAsString = string.Empty;
[ObservableProperty]
private ObservableCollection<ByteDataViewModel>? selected = new();
[ObservableProperty]
NewlineSeparatorType newlineSeparator = NewlineSeparatorType.None;
[ObservableProperty]
private string selectedDataFirstAbsoluteTime = string.Empty;
[ObservableProperty]
private string selectedDataTimediff = string.Empty;
#endregion
/// <summary>
/// Newline Sequence string inserted on every newline in the <see cref="DataAsString"/>.
/// </summary>
public static string NewlineSequence = Environment.NewLine;
public MultiFormatDataViewModel(IContext context)
{
this.uiContext = context;
}
public void HandleNewData(IEnumerable<ExtendedByte> newRawData)
{
// update collection and string, invoke ui thread if necessary
ContextHelpers.InvokeIfNecessary(this.uiContext, (Action)delegate
{
this.DataAsString = InsertNewNewCharactersIntoCollection(this.Data, this.DataAsString,
ref this.dataCharacterCount, ref this.listOfPreviousCharacters,
this.NewlineSeparator, newRawData);
});
}
/// <summary>
/// After changing the newline separator, the data needs to be reordered.
/// </summary>
partial void OnNewlineSeparatorChanged(NewlineSeparatorType value)
{
// update collection, invoke ui thread if necessary
ContextHelpers.InvokeIfNecessary(this.uiContext, (Action)delegate
{
var reorderedCollection = ReorderCollection(this.Data, value);
this.Data = reorderedCollection.Item1;
this.DataAsString = reorderedCollection.Item2;
});
}
[RelayCommand]
public void Clear()
{
// update collection and string, invoke ui thread if necessary
ContextHelpers.InvokeIfNecessary(this.uiContext, (Action)delegate
{
this.DataAsString = string.Empty;
this.Data.Clear();
});
}
partial void OnSelectedChanging(ObservableCollection<ByteDataViewModel>? value)
{
// remove event handler when there was previously a collection of selected
if(this.Selected != null)
{
this.Selected.CollectionChanged -= this.Selected_CollectionChanged;
}
}
partial void OnSelectedChanged(ObservableCollection<ByteDataViewModel>? value)
{
// add event handler
if (this.Selected != null)
{
this.Selected.CollectionChanged += Selected_CollectionChanged;
}
// manually update
this.UpdateTimeProperties();
}
private void Selected_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
this.UpdateTimeProperties();
}
private void UpdateTimeProperties()
{
if (this.Selected == null || this.Selected.Count == 0)
{
this.SelectedDataFirstAbsoluteTime = string.Empty;
this.SelectedDataTimediff = string.Empty;
return;
}
if (this.Selected.Count == 1)
{
this.SelectedDataFirstAbsoluteTime = $"Time: {this.Selected.First().Time:HH:mm:ss.ff}";
this.SelectedDataTimediff = string.Empty;
}
if (this.Selected.Count > 1)
{
TimeSpan timediff;
TimeOnly lastSelected = this.Selected.Last().Time, firstSelected = this.Selected.First().Time;
Debug.WriteLine($"First time selected: {firstSelected:HH.mm:ss.ff} Ticks: {firstSelected.Ticks}");
Debug.WriteLine($"Last time selected: {lastSelected:HH.mm:ss.ff} Ticks: {lastSelected.Ticks}");
// calculate timedifference according to selection direction
// (preventive. selection should arrive sorted correctly)
if (lastSelected > firstSelected)
{
timediff = lastSelected - firstSelected;
}
else
{
timediff = firstSelected - lastSelected;
}
Debug.WriteLine($"Calculated timediff Ticks: {timediff.Ticks}");
this.SelectedDataFirstAbsoluteTime = $"Time (first selected): {firstSelected:HH:mm:ss.ff}";
this.SelectedDataTimediff = $"Difference (first to last): {timediff.TotalMilliseconds:n} ms";
}
}
#region Collection Manipulation
/// <summary>
/// Function that reorders a given collection with item type <see cref="ByteDataViewModel"/> using the given <paramref name="newlineSeparatorType"/>.
/// Reordered collection is returned, but LineIdentifier is also overwritten in the <paramref name="currentCollection"/> parameter.
/// </summary>
/// <param name="currentCollection">items in this collection will be reordered</param>
/// <param name="newlineSeparatorType">separator between lines</param>
/// <returns>reordered collection and string</returns>
private static Tuple<ObservableCollection<ByteDataViewModel>, string> ReorderCollection(ObservableCollection<ByteDataViewModel> currentCollection, NewlineSeparatorType newlineSeparatorType)
{
ObservableCollection<ByteDataViewModel> newCollection = new();
StringBuilder stringBuilder = new();
// local vars
int lineCounter = 0;
List<byte>? previousBytes = null;
// iterate through items
foreach (ByteDataViewModel item in currentCollection)
{
item.LineIdentifier = lineCounter;
newCollection.Add(item);
stringBuilder.Append(item.DisplayStringChar);
switch (ShouldIntroduceNewlineAfterThisByte(item.Byte, previousBytes, newlineSeparatorType))
{
case ShouldIntroduceNewlineAfterThisByteResult.NoNewline:
// a decision could be made, previousBytes can be cleared
if (previousBytes != null) { previousBytes = null; }
// nothing to do
break;
case ShouldIntroduceNewlineAfterThisByteResult.IntroduceNewline:
// a decision could be made, previousBytes can be cleared
if (previousBytes != null) { previousBytes = null; }
// increase line count
lineCounter++;
// append line in string
stringBuilder.Append(NewlineSequence);
break;
case ShouldIntroduceNewlineAfterThisByteResult.RequiresMoreCharacters:
// first time that more bytes are required => create new list
previousBytes ??= new List<byte>();
// add current byte to list
previousBytes.Add(item.Byte);
break;
default:
throw new Exception($"'{nameof(ReorderCollection)}()' failed because of error when checking if a newline should be introduced.");
}
}
return Tuple.Create(newCollection, stringBuilder.ToString());
}
/// <summary>
/// Function that handles a collection of new bytes that should end up in the collection <paramref name="dataCollection"/>.
/// In case a new line is required, according to the given <paramref name="newlineSeparatorType"/>, it is automatically introduced.
/// Following parameters need to be referenced and stored outside: <paramref name="collectionLineCounter"/> and <paramref name="previousBytes"/>.
/// </summary>
/// <param name="dataCollection">collection to add the bytes to</param>
/// <param name="dataCollectionAsString">collection as string</param>
/// <param name="collectionLineCounter">current line count</param>
/// <param name="previousBytes">list of previous bytes, newest at the last position of the list, null if nothing is stored</param>
/// <param name="newlineSeparatorType">separator between seperate lines</param>
/// <param name="newBytes">bytes to add to the <paramref name="dataCollection"/></param>
/// <returns>string representation of data</returns>
/// <exception cref="Exception">in case of any error</exception>
private static string InsertNewNewCharactersIntoCollection(
ObservableCollection<ByteDataViewModel> dataCollection,
string dataCollectionAsString,
ref int collectionLineCounter,
ref List<byte>? previousBytes,
NewlineSeparatorType newlineSeparatorType,
IEnumerable<ExtendedByte> newBytes)
{
StringBuilder stringBuilder = new();
stringBuilder.Append(dataCollectionAsString);
// go through every byte
foreach (ExtendedByte newByte in newBytes)
{
// add to collection and string with the current counter, invoking UI context if necssary
var currentLineCounter = collectionLineCounter;
dataCollection.Add(new ByteDataViewModel(newByte, currentLineCounter));
stringBuilder.Append(newByte.ToCharacterString());
switch (ShouldIntroduceNewlineAfterThisByte(newByte.Byte, previousBytes, newlineSeparatorType))
{
case ShouldIntroduceNewlineAfterThisByteResult.NoNewline:
// a decision could be made, previousBytes can be cleared
if (previousBytes != null) { previousBytes = null; }
// nothing to do
break;
case ShouldIntroduceNewlineAfterThisByteResult.IntroduceNewline:
// a decision could be made, previousBytes can be cleared
if (previousBytes != null) { previousBytes = null; }
// increase line count
collectionLineCounter++;
// append line in string
stringBuilder.Append(NewlineSequence);
break;
case ShouldIntroduceNewlineAfterThisByteResult.RequiresMoreCharacters:
// first time that more bytes are required => create new list
previousBytes ??= new List<byte>();
// add current byte to list
previousBytes.Add(newByte.Byte);
break;
default:
throw new Exception($"'{nameof(InsertNewNewCharactersIntoCollection)}()' failed because of error when checking if a newline should be introduced.");
}
}
return stringBuilder.ToString();
}
/// <summary>
/// Result type for <see cref="ShouldIntroduceNewlineAfterThisByte"/>
/// </summary>
public enum ShouldIntroduceNewlineAfterThisByteResult
{
/// <summary>
/// No newline is required.
/// </summary>
NoNewline,
/// <summary>
/// A newline shall be introduced after this byte.
/// </summary>
IntroduceNewline,
/// <summary>
/// Following characters are required to finalize result wether a newline shall be introduced or not.
/// </summary>
RequiresMoreCharacters
}
/// <summary>
/// Function to check wether a newline shall be introduced after the given <paramref name="dataByte"/>.
/// Since some newline sequences will require multiple bytes in correct order, a more complex handling is required, which is possible using this function.
/// </summary>
/// <param name="dataByte">the current dataByte in the collection (or a single byte)</param>
/// <param name="previousBytes">list of previous bytes, newest at the last position of the list, null if not required</param>
/// <param name="newlineSeparatorType">separator type</param>
/// <returns>enum result of type <see cref="ShouldIntroduceNewlineAfterThisByteResult"/></returns>
/// <exception cref="NotImplementedException">if the handling for the <paramref name="newlineSeparatorType"/> is not implemented</exception>
private static ShouldIntroduceNewlineAfterThisByteResult ShouldIntroduceNewlineAfterThisByte(byte dataByte, List<byte>? previousBytes, NewlineSeparatorType newlineSeparatorType)
{
var result = ShouldIntroduceNewlineAfterThisByteResult.NoNewline;
switch (newlineSeparatorType)
{
case NewlineSeparatorType.None:
break;
case NewlineSeparatorType.CR:
if (dataByte == (byte)'\r')
{
result = ShouldIntroduceNewlineAfterThisByteResult.IntroduceNewline;
}
break;
case NewlineSeparatorType.LF:
if (dataByte == (byte)'\n')
{
result = ShouldIntroduceNewlineAfterThisByteResult.IntroduceNewline;
}
break;
case NewlineSeparatorType.CR_LF:
if (dataByte == (byte)'\r')
{
result = ShouldIntroduceNewlineAfterThisByteResult.RequiresMoreCharacters;
}
if (dataByte == (byte)'\n')
{
if (previousBytes != null && previousBytes.Last() == (byte)'\r')
{
result = ShouldIntroduceNewlineAfterThisByteResult.IntroduceNewline;
}
}
break;
default:
throw new NotImplementedException($"'{nameof(ShouldIntroduceNewlineAfterThisByte)}()' does not implement handling for {nameof(NewlineSeparatorType)} {newlineSeparatorType}");
}
return result;
}
#endregion
}