using RealTimeGraphX.EventArguments;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace RealTimeGraphX
{
///
/// Represents an base class.
///
/// The type of the graph data series.
/// The type of the x-axis data point.
/// The type of the y-axis data point.
///
///
///
public abstract class GraphRendererBase :
GraphInputOutputComponentBase, IGraphPainter>,
IGraphRenderer,
IGraphRenderer
where XDataPoint : GraphDataPointBase
where YDataPoint : GraphDataPointBase
where TDataSeries : IGraphDataSeries
{
private GraphDataQueue _pending_series_collection;
private Dictionary _to_render;
private DateTime _last_render_time;
private object _render_lock = new object();
private Thread _render_thread;
protected YDataPoint _current_min_y;
protected YDataPoint _current_max_y;
private PidController _pid_controller;
private DateTime _last_pid_sample_time = DateTime.Now;
#region Pending Series Class
protected class PendingSeries
{
public TDataSeries Series { get; set; }
public List XX { get; set; }
public List YY { get; set; }
public int NewItemsCount
{
get { return XX.Count - RenderedItems; }
}
public int RenderedItems { get; set; }
public bool IsInvalidationSeries { get; set; }
}
#endregion
#region Events
///
/// Occurs when the current effective x-axis minimum/maximum has changed.
///
public event EventHandler EffectiveRangeXChanged;
///
/// Occurs when the current effective y-axis minimum/maximum has changed.
///
public event EventHandler EffectiveRangeYChanged;
///
/// Occurs when the connected input controller has changed.
///
event EventHandler>> IGraphInputComponent>.InputChanged
{
add
{
InputChanged += new EventHandler>>((sender, e) => { value.Invoke(sender, new InputChangedEventArgs>(e.Input)); });
}
remove
{
InputChanged -= new EventHandler>>((sender, e) => { value.Invoke(sender, new InputChangedEventArgs>(e.Input)); });
}
}
#endregion
#region Properties
private TimeSpan _refreshRate;
///
/// Gets or sets the renderer refresh rate.
/// Higher rate requires more CPU time.
///
public TimeSpan RefreshRate
{
get
{
return _refreshRate;
}
set
{
_refreshRate = value;
RaisePropertyChangedAuto();
}
}
private GraphDataPointBase _effectiveMinimumX;
///
/// Gets the current effective x-axis minimum.
///
public GraphDataPointBase EffectiveMinimumX
{
get
{
return _effectiveMinimumX;
}
private set
{
_effectiveMinimumX = value;
RaisePropertyChangedAuto();
}
}
private GraphDataPointBase _effectiveMaximumX;
///
/// Gets the current effective x-axis maximum.
///
public GraphDataPointBase EffectiveMaximumX
{
get
{
return _effectiveMaximumX;
}
private set
{
_effectiveMaximumX = value;
RaisePropertyChangedAuto();
}
}
private GraphDataPointBase _effectiveMinimumY;
///
/// Gets the current effective y-axis minimum.
///
public GraphDataPointBase EffectiveMinimumY
{
get
{
return _effectiveMinimumY;
}
private set
{
_effectiveMinimumY = value;
RaisePropertyChangedAuto();
}
}
private GraphDataPointBase _effectiveMaximumY;
///
/// Gets the current effective y-axis maximum.
///
public GraphDataPointBase EffectiveMaximumY
{
get
{
return _effectiveMaximumY;
}
private set
{
_effectiveMaximumY = value;
RaisePropertyChangedAuto();
}
}
///
/// Gets the current data point x position.
///
public double CurrentXPosition { get; protected set; }
private bool _isPaused;
///
/// Gets or sets a value indicating whether to pause the graph movement.
///
public bool IsPaused
{
get { return _isPaused; }
set { _isPaused = value; RaisePropertyChangedAuto(); }
}
#endregion
#region Constructors
///
/// Initializes a new instance of the class.
///
public GraphRendererBase()
{
EffectiveMinimumX = GraphDataPointHelper.Init();
EffectiveMinimumY = GraphDataPointHelper.Init();
EffectiveMaximumX = GraphDataPointHelper.Init();
EffectiveMaximumY = GraphDataPointHelper.Init();
_last_render_time = DateTime.Now;
_to_render = new Dictionary();
_pending_series_collection = new GraphDataQueue();
RefreshRate = TimeSpan.FromMilliseconds(50);
_pid_controller = new PidController(10, 10, -10);
}
#endregion
#region Render Thread
///
/// The rendering thread method.
///
private void RenderThreadMethod()
{
while (Output != null)
{
if (!IsPaused)
{
for (int i = 0; i < Input.DataSeriesCollection.Count; i++)
{
var series = _pending_series_collection.BlockDequeue();
if (!series.IsInvalidationSeries)
{
if (_to_render.ContainsKey(series.Series))
{
var s = _to_render[series.Series];
s.XX.AddRange(series.XX);
s.YY.AddRange(series.YY);
}
else
{
_to_render[series.Series] = series;
}
if (i == Input.DataSeriesCollection.Count - 1)
{
if (DateTime.Now > _last_render_time.AddMilliseconds(RefreshRate.TotalMilliseconds))
{
if (Input.Range.AutoY)
{
_current_min_y = _to_render.Select(x => x.Value).SelectMany(x => x.YY).Min();
_current_max_y = _to_render.Select(x => x.Value).SelectMany(x => x.YY).Max();
}
_last_render_time = DateTime.Now;
RenderAndDraw(_to_render.ToList());
}
}
}
else
{
RenderAndDraw(_to_render.ToList());
}
}
}
else
{
RenderAndDraw(_to_render.ToList());
Thread.Sleep(50);
}
}
}
#endregion
#region Public Methods
///
/// Renders the specified data points collection.
/// • The number of data series of x and y must match.
/// • The number of x and y collections must match.
/// • The number of x and y data points must match.
///
/// The x-axis series collection.
/// The y-axis series collection.
/// When pushing data to a multi series renderer, each series must contain the same amount of data.
public void Render(IEnumerable seriesCollection, IEnumerable> xxxx, IEnumerable> yyyy)
{
List> xxxxList = xxxx.Select(x => x.ToList()).ToList();
List> yyyyList = yyyy.Select(x => x.ToList()).ToList();
int first_count_x = xxxxList[0].Count;
int first_count_y = yyyyList[0].Count;
bool is_data_valid = true;
for (int i = 0; i < xxxxList.Count; i++)
{
if (xxxxList[0].Count != first_count_x)
{
is_data_valid = false;
break;
}
if (xxxxList[0].Count != yyyyList[0].Count)
{
is_data_valid = false;
break;
}
}
if (!is_data_valid)
{
throw new ArgumentOutOfRangeException("When pushing data to a multi series renderer, each series must contain the same amount of data.");
}
var list = seriesCollection.ToList();
for (int i = 0; i < list.Count; i++)
{
_pending_series_collection.BlockEnqueue(new PendingSeries()
{
Series = list[i],
XX = xxxxList[i].ToList(),
YY = yyyyList[i].ToList(),
});
}
}
///
/// Invalidates the graph.
///
public virtual void Invalidate()
{
_pending_series_collection.BlockEnqueue(new PendingSeries() { IsInvalidationSeries = true });
}
///
/// Connects this renderer to an .
///
/// The graph painter.
/// Specifies whether this call was made from an component.
public override void ConnectOutput(IGraphPainter painter, bool fromOutput = false)
{
painter.ThrowIfNull("Cannot connect a null painter.");
DisconnectOutput();
Output = painter;
if (!fromOutput)
{
Output.ConnectInput(this, true);
}
_render_thread = new Thread(RenderThreadMethod);
_render_thread.IsBackground = true;
_render_thread.Name = "IGraphRenderer Render Thread";
_render_thread.Start();
OnEffectiveRangeYChanged(Input.Range.MinimumY, Input.Range.MaximumY);
}
///
/// Disconnects this renderer from the connected .
///
/// Specifies whether this call was made from an component.
public override void DisconnectOutput(bool fromOutput = false)
{
if (Output != null)
{
if (!fromOutput)
{
Output.DisconnectInput(true);
}
Output = null;
}
}
///
/// Connects this renderer to the specified .
///
/// The controller.
/// Specifies whether this call was made from an component.
public override void ConnectInput(IGraphController controller, bool fromInput = false)
{
controller.ThrowIfNull("Cannot connect a null controller.");
DisconnectInput();
if (!fromInput)
{
controller.ConnectOutput(this, true);
}
Input = controller;
Input.RangeChanged += Input_RangeChanged;
OnEffectiveRangeYChanged(Input.Range.MinimumY, Input.Range.MaximumY);
}
///
/// Disconnects this renderer from the current connected .
///
/// Specifies whether this call was made from an component.
public override void DisconnectInput(bool fromInput = false)
{
if (Input != null)
{
if (!fromInput)
{
(Input as IGraphOutputComponent>).DisconnectOutput(true);
}
Input.RangeChanged -= Input_RangeChanged;
Input = null;
}
}
///
/// Gets the connected input controller.
///
IGraphController IGraphInputComponent>.Input
{
get
{
return Input;
}
}
///
/// Connected this renderer to the specified graph controller.
///
/// The controller.
/// Specifies whether this call was made from an component.
void IGraphInputComponent>.ConnectInput(IGraphController controller, bool fromInput)
{
ConnectInput(controller as IGraphController, fromInput);
}
///
/// Clears the graph data.
///
public void Clear()
{
while (_pending_series_collection.Count > 0)
{
_pending_series_collection.BlockDequeue();
}
_to_render.Clear();
}
#endregion
#region Protected Methods
///
/// Returns an array of absolute graph data points to render.
/// This method is called per data series.
///
/// A collection of all data series that is currently in the rendering pass.
/// The current data series to render.
/// Pending data series object to render.
///
protected abstract List OnRender(IEnumerable seriesCollection, TDataSeries series, PendingSeries toRender);
///
/// Converts the specified relative x position to graph absolute position.
///
/// The relative x position.
///
protected virtual float ConvertXValueToRendererValue(double x)
{
return (float)(x * Output.SurfaceWidth / 100);
}
///
/// Converts the specified relative y position to graph absolute position.
///
/// The relative y position.
///
protected virtual float ConvertYValueToRendererValue(double y)
{
return (float)(Output.SurfaceHeight - (y * Output.SurfaceHeight / 100));
}
///
/// Raises the event.
///
/// The minimum x.
/// The maximum x.
protected virtual void OnEffectiveRangeXChanged(GraphDataPointBase minimumX, GraphDataPointBase maximumX)
{
EffectiveMinimumX = minimumX;
EffectiveMaximumX = maximumX;
EffectiveRangeXChanged?.Invoke(this, new RangeChangedEventArgs()
{
Maximum = minimumX,
Minimum = maximumX,
});
}
///
/// Raises the event.
///
/// The minimum y.
/// The maximum y.
protected virtual void OnEffectiveRangeYChanged(GraphDataPointBase minimumY, GraphDataPointBase maximumY)
{
EffectiveMinimumY = minimumY;
EffectiveMaximumY = maximumY;
EffectiveRangeYChanged?.Invoke(this, new RangeChangedEventArgs()
{
Maximum = minimumY,
Minimum = maximumY,
});
}
#endregion
#region Private Methods
///
/// Renders the and draws a complete set of data series and their data points.
///
/// To render.
private void RenderAndDraw(List> toRender)
{
lock (_render_lock)
{
var all_series = toRender.Select(x => x.Value);
foreach (var series in toRender)
{
var points = OnRender(all_series, series.Key, series.Value);
series.Value.RenderedItems = series.Value.XX.Count;
if (points.Count > 1)
{
Output.Draw(all_series.Select(x => x.Series), series.Value.Series, points);
}
}
}
}
#endregion
#region Event Handlers
///
/// Handles the controller range changed event.
///
/// The sender.
/// The event arguments.
private void Input_RangeChanged(object sender, GraphRange e)
{
OnEffectiveRangeYChanged(Input.Range.MinimumY, Input.Range.MaximumY);
OnEffectiveRangeXChanged(EffectiveMinimumX, Input.Range.MaximumX);
}
#endregion
}
}