From 2ce6afb909f34af7d78c20cfeb9f2d8311e91336 Mon Sep 17 00:00:00 2001 From: Roy Ben Shabat Date: Fri, 3 Oct 2025 01:55:11 +0300 Subject: Changed RealTimeGraphX to .NET 4.6.1 --- .../RealTimeGraphX/GraphController.cs | 662 +++++++++++++++++++++ 1 file changed, 662 insertions(+) create mode 100644 Software/Visual_Studio/SideChains/RealTimeGraphXNet/RealTimeGraphX/GraphController.cs (limited to 'Software/Visual_Studio/SideChains/RealTimeGraphXNet/RealTimeGraphX/GraphController.cs') diff --git a/Software/Visual_Studio/SideChains/RealTimeGraphXNet/RealTimeGraphX/GraphController.cs b/Software/Visual_Studio/SideChains/RealTimeGraphXNet/RealTimeGraphX/GraphController.cs new file mode 100644 index 000000000..1b452df57 --- /dev/null +++ b/Software/Visual_Studio/SideChains/RealTimeGraphXNet/RealTimeGraphX/GraphController.cs @@ -0,0 +1,662 @@ +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 + } +} -- cgit v1.3.1