using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Text; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Drawing; using RealTimeGraphX.EventArguments; using RealTimeGraphX.Renderers; using System.Diagnostics; namespace RealTimeGraphX { /// /// Represents an base class. /// /// The type of the data series. /// The type of the x data point. /// The type of the y data point. /// /// public abstract class GraphController : GraphObject, IGraphController where TXDataPoint : GraphDataPoint where TYDataPoint : GraphDataPoint 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; #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 IsClearSeries { get; set; } public bool IsUpdateSeries { get; set; } } #endregion #region Events /// /// Occurs when the current effective minimum/maximum has changed. /// public event EventHandler EffectiveRangeChanged; /// /// Occurs when the current virtual (effective minimum/maximum after transformation) minimum/maximum has changed. /// public event EventHandler VirtualRangeChanged; #endregion #region Properties /// /// Gets or sets the controller refresh rate. /// Higher rate requires more CPU time. /// public TimeSpan RefreshRate { get; set; } /// /// Gets or sets a value indicating whether to pause rendering. /// public bool IsPaused { get; set; } /// /// Gets or sets a value indicating whether to disable the rendering of data. /// public bool DisableRendering { get; set; } /// /// Gets the data series collection. /// public ObservableCollection DataSeriesCollection { get; } private IGraphRenderer _renderer; /// /// Gets or sets the graph renderer. /// public IGraphRenderer Renderer { get { return _renderer; } set { _renderer = value; RaisePropertyChangedAuto(); } } private IGraphSurface _surface; /// /// Gets or sets the rendering surface. /// public IGraphSurface Surface { get { return _surface; } set { var previous = _surface; _surface = value; RequestVirtualRangeChange(); OnSurfaceChanged(previous, _surface); } } private GraphRange _range; /// /// Gets or sets the graph range (data point boundaries). /// public GraphRange Range { get { return _range; } set { _range = value; RaisePropertyChangedAuto(); } } /// /// Gets the current effective x-axis minimum. /// public GraphDataPoint EffectiveMinimumX { get; private set; } /// /// Gets the current effective x-axis maximum. /// public GraphDataPoint EffectiveMaximumX { get; private set; } /// /// Gets the current effective y-axis minimum. /// public GraphDataPoint EffectiveMinimumY { get; private set; } /// /// Gets the current effective y-axis maximum. /// public GraphDataPoint EffectiveMaximumY { get; private set; } /// /// Gets the current virtual (effective minimum/maximum after transformation) x-axis minimum. /// public GraphDataPoint VirtualMinimumX { get; private set; } /// /// Gets the current virtual (effective minimum/maximum after transformation) x-axis maximum. /// public GraphDataPoint VirtualMaximumX { get; private set; } /// /// Gets the current virtual (effective minimum/maximum after transformation) y-axis minimum. /// public GraphDataPoint VirtualMinimumY { get; private set; } /// /// Gets the current virtual (effective minimum/maximum after transformation) y-axis maximum. /// public GraphDataPoint VirtualMaximumY { get; private set; } #endregion #region Commands /// /// Gets the clear command. /// public GraphCommand ClearCommand { get; private set; } #endregion #region Constructors /// /// Initializes a new instance of the class. /// public GraphController() { Renderer = new ScrollingLineRenderer(); DataSeriesCollection = new ObservableCollection(); Range = new GraphRange(); _last_render_time = DateTime.Now; _to_render = new Dictionary(); _pending_series_collection = new GraphDataQueue>(); RefreshRate = TimeSpan.FromMilliseconds(50); ClearCommand = new GraphCommand(Clear); _render_thread = new Thread(RenderThreadMethod); _render_thread.IsBackground = true; _render_thread.Priority = ThreadPriority.Highest; _render_thread.Start(); } #endregion #region Render Thread /// /// The rendering thread method. /// private void RenderThreadMethod() { while (true) { if (!IsPaused && !DisableRendering) { try { List> pending_lists = new List>(); var pending_list_first = _pending_series_collection.BlockDequeue(); pending_lists.Add(pending_list_first); while (_pending_series_collection.Count > 0) { var pending_list = _pending_series_collection.BlockDequeue(); pending_lists.Add(pending_list); } foreach (var pending_list in pending_lists) { foreach (var pending_series in pending_list) { if (pending_series.IsClearSeries) { _pending_series_collection = new GraphDataQueue>(); _to_render.Clear(); } else if (!pending_series.IsUpdateSeries) { if (_to_render.ContainsKey(pending_series.Series)) { var s = _to_render[pending_series.Series]; s.XX.AddRange(pending_series.XX); s.YY.AddRange(pending_series.YY); } else { _to_render[pending_series.Series] = pending_series; } } } } if (_to_render.Count > 0) { GraphDataPoint min_x = _range.MaximumX - _range.MaximumX; GraphDataPoint max_x = _range.MaximumX; GraphDataPoint min_y = _range.MinimumY; GraphDataPoint max_y = _range.MaximumY; if (_to_render.Count > 0 && _to_render.First().Value.XX.Count > 0) { min_x = _to_render.First().Value.XX.First(); max_x = _to_render.First().Value.XX.Last(); } else { continue; } if (_range.AutoY) { min_y = _to_render.Select(x => x.Value).SelectMany(x => x.YY).Min(); max_y = _to_render.Select(x => x.Value).SelectMany(x => x.YY).Max(); } if (min_y == max_y) { min_y = _range.MinimumY; max_y = _range.MaximumY; } EffectiveMinimumX = min_x; EffectiveMaximumX = max_x; EffectiveMinimumY = min_y; EffectiveMaximumY = max_y; VirtualMinimumX = EffectiveMinimumX; VirtualMaximumX = EffectiveMaximumX; VirtualMinimumY = EffectiveMinimumY; VirtualMaximumY = EffectiveMaximumY; _last_render_time = DateTime.Now; if (Surface != null) { var surface_size = Surface.GetSize(); var zoom_rect = Surface.GetZoomRect(); Surface.BeginDraw(); if (zoom_rect.Width > 0 && zoom_rect.Height > 0) { var zoom_rect_top_percentage = zoom_rect.Top / surface_size.Height; var zoom_rect_bottom_percentage = zoom_rect.Bottom / surface_size.Height; var zoom_rect_left_percentage = zoom_rect.Left / surface_size.Width; var zoom_rect_right_percentage = zoom_rect.Right / surface_size.Width; VirtualMinimumY = EffectiveMaximumY - GraphDataPointHelper.ComputeAbsolutePosition(EffectiveMinimumY, EffectiveMaximumY, zoom_rect_bottom_percentage); VirtualMaximumY = EffectiveMaximumY - GraphDataPointHelper.ComputeAbsolutePosition(EffectiveMinimumY, EffectiveMaximumY, zoom_rect_top_percentage); VirtualMinimumX = GraphDataPointHelper.ComputeAbsolutePosition(EffectiveMinimumX, EffectiveMaximumX, zoom_rect_left_percentage); VirtualMaximumX = GraphDataPointHelper.ComputeAbsolutePosition(EffectiveMinimumX, EffectiveMaximumX, zoom_rect_right_percentage); GraphTransform transform = new GraphTransform(); var scale_x = (float)(surface_size.Width / zoom_rect.Width); var scale_y = (float)(surface_size.Height / zoom_rect.Height); var translate_x = (float)-zoom_rect.Left * scale_x; var translate_y = (float)-zoom_rect.Top * scale_y; transform = new GraphTransform(); transform.TranslateX = translate_x; transform.TranslateY = translate_y; transform.ScaleX = scale_x; transform.ScaleY = scale_y; Surface.SetTransform(transform); } List>> to_draw = new List>>(); var to_render = _to_render.Select(x => x.Value).ToList(); foreach (var item in to_render) { if (item.YY.Count > 0) { item.Series.CurrentValue = item.YY.Last().GetValue(); } var points = Renderer.Render(Surface, item.Series, _range, item.XX, item.YY, min_x, max_x, min_y, max_y); to_draw.Add(new Tuple>(item.Series, points)); } for (int i = 0; i < to_draw.Count; i++) { if (to_draw[i].Item2.Count() > 2) { if (to_draw[i].Item1.IsVisible) { Renderer.Draw(Surface, to_draw[i].Item1, to_draw[i].Item2, i, to_draw.Count); } } } Surface.EndDraw(); } OnEffectiveRangeChanged(EffectiveMinimumX, EffectiveMaximumX, EffectiveMinimumY, EffectiveMaximumY); OnVirtualRangeChanged(VirtualMinimumX, VirtualMaximumX, VirtualMinimumY, VirtualMaximumY); } } catch (Exception ex) { Debug.WriteLine($"Error in RealTimeGraphX:\n{ex.ToString()}"); } } else { Thread.Sleep(RefreshRate); } } } #endregion #region Protected Methods /// /// Called when the surface has changed. /// /// The previous. /// The surface. protected virtual void OnSurfaceChanged(IGraphSurface previous, IGraphSurface surface) { if (previous != null) { previous.SurfaceSizeChanged += Surface_SurfaceSizeChanged; previous.ZoomRectChanged += Surface_ZoomRectChanged; } if (surface != null) { surface.SurfaceSizeChanged += Surface_SurfaceSizeChanged; surface.ZoomRectChanged += Surface_ZoomRectChanged; } } /// /// Raises the event. /// /// The minimum x. /// The maximum x. /// The minimum y. /// The maximum y. protected virtual void OnEffectiveRangeChanged(GraphDataPoint minimumX, GraphDataPoint maximumX, GraphDataPoint minimumY, GraphDataPoint maximumY) { EffectiveRangeChanged?.Invoke(this, new RangeChangedEventArgs(minimumX, maximumX, minimumY, maximumY)); } /// /// Raises the event. /// /// The minimum x. /// The maximum x. /// The minimum y. /// The maximum y. protected virtual void OnVirtualRangeChanged(GraphDataPoint minimumX, GraphDataPoint maximumX, GraphDataPoint minimumY, GraphDataPoint maximumY) { var range = new RangeChangedEventArgs(minimumX, maximumX, minimumY, maximumY); VirtualRangeChanged?.Invoke(this, range); } /// /// Converts the specified relative x position to graph absolute position. /// /// The relative x position. /// protected virtual float ConvertXValueToRendererValue(double x) { return (float)(x * Surface.GetSize().Width / 100); } /// /// Converts the specified relative y position to graph absolute position. /// /// The relative y position. /// protected virtual float ConvertYValueToRendererValue(double y) { return (float)(Surface.GetSize().Height - (y * Surface.GetSize().Height / 100)); } #endregion #region Surface Event Handlers /// /// Handles the ZoomRectChanged event of the Surface control. /// /// The source of the event. /// The instance containing the event data. private void Surface_ZoomRectChanged(object sender, EventArgs e) { if (!_pending_series_collection.ToList().SelectMany(x => x).ToList().Exists(x => x.IsUpdateSeries)) { List updateSeries = new List(); foreach (var pending_Series in _to_render) { updateSeries.Add(new PendingSeries() { IsUpdateSeries = true, Series = pending_Series.Value.Series, XX = new List(), YY = new List(), }); } _pending_series_collection.BlockEnqueue(updateSeries); } } /// /// Handles the SurfaceSizeChanged event of the Surface control. /// /// The source of the event. /// The instance containing the event data. private void Surface_SurfaceSizeChanged(object sender, EventArgs e) { if (!_pending_series_collection.ToList().SelectMany(x => x).ToList().Exists(x => x.IsUpdateSeries)) { List updateSeries = new List(); foreach (var pending_Series in _to_render) { updateSeries.Add(new PendingSeries() { IsUpdateSeries = true, Series = pending_Series.Value.Series, XX = new List(), YY = new List(), }); } _pending_series_collection.BlockEnqueue(updateSeries); } } #endregion #region Public Methods /// /// Submits the specified x and y data points. /// If the controller has more than one data series the data points will be duplicated. /// /// X data point. /// Y data point. public void PushData(TXDataPoint x, TYDataPoint y) { if (DataSeriesCollection.Count == 0) return; List> xxxx = new List>(); List> yyyy = new List>(); foreach (var series in DataSeriesCollection.ToList()) { xxxx.Add(new List() { x }); yyyy.Add(new List() { y }); } PushData(xxxx, yyyy); } /// /// Submits the specified collections of x and y data points. /// If the controller has more than one data series the data points will be distributed evenly. /// /// X data point collection. /// Y data point collection. public void PushData(IEnumerable xx, IEnumerable yy) { if (DataSeriesCollection.Count == 0) return; var xList = xx.ToList(); var yList = yy.ToList(); List> xxxx = new List>(); List> yyyy = new List>(); foreach (var series in DataSeriesCollection.ToList()) { xxxx.Add(new List()); yyyy.Add(new List()); } int counter = 0; for (int i = 0; i < xList.Count; i++) { xxxx[counter].Add(xList[i]); yyyy[counter].Add(yList[i]); counter++; if (counter >= xxxx.Count) { counter = 0; } } PushData(xxxx, yyyy); } /// /// Submits a matrix of x and y data points. Meaning each data series should process a single collection of x/y data points. /// /// X matrix. /// Y matrix. public void PushData(IEnumerable> xxxx, IEnumerable> yyyy) { if (DataSeriesCollection.Count == 0) return; IEnumerable> xxxxI = xxxx.Select(x => x.ToList()).ToList(); IEnumerable> yyyyI = yyyy.Select(x => x.ToList()).ToList(); List> xxxxList = xxxxI.Select(x => x.ToList()).ToList(); List> yyyyList = yyyyI.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 = DataSeriesCollection.ToList(); var pending_list = new List(); for (int i = 0; i < list.Count; i++) { pending_list.Add(new PendingSeries() { Series = list[i], XX = xxxxList[i].ToList(), YY = yyyyList[i].ToList(), }); } _pending_series_collection.BlockEnqueue(pending_list); } /// /// Clears all data points from this controller. /// public void Clear() { _pending_series_collection.BlockEnqueue(new List() { new PendingSeries() { IsClearSeries = true }, }); } /// /// Requests the controller to invoke a virtual range change event. /// public void RequestVirtualRangeChange() { OnVirtualRangeChanged(Range.MaximumX, Range.MaximumX, Range.MinimumY, Range.MaximumY); } #endregion } }