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 } }