diff options
Diffstat (limited to 'Software/Visual_Studio/SideChains/RealTimeGraphX/GraphRendererBase.cs')
| -rw-r--r-- | Software/Visual_Studio/SideChains/RealTimeGraphX/GraphRendererBase.cs | 573 |
1 files changed, 573 insertions, 0 deletions
diff --git a/Software/Visual_Studio/SideChains/RealTimeGraphX/GraphRendererBase.cs b/Software/Visual_Studio/SideChains/RealTimeGraphX/GraphRendererBase.cs new file mode 100644 index 000000000..fb4824837 --- /dev/null +++ b/Software/Visual_Studio/SideChains/RealTimeGraphX/GraphRendererBase.cs @@ -0,0 +1,573 @@ +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 +{ + /// <summary> + /// Represents an <see cref="IGraphRenderer{XDataPoint, YDataPoint}"/> base class. + /// </summary> + /// <typeparam name="TDataSeries">The type of the graph data series.</typeparam> + /// <typeparam name="XDataPoint">The type of the x-axis data point.</typeparam> + /// <typeparam name="YDataPoint">The type of the y-axis data point.</typeparam> + /// <seealso cref="RealTimeGraphX.GraphInputOutputComponentBase{RealTimeGraphX.IGraphController{XDataPoint, YDataPoint}, RealTimeGraphX.IGraphPainter}" /> + /// <seealso cref="RealTimeGraphX.IGraphRenderer{XDataPoint, YDataPoint}" /> + /// <seealso cref="RealTimeGraphX.IGraphRenderer" /> + public abstract class GraphRendererBase<TDataSeries, XDataPoint, YDataPoint> : + GraphInputOutputComponentBase<IGraphController<TDataSeries, XDataPoint, YDataPoint>, IGraphPainter<TDataSeries>>, + IGraphRenderer<TDataSeries, XDataPoint, YDataPoint>, + IGraphRenderer<TDataSeries> + where XDataPoint : GraphDataPointBase + where YDataPoint : GraphDataPointBase + where TDataSeries : IGraphDataSeries + { + private GraphDataQueue<PendingSeries> _pending_series_collection; + private Dictionary<TDataSeries, PendingSeries> _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<XDataPoint> XX { get; set; } + public List<YDataPoint> YY { get; set; } + + public int NewItemsCount + { + get { return XX.Count - RenderedItems; } + } + + public int RenderedItems { get; set; } + public bool IsInvalidationSeries { get; set; } + } + + #endregion + + #region Events + + /// <summary> + /// Occurs when the current effective x-axis minimum/maximum has changed. + /// </summary> + public event EventHandler<RangeChangedEventArgs> EffectiveRangeXChanged; + + /// <summary> + /// Occurs when the current effective y-axis minimum/maximum has changed. + /// </summary> + public event EventHandler<RangeChangedEventArgs> EffectiveRangeYChanged; + + /// <summary> + /// Occurs when the connected input controller has changed. + /// </summary> + event EventHandler<InputChangedEventArgs<IGraphController<TDataSeries>>> IGraphInputComponent<IGraphController<TDataSeries>>.InputChanged + { + add + { + InputChanged += new EventHandler<InputChangedEventArgs<IGraphController<TDataSeries, XDataPoint, YDataPoint>>>((sender, e) => { value.Invoke(sender, new InputChangedEventArgs<IGraphController<TDataSeries>>(e.Input)); }); + } + + remove + { + InputChanged -= new EventHandler<InputChangedEventArgs<IGraphController<TDataSeries, XDataPoint, YDataPoint>>>((sender, e) => { value.Invoke(sender, new InputChangedEventArgs<IGraphController<TDataSeries>>(e.Input)); }); + } + } + + #endregion + + #region Properties + + private TimeSpan _refreshRate; + /// <summary> + /// Gets or sets the renderer refresh rate. + /// Higher rate requires more CPU time. + /// </summary> + public TimeSpan RefreshRate + { + get + { + return _refreshRate; + } + set + { + _refreshRate = value; + RaisePropertyChangedAuto(); + } + } + + private GraphDataPointBase _effectiveMinimumX; + /// <summary> + /// Gets the current effective x-axis minimum. + /// </summary> + public GraphDataPointBase EffectiveMinimumX + { + get + { + return _effectiveMinimumX; + } + private set + { + _effectiveMinimumX = value; + RaisePropertyChangedAuto(); + } + } + + private GraphDataPointBase _effectiveMaximumX; + /// <summary> + /// Gets the current effective x-axis maximum. + /// </summary> + public GraphDataPointBase EffectiveMaximumX + { + get + { + return _effectiveMaximumX; + } + private set + { + _effectiveMaximumX = value; + RaisePropertyChangedAuto(); + } + } + + private GraphDataPointBase _effectiveMinimumY; + /// <summary> + /// Gets the current effective y-axis minimum. + /// </summary> + public GraphDataPointBase EffectiveMinimumY + { + get + { + return _effectiveMinimumY; + } + private set + { + _effectiveMinimumY = value; + RaisePropertyChangedAuto(); + } + } + + private GraphDataPointBase _effectiveMaximumY; + /// <summary> + /// Gets the current effective y-axis maximum. + /// </summary> + public GraphDataPointBase EffectiveMaximumY + { + get + { + return _effectiveMaximumY; + } + private set + { + _effectiveMaximumY = value; + RaisePropertyChangedAuto(); + } + } + + /// <summary> + /// Gets the current data point x position. + /// </summary> + public double CurrentXPosition { get; protected set; } + + private bool _isPaused; + /// <summary> + /// Gets or sets a value indicating whether to pause the graph movement. + /// </summary> + public bool IsPaused + { + get { return _isPaused; } + set { _isPaused = value; RaisePropertyChangedAuto(); } + } + + #endregion + + #region Constructors + + /// <summary> + /// Initializes a new instance of the <see cref="GraphRendererBase{XDataPoint, YDataPoint}"/> class. + /// </summary> + public GraphRendererBase() + { + EffectiveMinimumX = GraphDataPointHelper.Init<XDataPoint>(); + EffectiveMinimumY = GraphDataPointHelper.Init<YDataPoint>(); + EffectiveMaximumX = GraphDataPointHelper.Init<XDataPoint>(); + EffectiveMaximumY = GraphDataPointHelper.Init<YDataPoint>(); + _last_render_time = DateTime.Now; + _to_render = new Dictionary<TDataSeries, PendingSeries>(); + _pending_series_collection = new GraphDataQueue<PendingSeries>(); + RefreshRate = TimeSpan.FromMilliseconds(50); + + _pid_controller = new PidController(10, 10, -10); + } + + #endregion + + #region Render Thread + + /// <summary> + /// The rendering thread method. + /// </summary> + 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 + + /// <summary> + /// 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. + /// </summary> + /// <param name="xxxx">The x-axis series collection.</param> + /// <param name="yyyy">The y-axis series collection.</param> + /// <exception cref="ArgumentOutOfRangeException">When pushing data to a multi series renderer, each series must contain the same amount of data.</exception> + public void Render(IEnumerable<TDataSeries> seriesCollection, IEnumerable<IEnumerable<XDataPoint>> xxxx, IEnumerable<IEnumerable<YDataPoint>> yyyy) + { + List<List<XDataPoint>> xxxxList = xxxx.Select(x => x.ToList()).ToList(); + List<List<YDataPoint>> 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(), + }); + } + } + + /// <summary> + /// Invalidates the graph. + /// </summary> + public virtual void Invalidate() + { + _pending_series_collection.BlockEnqueue(new PendingSeries() { IsInvalidationSeries = true }); + } + + /// <summary> + /// Connects this renderer to an <see cref="IGraphPainter"/>. + /// </summary> + /// <param name="painter">The graph painter.</param> + /// <param name="fromOutput">Specifies whether this call was made from an <see cref="IGraphPainter"/> component.</param> + public override void ConnectOutput(IGraphPainter<TDataSeries> 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); + } + + /// <summary> + /// Disconnects this renderer from the connected <see cref="IGraphPainter"/>. + /// </summary> + /// <param name="fromOutput">Specifies whether this call was made from an <see cref="IGraphPainter"/> component.</param> + public override void DisconnectOutput(bool fromOutput = false) + { + if (Output != null) + { + if (!fromOutput) + { + Output.DisconnectInput(true); + } + + Output = null; + } + } + + /// <summary> + /// Connects this renderer to the specified <see cref="IGraphController{XDataPoint, YDataPoint}"/>. + /// </summary> + /// <param name="controller">The controller.</param> + /// <param name="fromInput">Specifies whether this call was made from an <see cref="IGraphController{XDataPoint, YDataPoint}"/> component.</param> + public override void ConnectInput(IGraphController<TDataSeries, XDataPoint, YDataPoint> 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); + } + + /// <summary> + /// Disconnects this renderer from the current connected <see cref="IGraphController{XDataPoint, YDataPoint}"/>. + /// </summary> + /// <param name="fromInput">Specifies whether this call was made from an <see cref="IGraphController{XDataPoint, YDataPoint}"/> component.</param> + public override void DisconnectInput(bool fromInput = false) + { + if (Input != null) + { + if (!fromInput) + { + (Input as IGraphOutputComponent<IGraphRenderer<TDataSeries, XDataPoint, YDataPoint>>).DisconnectOutput(true); + } + + Input.RangeChanged -= Input_RangeChanged; + + Input = null; + } + } + + /// <summary> + /// Gets the connected input controller. + /// </summary> + IGraphController<TDataSeries> IGraphInputComponent<IGraphController<TDataSeries>>.Input + { + get + { + return Input; + } + } + + /// <summary> + /// Connected this renderer to the specified graph controller. + /// </summary> + /// <param name="controller">The controller.</param> + /// <param name="fromInput">Specifies whether this call was made from an <see cref="IGraphController"/> component.</param> + void IGraphInputComponent<IGraphController<TDataSeries>>.ConnectInput(IGraphController<TDataSeries> controller, bool fromInput) + { + ConnectInput(controller as IGraphController<TDataSeries, XDataPoint, YDataPoint>, fromInput); + } + + /// <summary> + /// Clears the graph data. + /// </summary> + public void Clear() + { + while (_pending_series_collection.Count > 0) + { + _pending_series_collection.BlockDequeue(); + } + + _to_render.Clear(); + } + + #endregion + + #region Protected Methods + + /// <summary> + /// Returns an array of absolute graph data points to render. + /// This method is called per data series. + /// </summary> + /// <param name="seriesCollection">A collection of all data series that is currently in the rendering pass.</param> + /// <param name="series">The current data series to render.</param> + /// <param name="toRender">Pending data series object to render.</param> + /// <returns></returns> + protected abstract List<GraphPoint> OnRender(IEnumerable<PendingSeries> seriesCollection, TDataSeries series, PendingSeries toRender); + + /// <summary> + /// Converts the specified relative x position to graph absolute position. + /// </summary> + /// <param name="x">The relative x position.</param> + /// <returns></returns> + protected virtual float ConvertXValueToRendererValue(double x) + { + return (float)(x * Output.SurfaceWidth / 100); + } + + /// <summary> + /// Converts the specified relative y position to graph absolute position. + /// </summary> + /// <param name="y">The relative y position.</param> + /// <returns></returns> + protected virtual float ConvertYValueToRendererValue(double y) + { + return (float)(Output.SurfaceHeight - (y * Output.SurfaceHeight / 100)); + } + + /// <summary> + /// Raises the <see cref="EffectiveRangeXChanged"/> event. + /// </summary> + /// <param name="minimumX">The minimum x.</param> + /// <param name="maximumX">The maximum x.</param> + protected virtual void OnEffectiveRangeXChanged(GraphDataPointBase minimumX, GraphDataPointBase maximumX) + { + EffectiveMinimumX = minimumX; + EffectiveMaximumX = maximumX; + + EffectiveRangeXChanged?.Invoke(this, new RangeChangedEventArgs() + { + Maximum = minimumX, + Minimum = maximumX, + }); + } + + /// <summary> + /// Raises the <see cref="EffectiveRangeYChanged"/> event. + /// </summary> + /// <param name="minimumY">The minimum y.</param> + /// <param name="maximumY">The maximum y.</param> + 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 + + /// <summary> + /// Renders the and draws a complete set of data series and their data points. + /// </summary> + /// <param name="toRender">To render.</param> + private void RenderAndDraw(List<KeyValuePair<TDataSeries, PendingSeries>> 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 + + /// <summary> + /// Handles the controller range changed event. + /// </summary> + /// <param name="sender">The sender.</param> + /// <param name="e">The event arguments.</param> + private void Input_RangeChanged(object sender, GraphRange<XDataPoint, YDataPoint> e) + { + OnEffectiveRangeYChanged(Input.Range.MinimumY, Input.Range.MaximumY); + OnEffectiveRangeXChanged(EffectiveMinimumX, Input.Range.MaximumX); + } + + #endregion + } +} |
